-
Notifications
You must be signed in to change notification settings - Fork 1
Description
Noncopyable type에 대해 알아보자.
이번 WWDC24에서 Swift 6에 대한 설명을 하면서 나온 noncopyable type..! 에 대해 간단히 알아보자.
Noncopyable type은 Swift 5.9에서 처음 등장하였으며, 자세한 내용은 [Swiftlang Github] Noncopyable structs and enums에서 확인이 가능하다.
Noncopyable type이란 무엇일까?
말 그대로 복사가 불가능한 타입이다.
Swift의 모든 타입은 복사가 가능한데, 그 중 구조체와 열거형은 값 복사가 이루어지기 때문에 고유 리소스에 대해서 좋은 모델이 아니라고 한다.
따라서, 이를 noncopyable하게 만듦으로써
고유한 id와 deinit를 가지고 있을 수 있도록 한다는 게 가장 큰 특징이다.
Noncopyable type의 사용
복사 불가능한 구조체를 선언하는 방법은 다음과 같다.
struct FileDescriptor: ~Copyable {
private var fd: Int32
init(fd: Int32) { self.fd = fd }
func write(buffer: Data) {
buffer.withUnsafeBytes {
write(fd, $0.baseAddress!, $0.count)
}
}
deinit {
close(fd)
}
}- FileDescriptor 선언 시
~Copyable를 채택함으로써 복사가 불가능하게 만들 수 있다. deinit또한 작성할 수 있다.
Noncopyable type을 저장 속성으로 가지고 있거나, 연관값으로 갖는 타입 또한 ~Copyable로 선언해야 한다.
struct SocketPair: ~Copyable {
var in, out: FileDescriptor
}
enum FileOrMemory: ~Copyable {
// write to an OS file
case file(FileDescriptor)
// write to an array in memory
case memory([UInt8])
}
// ERROR: copyable value type cannot contain noncopyable members
struct FileWithPath {
var file: FileDescriptor
var path: String
}- FileDescriptor를 저장 속성으로 갖는 구조체의 경우 ~Copyable로 선언해주어야 한다.
- FileDescriptor를 연관값으로 갖는 열거형의 경우 ~Copyable로 선언해주어야 한다.
// ERROR: classes must be `Copyable`
class SharedFile: ~Copyable {
var file: FileDescriptor
}- 하지만 클래스의 경우는 Copyable를 따르기 때문에 ~Copyable로 선언하게 되면 에러가 난다.
그렇다면, 복사가 이루어지지 않는다는 게 뭘까?
Noncopyable type으로 선언된 구조체를 예시로 가져와봤다.
struct User: ~Copyable {
var name: Stirng
}
func createUser() {
let user = User(name: "Anonymous")
let newUser = user
print(newUser.name)
print(user.name) // ERROR! 'user' used after consume
}- newUser를 copyUser에 할당한 후, print문으로 newUser의 name을 출력하려고 하면 다음과 같이 에러가 난다.
- 이는 newUser로 생성한 인스턴스의 소유권이 copyUser로 넘어갔기 때문에 더는 newUser를 사용할 수 없음을 뜻한다.
아까 deinit을 생성할 수 있다고 했는데, 그럼 deinit이 호출되는 순간은 언제일까?
소유권을 넘겨주면서 deinit이 호출되는지 확인해보았다.
struct User: ~Copyable {
var name: String
deinit {
print("해제")
}
}
func f() {
var user = User(name: "Taco")
print(user.name)
var newUser = user
newUser.name = "타코"
print(newUser.name)
}
f()
// Taco
// 타코
// 해제- newUser에 user에 대한 소유권을 넘겨줄 때는 user에 대한 deinit이 호출되지 않는다.
- 함수가 종료되면서 newUser에 대한 deinit이 호출됨을 알 수 있다.
그렇다면, 복사되지 않는 타입이라는 것은 값을 재할당하는 게 아니라 넘겨줌으로써 인스턴스를 하나로 유지하는 것이라고 생각된다.
여기서, 함수의 매개변수에 대한 의문이 생길 수 있다.
우리는 함수 내에 상수를 생성한 후 매개변수의 값을 복사하여 사용한다는 것을 알고 있는데, Noncopyable type은 복사하지 못하면... 어떤 식으로 동작하는 것일까?
일단 우리가 평소에 쓰는 함수의 매개변수 형태로 코드를 작성해보았다.
func createAndGreetUser() {
let newUser = User(name: "Taco")
greet(newUser)
print("Good Bye, \(newUser.name)")
}
func greet(_ user: User) { // ERROR! Noncopyable parameter must specify its ownership
print("Hello, \(user.name)")
}
createAndGreetUser()
위처럼 에러가 나게 된다.
Noncopyable type은 값을 빌려서 사용하거나, 값 자체를 바꾸거나, 소유권을 이전하는 방식으로 동작해야 한다고 한다.
inout 방식은 다들 알 거라고 생각해서 넘어간다.
// borrowing 키워드를 사용하여 값을 빌려 사용
func greet(_ user: borrowing User) {
print("Hello, \(user.name)")
}
// consuming 키워드를 사용하여 소유권을 이전
func greet(_ user: consuming User) {
print("Hello, \(user.name)")
}다음과 같이 borrowing 키워드를 사용하는 방식과 consuming을 사용하는 방식이 있다.
- borrowing 키워드는 '값을 빌려서 사용'하게 되며, 이는 read-only로 동작하게 된다.
- consuming 키워드는 '소유권을 이전'하게 되며, consuming으로 값을 넘겨주며 함수를 호출한 후에는 해당 인스턴스에 대한 접근이 불가능하다. (여기서는 greet(newUser)로 호출하였기 때문에, 호출 후 newUser를 사용할 수 없게 된다.)
consuming 키워드를 메서드 선언 시에도 사용할 수 있는데,
이 때는 함수 호출을 한 번으로 제한하며, 함수가 호출된 후 소멸자가 호출되게 된다.
consuming으로 선언된 메서드의 동작을 확인해보면 다음과 같다.
struct SecretMessage: ~Copyable {
private var message: String
init(message: String) {
self.message = message
}
consuming func readMessage() {
print(message)
}
deinit {
print("해제")
}
}func makeMessage() {
let message = SecretMessage(message: "Hello")
message.readMessage()
message.readMessage() // ERROR. 'message' consumed more than once
}
makeMessage()func makeMessage() {
let message = SecretMessage(message: "Hello")
message.readMessage()
let newMessage = message // ERROR. 'message' consumed more than once
}
makeMessage()- 다음과 같이, 메서드를 두 번 호출하거나 이미 한 번 호출한 후 다른 변수에 소유권을 넘겨주고자 하면 에러가 발생한다.
- 함수의 호출이 한 번으로 제한되는 것을 확인할 수 있다.
func makeMessage() {
let message = SecretMessage(message: "Hello")
message.readMessage()
print("함수 종료")
}
makeMessage()
- consuming으로 선언한 메서드가 호출된 후 소멸자가 호출되는 것을 확인할 수 있다.
하지만, 이렇게 메서드 호출 후 바로 deinit이 되는 상황을 막을 수 있는 방법이 있다.
바로 discard self를 사용하는 것이다.
struct Score: ~Copyable {
private var score = 0
init(score: Int = 0) {
self.score = score
}
consuming func register() {
print(self.score)
discard self
}
deinit {
print("해제")
}
}
func createScore() {
let score = Score(score: 60)
score.register()
}
createScore()
- 위와 같이 "해제"는 출력되지 않는다.
- 그렇지만, consuming으로 선언된 메서드를 두 번 호출하거나 한 번 호출한 뒤 변수에 소유권을 넘겨주려고 하면 동일한 에러가 발생한다. (consuming 메서드가 존재하면 호출 후에 넘겨줄 수 없는 듯..)
- consuming 메서드를 호출한 뒤, 다른 메서드를 호출하는 경우에도 동일한 에러가 발생한다.
그럼 왜쓰는거야..
읽을 거리
[Hacking With Swift] Noncopyable structs and enums
[Swiftlang Github]Noncopyable structs and enums
이번 WWDC24 세선 (6/13)
WWDC2024 Consume noncopyable types in Swift
Swift 6에서는 모든 제네릭 타입에 대해 Noncopyable이 가능하다고 하던데....
[Swiftlang Github] Noncopyable Generic
consume 키워드를 사용하여 바인딩하기
[Hacking With Swift] consume operator to end the lifetime of a variable binding