iOS/swift

[Swift 공식문서 정리] - 클로저 (Closures)

skyiOS 2021. 11. 23. 16:24
반응형

클로저(Closures)  

코드에서 함수적인 것을 독립적으로 사용할 수 있는 코드 
코드블럭으로 C와 Objective-C의 블럭(blocks)과 다른 언어의 람다(lambdas)와 비슷 하다.
클로저는 어떤 상수나 변수의 참조를 캡쳐(capture)해 저장할 수 있습니다.
Swift는 이 캡쳐와 관련한 모든 메모리를 알아서 처리합니다.

 캡쳐의 개념에 대해 익숙하지 않다고 걱정하지 않으셔도 됩니다. 값 캡쳐는 아래에서 자세히 설명해 두었습니다.

전역 함수(global functions)중첩 함수(nested function)은 실제 클로저의 특별한 경우다.
클로저는 다음 세 가지 형태 중 하나를 갖는다.
  • 전역 함수 : 이름이 있고 어떤 값도 캡쳐하지 않는 클로저
  • 중첩 함수 : 이름이 있고 관련한 함수로 부터 값을 캡쳐 할 수 있는 클로저
  • 클로저 표현 : 경량화 된 문법으로 쓰여지고 관련된 문맥(context)으로부터 값을 캡쳐할 수 있는 이름이 없는 클로저
Swift에서 클로저 표현은 최적화 되어서 간결하고 명확합니다. 이 최적화에는 다음과 같은 내용을 포함합니다.
  • 문맥(context)에서 인자 타입(parameter type)과 반환 타입(return type)의 추론
  • 단일 표현 클로저에서의 암시적 반환
  • 축약된 인자 이름
  • 후위 클로저 문법(Trailing closure syntax) 함수의 마지막을 클로저로 마무리 할 수 있다.

반응형

클로저 표현(Closure Expressions)

클로저 표현은 인라인 클로저를 명확하게 표현하는 방법으로 문법에 초첨이 맞춰져 있다.
클로저 표현은 코드의 명확성과 의도를 잃지 않으면서도 문법을 축약해 사용할 수 있는 다양한 문법의 최적화 방법을 제공한다
반응형

정렬 메소드( The Sorted Method)

Swift의 기본 라이브러리에서 제공하는 sorted(by:) 메서드는 사용자가 정의한 값들을 Array 타입으로 반환해주는 클로저입니다.
sorted(by:)메서드는 Arrya를 반환해 주는 메서드 이기 때문에 Array를 정렬하여 저장하고 싶다면 새로운 변수나 상수에 할당해주합니다.

let names = ["Chris", "Alex", "Ewa", "Barry", "Daniella"]

 

sorted(by:) 메소드는 배열의 콘텐츠와 같은 타입을 갖고 두개의 인자를 갖는 클로저를 인자로 사용합니다. name의 콘텐츠는 String 타입이므로 (String, String) -> Bool 의 타입의 클로저를 사용해야 합니다.
클로저를 제공하는 일반적인 방법은 함수를 하나 만드는 것입니다. 위 타입을 만족 시키는 함수를 하나 만들면 정렬에 인자로 넣을 수 있는 클로저를 만들 수 있습니다.
func backward(_ s1: String, _ s2: String) -> Bool {
    return s1 > s2
}
var reversedNames = names.sorted(by: backward)
// reversedNames is equal to ["Ewa", "Daniella", "Chris", "Barry", "Alex"]

클로저 표현 문법 (Closure Expression Syntax)

클로저 표현 문법은 일반적으로 아래의 형태를 띱니다.
{ (parameters) -> return type in
    statements
}
인자로 넣을 parameters
 인자 값으로 처리할 내용을 기술하는 statements 
그리고 return type입니다.

앞의 backward클로저를 이용해 배열을 정렬하는 코드는 클로저 표현을 이용해 다음과 같이 바꿀 수 있습니다.

reversedNames = names.sorted(by: { (s1: String, s2: String) -> Bool in
    return s1 > s2
})

이렇게 함수로 따로 정의된 형태가 아닌 인자로 들어가 있는 형태의 클로저를 인라인 클로저라 부릅니다.
클로저의 몸통(body)은 in 키워드 다음에 시작합니다
사용할 인자 값과(parameters) 반환 타입(return type)을 알았으니 이제 그것들을 적절히 처리해 넘겨 줄 수 있다. 


