Noncopyable 유형 소비하기

Park Jong Ho·2024년 8월 17일
0

Copy

swift에서 copy는 타입이 값타입이냐 참조타입이냐에 따라 다르게 동작한다.

Copy on value semantic

값 타입의 경우 copy는 타입의 모든 저장 프로퍼티를 복사한 새로운 인스턴스를 생성한다. 이것을 Deep copy라고도 한다.

즉 위에서 player2는 player1과 독립적인 새로운 인스턴스이므로, player2의 icon을 변경한다고 해서 player1의 icon에도 영향을 끼치진않는다.

Copy on reference semantic

하지만 참조 타입의 경우 copy는 모든 저장 프로퍼티를 복사한 새로운 인스턴스를 생성하는게 아니라, Heap 메모리에 저장된 기존 인스턴스를 똑같이 참조하는 새로운 pointer가 생기는것이다.
이것을 shallow copy라고 한다

즉 여기선 player2의 icon을 변경하면 player1의 icon도 똑같이 변한다. 왜냐면 두 pointer는 똑같은 인스턴스를 참조하고있기 때문이다. 그리고 여기서 똑같은 인스턴스를 player1과 player2가 참조하고 있으므로, reference count는 2가된다.

Deep copy on reference semantic

보통 우리는 동시에 여러 곳에서 상태가 공유되길 원할 때 참조 타입을 사용한다.
또한 shallow copy라는 특성때문에 저장 프로퍼티가 많은 타입의 경우 참조 타입을 사용하는게 성능 상으로 좋을 수도 있다. 왜냐하면 값 타입의 경우 모든 저장 프로퍼티를 복사하기 때문에, 저장 프로퍼티가 많으면 많을수록 copy에 많은 시간이 소요될 수 있기 때문이다.

그라나 참조타입을 사용하면서도 deep copy를 하고싶을 수 있는데, 이럴 땐 아래와 같이 새로운 생성자를 선언할 수 있다.

final class PlayerClass {
    var icon: String

    init(from otherPlayer: PlayerClass) {
        self.icon = otherPlayer.icon
    }
}

reference type을 deep copy하는 것이 사실 copy on write(COW)의 본질이다.

Copyable protocol

swift에서 기본적으로 모든 타입은 복사가 가능하다. swift에선 이것을 새로운 Copyable 프로토콜 타입으로 설명한다. Sendable과 마찬가지로, Copyable 프로토콜은 아무런 member requirement가 없다.

swift에선 타입 뿐 아니라 모든 것이 암시적으로 Copyable 프로토콜을 채택한다.

예를 들면, 아래 예시들이 모두 암시적으로 Copyable 프로토콜을 따른다.

  • 모든 타입
  • 모든 generic parameter
  • protocol과 associated type
  • boxed protocol type

Error prone

그러나 copyable한 특성을 사용할 땐 실수를 만들기 쉽다.

예를 들어 은행의 transfer를 모델링한다고 생각해보자. real world에선 transfer는 3가지의 상태를 가질 수 있다.

  • pending
  • cancelled
  • complete

그리고 transfer를 스케줄링하는 schedule이라는 function을 선언하자.

final class BankTransfer {
    func run() {
        // ...
    }
}

func schedule(
    _ transfer: BankTransfer,
    _ delay: Duration
) async throws {
    if delay < .seconds(1) {
        transfer.run()
    }

    try await Task.sleep(for: delay)
    transfer.run()
}

schedule 함수는 만약 delay가 1초 미만이라면 transfer를 바로 실행하고, 아니면 지정된 delay만큼 기다렸다가 transfer를 실행하는 함수다.

이 코드에는 중대한 버그가 하나있는데, transfer가 두 번 이상 실행되면 안되지만 첫 번째 transfer.run() 후에 return을 하지않기 때문에 delay가 1초 미만인 경우 transfer가 두 번 이상 실행될 수 있다.

BankTransfer에서 run 메소드가 두 번 이상 실행되는 것을 방지하기 위해, Transfer의 상태를 나타내는 새로운 저장 프로퍼티를 선언하고, run 함수에서 그것을 체크하도록 변경해보자.

final class BankTransfer {
    private var completed = false
    func run() {
        guard !completed else {
            return
        }
        // ...

        completed = true
    }
}

