본문 바로가기

iOS/Swift

Swift 공식문서 7. Closures

https://docs.swift.org/swift-book/documentation/the-swift-programming-language/closures

Documentation

docs.swift.org

 
Swift 공식 문서 보면서 내 맘대로 정리


Overview

  • 클로저는 기능이 자체적으로 포함된 코드 블록이며, 코드에서 전달되어 사용될 수 있다.
  • 다른 프로그래밍 언어의 클로저, 익명 함수, 람다, 블록과 유사
  • 클로저는 정의된 컨텍스트에서 상수나 변수에 대해 참조를 캡처하고 저장할 수 있다.
  • Swift는 캡처와 관련된 모든 메모리를 자동으로 관리

 

  • 클로저가 가질 수 있는 모양
    1. 전역 함수(Global Function) : 이름을 갖고, 값을 캡처하지 않는 클로저
    2. 중첩 함수(Nested Fuction) : 이름을 갖고, 둘러싼 함수(Enclosing Function)의 값을 캡처할 수 있는 클로저
    3. 클로저 표현식(Closure Expression) : 이름이 없고, 주변 컨텍스트에서 값을 캡처할 수 있는 클로저
  • 클로저의 깔끔한 구문을 위한 최적화 기능
    1. 문맥에서 파라미터, 반환 값 타입 추론
    2. 단일 표현식 클로저에서 암시적 반환
    3. 약식 인자 이름
    4. 후행 클로저 몬법

 

Closure Expression Syntax

