클로저 내부에서 self를 캡쳐하는 방법 – Swift

항상 [unowned self]를 써야 할까요?

http://stackoverflow.com/questions/24320347/shall-we-always-use-unowned-self-inside-closure-in-swift 의 내용중 일부를 번역하였습니다.

아니오, 분명히 [unowned self]를 쓰지 말아야할 상황이 존재합니다. 특정한 클로져가 호출되었을 때에 그 시점까지 self가 파괴되지 않고 살아있음을 보장할 필요가 있을 때가 있단 말이죠. 예를 한가지 들어봅시다. 비동기 네트워크 요청을 하고, 요청에 대한 응답을 받았을 때 self로부터 어떤 무언가를 호출해야 한다고 가정해봅시다. 비동기 요청이 끝나는 시점 이전에 해당 객체가 해제되어 사라졌다면
프로그램이 뻗고 말겁니다.

그렇다면 언제 unowned selfweak self를 써야하는가? 바로 강한 참조 사이클이 만들어질 때입니다. 이는 두개 이상의 객체가 서로 순환하면서 서로를 순서대로 참조해나갈 때 생길 수 있습니다. 이 경우 사이클 내의 객체들은 어떤 경우에도 항상 참조수 1 이상을 갖기 때문에 파괴되지 않고 리소스를 낭비하게 됩니다. 클로저 내부에서 self를 캡쳐하는 방법 – Swift 더보기

Swift :: @noescape explained

#@noescape

@noescape는 함수 파라미터로 클로져를 선언할 때 해당 클로저내의 모든 정보가 외부로 나갈 수 없음을 명시한다. 표현이 좀 애매한데, @noescape로 선언된 클로져는 다음의 동작만이 가능하다.

  1. 클로저가 인자로 전달된 함수 내에서 호출 가능
  2. 함수 내에서 다른 함수(이 때 이 다른 함수도 클로져를 @noescape로 선언한 경우에만)로 전달하여 호출할 수 있다.
  3. 다른 함수나 클로저에서 역시 @noescape로 선언된 경우에 캡쳐

다음의 경우에는 쓸 수 없게 된다.

  1. 다른 지역변수에 대입이 불가하다. 이는 클로져 내의 캡쳐된 모든 변수에 대해서 추가적인 강한 참조를 더할 수 없다는 뜻이다.
  2. 같은 이치로 클로저를 받은 함수 내부에서 다시 다른 함수로 해당 클로저를 전달하려고 할 때, 그 함수의 파라미터에 @noescape가 선언되지 않은 함수로는 전달이 불가능하다.
  3. 2에 의해서 병렬처리 함수로 전달할 수 없다.

이는 함수를 호출하는, 즉 클로져를 제공하는 입장에서보면 클로져가 캡쳐하는 변수들의 라이프사이클에 대해서 더 이상 고민하지 않아도 된다는 뜻이다.

func doIt(code: () -> ()) {
    code()
}

class Foo {
    var i = 0
    func some() {
        doIt {
            println(self.i)
        }
    }
}

let foo = Foo()
foo.some()

위 코드에서 보면 클래스 Foo의 somedoIt으로 클로저를 전달하는데, 이 때 클로져는 self를 캡쳐하므로 참조 순환이 발생한다. (할 것이다 아마도)

이를 피하기 위해서는 [unowned self]를 추가하는 방법도 있지만,

func doIt(@noescape code: () -> ()) {
    code()
}
class Foo {
    var i = 0
    func some() {
        doIt {
            println(self.i)
        }
    }
}

doIt 함수가 @noescape를 명시하는 것도 방법이 될 수 있다. self 자체가 약한참조로 넘어가는 것이 보장되는 것은 클로져의 실행문맥이 self의 내부라는 의미가 되고 이는 클로져 내에서 굳이굳이 self를 쓰지 않아도 된다는 점이다.

Swift :: @autoclosure explained

#@autoclosure

인자로 클로저를 받는 함수를 하나 생각해보자.

func f(pred: () -> Bool) {
    if pred() {
        println("It's true")
    }
}

이 함수를 호출할 때에는 클로져 자체를 넘겨주게 된다.

f({ 2 > 1})
// 혹은
f{2 > 1}

@autoclosure는 함수의 파라미터를 자동으로 클로져로 감싸면서 호출한 시점의 문맥에서 동작하게 한다. 따라서

f(2 > 1)

로 호출할 수 있다.

이 기능의 의미는 인자로 전달되는 표현식의 평가 시점에 있다. 일반적인 함수 호출 흐름에서는 표현식이 함수의 인자로 들어가게 될 때, 함수는 값을 전달받게 되므로 표현식이 평가된 후에 들어가지만, @autoclosure에 의해서 자동으로 클로저로 캡쳐되기 때문에 이는 함수 내에서 평가된다.

조금 다른 예제

func Caller() {
    var vec = [1,2,3,4,5]
    var index = 0
    Callee(vec.count, vec[index++])
    // index is now 5
}

func Callee(count:Int, @autoclosure item: () -> Int) {
    for var i = 0; i < count ; i++ {
        println("Hello #(item())")
    }
}
/*:
Hello #1
Hello #2
Hello #3
Hello #4
Hello #5
*/

클로져 사용방법(Swift)

클로저 사용 시 인자 전달방법

함수의 인자로 클로저를 전달하는 경우를 생각해보자. 배열의 정렬된 사본을 만드는 sorted(_:,_:)의 경우 대략 다음과 같은 시그니처를 가지고 있다.

func sorted<C: SequenceType>(source: C, isOrderedBefore: (C.Geneartor.Element, C.Generator.Element) -> Bool) -> [C.Generator.Element]

만약 정수 배열을 정렬한다고하면 다음과 같이 사용한다.

let arr = [5, 8, 2, 4, 2, 1, 7]
let sortedArr = sorted(arr, { (x:Int, y:Int) -> Bool  in
    return x < y
})

이 때 함수의 마지막 인자는 trailing closure라고 해서 괄호 밖으로 빼낼 수 있다.

let sortedArr = sorted(arr){ (x:Int, y:Int) -> Bool in return x < y }

이때 클로저가 받는 각 인자는 순서대로 $0, $1, … 로 명명하는 것이 가능하다. 이 표현방식도 많이 알려져있는 형태이다. 클로져 사용방법(Swift) 더보기

unowned self

Swift의 클래스가 다른 인스턴스를 프로퍼티로 갖게 되는 경우, 만약 두 인스턴스가 서로를 프로퍼티로 참조하게 되면 상호간에 강한 참조 순환1이 발생하게 된다.

이를 해결하기 위해서는 변수 선언시에 weak이나 unowned를 명시하여 약한 참조가 발생하도록 한다. unowned self 더보기