GGURUPiOS

Swift 공식 문서 정리 - (7) Closure 본문

Swift/공식문서 정리 ( 문법 )

Swift 공식 문서 정리 - (7) Closure

꾸럽 2023. 4. 19. 16:24

Closure

함수를 만들지 않고 함께 실행되는 그룹코드

클로저는 정의된 문맥에서 모든 상수 및 변수에 대한 참조를 캡쳐하고 저장 가능.

Swift는 이 캡쳐와 관련한 모든 메모리를 알아서 처리함

클로저는 다음 세 가지 형태 중 하나를 갖는다

  • 전역 함수 : 이름이 있고 어떤 값도 캡쳐하지 않는 클로저 (함수도 클로저의 종류라는 말임)
  • 중첩 함수 : 이름이 있고 관련한 함수로 부터 값을 캡쳐 할 수 있는 클로저
  • 클로저 표현 : 경량화 된 문법으로 쓰여지고 관련된 문맥(context)으로부터 값을 캡쳐할 수 있는 이름이 없는 클로저

Swift에서 클로저 표현은 최적화 되어서 간결하고 명확함.

최적화 내용

  • 문맥(context)에서 인자 타입(parameter type)과 반환 타입(return type)의 추론
  • 단일 표현 클로저에서의 암시적 반환
  • 축약된 인자 이름
  • 후위 클로저 문법

클로저 표현식

클로저 표현은 인라인 클로저를 명확하게 표현하는 방법으로 문법에 초첨이 맞춰져 있음.

클로저 표현은 코드의 명확성과 의도를 잃지 않으면서 문법을 축약해 사용할 수 있는 최적화 방법 제공

정렬 메소드 ( 예제 )

sorted(by:) 는 스위프트에서 알려진 타입의 배열값을 정렬하는 메소드임.

by에는 어떤 방법으로 정렬을 수행할 것인지에 대해 기술한 클로저를 넣으면 그 방법대로 정렬된 배열을 얻을 수 있음.

원본 배열은 변경하지 않음!

위의 메소드와 클로저를 활용해 정렬을 시켜보자.

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)

// 하지만 위의 방법은 다소 장황한 방법임. 아래에서 다양한 문법 및 사용에 대해 다룸

클로저 표현 문법

클로저 표현 문법은 일반적으로 아래의 형태임

{ (parameters) -> return type in
    statements
}

클로저 표현식 구문의 파라미터는 in-out 파라미터 일 수는 있지만 기본값을 가질 수는 없음.

가변 파라미터는 가변 파라미터의 이름을 지정하면 사용 가능. 튜플은 파라미터 타입 및 반환 타입으로 사용 가능

아래 예제는 위 함수의 클로저 표현식 버전

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

파라미터 선언 및 반환 타입은 함수의 선언과 동일 함. 그러나 파라미터와 반환 타입은 중괄호 외부가 아니라 내부에 작성 됨

이렇게 함수로 따로 정의된 형태가 아니라 인자로 들어가 있는 형태의 클로저를 인라인 클로저라고 부름.

클로저 본문의 시작은 in 키워드에 의해 시작 됨. 이 키워드는 클로저의 파라미터와 반환 타입의 정의가 완료 되 었고 클로저의 본문이 시작되려고 함을 나타냄

문맥(Context) 에서 타입 추론 - 축약 1

sorted(by:) 에서 클로저가 인자로 전달되기 때문에 Swift는 파라미터와 반환 타입을 유추 할 수 있음.

즉 스위프트에서 타입추론을 해주기 때문에 생략 가능하다는 말.

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

// 위와 같이 (s1: String, s2: String) -> Bool 부분을 s1, s2 로 파라미터만 작성해서 생략 가능

결국 ! → 클로저가 함수 또는 메소드의 인수로 사용 될 때는 완전한 형식의 인라인 클로저를 작성할 필요가 없음.

가독성을 높이고 모호성을 피하기 위해서는 완전한 형태의 코드를 짤 수 도 있음.

단일 표현식 클로저의 암시적 반환 - 축약 2

단일 표현식 클로저는 return 키워드를 생략할 수 있음.

단일 표현? → 하나의 표현식으로 이루어진 구문. 여기서는 s1 > s2 임

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

// return 생략

인자 이름 생략 - 축약 3

스위프트는 인라인 클로저에 자동으로 축약 인자 이름을 제공 함

$0, $1, $2 처럼 사용할 수 있다. (인자 순서대로 $0, $1, $2 … )

축약 인자 이름을 사용하면 인자 값과 그 인자로 처리할 때 사용하는 인자가 같다는 것을 알기 때문에 인자를 입력받는 부분과 in 키워드를 부분 생략 할 수 있음

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

연산자 메소드 - 축약 4

여기서 더 축약 가능. 스위프트의 String타입 연산자에는 String 끼리 비교할 수 있는 비교연산자가 구현되어있다. 때문에 그냥 이 연산자를 사용하면 됨.

reversedNames = names.sorted(by: >)

축약 정리를 해보자

func backward(_ s1: String, _ s2: String) -> Bool {
    return s1 > s2
}
var reversedNames = names.sorted(by: backward)

// 파라미터에 함수를 전달 했을 때

