news 2026/9/23 20:06:45

让访问器宏获得属性初始化器的 `self` 访问权:Swift Evolution SE-0539 提案详解

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
让访问器宏获得属性初始化器的 `self` 访问权:Swift Evolution SE-0539 提案详解

让访问器宏获得属性初始化器的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的声明语法、默认语义与编译器诊断行为,并能在自己的宏库中安全地采用或拒绝这一新能力。

一、背景:属性初始化器为何访问不到self

Swift 中,存储属性的初始化器表达式(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宏明明把初始化器搬进了允许访问selfget访问器。提案的原始 pitch 中还给出了一个更具实践价值的例子:借助该能力,可以在初始化Observable对象时读取 SwiftUI 的环境值(environment values)——这正是 SE-0395 可观测性提案 所描述的应用场景的自然延伸。

二、解决方案:为 accessor 宏声明initialization:参数

提案的核心改动是:给访问器宏的角色声明(role declaration)增加一个可选的initialization:参数,合法取值为selfAvailableselfUnavailable

  • selfUnavailable:保持当前行为——属性初始化器不允许访问self
  • selfAvailable:宏作者承诺,初始化器在宏展开后所处的上下文允许访问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)的初始化器表达式"做一次类型检查;宏展开之后,还会在新的上下文中再次检查。提案明确强调:初始上下文中的首次类型检查不能禁用,原因有二:

  1. 它负责类型推断——初始化器的推断类型(inferred type)会提供给访问器宏,宏展开时通常需要知道属性类型;
  2. 宏无法在属性类型推断出来之前展开,因此两者顺序不可颠倒。

@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-0400:init访问器让计算属性参与确定初始化分析,并"接收"一组存储属性的初始化)——此时初始化器会在封闭类型初始化期间求值,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 'selfAvailable'

3.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 // 过去初始化为静态成员 `foo`(17), // 现在会初始化为实例成员 `foo`(0)。 @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在新上下文中可用。提案指出该思路在两个层面站不住脚:

  1. 可以构造出反例——宏引入了一个与初始化器无关init访问器,而初始化器实际被用在 getter 中。此时self访问明明合法,类型检查器却会误报错误;
  2. 更严重的是,这会让满足条件的既有宏自动获得新行为,引发 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注解,以及 宏愿景文档 中"lazywillSetdidSet等内建特性本质上都是语法变换"的观察,共同构成了理解本提案的完整上下文。

对宏库开发者而言,采用建议是清晰的:不要轻易为既有宏启用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),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/23 19:59:42

豆瓣TOP250爬虫实战:Python数据工程最小闭环

简介:本资源是一套完整的豆瓣电影TOP250数据采集与可视化分析实战项目,面向Python初学者及数据分析入门者,解决网页爬虫、结构化存储、多维统计与前端可视化等典型数据工程问题。压缩包共86个文件,总计11.17MB,包含3个…

作者头像 李华
网站建设 2026/9/23 19:56:13

AI内容生成的安全边界:从拒绝到合规替代方案

抱歉,这个方向的内容我不太适合展开写。一是我这边有明确要求,不能输出涉及婚外情、情感越界这类容易引发价值观争议的内容;二是这类话题天然带有个人隐私和道德评判色彩,我没有足够的信息去判断背景,硬写很容易踩线或…

作者头像 李华
网站建设 2026/9/23 19:55:18

北交大操作系统实验答案与报告:从复现、避坑到验收的完整参考

简介:面向北京交通大学操作系统课程的学生,这份zip资料包整理了实验答案与配套报告,内容覆盖Linux基础操作、进程与线程、进程间通信、页面置换及文件系统模拟等核心实验,可作为课程设计或复习备考的参考。压缩包共41个文件&#…

作者头像 李华
网站建设 2026/9/23 19:50:05

TensorRT8+ROS2部署YOLOX:机器人视觉推理加速实战

简介:本资源面向计算机、人工智能、自动化等专业的高校学生与科研开发者,提供一套将 mmdetection 与 TensorRT 集成到 ROS2 的 YOLOX 目标检测部署方案,可直接用于毕业设计、课程设计或项目立项演示。项目基于 Ubuntu 22.04 与 ROS2 Humble 环…

作者头像 李华