이제 Transfer가 두 번 이상 실행되진 않겠지만.. 여기선 schedule 함수엔 여전히 버그가 하나 있는데 그건 바로 Task.sleep 함수 때문이다. 만약 schedule 함수를 실행하는 Task가 취소되면 해당 함수는 Error를 던지고, 만약에 callee에서 해당 error를 적절히 처리하지 않으면 Transfer는 영원히 Pending 상태로 남게된다.

let transferTask = Task {
    let bankTransfer = BankTransfer()

    do {
        try await schedule(bankTransfer, .seconds(2))
    } catch {
        // Should handle error throwed by schedule function
    }
}

transferTask.cancel()

이걸 방지하기 위해서, BankTransfer가 deinit 메소드에서 Transfer를 취소해줄 수 있다.

final class BankTransfer {
    private var completed = false
    func run() {
        guard !completed else {
            return
        }
        // ...

        completed = true
    }

    func cancel() {
        //...
    }

    deinit { cancel() }
}

언뜻보면 문제가 모두 해결된 것 같다. 하지만 특정 코드가 bankTransfer의 인스턴스를 계속 참조하고 있는다면.. deinit은 불리지않고 BankTransfer는 적절히 완료되지않은 상태로 프로그램을 계속 떠돌게 될 것이다.

let transferTask = Task {
    let bankTransfer = BankTransfer()

    do {
        log.append(bankTransfer)
        try await schedule(bankTransfer, .seconds(2))
    } catch {
        // Should handle error throwed by schedule function
    }
}

위 코드에선 로깅을 위해 BankTransfer의 인스턴스를 복사(retain)했다.

Noncopyable

위 예시의 근본적인 문제는 BankTransfer이라는 타입을 무분별하게 복사할 수 있다는 점이다.

때때로 특정 타입은 Copyable 특성을 없애는게 훨씬 도움이 될 수도 있다. 그래서 swift에선 Noncopyable이라는 새로운 특성을 소개한다.

Noncopyable을 설명하기 위해, FloppyDisk라는 타입을 새로 생성해보겠다.

struct FloppyDisk: ~Copyable {
    
}

swift에서 특정 타입을 Noncopyable로 만들기 위해선 ~Copyable 키워드를 사용할 수 있다.

이제 FloppyDisk는 Noncopyable 타입이기 때문에, 아래와 같이 system이 가지고있는 인스턴스를 새로운 backup에 할당하는 코드는 copy 대신 consume(값을 복사하는 대신 소유권만 이전함)을 수행하게된다.

맨 초반에 swift에서 값 타입의 copy는 모든 저장 프로퍼티를 복사한 새로운 인스턴스를 생성하는 것이라고 했는데, consume은 copy 하는게 아니라 원래있던 값의 소유권만 이전한 것이다.

여기서 consume이라는 키워드는 명시적으로 작성할 수도 있지만, Noncopyable 타입이기 때문에 작성하지 않아도 상관없다. 컴파일러가 알아서 소유권만 옮겨준다.
system이 갖고있던 FloppyDisk의 소유권이 backup으로 옮겨진 상태이기 때문에, load(system) 함수를 호출하면 에러가 발생한다.

Ownership of Noncopyable type

이제 아래와 같이 새로운 디스크를 생성해서 반환하는 함수가 있다고 가정해보자.

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

func format(_ disk: FloppyDisk) {

}

위 코드에서 format 함수는 일반적으로 우리가 선언하는 함수의 시그니쳐를 가지고 있다. 만약 FloppyDisk가 Copyable 타입인 경우는 format 함수는 아무런 문제가 없다.
format 함수는 필요하다면 FloppyDisk의 copy본을 받아서 함수를 실행할 수 있다.

그러나 FloppyDisk는 필요한 경우 format 함수에서 인스턴스를 복사할 수 없기 때문에, format 함수가 FloppyDisk의 소유권을 어떻게 처리할지 명시해야한다.

consume

첫 번째로 명시할 수 있는 ownership은 consume이다. consume을 사용하면 format 함수가 disk의 ownership을 가져오게 된다. 그리고 consume은 소유권을 다시 caller에게 돌려주지 않는다.

func format(_ disk: consuming FloppyDisk) {

}

format 함수가 disk의 ownership을 가지기 때문에, 일반적인 함수와 달리 format 함수에서 disk를 직접 수정할 수도 있다.

func format(_ disk: consuming FloppyDisk) {
    disk.format = "NTF32"
}

