본문 바로가기

swift

Closure 클로저

https://docs.swift.org/swift-book/documentation/the-swift-programming-language/closures/

 

 

combine의 sink라는 메서드가 변수나 상수에 할당이 안되면 내부 클로저의 구문이 실행이 안돼서 의문을 가지고 closure를 찾아보게 되었다.

Closure

  • Closures can capture and store references to any constants and variables from the context in which they’re defined.
  • closure는 선언된 위치에서 해당 상수와 변수에 대해 reference를 capture한다. 이것을 상수와 변수에 대한 클로징이라고 한다.
    • 그렇다면 capture 때문에 메모리에 부담이 가는 것이 아닌가??
    • swift는 모든 메모리를 스스로 관리한다.
  • 클로저가 올라가는 메모리 알아보자.

closure의 3가지 형태

  • 이름이 있고 어떤 값도 capture 하지 않는 전역함수의 형태
  • 이름이 있고 다른 함수 내부의 값을 capture가 가능한 중첩함수의 형태
  • 이름이 없고 주변 문맥에 따라 값을 capture 할 수 있는 축약 문법으로 작성한 형태

Closure의 표현 방법

  • 클로저는 매개변수와 반환 값의 타입을 문맥을 통해 유추할 수 있기 때문에 매개변수와 반환 값의 타입을 생략할 수 있다
  • 클로저에 단 한 줄의 표현만 들어있다면 암시적으로 이를 반환 값으로 취급한다.
  • 축약된 전달인자 이름을 사용할 수 있다.
  • 후행 클로저 문법 사용 가능

기본 closure

swift에는 배열의 값을 정렬하기 위한 sorted(by:)라는 메서드가 존재한다.

  • sorted는 클로저를 통해 어떻게 정렬할 것인가에 대한 정보를 받아 처리하고 결과를 배열로 반환한다.
    • 어떤 클로저를 통해 정렬 정보를 받나? (Element, Element) throws -> Bool)
  • 기존의 배열은 변경하지 않는다.

sorted(by:)

@inlinable public func sorted(by areInIncreasingOrder: (Element, Element) 
                                                                    throws -> Bool) rethrows -> [Element]

closure 사용 예시

func backward(_ s1: String, _ s2: String) -> Bool {
    return s1 > s2
}
var reversedNames = names.sorted(by: backward)
  • 함수가 클로저 부분에 들어가는 것이 가능한 이유 → 함수도 클로저의 종류기 때문

Closure 표현식

