티스토리 뷰
대망의 클로져 입니다.
제일 많이 쓰이지만 아직도 어렵고 헷갈리는 그것.
공부해 봅시다.
클로저 (Closure)
클로저는 코드블럭으로 C와 Objective-C의 블럭(blocks), 다른 언어의 람다(lambdas)와 비슷 합니다.
어떤 상수나 변수의 참조를 캡쳐(capture)해 저장할 수 있습니다.
Swift는 캡쳐와 관련된 모든 메모리를 다룹니다.
전역 함수(Global Functions)와 중첩 함수(Nested Functions)는 클로저의 특별한 경우 입니다.
클로저는 세 가지 형태중 하나를 가집니다.
- 이름을 갖고 있고, 어떤 값들도 캡처하지 않는 클로저 (전역 함수)
- 이름을 갖고 있고 관련한 함수의 값 들을 캡처 할 수 있는 클로저 (중첩 함수)
- 경량화된 문법으로 쓰여지고 관련된 문맥으로 부터 값을 캡처할 수 있는 이름이 없는 클로저 (클로저 표현식)
Swift의 클로저 표현식은 최적화 되어 간결하고 명확합니다.
최적화에는 아래의 내용을 포함합니다.
- 문맥에서의 파라미터와 반환 값 타입 추론
- 단일 표현 클로저에서의 암시적 반환
- 축약된 인자 이름
- 후행 클로저 문법
클로저 표현 ( Closure Expressions )
클로저 표현은 코드의 명확성을 잃지 않으면서, 문법을 축약해 사용할 수 있는 최적화 방법을 제공 합니다.
아래는 sorted(by:) 메소드를 이용하여 여러 클로저 예시들을 보여줍니다.
각 예제는 동일한 기능을 보다 간결하게 표현합니다.
정렬 메소드 (The Sorted Method)
Swift의 표준 라이브러리에서는 sorted(by:) 라는 배열의 값을 정렬하는 메소드를 제공 합니다.
한번 정렬 프로세스가 끝이나면, sorted(by:) 메소드는 이전과 같은 형태지만 정렬된 새로운 배열을 반환하며, 기존에 있던 배열은 수정되지 않습니다.
아래는 String 으로 구성된 배열을 알파벳 역순으로 정렬해보는 예 입니다.
let names = ["Chris", "Alex", "Ewa", "Barry", "Daniella"]
sorted(by:) 메소드는 배열의 콘텐츠에서 같은 타입인 두개의 인자를 인자로 사용하며,
name의 배열의 컨텐츠는 String 타입. 즉, (String, String) -> Bool 의 타입을 갖는 클로저를 사용합니다.
클로저를 제공하는 방법중 하나는 위의 타입에 해당하는 함수를 만들어 sorted(by:) 메소드에 인자로 전달하는 것입니다.
func backward(_ s1: String, _ s2: String) -> Bool {
return s1 > s2
}
var reversedNames = names.sorted(by: backward)
print(reversedNames)
// ["Ewa", "Daniella", "Chris", "Barry", "Alex"]
코드 해석
backward의 첫번째 파라미터 (s1)이 두번째 파라미터 (s2) 보다 크면 true를 반환 하며,
알파벳 순으로는 A < Z 순이니 names 안에 E로 시작하는 Ewa가 가장 크니 제일 앞으로,
A로 시작하는 Alex가 가장 작으니 제일 뒤로 보내게 됩니다.
하지만 위의 코드를 더 줄일 수 있습니다.
클로저 표현 문법 (Closure Expression Syntax)
아래는 클로저의 형태 입니다.
{ (parameter) -> return type in
statements
}
In 을 기준으로
좌측: 파라미터, 리턴 타입
우측: 구현하고자하는 기능 (body)
이전에 사용했던 backward 클로저를 클로저 표현을 이용해 아래와 같이 바꿀 수 있습니다.
reversedNames = names.sorted(by: { (s1: String, s2: String) -> Bool in
return s1 > s2 // body
})
위와 같이 함수로 따로 정의된 형태가 아닌 인자로 들어가 있는 형태의 클로저를 인라인 클로저 라 합니다.
만약 body가 짧을 경우 한줄로 끝낼 수도 있습니다.
reversedNames = names.sorted(by: { (s1: String, s2: String) -> Bool in return s1 > s2 })
문맥에서 타입 추론 (Inferring Type From Context)
sorted(by:) 의 메소드에서 (String, String) -> Bool 타입의 인자가 들어와야 하는걸 알기 때문에 클로저에서 이 타입들은 생략 할 수 있습니다.
reversedNames = names.sorted(by: { s1, s2 in return s1 > s2 })
하지만 가독성과 코드의 모호성을 피하기 위해 타입을 명시하는걸 권장 합니다.
단일 표현 클로저에서의 암시적 반환 (Implicit Returns from Single-Express Closures)
단일 표현 클로저에서는 반환 (return) 키워드를 생략할 수 있습니다.
reversedNames = names.sorted(by: { s1, s2 in s1 > s2 }) // retrun 생략
인자 이름 축약 (Shorthand Arguments Names)
Swift는 자동으로 인라인 클로저에 축약 인자 이름을 제공합니다.
$0, $1, $2 등으로 사용할 수 있으며, in 키워드를 생략할 수 있고, $0 부터 인자값을 순서대로 사용 합니다.
reversedNames = names.sorted(by: { $0 > $1 }) // in 생략
코드 해석
- $0 과 $1 인자를 받습니다
- $0이 $1 보다 큰지 비교
- 결과 값 Bool (true, false) 반환
연산자 메소드 (Operator Methods)
여기서 더 줄일 수 있습니다.
sorted(by:) 메소드에는 인자 두개를 받아 두 값을 비교한다는걸 (>) 연산자 메소드로 추론할 수 있으므로 아래와 같이 축약도 가능 합니다.
reversedNames = names.sorted(by: > )
너무 많이 축약하게 되면 코드를 읽는데 어려움이 있으므로, 적당히 축약하자.
후행 클로저 (Trailing Closure)
만약 함수의 마지막 인자로 클로저를 넣었을때 클로저가 너무 길다면 후행 클로저를 사용할 수 있습니다.
후행 클로저를 사용하기전 아래 형태의 함수와 클로저가 있다고 가정해봅시다.
func someFunctionThatTakesAClosure(closure: () -> Void) {
// function body goes here
}
후행 클로저를 사용하면 아래와 같이 사용할 수 있습니다.
someFunctionThatTakesAClosure() {
// trailing closure's body goes here
}
중괄호 { } 안에 body 내용을 적어 기능구현을 할 수 있습니다.
앞에 sorted() 메소드를 후행 클로저로 표현하면 이렇게 표현 가능합니다.
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]
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
}
// strings is inferred to be of type [String]
// its value is ["OneSix", "FiveEight", "FiveOneZero"]
map(_:) 메소드 다음에 있는 number 인자의 값은 상수로 변하지 않지만,
클로저 body 부분에서 number를 변수로 선언하면서 값을 변경해 줄수 있습니다.
NOTE
digitNames[number % 10]! 뒤에 느낌표(!)가 붙은 이유는 사전(dictionary)의 subscript는 옵셔널이기 때문입니다.
사전에서 특정 key에 대한 값은 있을 수도 있고 없을 수도 있기 때문!
로직 풀이
(number[0]의 첫번째 루프)
% = 나머지
ex) 26 % 10 = 6
- number = 16, output = "" 로 시작
- (반복문 repeat 부분)
2-1. output = digitNames[16 % 10 = 6]! + ""
2-2. number = 16 / 10 --> number = 1 (타입추론으로 몫 1 추출) - (while 조건)
3-1. number 는 1.
즉 0보다 큼으로 repeat 루프로 이동
(number[0]의 두번째 루프)
- number = 1, output = "Six"
- (반복문 repeat 부분)
2-1. output = digitNames[1 % 10 = 1]! + "Six"
2-2. number = 1 / 10 --> number = 0 - (while 조건)
3-1. number 는 0
즉, 0보다 작음으로 return output = "OneSix"
(number[1]의 첫번째 루프)
위의 방법과 동일하게 루프를 돕니다.
값 캡쳐 (Capturing Values)
클로저는 정의된 주변 컨텍스트로부터 상수, 변수의 값을 캡처할 수 있습니다.
Swift에서 가장 간단한 클로저는 함수 내에 작성된 함수, 즉 중첩 함수 인데
중첩 함수는 밖에 있는 상수, 변수를 캡처할 수 있고, 밖에 있는 함수의 인자도 캡처할 수 있습니다.
아래는 makeIncrementer 함수가 incrementer 중첩 함수를 포함하는 예제 입니다.
func makeIncrementer(forIncrement amount: Int) -> () -> Int {
var runningTotal = 0
func incrementer() -> Int {
runningTotal += amount
return runningTotal
}
return incrementer
}
먼저 makeIncrementer함수는 (forIncrement amout: Int) 라는 파라미터를 받으며, 반환 타입은 () -> Int 입니다.
반환 값이 클로저 형태라는걸 짐작할 수 있죠.
중첩함수인 incrementer만 따로 해서 보겠습니다.
func incrementer() -> Int {
runningTotal += amount
return runningTotal
}
incrementer 중첩함수는 runningTotal과 amount값을 포함하고 있으며,
이 두 값들은 주변 컨텍스트(makeIncrementer)로부터 값을 캡처해 올수 있습니다.
값 들을 캡처해온 후 incrementer는 호출될 때마다 runningTotal과 amount의 값들을 더한 후 runningTotal 값에 대입시킵니다.
함수를 한번 실행 시켜보겠습니다.
var incrementByTen = makeIncrementer(forIncrement: 10)
incrementByTen() // 10
incrementByTen() // 20
incrementByTen() // 30
incrementByTen() // 40
같은 함수가 4번 실행되지만 실제로는 함수 내부에 runningTotal과 amount의 캡처 되어 있던 값들이 누적으로 계산이 되는겁니다.
로직 풀이
첫번째 함수실행
runningTotal = 0 , amount = 10
- makeIncrementer의 인자로 10을 파라미터에 전달
- makeIncrementer함수가 실행되면서 중첩함수인 incrementer도 실행
- incrementer 의 runningTotal은 0, amount는 인자로 전달받은값 10 runningTotal = 0 + 10
- incrementer 에서 return 10
- makeIncrementer에서 incrementer를 return 10
두번째 함수실행
runningTotal = 10 (값 캡처) , amount = 10
- makeIncrementer의 인자로 10을 파라미터에 전달
- 위 2번과 동일
- incrementer의 runningTotal은 10, amount는 인자로 전달받은값 10 runningTotal = 10 + 10
- incrementer 에서 return 20
- makeIncrementer에서 incrementer를 return 20
... 위와 같은 방식으로 함수가 실행 됩니다.
만약 아래와 같이 새로운 클로저를 생성하게 되면 ?
let incrementBySeven = makeIncrementer(forIncrement: 7)
incrementBySeven()
// 7
다른 클로저이기 때문에 다른 저장소에 runningTotal과 amount를 캡처해 사용합니다.
여기서 이전 클로저를 실행하면 ?
incrementByTen() // 50
위에도 언급했듯 각기 다른저장소에 저장되기 때문에 영향을 받지 않습니다.
클로저는 참조 타입 (Closures Are Reference Types)
위 예제에서 incrementBySeven 과 incrementByTen은 상수지만,
함수와 클로저는 참조타입이기 때문에 이 상수들이 참조하는 클로저는 캡처된 runningTotal을 계속 증가시킬 수 있습니다.
만약 한 클로저를 두 상수나 변수에 할당하면 그 두 상수나 변수는 같은 클로저를 참조 하게 됩니다.
let alsoIncrementByTen = incrementByTen // 같은 클로저 참조
alsoIncrementByTen() // 30
incrementByTen() // 40
다음 클로저에서는
Escaping Closure (탈출 클로저)
Autoclosure (자동 클로저)
에 대해 블로깅 하겠습니다~
'iOS' 카테고리의 다른 글
[iOS] CocoaPods 설치 및 실행 (0) | 2022.02.06 |
---|---|
[Swift] 클로저 (Closure) (2) - Escaping Closure, Autoclosure (0) | 2022.02.02 |
ModerRIBs_tutorial 2 - 2 (0) | 2022.01.26 |
ModernRIBs_tutorial2 - 1 (0) | 2022.01.23 |
[Swift] 함수 (Functions) (2) (0) | 2022.01.21 |
- Total
- Today
- Yesterday
- Swift Leetcode
- Swift inout
- swift protocol
- Swift 프로그래머스
- Swift ModernRIBs
- Swift 알고리즘
- iOS error
- Swift init
- swift programmers
- Swift joined()
- CS 네트워크
- Swift 내림차순
- RTCCameraVideoCapturer
- swift (programmers)
- Swift final
- removeLast()
- ios
- swift reduce
- Swift Error Handling
- RIBs tutorial
- Swift RIBs
- 원티드 프리온보딩
- 2023년 회고
- swift 고차함수
- Swift joined
- Swift
- Class
- swift property
- Combine: Asynchronous Programming with Swift
- Swift 프로퍼티
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |