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
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의 사용
복사 불가능한 구조체를 선언하는 방법은 다음과 같다.
~Copyable를 채택함으로써 복사가 불가능하게 만들 수 있다.deinit또한 작성할 수 있다.Noncopyable type을 저장 속성으로 가지고 있거나, 연관값으로 갖는 타입 또한
~Copyable로 선언해야 한다.그렇다면, 복사가 이루어지지 않는다는 게 뭘까?
Noncopyable type으로 선언된 구조체를 예시로 가져와봤다.
아까 deinit을 생성할 수 있다고 했는데, 그럼 deinit이 호출되는 순간은 언제일까?
소유권을 넘겨주면서 deinit이 호출되는지 확인해보았다.
그렇다면, 복사되지 않는 타입이라는 것은 값을 재할당하는 게 아니라 넘겨줌으로써 인스턴스를 하나로 유지하는 것이라고 생각된다.
여기서, 함수의 매개변수에 대한 의문이 생길 수 있다.
우리는 함수 내에 상수를 생성한 후 매개변수의 값을 복사하여 사용한다는 것을 알고 있는데, Noncopyable type은 복사하지 못하면... 어떤 식으로 동작하는 것일까?
일단 우리가 평소에 쓰는 함수의 매개변수 형태로 코드를 작성해보았다.
위처럼 에러가 나게 된다.
Noncopyable type은 값을 빌려서 사용하거나, 값 자체를 바꾸거나, 소유권을 이전하는 방식으로 동작해야 한다고 한다.
inout 방식은 다들 알 거라고 생각해서 넘어간다.다음과 같이
borrowing키워드를 사용하는 방식과consuming을 사용하는 방식이 있다.consuming키워드를 메서드 선언 시에도 사용할 수 있는데,이 때는 함수 호출을 한 번으로 제한하며, 함수가 호출된 후 소멸자가 호출되게 된다.
consuming으로 선언된 메서드의 동작을 확인해보면 다음과 같다.
하지만, 이렇게 메서드 호출 후 바로 deinit이 되는 상황을 막을 수 있는 방법이 있다.
바로
discard self를 사용하는 것이다.그럼 왜쓰는거야..읽을 거리
[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