Closure Capture, Capture List

2 답변 글타래를 보이고 있습니다
  • 글쓴이
    • nobleidea
      참가자
      • 글작성 : 5
      • 답글작성 : 8

      안녕하세요 고귀한 생각을 가진 개발자 nobleidea입니다.
      오늘은 클로저 캡처 리스트에 대해 공부해보았습니다.

      먼저 클로저 캡처에 대해서 알아볼까요?
      클로저 내부에서 외부에 있는 값에 접근하면 값에 대한 참조를 획득합니다.
      라고 정의해보았습니다.

      var value = 0
      let closure = { print("capture value: \(value)" }
      

      위와 같은 코드를 보시면 capture value를 출력하는 코드를 클로저로 작성하고 상수에 저장했습니다.
      이 때, value값이 클로저 외부에 있는 값이므로 클로저에서 접근 시 값에 대한 참조를 획득합니다.(값을 캡처합니다.)
      캡처에는 두 가지 종류가 있습니다.
      Object-C에서는 복사본을 캡처하는 방식을 사용하고 있고
      Swift에서는 참조를 캡처하는 방식을 사용합니다.

      따라서 위와 같은 코드에서는 swift에서 복사본이 아닌 참조를 캡처합니다.
      참조를 캡처한다라는 말은 곧 클로저 내부에서 캡처한 값을 변경 시 원래 값도 함께 바뀌게 됩니다.

      위 코드에 있는 상수를 통해서 클로저를 호출해보겠습니다.

      closure()
      

      코드를 실행해보면 캡처한 값이 0이라는 결과값이 나옵니다.

      var value = 0 
      let closure = { 
      value += 10
      print("capture value: \(value)" }
      
      print(value)
      

      클로저 내부에서 캡처한 값을 변경시키면 원본값도 변경이되어
      “capture value: 10”
      10
      이라는 콘솔로그창을 보실 수 있습니다.

      그러나 클로저에서 값을 캡처할때는 메모리 관리를 하지 않는다면 참조사이클 문제가 발생합니다.
      클로저가 인스턴스를 캡처하고 인스턴스가 클로저를 강한참조로 저장하고 있다면
      인스턴스가 정상적으로 해제되지 않습니다.

      예를 들어보겠습니다.

      class Car {
       var speed = 100.0
       var distance = 100.0
      
       lazy var time: () -> Double = {
        return self.distance / self.speed
       } 
      
       deinit { 
         print("car deinit")
       }
      }
      
      var car: Car? = Car()
      //car.time()
      car = nil
      

      위 코드를 실행하면 정상적으로 인스턴스가 메모리에서 해제되어
      deinit가 호출됩니다.
      그러나 주석으로 처리된 car.time()을 주석해제를 하고
      코드를 실행한다면 deinit가 호출되지 않습니다.
      그 이유는 time이라는 클로저 내부에서 self라는 값을 캡처하고 있기때문에
      인스턴스의 reference count가 1증가되어 강한참조사이클이 발생합니다.(ARC 선행학습하시면 도움이 됩니다.)

      이 부분은 2가지 방법으로 해결할 수 있습니다.
      weak 키워드와 unowned키워드 입니다.

      class Car {
       var speed = 100.0
       var distance = 100.0
      
       lazy var time: () -> Double = { [weak self] in
        guard let strongSelf = self else { return 0.0 }
        return strongSelf.distance / strongSelf.speed
       } 
      
       deinit { 
         print("car deinit")
       }
      }
      
      var car: Car? = Car()
      car.time()
      car = nil
      

      weak키워드는 약함참조를 의미합니다.
      reference counting을 하지 않기때문에 강한참조사이클을 방지할 수 있습니다.
      다만 weak키워드는 옵셔널형식입니다.
      그래서 캡처대상을 사용할때는 언랩핑(옵셔널바인딩) 또는 옵셔널체이닝으로 접근해야합니다.
      클로저의 실행이 완료되지 않은 시점에 캡처대상이 해제될 수 있다면 약한참조를 사용합니다.
      만약 위 코드에서 car 인스턴스가 해제되고 클로저가 호출되었다면 약함참조를 사용하고 있기때문에
      self는 nil이 전달되어 옵셔널바인딩에 실패하고 코드가 종료됩니다.

      강한참조사이클을 해결할 수 있는 다른방법으로는 unowned 키워드(비소유 참조)가 있습니다.

      class Car {
       var speed = 100.0
       var distance = 100.0
      
       lazy var time: () -> Double = { [unowned self] in
        return self.distance / self.speed
       } 
      
       deinit { 
         print("car deinit")
       }
      }
      
      var car: Car? = Car()
      car.time()
      car = nil
      

      비소유 참조는 옵셔널 형식이 아닙니다.
      비소유 참조로 캡처한 대상은 클로저 실행이 종료되기전에 해제될 수 있습니다.
      만약 그렇게 된다면 해제 된 대상에 접근하게 되어 런타림오류가 발생합니다.
      위험한 접근이기때문에 비소유 참조는 캡처대상의 생명주기가 클로저와 같거나 더 긴경우에 사용합니다.
      즉 캡처대상이 메모리에서 해제되지 않았다고 알고있을 때 비소유 참조를 사용한다고 생각하시면 될 것같습니다.

      틀린부분이나.. 개선사항은 피드백 미리 감사드립니다!
      다음에는 더 치열히… 정리해보도록 하겠습니다.

    • 야곰
      키 마스터
      • 글작성 : 37
      • 답글작성 : 580

      멋진글입니다.

      1. Object-C (x) -> Objective-C (o)
      2. 미소유획득(Unowned Capture) 상수도 옵셔널 타입입니다. 다만, 일반 옵셔널이 아닌 암시적 추출 옵셔널(implicitly unwrapped optional) 타입입니다. 선언시에 타입 뒤에 느낌표 붙여주는 그 타입입니다. 그래서 (nil체크 등이) 필요하다면 저 상수도 옵셔널 바인딩을 해줄 수 있습니다.

       

      혹시 제가 틀린점이 있다면 말씀주시면 감사하겠습니다 🙂

      좋은글은 추천👍

    • nobleidea
      참가자
      • 글작성 : 5
      • 답글작성 : 8

      정신없이 쓰느라 오타가 많네요…
      피드백 감사드립니다 !!😀
      암시적 추출 옵셔널까지 숙지하겠습니다!

2 답변 글타래를 보이고 있습니다
  • 답변은 로그인 후 가능합니다.

logo landscape small

사업자번호 : 743-81-02195
통신판매업 신고번호 : 제 2022-충북청주-1278 호
고객센터 : 카카오톡채널 @yagom