문맥에서 타입 추론 (Inferring Type From Context)

정렬 클로저는 String 배열에서 sorted(by:) 메소드의 인자로 사용됩니다. 

sorted(by:)의 메소드에서 이미 (String, String) -> Bool 타입의 인자가 들어와야 하는지 알기 때문에 클로저에서 이 타입들은 생략 될 수 있습니다.

그래서 위 함수는 더 생략한 형태로 아래와 같이 기술 할 수 있습니다.

reversedNames = names.sorted(by: { s1, s2 in return s1 > s2 } )

단일 표현 클로저에서의 암시적 반환 (Implicit Returns from Single-Express Closures)

코드를 보면 s1 > s2의 결과가 Bool이기 때문에 Bool 값을 반환한다고 유추할 수 있습니다.
그래서 return 키워드를 생략 할 수 있습니다.

reversedNames = names.sorted(by: { s1, s2 in s1 > s2 } )

인자 이름 축약 (Shorthand Arguments Names)

Swift는 인라인 클로저에 자동으로 축약 인자 이름을 제공합니다.
이 인 자를 사용하면 인자 값을 순서대로 $0, $1, $2 등으로 사용할 수 있습니다.
축약 인자 이름을 사용하면 인자 값과 그 인자로 처리할 때 사용하는 인자가 같다는 것을 알기 때문에 인자를 입력 받는 부분과 in 키워드 부분을 생략 할 수 있습니다. 
reversedNames = names.sorted(by: { $0 > $1 } )

연산자 메소드 (Operator Methods)

여기서 더 줄일수 있습니다! 

Swift의 String 타입 연산자에는 String끼리 비교할 수 있는 비교 연산자(>) 를 구현해 두었습니다. 그냥 이 연산자를 사용하면 됩니다.

reversedNames = names.sorted(by: >)

후위 클로저 (Trailing Closures)

만약 함수의 마지막 인자로 클로저를 넣고 그 클로저가 길다면 후위 클로저를 사용할 수 있습니다. 이런 형태의 함수와 클로저가 있다면

func someFunctionThatTakesAClosure(closure: () -> Void) {
    // function body goes here
}

위 클로저의 인자 값 입력 부분과 반환 형 부분을 생략해 다음과 같이 표현할 수 있고

someFunctionThatTakesAClosure(closure: {
    // closure's body goes here
})

이것을 후위 클로저로 표현하면 아래와 같이 표현할 수 있습니다. 함수를 대괄호 ( {, } )로 묶어 그 안에 처리할 내용을 적으면 됩니다

someFunctionThatTakesAClosure() {
    // trailing closure's body goes here
}

reversedNames = names.sorted { $0 > $1 }

이번에는 후위 클로저를 이용해 숫자(Int)를 문자(String)로 매핑(Mapping)하는 예제를 살펴 보겠습니다. 다음과 같은 문자와 숫자가 있습니다.

let digitNames = [
    0: "Zero", 1: "One", 2: "Two",   3: "Three", 4: "Four",
    5: "Five", 6: "Six", 7: "Seven", 8: "Eight", 9: "Nine"
]
let numbers = [16, 58, 510]

 값을 배열의 map(_:)메소드를 이용해 특정 값을 다른 특정 값으로 매핑하는 할 수 있는 클로저를 구현합니다.

let strings = numbers.map { (number) -> String in
    var number = number
    var output = ""
    repeat {
        output = digitNames[number % 10]! + output
        number /= 10
    } while number > 0
    return output
}
// let strings는 타입 추론에 의해 문자 배열([String])타입을 갖습니다.
// 결과는 숫자가 문자로 바뀐 ["OneSix", "FiveEight", "FiveOneZero"]가 됩니다.

 해당 코드처럼 만들면 Array의 모든 항목에 대해 클로저 구문을 수행하게 됩니다.

클로저의 매개변수인. number의 타입은 Swift가 유추할 수 있기때문 입니다.


값 캡쳐 (Capturing Values) 

