Old_SWIFT(221012)/기본이야기

Closure에 대하여 - 2 (값 캡쳐, 캡쳐 리스트, ARC, 강한순환참조, 약한순환참조)

KataRN 2022. 10. 8. 20:49
반응형

안녕하세요.

 

KataRN입니다.

 

저번 글에 이어서 작성해보도록 하겠습니다.

 

참조 글 - https://babbab2.tistory.com/83?category=828998

너무 좋은 글이라 거의 따라썼습니다...ㅠㅠ

 

1번글

https://katarnios.tistory.com/84

 

Closure에 대하여 - 1 (Trailing Closure, @autoclosure , @escaping)

안녕하세요. KataRN입니다. 오늘은 Closure에 대해 알아보겠습니다. 📍 Closure란? func으로 선언하는 것이 아닌 함수를 변수에 선언하는 형태 공식 문서에는 클로저는 어떤 상수나 변수의 참조를 캡

katarnios.tistory.com

 

 

📍 값 캡쳐

func goKata() {
    var message = "Hello World"
 
    //클로저 범위 시작
    
    var num = 10
    let closure = { print(num) }
 
    //클로저 범위 끝
    
    print(message)
}

 

closure의 대괄호의 모든게 클로져입니다.

그리고 num의 값을 사용했기 때문에 num의 값을 클로저 내부적으로 저장하고있습니다.

이것을 num의 값이 캡쳐 되었다고 표합합니다.

message 변수는 클로저에 영향을 끼치고 있지 않기 때문에 클로저의 범위에 들어가지 않습니다.

 

📍클로저의 값 캡쳐 방법

클로저는 값을 캡쳐할때 Reference Capture 합니다.

Closure는 값의 타입이 Value건 Reference건 모두 Reference Capture를 합니다.

 

//내부 -> 외부
func goKata() {
    var num: Int = 0
    print("num check #1 = \(num)")
    
    let closure = {
        num = 20
        print("num check #3 = \(num)")
    }
    
    closure()
    print("num check #2 = \(num)")
}

//외부 -> 내부
func goKata() {
    var num: Int = 0
    print("num check #1 = \(num)")
    
    let closure = {
        print("num check #3 = \(num)")
    }
    
    num = 20
    print("num check #2 = \(num)")
    closure()
}

그렇기 때문에 클로저 내부에서 변경하면 외부도 변경되고 외부에서 변경해도 내부값이 변경됩니다.

 

📍클로저의 캡쳐 리스트

Value Type으로 Capture를 하고싶다면 Capture List를 이용하면 됩니다.

Value Type의 경우 Value Capture 하고 싶은 변수를 리스트로 명시해주는 것입니다.

 

문법은 아래와 같습니다.

let closure = { [num] in
	print(num)
}

 

많이 보셨을겁니다ㅎㅎ

 

이때 num 값은 선언기준으로 상수로 캡쳐가 됩니다.

func goKata() {
    var num: Int = 0
    print("num check #1 = \(num)")
    
    let closure = { [num] in
        print("num check #3 = \(num)")
    }
    
    num = 20
    print("num check #2 = \(num)")
    closure()
}

 

 #3은 0입니다.

 

그리고 상수로 캡쳐가 되기 때문에 내부에서 num값을 바꿀수 없습니다.(상수니까!)

 

📍클로저와 ARC

class Human {
    var name = ""
    lazy var getName: () -> String = {
        return self.name
    }
    
    init(name: String) {
        self.name = name
    }
 
    deinit {
        print("Human Deinit!")
    }
}

var kata: Human? = .init(name: "kataRN")
print(kata!.getName())

kata = nil

 

위의 코드를 보시면 최종적으로 인스턴스의 RC가 0이 되어 deinit이 호출되어야 합니다.

하지만 deinit함수는 작동하지 않았습니다.

 

이유는 클로저가 강한 순환 참조이기 때문입니다.

print(sodeul!.getName())

 

getName을 호출하는 순간 Heap에 할당되며 이 클로저를 참조하게됩니다.

그리고 getName을 보시면 self를 통해 Human의 인스턴스 프로퍼티에 접근하고 있습니다.

이것은 클로저를 어떤 특정 프로퍼티에 선언해줌과 동시에 클로저에서 self를 접근해서 나타나는 이슈입니다.

서로 참조하고있어서 둘다 메모리에서 해제되지 않는 강한 순환 참조가 발생한 것입니다.

 

강한 순환 참조를 해결하기 위해서는 Capture List weak 혹은 unowned를 이용해서 해결해야합니다.

lazy var getName: () -> String? = { [weak self] in
    return self?.name
}
    
lazy var getName: () -> String = { [unowned self] in
    return self.name
}

 

이렇게 사용하면 정상적으로 deinit이 실행됩니다.

weak의 경우 Optional Binding을 해줘야하지만 unowned의 경우는 Non-Optional 타입이 됩니다

 

아래는 참고~

 

익명함수가 아닌 전역함수, 중첩함수도 Named Closure라고 불립니다.

전역함수는 어떤한 값도 캡처하지 않습니다.

중첩 함수는 Reference Capture를 합니다.

 

아래는 중첩함수입니다.

func outer() {
    var num: Int = 0
    
    func inner() {
        print(num)
    }
}

inner에서 num을 캡처해야 되겠죠?

 

📍 @escaping의 메모리는??

@escaping 키워드가 없는 클로저는 무조건 non-escaping 클로저 입니다.

클로저는 기본적으로 함수가 종료되기 직전 무조건 실행이 되어야 합니다.

이유는 함수 내부에서만 쓰이기 때문에 컴파일러가 메모리 관리를 지저분하게 하지 않아도 되어서 성능이 향상 되기 때문입니다.

 

그래서 non-escaping의 경우 함수가 종료됨과 동시에 클로저도 사용이 끝나지만, escaping의 경우, 함수가 종료되더라도 실제 클로저가 사용되지 않을 때까지 메모리를 추적해야 합니다.

 

이번 공부를 통해서 참조에 대해 더 깊게 알게되어서 기쁘네요.

따로 공부할 생각이었는데 여기서 언급되어서 뭔가 추가점수 받은기분?

맘에 걸리는건 참조 관련해서 글 올리러 가야겠다는 생각이 드는점?...

 

오늘도 긴글 읽어주셔서 감사합니다.

반응형