3.7 属性特性
30 分钟阅读
原文链接: https://docs.swift.org/latest/documentation/the-swift-programming-language/attributes/
3.7 属性特性
为声明和类型添加信息。
Swift 中的属性特性有两类——一类作用于声明,一类作用于类型。属性特性提供关于声明或类型的附加信息。例如,函数声明上的 discardableResult 特性表示:虽然该函数返回一个值,但如果这个返回值没有被使用,编译器不应该生成警告。
书写属性特性时,用 @ 符号后跟特性名,以及该特性接受的任何实参:
| |
有些声明特性接受实参,用来提供关于该特性以及它如何作用于某个特定声明的更多信息。这些特性实参括在圆括号中,其格式由所属的特性定义。
附加宏和属性包装器也使用特性语法。关于宏如何展开的内容,参见宏展开表达式;关于属性包装器的内容,参见 propertyWrapper。
声明特性
声明特性只能作用于声明。
attached
把 attached 特性应用于宏声明。这个特性的实参表明宏的角色:对于有多个角色的宏,多次应用 attached 特性,每个角色一次。
这个特性的第一个实参表明宏的角色:
同伴(peer)宏:把
peer作为该特性的第一个实参。实现该宏的类型遵循PeerMacro协议。这类宏在与宏所依附声明相同的作用域中产生新声明。例如,把同伴宏应用于结构体的某个方法,可以在该结构体上定义额外的方法和属性。成员(member)宏:把
member作为该特性的第一个实参。实现该宏的类型遵循MemberMacro协议。这类宏产生的新声明是宏所依附类型或扩展的成员。例如,把成员宏应用于结构体声明,可以在该结构体上定义额外的方法和属性。成员属性(member attribute)宏:把
memberAttribute作为该特性的第一个实参。实现该宏的类型遵循MemberAttributeMacro协议。这类宏为宏所依附类型或扩展的成员添加特性。访问器(accessor)宏:把
accessor作为该特性的第一个实参。实现该宏的类型遵循AccessorMacro协议。这类宏为它们所依附的存储属性添加访问器,把它变成计算属性。扩展(extension)宏:把
extension作为该特性的第一个实参。实现该宏的类型遵循ExtensionMacro协议。这类宏可以添加协议遵循性、where子句,以及作为宏所依附类型成员的新声明。如果宏添加协议遵循性,就包含conformances:实参并指定那些协议。遵循性列表包含协议名、引用遵循性列表项的类型别名,或者遵循性列表项的协议组合。嵌套类型上的扩展宏会展开为该文件顶层的扩展。你不能在扩展、类型别名或嵌套在函数内部的类型上写扩展宏,也不能用扩展宏添加带有同伴宏的扩展。
同伴宏和成员宏角色要求提供 names: 实参,列出该宏生成的符号名。如果访问器宏生成 willSet 或 didSet 属性观察器,则该角色要求提供 names: 实参;生成属性观察器的访问器宏不能添加其他访问器,因为属性观察器只适用于存储属性。如果扩展宏在扩展内添加声明,该角色也要求提供 names: 实参。当宏声明包含 names: 实参时,宏实现只能生成名字与该列表匹配的符号。话虽如此,宏不必为列出的每个名字都生成符号。该实参的取值是下列一项或多项组成的列表:
named(<#name#>),其中 name 是固定的符号名,用于事先已知的名字。overloaded,用于与已有符号同名的名字。prefixed(<#prefix#>),其中 prefix 被加到符号名之前,用于以固定字符串开头的名字。suffixed(<#suffix#>),其中 suffix 被加到符号名之后,用于以固定字符串结尾的名字。arbitrary,用于要到宏展开时才能确定的名字。
有一种特殊情况:对于行为类似属性包装器的宏,你可以写 prefixed($)。
available
应用这个特性来表明某个声明相对于某些 Swift 语言版本,或相对于某些平台与操作系统版本的生命周期。
available 特性总是与一个包含两项或更多、以逗号分隔的特性实参的列表一起出现。这些实参以下列平台名或语言名之一开头:
iOSiOSApplicationExtensionmacOSmacOSApplicationExtensionmacCatalystmacCatalystApplicationExtensionwatchOSwatchOSApplicationExtensiontvOStvOSApplicationExtensionvisionOSvisionOSApplicationExtensionswift
你也可以用星号(*)表明该声明在上述所有平台上可用。用 Swift 版本号指定可用性的 available 特性不能使用星号。
其余实参可以按任意顺序出现,用于指定该声明生命周期的附加信息,包括重要的里程碑。
unavailable实参表示该声明在指定平台上不可用。指定 Swift 版本可用性时不能使用这个实参。introduced实参表示该声明首次被引入的指定平台或语言的版本,其形式如下:1introduced: <#version number#>版本号由一到三个正整数构成,用句点分隔。
deprecated实参表示该声明被弃用的指定平台或语言的第一个版本,其形式如下:1deprecated: <#version number#>可选的版本号由一到三个正整数构成,用句点分隔。省略版本号表示该声明目前已被弃用,但不给出弃用发生的时间信息;如果你省略版本号,也要省略冒号(
:)。obsoleted实参表示该声明被废弃的指定平台或语言的第一个版本。声明被废弃时会从指定的平台或语言中移除,不能再使用。其形式如下:1obsoleted: <#version number#>版本号由一到三个正整数构成,用句点分隔。
noasync实参表示所声明的符号不能在异步上下文中直接使用。由于 Swift 并发在潜在的挂起点之后可能在不同线程上恢复,跨挂起点使用线程局部存储、锁、互斥量或信号量之类的元素可能导致错误结果。
为避免这个问题,给符号的声明加上
@available(*, noasync)特性:1 2 3 4 5 6extension pthread_mutex_t { @available(*, noasync) mutating func lock() { pthread_mutex_lock(&self) } @available(*, noasync) mutating func unlock() { pthread_mutex_unlock(&self) } }当有人在异步上下文中使用该符号时,这个特性会产生编译期错误。你也可以用
message实参提供关于该符号的附加信息。1@available(*, noasync, message: "Migrate locks to Swift concurrency.") mutating func lock() { pthread_mutex_lock(&self) }如果你能保证代码以安全的方式使用某个可能不安全的符号,可以把它包在一个同步函数中,再从异步上下文调用该函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16// 为带 noasync 声明的方法提供同步包装。 extension pthread_mutex_t { mutating func withLock(_ operation: () -> ()) { self.lock() operation() self.unlock() } } func downloadAndStore(key: Int, dataStore: MyKeyedStorage, dataLock: inout pthread_mutex_t) async { // 在异步上下文中安全地调用该包装。 dataLock.withLock { dataStore[key] = downloadContent() } }你可以在大多数声明上使用
noasync实参,但不能在声明反初始化器时使用它:Swift 必须能够从任何上下文(同步和异步)调用类的反初始化器。message实参提供一条文字消息,当编译器就使用标记了deprecated、obsoleted或noasync的声明发出警告或错误时会显示这条消息。其形式如下:1message: <#message#>消息是一个字符串字面量。
renamed实参提供一条文字消息,指出已被重命名的声明的新名字。当编译器就使用已重命名的声明发出错误时,会显示这个新名字。其形式如下:1renamed: <#new name#>新名字是一个字符串字面量。
你可以把带
renamed和unavailable实参的available特性应用于类型别名声明(如下所示),用来表明某个声明的名字在框架或库的不同版本之间发生了改变。这种组合会产生"该声明已被重命名"的编译期错误。1 2 3 4// 首个版本 protocol MyProtocol { // 协议定义 }1 2 3 4 5 6 7// 后续版本把 MyProtocol 重命名为: protocol MyRenamedProtocol { // 协议定义 } @available(*, unavailable, renamed: "MyRenamedProtocol") typealias MyProtocol = MyRenamedProtocol
你可以在单个声明上应用多个 available 特性,指明该声明在不同平台和不同 Swift 版本上的可用性。如果某个 available 特性指定的平台或语言版本与当前目标不匹配,该特性所作用的声明就会被忽略。如果使用了多个 available 特性,有效可用性是平台可用性与 Swift 可用性的组合。
如果某个 available 特性除了平台名或语言名实参之外只指定了 introduced 实参,你可以改用下列简写语法:
| |
available 特性的简写语法可以简洁地表达多个平台上的可用性。两种形式在功能上等价,但只要有条件就应优先使用简写形式。
| |
用 Swift 版本号指定可用性的 available 特性不能同时指定声明的平台可用性。此时应改用多个 available 特性,分别指定 Swift 版本可用性和一个或多个平台可用性。
| |
backDeployed
把这个特性应用于函数、方法、下标或计算属性,把该符号实现的一份副本包含进调用或访问该符号的程序中。你用这个特性标注随平台一起发布的符号,例如操作系统附带的 API。它标记的符号可以通过把其实现的一份副本包含进访问它们的程序而追溯地变得可用。复制实现也称为发射进客户端。
这个特性接受一个 before: 实参,指定提供该符号的平台的第一个版本。这些平台版本的含义与你在 available 特性中指定的平台版本相同。与 available 特性不同的是,这个列表不能包含星号(*)来指代所有版本。例如,考虑下面的代码:
| |
在上面的例子里,iOS SDK 从 iOS 17 开始提供 someFunction(),此外该 SDK 还通过向后部署让 someFunction() 在 iOS 16 上可用。
编译调用这个函数的代码时,Swift 会插入一层用于查找该函数实现的间接引用:如果代码运行在包含该函数的 SDK 版本上,就使用 SDK 的实现;否则就使用调用方中包含的那份副本。在上面的例子里,在 iOS 17 或更高版本上运行时会使用 SDK 中的实现,而在 iOS 16 上运行时会使用调用方中包含的 someFunction() 副本。
注意: 当调用方的最低部署目标 大于或等于 包含该符号的 SDK 的第一个版本时, 编译器可以优化掉这次运行时检查, 直接调用 SDK 的实现。 在这种情况下, 如果你直接访问向后部署的符号, 编译器还可以省略 客户端中该符号实现的副本。
满足下列条件的函数、方法、下标和计算属性可以向后部署:
- 声明是
public或@usableFromInline。 - 对于类实例方法和类类型方法,该方法被标记为
final,且没有被标记为@objc。 - 实现满足可内联函数的要求,详见 inlinable。
discardableResult
把这个特性应用于函数或方法声明,当这个有返回值的函数或方法被调用却没有使用其结果时,抑制编译器警告。
dynamicCallable
把这个特性应用于类、结构体、枚举或协议,把该类型的实例当作可调用的函数来对待。该类型必须实现 dynamicallyCall(withArguments:) 方法、dynamicallyCall(withKeywordArguments:) 方法,或两者都实现。
你可以像调用一个接受任意多个实参的函数那样,调用可动态调用类型的实例。
| |
dynamicallyCall(withArguments:) 方法的声明必须只有一个遵循 ExpressibleByArrayLiteral 协议的参数——例如上面例子中的 [Int]。返回类型可以是任意类型。
如果你实现了 dynamicallyCall(withKeywordArguments:) 方法,就可以在动态方法调用中包含标签。
| |
dynamicallyCall(withKeywordArguments:) 方法的声明必须只有一个遵循 ExpressibleByDictionaryLiteral 协议的参数,返回类型可以是任意类型。该参数的 Key 必须是 ExpressibleByStringLiteral。上一个例子用 KeyValuePairs 作为参数类型,以便调用方可以包含重复的参数标签——在调用 repeat 时 a 和 b 都出现了多次。
如果你同时实现了两个 dynamicallyCall 方法,那么当方法调用包含关键字实参时调用 dynamicallyCall(withKeywordArguments:),其他所有情况下调用 dynamicallyCall(withArguments:)。
你只能用与某个 dynamicallyCall 方法实现中所指定类型相符的实参和返回值,调用可动态调用的实例。下面例子中的调用无法编译,因为不存在接受 KeyValuePairs<String, String> 的 dynamicallyCall(withArguments:) 实现。
| |
dynamicMemberLookup
把这个特性应用于类、结构体、枚举或协议,使成员可以在运行时按名字查找。该类型必须实现 subscript(dynamicMember:) 下标。
在显式成员表达式中,如果被命名的成员没有对应的声明,该表达式就被理解为对该类型 subscript(dynamicMember:) 下标的调用,并把关于该成员的信息作为实参传入。下标可以接受键路径或成员名作为参数;如果你同时实现了这两种下标,则使用接受键路径实参的那个下标。
subscript(dynamicMember:) 的实现可以用 KeyPath、WritableKeyPath 或 ReferenceWritableKeyPath 类型的实参接受键路径;也可以用遵循 ExpressibleByStringLiteral 协议的类型的实参接受成员名——大多数情况下是 String。下标的返回类型可以是任意类型。
按成员名做动态成员查找,可以用来围绕无法在编译期做类型检查的数据创建包装类型,例如把其他语言的数据桥接进 Swift 时。例如:
| |
按键路径做动态成员查找,可以用来以实现编译期类型检查的方式实现包装类型。例如:
| |
export
把这个特性应用于函数或方法声明,控制它的定义如何导出到客户端模块。包含下列实参之一,表明要导出声明的哪一部分:
interface实参表示只把接口以可调用符号的形式导出给客户端,定义(函数体)不对客户端开放,不能用于内联、优化或任何其他用途。用这个实参对客户端隐藏实现。implementation实参表示只把定义(函数体)导出给客户端:二进制中不会为该函数发射符号,客户端负责在需要的位置发射一份定义副本。用这个实参可以在不影响应用二进制接口(ABI)的前提下引入新的函数或方法。
freestanding
把 freestanding 特性应用于独立宏的声明。
frozen
把这个特性应用于结构体或枚举声明,限制你可以对该类型做的改动种类。只有在库演进模式下编译时才允许使用这个特性。该库的未来版本不能通过添加、移除或重排枚举成员或结构体的存储实例属性来改变该声明:这些改动在非冻结类型上是允许的,但会破坏冻结类型的 ABI 兼容性。
在库演进模式下,与冻结结构和枚举的成员交互的代码,其编译方式能让它在库的未来版本添加、移除或重排某些成员时无需重新编译仍可继续工作。编译器通过运行时信息查找和增加一层间接引用之类的技术实现这一点。把结构体或枚举标记为 frozen 就是放弃这种灵活性以换取性能:库的未来版本只能对该类型做有限改动,但编译器可以在与该类型成员交互的代码中做额外的优化。
冻结类型、冻结结构体存储属性的类型,以及冻结枚举成员的关联值都必须是 public,或者用 usableFromInline 特性标记。冻结结构体的属性不能有属性观察器,而为存储实例属性提供初始值的表达式必须遵循与可内联函数相同的限制,详见 inlinable。
要在命令行启用库演进模式,把 -enable-library-evolution 选项传给 Swift 编译器。要在 Xcode 中启用它,把 “Build Libraries for Distribution” 构建设置(BUILD_LIBRARY_FOR_DISTRIBUTION)设为 Yes,详见 Xcode Help。
对冻结枚举做 switch 语句不需要 default case,详见对未来的枚举成员做 switch。对冻结枚举做 switch 时包含 default 或 @unknown default case 会产生警告,因为那段代码永远不会执行。
GKInspectable
应用这个特性把自定义的 GameplayKit 组件属性暴露给 SpriteKit 编辑器 UI。应用这个特性同时隐含 objc 特性。
globalActor
把这个特性应用于 actor、结构体、枚举或 final 类。该类型必须定义一个名为 shared 的静态属性,提供该 actor 的共享实例。
全局 actor 把 actor 隔离的概念推广到分散在代码中若干不同位置(例如多个类型、文件和模块)的状态,使从并发代码安全访问全局变量成为可能。全局 actor 作为 shared 属性值提供的那个 actor 会串行化对所有这类状态的访问。你也可以用全局 actor 为并发代码中的约束建模,例如所有代码都必须在同一个线程上执行。
全局 actor 隐式遵循 GlobalActor 协议。主 actor 是标准库提供的全局 actor,详见主 actor。大多数代码可以使用主 actor,而不必定义新的全局 actor。
inlinable
把这个特性应用于函数、方法、计算属性、下标、便利构造器或反初始化器声明,把该声明的实现作为模块公开接口的一部分暴露出去。编译器可以在调用点用该符号实现的一份副本替换对该可内联符号的调用。
可内联代码可以与其他任何模块中声明的 open 和 public 符号交互,也可以与同一模块中用 usableFromInline 特性标记的 internal 符号交互,但不能与 private 或 fileprivate 符号交互。
这个特性不能应用于嵌套在函数内部的声明,也不能应用于 fileprivate 或 private 声明。在可内联函数内部定义的函数和闭包隐式地可内联,即使它们不能用这个特性标记。
main
把这个特性应用于结构体、类或枚举声明,表示它包含程序流程的顶层入口点。该类型必须提供一个不接受任何实参、返回 Void 的 main 类型函数。例如:
| |
描述 main 特性要求的另一种方式是:你写这个特性的类型必须满足与遵循下列假设协议的类型的相同要求:
| |
你编译成可执行文件的 Swift 代码最多只能包含一个顶层入口点,详见顶层代码。
nonobjc
把这个特性应用于方法、属性、下标或构造器声明,抑制隐式的 objc 特性。nonobjc 特性告诉编译器让该声明在 Objective-C 代码中不可用,即使它可以被表示成 Objective-C。
把这个特性应用于扩展,等同于把它应用于该扩展中所有没有被显式标记 objc 特性的成员。
你可以用 nonobjc 特性解决标记了 objc 特性的类中桥接方法的循环问题,也可以用它让标记了 objc 特性的类可以重载方法和构造器。
标记了 nonobjc 特性的方法不能覆盖标记了 objc 特性的方法;不过,标记了 objc 特性的方法可以覆盖标记了 nonobjc 特性的方法。类似地,标记了 nonobjc 特性的方法不能满足标记了 objc 特性的方法的协议要求。
NSApplicationMain
已弃用: 这个特性已被弃用; 请改用 main 特性。 在 Swift 6 中, 使用这个特性会产生编译期错误。
把这个特性应用于类,表示它是应用委托。使用这个特性等同于调用 NSApplicationMain(_:_:) 函数。
如果你不使用这个特性,就提供一个 main.swift 文件,其中在顶层调用 NSApplicationMain(_:_:) 函数,如下所示:
| |
你编译成可执行文件的 Swift 代码最多只能包含一个顶层入口点,详见顶层代码。
NSCopying
把这个特性应用于类的存储变量属性。这个特性使该属性的设值器用属性值的一份副本(由 copyWithZone(_:) 方法返回)来合成,而不是该属性值本身。该属性的类型必须遵循 NSCopying 协议。
NSCopying 特性的行为与 Objective-C 的 copy 属性特性类似。
NSManaged
把这个特性应用于继承自 NSManagedObject 的类的实例方法或存储变量属性,表示 Core Data 会在运行时根据关联的实体描述动态提供它的实现。对于标记了 NSManaged 特性的属性,Core Data 还会在运行时提供存储。应用这个特性同时隐含 objc 特性。
objc
把这个特性应用于任何可以用 Objective-C 表示的声明——例如非嵌套类、协议、非泛型枚举(限定为整数原始值类型)、类的属性和方法(包括取值器和设值器)、协议及其可选成员、构造器和下标。objc 特性告诉编译器该声明可以在 Objective-C 代码中使用。
把这个特性应用于扩展,等同于把它应用于该扩展中所有没有被显式标记 nonobjc 特性的成员。
对于在 Objective-C 中定义的任何类的子类,编译器会隐式添加 objc 特性;不过该子类不能是泛型的,也不能继承任何泛型类。你可以给满足这些条件的子类显式添加 objc 特性,按后文所述指定它的 Objective-C 名字。标记了 objc 特性的协议不能继承没有标记该特性的协议。
在下列情况下也会隐式添加 objc 特性:
- 该声明是子类中的覆盖,且父类的声明带有
objc特性。 - 该声明满足某个带有
objc特性的协议的要求。 - 该声明带有
IBAction、IBSegueAction、IBOutlet、IBDesignable、IBInspectable、NSManaged或GKInspectable特性。
如果你把 objc 特性应用于枚举,每个枚举成员都会以"枚举名 + 成员名"拼接的形式暴露给 Objective-C 代码,其中成员名的首字母大写。例如 Swift Planet 枚举中名为 venus 的成员,会以名为 PlanetVenus 的成员暴露给 Objective-C 代码。
objc 特性可选地接受一个由标识符构成的特性实参,该标识符指定 objc 特性所作用实体暴露给 Objective-C 的名字。你可以用这个实参为类、枚举、枚举成员、协议、方法、取值器、设值器和构造器命名。如果你为类、协议或枚举指定 Objective-C 名字,请在名字上包含三个字母的前缀,详见 Programming with Objective-C 中的 Conventions。下面的例子把 ExampleClass 的 enabled 属性的取值器以 isEnabled 而非属性本身的名字暴露给 Objective-C 代码。
| |
更多内容参见 Importing Swift into Objective-C。
注意:
objc特性的实参 也可以改变该声明的运行时名字。 当你调用与 Objective-C 运行时交互的函数时 (例如NSClassFromString(_:)), 以及在应用的 Info.plist 文件中指定类名时,会用到这个运行时名字。 如果你通过传实参指定了名字, 那么该名字既用作 Objective-C 代码中的名字, 也用作运行时名字; 如果省略实参, Objective-C 代码中使用的名字与 Swift 代码中的名字一致, 而运行时名字则遵循 Swift 编译器通常的名字修饰约定。
objcMembers
把这个特性应用于类声明,把 objc 特性隐式应用于该类及其扩展、它的子类以及子类的所有扩展中所有与 Objective-C 兼容的成员。
大多数代码应改用 objc 特性,只暴露需要的声明。如果你需要暴露很多声明,可以把它们归入一个带 objc 特性的扩展中。objcMembers 特性是为大量使用 Objective-C 运行时内省设施的库提供的便利。在不需要时应用 objc 特性可能增大二进制体积,并对性能产生不利影响。
preconcurrency
把这个特性应用于声明,抑制严格的并发检查。你可以把这个特性应用于下列种类的声明:
- 导入
- 结构体、类和 actor
- 枚举和枚举成员
- 协议
- 变量和常量
- 下标
- 构造器
- 函数
在导入声明上,这个特性会降低使用所导入模块中类型的代码的并发检查严格程度。具体来说,所导入模块中没有被显式标记为不可发送的类型,可以用在要求可发送类型的上下文中。
在其他声明上,这个特性会降低使用所声明符号的代码的并发检查严格程度。当你在并发检查最宽松的作用域中使用该符号时,该符号所指定的并发相关约束(例如 Sendable 要求或全局 actor)不会被检查。
你可以按下列方式使用这个特性,帮助把代码迁移到严格并发检查:
- 启用严格检查。
- 为尚未启用严格检查的模块,给它们的导入加上
preconcurrency特性。 - 把某个模块迁移到严格检查之后,移除
preconcurrency特性。对于导入上的preconcurrency特性已不再起作用、应当移除的位置,编译器会给出警告。
对于其他声明,如果你仍有尚未迁移到严格检查的调用方,就在给声明添加并发相关约束时加上 preconcurrency 特性;等所有调用方都迁移完成后,再移除 preconcurrency 特性。
来自 Objective-C 的声明总是像被标记了 preconcurrency 特性那样导入。
propertyWrapper
把这个特性应用于类、结构体或枚举声明,把该类型用作属性包装器。把这个特性应用于某个类型时,你就创建了一个与类型同名的自定义特性。把那个新特性应用于类、结构体或枚举的某个属性,就通过包装器类型的实例来包装对该属性的访问;把这个特性应用于局部的存储变量声明,也会以同样方式包装对该变量的访问。计算变量、全局变量和常量不能使用属性包装器。
包装器必须定义一个 wrappedValue 实例属性。该属性的被包装值就是该属性的取值器和设值器所暴露的值。大多数情况下 wrappedValue 是一个计算值,但它也可以是存储值。包装器定义并管理其被包装值所需的任何底层存储。编译器会为包装器类型的实例合成存储,做法是在被包装属性名前加下划线(_)——例如 someProperty 的包装器存放为 _someProperty。合成出来的包装器存储的访问控制级别为 private。
带属性包装器的属性可以包含 willSet 和 didSet 块,但不能覆盖编译器合成的 get 或 set 块。
Swift 为属性包装器的初始化提供了两种语法糖:你可以在被包装值的定义中使用赋值语法,把赋值号右侧的表达式作为实参传给属性包装器构造器的 wrappedValue 参数;你也可以在把特性应用于属性时提供实参,这些实参会被传给属性包装器的构造器。例如在下面的代码中,SomeStruct 分别调用了 SomeWrapper 定义的各个构造器。
| |
被包装属性的投影值是属性包装器可以用来公开额外功能的第二个值。属性包装器类型的作者负责确定其投影值的含义,并定义该投影值所公开的接口。要从属性包装器投影一个值,就在包装器类型上定义一个 projectedValue 实例属性。编译器会为投影值合成标识符,做法是在被包装属性名前加美元符号($)——例如 someProperty 的投影值是 $someProperty。投影值与原来的被包装属性具有相同的访问控制级别。
| |
resultBuilder
把这个特性应用于类、结构体或枚举,把该类型用作结果构建器。结果构建器是一种逐步构建嵌套数据结构的类型。你用结果构建器实现领域特定语言(DSL),以自然、声明式的方式创建嵌套数据结构。关于如何使用 resultBuilder 特性的示例,参见结果构建器。
结果构建方法
结果构建器实现下面描述的静态方法。由于结果构建器的全部功能都通过静态方法公开,你永远不需要初始化该类型的实例。结果构建器必须实现 buildBlock(_:) 方法,或者同时实现 buildPartialBlock(first:) 和 buildPartialBlock(accumulated:next:) 两个方法。其余方法——它们在 DSL 中启用附加功能——是可选的。结果构建器类型的声明其实不必包含任何协议遵循性。
这些静态方法的描述中使用三个类型作为占位符:类型 Expression 是结果构建器输入类型的占位符,Component 是部分结果类型的占位符,FinalResult 是结果构建器所产生结果类型的占位符。你用结果构建器实际使用的类型替换这些类型。如果你的结果构建方法没有为 Expression 或 FinalResult 指定类型,它们默认与 Component 相同。
构建块的方法如下:
术语
static func buildBlock(_ components: Component...) -> Component:把部分结果数组合并成单个部分结果。术语
static func buildPartialBlock(first: Component) -> Component:从第一个组件构建一个部分结果组件。同时实现这个方法和buildPartialBlock(accumulated:next:),以支持一次构建一个组件的块。与buildBlock(_:)相比,这种做法减少了对处理不同实参数量的泛型重载的需要。术语
static func buildPartialBlock(accumulated: Component, next: Component) -> Component:把一个累积组件与一个新组件组合,构建一个部分结果组件。同时实现这个方法和buildPartialBlock(first:),以支持一次构建一个组件的块。与buildBlock(_:)相比,这种做法减少了对处理不同实参数量的泛型重载的需要。
结果构建器可以实现上面列出的全部三个构建块方法;此时由可用性决定调用哪个方法。默认情况下,Swift 调用 buildPartialBlock(first:) 和 buildPartialBlock(accumulated:next:) 方法。要让 Swift 改而调用 buildBlock(_:),就把外围声明的可用性标记为早于你写在 buildPartialBlock(first:) 和 buildPartialBlock(accumulated:next:) 上的可用性。
其他结果构建方法如下:
术语
static func buildOptional(_ component: Component?) -> Component:从可以为nil的部分结果构建一个部分结果。实现这个方法以支持不带else子句的if语句。术语
static func buildEither(first: Component) -> Component:构建一个取值随某个条件而变化的部分结果。同时实现这个方法和buildEither(second:),以支持switch语句和带else子句的if语句。术语
static func buildEither(second: Component) -> Component:构建一个取值随某个条件而变化的部分结果。同时实现这个方法和buildEither(first:),以支持switch语句和带else子句的if语句。术语
static func buildArray(_ components: [Component]) -> Component:从部分结果数组构建一个部分结果。实现这个方法以支持for循环。术语
static func buildExpression(_ expression: Expression) -> Component:从一个表达式构建一个部分结果。你可以实现这个方法做预处理——例如把表达式转换成内部类型——或者为使用处的类型推断提供附加信息。术语
static func buildFinalResult(_ component: Component) -> FinalResult:从部分结果构建最终结果。当结果构建器对部分结果和最终结果使用不同类型时,或者想在返回结果之前对它做其他后处理时,你可以实现这个方法。术语
static func buildLimitedAvailability(_ component: Component) -> Component:构建一个抹除类型信息的部分结果。你可以实现这个方法,防止类型信息传播到执行可用性检查的编译器控制语句之外。
例如,下面的代码定义了一个构建整数数组的简单结果构建器。这段代码把 Component 和 Expression 定义为类型别名,以便把下面的例子与上面的方法列表对照起来。
| |
结果变换
下列语法变换会被递归应用,把使用结果构建器语法的代码转换成调用结果构建器类型静态方法的代码:
如果结果构建器有
buildExpression(_:)方法,每个表达式都会变成对该方法的调用。这一变换总是最先进行。例如,下面两个声明是等价的:1 2@ArrayBuilder var builderNumber: [Int] { 10 } var manualNumber = ArrayBuilder.buildExpression(10)赋值语句的变换方式与表达式相同,但被理解为求值为
()。你可以为buildExpression(_:)定义一个接受()类型实参的重载,专门处理赋值。检查可用性条件的分支语句会变成对
buildLimitedAvailability(_:)方法的调用(如果实现了该方法)。如果你没有实现buildLimitedAvailability(_:),那么检查可用性的分支语句使用与其他分支语句相同的变换。这一变换发生在变换成对buildEither(first:)、buildEither(second:)或buildOptional(_:)的调用之前。你用
buildLimitedAvailability(_:)方法抹除随所走分支而变化的类型信息。例如,下面的buildEither(first:)和buildEither(second:)方法使用了一个泛型类型,它捕获了两个分支的类型信息。1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33protocol Drawable { func draw() -> String } struct Text: Drawable { var content: String init(_ content: String) { self.content = content } func draw() -> String { return content } } struct Line<D: Drawable>: Drawable { var elements: [D] func draw() -> String { return elements.map { $0.draw() }.joined(separator: "") } } struct DrawEither<First: Drawable, Second: Drawable>: Drawable { var content: Drawable func draw() -> String { return content.draw() } } @resultBuilder struct DrawingBuilder { static func buildBlock<D: Drawable>(_ components: D...) -> Line<D> { return Line(elements: components) } static func buildEither<First, Second>(first: First) -> DrawEither<First, Second> { return DrawEither(content: first) } static func buildEither<First, Second>(second: Second) -> DrawEither<First, Second> { return DrawEither(content: second) } }不过,这种做法会在带可用性检查的代码中引发问题:
1 2 3 4 5 6 7 8 9 10 11 12 13 14@available(macOS 99, *) struct FutureText: Drawable { var content: String init(_ content: String) { self.content = content } func draw() -> String { return content } } @DrawingBuilder var brokenDrawing: Drawable { if #available(macOS 99, *) { FutureText("Inside.future") // 问题 } else { Text("Inside.present") } } // brokenDrawing 的类型是 Line<DrawEither<Line<FutureText>, Line<Text>>>在上面的代码中,
FutureText出现在brokenDrawing类型的一部分中,因为它是DrawEither泛型类型中的一个类型。如果FutureText在运行时不可用,这可能导致程序崩溃——即使在该类型明确不会被使用的情况下也是如此。为解决这个问题,实现一个
buildLimitedAvailability(_:)方法,通过返回总是可用的类型来抹除类型信息。例如,下面的代码从可用性检查构建出一个AnyDrawable值。1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18struct AnyDrawable: Drawable { var content: Drawable func draw() -> String { return content.draw() } } extension DrawingBuilder { static func buildLimitedAvailability(_ content: some Drawable) -> AnyDrawable { return AnyDrawable(content: content) } } @DrawingBuilder var typeErasedDrawing: Drawable { if #available(macOS 99, *) { FutureText("Inside.future") } else { Text("Inside.present") } } // typeErasedDrawing 的类型是 Line<DrawEither<AnyDrawable, Line<Text>>>分支语句会变成一系列对
buildEither(first:)和buildEither(second:)方法的嵌套调用:语句的条件和分支被映射到一棵二叉树的叶节点上,该语句变成沿着从根节点到该叶节点的路径嵌套调用buildEither方法。例如,如果你写一个有三个分支的 switch 语句,编译器会使用一棵有三个叶节点的二叉树。同样,由于从根节点到第二个分支的路径是"第二个子节点"然后"第一个子节点",该分支会变成像
buildEither(first: buildEither(second: ... ))这样的嵌套调用。下面两个声明是等价的:1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24let someNumber = 19 @ArrayBuilder var builderConditional: [Int] { if someNumber < 12 { 31 } else if someNumber == 19 { 32 } else { 33 } } var manualConditional: [Int] if someNumber < 12 { let partialResult = ArrayBuilder.buildExpression(31) let outerPartialResult = ArrayBuilder.buildEither(first: partialResult) manualConditional = ArrayBuilder.buildEither(first: outerPartialResult) } else if someNumber == 19 { let partialResult = ArrayBuilder.buildExpression(32) let outerPartialResult = ArrayBuilder.buildEither(second: partialResult) manualConditional = ArrayBuilder.buildEither(first: outerPartialResult) } else { let partialResult = ArrayBuilder.buildExpression(33) manualConditional = ArrayBuilder.buildEither(second: partialResult) }可能不产生值的分支语句(例如不带
else子句的if语句)会变成对buildOptional(_:)的调用:如果if语句的条件得到满足,它的代码块会被变换并作为实参传入;否则以nil作为实参调用buildOptional(_:)。例如,下面两个声明是等价的:1 2 3 4 5 6 7 8 9@ArrayBuilder var builderOptional: [Int] { if (someNumber % 2) == 1 { 20 } } var partialResult: [Int]? = nil if (someNumber % 2) == 1 { partialResult = ArrayBuilder.buildExpression(20) } var manualOptional = ArrayBuilder.buildOptional(partialResult)如果结果构建器实现了
buildPartialBlock(first:)和buildPartialBlock(accumulated:next:)方法,代码块或do语句就会变成对这两个方法的调用:块中的第一条语句被变换为buildPartialBlock(first:)方法的实参,其余语句变成对buildPartialBlock(accumulated:next:)方法的嵌套调用。例如,下面两个声明是等价的:1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33struct DrawBoth<First: Drawable, Second: Drawable>: Drawable { var first: First var second: Second func draw() -> String { return first.draw() + second.draw() } } @resultBuilder struct DrawingPartialBlockBuilder { static func buildPartialBlock<D: Drawable>(first: D) -> D { return first } static func buildPartialBlock<Accumulated: Drawable, Next: Drawable>( accumulated: Accumulated, next: Next ) -> DrawBoth<Accumulated, Next> { return DrawBoth(first: accumulated, second: next) } } @DrawingPartialBlockBuilder var builderBlock: some Drawable { Text("First") Line(elements: [Text("Second"), Text("Third")]) Text("Last") } let partialResult1 = DrawingPartialBlockBuilder.buildPartialBlock(first: Text("first")) let partialResult2 = DrawingPartialBlockBuilder.buildPartialBlock( accumulated: partialResult1, next: Line(elements: [Text("Second"), Text("Third")]) ) let manualResult = DrawingPartialBlockBuilder.buildPartialBlock( accumulated: partialResult2, next: Text("Last") )否则,代码块或
do语句就会变成对buildBlock(_:)方法的调用:块中的每条语句依次被变换,成为buildBlock(_:)方法的实参。例如,下面两个声明是等价的:1 2 3 4 5 6 7 8 9 10 11@ArrayBuilder var builderBlock: [Int] { 100 200 300 } var manualBlock = ArrayBuilder.buildBlock( ArrayBuilder.buildExpression(100), ArrayBuilder.buildExpression(200), ArrayBuilder.buildExpression(300) )for循环会变成一个临时变量、一个for循环和对buildArray(_:)方法的调用:新的for循环遍历该序列,把每个部分结果追加到那个数组,临时数组作为buildArray(_:)调用的实参传入。例如,下面两个声明是等价的:1 2 3 4 5 6 7 8 9 10 11 12@ArrayBuilder var builderArray: [Int] { for i in 5...7 { 100 + i } } var temporary: [[Int]] = [] for i in 5...7 { let partialResult = ArrayBuilder.buildExpression(100 + i) temporary.append(partialResult) } let manualArray = ArrayBuilder.buildArray(temporary)如果结果构建器有
buildFinalResult(_:)方法,最终结果会变成对该方法的调用。这一变换总是最后进行。
尽管这些变换行为是用临时变量来描述的,但使用结果构建器实际上不会创建任何在代码其余部分可见的新声明。
在结果构建器所变换的代码中,你不能使用 break、continue、defer、guard 或 return 语句,也不能使用 while 语句或 do-catch 语句。
变换过程不会改变代码中的声明,因此你可以用临时常量和变量逐步构建表达式。它也不会改变 throw 语句、编译期诊断语句,或者包含 return 语句的闭包。
只要可能,变换就会被合并。例如表达式 4 + 5 * 6 会变成 buildExpression(4 + 5 * 6),而不是多次调用该函数;同样,嵌套的分支语句会变成一棵由对 buildEither 方法的调用构成的二叉树。
自定义结果构建器特性
创建结果构建器类型会创建与之同名的自定义特性。你可以在下列位置应用该特性:
- 在函数声明上,结果构建器构建函数体。
- 在带取值器的变量或下标声明上,结果构建器构建取值器体。
- 在函数声明的参数上,结果构建器构建作为相应实参传入的闭包体。
应用结果构建器特性不影响 ABI 兼容性。把结果构建器特性应用于参数会使该特性成为函数接口的一部分,这可能影响源代码兼容性。
requires_stored_property_inits
把这个特性应用于类声明,要求该类中所有存储属性都在定义中提供默认值。对于任何继承自 NSManagedObject 的类,会推断出这个特性。
testable
把这个特性应用于 import 声明,以改变所导入模块的访问控制的方式来导入该模块,从而简化对该模块代码的测试。所导入模块中用 internal 访问级别修饰符标记的实体,会像用 public 访问级别修饰符声明那样被导入;用 internal 或 public 访问级别修饰符标记的类和类成员,会像用 open 访问级别修饰符声明那样被导入。所导入的模块必须在启用测试的情况下编译。
UIApplicationMain
已弃用: 这个特性已被弃用; 请改用 main 特性。 在 Swift 6 中, 使用这个特性会产生编译期错误。
把这个特性应用于类,表示它是应用委托。使用这个特性等同于调用 UIApplicationMain 函数,并把该类的名字作为委托类的名字传入。
如果你不使用这个特性,就提供一个 main.swift 文件,其中在顶层调用 UIApplicationMain(_:_:_:_:) 函数。例如,如果你的应用用 UIApplication 的自定义子类作为其主体类,就调用 UIApplicationMain(_:_:_:_:) 函数,而不使用这个特性。
你编译成可执行文件的 Swift 代码最多只能包含一个顶层入口点,详见顶层代码。
unchecked
把这个特性作为类型声明所采纳协议列表的一部分应用于协议类型,关闭对该协议要求的强制检查。
唯一受支持的协议是 Sendable。
usableFromInline
把这个特性应用于函数、方法、计算属性、下标、构造器或反初始化器声明,允许该符号在与声明同一模块中定义的可内联代码里使用。该声明必须带 internal 访问级别修饰符。标记了 usableFromInline 的结构体或类,其属性只能使用 public 或 usableFromInline 的类型;标记了 usableFromInline 的枚举,其成员的原始值和关联值只能使用 public 或 usableFromInline 的类型。
与 public 访问级别修饰符一样,这个特性把声明作为模块公开接口的一部分暴露出去;但与 public 不同的是,编译器不允许模块之外的代码按名字引用标记了 usableFromInline 的声明,即使该声明的符号已经被导出。不过,模块之外的代码仍有可能通过运行时行为与该声明的符号交互。
标记了 inlinable 特性的声明隐式地可以被可内联代码使用。虽然 inlinable 和 usableFromInline 都可以应用于 internal 声明,但同时应用两个特性会报错。
warn_unqualified_access
把这个特性应用于顶层函数、实例方法或类方法/静态方法,当该函数或方法被使用时没有前置限定(例如模块名、类型名或实例变量/常量),就会触发警告。用这个特性帮助减少同一作用域中可访问的同名函数之间的歧义。
例如,Swift 标准库既包含顶层 min(_:_:) 函数,也包含用于元素可比较序列的 min() 方法。序列方法声明时带有 warn_unqualified_access 特性,以帮助减少在 Sequence 扩展内部想使用其中之一时产生的困惑。
Interface Builder 使用的声明特性
Interface Builder 特性是 Interface Builder 用来与 Xcode 同步的声明特性。Swift 提供下列 Interface Builder 特性:IBAction、IBSegueAction、IBOutlet、IBDesignable 和 IBInspectable。这些特性在概念上与它们在 Objective-C 中的对应物相同。
你把 IBOutlet 和 IBInspectable 特性应用于类的属性声明;把 IBAction 和 IBSegueAction 特性应用于类的方法声明;把 IBDesignable 特性应用于类声明。
应用 IBAction、IBSegueAction、IBOutlet、IBDesignable 或 IBInspectable 特性同时隐含 objc 特性。
类型特性
类型特性只能应用于类型。
autoclosure
应用这个特性,通过把表达式自动包装进一个无参数闭包来延迟它的求值。你把它应用于函数或方法声明中某个参数的类型,该参数的类型必须是不接受任何实参、并返回该表达式类型的值的函数类型。关于如何使用 autoclosure 特性的示例,参见自动闭包和函数类型。
convention
把这个特性应用于函数的类型,表明它的调用约定。
convention 特性总是与下列实参之一一起出现:
swift实参表示 Swift 函数引用。这是 Swift 中函数值的标准调用约定。block实参表示与 Objective-C 兼容的块引用。函数值表示为对块对象的引用,块对象是一个与id兼容的 Objective-C 对象,它把其调用函数嵌在对象内部。该调用函数使用 C 调用约定。c实参表示 C 函数引用。函数值不携带上下文,并使用 C 调用约定。
除少数例外,当需要某种调用约定的函数时,可以使用任何其他调用约定的函数。非泛型全局函数、不捕获任何局部变量的局部函数,或者不捕获任何局部变量的闭包,都可以转换成 C 调用约定;其他 Swift 函数不能转换成 C 调用约定。使用 Objective-C 块调用约定的函数不能转换成 C 调用约定。
escaping
把这个特性应用于函数或方法声明中某个参数的类型,表示该参数的值可以被存放起来稍后执行。这意味着该值的存活期允许超过调用的存活期。带 escaping 类型特性的函数类型参数,访问属性或方法时必须显式使用 self.。关于如何使用 escaping 特性的示例,参见逃逸闭包。
Sendable
把这个特性应用于函数的类型,表示该函数或闭包是可发送的。把这个特性应用于函数类型,与非函数类型遵循 Sendable 协议含义相同。
如果函数或闭包被用在期望可发送值的上下文中,并且该函数或闭包满足可发送的要求,那么就会为它推断出这个特性。
可发送的函数类型是相应不可发送函数类型的子类型。
switch case 特性
switch case 特性只能应用于 switch 的分支。
unknown
把这个特性应用于 switch 分支,表示不期望它被编译该代码时已知的任何枚举成员匹配。关于如何使用 unknown 特性的示例,参见对未来的枚举成员做 switch。
Grammar of an attribute:
attribute →
@attribute-name attribute-argument-clause?
attribute-name → identifier
attribute-argument-clause →(balanced-tokens?)
attributes → attribute attributes?balanced-tokens → balanced-token balanced-tokens?
balanced-token →(balanced-tokens?)
balanced-token →[balanced-tokens?]
balanced-token →{balanced-tokens?}
balanced-token → Any identifier, keyword, literal, or operator
balanced-token → Any punctuation except(,),[,],{, or}