ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

Swift SE-0519:用 `Ref` 与 `MutableRef` 构建安全的一等引用类型

Swift SE-0519:用 `Ref` 与 `MutableRef` 构建安全的一等引用类型 文档【免费下载链接】swift-evolutionThis 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/MutableRef把inout与borrowing参数的能力提升为一等公民——存进局部变量、放进结构体字段、塞进泛型容器并用编译期生命周期检查替代不安全的UnsafePointer。背景与动机为什么需要一等引用Swift 原本只能在函数调用的边界内临时借用值inout参数调用方把某个值的独占可写访问权临时交给被调函数。被调函数可以修改参数、甚至可以消耗consume其当前值只要最后用新值把它填回去返回后所有权归还调用方。borrowing参数调用方把某个值的共享只读访问权临时交给被调函数。由于同一时刻可能还有其他访问被调函数通常只能读取好处是不需要复制一份独立的值。这两种能力都受限于函数调用这一上下文。开发者常常需要把这类引用移出函数调用的边界做成局部变量绑定、作为其他类型的成员、放进泛型容器里。SE-0519 之前只有三条不太理想的路用class 装箱把值包进一个堆上对象再传引用。代价是引入堆分配、引用计数与动态排他性检查的开销。用UnsafePointer不安全且与 Swift 的高层语义格格不入用错极易造成悬垂指针。反复通过下标/属性访问例如每次操作都去哈希表里查一次性能与可读性都不佳。Ref与MutableRef正是为填补这一空缺而生它们把借用和独占访问封装成一等公民、非逃逸~Escapable的泛型值由编译器静态保证其生命周期不越过被引用值。核心 APIRef与MutableRef提案为标准库新增两个泛型类型。Ref表示对另一个值的共享借用可以读取目标值但不能消耗或修改它public struct RefValue: ~Copyable: Copyable ~Escapable { _lifetime(borrow target) public init(_ target: borrowing Value) public var value: Value { borrow } }MutableRefValue表示对另一个值的独占访问只要MutableRef存活其持有者就能修改目标值并独占使用它public struct MutableRefValue: ~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值的关键机制。borrow与mutate访问器value属性通过 SE-0507 引入的 borrow/mutate 访问器暴露目标值。与get访问器不同borrow访问器暴露存储值而无需复制与yielding borrow/yielding mutate协程访问器SE-0474 产出式访问器不同它不需要协程的工作空间开销在无法完全内联时性能更好。注上文中_lifetime、borrow/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) - MutableRefDouble { 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: MutableRefString var age: MutableRefInt }引用还能作为泛型参数使用让支持非逃逸类型的容器和包装器直接容纳引用。下面的函数在越界时返回nil实现了可空的可变引用_lifetime(array) func element(of array: inout [Int], at: Int) - MutableRefInt? { if at 0 at array.count { return array[at] } else { return nil } }生命周期依赖模型Ref和MutableRef都是非逃逸类型。一旦形成它们就携带着对目标值的生命周期依赖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.span和Array.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) - MutableRefInt { // ERROR, would extend the lifetime of MutableRef outside of the formal access return MutableRef(target.value) }而以下三类访问在结束时不要求执行任何代码因此指向它们的引用只受投影出该属性/下标的那次父访问的限制可以把引用自由传出函数对struct存储属性的直接访问对不可变class存储属性的直接访问经由borrow/mutate访问器的访问。协议遵从Sendable与BitwiseCopyable条件性Sendable当目标类型Value是Sendable时Ref和MutableRef条件性遵从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类型参数的性质RefValue可能表示为指向目标值内存的指针也可能表示为目标值表示的按位拷贝。当Value满足以下任一条件时使用指针表示MemoryLayoutValue.size大于4 * MemoryLayoutInt.size或Value不是按位可借用bitwise-borrowable的或Value是依赖可寻址addressable-for-dependencies的。该大小阈值与 Swift 调用约定中值是否通过寄存器传递/返回的阈值对齐目的是避免对超大值进行浪费的按位拷贝同时确保Ref可以跨函数边界传递和返回而不依赖临时的栈上分配。按位可借用Bitwise borrowability一个Int无论出现在内存的哪个位置含义都相同因此即使它作为borrowing参数传递Swift 在机器调用约定层面也会按值传递Int不做间接寻址。类似地对象引用的指针值在任何内存位置都是等价的——尽管复制强引用需要增加引用计数但底层指针可以按值传递。这类类型被称为按位可借用类型借用可以通过按位拷贝跨函数传递。因此按位可借用类型的不可变值没有稳定的地址。但有了Ref之后理应可以定义这样的函数给定一个借用返回与之同生命周期的Ref_lifetime(borrow target) func referT(to target: T) - RefT { // This ought to be allowed Ref(target) }如果Ref一律使用指向目标的指针表示那么对按位可借用的值构造Ref就必须先把值放进内存可能是一次临时栈分配。临时栈分配意味着函数无法接收借用参数 → 构造Ref→ 返回它因为返回的Ref会依赖函数自己的栈帧_lifetime(borrow target) func refer(to target: AnyObject) - RefAnyObject { // This ought to be allowed, so target cant 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]) - SpanInt8 { // 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]) - SpanInt8 { // 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 // callers memory. Ref(target) }此外从 C、Objective-C 与 C 导入的struct、union与class类型总是被视为依赖可寻址。这是为了让Ref类型更容易与这些语言中用指针和/或 C 引用表达值间关系的数据类型协作。MutableRef的表示inout参数在机器调用约定层面总是按地址传递因此MutableRef在所有情况下都可以使用指针表示而不会限制其跨函数调用边界传递的能力——这也是MutableRef比Ref的表示更简单的原因。兼容性与采用影响源码兼容本提案只向标准库新增两个顶层声明Ref与MutableRef。Swift 的名称查找规则优先使用局部定义与显式导入的名称而非标准库名称因此已有代码应当继续正常编译与运行。ABI 兼容本提案是纯增量式的不影响既有代码的 ABI。采用影响对Ref的泛型支持需要新的运行时类型布局功能这可能会限制这些类型在面向较旧 Swift 运行时部署时的可用性。相关演进Ref在泛型与非拷贝迭代中的位置Ref/MutableRef作为标准库中的一等引用类型已经出现在多个后续提案的讨论中从侧面印证了其设计价值借用迭代SE-0516该提案讨论了BorrowingSequence如何让只能提供借用访问其元素的序列以RefElement作为迭代元素类型例如Span的借用迭代器就可以声明mutating func next() - RefElement同时它也指出让调用方通过(borrowing RefNoncopyableInt) - Bool这样的包装类型交互可用性不佳因此最终没有采用该备选路线。DisconnectedSE-0538该提案讨论了容器持有DisconnectedValue元素时会产出RefDisconnectedValue投影、调用方可通过借用访问器深入访问包装值但鉴于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) - MutableRefInt { // this would be OK, since were yielding MutableRef to the caller without // ending the current execution context yield MutableRef(target.value) }捕获访问的引用类型另一种处理指向非平凡访问的引用的工具是胖引用捕获被挂起的访问执行上下文随值引用一起传递。这类类型自然占用更多空间、带来更多执行开销但在某些场景下可能有用。MutableRef.value的exclusive所有权或重借用MutableRef与MutableSpan共用一个可用性问题为安全起见通过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) - MutableRefString { 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的机制可变引用被投影操作消耗但在这些依赖投影完成后可以重新形成。备选方案回顾命名为Borrow和Inout评审阶段曾用这两个名字最终采纳时改为Ref/MutableRef。Ref更贴切讨论的确实是安全的引用MutableRef则与Span/MutableSpan的命名模式保持一致。value属性的命名也考虑过无参数下标reference[]解引用、更引用化的名字target、以及类似 C 语言*x的专用解引用运算符。最终选定value与当时正在提案的Unique类型接口保持一致。小结SE-0519 用Ref与MutableRef两个标准库类型把 Swift 的借用borrowing与独占inout语义从函数参数提升为一等值它们可以作为局部变量、结构体字段与泛型参数存在由编译器通过生命周期依赖静态保证安全同时避免了 class 装箱的分配/引用计数开销与UnsafePointer的未定义行为。再加上Ref的条件性Sendable、Ref的BitwiseCopyable、与Span/MutableSpan一致的表示策略这一提案为 Swift 后续的借用迭代SE-0516、非拷贝容器SE-0538等特性提供了关键的引用基础设施也为未来原生引用绑定语法铺平了道路。赞分享文档【免费下载链接】swift-evolutionThis maintains proposals for changes and user-visible enhancements to the Swift Programming Language.项目地址https://gitcode.com/gh_mirrors/sw/swift-evolution点击查看免费下载相关推荐Swift Regex Builder DSL 完全指南用 result builder 构建类型安全的正则表达式SE-0351Swift Regex Builder DSL 完全指南用 result builder 构建类型安全的正则表达式SE 0351 本文基于 swift e文档Swift SE-0068 解读将 Self 扩展到类成员与值类型统一动态类型引用语法Swift SE 0068 解读将 Self 扩展到类成员与值类型统一动态类型引用语法 导读 本文围绕 swift evolution 仓库中的 SE 00文档Swift dynamicCallable 实战指南让 Swift 类型像 Python/JavaScript 一样可调用SE-0216 全解读Swift dynamicCallable 实战指南让 Swift 类型像 Python/JavaScript 一样可调用SE 0216 全解读 文档上一篇MarS 项目安装与使用教程下一篇Transformers-Interpret 安装与配置指南创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表