[swift]TIL_Closures[2]

Jeff·2024년 11월 13일
1

오늘은 저번에 다뤘던 클로저에 대해서 더 알아보기로 하자.

Capturing Values(캡처값), Reference Types (참조 타입)

클로저는 정의된 둘러싸인 컨텍스트에서 상수와 변수를 캡쳐 할 수 있다. 즉, 클로저는 상수와 변수를 정의한 원래 범위가 더이상 존재하지 않더라도 본문 내에서 해당 상수와 변수의 값을 참조하고 수정할 수 있다.

  • swift에서 값을 캡처할 수 있다라는건 중첩 함수에서 바깥 함수의 어떤 인수도 캡처할 수 있고 바깥 함수 내에 정의된 상수와 변수를 캡처할 수도 있다.
  • 실제로 클로저를 변수에 할당하거나 클로저를 호출하는 순간, 클로저는 자신이 참조하는 외부의 변수를 캡처함(지속적으로 외부 변수를 사용해야 하기 때문)
  • 클로저가 캡처한 변수는 해당 클로저가 소멸될 때까지 메모리에 유지된다. 이를 캡쳐리스트라고 하며, 메모리 누수를 방지하기위해서 weak, unwoned를 캡쳐 리스트로 사용하여 순환 참조를 방지하는 것이 중요하다.

# 예시 코드

# 기본적인 캡쳐 현상

func upCount() -> () -> Int {
   var count = 0
   
   let total = {
       count += 1  // 클로저 밖에 있는 count 변수를 캡처(저장)하여 사용
       return count
   }
   
   return total
}

let incrementer = upCount()
print(incrementer()) // 캡쳐한 count = 0 -> total = 1 (count값도 1로 증가)
print(incrementer()) // 캡쳐한 count = 1 -> total = 2 (count값도 2로 증가)
print(incrementer()) // 캡쳐한 count = 2 -> total = 3 (count값도 3로 증가)

클로저가 참조하는 변수의 값(count)은 클로저가 생성될 당시의 값을 복사해오는 것이 아니라, 변수 자체에 대한 참조를 한다. 그렇기에 count값이 변하더라도 그대로 유지되어 있다.

# 클로저의 상태 유지 및 독립적인 참조

func makeIncrementer(amount: Int) -> () -> Int {
    var total = 0
    return {
        total += amount
        return total
    }
}

let incrementByTen = makeIncrementer(amount: 10)
let incrementBySeven = makeIncrementer(amount: 7)

// incrementByTen은 10씩 증가시키고,total은 incrementByTen의 클로저 내부에 독립적으로 존재
incrementByTen() // returns 10
incrementByTen() // returns 20

// incrementBySeven은 7씩 증가시키고, 그만의 total을 사용
incrementBySeven() // returns 7

// 다시 incrementByTen을 호출하면, 이전 total에서 이어서 증가
incrementByTen() // returns 30

# 클로저의 값 캡처와 참조 캡처 비교

  • 값 캡처

    var number = 42
    
    // 값 타입으로 캡처 (캡처 시점의 값을 복사) -> [] 안에 넣은 값은 클로저 생성 당시의 값을 복사(캡처리스트)
     let closure = { [number] in
        print("Number is: \(number)")  // 캡처된 시점의 42를 사용
     }
    
     number = 100
     closure()  // "Number is: 42" 출력
  • 참조 캡처

    var number = 42
    
    // 참조 타입으로 캡처(외부에 있는 값을 복사)
     let closure = {
        print("Number is: \(number)")  // 클로저 실행 시점의 number 값 사용
     }
    
     number = 100 // 값을 참조하였기에 값이 변동되면 바뀐다.
     closure()  // "Number is: 100" 출력

Escaping Closures (탈출 클로저)

# @escaping 클로저란?

클로저의 실행이 본래 함수를 벗어나서도 실행되도록 하는 키워드
1) 내부 클로저를 외부 변수에 저장
2) 보통 GCD(비동기코드 사용) 비동기 작업에 흔히 사용된다. 함수 내부에서 선언된 클로저는 함수가 종료될 때 자동으로 해제되도록 하는데 @escaping이 붙은 클로저는 함수의 생명 주기를 넘어 함수 밖에서도 계속해서 참조될 수 있도록 만든것이다.

