Closures 간단 정리

2023. 2. 6. 03:16Swift

반응형
  • 해당 글은 공식문서의 내용을 간단하게 정리한 것이며 번역 오류가 있을 수 있습니다

참고문서

Closures

  • 글로벌 함수는 이름이 있으면서 어떠한 값도 캡쳐하지않는 클로저이다
  • 중첩함수는 이름이 있으면서 가장 가까운 함수의 값을 캡쳐할 수 있는 클로저이다
  • 클로저 구문은 구문에 있는 값을 캡쳐 가능하며, 가벼운 구문(lightweight syntax)으로 작성 된 이름없는 클로저이다

클로저 표현법

{ (parameters) -> return type in 
    statement
}
  • 위는 기본적인 클로저의 표현법이다
  • 위 파라미터는 in-out파라미터이지만, 기본값을 가지지는 않는다
let numbers = [1, 2, 4, 3, 7, 5, 2]

let sortedNumbers = numbers.sorted(by: { (s1: Int, s2: Int) -> Bool in
    return s1 > s2 
})
  • 위의 sorted는 애플 기본 라이브러리에서 제공하는 정렬메소드로 클로저를 전달 받아 해당 클로저의 내용에 따라 정렬해주는 메소드이다
  • 이를 통해 s1이 s2보다 클 경우 true를 리턴하면서 정렬을 해줄텐데 이럴 경우 numbers안의 요소들이 바뀌기 때문에 해당 s1, s2파라미터들이 위에서 말한 in-out파라미터인것을 알 수 있다
  • Swift의 컴파일러는 타입을 추정하기 때문에 Int타입을 써주지 않고 더욱 간단하게 클로저를 작성할 수 있다
    numbers.sorted(by: { s1, s2 in return s1 > s2 })
  • Single-Express Closure의 암시적 반환
  • 리턴 타입이 명확한 시점에서는 return타입을 생략해도 어떠한 모호성이 없다
    numbers.sorted(by: { s1, s2 in s1 > s2 })
  • 클로저의 파라미터들은 축약하여 사용할 수 있는 이름을 제공한다
  • $0, $1등 을 사용할 경우 클로저 첫부분 in과 앞의 파라미터 정의 부분을 생략할 수 있다.
    number.sorted(by: { s1 > s2 })
  • 놀랍게도 여기서는 연산자를 사용하여 리턴값을 반환하므로 단순하게 연산자만을 이용하여 클로저 선언도 가능하다
    numbers.sorted(by: >)

Trailing Closures(후행 클로저)

  • 위 sorted의 인자에서 마지막 인자를 받을 경우에도 다양한 형태의 포현 방식을 쓸 수 있다
numbers.sorted() { $0 > $1 }
numbers.sorted { $0 > $1 }
  • 이와 같이 후행 클로저를 쓸 경우 ()를 생략할 수 있다

Capturing Values

  • 맨 위에 서술했듯이 클로저는 값을 캡처할 수 있다
  • 값을 캡쳐한다는 의미는 특정 문맥에서 상수나 변수 값이 사라진다 하더라도 클로저 안에서 사용할 수 있는 것을 뜻한다
    func makeIncrementer(forIncrement amount: Int) -> () -> Int {
      var runningTotal = 0
      func incrementer() -> Int {
          runningTotal += amount
          return runningTotal
      }
      return incrementer
    }
  • 위 함수는 makeIncrementer함수 내에서 incremented이라는 중첩함수가 있는 형태이다
  • makeIncrementer라는 함수의 반환값은 () -> Int형태를 가진 클로저를 반환하며 코드내에서 확인해보면 incrementer을 반환하는 모습을 볼 수 있다

increment

func incrementer() -> Int {
    runningTotal += amount
    return runningTotal
}
  • 중첩함수인 incrementrunningTotalamount를 더하는 모습을 보여주는데 함수 내에는 이 두가지 변수가 선언도 되어있지않고, 파라미터로 받는 형태도 아니지만 해당 함수는 동작을 한다
  • 이는 makeIncrementer내에서 runningTotal변수와 amount인자가 캡쳐되었기 때문이다
let incrementByOne = makeIncrementer(forIncrement: 1)

