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 |