@escaping 클로저에서는 self를 명시적으로 참조해야한다. 왜냐하면 @escaping 클로저는 클래스 인스턴스의 생명주기를 벗어나서 실행되기 때문이다.
@escaping클로저는 클로저가 실행되기까지 클래스 인스턴스가 메모리에 유지되도록 하기위해 강하게 self를 참조를 하게 된다. 이 경우 메모리 누수가 일어날 수 있기에 이를 방지하기 위해 [weak self]를 캡쳐 목록을 사용하는게 좋다.

# 예시 코드

var completionHandlers: [() -> Void] = []

// 클로저를 배열에 추가
func someFunctionWithEscapingClosure(completionHandler: @escaping () -> Void) {
    completionHandlers.append(completionHandler)
}
// 클로저를 바로 실행 
func someFunctionWithNonescapingClosure(closure: () -> Void) {
    closure()
}


class SomeClass {
    var x = 10
    func doSomething() {
        someFunctionWithEscapingClosure { self.x = 100 }	// 나중에 실행
        someFunctionWithNonescapingClosure { x = 200 }		// 바로 실행
    }
}


let instance = SomeClass()
instance.doSomething()
print(instance.x)
// Prints "200"


completionHandlers.first?()
print(instance.x)
// Prints "100"
  • 위의 두개의 클로저 함수를 만들었다. 하나는 @escaping함수 다른 하나는 nonescaping함수 이 두개의 차이는 실행 시점에 있다. @escaping함수는 함수가 반환 되더라도 나중에 실행 할 수 있다. 그렇기에 함수가 끝난 뒤에도 인스턴스를 계속 참조하기 위해서 명확히 클로저가 어떤 인스턴스를 참조하는지 알리기 위해 self키워드를 통해 알려준다.

  • 즉 실행 순서를 보면
    1) doSomething()이 실행된다
    2) @escaping 클로저에서 self.x = 100을 수행하도록 클로저가 저장되지만, 아직 실행되지 않는다.
    3)someFunctionWithNonescapingClosure는 즉시 실행되어 x = 200으로 설정된다.
    4) completionHandlers.first?()를 호출하면, @escaping 클로저가 나중에 실행되므로 x = 100이 설정된다.

# 클래스에서 self를 @escaping 클로저에서 참조

class SomeClass {
    var x = 10

    func doSomething() {
        someFunctionWithEscapingClosure { self.x = 100 }
    }
}
  • @escaping 클로저는 함수 실행 범위를 벗어나 실행될 수 있기 때문에 self를 강하게 참조하여 인스턴스의 메모리 유지가 필요하다.
  • 이 경우 [weak self][unowned self]를 사용해 순환 참조를 방지할 수 있다.

# 구조체에서 @escaping 클로저 사용

struct SomeStruct {
    var x = 10

    mutating func doSomething() {
        someFunctionWithEscapingClosure { x = 100 } // Error 발생
    }
}
  • @escaping 클로저는 함수의 실행 범위를 벗어나므로, 변경 가능한 참조를 통해 구조체 내부 값을 수정할 수 없다.
  • 값 타입인 구조체는 @escaping 클로저에서 self를 가변 참조로 캡처할 수 없으며, 이는 Swift의 값 타입 관리와 일관성을 지키기 위한 제한이된다.
profile
기본에 충실한 개발자가 목표!

1개의 댓글

comment-user-thumbnail
2024년 11월 14일

그럼 @escaping으로 남아있던 함수가 메모리에서 해제되는 시점은 언젠가여?? 만약 계속해서 해당 클로저를 안 사용한다면 어떻게 되나여
또 weak self로 해서 참조카운트를 증가시키지 않았을 때, 본래 객체가 사라지면 @escaping 클로저는 어떻게 self에 대해 처리하나요?

답글 달기