Open kmh5038 opened 6 months ago
클로저
는 사용자의 코드 안에서 전달되어 사용할 수 있는 로직을 가진 중괄호 구분된 코드의 블럭입니다. 클로저는 두 가지 종류가 있습니다.
이름이 있는 함수(Named Closure), 익명함수(Unnamed Closure)
우리가 보통 부르는 함수는 이름이 있는 함수(Named Closure)로 클로저라 부르지 않지만 사실은 클로저입니다.
func thinkBig() {
print("Closure")
}
우리가 평소에 부르는 클로저는 익명함수(Unnamed Closure)를 클로저라 부릅니다.
let thinkBig = { print("Closure") }
후행 클로저는 클로저 표현을 간소화 하는 방법입니다. 클로저를 좀 더 쓰기 편하게, 보기 편하게 하기 위한 방법입니다. 함수의 마지막 파라미터가 클로저일 때, 이를 파라미터 값 형식이 아닌 함수 뒤에 붙여 작성하는 문법입니다. 이때, Argument Label은 생략됩니다.
func closureFunc(closure: () -> ()) {
// 함수 내부에서 클로저 실행
}
이런 코드를
closureFunc {
// 클로저의 내용을 여기에 작성
}
이렇게 간략하게 표현하여 가독성을 향상 시킬 수 있습니다.
캡처 리스트는 값 타입 일때는 클로저 내부에서 클로저 외부의 값을 참조할 때 참조하는 값이 변경되면 클로저 내부에서도 참조하는 값 또한 바뀌게 되므로 이를 방지하고자 주로 사용되고, 참조 타입 일때는 클로저의 강한 참조 순환 문제를 해결하기 귀해 사용됩니다.
클로저 내부에서 외부의 값을 참조할 때 의 예시입니다.
var n = 0
var numberPrint = {
print("반환값: \(n) 입니다.") // 클로저 내부에서 외부 변수 사용
}
numberPrint() // 반환값: 0 입니다.
n = 10
numberPrint() // 반환값: 10 입니다.
n = 100
numberPrint() // 반환값: 100 입니다.
위 코드처럼 클로저 내부에서 외부 변수를 캡처를 하는 경우인데 변수의 주소값을 힙(Heap) 영역에 저장하기 때문에 값이 변경되면 클로저 내부에도 변경이된다.
캡처 리스트에 의한 캡처의 예시입니다.
var n = 0
var numberPrint = { [n] in // 캡처 리스트 구현
print("반환값: \(n) 입니다.") // 클로저 내부에서 외부 변수 사용
}
numberPrint() // 반환값: 0 입니다.
n = 10
numberPrint() // 반환값: 0 입니다.
n = 100
numberPrint() // 반환값: 0 입니다.
이 경우는 외부 변수 값 자체를 힙(Heap) 영역에 저장하여 외부 변수에 다른 값을 할당하더라도 내부 값이 변경되지않습니다.
강한 참조가 문제가 되는 예시를 설명하겠습니다.
class Person {
var name: String
var run: (()->Void)?
init(name: String) {
self.name = name
}
func runClosure() {
run = {
print("\(self.name)이 달리고 있습니다.")
}
}
deinit {
print("\(self.name) 메모리에서 제거되었습니다.")
}
}
func doSomething() {
var phang: Person? = Person(name: "이창준") // phang 인스턴스 생성 (phang RC 1증가)
phang?.runClosure() // 클로저(run)가 메모리의 Heap 영역에 생성
}
doSomething()
결과 : 최종적으로 phang 인스턴스의 카운트 1, 클로저(run)의 카운트 1 인스턴스와 클로저가 강한 참조 사이클을 유지하고 있기 때문에 소멸자가 작동하고 있지 않습니다.
메모리 누수를 해결한 예시입니다.
class Person {
var name: String
var run: (()->Void)?
init(name: String){
self.name = name
}
func runClosure() {
run = { [weak self] in
print("\(self?.name)이 달리고 있습니다.")
}
}
deinit {
print("\(self.name) 메모리에서 제거되었습니다.")
}
}
func doSomething() {
var phang: Person? = Person(name: "이창준") // phang 인스턴스 생성 (phang RC 1증가)
phang?.runClosure() // 클로저(run)가 메모리의 Heap 영역에 생성
}
doSomething() // 이창준 메모리에서 제거되었습니다.
결과 : 최종적으로 phang 인스턴스의 카운트 0, 클로저(run)의 카운트0 이기때문에 소멸자 작동
func thinkBig(completion: () -> ()) {
completion()
}
thinkBing {
print("study hard")
}
이런 코드처럼 우리가 일반적으로 아무런 키워드 없이 파라미터로 받는 클로저를 모두 non-escaping 클로저라 부릅니다.
func thinkBig(completion: () -> ()) {
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
completion()
}
}
같은 코드를 3초 뒤에 실행하고 싶어 위와 같이 수정을 하면
이런 에러가 발생합니다. 그 이유는 이름 그대로 탈출이 불가능한 클로저이기 때문에 함수의 "흐름"을 탈출 하지 않는 클로저 이기 때문입니다.
한 마디로 함수가 종료되고 나서 클로저가 실행될 수 없고, 함수가 종료되기 전에 클로저가 사용되어야합니다.
class example {
var property: (() -> ())
func closureFunc(_ closure : () -> ()) {
self.property = closure // error
}
}
따라서 위와 같이 함수 외부의 변수에 값을 할당하는 이런 경우도 에러가 발생합니다.
non-escaping 클로저에서 함수의 흐름을 벗어날 수 없던 경우에 @escaping 키워드를 붙여 함수를 탈출하여 사용할 수 있습니다.
func sodeul(completion: @escaping () -> ()) {
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
completion()
}
}
이렇게 사용을하면 이 클로저는 함수의 실행 흐름에서 벗어나도 상관 없이 실행되는 클로저이기 때문에 에러가 발생하지 않습니다.
class example {
var property : (() -> Void)?
func closureFunc(_ closure : @escaping () -> Void ) {
self.property = closure
}
}
이 에시 또한 함수 외부의 변수에 할당을 해주고싶을때 @escaping 키워드를 사용하여 사용할 수 있습니다.
escaping 클로저는 이러한 특징 때문에 비동기작업을 시행할 때 주로 사용이 많이됩니다.