ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [SE-0518] tide-Sendable
    Swift 2026. 4. 5. 08:26

    안녕하세요. 그린입니다 🍏

    이번 포스팅에서는 SE-0518 — ~Sendable로 non-Sendable 타입을 명시적으로 표현하기에 대해 정리해보겠습니다 🙋🏻


    Intro

    • Proposal: SE-0518
    • Status: Implemented (Swift 6.4)
    • Experimental Feature Flag: TildeSendable

    Motivation

    public 타입이 Sendable을 명시적으로 conform하지 않을 때, 그 의도를 파악하기가 쉽지 않습니다.

    아직 Sendable conformance를 추가하지 않은 건지, 아니면 의도적으로 non-Sendable로 설계한 건지가 불분명하거든요.


    이를 판단하려면 타입의 스토리지 구조와 동기화 메커니즘 같은 구현 세부사항을 알아야 하는데, 라이브러리 외부에서는 접근하기 어렵습니다.

     

    또한 클래스 자체는 non-Sendable이지만, 특정 서브클래스는 Sendable이어야 하는 상황도 있습니다.

     


    기존에는 이를 아래처럼 unavailable extension으로 표현했는데요.

    class Base {
       // ...
    }
    
    @available(*, unavailable)
    extension Base: Sendable {
    }

    문제는 unavailable conformance는 서브클래스에 그대로 상속된다는 점입니다.


    그래서 thread-safe 서브클래스를 만들려고 해도,

    final class ThreadSafe: Base, @unchecked Sendable {
       // ...
    }

     

    다음과 같은 경고가 발생합니다.

    warning: conformance of 'ThreadSafe' to protocol 'Sendable' is already unavailable

    서브클래스가 Sendable이 될 수도 있고 아닐 수도 있는, 이 세 번째 상태를 언어 차원에서 명확하게 표현할 방법이 없었던 거죠.
    라이브러리 작성자 입장에서는 API의 Sendable 판단을 체계적으로 하기도 어렵고, 클라이언트에게 의도를 전달하기도 힘든 상황이었습니다.

     


    Solution

    SE-0518은 ~Sendable conformance 문법을 도입해 타입이 명시적으로 non-Sendable임을 나타낼 수 있게 합니다.

    // `ExecutionResult`는 감사 결과 non-`Sendable`로 명시.
    // non-Sendable associated value를 가진 케이스가 있기 때문입니다.
    public enum ExecutionResult: ~Sendable {
    case success
    // ...
    // ...
    case failure(NonSendable)
    // ...
    }
    
    // `Base`는 감사 결과 non-`Sendable`로 명시.
    // 서브클래스는 non-`Sendable` mutable state를 추가하거나,
    // 상태를 보호하거나 상수로 만들어 `Sendable`로 표시될 수 있습니다.
    public class Base: ~Sendable {
      // ...
    }
    
    // 이 서브클래스는 감사 결과 `Sendable`로 판단됩니다.
    public class ThreadSafeImpl: Base, @unchecked Sendable {
        // `Base`의 `value`를 lock 등을 통해 안전하게 접근합니다.
    }
    
    // 이 서브클래스는 mutable non-`Sendable` state를 가지므로 non-`Sendable`입니다.
    public class UnsafeImpl: Base {
       var x: NonSendable
    }

    ~Sendable 문법은 타입에만 적용 가능합니다. 제네릭 파라미터 같은 다른 선언은 기본적으로 이미 ~Sendable이기 때문입니다.

     


    Detailed Design

    ~Sendable은 ~Copyable, ~Escapable, ~BitwiseCopyable과 같이 tilde(~) 접두사를 사용해 conformance 억제를 표현합니다.

     

    기본 사용

    // Sendable inference 억제
    struct NotSendableType: ~Sendable {
        let data: String
    }
    
    // 다른 conformance와 함께 사용 가능
    struct MyType: Equatable, ~Sendable {
        let id: UUID
    }
    
    // 클래스에도 적용 가능
    class MyClass: ~Sendable {
        private let data = 0
    }

    Extension에서는 사용 불가

    억제 선언은 반드시 타입 선언부에서 해야 합니다.

    Extension에서 사용하면 기존 코드의 의미가 바뀔 위험이 있거든요.

    extension Test: ~Sendable {} // Error!

    제네릭 파라미터, 프로토콜에는 사용 불가

    억제할 implied Sendable 요구사항이 없기 때문에 거부됩니다.

    protocol P: ~Sendable {} // Error!
    struct Test<T: ~Sendable> {} // Error!
    func test<T: ~Sendable>(_: T) {} // Error!

    Sendable 요구사항을 만족시킬 수 없음

    unavailable extension과 마찬가지로, ~Sendable 타입은 Sendable 요구사항을 만족시킬 수 없습니다.

    func processData<T: Sendable>(_ data: T) { }
    
    struct NotSendable: ~Sendable {
        let value: Int
    }
    
    processData(NotSendable(value: 42)) // error: type 'NotSendable' does not conform to the 'Sendable' protocol

    서브클래스에는 영향 없음

    unavailable extension과의 가장 큰 차이점입니다. 

    ~Sendable은 서브클래스에 상속되지 않습니다.
    class A: ~Sendable {
    }
    
    final class B: A, @unchecked Sendable {
    }
    
    func takesSendable<T: Sendable>(_: T) {
    }
    
    takesSendable(B()) // Ok!

    Sendable과 ~Sendable 동시 사용 불가

    무조건적으로 둘 다 conform하려 하면 컴파일 오류가 발생합니다.
    // actor는 항상 `Sendable`입니다.
    actor A: ~Sendable { // error: cannot both conform to and suppress conformance to 'Sendable'
    }
    
    struct Container<T>: ~Sendable {
        let value: T
    }
    
    extension Container: Sendable {} // error: cannot both conform to and suppress conformance to 'Sendable'

     

    상위 클래스나 프로토콜에서 상속된 명시적/파생 Sendable conformance에도 동일하게 적용됩니다.

    protocol IsolatedProtocol: Sendable {
    }
    
    struct Test: IsolatedProtocol, ~Sendable { // error: cannot both conform to and suppress conformance to 'Sendable'
    }
    
    @MainActor
    class IsolatedBase { // global actor isolated 타입은 `Sendable`입니다.
    }
    
    class Refined: IsolatedBase, ~Sendable { // error: cannot both conform to and suppress conformance to 'Sendable'
    }

    조건부 conformance는 허용

    extension Container: Sendable where T: Sendable {} // Ok!

    조건부 Sendable conformance는 여전히 유용하거든요.
    API 작성자가 타입이 특정 조건에서만 Sendable임을 표현하고 싶을 때 활용할 수 있습니다.

     


    ExplicitSendable 추적 플래그

    Swift 컴파일러는 public 타입의 sendability를 감사할 수 있는 방법을 제공합니다.
    기존에는 -require-explicit-sendable 플래그를 사용했는데, 이번 제안에서 ~Sendable을 지원하도록 업데이트되고 ExplicitSendable 진단 그룹으로 전환되었습니다.
    기본적으로 비활성화되어 있으며, -Wwarning ExplicitSendable로 활성화할 수 있습니다.

     


    Alternatives Considered

    @nonSendable 속성

    @nonSendable
    struct MyType {
        let value: Int
    }

    이 방법도 고려됐지만, 프로토콜 conformance 방식이 더 ergonomic하고,
    ~Copyable, ~Escapable 같은 기존 conformance 억제 컨벤션과도 일관성이 있기 때문에 채택되지 않았습니다.

     


    Future Directions

    이번 제안은 Sendable에만 집중했지만, Equatable, Hashable, RawRepresentable 같이 암묵적으로 추론되는 다른 프로토콜 conformance에도 ~ 문법이 적용될 수 있습니다.
    각각 고유한 특이점이 있어 별도 제안이 필요할 것으로 보입니다.

     


    Conclusion

    ~Sendable은 작지만 굉장히 실용적인 추가입니다.

    기존에는 non-Sendable 의도를 표현하려면 unavailable extension이라는 다소 우회적인 방법을 써야 했고,
    서브클래스에 상속된다는 부작용까지 감수해야 했거든요.

    이제 ~Sendable로 그 의도를 명확하게 선언하면서, 서브클래스의 Sendable 가능성도 열어둘 수 있게 되었습니다.
    라이브러리 작성자 입장에서는 API의 Sendability 추적 및 판단을 훨씬 체계적으로 할 수 있을 것 같습니다 🙌

     


    References

     

    swift-evolution/proposals/0518-tilde-sendable.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

Designed by Tistory.