恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
让访问器宏获得属性初始化器的 `self` 访问权:Swift Evolution SE-0539 提案详解
首页
资讯中心
/
让访问器宏获得属性初始化器的 `self` 访问权:Swift Evolution SE-0539 提案详解
让访问器宏获得属性初始化器的 `self` 访问权:Swift Evolution SE-0539 提案详解
发布时间:2026/9/23 20:06:59
让访问器宏获得属性初始化器的self访问权Swift Evolution SE-0539 提案详解【免费下载链接】swift-evolutionThis maintains proposals for changes and user-visible enhancements to the Swift Programming Language.项目地址: https://gitcode.com/gh_mirrors/sw/swift-evolution本提案SE-0539当前状态为Returned for revision即退回修订为 Swift 宏系统引入了可选的initialization:参数使accessor宏可以向编译器声明属性初始化器在宏展开之后会被移入一个允许访问self与实例成员的新上下文。本文将围绕提案原文结合仓库中 SE-0389 附加宏提案、SE-0400 init 访问器提案 与 宏愿景文档 的源码级证据完整讲解该特性的动机、设计、类型检查影响与备选方案。读完本文你将理解访问器宏在属性初始化器被接收subsume这一环节的类型检查机制掌握initialization: selfAvailable / selfUnavailable的声明语法、默认语义与编译器诊断行为并能在自己的宏库中安全地采用或拒绝这一新能力。一、背景属性初始化器为何访问不到selfSwift 中存储属性的初始化器表达式property initializer会在类型初始化期间、self可用之前被求值因此编译器默认禁止在初始化器中引用self或实例成员struct Earth { let mice 21 let noGood mice * 2 // ^ cannot use instance member mice within property initializer; // property initializers run before self is available // ✅ 初始化器表达式可以访问 self lazy var theAnswer mice * 2 }lazy关键字是例外它把初始化推迟到首次访问属性时执行此时self已完全可用。根据提案描述编译器对lazy var foo: T initExpr()会施加如下变换在同作用域内新增一个类型为T?、初值为nil的私有后备存储属性原属性被改写为计算属性在get访问器中当后备存储为nil时执行初始化器。变换结果大致如下private __foo: T? nil var foo: T { get { if let value __foo { return value } let newValue initExpr() __foo newValue return newValue } set { __foo newValue } }关键点在于初始化器表达式会像已经被搬进 getter一样在原始上下文中接受类型检查因此它可以自由使用self。而访问器宏accessor macro无法享受这一待遇。SE-0389 中定义访问器宏可以为一个存储属性或下标添加访问器例如把存储属性变成计算属性并且展开的结果是一个计算属性副作用是从存储属性上移除初始化器由宏实现决定是诊断该初始化器还是把它合并进结果。然而在 SE-0539 之前编译器一律假设属性初始化器会被急切求值无论宏展开后的真实上下文如何。于是下面的代码会报错struct Earth { let mice 21 Lazy var theAnswer mice * 2 // ^ cannot use instance member mice within property initializer; // property initializers run before self is available }这个报错是不必要的限制Lazy宏明明把初始化器搬进了允许访问self的get访问器。提案的原始 pitch 中还给出了一个更具实践价值的例子借助该能力可以在初始化Observable对象时读取 SwiftUI 的环境值environment values——这正是 SE-0395 可观测性提案 所描述的应用场景的自然延伸。二、解决方案为 accessor 宏声明initialization:参数提案的核心改动是给访问器宏的角色声明role declaration增加一个可选的initialization:参数合法取值为selfAvailable或selfUnavailableselfUnavailable保持当前行为——属性初始化器不允许访问selfselfAvailable宏作者承诺初始化器在宏展开后所处的上下文允许访问self。selfUnavailable是默认值从而保证既有宏的行为完全不变。// 示例 Lazy 宏的声明 // // 我们承诺将初始化器用于 self 可用的上下文 attached(accessor, initialization: selfAvailable, names: named(get), named(set)) attached(peer, names: prefixed(_)) public macro Lazy() #externalMacro( ... ) // 使用方式 struct Earth { let mice 21 // ✅ 这里可以使用 self Lazy var theAnswer self.mice * 2 }需要说明的是Lazy宏同时使用了accessor提供get/set与peer提供前缀_的后备存储属性两种角色这与 SE-0389 中一个附加宏可以同时扮演多种角色、按角色分别展开的设计一致——例如Clamping宏同样同时声明了attached(peer, prefixed(_))与attached(accessor)。三、详细设计类型检查的顺序与理由3.1 初始化器为什么必须先在原始上下文被检查编译器在宏展开之前会先在原始上下文中对被接收subsumed的初始化器表达式做一次类型检查宏展开之后还会在新的上下文中再次检查。提案明确强调初始上下文中的首次类型检查不能禁用原因有二它负责类型推断——初始化器的推断类型inferred type会提供给访问器宏宏展开时通常需要知道属性类型宏无法在属性类型推断出来之前展开因此两者顺序不可颠倒。以Lazy宏为例完整流程如下。首先属性类型由初始化器推断确定Lazy var foo 42 // ^ 类型检查属性初始化器后推断出的类型是 Int宏带着这一信息被调用Lazy var foo: Int 42 // ^ 宏可以看到推断出的类型 Int宏在展开时使用该推断类型private var _foo: Int? // ^ 宏在这里使用推断出的类型 var foo: Int { get { if let value _foo { return value } let newValue 42 // ^ 宏在这里重新设置了初始化器的上下文re-contextualized。 // 它将在此上下文中被再次检查以确保展开结果有效。 _foo newValue return newValue } set { _foo newValue } }由于首次类型检查发生在宏展开之前编译器此刻并不知道初始化器会被宏如何使用。例如宏可能把初始化器放进init访问器见 SE-0400init访问器让计算属性参与确定初始化分析并接收一组存储属性的初始化——此时初始化器会在封闭类型初始化期间求值self尚不可用。对这种情形现有的原始上下文中禁止self的实现是正确的。而Lazy这类宏的展开结果中重新定位后的初始化器访问self是合法的——但首次检查时无从得知这一点于是self访问被一律判为非法。这就是本提案要解决的问题。3.2 角色声明Role Declaration的完整语法initialization:参数只对 accessor 宏有效取值只有两种省略时默认selfUnavailable// Lazy 宏的声明 // // 我们承诺将初始化器用于 self 可用的上下文 attached(accessor, initialization: selfAvailable, names: named(get), named(set)) attached(peer, names: prefixed(_)) public macro Lazy() #externalMacro( ... ) // 该宏将初始化器用于 init 访问器此时 self 访问非法 attached(accessor, initialization: selfUnavailable, names: named(init)) public macro SomeEagerMacro() #externalMacro( ... ) // 如果初始化器所在上下文没有 self可以省略 initialization: 参数 attached(accessor, names: named(init)) public macro SomeEagerMacro() #externalMacro( ... )编译器对错误用法会给出明确的诊断与 Fix-it 提示// initialization: 仅对 accessor 宏可用 attached(body, initialization: selfAvailable) // | |˜˜˜˜˜˜˜˜˜˜˜˜˜˜˜˜˜˜˜˜˜˜˜˜˜˜˜˜ // | |- Error: initialization unsupported for body macros // | - Fix-it: remove initialization: selfAvailable // - Fix-it: did you mean accessor here? // 只允许 selfAvailable 或 selfUnavailable attached(accessor, initialization: ridiculous, names: named(get), named(set)) // |- Error: Unknown initialization context kind // |- Fix-it: replace ridiculous with selfAvailable // |- Fix-it: replace ridiculous with selfUnavailable // - Fix-it: remove initialization: ridiculous for default selfUnavailable context // initialization: 只接受一个参数 attached(accessor, initialization: ridiculous, absurd, names: named(get), named(set)) // ˜˜˜˜˜˜˜˜˜˜˜˜˜˜˜˜˜˜ // |- Error: initialization does not support multiple arguments // |- Fix-it: replace ridiculous, absurd with selfAvailable // |- Fix-it: replace ridiculous, absurd with selfUnavailable // - Fix-it: remove initialization: ridiculous, absurd for default selfUnavailable context // 多参数中若有一个合法Fix-it 会保留它 attached(accessor, initialization: selfAvailable, ridiculous, names: named(get), named(set)) // ˜˜˜˜˜˜˜˜˜˜˜˜˜˜˜˜˜˜˜˜˜˜˜˜˜ // |- Error: initialization does not support multiple arguments // - Fix-it: keep selfAvailable3.3 对类型检查的影响当类型检查器在原始上下文中检查初始化器时会查找附加在该属性上的、声明了initialization: selfAvailable的访问器宏。若找到则复用为lazy关键字实现的同一套类型检查逻辑——即允许访问self。其余情况行为不变self访问照旧报错。这里有一个值得注意的信任模型如果宏作者在角色声明中承诺了selfAvailable但展开时却把初始化器放进了self不可用的上下文编译器会在原始上下文中放行self访问然后在宏展开后的新上下文中对初始化器表达式进行第二次检查错误会如期在展开代码中被诊断出来。提案给出了NotLazy反例// 宏声明 attached(accessor, initialization: selfAvailable, names: named(get), named(init)) attached(peer, names: prefixed(_)) public macro NotLazy() #externalMacro( ... ) // 宏使用 struct Earth { let mice 21 NotLazy var theAnswer mice * 2 } // 展开结果 struct Earth { let mice 21 private var _theAnswer: Int var theAnswer: Int { storageRestrictions(initializes: _theAnswer) init { _theAnswer mice * 2 // ^ cannot use instance member mice within property initializer; // property initializers run before self is available } get { _theAnswer } } }注意这里的展开代码用到了storageRestrictions(initializes:)与init访问器——这正是 SE-0400 引入的语法init访问器可以接收一组存储属性的初始化其函数体被要求在全部控制流路径上完成对这些存储属性的初始化。在这一展开中mice * 2是在init访问器类型初始化期间被求值的self尚未可用因此第二次类型检查按预期报错。这也印证了提案的设计哲学selfAvailable是宏作者对编译器的承诺而非豁免——撒谎的宏会在展开后被编译器当场揭穿。四、兼容性与采用影响Source compatibility源码兼容纯增量式改动。既有宏的角色声明可以原封不动行为与今日完全一致ABI compatibility对 ABI 无任何影响采用影响Implications on adoption该特性可以在源码中自由采用与弃用不需要新的运行时版本也不依赖任何新库特性。这意味着宏库作者可以按自己的节奏评估并决定是否为宏启用selfAvailable。五、备选方案Alternatives considered提案作者对四种备选思路做了逐一审视这些讨论对理解该设计边界极有帮助。5.1 为所有被接收的初始化器普遍开启self访问一种更激进的思路是既然非法的self访问会在新上下文里被二次检查诊断出来那么所有被接收的属性初始化器都应该允许访问self。提案否定了这一方案因为它会引入源码兼容性问题——一旦self变得可用某些引用的含义会发生变化静态成员与实例成员同名时引用会被劫持struct S { let foo 0 static let foo 17 // 过去初始化为静态成员 foo17 // 现在会初始化为实例成员 foo0。 Lazy var x foo }存在名为self的实例成员时。例如从NSObject派生的类拥有一个self()方法class C: NSObject { // 过去是对继承方法 self() - Self 的未应用引用 // 现在变成 C 的一个实例。 Lazy var x self }这正是本提案把决定权交给宏作者的原因为既有宏启用selfAvailable应被视为潜在源码破坏性变更宏厂商需要谨慎评估并在文档中明确说明。5.2 利用引入的名字introduced names推断初始化上下文类型检查器本来就能看到访问器宏的引入名字introduced names。一个想法是当满足宏引入了非观察型访问器且未引入init访问器假设初始化器不会用在其中这两个条件时就自动假设self在新上下文中可用。提案指出该思路在两个层面站不住脚可以构造出反例——宏引入了一个与初始化器无关的init访问器而初始化器实际被用在 getter 中。此时self访问明明合法类型检查器却会误报错误更严重的是这会让满足条件的既有宏自动获得新行为引发 5.1 节所述的源码兼容性担忧。宏作者应当掌握是否采用该特性的主动权。5.3 宏 现有lazy关键字有人讨论过把宏附加到lazy var上——lazy本身让初始化器可用self似乎能免费达成目标。但编译器会报错MyMacro lazy var value self.compute() // lazy cannot be used on a computed property这个错误是正确的编译器无法核实宏是否真的具备惰性语义。如果放行这种组合调用方程序员就不得不了解宏实现的内部细节并自己保证lazy关键字用得没错。该思路此前已被明确拒绝过。5.4 命名与initialization: ignored扩展位命名演进最初提议的拼写是initialization: lazy|eager与lazy关键字对齐但不能准确描述类型检查阶段实际发生的事情initialization: deferred|immediate同理。还考虑过布尔形式的selfAvailable: true|false但无法表明self可用/不可用的对象究竟是什么即初始化器表达式。最终选定selfAvailable/selfUnavailable语义一目了然。initialization: ignored第三选项曾考虑增加一个initialization: ignored——若宏声明将忽略初始化器则调用方代码若提供了初始化器就给出警告。提案认为其收益存疑宏完全可以自己实现所需诊断。不过该参数并未被锁死为布尔true|false未来若确有需要仍可扩展出更多选项。六、总结与在宏生态中的定位SE-0539 是一次小而精准的语言能力扩展它只新增一个可选的initialization:参数通过显式承诺 展开后二次类型检查兜底的机制把lazy关键字享有的self访问特权安全地开放给访问器宏作者。它与既有宏体系完全正交——SE-0389 提供的attached(accessor)角色声明、SE-0400 提供的init访问器与storageRestrictions注解以及 宏愿景文档 中lazy、willSet、didSet等内建特性本质上都是语法变换的观察共同构成了理解本提案的完整上下文。对宏库开发者而言采用建议是清晰的不要轻易为既有宏启用selfAvailable——它会改变初始化器内名称解析的语义如 5.1 节所示属于潜在的源码破坏性变更而新设计的、确认会把初始化器移入self可用上下文的宏如各种Lazy变体、需要在初始化时读取环境值的Observable辅助宏则可以放心声明selfAvailable让调用方的代码在类型检查阶段就能获得正确的诊断体验。【免费下载链接】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),仅供参考