🌟 클로저를 잘 이해해야 스위프트의 함수형 프로그래밍 패러다임 스타일을 명확하게 이해할 수 있음
🌟 클로저, 제네릭, 프로토콜, 모나드 등이 결합해서 스위프트는 강력한 언어가 됨.
🌟 C, Object-C의 블록 또는 다른 언어의 람다와 유사함.
🌟 클로저(closures) : 일정 기능을 하는 코드를 하나의 블록으로 모은 것
🌟 함수는 클로저의 한 형태, 이름이 있는 클로저
let names = ["Chris", "Alex", "Ewa", "Barry", "Daniella"]
//sorted에 backward함수를 전달인자로 보내기
func backward(_ s1: String, _ s2: String) -> Bool {
return s1 > s2
}
var reversedNames = names.sorted(by: backward)
//reversedNames is equal to ["Ewa", "Daniella", "Chris", "Barry", "Alex"]
sorted(by: ) : true를 반환하면 첫 번째 전달인자가 두 번째 전달인자보다 앞에 옴➕ 함수를 메서드의 전달인자로 보내는 일은 함수형 프로그래밍 패러다임에서는 아주 당연한 일
{( parameters ) -> return type in
statements
}
general form : 인자로 넣을 parameters, 인자 값으로 처리할 내용을 기술하는 statements 그리고 return type//backward대신 클로저를 전달하기
reversedNames = names.sorted(by: { (s1: String, s2: String) -> Bool in
return s1 > s2
})
//바디가 짧으니 한 줄로 표현 가능
reversedNames = names.sorted(by: { (s1: String, s2: String) -> Bool in return s1 > s2 } )
in 다음 시작inline closure : 함수로 따로 정의된 형태가 아닌 클로저reversedNames = names.sorted(by: { s1, s2 in return s1 > s2 } )
sorted(by:)의 메소드에서 이미 (String, String) -> Bool 타입의 인자가 들어와야 하는지 알기 때문에 클로저에서 이 타입들은 생략 될 수 있음//반환 키워드 생략, 모호성 없음
reversedNames = names.sorted(by: { s1, s2 in s1 > s2 } )
reversedNames = names.sorted(by: { $0 > $1 } )
$와 숫자의 조합으로 표현in 키워드 사용할 필요 없음reversedNames = names.sorted(by: >)
Swift의 String 타입 연산자에는 String끼리 비교할 수 있는 비교 연산자(>) 를 구현> 자체가 함수 이름🌟 만약 함수의 마지막 인자로 클로저를 넣고 그 클로저가 길다면 후위 클로저를 사용할 수 있음
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() { $0 > $1 }
reversedNames = names.sorted { $0 > $1 }
후위 클로저를 사용하면 괄호() 생략 가능//후위 클로저를 이용해 숫자(Int)를 문자(String)로 매핑(Mapping)
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]
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"]가 됨.
map(_:) 메소드를 이용해 특정 값을 다른 특정 값으로 매핑하는 할 수 있는 클로저를 구현number값은 상수인데, 이 상수 값을 클로저 안에서 변수 var로 재정의 했기 때문에 number값의 변환이 가능. 기본적으로 함수와 클로저에 넘겨지는 인자 값은 상수🚨 digitNames[number % 10]!에 뒤에 느낌표(!)가 붙어있는 것은 사전(dictionary)의 subscript는 옵셔널이기 때문. 사전에서 특정 key에 대한 값은 있을 수도 있고 없을 수도 있기 때문에 논리적으로 당연.
🌟 클로저는 특정 문맥의 상수나 변수의 값을 캡쳐할 수 있음.(주변 문백을 통해 상수나 변수를 획득) 다시말해 원본 값이 사라져도 클로져의 body안에서 그 값을 활용 가능.
🌟 중첩 함수(nested function) : Swift에서 값을 캡쳐 하는 가장 단순한 형태
func makeIncrementer(forIncrement amount: Int) -> () -> Int {
var runningTotal = 0
func incrementer() -> Int {
runningTotal += amount
return runningTotal
}
return incrementer
}
(forIncrement amount: Int) : 인자 값() -> Int : 반환 값(함수 객체를 반환)//위에서 func incrementer 만 보자면
func incrementer() -> Int {
runningTotal += amount
return runningTotal
}
runningTotal과 amount가 없지만 이 함수는 돌아감.runningTotal과 amount가 캡쳐링 되서 그런 것.🚨 최적화 이유로 Swift는 만약 더 이상 클로저에 의해 값이 사용되지 않으면 그 값을 복사해 저장하거나 캡쳐링 하지 않음. Swift는 특정 변수가 더 이상 필요하지 않을 때 제거하는 것과 관련한 모든 메모리 관리를 알아서 처리.
//중첩함수 실행
let incrementByTen = makeIncrementer(forIncrement: 10)
incrementByTen()
// 값으로 10을 반환합니다.
incrementByTen()
// 값으로 20을 반환합니다.
incrementByTen()
// 값으로 30을 반환합니다.
makeIncrementer 내부의 incrementer 함수를 실행하는 메소드를 반환runningTotal과 amount가 캡쳐링 되서 그 변수를 공유하기 때문에 계산이 누적//새로운 클로저 생성
let incrementBySeven = makeIncrementer(forIncrement: 7)
incrementBySeven()
// returns a value of 7
runningTotal과 amount를 캡쳐링 해서 사용//이전 클로저 실행
incrementByTen()
// 값으로 40을 반환합니다.
🌟 incrementBySeven과 incrementByTen은 상수. 함수와 클로저는 참조 타입이기 때문에 runningTotal변수를 계속 증가 시킬 수 있음. 함수와 클로저를 상수나 변수에 할당할 때 실제로는 상수와 변수에 해당 함수나 클로저의 참조(reference)가 할당. 그래서 만약 한 클로저를 두 상수나 변수에 할당하면 그 두 상수나 변수는 같은 클로저를 참조(함수 포인터와 유사).
let alsoIncrementByTen = incrementByTen
alsoIncrementByTen()
// 50을 반환합니다.
🌟 클로저를 함수의 파라미터로 넣을 수 있는데, 함수 밖(함수가 끝나고)에서 실행되는 클로저 예를들어, 비동기로 실행되거나 completionHandler로 사용되는 클로저는 파라미터 타입 앞에 @escaping이라는 키워드를 명시해야 함.
var completionHandlers: [() -> Void] = []
func someFunctionWithEscapingClosure(completionHandler: @escaping () -> Void) {
completionHandlers.append(completionHandler)
}
completionHandler는 someFunctionWithEscapingClosure 함수가 끝나고 나중에 처리@escaping 키워드를 붙이지 않으면 컴파일 시 오류 발생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?()
print(instance.x)
// Prints "100"
@escaping 를 사용하는 클로저에서는 self를 명시적으로 언급해야 함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"
let customerProvider = { customersInLine.remove(at: 0) } 이 클로저 코드를 지났음에도 불구하고 customersInLine.count 는 변함없이 5 print("Now serving \(customerProvider())!") 이후에야 배열에서 값이 하나 제거되어 배열의 원소 개수가 4로 줄어듬자동 클로저는 적혀진 라인 순서대로 바로 실행되지 않고, 실제 사용될 때 지연 호출//자동클로저를 함수의 인자 값으로 넣는 예제
// 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 : 인자가 없고, String을 반환하는 클로저를 받는 함수함수 실행 : serve(customer: { customersInLine.remove(at: 0) } )와 같이 클로저{ customersInLine.remove(at: 0) }를 명시적으로 직접 넣을 수 있음// 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 키워드를 이용해서 보다 간결하게 사용serve함수의 인자를 받는 부분 customerProvider: @autoclosure ()에서 클로저의 인자()앞에 @autoclosure라는 키워드. 자동으로 클로저로 변환 serve(customer: { customersInLine.remove(at: 0) } ) : @autoclosure키워드를 사용했기 때문에 serve(customer: customersInLine.remove(at: 0)) 이렇게 {} 없이 사용. 정리 : 클로저 인자에 @autoclosure를 선언하면 함수가 이미 클로저 인것을 알기 때문에 리턴값 타입과 같은 값을 넣어줄 수 있음🚨 자동클로저를 너무 남용하면 코드를 이해하기 어려워 질 수 있음. 그래서 문맥과 함수 이름이 autoclosure를 사용하기에 분명해야 합니다.
//자동클로저@autoclosure, 이스케이프@escaping 같이 사용
// 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!"
collectCustomerProviders함수의 인자 customerProvider는 @autoclosure이면서 @escaping로 선언@autoclosure로 선언됐기 때문에 함수의 인자로 리턴값 String만 만족하는 customersInLine.remove(at: 0)형태로 함수 인자에 넣을 수 있음collectCustomerProviders함수가 종료된 후에 실행되는 클로저 이기 때문에 인자 앞에 @escaping 키워드