- 소들이님 클로져 글 + 개인적으로 공부한 내용 정리한 내용입니다 :)
참고 - https://babbab2.tistory.com/81
클로져(Closure)
- Named Closure ⇒ 우리가 알고있는 함수 그 자체
- Unnamed Closure ⇒ 우리가 알고있는 클로져!
클로져 표현식
→ Parameter Name도 존재하고, Argument label은 존재하지 않는다.
{(파라미터) -> 리턴타입 in
코드 블럭
}
// 실제 예
let 클로져 = { (age: Int) -> String in
return "내 나이, \(age)"
}
클로져는 1급 객체이다
- 변수나 상수에 대입이 가능하다.
- 함수의 파라미터 값으로 클로져를 전달할 수 있다.
func closureExample(completionHandler: () -> ()) {
completionHandler()
}
// 실행 부
closureExample(completionHandler: { () -> () in
print("Hi!")
})
- 함수의 반환 타입으로써의 클로져
func closureExample() -> () -> () {
return { () -> () in
print("Hello Miro!")
}
}
클로져 실행시키기
- 상수, 변수로 실행시키기
let closure = { () -> String in
return "Hello Miro!"
}
closure()
- 클로져를 직접 실행시키기
- 클로져를 소괄호로 감싸고
()
로 호출해주기
- 클로져를 소괄호로 감싸고
({ () -> String in
return "Hello Miro!"
})()
Trailing Clousure
- 함수의 마지막 파라미터가 클로져일 경우, 함수 뒤에 붙여서 작성하는 문법!
- Argument Label은 생략된다
func closureExample(completionHandler: () -> ()) {
completionHandler()
}
// 실행 부
closureExample(completionHandler: { () -> () in
print("Hi!")
})
// Trailing Clousure의 활용
closureExample() { () -> () in
print("Hi!")
}
- 아래와 같이
()
도 생략해줄 수 있다.
closureExample { () -> () in
print("Hi!")
}
‼️ 만약 파라미터가 여러 개인 함수일 경우 ‼️
func doSomething(first: () -> (), last: () -> ()) {
// doSomething
}
- 정석적인 함수 호출
doSomething(first: { () -> () in
print("first")
}, last: {() -> () in
print("last")
})
- Trailing closure의 활용
doSomething(first: { () -> () in
print("first")
}) { () -> () in
print("last")
}
클로져의 경량문법 과정
// 함수 정의
func doSomething(first: (String, String) -> String) {
first("Miro", "JJang")
}
// 제일 기본적인 함수 호출
doSomething(first: { (one: String, two: String) -> String in
return one + two
})
// 파라미터 형식과 리턴 형식을 생략하기
doSomething(first: { (one, two) in
return one + two
})
// Shortand Argument Names의 활용
doSomething(first: {
return $0 + $1
})
// 단일 리턴문만 남았을 경우, return도 생략하기
doSomething(first: {
$0 + $1
})
// 클로져가 마지막 파라미터라면?
doSomething() {
$0 + $1
}
// 최종 생략본
doSomething {
$0 + $1
}
Completion Handler
- 아래와 같이 closure의 trailing closure를 활용해서 사용할 수 있다.
import Foundation
func completionHandlerFunction(name: String, completionHandler: (String) -> ()) {
print(name)
completionHandler(name)
print("핸들러 끝나고", name)
}
completionHandlerFunction(name: "kim") { String in
print("Handler 시작이요~")
print(String)
print("Handler 끝이요~")
}
/*
kim
Handler 시작이요~
kim
Handler 끝이요~
핸들러 끝나고 kim
*/
@autoclosure
- 파라미터로 전달된 일반 구문 & 함수를 클로저로 래핑하는 것
- 단, 파라미터가 없어야된다!
아래의 예에서 함수 정의 상 파라미터가 클로져이지만, @autoclosure
를 통하여 클로져가 아닌 일반 구문도 파라미터로 넘길 수 있게 된다.
// 함수 정의 부
func isTrueOrFalse(closure: @autoclosure () -> ()) {
closure()
}
// 파라미터가 클로져가 아닌 일반 구문이다!
isTrueOrFalse(closure: 1 > 2)
@escaping
- 함수가 끝난 뒤에도 클로저를 실행하고 싶다면! → completion handler에 사용될 때가 많다.
- 변수, 상수에 클로져를 대입하고 싶다면!
- 중첩함수에서 실행 후 중첩 함수를 리턴하고 싶다면!
func doSomething(closure: @escaping () -> ()) {
// 상수에 대입이 가능하다
let function: () -> () = closure
// 함수가 끝난 뒤에도 실행이 가능하다.
DispatchQueue.global().asyncAfter(deadline: .now() + 2) {
function()
}
}
doSomething {
print("Miro")
}
클로져의 값 캡쳐
- 함수, 클로져는 reference type이다.
- 클로저 내부에서 외부 변수를 사용할 때, 해당 변수를 클로져 내부적으로 저장한다.
- ⇒ 외부 변수의 값이 캡쳐되었다.
func doSomething() {
var statement = "Miro!"
// Int => 구조체 => Value Type
var num = 20
let closure = { print(num) }
//클로저 범위 끝
print(statement)
}
Swift) 클로저(Closure) 정복하기(3/3) - 클로저와 ARC
값 캡쳐 방식
- 캡쳐하는 값이 값 타입인지 참조 타입인지와 관계없이
reference capture
를 한다.
아래의 예에서는 num이 Int
이므로 값 타입이다. 그러나 reference capture
가 된다!
import Foundation
func doSomething() {
var num: Int = 0
print("num check #1 = \(num)")
let closure = {
print("num check #3 = \(num)")
}
num = 20
print("num check #2 = \(num)")
closure()
}
doSomething()
/*
num check #1 = 0
num check #2 = 20
num check #3 = 20
*/
만약 아래와 같이 클로져 안에서 값이 변경되면 클로져 외부에서도 값이 같이 변경된다.(완전 Reference Type 같이!)
func doSomething() {
var num: Int = 0
print("num check #1 = \(num)")
let closure = {
num = 20
print("num check #3 = \(num)")
}
closure()
print("num check #2 = \(num)")
}
/*
num check #1 = 0
num check #3 = 20
num check #2 = 20
*/
위의 예에서 closure 속 num은 위의 var num이라고 정의된 num을 memory capture를 한다. 즉, 외부 변수인 num을 계속해서 참조하는 것이다. Memory capture란, num의 주소 값이 closure에게 capture가 되는 것을 의미한다.
값 타입이여도, 클로져 캡쳐를 하면 내부적으로 reference count가 1 증가하기에 heap 영역으로 옮겨집니다.(추측)
아래의 예는 또 다른 Reference Capture
의 예이다.
func makeIncrementer(forIncrement amount: Int) -> () -> Int {
var runningTotal = 0
let incrementer = {
runningTotal = runningTotal + amount
return runningTotal
}
return incrementer
}
let increment = makeIncrementer(forIncrement: 10) // amount를 10으로 고정!
increment() // 10, runningTotal의 주소 값을 가지고 있으므로, runningTotal을 10으로 변경!
increment() // 20, runningTotal(20) = runningTotal(10) + amount(10)
increment() // 30, runningTotal(30) = runningTotal(20) + amount(10)
let alsoIncrement = increment // increment(closure)의 주소 값을 건네준다.
alsoIncrement() // 40, 그래서 위와 같이 동일하게 작동한다.
increment() // 50
캡쳐 리스트(Capture Lists)
- closure가 정의되는 시점에 복사되는 변수들의 list를 이야기합니다. 해당 list에 나열된 변수의 복사본들은 closure가 메모리에서 소멸되는 순간까지 내용이 변경되지 않은 채로 유지됩니다. 즉, closure안에서 활용한 복사본을 만드는 것입니다!
- 따라서 값 타입은 복사가 되면 새로운 stack에 값을 할당하지만, 참조 타입은 복사가 되어도, 주소 값이 복사가 되므로 같이 변경이됩니다. 따라서 참조 타입의 복사일 경우, reference count를 늘리지 않기 위하여 weak OR unowned를 활용해야됩니다!
- 값 타입도 closure의 캡쳐 현상으로 reference count가 1 증가하게 되는데, 이럴 때 값 타입을
Value Capture
를 사용할 수 있는 방법! - 클로져 시작
{
바로 뒤에[ .. ]
를 활용하여 캡쳐할 멤버를 나열 후in
키워드 추가- 단, 캡쳐된 멤버는 상수로 캡쳐가 된다. 따라서 클로져 안에서 변경이 불가능하다.
- 단, 참조 타입은 캡쳐 리스트를 활용해도 무조건 참조 캡쳐를 한다! -> Default로 무조건 강한 참조를 하지만, 캡쳐 리스트를 활용하여 weak, unowned 키워드를 활용하면 약한 참조를 할 수 있게 구현할 수 있다.
func doSomething() {
var num: Int = 0
print("num check #1 = \(num)")
let closure = { [num] in // 외부 변수는 closures 내에서 사용되기 위해 변수가 복사된다.
print("num check #3 = \(num)") // 그리고, 위에서 복사된 변수를 활용한다.
}
num = 40
print("num check #2 = \(num)")
closure()
}
doSomething()
/*
num check #1 = 0
num check #2 = 40
num check #3 = 0
*/
클로저의 강한 순환 참조
- 클로저는 참조타입 → Heap에 저장이된다.
- 클로저가 참조 값을 캡쳐할 때 기본적으로 강한 참조를 한다.
- Human instance는 클로저를 참조하고, 클로저는 Human instance를 참조한다.
class Person {
var name = ""
// 초기화가 진행되기 전에 self로 접근하기에 lazy를 해줘야된다.
lazy var getName: () -> String = {
return self.name
}
init(name: String) {
self.name = name
}
deinit {
print("")
}
}
var miro: Person? = .init(name: "Miro")
print(miro!.getName())
miro = nil // 이래도 deinit이 실행되지 않는다.
해결 방법
- 클로저가 프로퍼티에 접근을 할 때, self를 참조하면서 문제가 발생했다!
- weak/unowned로 캡쳐해버리기!(캡쳐 리스트 활용)
class Person {
lazy var getName: () -> String? = { [weak self] in
return self?.name
}
}
- 아래와 같은 경우에는 클로져 안에서 self에 접근을 할 때, self가 nil이여서 에러가 난다! 따라서 weak를 쓰자
(출처: 소들이님의 블로그 속 댓글)
class Human {
var name = ""
lazy var getName: () -> Void = { [unowned self] in
while true {
self.name += "Sdaq"
if self.name.count > 100 {
self.name = "Sdaq"
}
}
}
init(name: String) {
self.name = name
}
deinit {
print("Human Deinit!")
}
}
var sdaq: Human? = .init(name: "Park-Sdaq")
DispatchQueue.global().async {
sdaq!.getName()
}
sdaq = nil
'Swift' 카테고리의 다른 글
[Swift] self는 언제 쓸까? (0) | 2023.04.13 |
---|---|
[Swift] initializers(생성자) (0) | 2023.03.17 |
[Swift] Enumeration(열거형) (0) | 2023.03.06 |
[Swift] mutating (0) | 2023.02.24 |
[Swift] 값 타입, 참조 타입, let, var (0) | 2023.02.24 |