Swift 参数所有权修饰符 `borrowing` 与 `consuming` 完全指南:SE-0377 的设计、语法与实战
Swift 参数所有权修饰符borrowing与consuming完全指南SE-0377 的设计、语法与实战【免费下载链接】swift-evolutionThis maintains proposals for changes and user-visible enhancements to the Swift Programming Language.项目地址: https://gitcode.com/gh_mirrors/sw/swift-evolution本文以 Swift Evolution 提案 SE-0377《borrowingandconsumingparameter ownership modifiers》 为核心系统讲解 Swift 中两个显式参数所有权修饰符的动机、语法、语义规则、协议交互、ABI 影响与生态演进。读者将掌握如何用borrowing/consuming精确控制函数接收不可变参数的拷贝与 ARC 开销如何配合copy、consume、borrow操作符管理值生命周期以及为何这是非拷贝类型~Copyable落地的前置基石。一、背景与动机为什么需要显式的参数所有权Swift 依赖自动引用计数ARC管理引用计数对象的生命周期。当调用者向被调函数按值传递一个对象时编译器在两种主流约定之间选择其一借用borrow调用者保证其参数对象在整个调用期间存活被调函数无需释放它除非它自身额外执行了 retain。消费consume被调函数负责释放该参数或把所有权继续转交给别处。如果调用者不想放弃自己对实参的所有权就必须先 retain 实参让被调函数消费掉多出来的那份引用计数。这两种约定对值类型同样成立——值类型中的retain是一次独立的拷贝release则是该拷贝的析构与释放。1.1 Swift 的默认选择及其局限Swift 默认按一套启发式规则决定约定初始化器和属性 setter 更倾向于消费参数因为它们通常要用参数构造或更新另一个值直接转发所有权更高效其余函数默认借用参数因为实践中这在大多数场景下更高效。这些默认选择通常表现良好但并非总是最优优化器虽然支持函数签名优化function signature optimization可以在看到机会时改变函数内部采用的约定以降低整体 ARC 流量但能自动化的场景有限对 ABI 稳定的库所有权约定一旦固化就属于 ABI 的一部分之后无法更改优化器不会尝试优化多态接口例如非 final 类方法或协议要求。在这些场景下如果程序员想要不同于默认的行为此前没有任何办法。1.2 非拷贝类型的到来使约定成为 API 契约SE-0390《Noncopyable structs and enums》 为 Swift 引入了非拷贝类型struct/enum可通过~Copyable抑制默认的Copyable约束。由于非拷贝类型的值无法被拷贝借用与消费的区别就变成了 API 契约中至关重要的一部分借用非拷贝值临时使用该值之后它仍然有效就像读文件句柄消费非拷贝值使用后该值即告失效无法再被使用就像关闭文件句柄。依赖编译器隐式选择参数约定无法满足这类类型的需求。SE-0377 由此提出两个显式修饰符把选择权交给开发者。二、核心概念借用、消费与修改参照 SE-0390 的语义定义SE-0377 文档明确将借用操作、消费操作与修改操作的定义锚定在 SE-0390 的 《Using noncopyable values》 一节原文档约 334 行起。三者构成 Swift 值语义操作的三元组操作访问权限对值的影响对应修饰符/语法borrowing共享访问shared access不使值失效可被多个借用者同时共享但不能修改或消费borrowingconsuming独有所有权使值失效销毁、释放资源或将所有权转交他人consumingmutating独占访问exclusive access可自由修改受排他律law of exclusivity约束可消费当前值但完成前必须填入新值inout/mutating对可拷贝类型这三者的区别在很大程度上对程序员隐藏——编译器会按需插入隐式拷贝来维持值的语义把消费操作改造成先拷贝、再把拷贝交给消费方即可继续使用原值。对不可拷贝的值这条路走不通于是区别被迫显式化——这正是 SE-0377 与 SE-0390 相辅相成的原因。三、语法设计修饰符可以出现在哪些位置borrowing和consuming成为参数类型声明内部的上下文关键字。它们可以出现在inout能出现的相同位置且三者两两互斥。3.1 在func、subscript、init声明中func foo(_: borrowing Foo) func foo(_: consuming Foo) func foo(_: inout Foo)3.2 在闭包中bar { (a: borrowing Foo) in a.foo() } bar { (a: consuming Foo) in a.foo() } bar { (a: inout Foo) in a.foo() }3.3 在函数类型中let f: (borrowing Foo) - Void { a in a.foo() } let f: (consuming Foo) - Void { a in a.foo() } let f: (inout Foo) - Void { a in a.foo() }3.4 方法的self参数方法同样可以用consuming或borrowing修饰符分别表示消费self的所有权或借用self。它们与既有的mutating修饰符两两互斥struct Foo { consuming func foo() // consuming self borrowing func foo() // borrowing self mutating func foo() // 以 inout 语义修改 self }3.5 明确的限制consuming不能应用于非逃逸闭包类型的参数——非逃逸闭包本质上永远是借用的// ERROR: cannot consume a nonescaping closure func foo(f: consuming () - ()) { }3.6 调用侧语法完全不变consuming或borrowing修饰符不影响调用者传递实参的语法也不影响方法调用中self的书写方式。对典型的 Swift 代码而言添加、移除或修改这些修饰符没有任何源码破坏性影响文档同时在Related directions中提醒某些正在考虑中的语言特性可能与之交互并产生破坏。四、协议要求与函数类型中的所有权约定转换4.1 协议要求协议要求也可以使用consuming和borrowing修饰符会影响泛型接口调用该要求的约定。只要参数类型是可拷贝的实现方可以使用不同的约定来满足要求protocol P { func foo(x: consuming Foo, y: borrowing Foo) } // 以下都是合法实现 struct A: P { func foo(x: Foo, y: Foo) } struct B: P { func foo(x: borrowing Foo, y: consuming Foo) } struct C: P { func foo(x: consuming Foo, y: borrowing Foo) }4.2 函数值的隐式转换函数值也可以被隐式转换为改变了可拷贝参数约定的函数类型转换可以在未指定、borrowing、consuming之间进行let f { (a: Foo) in print(a) } let g: (borrowing Foo) - Void f let h: (consuming Foo) - Void f let f2: (Foo) - Void h4.3 非拷贝类型的硬性限制上述协议实现与函数值的隐式转换对非拷贝参数类型一律不适用——非拷贝类型必须精确匹配约定。这一设计直接呼应了 SE-0377 作为 SE-0390 前置条件的定位。五、参数绑定的使用规则不可隐式拷贝与copy x这是 SE-0377 修订后的核心语义borrowing和consuming参数绑定在函数或闭包体内不再隐式可拷贝。5.1consuming参数可变函数或闭包体内的consuming参数可以被修改consuming func方法的self参数同样可变。这些修改发生在函数自己取得所有权的值上不会体现在调用者那里可能仍然存在的任何拷贝中。这让我们可以充分利用所有权转移后的唯一性对值做高效的本地修改extension String { // 把 self 追加到另一个 String 上尽可能利用就地修改 consuming func plus(_ other: String) - String { // 就地修改我们拥有的 self 拷贝利用唯一性如果可能 self other return self } } // 摊还复杂度是 O(n) 而不是 O(n^2) let helloWorld hello .plus(cruel ).plus(world)5.2 不可隐式拷贝带来的编译错误borrowing和consuming参数值在函数或闭包体内不可隐式拷贝func foo(x: borrowing String) - (String, String) { return (x, x) // ERROR: needs to copy x } func bar(x: consuming String) - (String, String) { return (x, x) // ERROR: needs to copy x }方法级borrowing/consuming修饰符下的self参数同理extension String { borrowing func foo() - (String, String) { return (self, self) // ERROR: needs to copy self } consuming func bar() - (String, String) { return (self, self) // ERROR: needs to copy self } }5.3 何时需要隐式拷贝一个值需要被隐式拷贝的情况包括对borrowing绑定执行消费操作对consuming绑定在已被消费之后或同时有其他借用/修改操作进行中时再执行消费操作。其中消费操作、借用操作、修改操作的定义与 SE-0390 中对非拷贝类型值的描述完全一致详见 SE-0390 文档。本质上为一个绑定禁用隐式拷贝会让该绑定表现得就像某种非拷贝类型的值。5.4 用copy x显式允许拷贝允许拷贝发生的办法是使用copy x操作符func dup(_ x: borrowing String) - (String, String) { return (copy x, copy x) // OK这里显式允许拷贝 }copy x是对x的一次借用操作返回当前x值的一个独立拥有的拷贝。该拷贝随后可以被独立地消费或修改而不会影响原始的x。需要注意copy只是允许拷贝发生并不构成编译器必须拷贝的严格义务——如果语义上认为拷贝不必要拷贝仍可能被优化掉。copy是上下文关键字与之前的consume x类似如果它后面紧跟同一行中的标识符就被解析为操作符其他所有情况下copy仍被当作名为copy的声明引用与提案之前的行为一致。5.5 约束边界仅限绑定本身不可隐式拷贝的约束只作用于参数绑定本身。参数的值可以传给其他函数或在约定允许时赋给其他变量——此时值可以经由那些参数或变量绑定被隐式拷贝func foo(x: borrowing String) { let y x // ERROR: attempt to copy x bar(z: x) // OK调用 bar(z:) 不要求拷贝 x } func bar(z: String) { let w z // OKz 在这里隐式可拷贝 } func baz(a: consuming String) { // let aa (a, a) // ERROR: attempt to copy a let b a let bb (b, b) // OKb 隐式可拷贝 }要澄清约束的作用边界在调用者一侧作为调用表达式一部分的参数绑定值被视为不可拷贝——如果构成调用需要拷贝就会报错即使参数在被调函数体内是可隐式拷贝的也一样。函数体是不可隐式拷贝约束的边界struct Bar { var a: String var b: String init(ab: String) { // OKab 在这里隐式可拷贝 a ab b ab } } func foo(x: borrowing String) { _ Bar(ab: x) // ERROR: would need to copy x to let Bar.init consume it }六、源码兼容性让 API 作者自由微调而客户端无感6.1 对现有代码的影响今天给参数加上consuming或borrowing不影响该函数之外既有代码的源码兼容性调用者可以像往常一样调用该函数函数体也可以照旧使用参数。带修饰符参数的方法仍可用于满足带不同修饰符的协议要求。虽然consuming参数绑定变得可变、带两种修饰符的参数不再隐式可拷贝但这些效果都局限于采用修饰符的函数内部。这意味着 API 作者可以用consuming/borrowing注解微调实现的拷贝行为而不必强迫客户端了解所有权才能使用这些注解。只发布源码的包可以在可拷贝类型上随时增删或调整这些注解而不破坏客户端。6.2 需要注意的破坏方向把参数修饰符从borrowing改成consuming可能破坏那些同样采用了参数修饰符的客户端代码因为改动会影响调用者一侧需要发生拷贝的位置从consuming改成borrowing对可拷贝类型一般不具源码破坏性若参数类型是非拷贝类型则任何方向的修改都具有源码破坏性。七、对 ABI 稳定性与 API 弹性的影响ABI 稳定性consuming或borrowing影响 ABI 层面的调用约定对 ABI 稳定的库不可更改平凡类型除外——对平凡类型而言拷贝等价于memcpy、析构是空操作此时consuming/borrowing也没有实际效果。API 弹性修饰符对 ABI 稳定的库会破坏 ABI但对源码级 API 的影响极小。使用可拷贝类型时为 API 添加或更改这些注解不应影响既有客户端——除非那些客户端也采用了不可隐式拷贝的约定。八、设计权衡被否决的备选方案SE-0377 记录了多个被认真讨论后否决的备选方向理解它们有助于把握最终设计的边界。8.1 让consuming参数保持不可变提案最终选择让consuming参数在 callee 内可变理由是 callee 很可能想利用自己拥有的值做修改。虽然有人担心这种修改在调用者的拷贝中不可见、可能引起困惑这正是 SE-0003 当初移除var参数的动机但consuming与var有本质区别var/inout都暗示可变性且var不指明修改可见的方向而consuming对调用者不暗示任何可变性并明确表达了所有权转移的方向。对非拷贝类型而言混淆的可能更是无从谈起——所有权转移后调用者本来就无法再使用该值。8.2 命名演进从shared/owned、take到consuming编译器最初的实现使用__shared和__owned去下划线即shared/owned但共享借用与独占借用的技术措辞对解释模型来说过于绕早期 pitch 用过nonconsuming/consuming实现中也用__consuming func表示消费self的方法最终认为用借用borrowing表述其含义本身比消费的对立面更好第一次评审的修订版用过take与borrow相对但评审者指出take与口语中函数 take 它的参数冲突consume同样顺口且更具体评审中还提出过use、own、sink等替代名。最终命名刻意与对应的consume和borrow操作符保持一致以强化调用约定与表达式操作符之间的关联调用侧写foo(consuming x)显式转移所有权声明侧写func foo(_: consuming T)借用同理foo(borrow x)与func foo(_: borrowing T)配对。8.3noImplicitCopy属性另一种思路是用一个可附加到任意绑定的属性来表达不可隐式拷贝noImplicitCopy(self) func foo(x: noImplicitCopy String) { noImplicitCopy let y copy x }社区反馈正确地指出这种写法语法笨重、噪音大且作为属性让控制拷贝显得像事后补充、与语言整体结合不紧密。最终决定放弃该方向把不可隐式拷贝行为直接绑定在所有权修饰符上设计更内聚。8.4copy作为普通函数与consume x/borrow x操作符不同拷贝本身没有非要用操作符不可的语义需求copy本可以被定义为标准库普通函数func copyT(_ value: T) - T { return value }但最终选择copy x操作符原因有二一是与consume x、borrow x保持形式上的呼应二是避免污染全局标识符命名空间也避免作为标准库函数时偶尔需要写成Swift.copy的窘境。8.5 传递性约束不可隐式拷贝约束只作用于该绑定本身不会沿值传递链传给接收该绑定值的其他变量或函数调用实参。曾考虑过更严格的方案——例如参数只能传给同样用borrowing/consuming修饰的函数参数或者必须绑定到借用绑定上——但这样做会大幅抬高采用门槛开发者只能从叶子函数自底向上引入修饰符而且并不能真正改善局部推理约束只针对隐式拷贝显式拷贝依然可能调用另一个函数仍可能导致拷贝发生唯一可靠的办法是检查被调方的实现。SE-0377 的目标之一就是以最小扰动引入修饰符允许在必要处轻松采用传递性要求会妨碍这一目标且收益甚微。九、相关方向与生态演进SE-0377 不是孤立特性它与所有权体系中的一系列特性相互配合。这些内容也直接体现在本仓库的后续提案中。9.1consume操作符SE-0366SE-0366《consumeoperator to end the lifetime of a variable binding》 引入了显式结束局部变量生命周期的操作符它让编译器能够在变量最后使用处可靠地销毁或转移所有权而不依赖优化器。当变量生命周期在传给consuming参数的实参处结束时所有权可以直接转移给 callee 而无需任何拷贝func consume(x: consuming Foo) func produce() { let x Foo() consume(x: consume x) doOtherStuffNotInvolvingX() }9.2borrow操作符另一类场景是编译器在理论上可以借用、却默认选择拷贝——典型是共享可变状态全局/静态变量、逃逸闭包捕获、类的存储属性。编译器这样做是为了避免与排他律exclusivity冲突var global Foo() func useFoo(x: borrowing Foo) { // 这里我们需要对 global 的独占访问 global Foo() } func callUseFoo() { // callUseFoo 不知道 useFoo 是否会访问 global // 所以不想过久地施加共享访问于是传一份值的拷贝。这会编译成大致等价于 // let globalCopy copy(global) // useFoo(x: globalCopy) // destroy(globalCopy) useFoo(x: global) }编译器很难彻底证明对共享可变状态不存在潜在的干扰写因此理论上虽可以消除这种防御性拷贝实践中几乎不会。如果开发者能确认调用期间程序不会修改同一对象或全局变量显式borrow操作符就可以抑制这份拷贝var global Foo() func useFooWithoutTouchingGlobal(x: borrowing Foo) { /* global 在这里不被使用 */ } func callUseFoo() { // 程序员知道 useFooWithoutTouchingGlobal 不会碰 global // 所以想不拷贝地传过去 useFooWithoutTouchingGlobal(x: borrow global) }如果useFooWithoutTouchingGlobal实际上试图在调用者借用期间修改global就会触发排他性失败。9.3 非拷贝类型SE-0390与泛型SE-0427consuming与borrowing的区别对无法隐式拷贝的值而言更为重要和突出。SE-0390 引入的非拷贝类型要求非拷贝类型的参数必须显式声明是borrowing还是consuming因为不存在一个总是安全、可默认假定的清晰缺省值。原提案给出了非常直观的文件句柄示例struct FileHandle: ~Copyable { ... } // 打开文件句柄的操作返回新的 FileHandle 值 func open(path: FilePath) throws - FileHandle // 操作打开中的句柄且保持其打开借用手柄 func read(from: borrowing FileHandle) throws - Data // 关闭句柄并使其不可再用的操作消费手柄 func close(file: consuming FileHandle) func hackPasswords() throws - HackedPasswords { let fd try open(path: /etc/passwd) // read 借用 fd所以之后还能继续使用 let contents try read(from: fd) // close 消费 fd所以不能再使用它 close(fd) let moreContents try read(from: fd) // compiler error: use after consume return hackPasswordData(contents) }SE-0427《Noncopyable generics》 进一步把这条规则推广到泛型参数非拷贝泛型参数类型在作为函数参数类型出现时必须以borrowing、consuming或inout之一作为前缀该提案在 160 行处明确引用了 SE-0377 作为细节来源func identityT: ~Copyable(x: consuming T) { return x }9.4set/out参数约定显式化borrowing/consuming后参数处理方式的集合基本补全inout参数获得独占访问可修改或替换当前值borrowing参数获得共享访问多段代码可共享同一值而不拷贝只要都不修改它consuming参数消费掉值、什么都不留下。但还缺一个对立面接收未初始化实参并用新值填充它——即 C# 和 Objective-C配合 Distributed Objects 使用中的out参数约定Val 语言称之为set。Swift 至今偏好用返回值向调用者回传值本提案不引入out参数但未来提案可以。9.5borrowing/mutating/consuming局部变量与后续生态SE-0377 还预告了borrow和inout局部绑定声明将把同样的不可隐式拷贝约束应用于这些绑定。这一方向的后续发展在仓库中清晰可见SE-0516《Iterable》原BorrowingSequenceSwift 6.4 实现为迭代引入Iterable协议使~Copyable、~Escapable类型和非拷贝元素也能被for-in借用迭代SE-0519《RefandMutableReftypes for safe, first-class references》 以RefValue: ~Copyable等类型把借用引用提升为一等公民其初始化器正是init(_ target: borrowing Value)。这些提案共同构成了 Swift 所有权ownership能力的完整拼图而 SE-0377 的参数修饰符正是其中最基础、最常用的一块。十、修订历程从take到最终形态第一次评审修订版以take/taking命名 callee 销毁约定第二次评审修订版使用祈使形式consume和borrow作为参数修饰符评审时改为动名词consuming和borrowing该版本曾被接受当前修订版改动为borrowing和consuming参数绑定不可隐式拷贝并引入copy x操作符在需要处显式允许拷贝——这也正是本仓库中该文档最终版本的完整语义。总结SE-0377 通过borrowing与consuming两个参数所有权修饰符把 Swift 此前由编译器隐式决定的参数约定交还给开发者使其能够精确削减函数调用中的 ARC 开销与拷贝次数并为~Copyable非拷贝类型SE-0390与所有权操作符生态consume/borrow/copy、SE-0366 及后续 SE-0427、SE-0516、SE-0519 等提供了不可或缺的语法与语义地基。无论是追求极致性能的可拷贝类型代码还是管理文件句柄、缓冲区等唯一资源理解并善用这两个修饰符都已成为现代 Swift 开发者的基本功。更多细节可直接研读本仓库中的 SE-0377 提案原文 及其引用的系列提案文档。【免费下载链接】swift-evolutionThis maintains proposals for changes and user-visible enhancements to the Swift Programming Language.项目地址: https://gitcode.com/gh_mirrors/sw/swift-evolution创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考