클로저는 특정 문맥의 상수나 변수의 값을 캡쳐할 수 있습니다.
원본 값이 사라져도 클로져의 body안에서 그 값을 활용할 수 있습니다.
Swift에서 값을 캡쳐 하는 가장 단순한 형태는 중첩 함수(nested function) 입니다.
중첩 함수는 함수의 body에서 다른 함수를 다시 호출하는 형태로 된 함수 입니다. 
func makeIncrementer(forIncrement amount: Int) -> () -> Int {
    var runningTotal = 0
    func incrementer() -> Int {
        runningTotal += amount
        return runningTotal
    }
    return incrementer
}

 예제를 쪼개보면 (forIncrement amount: Int) -> () -> Int중에 처음 -> 를 기준으로 앞은 인자값이고 뒤 () ->  Int 는 반환값이다.

함수 안에 incrementer함수만 따로보겠습니다.

func incrementer() -> Int {
    runningTotal += amount
    return runningTotal
}

함수 에는 runninngTotal 과 amount가 정의 되있지 않지만 캡쳐하여 사용 할 수 있는 것입니다.

최적화 이유로 Swift는 만약 더 이상 클로저에 의해 값이 사용되지 않으면 그 값을 복사해 저장하거나 캡쳐링 하지 않습니다. Swift는 또 특정 변수가 더 이상 필요하지 않을 때 제거하는 것과 관련한 모든 메모리 관리를 알아서 처리합니다.
let incrementByTen = makeIncrementer(forIncrement: 10)
incrementByTen()
// 값으로 10을 반환합니다.
incrementByTen()
// 값으로 20을 반환합니다.
incrementByTen()
// 값으로 30을 반환합니다.
함수가 각기 실행 되지만 실제로는 변수 runningTotal과 amount가 캡쳐링되어 변수를 공유하기 때문에 계산이 누적된다.

새로운 클로저를 생성하면 어떻게될까?

let incrementBySeven = makeIncrementer(forIncrement: 7)
incrementBySeven()
// returns a value of 7

 다른 클로저를 생성했기 때문에 고유의 저장소에 runningTotal amount를 캡쳐링 해서 사용합니다. 

만약 클로저를 어떤 클래스 인스턴스의 프로퍼티로 할당하고 그 클로저가 그 인스턴스를 캡쳐링하면 강한 순환참조에 빠지게 됩니다. 즉, 인스턴스의 사용이 끝나도 메모리를 해제하지 못하는 것이죠. 그래서 Swift는 이 문제를 다루기 위해 캡쳐 리스트(capture list)를 사용합니다.

클로저는 참조 타입 (Closures Are Reference Types)

위의 예에서 본 incrementByTen은 상수로 선언된 값이지만 계속해서 그 안의 값들이 변하는 것을 볼 수 있었습니다.그 이유는 함수와 클로저는 참조 타입이기 때문입니다. 함함수와 클로저를 상수나 변수에 할당할 때 실제로는 상수와 변수에 해당 함수나 클로저의 참조(reference)가 할당 됩니다.만약 한 클로저를 두 상수나 변수에 할당하면 그 두 상수나 변수는 같은 클로저를 참조하고 있습니다

let alsoIncrementByTen = incrementByTen
alsoIncrementByTen()
// 50을 반환합니다.

이스케이핑 클로저 (Escaping Closures)

스위프트에서는 함수의 파라미터로 전달된 클로저는 기본적으로 해당 함수 내부에서만 사용이 가능합니다.

이걸 탈출 불가 상태라고 합니다.

클로저를 함수의 파라미터로 넣을 수 있는데, 함수 밖(함수가 끝나고)에서 실행되는 클로저 예를들어, 비동기로 실행되거나 completionHandler로 사용되는 클로저는 파라미터 타입 앞에 @escaping이라는 키워드를 명시해야 합니다.

var completionHandlers: [() -> Void] = []
func someFunctionWithEscapingClosure(completionHandler: @escaping () -> Void) {
    completionHandlers.append(completionHandler)
}

@escaping 를 사용하는 클로저에서는 self를 명시적으로 언급해야 합니다.

func someFunctionWithNonescapingClosure(closure: () -> Void) {
    closure()    // 함수 안에서 끝나는 클로저
}

class SomeClass {
    var x = 10
    func doSomething() {
        someFunctionWithEscapingClosure { self.x = 100 } // 명시적으로 self를 적어줘야 합니다.
        someFunctionWithNonescapingClosure { x = 200 }
    }
}

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

completionHandlers.first?()
print(instance.x)
// Prints "100"

자동클로저 (Autoclosures)