하지만.. format 함수를 실행한 이후에도 result는 사용해야하기 때문에, value의 ownership을 다시 caller에게 돌려주지 않는 consuming ownership은 format 함수에 적합하지 않다.

borrow

두 번째로 명시할 수 있는 ownership은 borrow이다. borrow를 사용하면 format 함수가 disk의 ownership을 잠시 빌려온다. 함수를 종료하고나면, 소유권을 다시 caller에게 돌려준다.

func format(_ disk: borrowing FloppyDisk) {
}

ownership을 잠시 빌려오는 것이기 때문에, format 함수에서는 disk에 대해 read only access만 허용한다.

consuming과의 차이점은, borrowing한 argument는 수정할 수 없고, copy만 할 수 있는데, 알다시피 FloppyDisk는 Noncopyable이기 때문에 복사할 수 없다.

inout(mutating)

마지막으로 명시할 수 있는 ownership은 inout이다. inout은 caller의 변수에 임시 write-access를 제공하며, 함수가 종료된 후 FloppyDisk의 소유권을 다시 caller에게 돌려준다.

func format(_ disk: inout FloppyDisk) {
    disk.format = "NTF32"
}

즉 format 함수는

  • FloppyDisk를 수정할 수 있어야 하고,
  • format 함수 이후 FloppyDisk의 소유권을 다시 caller에게 전달해야 하기 때문에,

FloppyDisk에 대한 onwership은 mutating이 맞다.

Consumable resources

위에서 배운 Noncopyable, Ownership 내용을 토대로, BankTranser 예제를 다시 수정해보자. 마지막으로 작성했던 BankTransfer 코드는 아래와 같다.

final class BankTransfer {
    private var completed = false
    func run() {
        guard !completed else {
            return
        }
        // ...

        completed = true
    }

    func cancel() {
        //...
    }

    deinit { cancel() }
}

요구사항은 아래와 같다.

  • BankTransfer 객체는 기본적으로 run이 한 번 호출되면 더 이상 사용불가능해야한다. 즉 Consumable resource다.
  • BankTransfer의 인스턴스가 해제될 때 Transfer를 Cancel 해야함
struct BankTransfer: ~Copyable {
    consuming func run() {
        //...
    }
}

변경점은 아래와 같다.

  • BankTransfer가 NonCopyable struct로 바뀜: class는 NonCopyable일 수 없음
  • run method가 caller로 부터 self의 소유권을 가져오는 consuming function으로 변경됨

이제 BankTransfer는 복사될 수 없기 때문에 항상 unique하며, run 메소드를 두 번 이상 절대 호출할 수 없기 때문에 complete 프로퍼티를 활용한 assertion도 필요없다.

Noncopyable한 BankTransfer의 인스턴스가 해제되는 시점도 이제 명확하기 때문에.. struct 임에도 불구하고 deinit을 활용할 수 있다.

struct BankTransfer: ~Copyable {
    consuming func run() {
        //...
    }

    consuming func cancel() {
        //...
    }

    deinit { cancel() }
}

그러나 이렇게되면 run 함수를 정상적으로 실행했음에도 불구하고 deinit으로 인해 Transfer가 cancel 될 수 있는데.. 이것을 방지하기 위해 swift 5.9에선 discard self 라는 키워드를 소개한다.

discard self는 Noncopyable type의 consuming 함수에서만 사용할 수 있으며, deinit 호출없이 인스턴스를 해제한다.

struct BankTransfer: ~Copyable {
    consuming func run() {
        //...
        discard self
    }

    consuming func cancel() {
        //...
    }

    deinit { cancel() }
}

주의할 점

BankTransfer는 지금 deinit 시점에 cancel 함수를 호출하고 있다. 그러나 cancel 함수에선 discard self를 호출해주지 않는데, 이렇게 되면 deinit - cancel - deinit - cancel... 무한 루프에 빠지게 된다.

따라서 consuming cancel 함수에서도 discard self를 호출해주자

schedule 함수 리팩토링하기

요구사항에 맞게 BankTransfer 타입도 잘 리팩토링했기 때문에, 버그를 만들던 schedule 함수도 리팩토링해보자.

func schedule(
    _ transfer: BankTransfer,
    _ delay: Duration
) async throws {
    if delay < .seconds(1) {
        transfer.run()
    }

    try await Task.sleep(for: delay)
    transfer.run()
}

