iyOmSd/Title: Swift

[Swift] WWDC24 Consume noncopyable types in Swift

냄수 2024. 11. 11. 19:34
반응형

Consume noncopyable types in Swift 세션을 정리한 글입니다.

 

Copying

struct Player {
  var icon: String
}

func test() {
  let player1 = Player(icon: "🐸") //1
  var player2 = player1 //2
  player2.icon = "🚚" //3
  assert(player1.icon == "🐸")
}

위와같은 코드가있을때 1단계식보면

  1. 플레이어1 🐸 생성
  2. 플레이어2도 1과 동일하게 생성, 플레이어1의 복사본을 만든다는 의미 (player1 = player2 = 🐸)
  3. 플레이어2의 아이콘변경시 플레이어1로부터 독립적인 플레이어를 변경하는것임 (player1 = 🐸, player2 = 🚚)

하지만 Player가 참조타입이라면 어땟을까요?

class로 변경하면

플레이어2는 객체 자체가아니라 참조를 복사하게됨

얕은 복사라고도하며, 두 플레이어 모두 동일한 객체를 참조하므로 아이콘을 변경하면 모두에대해 변경됨

참조유형이 값유형처럼 동작하게 만들수있음

 

class PlayerClass {
  var data: Icon
  init(_ icon: String) { self.data = Icon(icon) }

  init(from other: PlayerClass) {
    self.data = Icon(from: other.data)
  } 
}

깊은복사를 수행하도록 이니셜라이저를 정의하면됨

실제로, 이것이 COW(Copy-On-Write)의 핵심이죠 독립성이 부여되므로 값 유형과 동일한 동작이 구현됩니다

새로운 타입을 디자인할 때 그 값을 깊이 복사할 수 있는지 여부를 이미 제어할 수 있습니다

제어할 수 없는 것은 Swift가 자동 복사본을 만들 수 있는지 여부입니다

Copyable은 타입이 자동으로 복사될 수 있는 기능을 설명하는 새로운 프로토콜입니다
Sendable과 마찬가지로 멤버 요구 사항이 없습니다

 

Swift에서는 모든 것이 기본적으로 Copyable이라고 추론됩니다

모든 것이 그렇습니다 모든 유형은 자동으로 Copyable을 준수하려고 합니다
모든 일반 매개변수는 개발자가 입력하는 유형이 Copyable일 것을 자동으로 요구합니다
모든 프로토콜 및 associatedtype은 구체적인 유형이 Copyable을 준수할 것을 자동으로 요구합니다
any 프로토콜 유형도 Copyable로 자동 구성됩니다.

Copyable을 직접 작성할 필요가 없습니다 눈에 보이지 않더라도 이미 존재합니다

 

 

Noncopyable types

Copyable이라는 단어 앞에 물결 기호(~)를 사용해 Copyable에 대한 기본 준수를 억제할수있음

struct FloppyDisk: ~Copyable {}

func copyFloppy() {
  let system = FloppyDisk()
  let backup = system
  load(system)
  // ...
}

backup이 system을 복사하려고하면

consume은 system메모리의 콘텐츠를 backup메모리로 이동시킴

이후 system을 읽으면 아무것도없어서 오류발생

struct FloppyDisk: ~Copyable { }

func newDisk() -> FloppyDisk {
  let result = FloppyDisk()
  format(result) // <<<
  return result
}

func format(_ disk: FloppyDisk) {
  // ...
}

format을 호출하면 변수 결과가 어떻게 될까요?

알기 어렵습니다 함수의 서명에서 해당 디스크에 대해 어떤 소유권이 필요한지 선언하지 않기 때문입니다

copyable 매개변수의 경우 이에 대해 생각할 필요가 없었죠 format이 디스크의 복사본을 효과적으로 수신하니까요

하지만 noncopyable 매개변수의 경우에는 함수가 해당 값에 대해 어떤 소유권을 갖는지 선언해야 합니다 복사할 수 없기 때문입니다

 

consuming

첫 번째 소유권 종류는 consuming라고 합니다 함수가 호출자로부터 인수를 가져갈 것임을 의미합니다 이것은 개발자 소유이므로 변이시킬 수도 있습니다

func format(_ disk: consuming FloppyDisk) {
  // ...
}

하지만 여기서 소비를 사용하면 문제가 될 수 있습니다

format이 디스크를 돌려주지 않고 아무것도 반환하지 않기 때문이죠

하지만 생각해 보면 디스크 포맷에는 임시 접근 권한만 필요합니다

 

borrowing