{ (매개변수) -> 반환타입 in
        실행코드
}
  • 클로저의 표현식이다.
  • 함수와 마찬가지로 입출력 매개변수를 사용이 가능하다.
  • 매개변수 기본값을 사용이 불가능하다.
  • 매개변수의 이름을 지정한다면 가변 매개변수를 사용이 가능하다.
  • 가변 매개변수란????
    • 0개 이상의 값을 인자로 받을 수 있는 매개변수
    • func aa(nums: Int...) { print(nums) // [1, 2, 3, 4, 5] print(nums[0]) // 1 } aa(nums: 1,2,3,4,5)
  • 튜플도 매개변수 타입으로 사용이 가능하고 반환값으로도 사용이 가능하다.
reversedNames = names.sorted(by: { (s1: String, s2: String) -> Bool in
    return s1 > s2
})
  • 아까와 동일한 backward함수의 내용을 직접 작성한 것
  • { (s1: String, s2: String) -> Bool in return s1 > s2 }
    • 메서드의 전달 인자로 전달하는 클로저 = 인라인 클로저
      • 함수가 아닌 그냥 클로저로 전달하는 형태?
    • 메서드에서 요구하는 형태로 전달해야한다.

Closure 표현 생략하기

func sorted(by areInIncreasingOrder: (Element, Element) throws -> Bool)
  • 기존의 sorted코드가 이렇게 요구를 하기 때문에 그 형태에 맞춰 타입을 명시 안하고 값을 작성 해줘도 된다.
reversedNames = names.sorted(by: { s1, s2 in return s1 > s2 } )
reversedNames = names.sorted(by: { s1, s2 in s1 > s2 } )
  • 클로저 내부에 실행문이 한 줄이면 return을 생략해도 된다.

swift는 inline closure에 약식 인수 이름을 자동으로 제공한다.

reversedNames = names.sorted(by: { $0 > $1 } )
  • 그렇기 때문에 이렇게 쓰는 것이 가능하다.
public func <T : Comparable>(lhs: T, rhs: T) -> Bool

public func sorted(by areInIncreasingOrder: (Element, Element) 
                                                                    throws -> Bool) rethrows -> [Element]
  • 연산자 > 를 구현한 함수의 모양과 클로저 sorted 의 매개변수 타입과 반환타입이 일치한다.
    • 즉, 연산자 > 의 함수 내용이 sorted 의 매개변수에 들어가는 것이고, 클로저에서는 매개변수 타입, 반환 타입, 매개변수 목록, return 키워드, in 키워드 모두가 생략 가능하기 때문에
    • → 연산자만 표기하더라도 알아서 연산하고 반환한다.
reversedNames = names.sorted(by: >)
  • 그렇기 때문에 최종적으로 이렇게 작성하는 것이 가능하다.

후행 클로저

  • xcode에서 자동완성으로 enter누르면 작성 하던 클로저가 후행 클로저다.
  • 단, 후행 클로저는 맨 마지막에 전달인자로 전달되는 클로저에만 해당된다.
  • 매개변수에 여러 클로저가 있는 경우에는 다중 후행 클로저로 사용이 가능하다.
    • swift 5.3에서는 맨 마지막만 가능했다.
    • 지금은 상관이 없다.

매개변수(parameter): 함수를 호출 했을 때 값이 저장되는 변수 add(x: 3, y: 9) → x,y

전달인자(argument): 함수를 호출 했을 때 변수에 들어가는 값 add(x: 3, y: 9) → 3, 9

후행 클로저 사용

  • 후행 클로저 사용
func someFunctionThatTakesAClosure(closure: () -> Void) { }

// 후행 클로저 사용
someFunctionThatTakesAClosure(closure: { })

//클로저의 이름을 생략이 가능하다.
someFunctionThatTakesAClosure() { }

//1개의 클로저만 전달인자로 전달하는 경우는 ()도 생략이 가능하다.
someFunctionThatTakesAClosure { }
  • 다중 후행 클로저 사용
func doSomething(do: (String) -> Void, 
                                    onSuccess: (Any) -> Void, 
                                    onFailure: (Error) -> Void) {
}

doSomething { (someString: String) in
    //do closure
} onSuccess: { (result: Any) in

} onFailure: { (error: Error) in

}
  • 또 이때는 첫번째 클로저만 이름을 명시 안해도 된다.

Capture

  • 클로저는 자신이 정의된 위치의 주변 문맥을 통해 상수나 변수를 capture를 할 수 있다.
    • caputure를 통해 주변에 없어도 해당 상수, 변수의 값을 closure 내부에서 참조하거나 수정이 가능
    • 상수와 변수를 정의한 원래의 범위(original scpoe)에 존재하지 않아도 가능

❓ 캡처를 해야 하는 이유??? → 클로저가 비동기 작업에 많이 사용된다. 현재의 상태를 미리 획득해두지 않으면 실제로 클로저의 기능을 실행하는 순간에 상수와 변수가 메모리에 존재하지 않는 경우가 발생한다.

  • 중첩함수는 값을 capture를 할 수 있는 가장 간단한 형태의 클로저
    • 중첩 함수: 다른 함수 내부에서 사용된 함수
    • 중첩 함수는 외부 함수의 모든 인수(지역변수, 지역상수)를 캡처할 수 있다.

예시)

func makeIncrementer(forIncrement amount: Int) -> () -> Int {
    var runningTotal = 0
    func incrementer() -> Int {
                //runningTotal과 amount에 대한 참조를 캡처한다.
        runningTotal += amount
        return runningTotal
    }
    return incrementer
}
  • 반환 타입이 () → Int 함수를 반환한다는 의미
  • 결국은 icrementer의 반환값을 반환한다는 의미
  • 원래는 incrementer 함수만 존재한다면 runningTotal과 amount를 모르기 때문에 틀린 함수다.
  • 하지만 중첩 함수의 형태로 있다면 함수의 실행이 끝나도 참조는 사라지지 않기 때문에 호출될 때마다 사용하던 runnigTotal을 계속해서 사용할 수 있다.