schedule 함수는

  1. transfer의 run 함수를 두 번 이상 호출하는 버그와,
  2. Task.sleep 중 Task가 취소된 경우 에러를 던져서, transfer가 영원히 pending 상태로 남는

두 가지 버그가 있었다.

우선 NonCopyable 타입을 parameter로 받는 경우에, 소유권을 어떻게 처리해야할지 명시해야한다. 이 함수에선 BankTransfer를 consume 할 것이기 때문에 consuming 키워드를 추가하자.

func schedule(
    _ transfer: consuming BankTransfer, // 'transfer' consumed more than once 
    _ delay: Duration
) async throws {
    if delay < .seconds(1) {
        transfer.run()
    }

    try await Task.sleep(for: delay)
    transfer.run()
}

consuming 키워드를 추가하면, 컴파일러가 BankTransfer가 두 번 이상 consume 되었다고 알려주는데, 1번 이슈를 컴파일러 단에서 체크하고 수정을 요구하는 것이다. 이 경우엔 if 문 안에서 return을 추가하면 된다.

func schedule(
    _ transfer: consuming BankTransfer,
    _ delay: Duration
) async throws {
    if delay < .seconds(1) {
        transfer.run()
        return
    }

    try await Task.sleep(for: delay)
    transfer.run()
}

이제 1번 이슈가 해결되었을 뿐만 아니라, 2번 이슈도 schedule이 NonCopyable 타입인 Transfer를 사용하는 마지막 owner이기 때문에, 혹여나 Task가 취소되어 Task.sleep이 에러를 뱉더라도, Transfer의 deinit 함수가 호출되어 적절히 cancel된다.

Onwership 관련 주의할 사항

consume은 소유권을 이전하는 키워드이고, 실제로 BankTransfer의 경우 consuming function인 run을 호출하면, 더 이상 run을 다시 호출할 수는 없었다.

그러나 이건 BankTransfer가 Noncopyable이기 때문이고, 만약 BankTransfer가 Copyable한 객체라면 이야기가 달라진다.

struct BankTransfer {
    consuming func run() {}
    consuming func cancel() {}
}

func schedule(
    _ transfer: consuming BankTransfer,
    _ delay: Duration
) async throws {
    if delay < .seconds(1) {
        transfer.run()
        return
    }

    try await Task.sleep(for: delay)
    transfer.run()
}

func main() async throws {
    let bankTransfer = BankTransfer()

    // consuming function에 두 번이나 같은 Value를 넘겼는데.. 아무런 에러가 발생하지 않는다.
    try await schedule(bankTransfer, .seconds(1))
    try await schedule(bankTransfer, .seconds(2))
}

위 예제에선 BankTransfer가 Copyable한 타입으로 변경되었다.
그리고 BankTransfer를 consume하는 schedule 함수를 두 번이나 호출하지만 아무런 컴파일 에러가 발생하지 않는다.

이유가 뭘까? 왜냐하면 컴파일러가 schedule 함수엔 bankTransfer 의 복사본을 넘기기 때문이다..

좀 더 이해하기 위해서 다른 예제를 살펴보자.

func testConsuming() {
    var array = [1,2,3,4,5]
    append6ToArray(array)
    print(array) // [1, 2, 3, 4, 5]
}

func append6ToArray(_ arr: consuming [Int]) {
    arr.append(6) // [1, 2, 3, 4, 5, 6]
    print(arr)
}

위 코드에서 매개변수로 넘어온 array를 consuming 하는 append6ToArray에 array를 넘기고 난 뒤, 배열의 요소를 출력해보면, testConsuming 함수의 array는 아무런 변화가 없는 것을 볼 수 있다. 즉 array의 복사본이 append6ToArray 함수로 넘어간 것이다.

swift evolution 문서에서도 이에 대해 설명하고 있다.

그러나 여기서 우리가 append6ToArray로 넘길 때, 직접 array를 consume 하면 정상적으로 array의 copy본이 생성되지 않고 소유권이 함수로 이전된다.

또한 아래 예제를 봐도 Copyable한 타입의 consuming, mutating 함수가 어떻게 동작하는지 볼 수 있다.

struct OnwershipTest {
    var value: Int = 10

    consuming func addValueWithConsuming() {
        value += 1
    }

    mutating func addValueWithMutating() {
        value += 1
    }
}

var ownershipTest = OnwershipTest()
ownershipTest.addValueWithConsuming()
print(ownershipTest.value) // 10

