SE-0367, Swift 5.8 中实现
现有问题
随着时间的推移,Swift 引入了许多新的属性,用来在源代码中传递额外信息。现有代码可以利用新的构造来改进,引入新功能,提供新的编译检查,更好的性能等等。但是,现有代码引入新属性意味着不能在旧的编译器上使用。自然而然你会想到用条件编译来解决该问题。例如可以使用#if
检查编译器版本,查看是否可以使用@preconcurrency
属性:
#if compiler(>=5.6)
@preconcurrency protocol P: Sendable {
func f()
func g()
}
#else
protocol P: Sendable {
func f()
func g()
}
#endif
有没有发现这段代码有几个缺点?首先,有两段重复代码,因为P
协议被定义2次;其次,Swift 5.6 是第一个包含@preconcurrency
属性的编译器,但这不是由编译器自动记录的:该属性可能是由编译器标志启用的,也可能是在 Swift 5.7 开发到一半弃用的,因此检查不正确。而且,一些属性是否可用不是依赖编译器,而是平台和配置标志。例如,@objc
仅在 Swift 运行时编译用于和 Objective-C 交互时可用。
尽管上述这些都是孤立的小问题,但它们让在现有代码中采用新属性比实际情况更困难。
提议方案
为了在现有代码中更容易使用新属性,本篇提议 2 个更改:
- 无论属性声明在哪里,允许
#if
检查出现在声明属性的前面,无需再复制属性声明,仅表示为了采用新属性。
#if hasAttribute(preconcurrency)
@preconcurrency
#endif
protocol P: Sendable {
func f()
func g()
}
- 新增条件命令
hasAttribute(AttributeName)
,如果当前语言环境支持AttributeName
属性,则返回true
, 反之则false
。 如果把这两点运用到上面的例子,是不是更加简洁,可读性更强。
设计细节
语法改变
当前属性列表生成语法为:
代码语言:Swift复制attributes → attribute attributes[opt]
将通过添加条件属性来生成:
代码语言:Swift复制attributes → conditional-compilation-attributes attributes[opt]
conditional-compilation-attributes → if-directive-attributes elseif-directive-attributes[opt] else-directive-attributes[opt] endif-directive
if-directive-attributes → if-directive compilation-condition attributes[opt]
elseif-directive-attributes → elseif-directive-attributes elseif-directive-attributes[opt]
elseif-directive-attributes → elseif-directive compilation-condition attributes[opt]
else-directive-attributes → else-directive attributes[opt]
在属性列表中,可以存在一个条件子句#if...#endif
来包含另外一个属性列表,嵌套使用。
hasAttribute
的校验属性对象是:作为当前语言的一部分
许多语言特性,包括属性包装器(property wrapper), 结果构建器(result builder),还有 global actors, 等等,都引入了自定义属性的方式来实现。例如, 类型 MyWrapper
使用属性@propertyWrapper
标记,该类型已经实现@propertyWrapper
属性的要求,那么该类型可以在其他地方通过@MyWrapper
用法来使用它。虽然启用该功能的内置属性(也可以说是原始属性)可以被hasAttribute
识别,比如hasAttribute(propertyWrapper)
结果会被判断为true
,但是基于原始属性的自定义属性不会被识别。也就是说hasAttribute(MyWrapper)
的结果是false
, 不是true
, 因为MyWrapper
是自定义属性。
解析编译器不接受的条件编译 if 分支
由于支持自定义属性,属性具有非常通用的语法,对于我们在 Swift 引入任何新的特性来说,都足够了。
代码语言:Swift复制attribute → @ attribute-name attribute-argument-clause[opt]
attribute-name → identifier
attribute-argument-clause → ( balanced-tokens[opt] )
因此,基于#if hasAttribute(UnknownAttributeName)
的条件编译分支,仍然能在现有的编译器上解析,即使该条件不能用于声明上,因为虽然走进了对应的 if 分支,但是编译器有可能无法识别该内容。
#if hasAttribute(UnknownAttributeName)
@@UnknownAttributeName(something we do not understand) // okay, we parse this but don't reject it
#endif
func f()
总结
- Swift 5.8 中使用
#if hasAttribute(AttributeName)
来检查当前语言环境下支持的属性关键字,取代冗长的版本判断,去除对声明的重复定义。 - 新属性在旧的编译器声明,编译检查不会报错。