자동클로저는 인자 값이 없으며 특정 표현을 감싸서 다른 함수에 전달 인자로 사용할 수 있는 클로저입니다.
자동클로저는 클로저를 실행하기 전까지 실제 실행이 되지 않습니다.
계산이 복잡한 연산을 하는데 유용합니다.
실제 계산이 필요할 때 호출되기 때문입니다.
var customersInLine = ["Chris", "Alex", "Ewa", "Barry", "Daniella"]
print(customersInLine.count)
// Prints "5"

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

print("Now serving \(customerProvider())!")
// Prints "Now serving Chris!"
print(customersInLine.count)
// Prints "4"
위 예제 코드를 보면let customerProvider = { customersInLine.remove(at: 0) }이 클로저 코드를 지났음에도 불구하고customersInLine.count는 변함없이 5인 것을 볼 수 있습니다.
그리고 그 클로저를 실행시킨
print("Now serving \(customerProvider())!")
 이후에야 배열에서 값이 하나 제거되어 배열의 원소 개수가 4로 줄어든 것을 확인할 수 있습니다.
이렇듯 자동 클로저는 적혀진 라인 순서대로 바로 실행되지 않고, 실제 사용될 때 지연 호출 됩니다.

자동클로저를 함수의 인자 값으로 넣는 예제는 아래와 같습니다.

// customersInLine is ["Alex", "Ewa", "Barry", "Daniella"]
func serve(customer customerProvider: () -> String) {
    print("Now serving \(customerProvider())!")
}
serve(customer: { customersInLine.remove(at: 0) } )
// Prints "Now serving Alex!"
serve함수는 인자로 () -> String) 형, 즉 인자가 없고, String을 반환하는 클로저를 받는 함수 입니다.
그리고 이 함수를 실행할 때는 serve(customer: { customersInLine.remove(at: 0) } )이와 같이 클로저{ customersInLine.remove(at: 0) }를 명시적으로 직접 넣을 수 있습니다.
위 예제에서는 함수의 인자로 클로저를 넣을 때 명시적으로 넣는 경우에 대해 알아 보았습니다. 위 예제를 @autoclosure키워드를 이용해서 보다 간결하게 사용할 수 있습니다. 예제를 보시겠습니다.
// customersInLine is ["Ewa", "Barry", "Daniella"]
func serve(customer customerProvider: @autoclosure () -> String) {
    print("Now serving \(customerProvider())!")
}
serve(customer: customersInLine.remove(at: 0))
// Prints "Now serving Ewa!"
serve함수의 인자를 받는 부분 customerProvider: @autoclosure () 에서 클로저의 인자()앞에 @autoclosure라는 키워드를 붙였습니다. 이 키워드를 붙임으로써 인자 값은 자동으로 클로저로 변환됩니다.

그래서 함수의 인자 값을 넣을 때 클로저가 아니라 클로저가 반환하는 반환 값과 일치하는 형의 함수를 인자로 넣을 수 있습니다. 정리하면 클로저 인자에 @autoclosure를 선언하면 함수가 이미 클로저 인것을 알기 때문에 리턴값 타입과 같은 값을 넣어줄 수 있습니다.

자동클로저@autoclosure는 이스케이프@escaping와 같이 사용할 수 있습니다. 동작에 대한 설명은 코드에 직접 주석을 달았습니다.

// customersInLine is ["Barry", "Daniella"]
var customerProviders: [() -> String] = []        //  클로저를 저장하는 배열을 선언
func collectCustomerProviders(_ customerProvider: @autoclosure @escaping () -> String) {
    customerProviders.append(customerProvider)
} // 클로저를 인자로 받아 그 클로저를 customerProviders 배열에 추가하는 함수를 선언
collectCustomerProviders(customersInLine.remove(at: 0))    // 클로저를 customerProviders 배열에 추가
collectCustomerProviders(customersInLine.remove(at: 0))

print("Collected \(customerProviders.count) closures.")
// Prints "Collected 2 closures."        // 2개의 클로저가 추가 됨
for customerProvider in customerProviders {
    print("Now serving \(customerProvider())!")    // 클로저를 실행하면 배열의 0번째 원소를 제거하며 그 값을 출력
}
// Prints "Now serving Barry!"
// Prints "Now serving Daniella!"

 

 

 

 

반응형