-
[SE-0544] Mutation and consumption in non-Copyable type deinitsSwift 2026. 8. 31. 13:37
안녕하세요. 그린입니다 🍏
이번 포스팅에서는 SE-0544 — noncopyable 타입 deinit에서의 mutation과 consumption에 대해 정리해보겠습니다 🙋🏻
Intro
Proposal: SE-0544
Author: Joe Groff
Review Manager: John McCall
Status: Accepted
Implementation: swiftlang/swift#90836
noncopyable 타입은
deinit을 정의해서 생명주기가 끝날 때 소유하고 있던 리소스를 정리할 수 있어요.하지만 이 제안 이전에는
deinit본문 안에서self가 immutable하고 오직 borrow만 가능했어요.이 제안은
deinit이self의 필드를 mutate하거나 consume할 수 있게 허용하되, value resurrection 문제를 피하기 위해self전체를 하나의 값으로서 mutate하거나 consume하는 건 여전히 막아두자고 제안합니다.Motivation
많은 noncopyable 타입 구현체는 리소스를 소유한 다른 noncopyable 값들로 구성돼요.
그 구성 요소들이 전체 값의 정리 과정에서 어떻게 consume되는지 제어하고 싶은 건 자연스러운 요청일겁니다.
struct File: ~Copyable { consuming func close() {...} } struct Buffer: ~Copyable { borrowing func flush(to file: borrowing File) {...} consuming func release() {...} } struct BufferedFile: ~Copyable { let file: File let buffer: Buffer deinit { // Flush then close the buffer buffer.flush(to: file) buffer.release() // Then close the file file.close() } }비슷한 맥락에서, deinit이 정리 과정의 일부로 mutating 메서드에 담아둔 코드를 쓰고 싶을 수도 있어요.
Proposed Solution
deinit이self의 필드를 mutate하고 consume할 수 있도록 허용하자고 제안합니다.Detailed Design
"resurrection"과 의도치 않은 재귀 피하기
noncopyable 타입의
deinit은 값의 소유권을 가진 컨텍스트 중에서도 독특한 존재예요.다른 소유 컨텍스트라면 값을 암묵적으로
deinit을 호출해서 파괴하겠지만,deinit자신은 당연히 그럴 수 없죠.deinit은 오직 값을 구성하는 저장 프로퍼티나 채워진 enum case만 파괴합니다.만약
deinit이self를 consuming이나 mutating 연산에 통째로 넘길 수 있다면, 그 callee의 생명주기가 끝나는 시점에 다시deinit이 호출될 테니 값이 callee 안에서 "부활"하는 셈이 돼요.이러면 실수로 무한 루프를 만들기 쉬워집니다.
struct Foo: ~Copyable { deinit { self.foo() } consuming func foo() { // oops, implicitly calls back into \`deinit\` } } struct Bar: ~Copyable { deinit { self.bar() } mutating func bar() { // oops, implicitly calls \`deinit\` on the old value of \`self\` // before reassigning it self = Bar() } }이 제안은 이런 문제를 피하기 위해
self를 하나의 값으로서 mutate하거나 consume하는 걸 금지해요.대부분의 경우 이 정도 제약은 받아들일 만하다고 봅니다.
다만 이 제약의 한 가지 결과로,
deinit은 같은 타입의mutating이나consuming메서드를 호출할 수 없어요.그래도
deinit은 필드를 다루는static메서드를 통해 다른 메서드와 로직을 공유할 수는 있습니다.struct Resource: ~Copyable { var resourceID: Int // Shared logic for releasing the underlying resource by ID. // The release operation may surface error conditions, but these can be // ignored in normal use. private static func release(resourceID: Int) throws {...} // Consuming method that releases the resource, surfacing errors to be // handled consuming func release() throws { try Self.release(resourceID: self.resourceID) discard self } // Deinit that implicitly closes the resource, swallowing errors deinit { do { try Self.release(resourceID: self.resourceID) } catch { // Ignore the error } } }또한
deinit이 값 전체의 소유권을 넘기고 싶은 상황도 있을 수 있어요.예를 들어 값을 정리하는 데 시간이 오래 걸린다면,
deinit도중 바로 정리하기보다는 죽어가는 값을 나중에 정리하도록 큐에 넣어두는 게 나을 수 있죠.deinit은 항상 타입의 원본 선언 안에 정의되기 때문에, 언제나struct의 레이아웃과 memberwise 이니셜라이저에 접근할 수 있어요.그래서 self의 필드들을 memberwise 이니셜라이저에 넘겨서 값을 명시적으로 "부활"시킬 수 있습니다.
let deferredCleanupValues: ConcurrentQueue struct DeferredCleanup: ~Copyable { var resource1: Resource1 var resource2: Resource2 deinit { // Instead of cleaning up this value's resources immediately, push an // equivalent value into the queue to be cleaned up later let newSelf = Self(resource1: self.resource1, resource2: self.resource2) deferredCleanupValues.push(newSelf) } consuming func runTimeConsumingCleanup() async { ... } } func runDeferredCleanups() async { while let value = deferredCleanupValues.pop() { await value.runTimeConsumingCleanup() } }그 밖의 제약
deinit도중 클로저 안에서self를 캡처하는 건 여전히 허용되지 않아요.부분적으로 consume된 self의 정리
deinit이 리턴하는 시점에self의 어떤 구성 요소가 아직 consume되지 않았다면, 남은 구성 요소들은 암묵적으로 파괴돼요.여기엔 noncopyable 구성 요소의
deinit을 실행하는 것도 포함됩니다.Alternatives Considered
self를 전체 값으로서 mutate하거나 consume하는 제약이 실제로 너무 부담스럽다고 판명되면, 이 제약을 완화하는 방향도 열려 있어요.resurrection 위험을 완화할 다른 방법들도 있습니다.
resurrect self 연산 도입하기
이 제안의
deinit들은 필드로부터 memberwise-initialize해서self를 수동으로 "부활"시킬 수 있지만, 이건 장황해요.이게 흔한 패턴이 되면 이를 위한 축약 문법을 도입할 수도 있어요.
이건
discard self의 반대로 볼 수 있는데,discard self가self의 암묵적deinit을 비활성화하고self를 전체 값으로 더 이상 쓸 수 없게 만드는 거라면, 이건 반대로self의 암묵적deinit을 다시 활성화해서 전체 값으로 다시 다룰 수 있게 하는 셈이니까요."deinit-safe" 메서드에 어노테이션 달기
deinit이 전체 값에 적용할 수 있는 연산을, 어떤 방식으로든 "deinit-safe"임을 명시적으로 표시한 메서드로만 제한할 수도 있어요.그렇게 표시된
consuming메서드는 반드시discard self를 하도록 요구하고, 표시된mutating메서드는self를 완전히 재할당하는 걸 막는 식입니다.deinit이 지역적으로 정의된 메서드만 호출하도록 제한하기
명시적인 어노테이션 대신,
deinit이self를 mutate하거나 consume할 수 있는 방법을,deinit과 함께 원본 타입 정의 안에 정의된 메서드나 같은 모듈 안의 메서드로만 제한할 수도 있어요.그러면 파일/모듈 단위 분석으로
deinit에서 호출되는 메서드가 잠재적으로 다시deinit을 호출하는 지점을 감지할 수 있을 거예요.mutation 이후 self의 명시적 consumption이나 discard 요구하기
첫 번째 pitch 스레드에서 ellie20은 "값이 mutate된 이후엔 어떤 의미에서 다른 값이다"라고 지적하면서,
self를 전체 값으로 mutating한 이후에는 그 뒤의 값을 명시적으로discard하거나consume하도록 요구해야 한다고 주장했어요.그 시점의 값은 더 이상 원래
deinit대상이던 값이 아니기 때문이에요.struct A: ~Copyable { deinit { self.replace() // explicitly indicate that the replaced \`self\` is discarded, rather than // deinit-ed discard self } mutating func replace() { ... } } struct B: ~Copyable { deinit { self.replace() // explicitly indicate that the replaced \`self\` is consumed, causing deinit // to run again on the new value _ = consume self } mutating func replace() { ... } }Conclusion
그동안 noncopyable 타입의
deinit은self를 오직 borrow만 할 수 있어서, 내부 필드를 정리하는 로직을mutating/consuming메서드로 재사용하기 어려웠어요.이 제안 덕분에 필드 단위의 mutate/consume은 자유로워지면서도,
self전체를 다루는 건 여전히 막아서 "부활"과 무한 재귀라는 위험한 함정은 피했다는 점이 인상적이에요.static메서드로 로직을 공유하거나, memberwise 이니셜라이저로 명시적으로 부활시키는 우회로도 함께 마련해둔 균형 잡힌 제안 같습니다 🙌References
swift-evolution/proposals/0544-mutate-or-consume-in-deinit.md at main · swiftlang/swift-evolution
This maintains proposals for changes and user-visible enhancements to the Swift Programming Language. - swiftlang/swift-evolution
github.com
'Swift' 카테고리의 다른 글
[SE-0538] Disconnected (0) 2026.08.22 [SE-0537] Section Placement Control for Functions (2) 2026.08.15 [SE-0535] Add CLI for editing global mirrors configuration (0) 2026.08.08 [SE-0527] RigidArray & UniqueArray (1) 2026.08.02 [SE-0525] Safe loading API for RawSpan (1) 2026.07.26