{ (<#parameters#>) -> <#return type#> in
   <#statements#>
}
  • 파라미터에 in-out 파라미터를 사용해도 된다. (inout 파라미터 특성에 따라 default 값은 가질 수 없음)
  • 가변 파라미터(variadic parameter) 사용 가능
  • tuple을 파라미터 및 반환 타입으로 사용 가능
reversedNames = names.sorted(by: { (s1: String, s2: String) -> Bool in return s1 > s2 } )

 

Inferring Type From Context

  • 매개변수 타입, 반환 타입을 유추 가능할 경우 생략 가능
  • 함수/메서드의 인자로 전달할 때는 항상 추론 가능
reversedNames = names.sorted(by: { s1, s2 in return s1 > s2 } )

 

Implicit Returns from Single-Expression Closures

  • 한 줄일 경우 return생략 가능
reversedNames = names.sorted(by: { s1, s2 in s1 > s2 } )

 

Shorthand Argument Names

  • 인자 이름in을 생략하고 $0, $1, $2으로 접근 가능
reversedNames = names.sorted(by: { $0 > $1 } )

 

Operator Methods

reversedName = names.sorted(by : >)

 

Trailing Closures

  • 함수의 마지막 인자로 클로저 표현식을 사용하고 표현식이 긴 경우 후행 클로저를 사용이 유용할 수 있음
reversedNames = names.sorted() { $0 > $1 }
  • 클로저 표현식이 함수의 유일한 파라미터이면서 후행 클로저로 사용할 경우 ()를 생략 가능
reversedName = names.sorted { $0 > $1 }
  • 하나의 함수에서 여러 개의 클로저를 사용할 경우
    • 첫 번째 후행 클로저argument label 생략
    • 그 이후 클로저들은 지정 가능
func loadPicture(from server: Server, completion: (Picture) -> Void, onFailure: () -> Void) {
    if let picture = download("photo.jpg", from: server) {
        completion(picture)
    } else {
        onFailure()
    }
}

loadPicture(from: someServer) { picture in
    someView.currentPicture = picture
} onFailure: {
    print("Couldn't download the next picture.")
}

 
 

Capturing Values

  • 클로저는 정의된 컨텍스트에서 상수나 변수에 대해 참조를 캡처하고 저장할 수 있다.
  • 상수와 변수를 정의한 원래 스코프가 더 이상 존재하지 않더라도 클로저 내부에서 사용 가능

 

  • Swift에서 가장 단순한 형태의 클로저는 중첩 함수이다.
  • 중첩 함수는 외부 함수의 인자캡처할 수 있으며, 외부 함수 내에서 정의된 상수와 변수캡처할 수 있다.
func makeIncrementer(forIncrement amount: Int) -> () -> Int {
    var runningTotal = 0
    func incrementer() -> Int {
        runningTotal += amount
        return runningTotal
    }
    return incrementer
}

let incrementByTen = makeIncrementer(forIncrement: 10)
print(incrementByTen())  // 10
print(incrementByTen())  // 20
print(incrementByTen())  // 30

 

  • 위의 예시에서는 incrementer()가 외부 함수의 runningTotal과 amount를 캡처하고 있다.
  • makeIncrementer(forIncrement:)의 호출이 끝난 후에도 runningTotal과 amount가 사라지지 않는다.

[Note 1]
 
Swift에서 클로저는 기본적으로 참조를 통해 값을 캡처한다. 하지만, 최적화를 위해 값을 복사(copy of value)하여 캡처할 수도 있다. 클로저가 값을 변경하지 않으며, 값이 클로저 생성 이후에도 변하지 않는 경우, Swift는 복사본을 생성하여 메모리 효율성을 높일 수 있다.
 
[Note 2]
 
클로저를 클래스 인스턴스의 프로퍼티에 할당하고, 그 클로저가 인스턴스 또는 인스턴스의 멤버로 참조하면 강한 순환 참조(strong reference cylce)가 발생한다. Swift는 이러한 강한 순환 참조를 캡처 리스트를 사용해 해결한다.
 

Closures Are Reference Types

  • 함수나 클로저는 참조 타입이다.
let incrementByTen = makeIncrementer(forIncrement: 10)
let incrementBySeven = makeIncrementer(forIncrement: 7)
  • incrementByTen, incrementBySeven이 상수이지만 이 상수가 참조하는 클로저는 여전히 캡처한 runningTotal 변수를 증가시킬 수 있다.
  • 참조 타입의 특성 덕분에, 클로저가 참조하는 변수는 계속해서 변경될 수 있다.
  • 클로저를 두 개의 다른 상수나 변수에 할당하면, 두 상수나 변수 모두 동일한 클로저를 참조한다.
let alsoIncrementByTen = incrementByTen

alsoIncrementByTen()
// returns a value of 50

incrementByTen()
// returns a value of 60

 
 

Escaping Closures

  • Escaping 클로저는 클로저가 함수의 인자로 전달될 때 함수가 반환된 후에 실행되는 클로저
    • 비동기로 실행되거나 completionHandler로 사용되는 클로저에 사용
  • @escaping 키워드
var completionHandlers: [() -> Void] = []
func someFunctionWithEscapingClosure(completionHandler: @escaping () -> Void) {
    completionHandlers.append(completionHandler)
}

 
[클래스 인스턴스에서 클로저]

  • 클래스 인스턴스를 참조하는 escaping 클로저는 self 참조로 인한 강한 순환 참조를 주의해야 한다.
    • 일반적으로 클로저는 본문에서 변수를 사용함으로써 암시적으로 변수를 캡처
    • 하지만 self를 캡처하려면 클로저 내부에서 self를 명시적으로 작성하거나, 캡처리스트에 포함해야 함
    • 강한 참조 순환이 없는지 확인하도록 상기시켜 줌!
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"
  • someFunctionWithEscapingClosure는 self를 명시적으로 참조해야 함
  • someFunctionWithNonescapingClosure는 self를 암시적으로 참조 가능

 

  • 또는 캡처리스트에 담아두고 암시적 참조 가능
class SomeOtherClass {
    var x = 10
    func doSomething() {
        someFunctionWithEscapingClosure { [self] in x = 100 }
        someFunctionWithNonescapingClosure { x = 200 }
    }
}

 
[구조체 / 열거형 인스턴스에서 클로저]

  • 항상 암시적으로 self를 참조할 수 있다.
  • non-escaping 클로저에서 암시적으로 참조하고 값을 변경할 수 있다.
  • escaping 클로저에서 변경 가능한 self를 캡처할 수 없다.

 
구조체와 열거형의 값 타입 특성

  • 구조체와 열거형은 값 타입이다. 참조를 통한 공유가 이뤄지지 않는다.
  • Swift에서는 값 타입의 공유 가능한 가변성(shared mutability)허용하지 않는다.
  • 따라서 escaping 클로저는 값 타입에서 변경 가능한 self를 캡처할 수 없다.
    • 즉 mutating func에서는 구조체 인스턴스의 멤버를 참조할 수 없다.
struct SomeStruct {
    var x = 10
    mutating func doSomething() {
        someFunctionWithNonescapingClosure { x = 200 }  // Ok
        someFunctionWithEscapingClosure { x = 100 }     // Error
    }
}

 

 Autoclosures

  • autoclosure는 함수의 인자로 전달되는 표현식을 자동으로 클로저로 래핑 하는 클로저이다.
  • 파라미터를 받지 않으며, 호출되면 내부에 래핑 된 표현식의 값을 반환
  • 이러한 문법적 편의성 덕분에, 함수의 파라미터에 대한 명시적인 클로저 대신 일반 표현식을 작성하여 중괄호 생략 가능 
func printResult(_ closure: () -> Bool) {
    if closure() {
        print("Expression is true")
    } else {
        print("Expression is false")
    }
}

printResult({ 5 > 3 })  // 명시적으로 클로저를 작성해야 함

func printResult(_ closure: @autoclosure () -> Bool) {
    if closure() {
        print("Expression is true")
    } else {
        print("Expression is false")
    }
}

printResult(5 > 3)  // 자동으로 클로저로 변환됨

 

  • autoclosure를 사용하는 함수를 호출하는 것은 흔하지만, 이를 구현하는 상황은 흔치 않다.
  • 예를 들어 assert(condition:message:file:line) 함수는 conditonmessage 파라미터에 autoclosure를 사용한다.
    • condition 파라미터는 디버그 빌드에서만 평가되고, message 파라미터는 condition이 거짓일 때만 평가된다.
assert(5 > 3, "Five should be greater than three")

 

  • autoclosure는 평가를 지연시킬 수 있다.
    • 클로저 내부의 코드는 클로저가 호출될 때까지 실행되지 않기 때문
  • 이는 Side-effect가 있는 곳이나 계산 비용이 큰 코드에서 코드 사용 시점을 제어할 수 있도록 돕는다.

[클로저가 어떻게 평가를 지연하는가]

  • customerProvider가 실제로 호출될 때까지 배열의 요소가 제거되지 않는다.
  • customerProvider타입이 String이 아닌 () -> String 임에 주목하라 (파라미터가 없고 문자열을 반환하는 함수)
var customersInLine = ["Chris", "Alex", "Ewa", "Barry", "Daniella"]
print(customersInLine.count)
// 5

let customerProvider = { customersInLine.remove(at: 0) }
print(customersInLine.count)
// 5

print("Now serving \(customerProvider())!")
// Now serving Chris!
print(customersInLine.count)
// 4

 
클로저를 함수의 인자로 전달할 때도 평가 지연을 동일하게 얻을 수 있다.

// customersInLine은 ["Alex", "Ewa", "Barry", "Daniella"]
func serve(customer customerProvider: () -> String) {
    print("Now serving \(customerProvider())!")
}
serve(customer: { customersInLine.remove(at: 0) })
// Now serving Alex!

 
autoclosure를 사용하여 함수가 클로저가 아닌 문자열을 인자로 받는 것처럼 만들 수 있다.

// customersInLine은 ["Ewa", "Barry", "Daniella"]
func serve(customer customerProvider: @autoclosure () -> String) {
    print("Now serving \(customerProvider())!")
}
serve(customer: customersInLine.remove(at: 0))
// Now serving Ewa!

 
autoclosure를 escaping 클로저로 만들 수도 있다.

// customersInLine 배열은 ["Barry", "Daniella"]입니다.
var customerProviders: [() -> String] = []

func collectCustomerProviders(_ customerProvider: @autoclosure @escaping () -> String) {
    customerProviders.append(customerProvider)
}

collectCustomerProviders(customersInLine.remove(at: 0))
collectCustomerProviders(customersInLine.remove(at: 0))

print("Collected \(customerProviders.count) closures.")
// "Collected 2 closures."

for customerProvider in customerProviders {
    print("Now serving \(customerProvider())!")
}
// "Now serving Barry!"
// "Now serving Daniella!"

 
 
[Note]
 
autoclosure를 남용하면 가독성 문제가 생긴다.
평가 지연이 이뤄진다는 사실은 문맥과 함수 이름을 통해 명확히 드러나야 한다.

'iOS > Swift' 카테고리의 다른 글

Swift 공식문서 9. Structures and Classes  (1) 2024.10.05
Swift 공식문서 8. Enumeration  (0) 2024.10.03
Swift 공식문서 6. Functions  (0) 2024.09.20
Swift - In-Out 파라미터  (0) 2024.09.20
Swift 공식문서 5. Control Flow  (0) 2024.09.19