아리의 iOS 탐구생활

[Swift] Closure에 대해 알아보자. 본문

Swift/문법

[Swift] Closure에 대해 알아보자.

Ari Lee 2021. 9. 7. 19:25
반응형
 

Closures — The Swift Programming Language (Swift 5.5)

Closures Closures are self-contained blocks of functionality that can be passed around and used in your code. Closures in Swift are similar to blocks in C and Objective-C and to lambdas in other programming languages. Closures can capture and store referen

docs.swift.org

클로저(Closure)는 코드의 블록이다. 함수는 ‘이름이 있는 클로저’라고 표현한다.

보통 클로저는 ‘이름이 없는 코드블록’을 클로저라고 한다.

클로저는 일급시민(first-citizen)으로 전달인자, 변수, 상수 등으로 저장, 전달이 가능하다.

 

 

 

🤔 일급시민(first-citizen)이란?

  • 변수나 데이터 구조안에 담을 수 있다
  • 파라미터로 전달할 수 있다
  • 반환값(return value)으로 사용할 수 있다.
  • 할당에 사용된 이름과 관계없이 고유한 구별이 가능하다.
  • 동적으로 프로퍼티 할당이 가능하다.

 

 

 

🔍 Closure의 유형

 

Global 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
}

 

Closure expressions

이것이 흔히 클로저라고 불리는 유형이다.

 

✍🏻  기본 클로저 문법

{ (매개변수) -> 반환타입 in
	실행코드
}

 

 

 

🔍  클로저의 변수 할당

 

클로저는 일급시민이기 때문에 변수에 할당될 수 있다.

let closureValue = { (name: String) in
    print(name)
}
closureValue("Ari Lee")

 

또한 함수의 인자 값으로도 전달될 수 있다.

func closureOperation(then closure: () -> Void) {
	// Some code
}

 

 

🔍  클로저 축약하기

Swift에서의 클로저는 다른 언어에서보다 유연하다

그래서 보다 자유롭게 축약하거나 변형할 수 있다.

// 이랬던 코드가
let A = [5,3,4,2,6].sorted { a, b in
    return a < b
}

// 이렇게 축약된다.
let B = [5,3,4,2,6].sorted { $0 < $1 }
  • 후행 클로저를 사용해 함수의 파라미터를 축약할 수 있다.
  • $0과 같은 인자 값을 사용해 첫 번째 인자 값으로 대체할 수 있다.
  • 단일 표현 클로저에서는 return키워드를 생략할 수 있다.

 

 

🔍 Trailing Closure

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

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

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

// 함수의 마지막 파라미터로 클로저가 전달되면 괄호 밖으로 빼서 표현할 수 있다.
let floatNumbers = numbers.map() { Double($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 Values

Closure는 주변의 value를 포착(capture)한다.

캡쳐한 상수 및 변수는 나중에 없어지더라도 클로저 내에서 값을 참조하고 있기 때문에 접근하여 수정할 수 있다.

Swift에서 값을 캡쳐하는 가장 단순한 형태는 중첩 함수다.

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

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

 

incrementer 함수만 떼어놓고 보면,

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

incrementer 함수 내부에는 없는 runningTotal과 amount를 사용하고 있다.

즉 외부에서 선언된 변수를 캡쳐해서 사용하고 있다는 것이다.

 

그래서 다음과 같이 코드를 반복 실행하면 값이 계속 증가하게 된다.

let incrementByTen = makeIncrementer(forIncrement: 10)

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

 

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

 

 

 

🔍 Escaping Closure

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

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

 

일반적으로 클로저는 내부에서 사용되는 변수를 암시적으로 캡쳐하게 된다. 하지만 escaping 클로저의 경우 사용되는 변수를 명시적으로 표현해줘야 한다. 만약 self를 캡쳐하려면 self를 사용할 때 직접 명시해주거나, 캡쳐 리스트(capture list)에 포함하여 캡쳐한다는 의도를 명확하게 보여줘야 한다.

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를 사용하면 클로저가 호출될 때 까지 코드가 실행되지 않는다.

이런 코드 지연의 특성을 이용하면 실행 시점을 제어할 수 있기 때문에 부작용을 줄이고 복잡한 연산이 필요할 때 유용하다.

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

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

 

 

반응형
Comments