클로저
포획된 블락과 프록 리터럴은 지역 변수와 self
를 클로저로 저장합니다. 예시가 말보다는 이해하기 쉬울 것입니다.
x = 0
proc = ->{ x += 1; x }
proc.call #=> 1
proc.call #=> 2
x #=> 2
메서드에서 반환한 프록을 사용할 수도 있습니다.
def counter
x = 0
->{ x += 1; x }
end
proc = counter
proc.call #=> 1
proc.call #=> 2
위 예시에서, x
는 지역 변수이지만 프록 리터럴에 의해 포획되어 있습니다. 이 경우 컴파일러는 x
를 힙에 할당하여 프록이 동작하기 위한 맥락 데이터로 사용합니다. 일반적인 지역 변수는 스택에 저장되므로 메서드가 반환할 때 소멸하기 때문입니다.
클로저로 저장된 변수의 타입
컴파일러는 보통 지역 변수의 타입을 알아서 적당히 추론합니다.
def foo
yield
end
x = 1
foo do
x = "안녕"
end
x # : Int32 | String
컴파일러는 블락이 끝난 후 x
는 Int32일 수도, String일 수도 있다는 사실을 알아냅니다. (메서드가 항상 yield를 사용하므로 x
가 항상 String이라는 것을 추론할 수도 있지만, 이는 추후 개선될 사항입니다.)
블락이 지나서 x
에 다른 값이 할당된다면 컴파일러는 이 또한 알아차립니다.
x = 1
foo do
x = "안녕"
end
x # : Int32 | String
x = 'a'
x # : Char
하지만 x
가 프록에 클로저로 저장된다면 그 타입은 할당된 모든 값의 공용체입니다.
def capture(&block)
block
end
x = 1
capture { x = "안녕" }
x = 'a'
x # : Int32 | String | Char
이는 포획된 블락이 클래스나 인스턴스 변수로 저장되어 다른 스레드에서 실행될 가능성이 있기 때문입니다. 컴파일러는 이런 사항을 모두 분석하지는 않기 때문에, 어떤 변수가 프록에 의해 포획된다면 그 프록이 호출될 시점에서는 변수의 타입을 알 수 없다고 가정할 뿐입니다.
이는 평범한 프록에 대해서도 동일합니다. 프록이 절대로 호출되거나 저장되지 않음이 분명하더라도 같습니다.
def capture(&block)
block
end
x = 1
->{ x = "안녕" }
x = 'a'
x # : Int32 | String | Char