⚠️ 최적화를 위해 swift가 클로저에 의해 값이 변경되지 않고 클로저가 생성된 후에도 값이 변경되지 않는 경우에는 값의 복사본을 캡처하여 저장한다. - 원래는 값의 reference를 캡처한다.

  • 값의 복사본을 캡처한다?? → 클로저가 외부 변수를 복사하여 소유하고 있기 때문에 클로저 내에서의 변경이 외부에 영향을 미치지 않는다.
  • 상수와 변수의 참조를 캡처한다??
  • → 클로저가 외부 변수에 대한 참조를 유지하므로 클로저 내에서의 변경이 외부에 반영된다.
  • 변수가 더 이상 필요하지 않을 때, 변수를 폐기하는 것과 관련된 모든 메모리 관리도 Swift에서 처리한다.

예시)

let incrementByTwo: (() -> Int) = makeIncrementer(forIncrement: 2)
let incrementByTwo2: (() -> Int) = makeIncrementer(forIncrement: 2)
let incrementByTen: (() -> Int) = makeIncrementer(forIncrement: 10)

let first = incrementByTwo()   //2
let second = incrementByTwo()  //4
let third = incrementByTwo()   //6

let first2 = incrementByTwo2()  //2
let second2 = incrementByTwo2() //4
let third2 = incrementByTwo2()  //6

let first10 = incrementByTen()  //10
let second10 = incrementByTen() //20
let third10 = incrementByTen()  //30
  • 각각의 incrementer 함수는 언제 호출이 되더라도 자신만의 runningTotal 변수를 가지고 실행이 된다.

⚠️ 클래스 인스턴스의 프로퍼티로 클로저를 할당한다면 클로저는 해당 인스턴스의 멤버의 참조를 획득할 수있으나, 클로저와 인스턴스 사이에 강한참조 순환 문제가 발생할 수 있다. → 강한 참조 문제는 capture list를 통해 없앨 수 있다.

Closure는 Reference Type

  • incrementByTwo와 incrementByTen은 상수다.
  • 이 상수가 참조하는 클로저는 여전히 캡처한 값을 증가 시키는 것이 가능하다.
  • → 함수와 클로저는 reference type이기 때문이다.
  • 함수나 클로저를 상수, 변수에 할당할 때마다 실제로는 해당 상수, 변수가 함수나 클로저에 대한 참조가 되도록 설정하는 것이다.
  • → 함수,클로저를 상수, 변수에 할당 = 상수, 변수가 함수나 클로저를 참조한다.
  • 예제에서 incrementByTen을 할당하는 것은 클로저의 내용물을 할당하는 것이 아니다.
  • 클로저에 대한 참조를 할당하는 것이다.
let alsoIncrementByTen = incrementByTen
alsoIncrementByTen()  //40
incrementByTen()      //50
  • 이런식으로 하면 동일한 클로저를 참조하기 때문에 동일한 runningTotal의 숫자를 올리게 된다.

