【Ruby】スコープの影響範囲ってちゃんと理解してる?

・スコープについて詳しく知りたい

・JavaからRuby学びに来たんだけど、なんか想定外の動きするんだけど。。。

・ローカル変数の束縛から解放された!!

(煽りタイトルっぽいですが、正直私は理解してませんでした、煽ってるのは過去の自分へですww)

Rubyのローカル変数の影響範囲ってどこからどこまでで、どのタイミングで影響範囲変わるか明確答えることはできますか?

少しでも疑問が残るようであれば一読してみてくださいな^^

「スコープ」って何??

まず、スコープとは?

プログラム中で定義された変数や定数、関数などを参照・利用できる有効範囲を表し、多くの言語では変数名などの宣言や定義が記述されている位置によって決定される。
参照:スコープ 【 scope 】

変数の影響範囲などを示します。

変数の種類によっても影響範囲も変わりますが、今回の記事ではローカル変数に絞って記載します。
(グローバル変数とかだとどこからでも参照できる、、とか)

かなり重要「スコープの変更タイミング」

Javaや C#との比較

JavaやC#には、「内部スコープ」、「外部スコープ」の変数を参照する仕組みがあります。
下のコードの14行目を見ると、11行目で定義された変数aが参照できているのが分かります。
(実行結果:test:1)

import java.util.*;

public class Main {
    public static void main(String[] args) throws Exception {
        Test test = new Test();
        test.method();
    }
}

class Test {
    private int a = 1;
    public void method() {
        // 変数aが参照できる
        System.out.println("test:" + Test.this.a);
    }
}

コレと似たようなコードをRubyで書いてみます。
fooメソッドを呼び出し時に、変数aを参照できずにエラーとなってしまいます。

a = 1
p a #> 1

def foo 
  ## エラー
  p a
end

foo() #> 呼び出し時にエラー

Rubyには、Javaなどのように可視性の入れ子構造が存在しないため、変数aを参照できません。
Rubyの場合は、スコープをきちんと区別されています。

スコープなどで紐づけられた値などを束縛と呼んだりしますが、Rubyは新しいスコープに入ると以前の束縛は新し束縛に置き換えられます。

後ほど、スコープ変更するタイミングを詳しく紹介しますが、

defで定義されたfooメソッドに入ったタイミングでスコープが切り替わり、トップレベルで束縛されていた変数aが、新しい束縛で置き換えられたため参照できなくなった。。。という流れになります。
(この時点で「?」な人は一度全部読んでから読み直すと理解できるかと思いますm(_ _)m)

厳しい門番「スコープゲート」くん

Rubyの場合、スコープが切り替わるタイミングが束縛されている変数が変わります
そのため、どのタイミングでスコープが切り替わるのかを把握することはソースを読む上で非常に重要なポイントになります。

では、どのタイミングでスコープが切り替わるのか?

  1. クラス定義(class)
  2. モジュール定義(module)
  3. メソッド(def)

の3つが切り替わるポイントです。
門のような役割からスコープゲートとも呼ばれるそうです。

メソッドとの違い

クラスやモジュールに定義されたコードはすぐに実行されますが、メソッドに定義されたコードはメソッドを呼び出した時に実行される、、という違いがあります。

class MyClass
  p "1"
  def test
    p "2"
  end
end
p "------"
obj = MyClass.new
obj.test

上記コードを実行した場合

“1”

“——“

“2”

このように実行結果が出ます。
“1”の部分が、MyClassクラスをnewする前に中のソースコードを実行しているのが分かると思います。
(上から順番に実行している)

一方メソッドは、メソッドが呼び出された後にならないと、”2″が呼び出されません。

言われると当たり前な感じに聞こえますが、結構気が付きにくい点です!気をつけましょうー。
(Rubyの資格試験でも似たような問題があって頭を悩ませました。。。)

罠かよ!for文内で変数を定義した際の注意点

