티스토리 뷰

Apple/Swift

[Swift 5.9] Noncopyable structs and enums

doyeonjeong_ 2024. 1. 11. 19:51

원문 : https://www.hackingwithswift.com/swift/5.9/noncopyable-structs-and-enums

 

SE-0390에서는 복사할 수 없는 구조체와 열거형 개념을 도입하여 구조체나 열거형의 단일 인스턴스를 여러 곳에서 공유할 수 있게 되었으며, 궁극적으로 소유자는 여전히 한 명이지만 이제 코드의 여러 부분에서 액세스할 수 있게 되었다.

 

중요:

이 변경 사항에는 여러 가지 미묘한 점이 있으므로 아래에서 명확히 설명하려고 노력했지만,

몇 가지 사항을 몇 번 읽어야 한다고 해도 놀라지 말자.

 

첫째, 이 변경 사항에는 요구 사항을 억제하는 새로운 구문인 ~Copyable이 도입되었다. 이는 "이 유형은 복사할 수 없습니다"라는 의미이며, 이 억제 구문은 현재 다른 곳에서는 사용할 수 없다.(?) 예를 들어 ~Equatable 을 사용하여 유형에 대해 ==를 선택 해제할 수 없다. 따라서 다음과 같이 복사할 수 없는 새로운 User 구조체를 만들 수 있다:

struct User: ~Copyable {
    var name: String
}

 

참고: 복사할 수 없는 유형은 Sendable 이외의 프로토콜을 준수할 수 없다.

 

User 인스턴스를 생성하면 복사할 수 없는 특성으로 인해 이전 버전의 Swift와는 매우 다르게 사용된다.

예를 들어, 이런 종류의 코드는 특별한 것이 없는 것처럼 보일 수 있다:

func createUser() {
    let newUser = User(name: "Anonymous")

    var userCopy = newUser
    print(userCopy.name)
}

createUser()

 

하지만 User 구조체를 복사할 수 없는 것으로 선언했는데, 어떻게 newUser의 복사본을 가져올 수 있을까요?

정답은 '복사할 수 없다': newUseruserCopy에 할당하면 원래의 newUser 값이 소비되어, 소유권이 이제 userCopy에 속하기 때문에 더 이상 사용할 수 없게 된다는 뜻이다. print(userCopy.name)print(newUser.name)으로 변경하려고 하면 Swift에서 컴파일러 오류가 발생하는데, 이는 허용되지 않는다.

 

복사할 수 없는 유형을 함수 매개변수로 사용하는 방법에도 새로운 제한이 적용된다:

SE-0377에 따르면 함수는 값을 소비하여 함수가 완료된 후 호출 사이트에서 값을 무효화할 것인지, 아니면 값을 차용하여 코드의 다른 차용 부분과 동시에 모든 데이터를 읽을 수 있도록 할 것인지를 지정해야 한다.

따라서 사용자를 생성하는 함수를 하나 작성하고, 그 데이터에 대한 읽기 전용 액세스 권한을 얻기 위해 사용자를 차용하는 다른 함수를 작성할 수 있다:

func createAndGreetUser() {
    let newUser = User(name: "Anonymous")
    greet(newUser)
    print("Goodbye, \(newUser.name)")
}

func greet(_ user: borrowing User) {
    print("Hello, \(user.name)!")
}

createAndGreetUser()

 

반대로 greet() 함수가 consuming User를 사용하도록 했다면 print("Goodbye, \(newUser.name)")는 허용되지 않으며, Swift는 greet()가 실행된 후 newUser 값을 유효하지 않은 것으로 간주할 것이다. 반대로, 메서드를 사용하면 객체의 수명이 종료되어야 하므로 객체의 속성을 자유롭게 변경할 수 있다.(?)

 

이러한 공유 동작은 복사 불가능한 구조체에 이전에는 클래스와 액터에만 국한되었던 강력한 기능을 부여한다.

복사 불가능한 인스턴스에 대한 최종 참조가 파괴될 때 자동으로 실행되는 이니셜라이저를 제공할 수 있다.

 

중요: 이것은 클래스의 이니셜라이저와 약간 다르게 동작하는데, 이는 초기 구현상의 결함일 수도 있고 고의적인 동작일 수도 있다.

먼저 클래스에 이니셜라이저를 사용하는 코드를 살펴보자:

 

class Movie {
    var name: String

    init(name: String) {
        self.name = name
    }

    deinit {
        print("\(name) is no longer available")
    }
}

func watchMovie() {
    let movie = Movie(name: "The Hunt for Red October")
    print("Watching \(movie.name)")
}

watchMovie()

 