임시 접근 권한이 있다는 것은 해당 항목을 borrowing하는 것입니다 차용하면 인수에 대한 읽기 권한이 부여되죠, let 바인딩처럼

실제로 거의 모든 매개변수와 메서드는 Copyable 유형에 이렇게 작동하죠

func format(_ disk: borrowing FloppyDisk) {
  var tempDisk = disk
  // ...
}

명시적으로 차용된 인수는 소비나 변이가 불가능하고 복사만 할 수 있다는 것입니다(읽기만가능) format 함수는 결국 디스크를 변이시켜야 하므로 이를 위해 차용을 사용할 수도 없습니다

inout

마지막 소유권 종류는 inout입니다! 또는 메서드에서 이와 동등한 것은 written mutating입니다

func format(_ disk: inout FloppyDisk) {
  var tempDisk = disk
  // ...
  disk = tempDisk
}

inout은 호출자의 변수에 대한 임시 쓰기 접근 권한을 제공합니다

개발자는 쓰기 접근 권한이 있어 매개변수를 소비할 수 있습니다

하지만 함수가 종료되기 전 어느 시점에 inout 매개변수를 다시 초기화해야 합니다

return할 때 호출자는 값을 기대하기 때문입니다

 

문제상황 as-is

startPayment 함수를 자세히 살펴보세요

이체가 예약되었는지 추적하기 위해 이체의 복사본을 유지합니다

이것은 문제입니다 이체의 모든 복사본이 파기되어야 이체의 deinit이 실행되니까요

이것이 문제의 근원입니다

프로그램에 이 이체의 복사본이 몇 개나 존재하는지를 제어할 수 없습니다

값을 복사하는 기능이 유형의 올바른 기본값인 경우가 많지만

일부 상황에서는 유형이 noncopyable인 것이 더 낫습니다

 

해결 to-be

위의 문제에 noncopyable를 적용해보면

먼저, BankTransfer를 복사할 수 없는 struct로 지정하고 그 run 메서드를 consuming으로 표시하여 자신을 위해 호출자로부터 값을 가져오도록 했습니다

이 두 가지 변경 사항만으로 더 이상 assert가 필요 없습니다

Swift는 같은 이체에 run 메서드를 두 번 호출할 수 없음을 보장하죠

실제로, 소유권은 매우 정밀하게 추적되므로 소유권이 ’실행’되는 대신에 파기되면 작업을 트리거하는 deinit을 struct에 추가할 수 있습니다

’run’ 메서드의 종료도 자동으로 자신을 파기하므로 discard self를 작성하겠습니다

그러면 deinit을 호출하지 않고 파기합니다

 

transfer 매개변수에 대한 소유권을 추가해야 합니다

’schedule’은 마지막 사용이므로 소비로 지정하는 것이 좋습니다

이제 컴파일해 보면 Swift에서 버그가 0이 된 것을 볼 수 있죠

if 구문은 누락되므로 이체를 두 번 소비할 수 있습니다 return을 추가하면 이를 방지합니다

이제 나머지 다른 버그는 어떨까요? 이 버그도 정의를 통해 해결되었습니다

Schedule은 해당 이체의 마지막 소유자이므로 Sleep가 발생하면 deinit이 실행되며 이로 인해 이체가 취소됩니다

 

Generics

noncopyable 유형은 프로그램의 정확성을 높이는 데 유용합니다

일반 코드를 비롯해 모든 곳에서 사용할 수 있습니다 Swift 6에서는 이제 가능합니다

noncopyable 제네릭을 사용해서요! 이는 새로운 제네릭 시스템이 아닙니다

Swift의 기존 제네릭 모델에 기반합니다

 

다이어그램으로 포함관계를 알아봅시다

Command를 Runnable을 준수 하도록 확장하면 해당 점이 Runnable 공간으로 이동합니다

execute라는 이 일반 함수를 사용해 이에 대해 생각해 보죠 꺾쇠괄호 안에 있는 T를 확인하세요

새로운 일반 유형 매개변수를 선언하여 이 우주의 어떤 유형을 나타내지만 어떤 것인지는 모릅니다

제가 Copyable은 어디에나 있다고 말했던 것을 기억하시나요?

이 T에 대한 기본 제약 조건이 있습니다 Copyable을 준수하기 위해 입력 유형이 필요합니다

 

Command는 기본적으로 Copyable을 준수하며 Runnable도 Copyable에서 가져옵니다

실제로, 최근까지 Swift의 유형 우주 전체가 위장된 상태의 Copyable이었습니다

