Swift SE-0519:用 `Ref` 与 `MutableRef` 构建安全的一等引用类型
2026/9/23 16:00:13 网站建设 项目流程
  • 文档

【免费下载链接】swift-evolution

This maintains proposals for changes and user-visible enhancements to the Swift Programming Language.

项目地址:https://gitcode.com/gh_mirrors/sw/swift-evolution
点击查看免费下载

本篇文章围绕 Swift 演进提案 SE-0519《RefandMutableReftypes for safe, first-class references》(Swift 6.4 已实现)展开,系统讲解Ref(共享只读引用)与MutableRef(独占可变引用)两个标准库泛型类型的设计动机、接口定义、生命周期依赖模型、内部表示原理,以及它们与borrow/mutate访问器(SE-0507)、非逃逸类型(SE-0446)等既有特性的关系。读完本文,你将掌握如何用Ref/MutableRefinoutborrowing参数的能力提升为一等公民——存进局部变量、放进结构体字段、塞进泛型容器,并用编译期生命周期检查替代不安全的UnsafePointer

背景与动机:为什么需要一等引用

Swift 原本只能在函数调用的边界内临时借用值:

  • inout参数:调用方把某个值的独占可写访问权临时交给被调函数。被调函数可以修改参数、甚至可以消耗(consume)其当前值(只要最后用新值把它填回去),返回后所有权归还调用方。
  • borrowing参数:调用方把某个值的共享只读访问权临时交给被调函数。由于同一时刻可能还有其他访问,被调函数通常只能读取,好处是不需要复制一份独立的值。

这两种能力都受限于"函数调用"这一上下文。开发者常常需要把这类引用移出函数调用的边界:做成局部变量绑定、作为其他类型的成员、放进泛型容器里。SE-0519 之前只有三条不太理想的路:

  1. class 装箱:把值包进一个堆上对象再传引用。代价是引入堆分配、引用计数与动态排他性检查的开销。
  2. UnsafePointer:不安全,且与 Swift 的高层语义格格不入,用错极易造成悬垂指针。
  3. 反复通过下标/属性访问:例如每次操作都去哈希表里查一次,性能与可读性都不佳。

RefMutableRef正是为填补这一空缺而生:它们把"借用"和"独占访问"封装成一等公民、非逃逸(~Escapable)的泛型值,由编译器静态保证其生命周期不越过被引用值。

核心 API:RefMutableRef

提案为标准库新增两个泛型类型。Ref表示对另一个值的共享借用,可以读取目标值,但不能消耗或修改它:

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

MutableRef<Value>表示对另一个值的独占访问,只要MutableRef存活,其持有者就能修改目标值并独占使用它:

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

两个类型接口有四个值得注意的设计点:

  • Value: ~Copyable:目标值可以是不可拷贝类型(~Copyable语法来自 SE-0390 非拷贝结构体与枚举),引用本身无需拷贝目标。
  • ~EscapableRef/MutableRef是非逃逸类型,不能逃出产生它的上下文,从而保证指向的目标始终有效。~Escapable约束体系由 SE-0446 非逃逸类型引入,其中Escapable是标准库中一个可被抑制的协议:
    protocol Escapable: ~Copyable {}
  • @_lifetime(...)生命周期依赖标注:用于声明引用的存活期与目标值的借用/独占访问绑定,这是构造~Escapable值的关键机制。
  • borrowmutate访问器value属性通过 SE-0507 引入的 borrow/mutate 访问器暴露目标值。与get访问器不同,borrow访问器暴露存储值而无需复制;与yielding borrow/yielding mutate协程访问器(SE-0474 产出式访问器)不同,它不需要协程的工作空间开销,在无法完全内联时性能更好。

注:上文中@_lifetimeborrow/mutate访问器语法在写作时尚属演进中的特性,实际使用请以对应 Swift 6.4 开发快照与后续正式文档为准。

典型用法:一次性投影、反复修改

引用的构造方式是把目标值传给初始化器,之后通过value属性访问目标。下面这个例子把inout参数的"一次性投影"能力发挥出来——只查一次字典,随后反复累加,避免每次循环都去哈希表中查找:

