- 文档
【免费下载链接】swift-evolution
This maintains proposals for changes and user-visible enhancements to the Swift Programming Language.
导读
SE-0385(Custom Reflection Metadata)为 Swift 语言提出了一种全新的机制:库作者可以通过内置属性@reflectionMetadata把自己定义的普通类型标记为"自定义属性",客户端代码即可用这些自定义属性(如@Flag、@Named(...))标注任意可用作值的声明,随后库通过新的 Reflection API(Attribute.allInstances(of:))在运行时统一收集、懒加载并查询这些元数据。本文基于 swift-evolution 仓库中的 proposals/0385-custom-reflection-metadata.md 完整解析该提案的设计动机、init(attachedTo:)初始化器约定、协议推断规则、Reflection 查询 API 及可用性限制,帮助你理解如何在测试发现、插件注册、持久化框架等场景中落地这一模式。
注意:该提案当前状态为Returned for revision(退回修订),属于语言演进讨论中的设计文档,尚未成为 Swift 官方已实现特性。本文描述的是提案设计本身。
一、动机:声明标注是 Swift 长期缺失的一等能力
在 Swift 中,声明可以通过属性(attribute)选择内置语言特性(如@available)或库功能(如@RegexComponentBuilder、@propertyWrapper)。但长期以来,Swift 缺少让库定义"自己的属性"并把属性参数作为反射元数据查询的官方机制,这导致很多常见编程模式只能靠"约定"或"工具特殊逻辑"来凑合。
1.1 测试发现的经典痛点
单元测试库是最典型的例子:用户定义一个继承库基类的类型,把某些方法标注为测试,库自动定位、初始化并运行所有测试。Swift 至今没有官方的测试发现机制,XCTest 只能依赖两种变通方案:
- Apple 平台:借助 Objective-C 运行时枚举已知基类的全部子类与方法,把所有"签名受支持且名称以
test前缀开头"的实例方法当作测试方法; - 其他平台:Swift Package Manager 编写特殊逻辑,内省构建期索引数据来定位测试方法,再显式把发现的测试列表传给 XCTest 执行。
由此带来的三个明显缺陷被提案点名:
- 强制命名约定:所有测试方法必须以
test开头,冗余且隐式——用户可能在非测试方法上误用此前缀而不自知; - 无法携带元数据:测试是隐式声明的,用户无法说明某个测试是否启用、有哪些前置要求等附加信息,测试库也因此无法据此做更精细的执行决策;
- 工具耦合:缺少内建运行时发现机制,导致 SwiftPM 等工具必须为每种测试库写专门的发现逻辑,新增测试库支持非常困难。
1.2 注册模式与插件架构
"把代码注册给框架发现"是 Swift 程序中的通用模式。插件架构通常先用协议定义插件接口,再由客户端的具体类型实现。但这一模式强迫客户端显式提交插件类型列表或逐类型注册,容易遗漏、重复且样板代码多。
1.3 Realm@Persisted的存储与初始化开销
更进一步的例子来自 Realm Swift 的Persisted属性包装器:
@propertyWrapper public struct Persisted<Value: _Persistable> { ... } class Dog: Object { @Persisted var name: String @Persisted var age: Int }若想支持"高级 schema 定制"——比如@Persisted(named: "CustomName")指定数据库列名——把该字符串存进属性包装器会带来双重代价:
- 每个实例都要为这份声明级常量元数据多付存储空间;
- 元数据值被急切求值,容器类型每次实例化都会重复计算,代价过高。
这正是"声明级元数据不应绑定到实例存储"这一核心洞察的来源。
二、方案总览:内置属性 + 初始化器约定 + Reflection 查询
提案的解决方案由三部分组成:
- 新增内置属性
@reflectionMetadata,可应用于结构体、枚举、类和 actor; - 被标注的类型可当作自定义属性用在任何"可用作值"的声明上;自定义属性可携带额外参数,编译器会把属性应用合成为一次初始化器调用——声明值作为第一个参数传入;
- 新增Reflection API,可收集所有挂载了某个自定义属性的声明,并惰性构造元数据值。
结合 attached macros(SE-0389),Realm 的@Persisted可以演化为"宏 + 自定义元数据属性"的组合——宏负责扩展持久化类型,自定义元数据属性提供逐声明的 schema 定制:
@reflectionMetadata struct Named { let name: String init<T: _Persistable>(attachedTo: T.Type, _ name: String) { self.name = name } } @Persisted class Dog: Object { var name: String @Named("CustomName") var age: Int }该方案彻底消除了属性包装器的初始化开销,元数据独立存储,并且只在框架请求时惰性求值。
三、详细设计
3.1 声明反射元数据属性
把内置属性@reflectionMetadata附加到名义类型(struct、enum、class、actor)即可声明一个反射元数据属性类型:
@reflectionMetadata struct Example { ... }约束条件:反射元数据类型必须声明一个同步的init(attachedTo:)初始化器。attachedTo:参数的类型决定了该自定义属性可以应用于哪些种类的声明。
3.2 可应用的声明种类与attachedTo:参数形态
反射元数据自定义属性可应用于任何"可用作一等值"的声明,包括:
- 类型(Types)
- 全局函数(Global functions)
- 静态方法(Static methods)
- 实例方法(Instance methods),含非 mutating 与 mutating
- 实例属性(Instance properties)
元数据类型通过以attachedTo:标签开头的初始化器重载来声明自己支持哪些声明种类。属性应用合法当且仅当元数据类型声明了能接受相应值的初始化器。编译器合成初始化器调用时,把属性参数作为附加实参、声明值作为第一个实参:
| 声明种类 | 传入的attachedTo:值 |
|---|---|
| 类型 | 元类型(metatype),如Test.self |
| 全局函数 | 未应用的函数引用(unapplied function reference) |
T上的静态方法 | 以T.Type为首参的函数(T.Type, Args) -> Result |
T上的实例方法 | 以实例T为首参的函数(T, Args) -> Result;首参为inout T时支持 mutating 方法 |
| 实例属性 | 键路径(key-path),如\Test.answer |
提案给出了完整示例——Flag元数据类型通过六个重载覆盖全部声明种类:
@reflectionMetadata struct Flag { // Initializer that accepts a metatype of a nominal type init<T>(attachedTo: T.Type) { // ... } // Initializer that accepts an unapplied reference to a global function init<Args, Result>(attachedTo: (Args) -> Result) { // ... } // Initializer that accepts a function which calls a static method init<T, Args, Result>(attachedTo: (T.Type, Args) -> Result) { // ... } // Initializer that accepts a function which calls an instance method init<T, Args, Result>(attachedTo: (T, Args) -> Result) { // ... } // Initializer that accepts a function which calls a mutating instance method init<T, Args, Result>(attachedTo: (inout T, Args) -> Result) { // ... } // Initializer that accepts a reference to an instance property init<T, V>(attachedTo: KeyPath<T, V>, custom: Int) { // ... } } // The compiler will synthesize the following initializer call // -> Flag.init(attachedTo: doSomething) @Flag func doSomething(_: Int, other: String) {} // The compiler will synthesize the following initializer call // -> Flag.init(attachedTo: Test.self) @Flag struct Test { // The compiler will synthesize the following initializer call // -> Flag.init(attachedTo: { metatype in metatype.computeStateless() }) @Flag static func computeStateless() {} // The compiler will synthesize the following initializer call // -> Flag.init(attachedTo: { instance, values in instance.compute(values: values) }) @Flag func compute(values: [Int]) {} var state = 1 // The compiler will synthesize the following initializer call // -> Flag.init(attachedTo: { (instance: inout Test) in instance.incrementState() }) @Flag mutating func incrementState() { state += 1 } // The compiler will synthesize the following initializer call // -> Flag.init(attachedTo: \Test.answer, custom: 42) @Flag(custom: 42) var answer: Int = 42 }可见这套设计的表达力很强:属性参数(如custom: 42)会原样传给初始化器的额外参数;编译器注释清晰地展示了每个应用点最终合成的初始化器调用形态。
3.3 应用限制
同一类型只能出现一次
同一声明可以挂多个反射元数据属性,但同一个元数据类型不允许重复出现:
@Flag @Ignore func ignored() { // ✅ 合法 // ... } @Flag @Flag func specialFunction() { // 🔴 非法 // ^ error: duplicate reflection metadata attribute // ... }只能作用于主声明或同模块的 unavailable 扩展
反射元数据属性必须应用于类型的主声明,或同模块内、不可用(unavailable)、无约束的扩展中。允许 unavailable 扩展是为了让 API 实现者能"退出"某个属性;禁止应用于 available/带约束的扩展或模块外扩展,是为了防止同一类型携带多份同类反射元数据标注:
@available(*, unavailable) @Flag extension MyType { // ✅ 同模块内的 unavailable 扩展合法 }@Flag extension MyType { // 🔴 非法 // ^ error: cannot associate reflection metadata @Flag with MyType in extension }@Flag extension MyType where ... { // 🔴 非法:约束扩展 // ^ error: cannot associate reflection metadata @Flag with MyType in constrained extension }声明必须完全具体(fully concrete)
带自定义反射元数据属性的声明不能是泛型的。原因在于:泛型值在运行时必须有替换(substitution),无法以 higher-kinded 形式表示,因此无法通过"收集所有实例"的反射查询发现:
struct GenericType<T> { @Flag var genericValue: T // 🔴 error } extension GenericType where T == Int { @Flag var concreteValue: Int // ✅ 合法 }提案同时指出,未来可考虑为另一方向增加查询(例如给定键路径\Generic<Int>.value返回其自定义反射元数据)来支持泛型声明。
3.4 属性的协议推断(Inference)
反射元数据属性可以应用于协议:
@EditorCommandRecord protocol EditorCommand { /* ... */ }概念上,该属性被应用到代表具体遵循类型的泛型Self上。当某个具体类型在主声明处写下协议遵循时,属性会被自动推断:
// @EditorCommandRecord is inferred struct SelectWordCommand: EditorCommand { /* ... */ }推断规则有两个重要边界:
- 扩展中声明的遵循不允许推断。协议上的反射元数据属性是一种"要求"(requirement),因此除非主声明已显式写出该属性,否则在扩展中声明遵循会报错:
// Error unless the primary declaration of 'SelectWordCommand' has '@EditorCommandRecord' extension SelectWordCommand : EditorCommand { // 🔴 // ... }- 协议上的反射元数据属性不能带额外参数;参数必须显式写在遵循类型上。
具体类型可以显式覆写从协议推断出的属性——当元数据类型的init(attachedTo:)还有额外参数时,这允许遵循类型传入定制参数:
// Overrides the inferred `@EditorCommandRecord` attribute from `EditorCommand` @EditorCommandRecord(keyboardShortcut: "j", modifier: .command) struct SelectWordCommand: EditorCommand { /* ... */ }3.5 通过 Reflection 访问元数据
提案认为,新的 Reflection 模块是承载反射查询的自然位置,给出的 API 设计如下:
/// Get all the instances of a custom reflection attribute wherever it's attached to. /// /// - Parameters: /// - type: The type of the attribute that is attached to various sources. /// - Returns: A sequence of attribute instances of `type` in no particular /// order. public enum Attribute { public static func allInstances<T>(of type: T.Type) -> AttributeInstances<T> } /// A sequence wrapper over some runtime attribute instances. /// /// Instances of `AttributeInstances` are created with the /// `Attribute.allInstances(of:)` function. public struct AttributeInstances<T> {} extension AttributeInstances: IteratorProtocol { @inlinable public mutating func next() -> T? } extension AttributeInstances: Sequence {}关键语义:
Attribute.allInstances(of:)会跨所有模块收集某个反射属性的全部实例;- 元数据实例在查询时才被初始化(惰性);
- 当前运行 OS 上不可用的属性(即
attachedTo声明不可用)会从结果中排除,而不是返回nil占位。
3.6 魔法字面量:#function/#file/#line/#column
当反射元数据类型通过 Reflection API 被访问时,init(attachedTo:)内的魔法字面量有特殊行为:尽管实际由编译器生成的生成器函数调用,#function仍然指向属性所挂载的声明,而#file、#line、#column指向属性使用处(若属性是被推断的,则指向声明处)。
提案用跨文件示例说明:
test.swift
1: @reflectionMetadata 2: struct Flag { 3: init<T>(attachedTo: T.Type, 4: func: String = #function, 5: file: String = #file, 6: line: Int = #line, 7: column: Int = #column) {} 8: 9: init<B, V>(attachedTo: KeyPath<B, V>, 10: func: String = #function, 11: file: String = #file, 12: line: Int = #line, 13: column: Int = #column) {} 14: } 15: 16: struct Test { 17: @Flag var value: Int = 42 18: } 19: 20: @Flag 21: protocol Flagged {} 22: 23: struct InferredTest : Flagged {}other.swift
1: let flags = Attribute.allInstances(of: Flag.self)与Test.value关联的Flag.init(attachedTo:)将收到:
#function="value"#file="test.swift"#line=17#column=4
而对InferredTest上隐式推断的属性,将收到:
#function="InferredTest"#file="test.swift"#line=23#column=1
提案认为该行为对用户收益最大,因为它完整保留了属性位置信息(例如测试框架可以用#function/#line精确定位失败用例)。
3.7 API 可用性(Availability)
自定义元数据属性可以附加到具有受限可用性的声明上。对单个元数据实例的反射查询会按匹配的可用性条件门控,运行时不可用的实例返回nil:
@available(macOS 12, *) @Flag struct NewType { /* ... */ }产生NewType的Flag实例的反射查询等价于执行:
if #available(macOS 12, *) { return Flag(attachedTo: NewType.self) } else { return nil }返回nil时,Attribute.allInstances(of:)返回的集合中就不会包含代表NewType的Flag实例。
四、与仓库其他演进提案的关联
在 swift-evolution 仓库中,SE-0385 处于一条"反射与元数据"演进线索的中间位置,与之直接相关的提案包括:
- SE-0379 Opt-In Reflection Metadata:同一时期讨论的姊妹提案。它聚焦于"何时发射反射元数据"——通过引入
Reflectable标记协议、-enable-upcoming-feature OptInReflection与-enable-full-reflection-metadata等编译器旗标,让反射元数据的发射从"全有或全无"变为按需选择,并减少二进制体积。SE-0385 则解决"反射元数据里装什么、如何查询"的问题,两者互补。 - SE-0389 Attached Macros:SE-0385 在 Motivation 与 Proposed solution 中多次以 attached macros 为组合前提(如
@Persisted宏 +@Named元数据属性)。SE-0389 已实现于 Swift 5.9,为声明提供 peer/accessor/member 等扩展角色,是自定义元数据属性在真实框架中发挥威力的关键配套。 - SE-0382 Expression Macros:宏体系的基础提案,SE-0385 中自定义属性的"编译器合成初始化器调用"思路与之共享"类型检查宏参数"的基础模型。
如果你正在 swift-evolution 仓库中通读这些提案,建议按 SE-0382 → SE-0389 → SE-0385 → SE-0379 的顺序阅读,可以更完整地理解宏、反射与元数据三者的演进脉络。
五、替代方案与设计取舍
提案对评审中出现的多种替代设计做了逐条回应,理解这些取舍有助于把握该特性的边界:
5.1 扩展现有语言特性(协议遵循 / 属性包装器)
- 用协议遵循元数据发现所有遵循类型:成本极高,且绝大多数协议并不需要反射能力;用"需要时才在协议上加属性"的方式显式 opt-in,正是为了控制成本。
- 仅能发现遵循协议的类型不足以覆盖全部用例——它无法在元数据里携带自定义值(如
@EditorCommandRecord(keyboardShortcut: "j", modifier: .command))。协议要求虽可为类型提供类似能力,但无法推广到函数或计算属性上。 - 用属性包装器表示属性元数据不理想:包装器需要为每个实例存储一份 backing 存储,而声明级元数据是常量;且纯元数据用途的属性包装器本不需要引入取值间接性——值直接内联存储即可,无需合成计算属性。
5.2 在init(attachedTo:)签名中使用 Reflection 类型
曾考虑让首参类型直接使用 Reflection 模块的Field等类型。但 Reflection 类型不暴露所代表声明的接口类型(例如Field不以字段类型参数化),无法利用泛型约束或attachedTo:后的附加参数做编译期强制,故被否决。
5.3 用静态方法替代init(attachedTo:)重载
曾考虑static func buildMetadata(attachedTo:),其优势是允许返回非Self类型、甚至关联类型:
protocol Attribute { associatedtype Metadata } @reflectionMetadata struct Flag<Metadata>: Attribute { static func buildMetadata(attachedTo: ...) -> Metadata { /* ... */ } }该方案便于@propertyWrapper类型兼任@reflectionMetadata类型(仅用于元数据的自定义值可与属性包装器实例存储分离)。但最终设计选择了初始化器重载方案。
5.4 属性命名与专属@test属性
- 备选拼写包括
@runtimeMetadata、@dynamicMetadata、@metadata、@runtimeAnnotation、@runtimeAttribute、@reflectionAnnotation,最终选定@reflectionMetadata; - 社区曾提议语言内建
@test属性,但注册是测试之外的通用代码模式,允许库声明自己的领域属性是更普适的方案。
六、演进状态与后续修订
提案状态为Returned for revision,意味着评审组已反馈意见、作者正在修订(相关论坛讨论见提案头部链接)。修订历史显示,设计在评审过程中已发生多处重要调整,可作为理解最终形态的参考:
- 实例方法/静态方法的
attachedTo:参数从"未应用的函数引用"改为以T/T.Type为首参的函数(inout T支持 mutating 方法); - 属性拼写从
@runtimeMetadata改为@reflectionMetadata; - Reflection API 从返回数组改为返回自定义
Sequence类型AttributeInstances<T>,且明确"排除不可用实例"而非"返回 nil 占位"; - 补充了扩展与自定义反射元数据属性交互的说明。
七、实践要点速查
针对计划在自己的库或框架中借鉴此设计的读者,以下是提案给出的核心约束清单:
- 声明:用
@reflectionMetadata标注 struct/enum/class/actor,并提供同步的init(attachedTo:); - 覆盖面:用初始化器重载声明支持的声明种类——metatype、函数引用、
(T.Type, Args) -> Result、(T, Args) -> Result、(inout T, Args) -> Result、KeyPath<T, V>; - 去重:同一声明不允许重复挂同一元数据类型;多个不同元数据类型可以共存;
- 位置:类型上的属性只能写在主声明或同模块 unavailable 无约束扩展里;
- 具体化:被标注声明必须是完全具体的,泛型声明暂不支持运行时发现;
- 协议推断:协议上的属性会推断到主声明处遵循的具体类型,扩展中的遵循不推断;协议上的属性不能带参数,具体类型可显式覆写并追加参数;
- 查询:
Attribute.allInstances(of:)跨模块收集、惰性求值、按可用性过滤; - 位置信息:魔法字面量
#function指向声明、#file/#line/#column指向属性使用处,推断场景指向声明处。
通过这一设计,Swift 有望为测试发现、插件注册、ORM schema 定制等场景提供"库自解释、声明自描述、查询自发现"的一等语言机制——这正是 SE-0385 的核心价值所在。深入细节可继续阅读仓库内的 完整提案原文 及配套的 SE-0379 反射元数据 opt-in 提案 与 SE-0389 附加宏提案。
- 文档
【免费下载链接】swift-evolution
This maintains proposals for changes and user-visible enhancements to the Swift Programming Language.
相关推荐
Swift Testing 自定义反射:CustomTestReflectable 协议与测试输出定制实战
Swift Testing 自定义反射:CustomTestReflectable 协议与测试输出定制实战 Swift Testing 在期望(expectat
文档Swift Package Manager 自定义 Target 布局(SE-0162)完全指南:从磁盘约定到显式声明
Swift Package Manager 自定义 Target 布局(SE 0162)完全指南:从磁盘约定到显式声明 SE 0162 为 Swift Pack
文档globe夜间模式探索:如何用ASCII字符模拟地球昼夜交替
globe夜间模式探索:如何用ASCII字符模拟地球昼夜交替 globe是一款强大的ASCII地球生成工具,它能够通过简单的字符组合在终端中呈现出逼真的地球模型
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考