이것이 실행되면 "Watching The Hunt for Red October"와 "The Hunt for Red October is no longer available"가 출력된다. 

하지만 타입의 정의를 'class Movie'에서 'struct Movie: ~Copyable'로 변경하면 두 개의 print() 문이 역순으로 실행되어 영화가 더 이상 제공되지 않는다고 표시된 다음 시청 중이라는 메시지가 표시된다.

 

복사 불가능한 타입 내부의 메서드는 기본적으로 차용이지만 복사 가능한 타입처럼 mutating으로 표시할 수 있으며, 메서드가 실행된 후 값이 유효하지 않다는 의미로 consuming으로 표시할 수도 있다.

 

예를 들어, 비밀 요원들이 한 번만 재생할 수 있는 자폭 테이프를 통해 임무 지시를 받는 영화 및 TV 시리즈 '미션 임파서블'을 예로 들어보자. 이런 소모적인 방식에 딱 맞는다:

struct MissionImpossibleMessage: ~Copyable {
    private var message: String

    init(message: String) {
        self.message = message
    }

    consuming func read() {
        print(message)
    }
}

 

이는 메시지 자체를 비공개로 표시하므로 인스턴스를 소비하는 consuming read() 메서드를 호출해야만 액세스할 수 있다.

변경하는 메서드와 달리 소비하는 메서드는 해당 유형의 상수 인스턴스에서 실행할 수 있다. 따라서 이와 같은 코드는 괜찮다:

func createMessage() {
    let message = MissionImpossibleMessage(message: "You need to abseil down a skyscraper for some reason.")
    message.read()
}

createMessage()

 

참고: message.read()message 인스턴스를 소비하기 때문에 message.read()를 두 번 호출하면 오류가 발생한다.

소비 메서드를 이니셜라이저와 함께 사용하면 정리 작업이 두 배로 늘어날 수 있으므로 조금 더 복잡해진다.

 

예를 들어, 게임에서 최고 점수를 추적하는 경우 가장 최근의 최고 점수를 영구 저장소에 쓰고 다른 사람이 점수를 더 이상 변경하지 못하도록 막는 소모적인 finalize() 메서드가 필요하지만, 객체가 파괴될 때 최신 점수를 디스크에 저장하는 이니셜라이저를 또한 사용할 수 있다.

 

이 문제를 방지하기 위해 Swift 5.9에서는 복사할 수 없는 유형의 메서드를 소비하는 데 사용할 수 있는 새로운 discard 연산자를 도입했다. 소비하는 메서드에 discard self를 사용하면 이 객체에 대한 이니셜라이저의 실행이 중지된다.

따라서 HighScore 구조체를 다음과 같이 구현할 수 있다:

struct HighScore: ~Copyable {
    var value = 0

    consuming func finalize() {
        print("Saving score to disk…")
        discard self
    }

    deinit {
        print("Deinit is saving score to disk…")
    }
}

func createHighScore() {
    var highScore = HighScore()
    highScore.value = 20
    highScore.finalize()
}

createHighScore()

 

팁: 해당 코드가 실행되면 value 프로퍼티를 변경하여 구조체를 효과적으로 파괴하고 다시 생성할 때 한 번, createHighScore() 메서드가 완료될 때 한 번, 이니셜라이저 메시지가 두 번 인쇄되는 것을 볼 수 있다.

이 새로운 기능으로 작업할 때 주의해야 할 몇 가지 추가 복잡성이 있다:

  • Class와 Actor는 Noncopyable할 수 없다.
  • Noncopyable 유형은 현재 제네릭을 지원하지 않으므로
    Optional Noncopyable Object와 Noncopyable Object의 배열도 당분간 사용할 수 없다.
  • Noncopyable 유형을 다른 구조체나 열거형 내부의 프로퍼티로 사용하는 경우 해당 상위 구조체나 열거형도 복사 불가능해야 하다.
  • 기존 유형에서 'Copyable'을 추가하거나 제거하면 사용 방식이 크게 달라지므로 매우 신중해야 하다. 라이브러리에서 코드를 배포하는 경우 ABI(?)가 손상될 수 있다.

이것은 Swift에서 정말 광범위한 변화이며, 어떻게 사용될지 정말 궁금하다. 답이 "Swift Data"가 아니라면 실망할 것 같다!

 


 

모든 문장을 100% 이해했다기 보다는

Noncopyable 을 이용하면 값이 1번 소모되는 형태로 전달할 수 있을 것 같다는 느낌 정도로 보고있다.

자세한건 직접 써봐야 깊게 이해할 것 같다.

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/06   »
1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28 29
30
글 보관함