print(incrementByOne())    // 1
print(incrementByOne())    // 2
print(incrementByOne())    // 3
print(incrementByOne())    // 4
  • 위 함수를 실행 할 시 runningTotalamount가 캡쳐되어 이전의 내용이 저장되어 출력되는 모습을 확인할 수 있다
let incrementByTen = makeIncrementer(forIncrement: 10)

print(incrementByTen())    // 10
print(incrementByOne())    // 5
print(incrementByTen())    // 20
  • 새로운 클로저를 만들 경우에는 서로 다른 runningTotalamount를 가지고 있으므로 서로 다른 값이 나오게 된다
  • 이는 클로저가 클래스와 같은 참조타입이므로 다음과 같이 동작할 수 있다는 것을 나타낸다

Closures are reference types

  • 위에도 서술했듯이 클로저는 참조타입이다
  • 그렇기 때문에 incrementByOneincrementByTen은 서로 다른 클로저이며, 힙에 할당된 각각의 클로저 주소를 가지고 있다
  • 또한 힙에 할당된 두 클로저는 값 캡쳐와 같이 힙에 할당되어 있기 때문에 지속적으로 업데이트 되며, 사용할 수 있다.

Escaping Closures

  • 클로저를 함수의 파라미터로 넣을 수 있다는 것은 위의 sorted 혹은 자주 사용하는 map, filter와 같은 메소드를 통해서도 알 수 있다.
  • 다만 이런 클로저가 함수가 종료된 이후에 실행된다면 해당 클로저는 함수 파라미터 앞에 @escaping 어노테이션을 붙여줘야한다
var completionHandlers: [() -> Void] = []

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

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

class SomeClass {
    var x = 10

    func doSomething() {
        someFunctionWithEscapingClosure { self.x = 100 }
        someFunctionWithNonescapingClosure { x = 200 }
    }
}

let instance = SomeClass()

instance.doSomething()
print(instance.x)

completionHandlers[0]()
print(instance.x)
  • 위 함수를 보면 escaping을 사용하는 함수의 경우 함수가 끝난 이후 클로저가 실행되며, escaping을 사용하지 않는 함수는 클로저를 함수 내부에서 사용이후 종료가 된다
  • SomeClass에서의 프로퍼티 x를 이스케이핑 클로저에서 사용하기 위해서는 self키워드를 붙여줘야한다
  • self키워드를 붙이지 않을 경우 Reference to property ‘x’ in closure requires explicit use of ‘self’ to make capture semantics explicit와 같은 오류가 난다
  • 이스케이핑 클로저에서 캡처를 사용하기 위해서는 명시적으로 self를 붙여야 한다는 에러이다

Autoclosures(자동 클로저)

  • 자동 클로저는 파라미터가 없으며 특정 구문을 통해 다른 함수에 전달인자로 사용 가능한 클로저이다
var customerInLine = ["KDB", "Rodri", "Ake", "Sliva", "Jack"]

let customerProvider = { customerInLine.remove(at: 0) }
print(customerInLine.count)    // 5
print(customerInLine[0])    // KDB
  • 위 구문은 클로저를 지났기 때문에 customerProvider에 “KDB”가 들어있고, customerProvider의 경우 개수가 4가 되어야 할 것 같지만 클로저는 실행되지 않는다
  • 이렇게 자동 클로저는 실행하기 전까지 실행되지 않는다
func printFirstCustomer(customers: () -> String) {
    print(customers())
}

printFirstCustomer(customers: customerProvider)    // KDB
  • 위와 같이 클로저를 전달 할 수도 있지만 @autoclosure을 사용하여 밑 코드와 같이 사용도 가능하다
func printFirstCustomer(customers: @autoclosure () -> String) {
    print(customers())
}

printFirstCustomer(customers: customerInLine.remove(at: 0))    // KDB
반응형

'Swift' 카테고리의 다른 글

Swift 컴파일, 컴파일러  (0) 2023.02.14
ARC-간단정리  (0) 2023.02.07
정규표현식(Swift)  (0) 2023.01.29
KVC(Key-value coding), KVO(Key-value observing)  (0) 2023.01.22
Opaque Types(불투명 타입) 간단하게 훑어보기  (0) 2023.01.21