ownershipTest.addValueWithConsuming()
print(ownershipTest.value) // 10

ownershipTest.addValueWithConsuming()
print(ownershipTest.value) // 10

ownershipTest.addValueWithMutating()
print(ownershipTest.value) // 11

즉 consuming function은 항상 매개변수로 전달되는 타입의 소유권을 가져와서 인스턴스를 해제하지만 전달되는 타입이 Copyable이면 복사본을 가져와서 consume 해버리고.. (사실상 거의 의미가 없는 것 같다). 타입이 Noncopyable이면 caller의 데이터를 copy하지않고 본래 데이터의 소유권을 직접 가져와서 해제해버린다.

Noncopyable generics

원래 Noncopyable type을 generic에 쓰지 못했는데, swift 6부터 가능하다. Noncopyable generic은 swift의 기존 generic model 위에 지어졌기 때문에, 기존 generic에 대해 먼저 상기해보자.

  • swift의 모든 타입을 Universe type이라고 한다면, String, Command와 같은 타입이 이 집합안에 존재하며, Runnable과 같은 프로토콜 타입은 새로운 부분집합을 형성한다.
  • 그리고 Command 타입이 Runnable을 채택하기 때문에, Runnable의 부분집합 안에 존재한다.
  • execute 함수는 T 라는 제네릭 파라미터를 가지고있는 함수이다. T라는 제네릭 파라미터는 Universe 안에있는 어떤 원소로도 대체될 수 있다.
  • 일전에 swift의 모든 것들은 암시적으로 Copyable하다고 했었는데, 따라서 사실 T도 Copyable 한 타입이어야 하고, Noncopyable type이 등장하기 전엔 Universe 집합이 Copyable 집합과 동일했다.

Broader space

  • 하지만 Noncopyable 타입이 등장했기 때문에, Universe가 확장되었다.
  • BankTransfer의 경우 Noncopyable 타입이기 때문에, Copyable 바깥에 존재한다.
  • (원문해석) 그렇다면, Copyable은 어떻게 tilde Copyable 내에 포함될까요? 이전과 마찬가지로, 이 더 넓은 공간 내에서는 특정 타입이 Copyable을 준수한다고 가정할 수 없습니다. 해당 타입이 Copyable일 수도 있지만, 그렇지 않을 수도 있습니다. 이것이 바로 tilde Copyable을 해석해야 하는 방식입니다.

이제 Noncopyable generic에 대해서 알아보자.

  • Runnable 프로토콜은 Copyable이기 때문에, BankTransfer 타입은 Runnable이 될 수 없다.
  • BankTransfer도 Runnable 프로토콜을 채택하고 싶고, Runnable protocol이 꼭 Copyable 할 필요는 없기때문에 Runnable 프로토콜의 Copyable constraint를 제거하자.

  • Runnable protocol의 Copyable constraint를 제거하면, 이제 BankTransfer도 Runnable 프로토콜을 채택할 수 있다.
  • 여기서 Command는 Runnable & Copyable 이지만, BankTransfer는 Runnable & Noncopyable이다.
  • execute 함수의 경우 T는 여전히 Copyable constraint를 가지고 있기 때문에, Runnable & Copyable 한 Command 타입만 execute 함수의 인자로 전달할 수 있다.
  • 따라서 execute 함수의 T generic parameter에서 Copyable constraint를 제거하자.

  • 이제 execute function은 Runnable 프로토콜을 채택하면서, Copyable & Noncopyable 타입 둘 다를 받을 수 있다.
  • 즉 이제 BankTransfer와 Command가 모두 execute 함수의 generic type으로 사용될 수 있으며, 여기서 볼 수 있는 Noncopyable generic의 키포인트는 보통 특정 제약조건은 허용되는 타입의 범위를 더 좁게 만들지만, ~Copyable은 허용되는 타입의 범위를 더 넓게 만든다는 점이다.

Put all of that theory into pratice

  • 위 예제에서 저장 프로퍼티로 사용되는 Action 타입은 Noncopyable 할 수 있다.
  • 하지만 Job이 Copyable하기 때문에, 저장 프로퍼티로 Noncopyable한 타입을 가질 수 없다.
  • Copyable한 타입에서 Noncopyable 타입을 저장 프로퍼티로 갖는 두 가지 방법이 있는데,
    1. Copyable한 타입이 Class 이거나, (Class Copy는 사실 retain만 하기때문)
    2. ~Copyable 을 사용해 Copyable constraint를 없애는 것이다.