즉, 이 공간의 모든 유형이 execute 함수에 전달될 수 있죠 유일한 제약 조건이 T가 Copyable을 준수한다는 것이니까요

 

따라서 Command가 Runnable도 준수하더라도 이는 여전히 이 더 넓은 공간인 Copyable에 존재합니다

여기서 특정 유형은 Runnable일 수 있지만, 그렇지 않을 수도 있죠

 

하지만 execute 함수 구현을 위해 사실 T가 Runnable이기를 원합니다

run 메서드를 호출해야 하기 때문입니다 따라서 where 절을 사용해 T에 Runnable 제약 조건을 추가합니다

Runnable공간에는 Command가 포함되지만 이제 String은 제외됩니다

String에는 Runnable 준수가 부재하기 때문입니다

 

Swift 5.9부터는 우주가 확장되었습니다 nonCopyable 유형이 있기 때문입니다

특정 유형이 Copyable을 준수한다고 가정할 수 없습니다

Copyable일 수 있지만 그렇지 않을 수도 있습니다 이러한 방식으로 ~Copyable을 읽어야 합니다

Any 유형은 어떨까요?

항상 Copyable이었으며 그래야 합니다 거의 모든 프로그래밍 언어에서 any 유형은 Copyable입니다

 

BankTransfer는 Copyable이 아니니 Runnable일 수도 없습니다

하지만 BankTransfer가 준수하기를 원하니 일반적으로 사용할 수 있죠

 

~Copyable을 추가해 Copyable 제약 조건을 Runnable에서 제거합니다

Copyable 공간이 Runnable을 포함하지 않고, 이와 중복됩니다

Command는 Runnable 및 Copyable로 해당 중복 내에 놓이죠

다음으로, BankTransfer를 Runnable 준수하도록 확장하면 그 점이 Copyable이 아닌 상태로 Runnable 내로 이동합니다

 

일반 함수인 execute를 다시 살펴보겠습니다

일반 매개변수 T에 대해 여전히 기본 제약 조건이 있습니다

따라서 Runnable이면서 동시에 Copyable인 Command 등의 유형만 execute에서 허용됩니다

 

~Copyable을 사용해 Copyable 제약 조건을 T에서 제거해 보죠

제약 조건을 제거하면 허용 유형이 모든 Runnable 유형으로 확장되죠

execute 함수는 T가 Copyable이 아닐 수 있음을 나타내고 있습니다

이것이 핵심적인 내용입니다


noncopyable을 다른 noncopyable 안에 저장하는 두 방법이 있습니다

  • class 복사는 참조만 복사하므로 class 내부에 있어야 하거나(클래스로 변경하기)
  • 포함하는 타입 자체에 대해 ~Copyable을 준수합니다

 

 

 

Extensions

Job의 일반적인 확장 프로그램을 사용해 추가하겠습니다

이를 호출하면 잘 작동하지만 Action의 복사본을 제공하지 않나요?

그렇습니다 작업을 반환하면 작업이 복사되죠 이는 확장 프로그램의 오류가 아닙니다

이 일반 확장 프로그램은 기본적으로 Action이 Copyable인 Job으로 제한되기 때문입니다

모든 확장 프로그램은 이렇게 작동합니다

확장된 유형 범위의 모든 일반 매개변수는 Copyable로 제한되며 여기에는 프로토콜의 Self가 포함됩니다

 

이렇게 동작하면 이점이 있습니다.

Job이 실제로 제가 작성하지 않은 JobKit 모듈의 일부라 가정해 보죠

Cancellable 유형을 설명하기 위한 프로토콜이 있습니다 noncopyable 유형이 무엇인지 모르지만

어쨌든 Job이 준수하기를 원한다고 가정해 보겠습니다

이 확장 프로그램을 작성할 수 있고 잘 작동할 테니 괜찮습니다

프로토콜 준수는 기본적으로 Action이 Copyable이라는 조건으로 지정되기 때문입니다

일반적으로 Action은 그렇지 않을 수 있으니까요

또한 Action이 Copyable이면 Job도 같아서 Cancellable을 준수하죠

Job 유형을 게시할 수 있으며 Copyable 유형으로만 작업하는 프로그래머가 이를 사용할 수 있죠

 

이 확장 프로그램이 Copyable 여부에 관계없이 모든 작업에 적용되기를 원한다면 어떨까요?

이 확장 프로그램에서 Action의 Copyable 제약 조건을 해제합니다

Action을 Copyable로 가정 않고 Job이 Cancellable을 준수합니다

반응형