func updateTotal(in dictionary: inout [String: Int], for key: String, with values: [Int]) { // Project a key out of a dictionary once... var entry = MutableRef(&dictionary[key, default: 0]) // ...and then repeatedly modify it, without repeatedly looking into the // hash table for value in values { entry.value += value } }

借助实验性的 lifetime 特性,开发者还能写出返回引用的函数,例如在可变结构体上按索引返回字段的可变引用:

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

Ref/MutableRef还可以作为其他非逃逸类型的字段出现。下面的例子用"结构体数组(struct-of-arrays)"布局存储人员记录,并通过下标投影出包含两个可变引用的Person

// A struct-of-arrays of people records. struct People { var names: [String] var ages: [Int] subscript(i: Int) -> Person { @_lifetime(&self) mutating get { return Person(name: &names[i], age: &ages[i]) } } } // A mutable reference to a single person. struct Person: ~Copyable, ~Escapable { var name: MutableRef<String> var age: MutableRef<Int> }

引用还能作为泛型参数使用,让支持非逃逸类型的容器和包装器直接容纳引用。下面的函数在越界时返回nil,实现了"可空的可变引用":

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

生命周期依赖模型

RefMutableRef都是非逃逸类型。一旦形成,它们就携带着对目标值的生命周期依赖

  • Ref只能存活于目标值仍可被借用(borrowed)的期间;
  • MutableRef只能存活于目标值仍可被独占访问的期间。

反过来,在引用的整个存活期内,目标值一直被借用(Ref)或独占访问(MutableRef)着:目标在被Ref依赖时只能发生其他借用访问,而被MutableRef独占期间则完全不能直接使用。提案中的例子非常直观:

var totals = [17, 38] do { let apples = Ref(totals[0]) print(apples.value) // prints 17 apples.value += 2 // ERROR, Ref.value is read only totals[1] += 1 // ERROR, cannot mutate `totals` while borrowed print(totals[1]) // prints 38. we can still borrow `totals` again print(apples.value) // prints 17 } do { var bananas = MutableRef(&totals[1]) bananas.value += 2 // we can mutate the value through `MutableRef` print(bananas.value) // prints 40 print(totals[1]) // ERROR, totals is exclusively accessed by `bananas` bananas.value += 2 print(bananas.value) // prints 42 } print(totals) // prints [17, 42]

这段示例清晰展示了三种约束:Ref.value只读;借用期间目标整体不可变;而MutableRef存活期间目标被独占,任何直接访问都会报错——但一旦引用生命周期结束,独占即解除,最终totals被更新为[17, 42]

提案指出,这一行为与Array与其派生的Span/MutableSpan之间的关系如出一辙:Array.spanArray.mutableSpan属性返回的 Span 值同样依赖数组自身的生命周期。事实上,可以把Ref/MutableRef看作单值版Span/MutableSpan——后者一次性引用多个值,前者只引用一个值。

与非平凡访问(nontrivial accesses)的交互

Ref可以指向任意值,MutableRef可以指向任意可变位置,包括:

  • get/set计算属性或下标的产物;
  • yielding协程访问器产出的值(SE-0474);
  • 受动态排他性检查保护的位置;
  • willSet/didSet观察的属性。

在这些场景下,访问会先被发起以形成目标值(调用 getter、启动yielding borrow/yielding mutate协程等),随后引用才指向该值;当引用生命周期结束时,访问被结束(调用 setter、恢复 yielding 协程、触发willSet/didSet观察器)。下面的NoisyCounter展示了这一过程:

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 = MutableRef(&counter.value) // begins access to `counter.value`, prints "counted 67" counterRef.value += 1 counterRef.value += 1 // access to `counter.value` ends, prints "updating counter to 69" }

需要注意的是,Ref/MutableRef依赖于这次访问,但它们自身只引用目标值,并不捕获任何用于结束访问的上下文。因此,源自非平凡访问的引用通常不能把生命周期延长到其直接调用方之外——调用方必须在引用生命周期结束时执行结束访问的代码。提案给出了会报错的示例:

@_lifetime(&target) func noisyCounterRef(from target: inout NoisyCounter) -> MutableRef<Int> { // ERROR, would extend the lifetime of `MutableRef` outside of the formal access return MutableRef(&target.value) }

而以下三类访问在结束时不要求执行任何代码,因此指向它们的引用只受"投影出该属性/下标的那次父访问"的限制,可以把引用自由传出函数:

  • struct存储属性的直接访问;
  • 对不可变class存储属性的直接访问;
  • 经由borrow/mutate访问器的访问。

协议遵从:SendableBitwiseCopyable

条件性Sendable:当目标类型ValueSendable时,RefMutableRef条件性遵从Sendable,这与Span/MutableSpan在元素类型为Sendable时遵从Sendable的做法一致:

extension Ref: Sendable where Value: Sendable & ~Copyable {} extension MutableRef: Sendable where Value: Sendable & ~Copyable {}

BitwiseCopyableRef永远是BitwiseCopyable(按位可拷贝):

extension Ref: BitwiseCopyable {}

MutableRef本身不可拷贝(~Copyable),因此不能是BitwiseCopyable,但提案保证它永远不会发生非平凡的移动或析构操作——这对可预测性能与嵌入式等受限环境很重要(这也是 SE-0446 强调"非逃逸类型可被编译器严格验证、从而允许高效管理"的出发点)。

内部表示:Ref何时是指针、何时是值

这一节虽非使用Ref所必需,但对理解其类型布局与实现很有价值。取决于Value类型参数的性质,Ref<Value>可能表示为指向目标值内存的指针,也可能表示为目标值表示的按位拷贝。当Value满足以下任一条件时使用指针表示:

  • MemoryLayout<Value>.size大于4 * MemoryLayout<Int>.size;或
  • Value不是按位可借用(bitwise-borrowable)的;或
  • Value依赖可寻址(addressable-for-dependencies)的。

该大小阈值与 Swift 调用约定中"值是否通过寄存器传递/返回"的阈值对齐,目的是避免对超大值进行浪费的按位拷贝,同时确保Ref可以跨函数边界传递和返回,而不依赖临时的栈上分配。

按位可借用(Bitwise borrowability)

一个Int无论出现在内存的哪个位置含义都相同,因此即使它作为borrowing参数传递,Swift 在机器调用约定层面也会按值传递Int,不做间接寻址。类似地,对象引用的指针值在任何内存位置都是等价的——尽管复制强引用需要增加引用计数,但底层指针可以按值传递。这类类型被称为按位可借用类型:借用可以通过按位拷贝跨函数传递。

因此,按位可借用类型的不可变值没有稳定的地址。但有了Ref之后,理应可以定义这样的函数:给定一个借用,返回与之同生命周期的Ref

@_lifetime(borrow target) func refer<T>(to target: T) -> Ref<T> { // This ought to be allowed Ref(target) }

如果Ref一律使用指向目标的指针表示,那么对按位可借用的值构造Ref就必须先把值放进内存(可能是一次临时栈分配)。临时栈分配意味着函数无法"接收借用参数 → 构造Ref→ 返回它",因为返回的Ref会依赖函数自己的栈帧:

@_lifetime(borrow target) func refer(to target: AnyObject) -> Ref<AnyObject> { // This ought to be allowed, so `target` can't be spilled to a local // temporary allocation Ref(target) }

因此,小尺寸按位可借用类型的Ref采用值本身作为表示,除非该类型还满足"依赖可寻址"。

依赖可寻址(Addressability for dependencies)

有些类型虽然按位可借用,但其接口会产出生命周期依赖的值(如指向自身内存表示的Span)。InlineArray就是典型例子:当元素类型按位可借用时InlineArray也可按位借用,但它的span属性会产出一个指向数组元素的Span,这个Span的生命周期必须受限于对InlineArray自身的借用:

@_lifetime(borrow array) func span(over array: [2 of Int8]) -> Span<Int8> { // This ought to be allowed return array.span }

Swift 将InlineArray(以及任何在其内联存储中包含InlineArray的类型)归类为依赖可寻址。这类类型的值在"返回值的生命周期依赖该参数"的函数调用中,总是以间接方式作为参数传递。在上例中,这保证了array参数存在于比调用更长寿的内存中,使Span可以安全地构造并返回给调用方。

Ref不应干扰从目标经Ref投影出的依赖值的生命周期,因此当Value是依赖可寻址类型时,Ref采用指针表示:

@_lifetime(copy borrow) func span(over borrow: Ref<[2 of Int8]>) -> Span<Int8> { // This also ought to be allowed return borrow.target.span }

由于调用约定规定:存在返回依赖时,依赖可寻址类型按指针传递;而任何被返回的Ref都是依赖,所以Ref采用指针表示不会妨碍"从借用参数构造并返回Ref":

@_lifetime(borrow target) func refer(to target: [2 of Int8]) -> Ref<[2 of Int8]> { // This ought to be allowed. `target` is received by pointer, so // `Ref<[2 of Int8]>` using the pointer representation can point to the // caller's memory. Ref(target) }

此外,从 C、Objective-C 与 C++ 导入的structunionclass类型总是被视为依赖可寻址。这是为了让Ref类型更容易与这些语言中"用指针和/或 C++ 引用表达值间关系"的数据类型协作。

MutableRef的表示

inout参数在机器调用约定层面总是按地址传递,因此MutableRef在所有情况下都可以使用指针表示,而不会限制其跨函数调用边界传递的能力——这也是MutableRefRef的表示更简单的原因。

兼容性与采用影响

  • 源码兼容:本提案只向标准库新增两个顶层声明RefMutableRef。Swift 的名称查找规则优先使用局部定义与显式导入的名称,而非标准库名称,因此已有代码应当继续正常编译与运行。
  • ABI 兼容:本提案是纯增量式的,不影响既有代码的 ABI。
  • 采用影响:对Ref的泛型支持需要新的运行时类型布局功能,这可能会限制这些类型在面向较旧 Swift 运行时部署时的可用性。

相关演进:Ref在泛型与非拷贝迭代中的位置

Ref/MutableRef作为标准库中的一等引用类型,已经出现在多个后续提案的讨论中,从侧面印证了其设计价值:

  • 借用迭代(SE-0516):该提案讨论了BorrowingSequence如何让"只能提供借用访问其元素"的序列以Ref<Element>作为迭代元素类型,例如Span的借用迭代器就可以声明mutating func next() -> Ref<Element>;同时它也指出,让调用方通过(borrowing Ref<NoncopyableInt>) -> Bool这样的包装类型交互,可用性不佳,因此最终没有采用该备选路线。
  • Disconnected(SE-0538):该提案讨论了容器持有Disconnected<Value>元素时会产出Ref<Disconnected<Value>>投影、调用方可通过借用访问器深入访问包装值;但鉴于Disconnected无条件遵从Sendable,任何此类访问器都不健全,因为从包装中拷贝出非Sendable的含引用值会在包装存储上制造编译器无法追溯的别名。

这两个例子说明:Ref正在成为 Swift 所有权与生命周期体系中的基础构件,被后续设计反复引用与校验。

未来方向

提案在结尾探讨了几个后续可能:

更多场景的局部引用绑定

借助Ref/MutableRef,开发者可以在任何能声明变量或属性的地方形成引用绑定。但这毕竟是两个独立类型,构造与解引用都有额外的记号开销。未来仍应引入原生的引用绑定语法(可视为显式Ref/MutableRef的语法糖):

// Explicitly-formed reference let x = Ref(y) x.value.foo() // Reference binding sugar (strawman syntax) borrow x = y x.foo()

但显式引用类型仍有其不可替代的价值:

  • 引用绑定的名字直接指代目标值,通过绑定赋值会重赋值目标值,因此绑定本身不可重赋值;而在由Ref/MutableRef相互引用的值图中,遍历循环可以原地更新Ref变量,却无法更新borrow绑定。
  • Ref/MutableRef可以作为泛型类型与函数的类型实参,让引用参与泛型算法与数据结构,无需这些结构了解其细节——泛型接口只需能接受~Escapable类型即可。

隐式解引用或成员转发

为降低记号开销,可以考虑给Ref/MutableRef增加动态成员查找能力,或引入类似 RustDereftrait 的机制,把名字查找自动转发到目标value。任何这类机制都不完美,因为Ref/MutableRef自身的成员会遮蔽转发机制。

支持~Escapable目标类型

当前Ref/MutableRef都要求目标类型Escapable,编译器实现上的限制阻止了指向非逃逸类型的引用;在当前生命周期体系下,从引用投影出的非逃逸value也会被人为地限制在引用生命周期内。

始终以指针表示的借用引用类型

正如"Ref的表示"一节所述,某些Value类型下Ref采用值表示而非指针。对做手工数据布局或跨语言互操作的场景,可能希望有"始终是指针"的变体,但它在目标需要临时内存位置时才可能被迫拥有比Ref更短的最大生命周期。

广义单产出协程

Ref/MutableRef指向非平凡访问的值时,引用生命周期被限制在发起访问的调用方内(因为同一调用方必须在引用生命周期结束后结束访问)。如果允许任意函数和方法定义为单产出协程,就能定义依赖非平凡访问的引用计算函数:

yielding func noisyCounterRef(from target: inout NoisyCounter) -> MutableRef<Int> { // this would be OK, since we're yielding `MutableRef` to the caller without // ending the current execution context yield MutableRef(&target.value) }

捕获访问的引用类型

另一种处理"指向非平凡访问的引用"的工具是"胖引用":捕获被挂起的访问执行上下文,随值引用一起传递。这类类型自然占用更多空间、带来更多执行开销,但在某些场景下可能有用。

MutableRef.valueexclusive所有权或重借用

MutableRefMutableSpan共用一个可用性问题:为安全起见,通过MutableRef修改目标value要求对MutableRef值本身有独占访问。在今天的 Swift 中,这只能通过可变绑定来实现,导致MutableRef即使从未被改写,也必须赋给var绑定;返回MutableRef的表达式也不能直接在修改表达式中使用:

var source = #"print("Hello World")"# let ref = MutableRef(&source) // ERROR, mutating `value` requires mutable access to `ref` ref.value += #"; print("Goodbye Universe")"# func getRef(from: inout String) -> MutableRef<String> { return MutableRef(&from) } // ERROR, mutating `value` requires mutable access to temporary value getRef(from: &source).value += #"; print("...except for that guy")"#

可以考虑两条修复路线,都同样适用于MutableSpan

  • 新增exclusive所有权模式:适用于"因提供可变访问或因属于拥有的不可变值而独占拥有"的值,从而安全地投影出临时与不可变的MutableRef
  • 类似 Rust "重借用"(reborrowing)的机制:可变引用被投影操作消耗,但在这些依赖投影完成后可以重新形成。

备选方案回顾

  • 命名为BorrowInout:评审阶段曾用这两个名字,最终采纳时改为Ref/MutableRefRef更贴切(讨论的确实是安全的引用),MutableRef则与Span/MutableSpan的命名模式保持一致。
  • value属性的命名:也考虑过无参数下标(reference[]解引用)、更"引用化"的名字target、以及类似 C 语言*x的专用解引用运算符。最终选定value,与当时正在提案的Unique类型接口保持一致。

小结

SE-0519 用RefMutableRef两个标准库类型,把 Swift 的借用(borrowing)与独占(inout)语义从"函数参数"提升为"一等值":它们可以作为局部变量、结构体字段与泛型参数存在,由编译器通过生命周期依赖静态保证安全,同时避免了 class 装箱的分配/引用计数开销与UnsafePointer的未定义行为。再加上Ref的条件性SendableRefBitwiseCopyable、与Span/MutableSpan一致的表示策略,这一提案为 Swift 后续的借用迭代(SE-0516)、非拷贝容器(SE-0538)等特性提供了关键的引用基础设施,也为未来原生引用绑定语法铺平了道路。

  • 文档

【免费下载链接】swift-evolution

This maintains proposals for changes and user-visible enhancements to the Swift Programming Language.

项目地址:https://gitcode.com/gh_mirrors/sw/swift-evolution
点击查看免费下载
上一篇:MarS 项目安装与使用教程
下一篇:Transformers-Interpret 安装与配置指南

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询