GGURUPiOS
Swift 공식 문서 정리 - (7) Closure 본문
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!"
'Swift > 공식문서 정리 ( 문법 )' 카테고리의 다른 글
Swift 공식 문서 정리 - (9) Structures and Classes (0) | 2023.04.27 |
---|---|
Swift 공식 문서 정리 - (8) Enumerations (0) | 2023.04.27 |
Swift 공식 문서 정리 - (6) Functions (1) | 2023.04.19 |
Swift 공식 문서 정리 - (5) Control Flow (0) | 2023.04.19 |
Swift 공식 문서 정리 - (4) Collection Types (0) | 2023.04.19 |