이 포스팅은 Swift 시리즈 20 편 중 3 번째 글 입니다.
목차
클로저
- 코드의 블럭
- 일급 시민
- 변수, 상수로 저장이 가능
- 전달 인자로 전달이 가능
- 함수에서 return 할 수 있어야 한다.
- 예를 들어 String 자료형은 이런 것이 가능하겠지
- 기존에는 함수가 이러한 일급 객체가 아니었지만, 함수형 프로그래밍에서는 이 방식을 채택하여 좀더 자유롭고 유연한 프로그래밍이 가능하게 된다.
- 함수 : “이름이 있는 클로저”라고 생각할 수 있다.
// 일반 함수
func sumFunction(a: Int, b: Int)->Int {
return a+b
}
var sumFunctionResult: Int = sumFunction(a: 2, b: 4)
print(sumFunctionResult) // 6
// 클로저
let sum: (Int, Int)-> Int = { (a: Int, b: Int) in
return a+b
}
let sumClosureResult = sum(1, 4)
print(sumClosureResult)
- 잘 보게되면 변수에 함수를 할당하는 그림이다.
- 이것이 가능한 이유는 일급 시민이기 때문이다.
- 이 때 이 함수의 형태 자체를 자료형, 클래스, 구조체등을 명시하는 것과 같이 선언해준다.
- 그리고 해당 함수부의 내용은 righthandside에 적어준다.
- 이 때, 함수의 parameter를 나타낸 뒤, in 이라는 키워드를 적고 내용을 적으면 된다.
함수의 전달인자로서의 클로저
- 이러한 클로저는 특정 함수를 실행하는데 있어 전달인자로 함수를 줘야할 경우 많이 사용하게 된다.
let add: (Int, Int) -> Int
add = { (a: Int, b: Int) in
return a + b
}
let subtract: (Int, Int) -> Int
subtract = { (a: Int, b: Int) in
return a - b
}
let divide: (Int, Int) -> Int
divide = { (a: Int, b: Int) in
return a / b
}
func calculate(a: Int, b: Int, method: (Int, Int) -> Int) -> Int {
return method(a, b)
}
print(calculate(a: 50, b: 10, method: add)) // 60
print(calculate(a: 50, b: 10, method: subtract)) // 40
print(calculate(a: 50, b: 10, method: divide)) // 5
- 또한 이렇게 함수를 클로저로 정의하고 사용하지 않고 바로 작성할 수도 있다.
print(calculate(a: 50, b: 10, method: { (left: Int, right: Int) -> Int in
return left*right
})) // 500
클로저 고급
- 후행 클로저
- 반환 타입 생략
- 단축 인자 이름
- 암시적 반환 표현
이 네가지를 배워볼 것이다. 말이 어려울 뿐 코드로 이해하자.
// 클로저를 매개변수로 갖는 함수
func calculate(a: Int, b: Int, method: (Int, Int) -> Int) -> Int {
return method(a, b)
}
var result: Int // calculate의 값을 받을 변수
후행 클로저
// 후행 클로저
// 클로저가 함수의 마지막 전달인자일 때,
// 마지막 매개변수의 이름을 생략하고 함수의 소괄호 외부에 클로저를 구현할 수 있다.
result = calculate(a: 10, b: 10, method: { (left: Int, right: Int) -> Int in
return left + right
})
print(result) // 20
result = calculate(a: 10, b : 10) { (left: Int, right: Int) -> Int in
return left + right
}
print(result) // 20
위에 적혀있는 것이 기본적으로 알고 있는 클로저의 사용방법이다. 그런데 클로저가 해당 함수의 마지막 위치에 있을 경우, 조금 변용된 방법으로 사용할 수가 있는데, 소괄호 외부에 중괄호로 연 뒤, 안에 클로저를 구현해서 같은 결과를 얻을 수 있다.
반환 타입 생략
// 반환타입 생략
// calculate라는 함수는 이미 closure 함수가 Int형을 반환한다는 것을 알고 있다.
// 그렇기 때문에 이를 생략할 수 있다. 다만 in 키워드는 생략 불가
result = calculate(a: 10, b: 10, method: {(left: Int, right: Int) -> Int in
return left + right
})
print(result) // 20
// 후행 클로저에도 적용가능
result = calculate(a: 10, b: 10) { (left: Int, right: Int) in
return left + right
}
print(result) // 20
- 클로저를 파라미터로 받는 함수의 경우, 이미 어떤 형식의 클로저가 들어올지 명시를 해두었기 때문에 가능한 방법이다.
- 해당 함수를 콜할 때, 클로저의 반환 타입을 생략하여도 제대로 함수를 적었다면 동작한다.
단축 인자 이름
// 단축 인자이름
// 굳이 closure에 들어오는 매개변수이름이 필요없다면 명시하지 않고 $를 사용해 접근할 수 있다.
// 이 때 in 키워드는 사용할 필요가 없다.
result = calculate(a: 10, b: 10, method: {
return $0 + $1
})
print(result) // 20
// 후행 클로저와도 가능!
result = calculate(a: 10, b: 10) {
return $0 + $1
}
print(result) // 20
- 클로저에서 사용하는 인자 이름이 필요없다면 단축 인자이름을 사용할 수 있다.
- 클로저의 매개변수의 순서대로
$0
,$1
,$3
과 같이 사용한다.
암시적 반환 표현
// 암시적 반환 표현
// 클로저를 파라미터로 받는 함수에서 클로저의 반환형이 있다면
// 클로저의 마지막 줄의 결과값은 암시적으로 반환값으로 취급한다.
result = calculate(a: 10, b: 10) {
$0 + $1
}
print(result) // 20
// 한줄 표현도 가능
result = calculate(a: 10, b: 10) { $0 + $1 }
print(result) // 20
- 이전 함수에서 클로저의 반환형을 정의했다면, 굳이 반환하지 않아도 클로저의 마지막줄을 반환값으로 취급한다.
클로저에서 값 캡쳐란?
- 클로저는 내부함수와 내부함수에 영향을 미치는 주변 환경을 모두 포함한 객체이다.
- 말이 어렵다.
func doSomething() {
var message = "Hi i am sodeul!"
//클로저 범위 시작
var num = 10
let closure = { print(num) }
//클로저 범위 끝
print(message)
}
- 이 상황에서 message라는 변수는 클로저 안에서 사용하지 않으니 클로저 안에서 내부적으로 저장하지 않음
- num은 사용하니, 해당 클로저에서 값을 저장함
- 이 익명함수는 기본적으로 reference type으로 동작한다고 함
- 이 때, num이라는 값을 클로저 내부적으로 저장한다는 것을 값이 캡쳐되었다. 라고 함
- 그럼 값을 어떻게 캡쳐하는데?
값 캡쳐 방식
- 값을 캡쳐할 때, Value/Reference 타입에 관계없이 Reference Capture 한다.
- 일반적으로 struct, enum, int와 같은 타입은 값타입이다.
- 즉 stack 영역에 저장이 된다는 것.
- 사실 struct 값이 커지면 heap 에 저장되기도 함
- 근데 클로저는 이 값들을 참조한다는 것
- 일단 클로저는 heap 영역에 저장될 것이고(참조 타입)
- 그 안에서 변수의 주소를 들고 있다는 것
- 이를 Referece Capture라 함
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()
}
- 만약이렇게 있다면, 클로저는
num
이라는 변수의 주소를 들고 있음 - 이말은, 해당 클로저에서 값을 변경하게되면 해당 변수의 값도 변경된다는 이야기
캡쳐 리스트
- Value type으로 값을 캡쳐하자.
- 클로저의 시작인 { 옆에
[]
을 이용해 캡쳐할 멤버를 나열한다.
let closure = { [num1, num2] in
// something..
}
- 값 타입으로 변수를 복사하고 싶은 경우 이렇게 리스트 형식으로 명시적으로 적어주면 된다.
- 그런데 문제는, 이렇게 Value Type으로 캡쳐한 경우,
- 선언할 당시의 num 값을 Const Value Type으로 캡쳐한다.
- 아마 멀티 프로세스 환경에서 병렬 처리 목적으로 이러한 부분을 명시하지 않았나 싶다.
- 따라서 변경이 불가하다.
- 추가
- 상태 값에 따라서 프로그래밍은 문제가 많이 발생한다.
- 이런 방법을 사용하면 상태 값에 독립적으로 연산이 가능
- 그럼 Reference Type 값도 Capture List에 작성하면 어떻게 되지?
class Human {
var name: String = "Sodeul"
}
var human1: Human = .init()
let closure = { [human1] in
print(human1.name)
}
human1.name = "Unknown"
closure()
- 이 코드에서 human1은 분명 reference type이다.
- 이 상태에서 human1 을 캡쳐하면, 복사가 될까?
- 출력 결과
Unknown
- 안된다. 즉 여전히 Reference type은 Reference Capture한다.
- 어떻게 하면 원하는 동작을 할 수 있게 할까?
Closure & ARC
- 클로저와 인스턴스간의 관계
- 클래스 안에서 클로저를 변수로 갖는 형태를 만들어 보자.
class Human {
var name = ""
// lazy : 사용되기 전까지는 연산이 되지 않음
lazy var getName: () -> String = {
return self.name
}
init(name: String) {
self.name = name
}
deinit {
print("Human Deinit!")
}
}
var wansik: Human? = Human(name: "choiwansik")
print(wansik?.getName)
wansik = nil
- lazy의 이유
- 쟤는 지금 프로퍼티임
- 프로퍼티는 인스턴스가 생성될 때 무조건 초기화가 되어있는 상태여야 함
- 멤버 메소드는 인스턴스가 초기화가 되어 있다는 가정이 되어 있기 때문에 해당 블록 안에서 self 사용이 가능
- 지금 쟤는 프로퍼티로서 할당이 되어 있는데, 초기화 단계에서 self 를 쓴다는 것이 말이 안됨
- 그러니까 self를 찾을 수 없다고 뜸
- 이렇게 만들어 놓고, getName이라는 지연 저장 프로퍼티를 호출해보았다.
- 그리고 해당
wansik
인스턴스가 필요없어서 nil을 할당했다. - 내가 원하는 동작은, 지금 heap 공간에 있는 Human 인스턴스 주소를 wansik이라는 변수가 참조하고 있는데, 이 참조를 끊었으니 reference count가 0이되어 heap 공간에서 할당 해제 되는 것임
- 하지만… deinit 함수는 호출되지 않는다.
클로저의 강한 순환 참조
- 클로저는 참조 타입
- Heap에 상주
- getName을 호출하면, getNAme 클로저가 Heap에 할당
- 그리고 그 클로저를 해당 Human 인스턴스의 프로퍼티가 참조
- 자, 이상황이면, 클래스 인스턴스가 클로저를 참조, 클로저는 인스턴스를 참조
Human instance | closure |
---|---|
2 wansik 변수, closure내에서 참조 |
human instance 변수인 getName이 참조 |
- 이런 상황이 만들어짐
- 그런데..! 이 때 RC를 보면 human instance의 RC가 2임
- 응? 그럼 결국 closure가 **강한 참조** 를 한다는 것!
- 와우 클래스 내에서 클로저를 만드는 경우 상당히 조심해야 함.
클로저의 강한 순환 참조 해결
- 이제 배운 것을 다 섞어야 함
- weak & unowned 와 캡쳐 리스트를 섞어야 함
- 앞에서 캡쳐리스트를 써도 값 타입으로 복사가 안된다는 것을 기억할 것
- 핵심은 간단하다.
- 클로저에서 인스턴스를 참조할 때, 참조하는 인스턴스의 RC를 안늘려주면 되지.
class Human {
lazy var getName: () -> String? = { [weak self] in
return self?.name
}
}
class Human {
lazy var getName: () -> String = { [unowned self] in
return self.name
}
}
- 차이점을 보면, weak의 경우 리턴이 옵셔널이다.
- unowned의 경우 리턴이 옵셔널이 아니다.
- weak인 경우, 무조건 옵셔널 타입이기 때문에 옵셔널 체이닝을 해주어야 한다.
- 그리고 그렇기 때문에 리턴 타입도 옵셔널이어야 한다.
- 그런데 unowned같은 경우에는 일단 Non optional type이다.
- swift 5.0에서는 Optional Type이 되긴한다고 한다.
- 하지만 기본은 그렇게 동작하지 않나보다.
- 의미도 unowned는, 참조하는 인스턴스의 생존 길이가 더 길 때 사용한다고 되어 있기 때문에, 해당 값이 클로저를 실행하고 난후에 있다는 것이 보장된다.
- 그렇기 때문에 리턴 타입을 옵셔널을 쓰지 않아도 된다!
- 좀더 의미가 맞게 사용이 가능하고, 옵셔널 바인딩을 안써도되니 코드가 깔끔해진다.
Closure는 여러개이다.
- Named Closure
- 전역 함수
- 중첩 함수
- Unnamed Closure
지금까지 한 것은 익명함수가 전부였다. 이번에는 나머지에 대해 알아보자.
- 전역 함수
func
쓰고 작성하는 함수- 일반적으로 사용하는 함수
- 어떠한 값도 캡쳐하지 않음
- 중첩 함수
- 자신을 포함하고 있는 함수의 값을 캡쳐함
func outer() {
var num: Int = 0
func inner() {
print(num)
}
}
inner()
함수는 자신을 포함하고 있는 outer 함수의 num이라는 값을 캡쳐한다.- 당연히 이렇게 쓰면 reference capture다.
- 안그러려면 캡쳐 리스트 사용
@escaping
- 기본적으로 지금까지 봐왔던 closure는 모두 non-escaping closure이다.
- 즉,
- 함수 내부에서 직접 실행하기 위해서만 사용된다.
- 그래서~ 파라미터로 받은 클로저를 변수, 상수에 대입할 수 없다.(실행 목적이니까)
- 중첩 함수에서 클로저를 사용할 경우 중첩 함수를 리턴할 수 없다.
- 함수의 실행 흐름을 탈출하지 않아, 종료되기 전에 무조건 실행되어야 한다.
- 원래는 클로저는 1급 객체이기 때문에, 변수 할당, 리턴, 매개변수로 넘길 수 있어야 했는데 그게 안된다는 것
@escaping
- 이런 에러가 뜬다.
- 또한 함수의 흐름을 탈출하지 않는다는 말은, 함수가 종료된 후에 클로저가 실행될 수 없다는 말이다.
- 이렇게 클로저 함수가 함수 끝나고 실행되기 때문에 에러가 난다.
- 또, 중첩함수 내부에서 매개 변수로 받은 클로저를 사용할 경우 리턴이 불가하다.
왜 나눈걸까?
- 결국 클로저를 함수 내부에서 실행하지 않을 경우, 클로저가 갖는 특징
- 리턴
- 할당
- 반환
- 을 사용할 수 없다는 것이다.
- 왜??
- 이건 컴파일러의 입장을 들어봐야 한다.
- 만약 non escaping인 경우, 클로저가 해당 함수 내에서만 사용된다는 것이 보장되기 때문에, 메모리 관리를 지저분하게 하지 않아도 되어 성능이 향상되기 때문이다.
- non escaping의 경우, 함수가 종료되면 동시에 클로저도 사용이 끝나기 때문에 클로저도 사용이 종료
- escaping의 경우 함수가 종료되어도 실제 클로저가 사용되지 않을 때까지 메모리를 추적해야 한다.
- 비동기로 통신 처리할 때, 함수가 끝난 후에 다른 스레드로 넘어가서 해당 클로저를 실행하기 때문에
@escaping
으로 처리해야 한다.