Escaping Closure : 탈출 클로저

  • 초기에 클로저를 공부하게 된 이유다.
  • 함수의 전달인자로 전달한 클로저가 함수 종료 후(return 호출) 에 호출될 때 클로저는 함수를 escape 한다.
  • 매개변수 이름뒤에 ( : 뒤에) @escaping 키워드를 사용해 해당 클로저가 탈출한다고 명시해야 한다.
    • 매개변수 경우에는 @escaping 명시해야한다.
    • 리턴값으로 클로저를 리턴할 경우에는 자연스럽게 함수의 실행이 끝난 후에 클로저가 호출 될 수 있기 때문에 @escaping을 명시하지 않아도 된다.
  • 비동기 작업을 실행하는 함수들은 컴플리션 핸들러(completion handler) 전달인자로 받아오고, 비동기 함수가 종료되고 난 후 호출할 필요가 있는 클로저를 사용해야 할 때 escaping 클로저가 필요하다.
    • 비탈출 클로저는 @escaping 키워드를 따로 명시하지 않은 클로저
    • 매개변수로 사용되는 클로저는 기본으로 비탈출 클로저다. 함수로 전달된 클로저가 함수의 동작이 끝난 후 사용할 필요가 없을 때 비탈출 클로저를 사용한다.
    • 예시) sorted(by:)
  • escaping 클로저가 함수를 탈출할 수 있는 경우
    • 함수의 전달인자로 전달받은 클로저를 다시 반환해야하는 경우
    • typealias VoidVoidClosure = () -> Void
      let firstClosure: VoidVoidClosure = {
          print("ClosureA")
      }
      let secondClosure: VoidVoidClosure = {
          print("ClosureB")
      }
      
      //first와 second는 함수의 반환 값으로 사용될 수 있기 때문에 탈출 클로저이다.
      func returnOneClosure(first: @escaping VoidVoidClosure, 
              second: @escaping VoidVoidClosure, 
              shouldResturnFirstClosure: Bool) 
              -> VoidVoidClosure {
          return shouldResturnFirstClosure ? first : second
      }
      
      
       var completionHandlers: [() -> Void] = []
          func someFunctionWithEscapingClosure(completionHandler: @escaping () -> Void) {
            completionHandlers.append(completionHandler)
          }
  •   함수 외부에 정의된 변수나 상수에 클로저가 저장되어, 함수가 종료된 후에 사용할 경우
    • 비동기 작업에서 컴플리션 핸들러로 클로저를 받는 함수에서, 함수는 연산을 시작한 후에 리턴을 하지만 연산이 완료되기 전까지 클로저는 호출되지 않는다
    • 연산을 하고나서 함수가 리턴한 후에 클로저가 호출된다.
    • 함수가 끝나고 escaping 클로저가 실행된다.

 

// 함수에서 반환한 클로저가 함수 외부에 저장됨
let returnedClosure: VoidVoidClosure = returnOneClosure(first: firstClosure, second: secondClosure, shouldResturnFirstClosure: true)
returnedClosure()  // ClosureA

// closure 매개변수 클로저는 함수 외부의 변수에 저장될 수 있으므로 탈출 클로저이다.
var closures: [VoidVoidClosure] = []
func appendClosure(closure: @escaping VoidVoidClosure) {
        // 전달인자로 전달 받은 클로저가 함수 외부의 변수 내부에 저장됨
    closures.append(closure)
}
  • escaping 클로저는 클로저 내부에서 해당 타입의 프로퍼티, 메서드, 서브스크립트에 접근을 하려면 self 키워드를 사용해야한다.
    • 일반 클로저는 클로저 내부에서 self키워드를 사용안해도 된다.
    • 탈출 클로저는 값 획득을 위해서 self키워드를 사용해 프로퍼티등에 접근해야한다.
    • 사용할때는 명시적으로 self를 사용하거나, 클로저의 캡처 리스트에 self를 포함해야한다.
    • someFunctionWithEscapingClosure {
      // 명시적으로 self 사용
      print(self.value)
      }
      
      someFunctionWithEscapingClosure { [self] in
      // 캡처 리스트에 self 포함
      print(value)
      }
  • self가 구조체나 열거형의 인스턴스인 경우, 항상 암시적으로 self를 참조할 수 있다.
  • escaping 클로저는 self가 구조체나 열거형의 인스턴스인 경우 자체에 대한 가변 참조를 포착할 수 없다.
    • 변경이 가능한 참조를 포착하는게 불가능하다???
    • 구조체나 열거형의 인스턴스 내부에서 self를 통해 인스턴스의 상태를 변경하는 것이 불가능하다.
struct SomeStruct {
    var x = 10
    mutating func doSomething() {
        someFunctionWithNonescapingClosure { x = 200 }  // Ok
        someFunctionWithEscapingClosure { x = 100 }     // Error
    }
}

withoutActuallyEscaping

  • 비탈출 클로저로 전달한 클로저가 탈출 클로저인 척 해야하는 경우
  • 실제로는 탈출하지 않는데 다른 함수에서 탈출 클로저를 요구하는 상황
