보통 self
키워드를 사용하게 되는 경우는 해당 코드가 “인스턴스 변수” 인지 또는 “지역 변수” 인지를 구분짓기 위해서 사용한다.
func events(at date: Date) -> [Event] {
if let events = cache[date] {
return events
}
let events = fetchEvents(at: date)
cache[date] = events
return events
}
예를 들어 이런 코드가 있다고 하면, 어떤 것이 인스턴스 프로퍼티인지 구분하기 힘들다.
func events(at date: Date) -> [Event] {
if let events = self.cache[date] {
return events
}
let events = self.fetchEvents(at: date)
self.cache[date] = events
return events
}
이렇게 self 키워드를 명시 해 줌으로서 명시적으로 표시할 수 있다.
그렇다면 항상 self 를 명시 해 줘야 하는 걸까? 그렇진 않다. 실제로 위의 코드에서 self 를 빠뜨리더라도 컴파일러 에러가 뜨진 않는다.
옵젝씨에서는 self 키워드를 붙여줘야 하는 경우가 더 많았기에, 옵젝씨에 익숙한 개발자들은 항상 붙여주는 경향도 있는 듯 하다.
하지만, self
키워드를 붙이지 않으면 컴파일러 에러가 뜨는 두가지 경우가 있다.
escaping closure
내부에서 (강한 참조 사이클을 만드는 것을 피하기 위해)각 경우에 대해 알아보자!
struct Person {
let name: String
init(name: String) {
self.name = name
}
}
이러한 경우에 이니셜라이저의 파라미터로 받은 name과 인스턴스 프로퍼티인 name 을 구분짓기 위해,
컴파일러는self
키워드를 강제한다!
여기까지는 굉장히 익숙하고 간단하다. 문제는 escaping closure...
공식문서의 closure 파트에 해당 내용이 명시되어 있다.
An escaping closure that refers to
self
needs special consideration ifself
refers to an instance of a class.
Capturingself
in an escaping closure makes it easy to accidentally create a strong reference cycle.
Normally, a closure captures variables implicitly by using them in the body of the closure, but in this case you need to be explicit.
If you want to captureself
, writeself
explicitly when you use it, or includeself
in the closure’s capture list.
Writingself
explicitly lets you express your intent, and reminds you to confirm that there isn’t a reference cycle.
클래스의 escaping 클로저 내부에서 self에 해당하는 변수, 메서드에 접근한다면:
self
가 클래스의 인스턴스를 참조한다. → self
를 캡쳐하게 된다. → 강한 참조 사이클을 만들기 쉽다.
이러한 구조이므로, 해당 케이스에는 각별히 조심해야 한다
즉, 클래스의 escaping 클로저 내부에서 인스턴스 프로퍼티, 메서드를 사용할 시 강한 참조 사이클이 일어나기 쉽기 때문에, 해당 클로저가 self
를 캡쳐한다는 것을 “명시적으로 표시하기 위해” self
키워드를 강제한다!
참고) 이전에는 모든
escaping
클로저에서self
가 강제되었지만, 강한 순환 참조가 발생할 가능성이 있는class
의escaping
클로저에서만self
가 강제되도록 변경되었다고 한다.
(스위프트 5.3 - SE-0269)
⭐️ 또한 주의해야 할 개념은, 여기서 escaping closure 라고 함은 @escaping 키워드를 붙이지 않아도
클래스 내부에서 해당 클로저를 다른 변수에 담거나, 함수의 리턴으로 받거나.. 하는 경우들에도 escaping 을 할 가능성이 있기에 self 키워드를 강제한다!
func someEscapingFunction(completion: @escaping () -> ()) {
completion
}
func noneEscapingFunction(completion: () -> ()) {
completion
}
class TestClass {
var number = 1
init(number: Int) {
self.number = number
}
func callEscaping() {
someEscapingFunction {
print(self.number) // self 명시하지 않으면 complie error 발생
}
noneEscapingFunction {
print(number)
}
}
func returnClosure() -> (() -> ()) {
return {
print(self.number) // self 명시하지 않으면 complie error 발생
}
}
func putClosureToVariable() {
var closureVariable = someEscapingFunction {
print(self.number) // self 명시하지 않으면 complie error 발생
}
closureVariable = noneEscapingFunction {
print(number)
}
}
}
struct TestStruct {
var number = 1
init(number: Int) {
self.number = number
}
func callEscaping() {
someEscapingFunction {
print(number)
}
}
func returnClosure() -> (() -> ()) {
return {
print(number)
}
}
func putClosureToVariable() {
let closureVariable = someEscapingFunction {
print(number)
}
}
}
클로저는 해당 클로저 블록 바깥 부분의 (함수 외부의) 값들을 사용할 수 있는데,
그 바깥 부분의 해당 원본 값이 사라져도 클로저 내부에서 계속 해당 값들을 사용할 수 있게 값을 “캡쳐” 해서 저장한다.
그리고, Closure는 값을 캡쳐할 때 Value/Reference 타입에 관계 없이 항상 Reference Capture
를 한다.
ex) 클로저를 통해 비동기 콜백을 작성하는 경우, 현재 상태를 미리 캡쳐해서 저장 해 두지 않으면 실제로 클로저의 기능을 실행하는 순간에는 상수나 변수에 접근하지 못할 수 있다.
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 #2 = 20
// num check #3 = 20 (값이 참조되지 캡쳐됐음. Int 인데도 복사되지 않고...)
클로저의 캡쳐 현상에서 클로저는 항상 Reference Capture
를 한다고 했는데,
이 때 Value Type이 (원래 값 타입의 작동하듯이) 값을 복사해서 Capture 하게끔 하고 싶을 때 사용함
Value Capture 를 해주고 싶은 변수를 [ ] 내부에 리스트로 담는 것 → 캡쳐 리스트!
참고1) 캡쳐리스트로 복사해서 캡쳐할 때, 마치 파라미터가 함수 내부에서 상수 (let) 이 되듯이, 상수 값으로 캡쳐해서 저장함
참고2) Reference Type 은 캡쳐리스트로 담더라도 Value Capture 가 불가함
func doSomething() {
var num: Int = 0
print("num check #1 = \(num)")
let closure = { [num] in
print("num check #3 = \(num)")
}
num = 20
print("num check #2 = \(num)")
closure()
}
// num check #1 = 0
// num check #2 = 20
// num check #3 = 0 (값이 참조되지 않았음)
클로저의 강한 순환 참조 문제를 해결하려면 weak & unowned 를 활용함
이 때, Reference Type일 땐 필요 없는 것처럼 보였던 캡쳐 리스트가 필요함
weak & unowned + Capture List
@escaping
키워드란?completionHandler
로 사용되는 클로저는 파라미터 타입 앞에 @escaping
이라는 키워드를 명시해야 한다.