ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [SE-0519] Borrow and Inout types for safe, first-class references
    Swift 2026. 4. 11. 09:55

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

    이번 포스팅에서는 안전한 first-class 참조를 위한 Borrow와 Inout 타입에 대해 정리해보겠습니다 🙋🏻


    Intro

    • Proposal: SE-0519
    • Authors: Joe Groff, Alejandro Alonso
    • Status: Active review (March 4...17, 2026)

    Motivation

    Swift는 함수 호출의 일부로 값에 대한 임시 접근을 제공할 수 있습니다.

    • inout 파라미터: 호출자가 소유한 값에 대한 임시 독점(exclusive) 접근을 받습니다. 호출된 쪽에서 파라미터를 수정하거나 현재 값을 소비(consume)할 수 있고, 호출자는 함수가 반환되면 소유권을 다시 가져옵니다.
    • borrowing 파라미터: 호출자로부터 값에 대한 임시 공유(shared) 접근을 받습니다. 다른 곳에서도 동시에 접근할 수 있으므로 일반적으로 값을 읽기만 할 수 있지만, 독립적인 복사본 없이도 접근이 가능합니다.

    이런 종류의 참조를 함수 호출의 범위 밖에서도 로컬 변수 바인딩, 다른 타입의 멤버, 제네릭 컨테이너의 요소 등으로 사용할 수 있으면 매우 유용할 것입니다.

     

    기존에는 클래스로 값을 박싱(boxing)하고 참조를 전달하는 방법을 썼는데, 이렇게 하면 메모리 할당, 레퍼런스 카운팅, 동적 독점성 검사 오버헤드가 발생합니다. 

    UnsafePointer는 당연히 unsafe하고 Swift의 고수준 의미론과 어색하게 상호작용해서 올바르게 사용하려면 굉장히 세심한 주의가 필요했거든요.

     


    Proposed Solution

    표준 라이브러리에 두 개의 새로운 제네릭 타입을 도입합니다.

    둘 다 non-Escapable 타입으로 표현됩니다.

     


    Borrow<T>

    다른 값에 대한 공유 borrow를 나타내며, 대상 값을 읽을 수 있지만 소비하거나 수정할 수는 없습니다.

    public struct Borrow<Value: ~Copyable>: Copyable & ~Escapable {
      @_lifetime(borrow target)
      public init(_ target: borrowing Value)
    
      public var value: Value { borrow }
    }

     


    Inout<T>

    다른 값에 대한 독점 접근을 나타내며, Inout의 소유자가 대상 값을 수정하고 Inout 값이 활성화된 동안 독점적으로 사용할 수 있습니다.

    public struct Inout<Value: ~Copyable>: ~Copyable & ~Escapable {
      @_lifetime(&target)
      public init(_ target: inout Value)
    
      public var value: Value { borrow; mutate }
    }

     

     

    두 타입의 인터페이스는 lifetime dependencies를 사용해 ~Escapable 값을 생성하고, borrow와 mutateaccessor를 통해 불필요한 제한 없이 대상 값에 효율적으로 접근합니다.

     

     


    기본 사용 예시

    참조는 Borrow.init 또는 Inout.init에 대상을 전달해 생성하고, value 프로퍼티로 대상 값에 접근합니다.

    딕셔너리에서 키를 한 번만 조회하고 반복 수정하는 예시입니다.

    func updateTotal(in dictionary: inout [String: Int], for key: String,
                     with values: [Int]) {
      // 딕셔너리에서 키를 한 번만 조회합니다...
      var entry = Inout(&dictionary[key, default: 0])
    
      // ...그리고 해시 테이블을 반복적으로 탐색하지 않고 반복 수정합니다
      for value in values {
        entry.value += value
      }
    }

     

    실험적인 lifetimes 기능을 사용하면 참조를 반환하는 함수도 작성할 수 있습니다.

    struct Vec3 {
      var x, y, z: Double
    
      @_lifetime(&self)
      mutating func at(index: Int) -> Inout<Double> {
        switch index {
        case 0: return Inout(&x)
        case 1: return Inout(&y)
        case 2: return Inout(&z)
        default:
          fatalError("out of bounds")
        }
      }
    }

     

    Borrow와 Inout은 다른 non-Escapable 타입의 필드로도 사용할 수 있습니다.

    // struct-of-arrays 형태의 사람 레코드
    struct People {
      var names: [String]
      var ages: [Int]
    
      subscript(i: Int) -> Person {
        @_lifetime(&self)
        mutating get {
          return Person(name: &names[i], age: &ages[i])
        }
      }
    }
    
    // 단일 사람에 대한 mutable 참조
    struct Person: ~Copyable, ~Escapable {
      var name: Inout<String>
      var age: Inout<Int>
    }

     

    또한 제네릭 파라미터로도 사용할 수 있어서, non-Escapable 타입을 지원하는 컨테이너와 래퍼에 참조를 담을 수 있습니다.

    @_lifetime(&array)
    func element(of array: inout [Int], at: Int) -> Inout<Int>? {
      if at >= 0 && at < array.count {
        return &array[at]
      } else {
        return nil
      }
    }

     

     


    Detailed Design

    Lifetime dependence

    Borrow와 Inout은 모두 non-Escapable 타입입니다.

    생성되면 대상에 대한 lifetime dependency를 가지므로, 대상 값이 borrow 또는 독점 접근 상태를 유지할 수 있는 동안에만 사용 가능합니다.

    var totals = [17, 38]
    
    do {
      let apples = Borrow(totals[0])
    
      print(apples.value) // 17 출력
    
      apples.value += 2 // ERROR, Borrow.value는 읽기 전용
    
      totals[1] += 1 // ERROR, borrow 중인 `totals`를 수정할 수 없음
    
      print(totals[1]) // 38 출력. totals를 다시 borrow하는 건 가능
      print(apples.value) // 17 출력
    }
    
    do {
      var bananas = Inout(&totals[1])
    
      bananas.value += 2 // `Inout`을 통해 값을 수정 가능
    
      print(bananas.value) // 40 출력
    
      print(totals[1]) // ERROR, totals는 `bananas`가 독점 접근 중
    
      bananas.value += 2
      print(bananas.value) // 42 출력
    }
    
    print(totals) // [17, 42] 출력

     

    이 동작은 Array와 span, mutableSpan 프로퍼티를 통해 접근하는 Span 또는 MutableSpan 값 사이의 상호작용과 유사합니다. 

    Borrow와 Inout은 각각 Span과 MutableSpan의 단일 값 버전이라고 볼 수 있거든요.

     


    nontrivial 접근과의 상호작용

    Borrow는 모든 값을, Inout은 모든 mutable 위치를 대상으로 할 수 있습니다. 

    get/set 쌍, yielding 코루틴 accessor, 동적 독점성 검사가 적용된 stored property, didSet/willSet observer가 있는 경우도 포함됩니다.

    struct NoisyCounter {
      private var _value: Int
    
      var value: Int {
        get {
          print("counted \(_value)")
          return _value
        }
        set {
          print("updating counter to \(newValue)")
          _value = newValue
        }
      }
    }
    
    var counter = NoisyCounter(67)
    do {
      var counterRef = Inout(&counter.value) // `counter.value` 접근 시작, "counted 67" 출력
      counterRef.value += 1
      counterRef.value += 1
      // `counter.value` 접근 종료, "updating counter to 69" 출력
    }

     

    단, Borrow와 Inout은 접근을 종료하기 위한 컨텍스트를 캡처하지 않기 때문에, nontrivial 접근에서 파생된 경우 일반적으로 lifetime을 즉각적인 호출자 너머로 확장할 수 없습니다.

    @_lifetime(&target)
    func noisyCounterRef(from target: inout NoisyCounter) -> Inout<Int> {
      // ERROR, formal access 밖으로 `Inout`의 lifetime을 확장하게 됨
      return Inout(&target.value)
    }

     

    struct stored property 직접 접근, 불변 class stored property 직접 접근, borrow나 mutate accessor를 통한 접근은 종료 시 코드 실행이 필요 없으므로 이 제한이 적용되지 않습니다.

     


    Borrow의 표현 방식

    Borrow<Value>는 Value 타입의 속성에 따라 포인터 표현 또는 값의 비트 복사 표현을 사용합니다.

    다음 조건 중 하나라도 해당되면 포인터 표현을 사용합니다.

    • MemoryLayout<Value>.size가 4 * MemoryLayout<Int>.size보다 큰 경우
    • Value가 bitwise-borrowable이 아닌 경우
    • Value가 addressable-for-dependencies인 경우

    Int처럼 메모리 어디에서나 동일한 의미를 갖는 타입은 bitwise-borrowable이라 포인터 없이 값으로 전달됩니다. 반면 InlineArray처럼 Span을 파생시키는 등 내부 메모리 주소에 의존하는 타입은 addressable-for-dependencies로 분류되어 포인터 표현을 사용합니다.

     

    C, Objective-C, C++에서 import된 struct, union, class 타입은 항상 addressable-for-dependencies로 간주됩니다.

     


    Inout의 표현 방식

    inout 파라미터는 항상 주소로 전달되므로, Inout은 모든 경우에 포인터 표현을 사용합니다.

     


    Source Compatibility / ABI

    • Borrow와 Inout이라는 이름을 가진 타입이 기존 코드에 존재할 수 있지만, Swift의 이름 조회 규칙상 로컬 정의 및 명시적으로 import된 이름이 표준 라이브러리보다 우선하므로 기존 코드는 그대로 컴파일됩니다.
    • 이 제안은 additive하며 기존 코드의 ABI에 영향을 주지 않습니다.
    • 단, 제네릭 지원에는 새로운 런타임 타입 레이아웃 기능이 필요하므로, 구버전 Swift 런타임을 타겟으로 할 때 가용성이 제한될 수 있습니다.

    Future Directions

    • 로컬 참조 바인딩 문법 (sugar): Borrow/Inout을 명시적으로 형성하는 것의 sugar로, borrow x = y처럼 참조 바인딩 문법을 도입하는 것을 고려 중입니다.
    • 암묵적 역참조 / 멤버 포워딩: @dynamicMemberLookup이나 Rust의 Deref 트레이트처럼 value를 거치지 않고 대상 멤버에 직접 접근하는 방법도 검토 중입니다.
    • ~Escapable 대상 타입 지원: 현재는 대상 타입이 Escapable이어야 한다는 제약이 있는데, 컴파일러 구현 한계로 인한 것으로 향후 개선될 수 있습니다.
    • exclusive 소유권 또는 reborrow: var 바인딩 없이도 Inout.value를 통한 mutation이 가능하도록 하는 방법을 검토 중입니다.

    Alternatives Considered

    Inout 이름 짓기

    일반적인 네이밍 컨벤션대로라면 InOut이 맞겠지만, 저자들은 inout이 두 단어의 조합이 아닌 하나의 키워드처럼 느껴지기 때문에 Inout을 선택했습니다.

    과거에 inout을 mutating으로 대체하는 논의가 있었던 만큼 Mutable 같은 이름도 후보였습니다.

     

    value 프로퍼티 이름

    subscript로 reference[]처럼 역참조하거나, target 같은 이름도 검토했지만, 제안된 Unique 타입의 인터페이스와 일관성을 맞추기 위해 value로 결정했습니다.

     


    Conclusion

    Borrow와 Inout은 Swift에서 오래도록 아쉬웠던 "안전한 참조" 문제를 드디어 언어 차원에서 해결하는 제안입니다.

    클래스 박싱의 오버헤드도, UnsafePointer의 위험성도 없이, 값에 대한 참조를 first-class로 다룰 수 있게 된다는 점이 정말 기대됩니다.

    특히 딕셔너리나 배열의 특정 요소를 반복적으로 수정해야 하는 시나리오에서 성능 이점이 체감될 것 같고, ~Escapable 타입 생태계와 함께 Swift의 소유권 모델이 한층 더 정교해지는 느낌이거든요 🙌

     


    References

     

    swift-evolution/proposals/0519-borrow-inout-types.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

     

     

    SE-0507: Borrow and Mutate Accessors

    Hi everyone, The review of SE-0507 "Borrow and Mutate Accessors" begins now and runs through February 9, 2026. Reviews are an important part of the Swift evolution process. All review feedback should be either on this forum thread or, if you would like to

    forums.swift.org

    'Swift' 카테고리의 다른 글

    [SE-0521] Improved Syntax for Optionals of Opaque and Existential Types  (0) 2026.05.01
    [SE-0520] Discardable result use in Task initializers  (0) 2026.04.25
    [SE-0518] tide-Sendable  (0) 2026.04.05
    Swift 6.3  (0) 2026.03.29
    Swift 6.2.4  (0) 2026.03.22
Designed by Tistory.