여기선 두 번째 옵션을 사용한다.

하지만 여기서 Job의 Action으로 여전히 Copyable & Runnable 타입인 Command를 넣을 수 있다. 또한 이전에 ~Copyable이란 키워드가 타입을 무조건 Noncopyable로 만드는게 아니고 Copyable constraint를 없애주는 것임을 봤기 때문에, 만약 Action이 Copyable 하다면 Job도 Copyable 하게 만들고 싶을 수도 있다.

그걸 이해하기 위해 아래 예제를 보도록 하자

func runEndlessly(_ job: Job<Command>) {
    while true {
        let current = job
        current.action?.run()
    }
}
  • 위 예제에서, Command는 Copyable 타입이지만, Job이 Noncopyable 타입이기 때문에 위 코드는 성립하지 않는다. 왜냐하면 함수에서 Noncopyable type에 대한 소유권을 명시해야하기 때문이다.
  • 만약 Action이 Copyable 할 때 Job도 Copyable하게 만들고 싶다면, 아래처럼 작성할 수 있다.
extension Job: Copyable where Action: Copyable {}

이제 Job<Command> 는 Copyable 하기 때문에, runEndlessly 함수도 정상적으로 컴파일된다.

Extensions

이번에는 NonCopyable type의 extension에 대해서 알아보자. 이를 위해, Job의 action을 반환하는 getter 메소드를 만들어보자.

struct Job<Action: Runnable & ~Copyable>: ~Copyable {
    var action: Action?
}
extension Job { // where Action: Copyable
    func getAction() -> Action? {
        return action
    }
}

// 1
func inspectCmd(_ cmdJob: Job<Command>) {
    let a = cmdJob.getAction()
    let b = cmdJob.getAction()
}

// 2
func inspectXfer(_ transferJob: borrowing Job<BankTransfer> {
    let a = transferJob.getAction() // method requires that `BankTransfer` conform to `Copyable`
}

새로 생성한 getAction() 이라는 함수는 extension에 속한 함수이다. swift는 모든 곳에 Copyable을 붙인다고 했기 때문에, getAction을 선언한 extension엔 사실 Action이 Copyable하다는 표현이 숨겨져있다.

따라서 getAction은 Action이 Copyable한 타입일 때만 사용할 수 있는 함수인 것이다.

1번 함수를 보면, Command는 Copyable한 타입이기 때문에 정상적으로 컴파일 되는 반면, 2번 함수의 경우 BankTransfer가 NonCopyable 타입이기 때문에, getAction 함수를 호출할 수 없다.

여기서 말하고자 하는 의도는, 특정 타입의 extension에서 Generic parmaeter엔 항상 Copyable이 붙는다는 사실이다. 또한 해당 extension에서는 Self도 또한 Copyable한 제약조건이 암시적으로 추가된다.

Advantages

extension에서 Self와 generic parameter에 자동으로 Copyable한 특성이 붙기 때문에 우리는 Job을 손쉽게 사용할 수 있다.

예를 들어서 Job이 우리가 작성한 타입이 아니고, JobKit이라는 모듈에 있는 타입이라고 가정해보자.
이제 Copyable한 프로토콜인 Cancellable을 선언하고, extension을 사용해 Job 타입이 해당 프로토콜을 채택하도록 만들어보자.

extension protocol conformance는 아무런 에러없이 정상적으로 작동하는데, 그 이유는 Job의 extension에 Action이 Copyable하다는 제약조건이 암시적으로 추가되기 때문이다. Job은 Action이 Copyable 하다면 자신또한 Copyable한 타입이 되므로, 문제없이 Copyable한 Cancellable 프로토콜을 채택할 수 있는 것이다.

만약 Cancellble이라는 프로토콜 또한 ~Copyable을 사용해 제약조건을 없애버린다면, 코드를 단순히 아래와 같이 수정하면 된다.

Wrap up

  1. ~Copyable 은 타입을 무조건 Noncopyable로 만드는게 아니라.. Copyable constraint를 없애는 것뿐이다.
  2. Noncopyable 타입을 사용해 program의 정확도를 높일 수 있다...
  3. swift는 이미 Optional, UnsafePointer, Result 에 Noncopyable generic을 적용했다.
profile
iOS 개발자입니다.

0개의 댓글