일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 | 30 | 31 |
- url
- Protocol
- enum
- delegate
- 스위프트
- IOS
- extension
- 이니셜라이저
- UIKit
- optional
- type
- 디자인패턴
- Method
- Foundation
- String
- Terminal
- Swift
- initializer
- init
- 코딩테스트
- property
- Unicode
- tuist
- instance
- Class
- struct
- Git
- interpace
- Xcode
- initalizer
- Today
- Total
아리의 iOS 탐구생활
[Swift] Closure에 대해 알아보자. 본문
클로저(Closure)는 코드의 블록이다. 함수는 ‘이름이 있는 클로저’라고 표현한다.
보통 클로저는 ‘이름이 없는 코드블록’을 클로저라고 한다.
클로저는 일급시민(first-citizen)으로 전달인자, 변수, 상수 등으로 저장, 전달이 가능하다.
🤔 일급시민(first-citizen)이란?
- 변수나 데이터 구조안에 담을 수 있다
- 파라미터로 전달할 수 있다
- 반환값(return value)으로 사용할 수 있다.
- 할당에 사용된 이름과 관계없이 고유한 구별이 가능하다.
- 동적으로 프로퍼티 할당이 가능하다.
🔍 Closure의 유형
Global function
우리가 흔히 알고 있는 함수이다. 클래스 밖의 함수라고 할 수 있다.
Nested function
중첩함수라고 말한다. 함수 내부에서 다시 함수를 정의해서 사용하는 함수이다.
외부에는 숨겨져 있고 선언된 함수 내부에서만 호출이 가능하다.
func chooseStepFunction(backward: Bool) -> (Int) -> Int {
func stepForward(input: Int) -> Int { return input + 1 }
func stepBackward(input: Int) -> Int { return input - 1 }
return backward ? stepBackward : stepForward
}
Closure expressions
이것이 흔히 클로저라고 불리는 유형이다.
✍🏻 기본 클로저 문법
{ (매개변수) -> 반환타입 in
실행코드
}
🔍 클로저의 변수 할당
클로저는 일급시민이기 때문에 변수에 할당될 수 있다.
let closureValue = { (name: String) in
print(name)
}
closureValue("Ari Lee")
또한 함수의 인자 값으로도 전달될 수 있다.
func closureOperation(then closure: () -> Void) {
// Some code
}
🔍 클로저 축약하기
Swift에서의 클로저는 다른 언어에서보다 유연하다
그래서 보다 자유롭게 축약하거나 변형할 수 있다.
// 이랬던 코드가
let A = [5,3,4,2,6].sorted { a, b in
return a < b
}
// 이렇게 축약된다.
let B = [5,3,4,2,6].sorted { $0 < $1 }
- 후행 클로저를 사용해 함수의 파라미터를 축약할 수 있다.
- $0과 같은 인자 값을 사용해 첫 번째 인자 값으로 대체할 수 있다.
- 단일 표현 클로저에서는 return키워드를 생략할 수 있다.
🔍 Trailing Closure
함수의 마지막 파라미터로 클로저를 전달할 경우 표현할 수 있는 방식이다.
let numbers = [1, 2, 3, 4, 5]
// map 함수의 파라미터로 클로저가 전달된다.
let negativeNumbers = numbers.map({ -$0 }) // [-1, -2, -3, -4, -5]
// 함수의 마지막 파라미터로 클로저가 전달되면 괄호 밖으로 빼서 표현할 수 있다.
let floatNumbers = numbers.map() { Double($0) * 1.0 } // [1.0, 2.0, 3.0, 4.0, 5.0]
// 클로저가 유일한 파라미터라면 괄호를 제거해도 된다.
let stringNumbers = numbers.map { String($0) } // ["1", "2", "3", "4", "5"]
🔍 Capturing Values
Closure는 주변의 value를 포착(capture)한다.
캡쳐한 상수 및 변수는 나중에 없어지더라도 클로저 내에서 값을 참조하고 있기 때문에 접근하여 수정할 수 있다.
Swift에서 값을 캡쳐하는 가장 단순한 형태는 중첩 함수다.
중첩 함수는 외부 함수의 인자와 외부 함수에서 정의한 상수 및 변수를 캡쳐할 수 있다.
func makeIncrementer(forIncrement amount: Int) -> () -> Int {
var runningTotal = 0
func incrementer() -> Int {
runningTotal += amount
return runningTotal
}
return incrementer
}
incrementer 함수만 떼어놓고 보면,
func incrementer() -> Int {
runningTotal += amount
return runningTotal
}
incrementer 함수 내부에는 없는 runningTotal과 amount를 사용하고 있다.
즉 외부에서 선언된 변수를 캡쳐해서 사용하고 있다는 것이다.
그래서 다음과 같이 코드를 반복 실행하면 값이 계속 증가하게 된다.
let incrementByTen = makeIncrementer(forIncrement: 10)
incrementByTen()
// returns a value of 10
incrementByTen()
// returns a value of 20
incrementByTen()
// returns a value of 30
클래스 인스턴스 속성(propertry)에 클로저를 할당하고 클로저가 인스턴스 및 인스턴스의 멤버를 참조하면, 캡쳐링이 발생하면서 클로저와 인스턴스 사이에 강한 순환 참조가 생성된다. 이런 경우, 인스턴스가 없어져도 강한 참조가 해제되지 않기 때문에 메모리 사이클이 발생한다. 이 문제를 해결하기 위해서는 캡쳐 리스트(capture list)를 사용해야 한다.
🔍 Escaping Closure
클로저를 함수의 파라미터로 전달될 때 전달된 클로저가 함수가 끝나고 함수 외부에서 실행될 수 있다.
예를 들면 비동기 작업을 하는 함수에서 완료 핸들러로 클로저 파라미터를 사용하는데 이때 클로저는 작업이 완료되고 함수 외부에서 호출하게 된다. 이 경우에는 클로저 앞에 @escaping 키워드를 명시해야 컴파일 오류가 발생하지 않는다.
일반적으로 클로저는 내부에서 사용되는 변수를 암시적으로 캡쳐하게 된다. 하지만 escaping 클로저의 경우 사용되는 변수를 명시적으로 표현해줘야 한다. 만약 self를 캡쳐하려면 self를 사용할 때 직접 명시해주거나, 캡쳐 리스트(capture list)에 포함하여 캡쳐한다는 의도를 명확하게 보여줘야 한다.
var completionHandlers: [() -> Void] = []
func someFunctionWithEscapingClosure(completionHandler: @escaping () -> Void) {
completionHandlers.append(completionHandler)
}
func someFunctionWithNonescapingClosure(closure: () -> Void) {
closure()
}
class SomeClass {
var x = 10
func doSomething() {
// self를 명시적으로 표현해야 에러가 발생하지 않음.
someFunctionWithEscapingClosure { self.x = 100 }
// self를 암시적으로 사용할 수 있어 self를 명시하지 않아도 됨.
someFunctionWithNonescapingClosure { x = 200 }
}
}
let instance = SomeClass()
instance.doSomething()
print(instance.x)
// Prints "200"
completionHandlers.first?()
print(instance.x)
// Prints "100"
캡쳐 리스트에 self를 추가하여 사용하는 예제
class SomeOtherClass {
var x = 10
func doSomething() {
// capture list에 self 추가하여 캡처
someFunctionWithEscapingClosure { [self] in x = 100 }
someFunctionWithNonescapingClosure { x = 200 }
}
}
🔍 AutoClosure
파라미터 값이 없으며 특정 표현을 감싸서 다른 함수의 파라미터로 사용할 수 있는 클로저이다.
AutoClosure를 사용하면 클로저가 호출될 때 까지 코드가 실행되지 않는다.
이런 코드 지연의 특성을 이용하면 실행 시점을 제어할 수 있기 때문에 부작용을 줄이고 복잡한 연산이 필요할 때 유용하다.
아래는 코드가 지연되어 사용되는 예제이다.
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"
AutoClosure를 함수의 파라미터로 전달하는 예제
/ 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을 반환하는 클로저를 파라미터로 받는다.
보통 클로저를 전달할 때 {}(중괄호)를 붙여야 하지만 @autoclosure 키워들르 명시해주면 중괄호 없이 클로저를 전달할 수 있다.
아래는 @autoclosure 키워드를 사용한 예제다.
// 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!"
'Swift > 문법' 카테고리의 다른 글
[Swift] 오류처리(Error Handling)는 어떻게 할까? (0) | 2021.08.25 |
---|---|
[Swift] Protocol 6) 프로토콜의 확장을 알아보자 (0) | 2021.08.18 |
[Swift] Protocol 5) 타입에 대하여... (0) | 2021.08.18 |
[Swift] Protocol 4) 이니셜라이저의 대한 요구사항 (0) | 2021.08.18 |
[Swift] Protocol 3) 메소드의 대한 요구사항 (0) | 2021.08.18 |