티스토리 뷰

Swift

[Swift] Closures

진태우 2021. 2. 21. 22:17

애플 문서를 보고 정리해본 내용입니다.

 

 

정의 및 특징

  1. 클로저는 코드 안에서 전달되어 사용할 수 있는 독립적인 기능을 가진 일급 객체입니다.
  2. 일급 객체는 변수/상수로 선언하거나 전달이 가능하며, 반환값으로도 사용될 수 있는 객체입니다.
  3. 클로저는 참조 타입입니다.

 

표현 방식

아래는 일반적인 표현 방식의 형태입니다.

{ (parameters) -> returnType in
    // do something..
}

 

아래는 숫자의 기호(양수/음수)를 변경 하는 클로저를 선언하여 사용하는 예제입니다.

let operation = { (operand: Double) -> Double in 
    return -operand 
}

operation(4.0)    // -4.0

 

위와 같이 선언한 클로저를 축약해서 표현할 수도 있습니다.

// 리턴 타입 생략
// 타입 추론을 통해 파라미터의 타입과 리턴되는 값의 타입을 제거할 수 있습니다.
operation = {	(operand) in return -operand }

// return 키워드 생략
// 어떤 값을 리턴할지 알고 있기 때문에 return 명령어를 제거할 수 있습니다.
operation = {	(operand) in -operand }

// 파라미터 이름 생략
// 파라미터 이름을 따로 명시하지 않을 경우 순서대로 $0, $1, $2 등으로 축약해서 사용할 수 있습니다.
// 때문에 operand는 $0으로 변환해주면서 in이 필요 없어지게 됩니다.
operation = { -$0 }

순차적으로 클로저를 축약해 보았습니다.

하지만 문법을 간결하게 한다고 해서 무조건 좋은 것은 아니기 때문에 상황에 따라서 읽기 편하게 사용하면 될 것 같습니다.

 

Trailing Closure(후위 클로저)

함수의 마지막 파라미터로 클로저를 전달할 경우 표현할 수 있는 방식입니다.

클로저를 사용하는 map 함수를 예제로 알아보겠습니다.

let numbers = [1, 2, 3, 4, 5]

// map 함수의 파라미터로 클로저가 전달됩니다.
let negativeNumbers = numbers.map({ -$0 })    // [-1, -2, -3, -4, -5]

// 함수의 마지막 파라미터로 클로저가 전달되면 괄호 밖으로 빼서 표현할 수 있습니다.
let floatNumbers = numbers.map() { $0 * 1.0 }    // [1.0, 2.0, 3.0, 4.0, 5.0]

// 클로저가 유일한 파라미터라면 괄호를 제거해도 됩니다.
let stringNumbers = numbers.map { String($0) }    // ["1", "2", "3", "4", "5"]

 

Capturing

클로저는 그것이 정의된 지역 외부에 있는 상수 및 변수를 캡처할 수 있습니다. 캡처한 상수 및 변수는 나중에 없어지더라도 클로저 내에서 값을 참조하고 있기 때문에 접근하여 수정할 수 있습니다.

Swift에서 값을 캡처 하는 가장 단순한 형태는 중첩 함수(nested function) 입니다.

중첩 함수는 외부 함수의 인자와 외부 함수에서 정의한 상수 및 변수를 캡처할 수 있습니다.

아래 예제를 보겠습니다.

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

    return incrementer
}

makeIncrementer 함수는 내부에 incrementer 함수를 반환하는 형태의 중첩 함수입니다.

 

incrementer 함수만 따로 보면,

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

어디에도 runningTotal, amount가 선언되지 않았습니다.

runningTotal은 함수 외부에서 선언된 변수이고, amount는 외부 함수에서 인자로 받은 값입니다.

incrementer 함수에서 해당 값을 캡처해서 사용하고 있다는 것입니다.

 

아래는 함수의 호출 예제입니다.

let incrementByTen = makeIncrementer(forIncrement: 10)

incrementByTen()    // returns a value of 10
incrementByTen()    // returns a value of 20
incrementByTen()    // returns a value of 30

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

 

클래스 인스턴스의 속성(property)에 클로저를 할당하고 클로저가 인스턴스 및 인스턴스의 멤버를 참조하면, 캡처링이 발생하면서 클로저와 인스턴스 사이에 강한 순환 참조가 생성됩니다.
이런 경우, 인스턴스가 없어져도 강한 참조가 해제되지 않기 때문에 메모리 사이클이 발생합니다.
이 문제를 해결하기 위해 캡처 리스트(capture list)를 사용해야 합니다.

 

escaping closure

클로저를 함수의 파라미터로 전달될 때, 전달된 클로저가 함수가 끝나고 함수 외부에서 실행될 수 있습니다.

예를 들면 비동기 작업을 하는 함수에서 완료 핸들러로 클로저 파라미터를 사용합니다. 이때 클로저는 작업이 완료되고 함수 외부에서 호출하게 되는데, 이 경우에는 클로저 앞에 @escaping 키워드를 명시해야 컴파일 오류가 발생하지 않습니다.

 

일반적으로 클로저는 내부에서 사용되는 변수를 암시적으로 캡처하게 됩니다. 하지만 escaping 클로저의 경우 사용되는 변수를 명시적으로 표현해야 합니다.

만약 self를 캡처하려면 self를 사용할 때 직접 명시해주거나, 캡처 리스트(capture list)에 포함하여 캡처한다는 의도를 명확하게 보여줘야 합니다.

 

아래는 escaping closure에서 self를 명시적으로 표현하는 예제입니다.

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

func someFunctionWithNonescapingClosure(closure: () -> Void) {
    closure()
}

class SomeClass {
    var x = 10
    func doSomething() {
        // self를 명시적으로 표현해야 에러가 발생하지 않음
        someFunctionWithEscapingClosure { self.x = 100 }

        // self를 암시적으로 사용할 수 있어 self를 명시하지 않아도 됨
        someFunctionWithNonescapingClosure { x = 200 }
    }
}

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

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

 

아래는 캡처 리스트에 self를 추가하여 사용하는 예제입니다.

class SomeOtherClass {
    var x = 10
    func doSomething() {
				// capture list에 self 추가하여 캡처
        someFunctionWithEscapingClosure { [self] in x = 100 }
        someFunctionWithNonescapingClosure { x = 200 }
    }
}

 

AutoClosure

AutoClosure는 파라미터 값이 없으며 특정 표현을 감싸서 다른 함수의 파라미터로 사용할 수 있는 클로저입니다.

AutoClosure를 사용하면 클로저가 호출될 때까지 코드가 실행되지 실행되지 않습니다. 이런 코드 지연의 특성을 이용하면 실행 시점을 제어할 수 있기 때문에 부작용을 줄이고, 복잡한 연산이 필요할 때 유용합니다.

아래는 코드가 지연되어 사용되는 예제입니다.

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"

 

AutoClosure를 함수의 파라미터로 전달하는 예제입니다.

// 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을 반환하는 클로저를 파라미터로 받습니다.

보통 클로저를 전달할 때, {}(중괄호)를 붙여야 하지만 @autoclosure 키워드를 명시해주면 {}(중괄호) 없이 클로저를 전달할 수 있습니다.

 

아래는 @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!"

 

 

Reference

https://docs.swift.org/swift-book/LanguageGuide/Closures.html#

https://jusung.gitbook.io/the-swift-language-guide/language-guide/07-closures

댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
글 보관함