클로저 (Closures)
클로저 : 어떤 상수나 변수의 참조를 캡쳐해 저장할 수 있다. ➡️ 익명의 함수
전역 함수 | 이름이 있고 어떤 값도 캡쳐하지 않는 클로저 |
중첩 함수 | 이름이 있고 관련한 함수로 부터 값을 캡쳐 할 수 있는 클로저 |
클로저 표현 | 경량화 된 문법으로 쓰여지고 관련된 문맥으로부터 값을 캡쳐할 수 있는 이름이 없는 클로저 |
⚠️ 클로저라고 하면 보통 익명의 함수를 의미하는데, 사실 func 키워드를 통해 이름을 붙여주는 함수들도 모두 클로저임.
func nameFunc() {
print("nameFunc")
}
// 이름이 있는 함수 = Named Closure
let unnamedFunc = { print("unnamedFunc") }
// 이름이 없는 함수 = Unnamed Closure
클로저는 Named Closure & Unnamed Closure 둘 다 포함하지만, 보통 Unnamed Closure를 말한다.
문맥에서 인자 타입과 반환 타입의 추론 |
단일 표현 클로저에서의 암시적 반환 |
축약된 인자 이름 |
후위 클로저 문법 |
✅ 스위프트에서 클로저 표현은 위와 같은 내용으로 최적화 되어서 간결하고 명확하다.
클로저 표현 (Closure Expressions)
{ (parameters) -> return type in
statements
}
✔️ 클로저 표현 문법은 일반적으로 위의 예제와 같은 형태를 띤다. 인자로 넣을 parameters, 인자 값으로 처리할 내용을 기술하는 statements, 그리고 return type으로 구성되어있다.
✅ 클로저 머리 (Closure Head) : (parameters) -> return type
✅ in : 머리와 몸통을 구분지어주는 키워드
✅클로저 몸통 (Closure Body) : statements
1️⃣ 인자와 리턴 타입이 없는 클로저
let closureEx = { () -> () in
print("closure")
}
2️⃣ 인자와 리턴 타입이 있는 클로저
let closureEx = { (food: String) -> String in
return "I like \(food)🍟"
}
let closureEx = { (food: String) -> String in return "I like \(food)🍟" }
✔️ 위의 예제와 같이 클로저의 body가 짧으면 한줄로 적을 수 있다.
인라인 클로저 (Inline Closure)
reversedNames = names.sorted(by: { (s1: String, s2: String) -> Bool in
return s1 > s2
})
// sorted의 인자로 들어가 있음. -> 인라인 클로저
✅ 위의 예제와 같이 함수로 따로 정의된 형태가 아닌 인자로 들어가 있는 형태의 클로저를 인라인 클로저라 부른다.
1급 객체로서의 클로저
1️⃣ 클로저를 변수나 상수에 대입할 수 있다.
let closureEx = { () -> () in
print("Closure")
}
// 대입과 동시에 클로저를 작성할 수 있다.
let closureEx = { () -> () in
print("Closure")
}
let closureEx2 = closure
// 기존 클로저를 새로운 변수나 상수에 대입할 수 있다.
2️⃣ 함수의 파라미터 타입으로 클로저를 전달할 수 있다.
func closureUseFunc(closureParam: () -> ()) {
closureParam()
}
// 함수를 인자로 전달받는 closureUseFunc라는 함수가 있다.
// 이 경우 인자로 함수를 전달하면 되지만
closureUseFunc(closureParam: { () -> () in
print("closure")
})
// 이런식으로 인자를 클로저로 전달해도 된다.
3️⃣ 함수의 반환 타입으로 클로저를 사용할 수 있다.
func returnClosureFunc() -> () -> () {
return { () -> () in
print("returnClosure")
}
}
let closure = returnClosureFunc()
closure()
// returnClosure
클로저 실행
1️⃣ 클로저가 대입된 변수나 상수로 호출
let closureEx = { () -> String in
return "closure"
}
closureEx()
2️⃣ 클로저를 직접 실행하기
({ () -> () in
print("closure")
})()
// closure
⚠️ 클로저를 다 작성한 후 마지막에 호출 구문()를 추가해야한다.
후위 클로저 (Trailing Closures)
후위 클로저 : 함수의 마지막 파라미터가 클로저일 때, 이를 파라미터 값 형식이 아닌 함수 뒤에 붙여 작성하는 문법이다.
func printHello(closure: () -> ()) {
closure()
}
printHello(closure: { () -> () in
print("Hello")
})
✔️ 위의 예제와 같이 클로저가 인자 값 형식으로 함수 호출 구문 () 안에 작성되어 있는 것을 인라인 클로저라 하는데, 가독성이 떨어진다는 단점이 있다. 따라서 이 클로저를 인자 값 형식으로 작성하는 것이 아닌, 함수의 가장 마지막에 클로저를 붙여 쓰는 것을 후위 클로저라 한다.
func printHello(closure: () -> ()) {
closure()
}
printHello() { () -> () in
print("Hello")
}
✔️ 위의 예제는 인라인 클로저를 후위 클로저로 변환하여 작성한 것이다.
✅ 후위 클로저에서 함수의 인자; printHello 함수의 인자 (위의 예제에서는 closure); 는 생략 가능하다.
⚠️ 괄호에 주의하며 작성한다.
func printHello(closure: () -> ()) {
closure()
}
printHello { () -> () in
print("Hello")
}
✔️ 위의 예제 처럼 파라미터가 클로저 하나일 경우 호출구문()도 생략 가능하다.
클로저 축약 - sorted(by:) 메소드 예제
let names = ["Yeo", "Bae", "Ahn"]
let sortedNames = names.sorted(by: <)
print(sortedNames)
✔️ 위의 예제와 같이 문자열 배열을 하나 생성한 뒤 이를 sorted(by:) 메소드를 사용하여 알파벳 순으로 정렬하려고 한다. 여기서 핵심이 되는 부분은 names.sorted(by: <)이다. 매개변수 by는 클로저를 받는데 이 클로저는 정렬 기준이 될 것이다. 그런데 이 클로저에 연산자 < 만 있다. 어째서 이런 표현이 가능한 것인가?
names.sorted(by: { (s1. String, s2: String) -> Bool in
return s1 < s2
})
✔️ names.sorted(by: <)를 생략하지 않고 풀어쓴 클로저이다.
names.sorted(by: { s1, s2 in
return s1 < s2
})
✔️ sorted(by:)의 메소드에서 이미 (String, String) -> Bool 타입의 인자가 들어와야하는 것을 알기 때문에 클로저에서 이 타입들은 생략 될 수 있다.
names.sorted(by: { s1, s2 in s1 < s2 })
✔️ 단일 표현 클로저에서는 반환 키워드를 생략할 수 있다.
names.sorted(by : {$0 < $1 })
✔️ 스위프트는 인라인 클로저에 자동으로 축약 인자 이름을 제공한다. 이 인자를 사용하면 인자 값을 순서대로 $0, $1, $2 등으로 사용할 수 있다. 축약 인자 이름을 사용하면 인자 값과 그 인자로 처리할 때 사용하는 인자가 같다는 것을 알기 때문에 인자를 입력 받는 부분과 in 키워드 부분을 생략 할 수 있다.
names.sorted(by: <)
✔️ String 타입 연산자에는 String끼리 비교할 수 있는 비교 연산자 (> , <)를 구현해 두었다. 이 때문에 그냥 연산자를 사용해서 인자를 생략할 수 있다.
클로저 축약 - 간단한 예제
func sumFunc(closureParam: (Int, Int) -> int) {
closureParam(5,10)
}
✔️ 위의 예제와 같이 클로저를 인자로 받아서 합을 구하는 함수가 있다.
sumFunc(closureParam: { (a: Int, b: Int) -> Int in
return a+b
})
✔️ sumFunc 함수를 호출하기 위해 축약하지 않은 인라인 클로저를 작성했다.
sumFunc(closureParam: { (a, b) in
return a + b
})
✔️ 위의 예제와 같이 함수의 인자 타입과 리턴 타입을 알고 있으므로 생략해서 쓸 수 있다.
sumFunc(closureParam: {
return $0 + $1
})
✔️ 위의 예제와 같이 축약 인자를 이용해서 간단하게 작성할 수 있다. 즉, 파라미터 이름 (여기에서는 a, b)를 대신해서 축약인자 $를 대신 사용할 수 있고, 이 경우 인자의 이름과 in 키워드를 삭제한다.
sumFunc(closureParam: {
$0 + $1
})
✔️ 위의 예제와 같이 단일 return문만 남았을 경우, return도 생략할 수 있다.
sumFunc() {
$1 + $2
}
✔️ 위의 예제와 같이 함수의 마지막 파라미터가 클로저이면 후위 클로저로 작성할 수 있다.
sumFunc {
$0 + $1
}
✔️ 위의 예제와 같이 파라미터가 하나인 경우 호출구문()도 생략 가능하다.
자동 클로저 (AutoClosure)
자동 클로저는 인자 값이 없으며 특정 표현을 감싸서 다른 함수에 전달 인자로 사용할 수 있는 클로저이다.
func helloFunc() -> String {
return "Hello"
}
func printHello(closure: @autoclosure () -> String) {
print(closure())
}
printHello(closure: helloFunc())
✅ 자동 클로저는 클로저를 실행하기 전까지 실제 실행이 되지 않는다. 그래서 계산이 복잡한 연산을 하는데 유용하다.
이스케이핑 클로저 (Escaping Closure)
함수 실행을 벗어나서 함수가 끝난 뒤에도 클로저를 실행하거나, 중첩함수에서 실행 후 중첩 함수를 리턴하고 싶거나, 변수와 상수에 대입하고 싶을 때 사용하는 것이 이스케이핑 클로저이다.
func escClosure(closure: @escaping () -> ()) {
let f: () -> () = closure
}
✔️ 위의 예제와 같이 변수나 상수에 인자로 받은 클로저를 대입할 수 있다.
var num = [() -> Void]()
func escClosure(closure: @escaping() -> Void) {
closure()
num.append(closure)
}
✔️ 위의 예제와 같이 함수 안에서 정의된 클로저가 외부 변수들에 대한 접근을 허용할 때 이스케이핑 클로저를 사용하면 된다.
값 캡쳐 (Capturing Values)
func captureEx() {
var hello = "Hello!"
var a = 1
let closureEx = {() -> () in
print(a)
}
print(hello)
closure()
}
captureEx()
✔️ 위의 예제와 같이 closureEx라는 익명함수는 클로저 내부에서 외부 변수인 a라는 변수를 사용하기 때문에 a의 값을 클로저 내부적으로 저장하고 있는데, 이것을 클로저에 의해 a의 값이 캡쳐 되었다라고 함.
func captureEx() {
var a = 1
print("1. a = \(a)")
let closure = { () -> () in
print("3. a = \(a)")
}
a = 5
print("2. a = \(a)")
closure()
}
captureEx()
// 1. a = 1
// 2. a = 5
// 3. a = 5
✔️ 위의 예제와 같이 클로저를 실행하기 전에 캡쳐한 값을 외부에서 변경하면 클로저 내부에서 사용하는 캡쳐한 값 또한 변경 된다.
func captureEx() {
var a = 1
print("1. a = \(a)")
let closure = { () -> () in
a = 5
print("3. a = \(a)")
}
closure()
print("2. a = \(a)")
}
captureEx()
// 1. a = 1
// 2. a = 5
// 3. a = 5
✔️ 위의 예제와 같이 클로저 내부에서 캡쳐한 값을 바꾸면 외부의 값도 변경된다.
func captureEx() {
var a = 1
print("1. a = \(a)")
let closure = { [a] () -> () in
print("3. a = \(a)")
}
a = 5
print("2. a = \(a)")
closure()
}
captureEx()
// 1. a = 1
// 2. a = 5
// 3. a = 1
✔️ 위의 예제와 같이 캡쳐 리스트를 이용하면 Value Type의 값을 복사해서 캡쳐할 수 있다. closure를 실행하기 전에 a의 값을 5로 변경했지만 클로저의 a에는 영향을 주지 않는다. Value Type으로 캡쳐한 경우 상수로 캡쳐하기 때문에 클로저 내부에서 캡쳐된 값을 변경할 수없다.
'Swift > Swift 기본기' 카테고리의 다른 글
08. 클래스과 구조체 (Classes and Structures) (0) | 2023.03.18 |
---|---|
07. 열거형 (Enumerations) (0) | 2023.03.18 |
05. 함수 (Functions) (0) | 2023.03.18 |
04. 제어문 (Control Flow) (0) | 2023.03.17 |
03. 콜렉션 타입 (Collection Types) (0) | 2023.03.17 |