実はコレが一番書きたかった内容です。
Rubyを書いていて一番、「???」な部分でした。

スコープゲートについて詳しく知ると納得ではありましたが、ハマりポイントでした。。。

for文の場合ですが、

for i in [1,2,3]
  a = i
end
p a #>= 3

このコードが問題なく通ります。

変数aが参照できてしまいます!
スコープが作成されないため起きる現象です。。。

変数aを参照させたくない場合は、eachメソッドを使ってブロックで値を束縛する解決できます。
次はブロックについて説明します。

注意!

for文以外にもif文などもスコープが作成されませんのでご注意を!
以下、色々試した結果です。お時間ある時に一読してみてください。

# if文
if true
  a = 1
end
p a #>= 1

# while文
while a == 2
  b = 22
  a = 2
end

p b #> nil

# case文
case "B"
  when "A"
    p "バツ"
  else
    c = 33
end

p c #> 33

# unless文
unless false
  d = 44
end
p d #>= 1

# エラー構文
begin
  e = 55
rescue
  p "バツ"
ensure
  f = 66
end

p e #>= 55
p f #>= 66

while文だけ、予想と違う動きでした。
ただ、構文をチェックするとdoが省力されている=ブロック扱いのためだったようです。

ブロックが終了したタイミングで束縛も消える、、、のですが。。。なんでnil?
束縛消えたら、エラーになるはず、、、なんだけどここだけ謎ですorz
ごめんなさい。。

 

意外と万能「ブロック」様

ブロックには特筆すべき特徴があります。

それが、「ローカル束縛を包み込んで、一緒に連れて行く

という特徴です。

コードで説明します。

a = 1

def foo 
  p a
end

このように書くと、スコープが切り替わるため変数aは参照できませんでした

けれども、ブロックを使うとこの変数aを参照できるようになります!

a = 1

def foo 
  p yield
end

foo { a } #> 1
# これでもOK
# foo do 
#   a 
# end

yieldなどのブロック構文については割愛しますが、このように書くとブロックがその時点の束縛(変数a)を包み込んでメソッドfooまで運んでくれます。そのため、変数aが参照できるようになり無事1を出力することができました!

ブロックのもう1つの特徴

ブロック中で新しい束縛を定義することもできます。
ただし、ブロックの中限定です。

ブロックが終了した時点で消えてしまいます。

以下例です。
ブロックの中で変数bを定義することが出来ますが、ブロックが終了した時点でbは消失するため
次の出力時にはエラーとなります。

def foo 
  p yield #> 999
end

foo do 
  b = 999
end

p b # エラー

痒いところまで手が届く「フラットスコープ」

スコープゲートを超えて自由に束縛を渡すことが出来るようになります。
その仕組みを「フラットスコープ」と言います。

例えば、

a = 999

class MyClass
  # p a として表示させたい
  p a #> エラー
end

このような場合どうでしょうか?
工夫することで、変数aを出力させることが可能です!

classキーワードをメソッドに置き換えます!

a = 999

MyClass =  Class.new do
  # p a として表示させたい
  p a #> エラー
end

こうすることで、スコープゲートから抜け出すことができます!

ブロックとclassなどのキーワードをメソッドに置き換える方法を知るだけで、痒いところにまで手が届くコードを書くことが出来るようになります!

是非マスターしてくださいー

まとめ

  • スコープが切り替わるタイミングを把握することは重要!
  • ブロックの特徴を理解すると、スコープゲートを回避出来る!

以上です!

個人的には、for文のなかで変数を定義したら変な動きをしたのでそれについて調べた結果を記載するつもりだったのですが、、、結構調べだすと奥が深かったです。

Rubyの重要な部分だと思いますので、何回か読んでコード書いてしっかり理解してください!
では!!

以下、参考書籍たちです。
特に魔術書を参考にしました。for文の動きは合格教法にチョロっとだけ載ってます。

 

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です