コンテンツにスキップ

クロージャ

キャプチャされたブロックとprocリテラルは、ローカル変数とselfをクロージャします。これは、例でよりよく理解できます。

x = 0
proc = ->{ x += 1; x }
proc.call # => 1
proc.call # => 2
x         # => 2

または、メソッドから返されたprocを使用します。

def counter
  x = 0
  ->{ x += 1; x }
end

proc = counter
proc.call # => 1
proc.call # => 2

上記の例では、xはローカル変数ですが、procリテラルによってキャプチャされました。この場合、コンパイラはxをヒープに割り当て、procのコンテキストデータとして使用して動作させます。これは、通常、ローカル変数はスタックに存在し、メソッドが返された後に消えるためです。

クロージャ変数の型

コンパイラは通常、ローカル変数の型についてある程度賢明です。例えば

def foo(&)
  yield
end

x = 1
foo do
  x = "hello"
end
x # : Int32 | String

コンパイラは、ブロックの後、xがInt32またはStringになる可能性があることを認識しています(メソッドは常にyieldするため、常にStringになることを認識している可能性があります。これは将来改善される可能性があります)。

ブロックの後にxに別のものが代入された場合、コンパイラは型が変更されたことを認識します。

x = 1
foo do
  x = "hello"
end
x # : Int32 | String

x = 'a'
x # : Char

ただし、xがprocによってクロージャされた場合、型は常にそれに割り当てられたすべての型の混合型になります。

def capture(&block)
  block
end

x = 1
capture { x = "hello" }

x = 'a'
x # : Int32 | String | Char

これは、キャプチャされたブロックがクラスまたはインスタンス変数に格納され、命令の間に別のスレッドで呼び出された可能性があるためです。コンパイラはこれについて徹底的な分析を行いません。変数がprocによってキャプチャされた場合、そのprocの呼び出し Zeitpunkt は不明であると想定します。

これは、procが呼び出されたり格納されたりしていないことが明らかであっても、通常のprocリテラルでも発生します。

x = 1
->{ x = "hello" }

x = 'a'
x # : Int32 | String | Char