reversedNames = names.sorted(by: { (s1: String, s2: String) -> Bool in
    return s1 > s2
}) // 함수가 아닌 (인라인)클로저로 전달
reversedNames = names.sorted(by: { s1, s2 in return s1 > s2 } ) // 타입추론 으로 생략
reversedNames = names.sorted(by: { s1, s2 in s1 > s2 } ) // 단일 표현식으로 return 생략
reversedNames = names.sorted(by: { $0 > $1 } ) // 인자 이름 생략 

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(by: { $0 > $1 } )
reversedNames = names.sorted() { $0 > $1 }

// 만약 함수의 마지막 인자가 클로저이고 후위 클로저를 사용한다면 () 를 생략할 수 있음 

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

후위 클로저를 이용해 숫자를 문자로 매핑하는 에제를 살펴보자

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"]가 됩니다.

값 캡쳐 (Capturing Values)

클로저는 특정 문맥의 상수나 변수의 값을 캡쳐할 수 있음.

다시말해 원본 값이 사라져도 클로져의 body안에서 그 값을 활용할 수 있다.

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

중첩 함수는 함수부분에서도 다뤘듯이 함수의 body에서 다른함수를 다시 호출하는 형태로 된 함수 임

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

// 반환값이 클로저인 () -> Int 형태

위 함수의 중첩함수만 쪼개서 보자

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

// runnigTotal 과 amount 가 없지만 동작을 한다.
// 이는 캡쳐링 되었기 때문

/* 공식문서에서 
스위프트는 최적화의 이유로 더 이상 클로저에 의해 값이 사용되지 않으면 그 값을 저장하거나 캡쳐링 하지 않음
또, 특정 변수가 더 이상 필요하지 않을 때 제거하는 것과 관련한 모든 메모리 관리를 알아서 함
*/

위의 중첩 함수를 실행해보자

let incrementByTen = makeIncrementer(forIncrement: 10)

incrementByTen()
// 값으로 10을 반환합니다.
incrementByTen()
// 값으로 20을 반환합니다.
incrementByTen()
// 값으로 30을 반환합니다.

// 함수는 각기 실행되지만 캡쳐링 되서 그 변수를 공유하기 때문에 계산이 누적된 결과를 가짐

// 만약 새로운 클로저를 생성하면 ?
let incrementBySeven = makeIncrementer(forIncrement: 7)
incrementBySeven()
// 값으로 7 반환 
// 다른 클로저이기 때문에 고유의 저장소에 캡쳐링 해서 사용
 
// 여기서 이전 클로저를 실행하면 ? 
incrementByTen() 
// 값으로 40 반환

// 만약 클로저를 어떤 클래스 인스턴스의 프로퍼티로 할당 하고 그 클로저가 그 인스턴스를 캡쳐링
// 하면 강한 순환참조에 빠지게 됨 
// -> 인스턴스의 사용이 끝나도 메모리를 해제 못함.. -> 캡쳐리스트를 사용해서 문제해결 
// 캡쳐리스트는 나중에 다룸.

클로저는 참조 타입이다.

위의 예제에서 incrementByTen, incrementBySeven 은 상수임.

어떻게 runningTotal 변수를 증가 시킬까?

→ 함수와 클로저는 참조 타입이기 때문

함수와 클로저를 상수나 변수에 할당할 때 실제로는 상수와 변수에 해당 함수나 클로저의 참조가 할당 됨.

만약 한 클로저를 두 상수나 변수에 할당하면 같은 클로저를 참조하고 있는 상태임.

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

클로저를 함수의 파라미터로 넣을 수 있는데, 함수 밖에서 실행되는 클로저는 파라ㅣ터 타입 앞에 @escaping 이라는 키워드를 명시 해야함.

클로저가 탈출할 수 있는 한가지 방법은 함수 외부에 저장되는 것임.

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

위 함수에서 인자로 전달된 completionHandler는 some~~~함수가 끝나고 나중에 처리됨

함수가 끝나고 실행되는 클로저에 @escaping을 붙이지 않으면 컴파일 오류가 남.

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?() // someFunctionWithEscapingClosure 실행 self.x == 100 이 실행됨
print(instance.x)
// Prints "100"

자동 클로저

자동 클로저는 인자 값이 없으며 특정 표현을 감싸서 다른 함수에 전달 인자로 사용할 수 있는 클로저

자동클로저는 클로저를 실행하기 전까지 실제 실행이 되지 않음

계산이 복잡한 연산을 하는데 유용함

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"

// 아래는 자동 클로저를 함수의 인자값으로 넣는 예시

func serve(customer customerProvider: () -> String) {
    print("Now serving \\(customerProvider())!") 
}
// 실행시켜야 동작.
serve(customer: { customersInLine.remove(at: 0) } )
// Prints "Now serving Alex!"

// @autoclosure 라는 키워드를 넣어서 간결하게 사용할 수 있다.
func serve(customer customerProvider: @autoclosure () -> String) {
    print("Now serving \\(customerProvider())!")
}
serve(customer: customersInLine.remove(at: 0))

자동클로저를 너무 남용하면 코드를 이해하기 어려워 질 수 있습니다.

그래서 문맥과 함수 이름이 사용하기에 분명해야 함

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