[Swift] self 키워드는 언제 반드시 사용해야 할까?

Mason Kim·2023년 1월 10일
0

swift

목록 보기
3/7

참고: When to Write self in Swift

보통 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 키워드를 붙이지 않으면 컴파일러 에러가 뜨는 두가지 경우가 있다.

  1. 이니셜라이저 내부에서 (불확실성을 피하기 위해 강제함)
  2. escaping closure 내부에서 (강한 참조 사이클을 만드는 것을 피하기 위해)

각 경우에 대해 알아보자!

이니셜라이저 내부의 self

struct Person {
	  let name: String
	  init(name: String) {
		    self.name = name
	  }
}

이러한 경우에 이니셜라이저의 파라미터로 받은 name과 인스턴스 프로퍼티인 name 을 구분짓기 위해,
컴파일러는 self 키워드를 강제한다!

여기까지는 굉장히 익숙하고 간단하다. 문제는 escaping closure...

class의 escaping closure 내부의 self

공식문서의 closure 파트에 해당 내용이 명시되어 있다.

An escaping closure that refers to self needs special consideration if self refers to an instance of a class.
Capturing self 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 capture self, write self explicitly when you use it, or include self in the closure’s capture list.
Writing self 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 가 강제되었지만, 강한 순환 참조가 발생할 가능성이 있는 classescaping 클로저에서만 self 가 강제되도록 변경되었다고 한다.
(스위프트 5.3 - SE-0269)

⭐️ 또한 주의해야 할 개념은, 여기서 escaping closure 라고 함은 @escaping 키워드를 붙이지 않아도
클래스 내부에서 해당 클로저를 다른 변수에 담거나, 함수의 리턴으로 받거나.. 하는 경우들에도 escaping 을 할 가능성이 있기에 self 키워드를 강제한다!

테스트 코드 - 정말 class만 self 를 강제할지

  • 정말 class + escaping 하는 클로저 조합의 경우에만 강제하는 것을 확인할 수 있었다
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)
        }
    }
}

참고) 캡쳐현상, 캡쳐리스트, escaping 이란?

출처: https://babbab2.tistory.com/83

  • “클로저의 캡쳐 현상”
    • 클로저는 해당 클로저 블록 바깥 부분의 (함수 외부의) 값들을 사용할 수 있는데,
      그 바깥 부분의 해당 원본 값이 사라져도 클로저 내부에서 계속 해당 값들을 사용할 수 있게 값을 “캡쳐” 해서 저장한다.

    • 그리고, 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이라는 키워드를 명시해야 한다.
profile
iOS developer

0개의 댓글