* 익명 함수 클로저에 대한것과 활용법 *
클로저(Closure)
이름없는 함수 즉, 코드 블록을 말합니다.
상수나 변수의 참조를 캡쳐(capture)해 저장할 수 있습니다
주변 환경에 있는 변수나 상수를 캡처하여 저장하고,
이를 나중에 사용할 수 있도록 합니다.
이는 클로저가 생성될 때 클로저가 참조하는
변수 또는 상수의 값에 대한 복사본을 유지하고 저장하는 메커니즘입니다
값(value) 캡처
클로저가 변수나 상수의 값을 캡처합니다.
이때, 클로저 내부에서 캡처한 값이 변경되어도 원본 값은 변경되지 않습니다.
참조(reference) 캡처
클로저가 변수나 상수의 참조를 캡처합니다.
따라서 클로저 내에서 해당 변수나 상수를 변경하면 원본 값도 변경됩니다.
// 값 캡처
func makeIncrementer(forIncrement amount: Int) -> () -> Int {
/*
리턴 타입의 부분은 요 부분입니다 : " () -> Int { " 이를 말로 표현 한다면
() => 파라메타가 없으며, Int => 리턴 타입이 Int인 어떠한 함수입니다. 즉, 함수를 리턴하는 것 입니다.
*/
var total = 0
// 클로저를 반환합니다.
let incrementer: () -> Int = {
/*
incrementer(인크리멘터)는 "()(괄호) ->(화살표) Int(인트)" 타입입니다.
위에서 정의된 타입이 동일 합니다. 때문에 incrementer를 리턴할 수 있는 것 입니다.
"()(괄호) ->(화살표) Int(인트)" 타입은 무엇인가? -> 파라미터는 없으며 Int를 리턴하는 함수입니다.
아래 total을 보면 중괄호 외부에 있는 것이 아닌 내부에 있는데 그 내부에 있는 값을 캡쳐를 하는 것입니다.
위에 내부에 정의된 값을 캡쳐(가져다가) 사용 하는 것 입니다.
그래서 amount를 가지고 total 에 더해서 다시 넣어주고, total을 리턴을 해줍니다.
*/
// total 변수를 캡처하여 저장합니다.
total += amount
return total
}
return incrementer
}
let incrementByTen = makeIncrementer(forIncrement: 10)
/*
incrementByTen은 "()(괄호) ->(화살표) Int(인트)" 타입이 들어가는 것입니다.
즉, incrementByTen은 특정한 값이 아니라 함수 자체가 들어가 있는 상태입니다.
그렇기 때문에 함수를 실행할 때에는 파라미터가 있다면 넣어주었겠지만 파라미터가 없으니
단순히 ()괄호를 열고 닫아 주어도 함수를 실행 할 수 있습니다. 그렇게 해주면 아래와 같이
total에 값이 출력이 됩니다.
*/
print(incrementByTen()) // total = 10, 결과: 10
print(incrementByTen()) // total = 20, 결과: 20 // 두번째 함수를 호출 할 때에는 같은 함수를 호출하였지만, total이라는 변수를 캡쳐를 하고 가지고 있는 상태인데 또 10이 들어있는 상태에서 한번더 10을 넣어주기 때문에 20이 호출됨을 알 수 있습니다.
// 참조 캡처
class SimpleClass { // 클래스 자체가 참조 타입이기 때문에 instance.value *= 2 이부분을 캡쳐할 수 있는 겁니다.
var value: Int = 10
}
func createClosure() -> (() -> Int) { // "()(괄호) ->(화살표) Int(인트)" 파라미터가 없고 Int형을 리턴해주는 함수를 선언.
/*
"()(괄호) ->(화살표) Int(인트)" 이 함수 안에 인스턴스가 있는데
인스턴스는 SimpleClass의 인스턴스입니다.
*/
let instance = SimpleClass()
// 참조 캡처를 사용하여 SimpleClass의 인스턴스를 캡처합니다.
let closure: () -> Int = { // closure라는 상수가 있고 아래에 "return closure' closure 자체를 리턴해 주고 있습니다.
// 클로저가 참조하는 인스턴스의 속성을 업데이트합니다.
instance.value *= 2 // 위에 있는 let instance의 instance를 캡쳐해서 var value의 value에 접근합니다. 접근 후 2를 곱하고 다시 넣어줍니다.
return instance.value 그리고 instance의 value를 return해 줍니다.
}
return closure
}
// 클로저 생성
let myClosure = createClosure() // 함수를 호출 하면 createClosure의 리턴 타입인 "()(괄호) ->(화살표) Int(인트)" 이 함수 즉, " let closure: () -> Int = { " 이 구문의 함수가 myClosure에 들어가게 됩니다.
print(myClosure()) // 20
/*
myClosure를 실행한다면 instance의 value를 처음에 접근하면 10이고 10에 2를 곱하고 넣어주면 20이 되며
그 다음 그렇게 20을 캡쳐하고 그 캡쳐한 것을 다시 가져와서 다시 2를 곱해주니 40이 되어 두번째로 호출한 부분은
40이 되는 구도입니다.
*/
print(myClosure()) // 40
// 클로저 내부에서 참조된 인스턴스의 속성을 변경하였으므로 원본에도 영향을 줍니다.
클로저를 사용하는 이유
가장 일반적으로는 기능을 저장하기 위해 사용합니다.
클로저는 비동기 처리가 필요할 때 사용할 수 있는 코드 블록입니다.(반드시 비동기에만 사용하는 것은 아님)
클로저는 클래스와 마찬가지로 참조 타입(reference type)입니다.
// 예시코드
{ (parameters) -> return type in
// 구현 코드
}
// 함수와 클로저 비교
func pay(user: String, amount: Int) {
// code
}
let payment = { (user: String, amount: Int) in
// code
}
/// 예시1
// 1) (클로저를 파라미터로 받는 함수)정의
func closureFunc2(closure: () -> ()) { // 이 부문은 파라미터로 클로저를 받고 있습니다. 즉, 파라미터로 함수를 받고 있는 것입니다.
print("시작")
closure()
} // 이함수를 실행 시키면 "() -> ()" 파라미터로 받은 함수를 closure()이걸로 시작을 시켜줍니다.
// 파라미터로 사용할 함수/클로저를 정의
func doneFunc() { // 함수를 정의
/*
파라미터가 없으며, 리턴형도 없어서 아래에서 losureFunc2(closure: doneFunc) 이렇게
실행이 가능합니다. 불편한점은 losureFunc2(closure: doneFunc) 이걸 호출 하기 위해서
func doneFunc(), let doneClosure를 따로 만들어 주어야 합니다.
*/
print("종료")
}
let doneClosure = { () -> () in // 클로저를 정의
print("종료")
}
// 함수를 파라미터로 넣으면서 실행 (그동안에 배운 형태로 실행한다면)
closureFunc2(closure: doneFunc)
closureFunc2(closure: doneClosure)
// 2) 함수를 실행할때 클로저 형태로 전달 (클로저를 사용하는 이유)
closureFunc2(closure: { () -> () in
print("프린트 종료") // 본래 정의된 함수를 실행시키면서, 클로저를 사후적으로 정의 가능
}) // (활용도가 늘어남)
closureFunc2(closure: { () -> () in
print("프린트 종료 - 1")
print("프린트 종료 - 2")
})
/// 예시2
// 1) (클로저를 파라미터로 받는 함수)정의
func closureCaseFunction(a: Int, b: Int, closure: (Int) -> Void) { // Void는 ()괄호 입니다. 즉, 빈값을 말합니다. 이는 리턴타입이 없다는 것을 의미하며 리턴 키워드는 쓸 수 있지만 리턴 뒤에 무어나가 붙지는 않습니다.
let c = a + b // 여기서 더한 값을 가지고 받아온 파라미터 "closure: (Int) -> Void)" 에 넣어서
closure(c) // closure를 실행을 시키는 함수를 말합니다.
}
/*
여기서 closureCaseFunction을 사용하고 싶다면 a: 1, b: 2 해주는데 클로저에 외부에서 따로 클로저를
만들지 않고 내부에서 한번에 실행을 시키고 싶은 것이죠 위 let c = a + b 이부분을보면 a와 b를 더한 후
그 값을 가지고 closure에 있는 함수를 실행을 시키는 것 입니다.
*/
// 2) 함수를 실행할 때 (클로저 형태로 전달)
closureCaseFunction(a: 1, b: 2, closure: { (n) in // 사후적 정의
print("plus : \(n)")
})
/*
자 먼저 a와 b를 더하면 3이 되죠 그후 closure를 실행 시켜줍니다. closure는 사후적정의 즉,
함수를 호출 하면서 정의를 해주는 것을 의미 합니다. 그렇다면 중괄호 내부의 코드를 보면 됩니다.
(n) in 여기서 n은 in앞에 있는 것들은 파라미터니까 위 예시 2 코드에서 closure: (Int) 이부분 입니다.
Int 타입의 파라미터가 들어오고 closure를보면 c가 됩니다. 즉, 더한 값이 파라미터로 들어오는 것 입니다.
그러면 3이 n으로 들어가는 것을 알 수 있겠죠.
*/
// 위를 조금더 간단하게 해봅시다.
/*
마지막 파라미터가 클로저이라면 파라미터 명 자체를 생각할 수 있습니다.
괄호의 위치만 바꿔주고 중괄호 안에 있는 내용들이 closure로 인식이 됩니다.
어떻게 보면 좀더 명확하게 좀더 간결하게 표현이 된 모습입니다.
*/
closureCaseFunction(a: 1, b: 2) {(number) in // 사후적 정의
print("result : \(number)")
} // 이렇게 하면 a와 b가 더해져서 3이 되고 number로 넘어가서 result로 3이 출력이 됩니다.
closureCaseFunction(a: 4, b: 3) { (number) in // 사후적 정의
print("value : \(number)")
} // 숫자만 바뀐 부분
/*
파라미터 생략 등 간소화 문법
*/
// 함수의 정의
/*
단계에 따른 간소화 문법 정리
1. performClosure 함수를 정의해줍니다. performClosure의 파라미터는
String타입을 받아 Int를 리턴해주는 함수를 말합니다. param에 Swift를 항상 넣어줍니다.
2. performClosure 를 실행시키면 param에 함수가 들어 올것이고 위 사후적정의 처럼
중괄호로 마무리 해줍시다. (param은 String을 받아 "return str.count"를 반환해주는 겁니다.)
count가 Int 타입이기 때문에 String에 count를 리턴한다는 것은 Int 타입을 리턴하겠다는 것입니다.
그래서 타입이 일치하게 됩니다. performClosure를 실행한다는 것은 param에 Swift를 넣게 되어 있는데,
Swift라는 문자열의 count는 5입니다. 그래서 5가 리턴이 되게 됩니다.
*/
func performClosure(param: (String) -> Int) {
param("Swift")
}
// 문법을 최적화하는 과정
// 1) 타입 추론(Type Inference)
performClosure(param: { (str: String) in
return str.count
})
/*
동작은 어느정도까지 줄일 수 있는지에 대한 부분입니다.
1. 위에서 "param: (String) -> Int" 타입이 정의가 되어 있기 때문에
이미 타입이 추론이 가능한 상태이며, str이라는 매개변수에 타입을 굳이 정의하지 않아도 됩니다.
때문에 아래와 같이 String을 in으로 구문했습니다.
in의 존재는 뒤에 있는 코드와 앞에 있는 파라미터 사이를 구분해 주는 역할을 했습니다.
만약 파라미터가 1개가 들어온다는 것을 컴파일러가 알고 있다면 $0으로도 축약이 가능합니다.
*/
performClosure(param: { str in
return str.count
})
// 2) 한줄인 경우, 리턴을 안 적어도 됨(Implicit Return)
performClosure(param: { str in
str.count
})
// 3) 아규먼트 이름을 축약(Shorthand Argements)
performClosure(param: {
$0.count
})
// 4) 트레일링 클로저
performClosure(param: {
$0.count
})
performClosure() {
$0.count
}
// 최종 축약
performClosure { $0.count }
// 다른예시
// 축약 전
let closureType1 = { (param) in // closureType1 이라는 상수 이는 param(파라미터)를 하나 받아서 param(파라미터)의 2로 나머지가 0인지를 리턴해주는 함수입니다.
return param % 2 == 0
}
// 축약 후
let closureType2 = { $0 % 2 == 0 } // 한줄표현
// 다른 예시 (파라미터가 2개가 들어온 케이스)
// 축약 형태로의 활용
let closureType3 = { (a: Int, b:Int) -> Int in
return a * b
} // 이를 타입으로 표현한 것이 closureType4 코드입니다.
let closureType4: (Int, Int) -> Int = { (a, b) in
return a * b
}
// in 지우고 return 지우고, (a, b)를 $0과 1로 바꿔준 케이스
let closureType5: (Int, Int) -> Int = { $0 * $1 }
탈출 클로저(Escaping closure)
// 코드의 순차적 실행과 비동기의 실행 순서
// 순차적 실행
func sequentialExecutionExample() {
print("Start")
// 1. 첫 번째 작업
for i in 1...3 {
print("Task \(i)")
}
// 2. 두 번째 작업
print("Next Task")
// 3. 세 번째 작업
let result = 5 + 3
print("Result: \(result)")
print("End")
}
sequentialExecutionExample()
/*
위의 코드는 함수 sequentialExecutionExample 내에서 순차적으로 실행됩니다.
각각의 작업은 순서대로 실행되며, 한 작업이 끝나야 다음 작업이 실행됩니다.
이 예시에서는
'Start', 'Task 1', 'Task 2', 'Task 3', 'Next Task', 'Result: 8', 'End'
와 같은 순서로 출력됩니다.
*/
// 코드의 순차적 실행과 비동기의 실행 순서
func asynchronousExecutionExample() {
print("Start")
// 1. 비동기로 실행되는 작업
/*
비동기란? : 쉽게 이야기해서 어떠한 일을 하는 것을 맡겼을 때 즉시 응답이 오지 않고,
작업이 완료 된다면 비로소 작업 결과물을 알려주는 것을 이야기 합니다.
async : 비동기 키워드
1. 우선 print("Start")로 시작
2. "DispatchQueue.global().async {" 비동기로 이 작업을 보내줍니다.
이 작업이 완료가 되면 결과를 가지고 오라는 "(i)" 뜻입니다. 이후
3. "print("Next Task")" 이 부분을 실행
4. 또다른 비동기 작업을 수행
5. 끝부분 print("End")로 마무리
추가로 완료되는 순서는 정의 되어 있지 않습니다.
*/
DispatchQueue.global().async {
for i in 1...3 {
print("Async Task \(i)") // 여긴 123이 프린트가 될 것이고 아래쪽은 8이 프린트가 될 것인데
}
}
// 2. 순차적으로 실행되는 작업
print("Next Task")
// 3. 또 다른 비동기 작업
DispatchQueue.global().async {
let result = 5 + 3
print("Async Result: \(result)") // 123뒤에 항상 8이 붙는상황이 아닐 수도 있다. 8이 앞으로 껴 있을 수도 있다는 의미입니다.
} // ex) 1283, 1823, 8123 등등이 될수도 있다는 의미입니다. 이게 비동기라고 보면 됩니다.
// 4. 끝 부분
print("End")
}
asynchronousExecutionExample()
/*
위의 코드는 비동기적으로 실행되는 예시입니다.
DispatchQueue.global().async를 사용하여 클로저가 다른 스레드에서 비동기적으로 실행됩니다.
따라서 비동기 작업은 순차적인 흐름을 방해하지 않고 별도의 스레드에서 실행됩니다.
실행 결과는
'Start', 'Next Task', 'End' 순서로 출력되고,
비동기 작업은 나중에 완료되어
'Async Task 1', 'Async Task 2', 'Async Task 3', 'Async Result: 8'와 같이
순서는 보장되지 않는 시점에 출력됩니다.
이는 비동기 작업이 별도의 스레드에서 동작하기 때문에,
주 스레드의 작업과 병행적으로 실행됨을 보여줍니다.
*/
이스케이핑 클로저(escaping closure)
어떤 함수의 내부에 존재하는 클로저(함수)를 외부 변수에 저장하는 경우
이스케이핑 클로저는 클로저가 메서드의 인자로 전달됐을 때, 메서드의 실행이 종료된 후 실행되는 클로저(비동기)
이 경우 파라미터 타입 앞에 @escaping이라는 키워드를 명시해야 합니다.
예를들어, 비동기로 실행되거나 completionHandler로 사용되는 클로저의 경우
클로저를 메서드의 파라미터로 넣을 수 있습니다.
// 이스케이핑 클로저 예시 코드
// 1) 외부 변수 저장
var defaultFunction: () -> () = { print("출력") }
func escapingFunc(closure: @escaping () -> ()) {
// 클로저를 실행하는 것이 아니라 aSavedFunction 변수에 저장.
// 함수는 변수와 달리 기본적으로 외부 할당이 불가능
defaultFunction = closure
}
// 2) GCD 비동기 코드
func asyncEscaping(closure: @escaping (String) -> ()) {
var name = "iOS튜터"
DispatchQueue.main.asyncAfter(deadline: .now() + 3) { // 3초뒤에 실행하도록 만들기
closure(name)
}
}
asyncEscaping { str in
print("name : \(str)")
}
@escaping 를 사용하는 클로저에서 self의 요소를 사용할 경우,
self를 명시적으로 언급해야 합니다.
// self를 명시적으로 사용하는 예시 코드
/*
completionHandlers 라는 배열이 있습니다.
"[() -> Void]"이 배열은 함수의 배열입니다.
파라미터, 보이드에 리턴형식도 없는 함수의 배열이자 빈 배열인 상태입니다.
"someFunctionWithEscapingClosure" 라는 함수가 있는데 이는 completionHandler 라는 이름으로
클로저를 받고 있는데 이 외부에 "(completionHandler)' 받은 함수를 .append 시켜주는것을 말합니다.
외부의 변수에 저장할 때에는 @escaping 반드시 써주어야 하고,
someFunctionWithNonescapingClosure 라는 함수가 있으며
(closure: () -> Void) 이부분은 단순히 파라미터로 받은 클로저를 closure() 실행해주는 것입니다.
단순히 함수 안에서 끝나는 클로저를 말하는 것입니다.
SomeClass에 x의 값에 10이 저장되어 있으며, doSomething 이라는 메서드를 지니고 있습니다.
이 메서드는 someFunctionWithNonescapingClosure 이를 실행하고,
someFunctionWithNonescapingClosure 를 실행합니다.
그중 순서는 someFunctionWithEscapingClosure { self.x = 100 } 이를 먼저 실행하는데,
여기서 { self.x = 100 } 100을 self의 x에 값에 넣어주는 이함수는 completionHandler(첫번째 인자)로 들어가며,
이후 someFunctionWithNonescapingClosure 여기서는 self를 적어주지 않았는데,
이는 이스케이프 클로저를 사용하는 것은 아닌데 이때는 self를 적어줄 필요가 없습니다.
someFunctionWithNonescapingClosure의 경우는 "var x = 10" 에 들어갑니다. 그러면 x의 값은 200이 됩니다.
*/
var completionHandlers: [() -> Void] = []
func someFunctionWithEscapingClosure(completionHandler: @escaping () -> Void) {
completionHandlers.append(completionHandler)
}
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?()
// first?()로 실행시 인스턴스의 x를 다시 프린트 했을때 100이 됩니다.
// 이유는 { self.x = 100 } 이부분이 first?() 이 함수를 실행할 때 호출이 되는 것이기 때문에 이때 인스턴스의 x 는 기존엔 200인 상태였지만 { self.x = 100 } 요때 100으로 세팅이되는 것이기 때문입니다.
print(instance.x)
// Prints "100"
클로저의 경우는 실전학습을 통해 많은 경험을 쌓으면서
익숙해져야하는 부분들이 많습니다.
지속적인 연습없이 이부분을 짚고 넘어가기는 어려움이 있습니다.
'IOS > Swift-Study' 카테고리의 다른 글
[Swift-Study] 심화 문법종합반 2주차 3일차 정리 - 예외처리 (0) | 2024.03.25 |
---|---|
[Swift-Study] 심화 문법종합반 2주차 3일차 정리 - 고차함수 (0) | 2024.03.25 |
[Swift-Study] 심화 문법종합반 2주차 2일차 정리 - 접근제한자 (0) | 2024.03.12 |
[Swift-Study] 심화 문법종합반 2주차 1일차 정리 - 타입 캐스팅 (0) | 2024.03.12 |
[Swift-Study] 심화 문법종합반 2주차 1일차 정리 - 프로퍼티 옵저버 (0) | 2024.03.12 |