Opaque 리턴타입(Swift 5.1)

이 글을 다음 문서를 부분 번역한 것입니다.
https://docs.swift.org/swift-book/LanguageGuide/OpaqueTypes.html

Opaque 리턴 타입이 있는 함수나 메소드는 리턴 값에 대한 정보를 숨깁니다. 함수의 리턴 타입에 대한 구체적인 타입 정보를 제시하는 대신에, 리턴 값은 그 것이 따르는 프로토콜만으로 기술됩니다. 타입 정보를 숨기는 것은 모듈이 외부에 내놓는 코드에서 실제 리턴값의 타입은 내부에서만 유지관리될 수 있게 만들기 때문에 유용하게 사용될 수 있습니다. 리턴 값의 타입이 프로토콜 타입인 것과는 달리 Opaque 타입은 동일성을 유지합니다. 그리고 이때 컴파일러는 타입 정보에 액세스할 수 있지만, 모듈의 클라이언트는 그렇게 할 수 없습니다.

Opaque 리턴타입(Swift 5.1) 더보기

Swift 프로퍼티의 옵저버에 대한 규칙

  • lazy 프로퍼티는 옵저버를 가질 수 없다. (단 최초 액세스 이후 변경하는 것은 가능하다.)
  • computed 프로퍼티에 대해서는 옵저버를 설치하는 것이 의미가 없다. 단 상속받은 computed 프로퍼티에 대해서는 옵저버를 설치할 수 있다.
  • 옵저버는 해당 클래스의 지정 이니셜라이저 내에서 값을 변경하는 경우에는 호출되지 않는다.
  • 단, 상속받은 프로퍼티가 옵저버가 설치되어 있다면 부모의 지정 이니셜라이저 호출 후에 변경한다면, 부모의 옵저버가 호출될 것이다.
  • 비슷하게 편의 이니셜라이저에서는 지정 이니셜라이저를 통해 초기화한 후, 프로퍼티값을 다시 변경하면 옵저버가 실행된다.

lazy 프로퍼티와 옵저버

lazy 프로퍼티는 옵저버를 가질 수 없다. 위에서 설명한 옵저버와 관련된 규칙들을 살펴보면, 다음과 같은 가설을 세울 수 있다. 1) 옵저버는 객체 인스턴스의 초기화가 완료된 시점부터 작동한다. 2) 객체의 초기화는 지정 이니셜라이저가 실행을 완료한 시점으로 본다.

그런데 lazy 프로퍼티는 약간 예외적이다. 왜냐하면 지정이니셜라이저가 실행을 완료했다하더라도 느긋하게 초기화되는 프로퍼티는 아직 초기화 되기 전이기 때문이다. 따라서 위 가설에서 1번 항목에 따라 지정 이니셜라이저가 실행되었더라도 옵저버가 동작 가능한 시점이 결정되지 않는다. 아래는 이 부분이 언급된 공식 문서의 내용.


You can add property observers to any stored properties you define, except for lazy stored properties. You can also add property observers to any inherited property (whether stored or computed) by overriding the property within a subclass. You don’t need to define property observers for nonoverridden computed properties, because you can observe and respond to changes to their value in the computed property’s setter. Property overriding is described in Overriding.

상속된 계산 프로퍼티에는 옵저버 설치가 가능하다?

보통 옵저버는 저장 프로퍼티에 설치한다. 계산 프로퍼티의 경우에는 set 하는 과정 자체가 별도의 코드를 실행하는 것이므로 사실상 옵저버가 필요 없다. 하지만 이렇게 set이 가능한 계산 프로퍼티에 대해서 이를 상속받는 서브 클래스는 해당 프로퍼티를 오버라이드하여 didSet, willSet 옵저버만 추가하는 것이 가능하다. (당연히 setter를 오버라이드하는 경우에는 옵저버를 추가할 수 없다.)

Raw 포인터 사용에 대해

https://developer.apple.com/documentation/swift/unsaferawpointer