// lazy 컬렉션은 비동기 작업을 할 때 사용 - filter 메서드는 탈출 클로저를 요구함 -> 오류
func hasElements(in array: [Int], match predicate: (Int) -> Bool) -> Bool {
       return (array.lazy.filter { predicate($0) }.isEmpty == false)
}
  • 오류가 발생하는 코드다.
  • hasElements는 비탈출 클로저를 전달 받는다
  • 내부에 lazy컬렉션에 있는 filter 메서드의 매개변수로 비탈출 클로저를 요구한다.
  • 하지만 lazy컬렉션은 비동기 작업때 사용을 하기 때문에 filter메서드 매개변수로 탈출 클로저를 요구 한다.
  • 비탈출 클로저를 탈출 클로저처럼 사용이 가능하게 만드는 withoutActuallyEscaping 함수가 존재한다.
let numbers: [Int] = [2, 4, 6, 8]
let evenNumberPredicate = { (number: Int) -> Bool in
    return number % 2 == 0
}
let oddNumberPredicate = { (number: Int) -> Bool in
    return number % 2 == 1
}

func hasElements(in array: [Int], match predicate: (Int) -> Bool) -> Bool {
    return **withoutActuallyEscaping**(predicate, do: { escapablePredicate in
        return (array.lazy.filter { escapablePredicate($0) }.isEmpty == false)
    })
}

let hasEvenNumber = hasElements(in: numbers, match: evenNumberPredicate)
let hasOddNumber = hasElements(in: numbers, match: oddNumberPredicate)

print(hasEvenNumber)  //true
print(hasOddNumber)   //false
  • withoutActuallyEscaping(_: do:)함수의 첫 번째 전달인자로 탈출 클로저인 척해야 하는 클로저가 전달되고, do전달인자로 실제로 작업을 실행할 탈출 클로저를 전달한다.

Autoclosure: 자동 클로저

  • 자동 클로저는 전달인자를 갖지 않는다
  • 자동 클로저를 과도하게 사용하면 코드를 이해하기 어려워짐, context와 함수의 이름을 통해 계산이 지연됨을 명확히 알 수 있어야 한다.
    • 자동 클로저는 호출 시 자신이 감싸고 있는 코드의 결과값을 반환한다.
    • 일반 표현의 코드를 클로저 표현의 코드로 만들어주는 역할
    • 소괄호, 중괄호 생략가능함수의 전달인자로 전달하는 표현을 자동으로 변환해주는 클로저를 자동 클로저라고 한다.
// @autoclosure를 사용하지 않는 경우
func normalPrint(_ closure: () -> Void) {
	closure()
}
normalPrint({ print("no autoclosure") })

//@autoclosure를 사용한 경우
func autoClosurePrint(_ closure: @autoclosure () -> Void) {
	closure()
}
autoClosurePrint(print("autoClosure"))
  • 위와 같이 자동 클로저를 직접 호출하여 사용하는 경우는 흔한 경우가 아니다.
  • 예를 들어 assert(condition:message:file:line:) 함수는 condition과 message 매개변수가 자동 클로저다. condition 매개변수는 디버그용 빌드에서만 실행되고 message 매개변수는 condition 매개변수가 false일 때만 실행된다

 

func serveCustomerWithAutoClosure(_ customerProvider: @autoclosure () -> String) {
    print("자동 클로저 실행: \(customerProvider())")
}
//자동 클로저의 매개변수는 클로저 대신 실행결과 타입을 받는다.
serveCustomerWithAutoClosure(customersInLine.removeFirst())
  • 자동 클로저는 클로저가 호출되기 전까지 클로저 내부의 코드가 동작하지 않는다.
    • 따라서 연산을 지연시키는 것이 가능하고 이 과정은 연산에 자원을 많이 소모하거나 부작용이 우려될 때 유용하게 사용하는 것이 가능하다.
    • 코드의 실행을 제어하기 때문이다.예시) 함수의 전달인자로 전달하는 클로저
    • 예시) 일반적인 클로저를 이용한 연산 지연 
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"

 

  • 예시) 오토 클로저를 사용한 예시
// 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를 사용하면서 클로저 대신 문자열 인수를 받는 것처럼 함수를 호출하는 것이 가능하다.
  • 그러면 {}삭제 말고 이점이 뭐지??
  • escaping과 autoclosure를 모두 사용하려면 @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' 카테고리의 다른 글

WWDC 16 Understanding Swift Performance ( 2 / 2 )  (0) 2024.06.21
WWDC 16 Understanding Swift Performance (1/2)  (0) 2024.06.19
Initialization 초기화  (0) 2024.05.25