[Swift/iOS] 클로저 (Closure) from 공식문서
! 공식문서를 참고한 글입니다
클로저?
클로저는 한마디로 말해서 코드블럭 { } 이다.
어렵게 생각하지 말자
클로저의 3가지 종류
클로저는 3가지 종류가 있다.
- 전역 함수(Global function) : 이름이 있고(Named Closure) 어떤 값도 캡쳐하지 않는 클로저
- 중첩 함수(Nested function) : 이름이 있고(Named Closure) 관련된 함수로부터 값을 캡쳐할 수 있는 클로저
- 클로저 표현(Closure expression) : 경량화된 문법으로 쓰여지고 관련된 문맥으로(context) 값을 캡쳐할 수 있는 이름이 없는(Unnamed Closure) 클로저
설명을 들으면 어렵다
캡쳐한다는 뜻은 그냥 미뤄두자
전역 함수는 우리가 생각하는 일반적인 형태의 '함수'를 생각하면 된다
func globalFunction(stringParameter: String) -> String {
return stringParameter
}
중첩 함수는 말 그대로 함수가 중첩된 것, 함수 안에 함수가 있는 것으로 보면 된다
func outter() {
func nested() {
}
}
전역 함수와 중첩 함수는 이름이 있는(Named Closure) 함수이고 Closure의 한 형태 이지만, 일반적으로 클로저를 지칭할 때에는 이름이 없는 클로저 표현을 말한다
클로저 표현 문법 Closure Expression Syntax
{ (<#parameters#>) -> <#return type#> in
<#statements#>
}
{ 파라미터1: 파라미터타입1, 파라미터2: 파라미터타입2 -> 리턴타입 in
코드
}
let 만나이계산기 = { (age: Int) -> String in
let 만나이 = age - 1
return "만나이는 \(만나이)입니다."
}
클로저 형태는 위와 같다
클로저를 만나이계산기 라는 상수에 할당했다
만나이 계산기의 타입을 보면 Int 파라미터를 갖고 String을 리턴값으로 갖는 클로저, 다시 말해서 함수 그 자체인 것을 알 수 있다
print(type(of: 만나이계산기)) // (Int) -> String
사용할 때는 아래와 같이 사용하면 된다
let value = 만나이계산기(10)
print(value) // 만나이는 9입니다.
print(type(of: value)) // String
만나이계산기(10) 로 클로저를 담은 만나이계산기를 실행하고, String의 리턴값을 value에 할당했다
문맥에서 타입 추론 Inferring Type From Context
이번에는 함수 안에서 클로저를 사용해보자
func 더하기(closure: (Int, Int) -> String) {
print(closure(1, 2))
}
"더하기" 라는 함수는 Int 파라미터 두개와 String을 리턴하는 클로저를 파라미터로 받는다
그리고 숫자 1과 2를 넣어서 클로저를 실행하고, 클로저의 return 값인 String을 Print하는 함수다
여기서 클로저를 사용하려면
더하기(closure: { (첫번째숫자: Int, 두번째숫자:Int) -> String in
return "\(첫번째숫자 + 두번째숫자)입니다"
})
// 3입니다
와 같은 구조로 실행할 수 있다
처음에 함수를 정의할 때, closure라는 파라미터는 Int 타입 두개를 파라미터로 받는다는 것을 알고 있기 때문에 클로저의 파라미터 타입을 생략할 수 있다
더하기(closure: { 첫번째숫자, 두번째숫자 -> String in
return "\(첫번째숫자 + 두번째숫자)입니다"
})
리턴 값 역시 String으로 반환된다는 것을 이미 알고 있기 때문에 생략 가능하다
더하기(closure: { 첫번째숫자, 두번째숫자 in
return "\(첫번째숫자 + 두번째숫자)입니다"
})
단일 표현 클로저에서의 암시적 반환 Implicit Returns from Single-Express Closures
단일 표현, 곧 return 한 줄의 코드만 남는 경우에는 return 키워드 역시 생략가능하다
더하기(closure: { 첫번째숫자, 두번째숫자 in
"\(첫번째숫자 + 두번째숫자)입니다"
})
인자 이름 축약 Shorthand Arguments Names
함수를 정의할 때 우리는 클로저의 파라미터들과 반환값의 타입이 정해져있다는 것이 아니라 그 형식 자체가 이미 정해져있다는 것을 알 수 있다
그래서 굳이 첫번째숫자, 두번째숫자 라는 인자의 이름또한 생략해줄 수 있다
그리고 생략해준 대신에 코드안에서 순서대로 $0, $1 ... 와 같이 사용할 수 있다
더하기(closure: {
"\($0 + $1)입니다"
})
// 3입니다
처음에 비해 훨씬 간결해졌다
후위 클로저 Trailing Closures
함수의 마지막 인자가 클로저라면, 그 클로저를 후위 클로저로 사용할 수 있다
처음 더하기 함수의 파라미터가 하나뿐이기 때문에 해당 클로저를 후위 클로저로 사용해보자
클로저 파라미터를 함수 뒤로 빼서 위치를 바꾼다고 생각하면 된다
더하기(closure: {
"\($0 + $1)입니다"
})
더하기() {
"\($0 + $1)입니다"
}
후위 클로저를 사용한다면 괄호()까지 생략가능하다
더하기 {
"\($0 + $1)입니다"
}
// 3입니다
값 캡쳐 (Capturing Values)
공식문서의 예제를 보면 조금 어렵고 헷갈린다
func makeIncrementer(forIncrement amount: Int) -> () -> Int {
var runningTotal = 0
func incrementer() -> Int {
runningTotal += amount
return runningTotal
}
return incrementer
}
조금 단순화해보자
func add(addNumber: Int) -> () -> Int {
var totalNumber = 0
func adding() -> Int {
totalNumber = totalNumber + addNumber
return totalNumber
}
return adding
}
단순화 실패...
아무튼 add라는 함수는 Int를 파라미터로 받고, "Int를 반환값으로 갖는 함수( () -> Int )"를 반환 값으로 갖는다
func adding() -> Int {
totalNumber = totalNumber + addNumber
return totalNumber
}
가운데 adding 함수만 살펴보면 내부 코드에 totalNumber와 addNumber를 정의해주지 않는데 잘 돌아가는데,
이것은 외부 함수인 add에서 totalNumb와 addNumber를 캡쳐링하기 때문이다
위 사례는 중첩함수로서 처음에 언급했던 값을 캡쳐할 수 있는 클로저 이다
let plusTen = add(addNumber: 10)
print(type(of: plusTen)) // () -> Int
print(plusTen()) // 10
print(plusTen()) // 20
print(plusTen()) // 30
plusTen이라는 상수에 함수를 할당해보자
plusTen은 곧 Int값을 반환하는 함수가 된 것이니 이를 실행하면 처음에는 10을 반환한다
또 실행하면 20, 30을 반환하는 것을 알 수 있는데
totalNumber와 addNumber가 캡쳐링되기 때문에 계산이 계속 누적된다
클로저는 참조 타입 Closures are Reference Types
let anotherPlusTen = plusTen
print(type(of: anotherPlusTen)) // () -> Int
print(anotherPlusTen()) // 40
함수, 클로저는 참조타입이기 때문에 상수나 변수에 할당할 때는 해당 함수와 클로저의 참조(reference)가 할당된다
이스케이핑(탈출) 클로저 Escaping Closures
클로저를 파라미터로 넣을 수 있는 경우는 위에서 보았는데, 사용할 수 없는 경우가 있다
var handler: (() -> Void)?
func 함수(closure: () -> Void) {
handler = closure
}
위와 같이 함수 외부에 정의된 변수에 저장하려면
Assigning non-escaping parameter 'closure' to an @escaping closure
위와 같은 에러가 뜬다
기본 closure는 non-escaping 클로저 인데, 이를 외부 변수에 저장할 수 없다(클로저가 함수에서 빠져나갈 수 없다)
외부 변수에 저장하려면 아래와 같이 @escaping 키워드를 붙여야 한다(클로저가 함수에서 빠져나가서 다른 외부 변수에 할당)
var handler: (() -> Void)?
func 함수(closure: @escaping () -> Void) {
handler = closure
}
오류 해결
+
탈출클로저와 자동클로저는 이어서...