UnsafeRawPointer 타입은 자동 메모리 관리, 타입 안정성 및 메모리 정렬 보장이 되지 않는 원시 포인터 액세스를 제공합니다. 이 타입을 사용하려면 누수를 피하고, 할당된 메모리의 라이프 사이클을 직접 관리해야 하며, 그 외의 정의되지 않는 동작들을 회피해야 합니다. 수동으로 직접 관리하는 메모리 영역은 특정한 타입에 바운드되거나, 타입이 지정되지 않을 수도 있습니다. 메모리 영역에서 해당 영역이 특정 타입에 묶여있는지 여부와 무관하게 순수 바이트를 액세스하려할 때 UnsafeRawPointer 타입을 사용할 수 있습니다.

막 할당된 Raw 메모리는 타입화되지도 초기화되지도 않은 상태입니다. 이 메모리는 타입화된 연산을 사용하기 전에 반드시 초기화되어야 합니다. (초기화되려면 초기값을 가져야하고, 이는 타입화를 수반해야한다는 의미가 됩니다.) 초기화되지 않은 상태에서 특정 타입에 바인등하려면 bindMemory(to: count:)를 사용합니다. 이 메소드는 타입화된 포인터를 반환하며, 이후에는 해당 포인터를 사용해야 합니다.

Raw 포인터 사용에 대해 더보기

버퍼 포인터 이해하기

C에서 특정한 T타입의 배열은 메모리 상에서 연속적인 공간입니다. 이 때문에 정적 배열이든 동적 배열이든 배열을 액세스하는 것은 필연적으로 포인터와 관련됩니다. 반면 Swift의 배열에서 원소들은 반드시 이런 식으로 배치되지는 않습니다. C의 배열이 단지 원소값이 나란히 배치된 메모리 영역임에 비해 Swift의 배열은 struct로 구성되는 보다 복잡한 내부 구조를 가지고 있습니다.

이 때 T 타입이 차지하는 바이트 수가 고정되어 있으므로 배열의 시작번지와 인덱스 값을 알고 있다면 해당 인덱스에 위치한 값을 액세스할 수 있습니다. C에서 배열 이름은 암묵적으로 배열의 시작번지를 의미하므로, arr[i]로 표현되는 i 번째 원소의 값은 실제 컴파일러는 *(arr + i) 로 변환하여 접근합니다.

Swift에서도 UnsafePointer를 사용하여 포인터를 다룰 수 있는데, 이 때 범위(capacity, Pointee 타입의 메모리 사이즈 x 원소의 개수)내에는 동일타입을 구성하는 값들이 연속하여 배치되어 있습니다. 따라서 (ptr + i).pointee 와 같은 식으로 i 번째 원소에 대해 액세스가 가능합니다. 이것은 C의 접근방법과 매우 유사합니다. 하지만 이것은 단순한 메모리 연속체에서 특정 지점을 액세스하는 법일 뿐, Swift의 배열을 다루는 것과는 차이가 있습니다. Swift의 배열은 원소가 연속해있으면서 Sequence, Collection 프로토콜에 의한 여러가지 연산을 지원받습니다.

버퍼 포인터 이해하기 더보기

Unmanaged 에 대해 – Swift

그 옛날(?) ARC가 없던 시절에 Objective-C 및 코어 파운데이션에서 객체에 대한 참조수 관리는 완전한 수동 방식에 의존하고 있다. 어떤 객체에 대한 retain(참조수를 늘리는 것) 동작은 반드시 그에 수반하는 release(참조수를 내리는 것) 동작을 필요로 했다. 그리고 이 짝이 제대로 맞지 않으면 객체는 필요한 시점에 사라지고 없거나, 반대로 메모리 누수가 발생했다. 그러던 중 자동 참조수 카운팅(ARC)이 도입되었는데, ARC 환경에서는 모든 retain/release/autorelease 콜이 컴파일러에 의해 코드에 자동으로 삽입되었다. 이는 전체적인 코드량의 감수는 물론 개발 난이도를 매우 낮춰주는 역할을 했다.

ARC가 도입된 이후 Objective-C 메소드에 의해 반환되는 모든 Objective-C 객체와 코어 파운데이션 객체의 메모리 관리는 자동으로 이루어졌다. 하지만 C 함수에 의해 리턴되는 코어 파운데이션 객체는 이러한 은혜를 받지 못했다. 따라서 여기에 속하는 객체들에 대해서는 여전히 CFRetain(), CFRelease()를 호출하거나, __bridge*로 시작하는 함수를 통해 Objecitve-C 객체로 브릿징해야 했다.

Unmanaged 에 대해 – Swift 더보기