본문 바로가기
Swift/Language Guide

Closures

by diosmio 2021. 9. 15.

클로저는 우리가 사용하거나 전달할 수 있는 기능 블럭이다

 

참고로, 다른 언어에도 비슷한 개념이 있는데

C언어 / Obj-C에서의 block과, 코틀린의 람다와 유사하다

 

클로저는 외부 상.변수의 참조를 캡쳐하여 저장할 수 있다

이를, closing over라고 표현한다

(캡쳐에 대해서는 본문에서 다룰 예정이니 넘어가자)

 

 

클로저는 함수와 같은 기능 블럭들 중 가장 포괄적인 개념이다.

함수는 일종의 special 클로저로 정의된다.

(이름을 가지고 캡쳐가 없는 클로저)

 

 

Swift의 클로저 표현은 깔끔하고 명확한 코드를 짤 수 있도록 최적화 기법들이 있다

  • parameter와 return 타입 알아서 추론하기
  • return이란 말을 굳이 안써도 되는 "암시적 return"
  • shorthand 버전의 argument 이름
  • 후행 클로저 표현

 

목차

1. 클로저 문법

2. 캡쳐란?

3. 클로저는 참조타입

4. Escaping 클로저

5. 자동 클로저

 

 

클로저 문법

클로저는 언제 유용할까?

먼저 이런 경우를 보자

상황에 따라 다른 Function을 수행하고 싶을 때 이런 식으로 Nested Function을 활용할 수 있다

func chooseStepFunction(backward: Bool) -> (Int) -> Int {
    func stepForward(input: Int) -> Int { return input + 1 }
    func stepBackward(input: Int) -> Int { return input - 1 }
    return backward ? stepBackward : stepForward
}

backward라는 기능블럭을 argument로 전달받고

이를 비교하기 위해 stepForward와 stepBackward라는 재사용성조차 없는 함수를

이름도 정해주며 full declaration해주어야 했다

좀 더 간단하게 구현할 방법이 없을까?

 

다른 경우도 있지만 이렇게 argument로 함수나 메소드를 받는 상황에서 특히 클로저가 유용하다

 

 

 

클로저의 다양한 shorthand 문법

 

Swift 표준 라이브러리에서 제공하는 sorted(by: ) 메소드를 예시로 문법을 알아보자

sorted는 "by: "로 넣어주는 정렬클로저에 기반하여 정렬하는 메소드이다

 

참고로, sorted는 원본은 건드리지 않고 정렬된 new Array를 생성하여 return한다

 

자 그럼, by: 에 들어갈 클로저를 구현해보자

 

 

1. 함수로 전달하기

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"]

 

2. 정석 클로저로 전달하기

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

 

3. parameter 타입 생략하기

이게 가능한 이유는 by: 의 타입이 이미 sorted에 정의되어 있기 때문

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

 

4. +암시적 return

이게 가능한 이유는 by: 의 타입이 이미 sorted에 정의되어 있기 때문

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

 

5. +Argument 이름 생략

reversedNames = names.sorted(by: { $0 > $1 } )

 

6. +소괄호 밖으로 빼버리기

마지막 parameter가 클로저인 경우, 이렇게 밖으로 빼버릴 수도 있다

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

 

7. 클로저를 여러개 받는 경우

아래 예시와 같이 연결하여 전달한다.

첫번째 클로저만 이름을 생략할 수 있다

//첫번째 클로저의 파라미터명은 생략가능
loadPicture(from: someServer) { picture in
    someView.currentPicture = picture
} onFailure: {
    print("Couldn't download the next picture.")
}


//정의부
func loadPicture(from server: Server, completion: (Picture) -> Void, onFailure: () -> Void) {
    if let picture = download("photo.jpg", from: server) {
        completion(picture)
    } else {
        onFailure()
    }
}

 

 

 

 

클로저의 캡쳐기능?

내 입장에선 굉장히 생소한 컨셉인데,,

클로저는 주변 context의 상.변수를 참조하고 원본수정도 할 수 있다.

 

클로저에서 parameter로 전달받지 않고 외부의 상.변수를 직접적으로 사용하면 "참조"로 가져온다

 

이 컨셉을 사용하는 예시인 Nested Function을 살펴보자

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

원래 runningTotal / amount라는 상.변수는 함수가 종료되며 사라져야 한다.

하지만 incrementer에서 이들을 참조하는 상태로 return되며 살아나가 버렸고

이들을 선언한 makeIncremental 함수가 종료되었음에도 참조 count가 남아서 해제되지 않고 살아남는다

 

이로 인해, Class에서 클로저를 사용할 때 강한참조 순환 문제도 발생할 수 있어
Capture List 라는 개념이 존재한다

 

 

※ 참고

참조하는 상.변수의 값이 바뀌지 않으면 최적화에 의해 copy로 저장시킬 수도 있긴하다

 

 

 

 

 

클로저는 참조타입이다

클로저는 참조타입이므로 다른 변수에 할당하더라도 클로저가 아닌 참조만 복사된다

 

let alsoIncrementByTen = incrementByTen
alsoIncrementByTen()
// returns a value of 50

incrementByTen()
// returns a value of 60

alsoIncrementByTen 변수에 코드블럭이 저장되는게 아니라

아까 만들었던 클로저에 대한 참조가 복사되었다

 

alsoIncrementByTen와 incrementByTen이 참조하는 원본은 완전히 동일하다

 

 

 

 

Escaping 클로저란?

함수 parameter로 전달된 클로저가 함수가 종료된 이후에 실행되는 것을 

클로저가 함수를 escape하였다라고 표현하며 parameter 타입 앞에 @escape를 붙혀 이를 허용해줘야 한다

 

2가지 경우가 있다

1. parameter로 전달된 클로저가 호출 함수 외부의 상.변수에 저장되어 함수가 종료된 후에 사용됨

2. parameter로 전달된 클로저가 return으로 다시 밖으로 나감

 

예제 코드로 보자

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

비동기 동작에서 흔히 볼 수 있는 포맷으로,

함수 외부에 있는 배열에 클로저가 저장되어 나중에 실행된다

 

 

 

Class 메소드 + Escaping 클로저 유의점

원래 메소드에서 프로퍼티를 참조할때 self는 생략해도 무관하다

 

하지만,

Escaping 클로저를 사용할땐 self를 반드시 붙혀줘야 한다

그 이유를 살펴보자

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

class SomeClass {
    var x = 10
    func doSomething() {
        someFunctionWithEscapingClosure { self.x = 100 }
        someFunctionWithNonescapingClosure { x = 200 }
    }
}

escaping 클로저에서 self를 사용하면 메소드 외부에 인스턴스에 대한 참조가 저장되면서

인스턴스를 nil 처리하더라도 참조가 남아 메모리가 해제되지 않는다던지 참조 순환의 이슈가 발생할 수 있다

 

개발자로 하여금 참조 순환을 유의하도록 하기위해 self 명시를 강제하였다

위의 예제처럼 self를 명시적으로 적어주거나 Capture List에 포함시켜야 한다

 

 

 

Struct/Enum 메소드 + Escaping 클로저 유의점

반면, 값타입에서는 항상 self를 암묵적으로 사용할 수 있다

 

하지만,

값타입에서는 Escaping 클로저가 프로퍼티를 변경할 수 없다

 

이유는 간단하다.

값타입에서는 mutating 선언된 내부 메소드에서만 인스턴스를 변경할 수 있기 때문이다.

mutating 내부 메소드가 아닌 외부에서는 self를 변경할 수 없다

 

struct SomeStruct {
    var x = 10
    mutating func doSomething() {
    	someFunctionWithEscapingClosure { x = 100 }     // Error
        someFunctionWithNonescapingClosure { x = 200 }  // Ok
    }
}

애초에 self를 변경할 수 없으므로 self를 명시해야 할지를 따질 필요가 없다

 

 

 

 

 

자동 클로저란?

함수 parameter로 클로저를 전달하는 경우에서 클로저에 인자값이 없다면

클로저 대신 클로저의 반환값을 넘기도록 축약표현할 수 있다

 

예시로 보는게 편하겠다

 

[ Original ] 

// 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!"

 

[ @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!"

 

@autoclosure 키워드를 사용하여

1. parameter를 넘길 때 클로저가 아닌 클로저의 반환값을 넘겼다

2. parameter 타입 앞에 @autoclosure를 명시하여 넘어오는 값이 클로저임을 알린다

 

 

 

 

escaping과 함께 사용될 수도 있다

// customersInLine is ["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.")
// Prints "Collected 2 closures."
for customerProvider in customerProviders {
    print("Now serving \(customerProvider())!")
}
// Prints "Now serving Barry!"
// Prints "Now serving Daniella!"

 

'Swift > Language Guide' 카테고리의 다른 글

Subscripts  (0) 2021.09.17
Enumerations(열거형)  (0) 2021.09.16
Error Handling - #1/1  (0) 2021.09.13
Access Control - #1/1  (0) 2021.09.13
Basic Operators - #1/1  (0) 2021.09.13

댓글