3.6 声明

原文链接: https://docs.swift.org/latest/documentation/the-swift-programming-language/declarations/

3.6 声明

引入类型、运算符、变量以及其他名称和构造。

声明把新的名称或构造引入你的程序。例如,你用声明来引入函数和方法、引入变量和常量,并定义枚举、结构体、类和协议类型。你还可以用声明扩展某个已有具名类型的行为,并把在别处声明的符号导入自己的程序。

在 Swift 中,大多数声明同时也是定义——它们在声明的同时就被实现或初始化。话虽如此,因为协议不实现自己的成员,所以大多数协议成员仅仅是声明。为方便起见,也因为这一区别在 Swift 中并不重要,声明一词同时涵盖声明和定义。

Grammar of a declaration:

declaration → import-declaration
declaration → constant-declaration
declaration → variable-declaration
declaration → typealias-declaration
declaration → function-declaration
declaration → enum-declaration
declaration → struct-declaration
declaration → class-declaration
declaration → actor-declaration
declaration → protocol-declaration
declaration → initializer-declaration
declaration → deinitializer-declaration
declaration → extension-declaration
declaration → subscript-declaration
declaration → macro-declaration
declaration → operator-declaration
declaration → precedence-group-declaration

顶层代码

Swift 源文件中的顶层代码由零个或多个语句、声明和表达式组成。默认情况下,在源文件顶层声明的变量、常量以及其他具名声明,对该模块中每个源文件里的代码都可访问。你可以给声明加上访问级别修饰符来覆盖这一默认行为,如访问控制级别所述。

顶层代码有两种:顶层声明和可执行顶层代码。顶层声明只由声明组成,在所有 Swift 源文件中都允许使用。可执行顶层代码包含语句和表达式,而不仅仅是声明,只能用作程序的顶层入口点。

无论代码如何组织到文件和模块中,你编译生成可执行文件所用的 Swift 代码最多只能包含下列标记顶层入口点的方式之一:一个包含可执行顶层代码的文件、一个 main.swift 文件、main 特性、NSApplicationMain 特性,或者 UIApplicationMain 特性。

Grammar of a top-level declaration:

top-level-declaration → statements?

代码块

代码块被各种声明和控制结构用来把语句组织在一起。它的形式如下:

1
2
3
{
   <#statements#>
}

代码块中的语句包括声明、表达式以及其他种类的语句,它们按照在源代码中出现的顺序依次执行。

Grammar of a code block:

code-block → { statements? }

import 声明

import 声明让你能够访问在当前文件之外声明的符号。基本形式会导入整个模块,它由 import 关键字后跟一个模块名组成:

1
import <#module#>

提供更多细节可以限定导入哪些符号——你可以指定某个特定的子模块,或者某个模块或子模块中的某个特定声明。使用这种详细形式时,只有被导入的符号本身(而不是声明它的模块)会在当前作用域中可用。

1
2
import <#import kind#> <#module#>.<#symbol name#>
import <#module#>.<#submodule#>

Grammar of an import declaration:

import-declaration → attributes? import import-kind? import-path

import-kind → typealias | struct | class | enum | protocol | let | var | func
import-path → identifier | identifier . import-path

常量声明

常量声明把一个具名常量值引入你的程序。常量声明用 let 关键字声明,形式如下:

1
let <#constant name#>: <#type#> = <#expression#>

常量声明在常量名与初始化表达式的值之间定义了一种不可变的绑定;常量的值一旦设定就不能再更改。话虽如此,如果用类对象初始化常量,对象本身可以发生变化,但常量名与它所引用的对象之间的绑定不能改变。

常量在全局作用域声明时,必须用值初始化。常量声明出现在函数或方法的上下文中时,可以稍后初始化,只要保证在第一次读取它的值之前已设置好值即可。如果编译器能证明该常量的值从未被读取,那么该常量根本不需要设置值。这种分析称为确定性初始化——编译器会证明某个值在被读取之前一定已被设置。

注意:确定性初始化无法构造需要领域知识的证明, 而且它跨条件分支跟踪状态的能力也是有限的。 如果你能确定某个常量总是被设置了值, 但编译器无法证明这一点, 可以试着简化设置该值的代码路径, 或者改用变量声明。

常量声明出现在类或结构体声明的上下文中时,它被视为常量属性。常量声明不是计算属性,因此没有 getter 或 setter。

如果常量声明的常量名是一个元组模式,那么元组中每一项的名称都会与初始化表达式中对应的值绑定。

1
let (firstNumber, secondNumber) = (10, 42)

在这个例子中,firstNumber 是值 10 的具名常量,secondNumber 是值 42 的具名常量。这两个常量现在可以独立使用:

1
2
3
4
print("The first number is \(firstNumber).")
// 打印 "The first number is 10."
print("The second number is \(secondNumber).")
// 打印 "The second number is 42."

当常量名的类型可以被推断出来时,常量声明中的类型注解(: type)是可选的,如类型推断所述。

要声明常量类型属性,给该声明加上 static 声明修饰符。类的常量类型属性始终是隐式的 final;你不能用 class 或 final 声明修饰符来允许或禁止子类覆盖它。类型属性在类型属性中讨论。

关于常量的更多信息,以及何时使用常量的建议,参见常量与变量和存储属性。

Grammar of a constant declaration:

constant-declaration → attributes? declaration-modifiers? let pattern-initializer-list

pattern-initializer-list → pattern-initializer | pattern-initializer , pattern-initializer-list
pattern-initializer → pattern initializer?
initializer → = expression

变量声明

变量声明把一个具名变量值引入你的程序,用 var 关键字声明。

变量声明有多种形式,用于声明不同种类的具名可变值,包括存储变量和计算变量、存储属性与计算属性、存储变量观察器与属性观察器,以及静态变量属性。该用哪种形式,取决于变量声明所在的作用域,以及你打算声明的变量种类。

注意:你也可以在协议声明的上下文中声明属性, 如协议属性声明所述。

你可以通过在子类的属性声明上添加 override 声明修饰符来覆盖父类中的属性,如重写所述。

存储变量与存储变量属性

下面的形式声明一个存储变量或存储变量属性:

1
var <#variable name#>: <#type#> = <#expression#>

你可以在全局作用域、函数的局部作用域,或者类或结构体声明的上下文中定义这种形式的变量声明。这种形式的变量声明在全局作用域或函数的局部作用域中声明时,称为存储变量。在类或结构体声明的上下文中声明时,称为存储变量属性。

初始化表达式不能出现在协议声明中,但在其他所有上下文中,初始化表达式都是可选的。话虽如此,如果没有初始化表达式,变量声明就必须包含显式的类型注解(: type)。

与常量声明一样,如果变量声明省略了初始化表达式,那么该变量必须在第一次被读取之前设置好值。同样与常量声明一样,如果变量名是一个元组模式,元组中每一项的名称会与初始化表达式中对应的值绑定。

顾名思义,存储变量或存储变量属性的值存储在内存中。

计算变量与计算属性

下面的形式声明一个计算变量或计算属性:

1
2
3
4
5
6
7
8
var <#variable name#>: <#type#> {
   get {
      <#statements#>
   }
   set(<#setter name#>) {
      <#statements#>
   }
}

你可以在全局作用域、函数的局部作用域,或者类、结构体、枚举或扩展声明的上下文中定义这种形式的变量声明。这种形式的变量声明在全局作用域或函数的局部作用域中声明时,称为计算变量。在类、结构体或扩展声明的上下文中声明时,称为计算属性。

getter 用于读取值,setter 用于写入值。setter 子句是可选的;只需要 getter 时,你可以把两个子句都省略,直接返回所需的值,如只读计算属性所述。但如果你提供了 setter 子句,就必须同时提供 getter 子句。

setter 名及其外面的圆括号是可选的。如果提供了 setter 名,它会被用作 setter 参数的名称。如果没有提供 setter 名,setter 的默认参数名是 newValue,如简写 setter 声明所述。

与存储的具名值和存储变量属性不同,计算具名值或计算属性的值并不存储在内存中。

关于计算属性的更多信息和示例,参见计算属性。

存储变量观察器与属性观察器

你还可以用 willSet 和 didSet 观察器来声明存储变量或属性。带观察器的存储变量或属性形式如下:

1
2
3
4
5
6
7
8
var <#variable name#>: <#type#> = <#expression#> {
   willSet(<#setter name#>) {
      <#statements#>
   }
   didSet(<#setter name#>) {
      <#statements#>
   }
}

你可以在全局作用域、函数的局部作用域,或者类或结构体声明的上下文中定义这种形式的变量声明。这种形式的变量声明在全局作用域或函数的局部作用域中声明时,观察器称为存储变量观察器。在类或结构体声明的上下文中声明时,观察器称为属性观察器。

你可以给任何存储属性添加属性观察器。你也可以在子类中重写属性,从而给任何继承来的属性(无论是存储属性还是计算属性)添加属性观察器,如重写属性观察器所述。

初始化表达式在类或结构体声明的上下文中是可选的,在其他地方则是必需的。当类型可以从初始化表达式推断出来时,类型注解是可选的。这个表达式会在你第一次读取该属性的值时求值。如果你没有读取就直接覆盖属性的初始值,那么这个表达式会在你第一次写入该属性之前求值。

willSet 和 didSet 观察器提供了一种在变量或属性被赋值时进行观察(并做出适当响应)的方式。变量或属性首次初始化时不会调用观察器。只有在初始化上下文之外赋值时才会调用它们。

willSet 观察器在变量或属性即将被赋值之前调用。新值作为常量传给 willSet 观察器,因此在 willSet 子句的实现中无法修改它。didSet 观察器在新值被设置之后立即调用。与 willSet 观察器不同,变量或属性的旧值会传给 didSet 观察器,以防你仍需要访问它。话虽如此,如果你在变量或属性自己的 didSet 观察器子句内给它赋一个新值,你所赋的这个新值会替换掉刚刚设置并传给 willSet 观察器的那个值。

willSet 和 didSet 子句中的 setter 名及其外面的圆括号是可选的。如果你提供了 setter 名,它们会被用作 willSet 和 didSet 观察器的参数名。如果你没有提供 setter 名,willSet 观察器的默认参数名是 newValue,didSet 观察器的默认参数名是 oldValue。

当你提供了 willSet 子句时,didSet 子句是可选的。同样,当你提供了 didSet 子句时,willSet 子句也是可选的。

如果 didSet 观察器的函数体引用了旧值,那么在调用观察器之前会先调用 getter,以便让旧值可用。否则,新值会被直接存储,而不会调用父类的 getter。下面的例子展示了一个由父类定义、并由其子类重写以添加观察器的计算属性。

 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
33
34
class Superclass {
    private var xValue = 12
    var x: Int {
        get { print("Getter was called"); return xValue }
        set { print("Setter was called"); xValue = newValue }
    }
}

// 这个子类的观察器不引用 oldValue,因此父类的 getter
// 只被调用一次,用于打印该值。
class New: Superclass {
    override var x: Int {
        didSet { print("New value \(x)") }
    }
}
let new = New()
new.x = 100
// 打印 "Setter was called"。
// 打印 "Getter was called"。
// 打印 "New value 100"。

// 这个子类的观察器引用了 oldValue,因此父类的 getter
// 会在 setter 之前被调用一次,之后为了打印该值再调用一次。
class NewAndOld: Superclass {
    override var x: Int {
        didSet { print("Old value \(oldValue) - new value \(x)") }
    }
}
let newAndOld = NewAndOld()
newAndOld.x = 200
// 打印 "Getter was called"。
// 打印 "Setter was called"。
// 打印 "Getter was called"。
// 打印 "Old value 12 - new value 200"。

关于属性观察器的更多信息和用法示例,参见属性观察器。

类型变量属性

要声明类型变量属性,给声明加上 static 声明修饰符。类可以把类型计算属性改用 class 声明修饰符标记,以允许子类覆盖父类的实现。类型属性在类型属性中讨论。

Grammar of a variable declaration:

variable-declaration → variable-declaration-head pattern-initializer-list
variable-declaration → variable-declaration-head variable-name type-annotation code-block
variable-declaration → variable-declaration-head variable-name type-annotation getter-setter-block
variable-declaration → variable-declaration-head variable-name type-annotation getter-setter-keyword-block
variable-declaration → variable-declaration-head variable-name initializer willSet-didSet-block
variable-declaration → variable-declaration-head variable-name type-annotation initializer? willSet-didSet-block

variable-declaration-head → attributes? declaration-modifiers? var
variable-name → identifier

getter-setter-block → code-block
getter-setter-block → { getter-clause setter-clause? }
getter-setter-block → { setter-clause getter-clause }
getter-clause → attributes? mutation-modifier? get code-block
setter-clause → attributes? mutation-modifier? set setter-name? code-block
setter-name → ( identifier )

getter-setter-keyword-block → { getter-keyword-clause setter-keyword-clause? }
getter-setter-keyword-block → { setter-keyword-clause getter-keyword-clause }
getter-keyword-clause → attributes? mutation-modifier? get
setter-keyword-clause → attributes? mutation-modifier? set

willSet-didSet-block → { willSet-clause didSet-clause? }
willSet-didSet-block → { didSet-clause willSet-clause? }
willSet-clause → attributes? willSet setter-name? code-block
didSet-clause → attributes? didSet setter-name? code-block

类型别名声明

类型别名声明把一个已有类型的具名别名引入你的程序。类型别名声明用 typealias 关键字声明,形式如下:

1
typealias <#name#> = <#existing type#>

类型别名声明之后,别名 name 就可以在程序中任何地方代替 existing type 使用。existing type 可以是具名类型,也可以是复合类型。类型别名不创建新类型;它们只是让一个名称可以指代某个已有类型。

类型别名声明可以使用泛型形参,为某个已有泛型类型起名字。类型别名可以为已有类型的部分或全部泛型形参提供具体类型。例如:

1
2
3
4
5
typealias StringDictionary<Value> = Dictionary<String, Value>

// 下面这两个字典的类型相同。
var dictionary1: StringDictionary<Int> = [:]
var dictionary2: Dictionary<String, Int> = [:]

当类型别名带泛型形参声明时,这些形参上的约束必须与已有类型泛型形参上的约束完全一致。例如:

1
typealias DictionaryOfInts<Key: Hashable> = Dictionary<Key, Int>

因为类型别名和已有类型可以互换使用,所以类型别名不能引入额外的泛型约束。

类型别名可以通过在声明中省略所有泛型形参,来转发已有类型的泛型形参。例如,这里声明的 Diccionario 类型别名与 Dictionary 具有相同的泛型形参和约束。

1
typealias Diccionario = Dictionary

在协议声明内部,类型别名可以给频繁使用的类型起一个更短、更方便的名称。例如:

1
2
3
4
5
6
7
8
protocol Sequence {
    associatedtype Iterator: IteratorProtocol
    typealias Element = Iterator.Element
}

func sum<T: Sequence>(_ sequence: T) -> Int where T.Element == Int {
    // ...
}

如果没有这个类型别名,sum 函数就只能把关联类型写成 T.Iterator.Element,而不能写成 T.Element。

另见协议关联类型声明。

Grammar of a type alias declaration:

typealias-declaration → attributes? access-level-modifier? typealias typealias-name generic-parameter-clause? typealias-assignment
typealias-name → identifier
typealias-assignment → = type

函数声明

函数声明把一个函数或方法引入你的程序。在类、结构体、枚举或协议的上下文中声明的函数称为方法。函数声明用 func 关键字声明,形式如下:

1
2
3
func <#function name#>(<#parameters#>) -> <#return type#> {
   <#statements#>
}

如果函数的返回类型是 Void,可以像下面这样省略返回类型:

1
2
3
func <#function name#>(<#parameters#>) {
   <#statements#>
}

每个形参的类型都必须写出——它无法被推断。如果你在形参类型前写 inout,该形参就可以在函数作用域内被修改。输入输出形参将在下面的输入输出形参中详细讨论。

如果函数声明的语句只包含一个表达式,那么该函数被理解为返回这个表达式的值。只有当表达式的类型和函数的返回类型都不是 Void,也不是像 Never 这样没有任何情况的枚举时,才会考虑这种隐式返回语法。

函数可以用元组类型作为返回类型,从而返回多个值。

函数定义可以出现在另一个函数声明内部。这种函数称为嵌套函数。

如果嵌套函数捕获的值保证永不逃逸——例如输入输出形参——或者是作为非逃逸函数实参传入的,那么该嵌套函数就是非逃逸的。否则,该嵌套函数是逃逸函数。

关于嵌套函数的讨论,参见嵌套函数。

形参名

函数形参是一个以逗号分隔的列表,每个形参有若干种形式之一。函数调用中实参的顺序必须与函数声明中形参的顺序一致。形参列表中最简单的条目形式如下:

1
<#parameter name#>: <#parameter type#>

形参既有名称(在函数体内使用),也有实参标签(调用函数或方法时使用)。默认情况下,形参名也被用作实参标签。例如:

1
2
func f(x: Int, y: Int) -> Int { return x + y }
f(x: 1, y: 2) // x 和 y 都带标签

你可以用下面两种形式之一覆盖实参标签的默认行为:

1
2
<#argument label#> <#parameter name#>: <#parameter type#>
_ <#parameter name#>: <#parameter type#>

写在形参名前面的名称给形参指定一个显式实参标签,它可以与形参名不同。对应的实参在函数或方法调用中必须使用给定的实参标签。

形参名前面的下划线(_)会取消实参标签。对应的实参在函数或方法调用中必须不带标签。

1
2
func repeatGreeting(_ greeting: String, count n: Int) { /* Greet n times */ }
repeatGreeting("Hello, world!", count: 2) //  count 带标签,greeting 不带

形参修饰符

形参修饰符改变实参传给函数的方式。

1
<#argument label#> <#parameter name#>: <#parameter modifier#> <#parameter type#>

要使用形参修饰符,在实参类型前写 inout、borrowing 或 consuming。

1
func someFunction(a: inout A, b: consuming B, c: C) { ... }

输入输出形参

默认情况下,Swift 中函数实参按值传递:在函数内所做的任何更改对调用方都不可见。要改为输入输出形参,你可以应用 inout 形参修饰符。

1
2
3
func someFunction(a: inout Int) {
    a += 1
}

调用包含输入输出形参的函数时,输入输出实参必须加上与号(&)前缀,以表明该函数调用可以改变实参的值。

1
2
3
var x = 7
someFunction(a: &x)
print(x)  // 打印 "8"

输入输出形参的传递方式如下:

  1. 调用函数时,实参的值被复制一份。
  2. 在函数体内,修改的是这份副本。
  3. 函数返回时,副本的值被赋给原来的实参。

这种行为称为写入复制、复制写出(copy-in copy-out)或传值结果(call by value result)。例如,当把计算属性或带观察器的属性作为输入输出实参传入时,函数调用过程中会调用它的 getter,函数返回过程中会调用它的 setter。

作为一种优化,当实参是存储在内存中某个物理地址上的值时,函数体内外会使用同一处内存位置。这种优化后的行为称为按引用传递(call by reference);它满足写入复制、复制写出模型的全部要求,同时省去了复制的开销。编写代码时应依据写入复制、复制写出的模型,而不要依赖按引用传递这一优化,这样无论有没有该优化,代码行为都正确。

在函数内部,不要访问作为输入输出实参传入的值,即使原值在当前作用域中可用。访问原值属于对该值的并发访问,会违反内存独占性。

1
2
3
4
5
6
7
var someValue: Int
func someFunction(a: inout Int) {
    a += someValue
}

// 错误:这会导致运行时的独占性冲突
someFunction(a: &someValue)

出于同样的原因,你不能把同一个值传给多个输入输出形参。

1
2
3
4
5
6
7
8
var someValue: Int
func someFunction(a: inout Int, b: inout Int) {
    a += b
    b += 1
}

// 错误:不能把同一个值传给多个输入输出形参
someFunction(a: &someValue, b: &someValue)

关于内存安全和内存独占性的更多信息,参见内存安全。

捕获输入输出形参的闭包或嵌套函数必须是非逃逸的。如果你需要捕获输入输出形参而不修改它,请使用捕获列表以不可变方式显式捕获该形参。

1
2
3
func someFunction(a: inout Int) -> () -> Int {
    return { [a] in return a + 1 }
}

如果你需要捕获并修改输入输出形参,请使用一个显式的局部副本,例如在多线程代码中,先确保所有修改都已完成,再让函数返回。

1
2
3
4
5
6
7
8
9
func multithreadedFunction(queue: DispatchQueue, x: inout Int) {
    // 创建局部副本,并手动把它复制回去。
    var localX = x
    defer { x = localX }

    // 异步操作 localX,然后在返回前等待。
    queue.async { someMutatingOperation(&localX) }
    queue.sync {}
}

关于输入输出形参的更多讨论和示例,参见输入输出形参。

borrowing 与 consuming 形参

默认情况下,Swift 使用一组规则在函数调用之间自动管理对象生命周期,并在需要时复制值。这些默认规则旨在尽可能降低大多数情况下的开销——如果你想要更精细的控制,可以应用 borrowing 或 consuming 形参修饰符。这种情况下,可以使用 copy 显式标记复制操作。此外,非可复制类型的值必须以 borrowing 或 consuming 的方式传递。

无论是否使用默认规则,Swift 都保证在所有情况下正确管理对象生命周期和所有权。这些形参修饰符只影响特定使用模式的相对效率,不影响正确性。

borrowing 修饰符表示函数不保留形参的值。这种情况下,调用方仍然拥有该对象,并负责该对象的生命周期。当函数只是短暂地使用该对象时,使用 borrowing 可以最小化开销。

1
2
3
4
// `isLessThan` 不保留任何一个参数
func isLessThan(lhs: borrowing A, rhs: borrowing A) -> Bool {
    ...
}

如果函数需要保留形参的值——例如把它存入全局变量——你可以用 copy 显式复制该值。

1
2
3
4
5
6
7
8
9
// 同上,但这个 `isLessThan` 还想记录最小的值
func isLessThan(lhs: borrowing A, rhs: borrowing A) -> Bool {
    if lhs < storedValue {
        storedValue = copy lhs
    } else if rhs < storedValue {
        storedValue = copy rhs
    }
    return lhs < rhs
}

相反,consuming 形参修饰符表示函数取得该值的所有权,并负责在函数返回之前存储或销毁它。

1
2
3
4
// `store` 保留了它的参数,所以标记为 `consuming`
func store(a: consuming A) {
    someGlobalVariable = a
}

当调用方在函数调用之后不再需要使用该对象时,使用 consuming 可以最小化开销。

1
2
// 通常,这是你对该值做的最后一件事
store(a: value)

如果函数调用之后你还要继续使用某个可复制对象,编译器会自动在函数调用之前复制该对象。

1
2
3
// 编译器在这里插入一次隐式复制
store(a: someValue)  // 这个函数会消耗 someValue
print(someValue)  // 这里使用的是 someValue 的副本

与 inout 不同,调用函数时 borrowing 和 consuming 形参都不需要任何特殊写法:

1
2
3
func someFunction(a: borrowing A, b: consuming B) { ... }

someFunction(a: someA, b: someB)

显式使用 borrowing 或 consuming 表明你打算更严格地控制运行时所有权管理的开销。因为复制可能引发意料之外的运行时所有权操作,所以带有这两种修饰符之一的形参不能被复制,除非使用显式的 copy 关键字:

 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
func borrowingFunction1(a: borrowing A) {
    // 错误:不能隐式复制 a
    // 这个赋值需要一次复制,因为
    // `a` 只是从调用方借来的。
    someGlobalVariable = a
}

func borrowingFunction2(a: borrowing A) {
    // 正确:显式复制是可行的
    someGlobalVariable = copy a
}

func consumingFunction1(a: consuming A) {
    // 错误:不能隐式复制 a
    // 这个赋值需要一次复制,原因
    // 是后面的 `print`
    someGlobalVariable = a
    print(a)
}

func consumingFunction2(a: consuming A) {
    // 正确:显式复制无论如何都可行
    someGlobalVariable = copy a
    print(a)
}

func consumingFunction3(a: consuming A) {
    // 正确:这里不需要复制,因为这是最后一次使用
    someGlobalVariable = a
}

特殊种类的形参

形参可以被忽略、接受可变数量的值,也可以用下面的形式提供默认值:

1
2
3
_ : <#parameter type#>
<#parameter name#>: <#parameter type#>...
<#parameter name#>: <#parameter type#> = <#default argument value#>

下划线(_)形参被显式忽略,无法在函数体内部访问。

基础类型名后紧跟三个点(...)的形参被理解为可变参数。紧跟在可变参数之后的形参必须带实参标签。一个函数可以有多个可变参数。可变参数被当作一个数组,其中包含基础类型名的元素。例如,可变参数 Int... 被当作 [Int]。使用可变参数的例子,参见可变参数。

类型后面跟等号(=)和表达式的形参被理解为具有给定表达式的默认值。这个给定的表达式在调用函数时求值。如果在调用函数时省略了该形参,就改用默认值。

1
2
3
4
func f(x: Int = 42) -> Int { return x }
f()       // 有效,使用默认值
f(x: 7)   // 有效,使用提供的值
f(7)      // 无效,缺少实参标签

特殊种类的方法

枚举或结构体上修改 self 的方法必须用 mutating 声明修饰符标记。

覆盖父类方法的方法必须用 override 声明修饰符标记。覆盖方法时不加 override 修饰符,或者对并不覆盖父类方法的方法使用 override 修饰符,都是编译期错误。

与类型(而非类型的实例)关联的方法,对于枚举和结构体必须用 static 声明修饰符标记;对于类则必须用 static 或 class 声明修饰符标记。用 class 声明修饰符标记的类类型方法可以被子类实现覆盖;用 class final 或 static 标记的类类型方法不能被覆盖。

具有特殊名称的方法

有若干具有特殊名称的方法可以启用函数调用语法的语法糖。如果某个类型定义了这些方法之一,该类型的实例就可以用于函数调用语法。这种函数调用被理解为对该实例上某个特殊命名方法的调用。

类、结构体或枚举类型可以通过定义 dynamicallyCall(withArguments:) 方法或 dynamicallyCall(withKeywordArguments:) 方法来支持函数调用语法,如 dynamicCallable所述;也可以通过定义下面描述的“作为函数调用”方法来支持。如果该类型既定义了“作为函数调用”方法,又定义了 dynamicCallable 特性所使用的方法之一,那么在两种方法都可用的情形下,编译器优先选用“作为函数调用”方法。

“作为函数调用”方法的名称为 callAsFunction(),或者以 callAsFunction( 开头并添加带标签或不带标签实参的其他名称——例如,callAsFunction(_:_:) 和 callAsFunction(something:) 也是有效的“作为函数调用”方法名。

下面这些函数调用是等价的:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
struct CallableStruct {
    var value: Int
    func callAsFunction(_ number: Int, scale: Int) {
        print(scale * (number + value))
    }
}
let callable = CallableStruct(value: 100)
callable(4, scale: 2)
callable.callAsFunction(4, scale: 2)
// 两个函数调用都打印 208。

“作为函数调用”方法与 dynamicCallable 特性提供的方法,在“有多少信息被编码进类型系统”与“运行时能做多少动态行为”之间做出了不同的取舍。声明“作为函数调用”方法时,你要指定实参的数量,以及每个实参的类型和标签。dynamicCallable 特性的方法则只指定用来存放实参数组的类型。

定义“作为函数调用”方法或 dynamicCallable 特性的方法,并不能让你在该类型实例的任何上下文中都像使用函数那样使用它,而仅限于函数调用表达式。例如:

1
2
let someFunction1: (Int, Int) -> Void = callable(_:scale:)  // 错误
let someFunction2: (Int, Int) -> Void = callable.callAsFunction(_:scale:)

subscript(dynamicMember:) 下标启用了成员查找的语法糖,如 dynamicMemberLookup所述。

抛出错误的函数和方法

可能抛出错误的函数和方法必须用 throws 关键字标记。这类函数和方法称为抛出函数和抛出方法。它们的形式如下:

1
2
3
func <#function name#>(<#parameters#>) throws -> <#return type#> {
   <#statements#>
}

抛出特定错误类型的函数形式如下:

1
2
3
func <#function name#>(<#parameters#>) throws(<#error type#>) -> <#return type#> {
   <#statements#>
}

对抛出函数或方法的调用必须包在 try 或 try! 表达式中(也就是说,必须处于 try 或 try! 运算符的作用域内)。

函数的类型包含它能否抛出错误以及抛出什么类型的错误。这种子类型关系意味着,例如你可以在需要抛出函数的地方使用不抛出错误的函数。关于抛出函数类型的更多信息,参见函数类型。使用具有显式类型的错误的例子,参见指定错误类型。

你不能仅仅根据函数能否抛出错误来重载函数。话虽如此,你可以根据函数形参能否抛出错误来重载函数。

抛出方法不能覆盖不抛出错误的方法,抛出方法也不能满足针对不抛出错误方法的协议要求。话虽如此,不抛出错误的方法可以覆盖抛出方法,不抛出错误的方法也可以满足针对抛出方法的协议要求。

重新抛出错误的函数和方法

函数或方法可以用 rethrows 关键字声明,以表明只有在它的某个函数形参抛出错误时,它才抛出错误。这类函数和方法称为重新抛出函数和重新抛出方法。重新抛出函数和方法必须至少有一个抛出错误的函数形参。

1
2
3
func someFunction(callback: () throws -> Void) rethrows {
    try callback()
}

重新抛出函数或方法只能在 catch 子句内包含 throw 语句。这样你就可以在 do-catch 语句内调用抛出函数,并在 catch 子句中通过抛出另一个错误来处理错误。此外,catch 子句只能处理由重新抛出函数的某个抛出形参所抛出的错误。例如,下面这段代码是无效的,因为 catch 子句会处理由 alwaysThrows() 抛出的错误。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
func alwaysThrows() throws {
    throw SomeError.error
}
func someFunction(callback: () throws -> Void) rethrows {
    do {
        try callback()
        try alwaysThrows()  // 无效,alwaysThrows() 不是抛出形参
    } catch {
        throw AnotherError.error
    }
}

抛出方法不能覆盖重新抛出方法,抛出方法也不能满足针对重新抛出方法的协议要求。话虽如此,重新抛出方法可以覆盖抛出方法,重新抛出方法也可以满足针对抛出方法的协议要求。

重新抛出的替代方案是在泛型代码中抛出特定错误类型。例如:

1
2
3
func someFunction<E: Error>(callback: () throws(E) -> Void) throws(E) {
    try callback()
}

这种传播错误的方式保留了关于该错误的类型信息。不过,与把函数标记为 rethrows 不同,这种方式并不能阻止函数抛出同类型的错误。

异步函数和方法

异步运行的函数和方法必须用 async 关键字标记。这类函数和方法称为异步函数和异步方法。它们的形式如下:

1
2
3
func <#function name#>(<#parameters#>) async -> <#return type#> {
   <#statements#>
}

对异步函数或方法的调用必须包在 await 表达式中——也就是说,它们必须处于 await 运算符的作用域内。

async 关键字是函数类型的一部分,同步函数是异步函数的子类型。因此,你可以在需要异步函数的地方使用同步函数。例如,你可以用同步方法覆盖异步方法,同步方法也可以满足要求异步方法的协议要求。

你可以根据函数是否异步来重载函数。在调用点,上下文决定使用哪个重载:在异步上下文中使用异步函数,在同步上下文中使用同步函数。

异步方法不能覆盖同步方法,异步方法也不能满足针对同步方法的协议要求。话虽如此,同步方法可以覆盖异步方法,同步方法也可以满足针对异步方法的协议要求。

永不返回的函数

Swift 定义了 Never 类型,表示函数或方法不返回到调用方。返回类型为 Never 的函数和方法称为不返回函数。不返回函数和方法要么导致不可恢复的错误,要么开启一段会无限持续的工作。这意味着原本会在调用之后立即执行的代码永远不会执行。即使抛出函数和重新抛出函数是不返回的,它们也可以把程序控制权转移到合适的 catch 块。

不返回函数或方法可以被调用,用来结束 guard 语句的 else 子句,如guard 语句所述。

你可以覆盖不返回方法,但新方法必须保持其返回类型和不返回的行为。

Grammar of a function declaration:

function-declaration → function-head function-name generic-parameter-clause? function-signature generic-where-clause? function-body?

function-head → attributes? declaration-modifiers? func
function-name → identifier | operator

function-signature → parameter-clause async? throws-clause? function-result?
function-signature → parameter-clause async? rethrows function-result?
function-result → -> attributes? type
function-body → code-block

parameter-clause → ( ) | ( parameter-list ,? )
parameter-list → parameter | parameter , parameter-list
parameter → external-parameter-name? local-parameter-name parameter-type-annotation default-argument-clause?
parameter → external-parameter-name? local-parameter-name parameter-type-annotation
parameter → external-parameter-name? local-parameter-name parameter-type-annotation ...

external-parameter-name → identifier
local-parameter-name → identifier
parameter-type-annotation → : attributes? parameter-modifier? type
parameter-modifier → inout | borrowing | consuming default-argument-clause → = expression

枚举声明

枚举声明把一个具名枚举类型引入你的程序。

枚举声明有两种基本形式,都用 enum 关键字声明。用这两种形式声明的枚举,其主体中都包含零个或多个值——称为枚举情况——以及任意数量的声明,包括计算属性、实例方法、类型方法、构造器、类型别名,甚至其他枚举、结构体、类和 actor 声明。枚举声明不能包含反初始化器声明或协议声明。

枚举类型可以遵循任意数量的协议,但不能继承自类、结构体或其他枚举。

与类和结构体不同,枚举类型没有隐式提供的默认构造器;所有构造器都必须显式声明。构造器可以委托给枚举中的其他构造器,但只有构造器把某个枚举情况赋给 self 之后,初始化过程才算完成。

与结构体一样(但与类不同),枚举是值类型;把枚举实例赋给变量或常量,或者作为实参传给函数调用时,都会发生复制。关于值类型的信息,参见结构体和枚举是值类型。

你可以用扩展声明来扩展枚举类型的行为,如扩展声明所述。

情况为任意类型的枚举

下面的形式声明一个包含任意类型枚举情况的枚举类型:

1
2
3
4
enum <#enumeration name#>: <#adopted protocols#> {
    case <#enumeration case 1#>
    case <#enumeration case 2#>(<#associated value types#>)
}

以这种形式声明的枚举,在其他编程语言中有时称为可辨识联合(discriminated union)。

在这种形式中,每个 case 块由 case 关键字后跟一个或多个以逗号分隔的枚举情况组成。每个情况的名称必须唯一。每个情况还可以指定它存储给定类型的值。这些类型在紧跟在情况名称之后的关联值类型元组中指定。

存储关联值的枚举情况可以作为函数使用,用来创建带指定关联值的枚举实例。而且就像函数一样,你可以取得对某个枚举情况的引用,并在代码中稍后应用它。

1
2
3
4
5
6
7
8
9
enum Number {
    case integer(Int)
    case real(Double)
}
let f = Number.integer
// f 是类型为 (Int) -> Number 的函数

// 把 f 应用到整数,创建一个 Number 实例数组
let evenInts: [Number] = [0, 2, 4, 6].map(f)

关于带关联值类型的情况的更多信息和示例,参见关联值。

带间接性的枚举

枚举可以有递归结构,也就是说,它可以有带关联值的情况,而这些关联值是枚举类型自身的实例。然而,枚举类型的实例具有值语义,这意味着它们在内存中有固定的布局。为了支持递归,编译器必须插入一层间接性。

要为某个特定的枚举情况启用间接性,用 indirect 声明修饰符标记它。间接情况必须带有关联值。

1
2
3
4
enum Tree<T> {
    case empty
    indirect case node(value: T, left: Tree, right: Tree)
}

要为枚举中所有带关联值的情况启用间接性,用 indirect 修饰符标记整个枚举——当枚举包含很多本来都需要用 indirect 修饰符标记的情况时,这样更方便。

用 indirect 修饰符标记的枚举可以同时包含带关联值的情况和不带关联值的情况。话虽如此,它不能包含任何也标记了 indirect 修饰符的情况。

情况为原始值类型的枚举

下面的形式声明一个包含同一基础类型枚举情况的枚举类型:

1
2
3
4
enum <#enumeration name#>: <#raw-value type#>, <#adopted protocols#> {
    case <#enumeration case 1#> = <#raw value 1#>
    case <#enumeration case 2#> = <#raw value 2#>
}

在这种形式中,每个 case 块由 case 关键字后跟一个或多个以逗号分隔的枚举情况组成。与第一种形式中的情况不同,每个情况都有一个同一基础类型的底层值,称为原始值。这些值的类型在原始值类型中指定,并且必须表示整数、浮点数、字符串或单个字符。具体来说,原始值类型必须遵循 Equatable 协议,以及下列协议之一:整数字面量用 ExpressibleByIntegerLiteral,浮点字面量用 ExpressibleByFloatLiteral,包含任意数量字符的字符串字面量用 ExpressibleByStringLiteral,只包含单个字符的字符串字面量用 ExpressibleByUnicodeScalarLiteral 或 ExpressibleByExtendedGraphemeClusterLiteral。每个情况的名称必须唯一,并且被赋予唯一的原始值。

如果原始值类型指定为 Int,并且你没有给这些情况显式赋值,它们会被隐式赋予值 0、1、2 等等。每个未赋值的 Int 类型情况都会被隐式赋予一个原始值,该值在前一个情况的原始值基础上自动递增。

1
2
3
enum ExampleEnum: Int {
    case a, b, c = 5, d
}

在上面的例子中,ExampleEnum.a 的原始值是 0,ExampleEnum.b 的值是 1。而由于 ExampleEnum.c 的值被显式设为 5,ExampleEnum.d 的值就在 5 的基础上自动递增,因此是 6。

如果原始值类型指定为 String,并且你没有给这些情况显式赋值,每个未赋值的情况都会被隐式赋予一个字符串,其文本与该情况的名称相同。

1
2
3
enum GamePlayMode: String {
    case cooperative, individual, competitive
}

在上面的例子中,GamePlayMode.cooperative 的原始值是 "cooperative",GamePlayMode.individual 的原始值是 "individual",GamePlayMode.competitive 的原始值是 "competitive"。

情况为原始值类型的枚举隐式遵循 Swift 标准库中定义的 RawRepresentable 协议。因此,它们有一个 rawValue 属性,以及一个签名为 init?(rawValue: RawValue) 的可失败构造器。你可以用 rawValue 属性访问枚举情况的原始值,如 ExampleEnum.b.rawValue。你也可以调用枚举的可失败构造器,用原始值找到对应的情况(如果存在的话),如 ExampleEnum(rawValue: 5),它返回一个可选的情况。关于带原始值类型的情况的更多信息和示例,参见原始值。

访问枚举情况

要引用枚举类型的情况,使用点(.)语法,如 EnumerationType.enumerationCase。当枚举类型可以从上下文推断出来时,你可以省略它(点仍然必需),如枚举语法和隐式成员表达式所述。

要检查枚举情况的值,使用 switch 语句,如用 switch 语句匹配枚举值所示。枚举类型会与 switch 语句 case 块中的枚举情况模式进行模式匹配,如枚举情况模式所述。

Grammar of an enumeration declaration:

enum-declaration → attributes? access-level-modifier? union-style-enum
enum-declaration → attributes? access-level-modifier? raw-value-style-enum

union-style-enum → indirect? enum enum-name generic-parameter-clause? type-inheritance-clause? generic-where-clause? { union-style-enum-members? }
union-style-enum-members → union-style-enum-member union-style-enum-members?
union-style-enum-member → declaration | union-style-enum-case-clause | compiler-control-statement
union-style-enum-case-clause → attributes? indirect? case union-style-enum-case-list
union-style-enum-case-list → union-style-enum-case | union-style-enum-case , union-style-enum-case-list
union-style-enum-case → enum-case-name tuple-type?
enum-name → identifier
enum-case-name → identifier

raw-value-style-enum → enum enum-name generic-parameter-clause? type-inheritance-clause generic-where-clause? { raw-value-style-enum-members }
raw-value-style-enum-members → raw-value-style-enum-member raw-value-style-enum-members?
raw-value-style-enum-member → declaration | raw-value-style-enum-case-clause | compiler-control-statement
raw-value-style-enum-case-clause → attributes? case raw-value-style-enum-case-list
raw-value-style-enum-case-list → raw-value-style-enum-case | raw-value-style-enum-case , raw-value-style-enum-case-list
raw-value-style-enum-case → enum-case-name raw-value-assignment?
raw-value-assignment → = raw-value-literal
raw-value-literal → numeric-literal | static-string-literal | boolean-literal

结构体声明

结构体声明把一个具名结构体类型引入你的程序。结构体声明用 struct 关键字声明,形式如下:

1
2
3
struct <#structure name#>: <#adopted protocols#> {
   <#declarations#>
}

结构体的主体包含零个或多个声明。这些声明可以包括存储属性和计算属性、类型属性、实例方法、类型方法、构造器、下标、类型别名,甚至其他结构体、类、actor 和枚举声明。结构体声明不能包含反初始化器声明或协议声明。关于包含各种声明的结构体的讨论和若干示例,参见结构体与类。

结构体类型可以遵循任意数量的协议,但不能继承自类、枚举或其他结构体。

创建一个已声明结构体的实例有三种方式:

  • 调用结构体内声明的某个构造器,如构造器所述。
  • 如果没有声明任何构造器,调用结构体的逐成员构造器,如结构体类型的逐成员构造器所述。
  • 如果没有声明任何构造器,并且结构体声明中的所有属性都给了初始值,调用结构体的默认构造器,如默认构造器所述。

初始化结构体所声明属性的过程在初始化中描述。

结构体实例的属性可以用点(.)语法访问,如访问属性所述。

结构体是值类型;把结构体实例赋给变量或常量,或者作为实参传给函数调用时,都会发生复制。关于值类型的信息,参见结构体和枚举是值类型。

你可以用扩展声明来扩展结构体类型的行为,如扩展声明所述。

Grammar of a structure declaration:

struct-declaration → attributes? access-level-modifier? struct struct-name generic-parameter-clause? type-inheritance-clause? generic-where-clause? struct-body
struct-name → identifier
struct-body → { struct-members? }

struct-members → struct-member struct-members?
struct-member → declaration | compiler-control-statement

类声明

类声明把一个具名类类型引入你的程序。类声明用 class 关键字声明,形式如下:

1
2
3
class <#class name#>: <#superclass#>, <#adopted protocols#> {
   <#declarations#>
}

类的主体包含零个或多个声明。这些声明可以包括存储属性和计算属性、实例方法、类型方法、构造器、一个反初始化器、下标、类型别名,甚至其他类、结构体、actor 和枚举声明。类声明不能包含协议声明。关于包含各种声明的类的讨论和若干示例,参见结构体与类。

类类型只能继承自一个父类,即它的超类,但可以遵循任意数量的协议。超类出现在类名和冒号之后的最前面,随后是任意所遵循的协议。泛型类可以继承自其他泛型类和非泛型类,但非泛型类只能继承自其他非泛型类。在冒号后写泛型超类的名称时,必须写出该泛型类的完整名称,包括它的泛型形参子句。

如构造器声明所述,类可以有指定构造器和便利构造器。类的指定构造器必须初始化该类声明的所有属性,而且必须在调用其超类的任何指定构造器之前完成这些初始化。

类可以覆盖其超类的属性、方法、下标和构造器。被覆盖的属性、方法、下标和指定构造器必须用 override 声明修饰符标记。

要要求子类实现超类的某个构造器,用 required 声明修饰符标记超类的该构造器。子类对该构造器的实现也必须用 required 声明修饰符标记。

尽管超类中声明的属性和方法会被当前类继承,但超类中声明的指定构造器只有在子类满足自动构造器继承所述条件时才会被继承。Swift 的类不继承自某个通用基类。

创建一个已声明类的实例有两种方式:

  • 调用类内声明的某个构造器,如构造器所述。
  • 如果没有声明任何构造器,并且类声明中的所有属性都给了初始值,调用类的默认构造器,如默认构造器所述。

用点(.)语法访问类实例的属性,如访问属性所述。

类是引用类型;把类实例赋给变量或常量,或者作为实参传给函数调用时,引用的是该实例而不是复制它。关于引用类型的信息,参见类是引用类型。

你可以用扩展声明来扩展类类型的行为,如扩展声明所述。

Grammar of a class declaration:

class-declaration → attributes? access-level-modifier? final? class class-name generic-parameter-clause? type-inheritance-clause? generic-where-clause? class-body
class-declaration → attributes? final access-level-modifier? class class-name generic-parameter-clause? type-inheritance-clause? generic-where-clause? class-body
class-name → identifier
class-body → { class-members? }

class-members → class-member class-members?
class-member → declaration | compiler-control-statement

Actor 声明

actor 声明把一个具名 actor 类型引入你的程序。actor 声明用 actor 关键字声明,形式如下:

1
2
3
actor <#actor name#>: <#adopted protocols#> {
    <#declarations#>
}

actor 的主体包含零个或多个声明。这些声明可以包括存储属性和计算属性、实例方法、类型方法、构造器、一个反初始化器、下标、类型别名,甚至其他类、结构体和枚举声明。关于包含各种声明的 actor 的讨论和若干示例,参见 Actor。

actor 类型可以遵循任意数量的协议,但不能继承自类、枚举、结构体或其他 actor。不过,用 @objc 特性标记的 actor 会隐式遵循 NSObjectProtocol 协议,并作为 NSObject 的子类型暴露给 Objective-C 运行时。

创建一个已声明 actor 的实例有两种方式:

  • 调用 actor 内声明的某个构造器,如构造器所述。
  • 如果没有声明任何构造器,并且 actor 声明中的所有属性都给了初始值,调用 actor 的默认构造器,如默认构造器所述。

默认情况下,actor 的成员被隔离到该 actor。代码(例如方法体或属性的 getter)会在该 actor 上执行。actor 内部的代码可以同步地与这些成员交互,因为这些代码已经在同一个 actor 上运行;但 actor 外部的代码必须用 await 标记它们,以表明这段代码是在另一个 actor 上异步运行的代码。键路径不能引用 actor 的隔离成员。actor 隔离的存储属性可以作为输入输出实参传给同步函数,但不能传给异步函数。

actor 也可以有非隔离成员,其声明用 nonisolated 关键字标记。非隔离成员的行为与 actor 外部的代码一样:它不能与 actor 的任何隔离状态交互,调用方使用它时也不需要用 await 标记。

只有当 actor 的成员是非隔离的或异步的时,才能用 @objc 特性标记这些成员。

初始化 actor 所声明属性的过程在初始化中描述。

actor 实例的属性可以用点(.)语法访问,如访问属性所述。

actor 是引用类型;把 actor 实例赋给变量或常量,或者作为实参传给函数调用时,引用的是该实例而不是复制它。关于引用类型的信息,参见类是引用类型。

你可以用扩展声明来扩展 actor 类型的行为,如扩展声明所述。

Grammar of an actor declaration:

actor-declaration → attributes? access-level-modifier? actor actor-name generic-parameter-clause? type-inheritance-clause? generic-where-clause? actor-body
actor-name → identifier
actor-body → { actor-members? }

actor-members → actor-member actor-members?
actor-member → declaration | compiler-control-statement

协议声明

协议声明把一个具名协议类型引入你的程序。协议声明用 protocol 关键字声明,形式如下:

1
2
3
protocol <#protocol name#>: <#inherited protocols#> {
   <#protocol member declarations#>
}

协议声明可以出现在全局作用域,也可以嵌套在非泛型类型或非泛型函数内部。

协议的主体包含零个或多个协议成员声明,它们描述任何遵循该协议的类型都必须满足的遵循性要求。具体来说,协议可以声明遵循类型必须实现某些属性、方法、构造器和下标。协议还可以声明一种特殊类型的别名,称为关联类型,用于指定协议各个声明之间的关系。协议声明不能包含类、结构体、枚举或其他协议声明。协议成员声明将在下面详细讨论。

协议类型可以继承自任意数量的其他协议。当协议类型继承自其他协议时,那些其他协议的要求会合并在一起,任何继承当前协议的类型都必须遵循所有这些要求。关于如何使用协议继承的示例,参见协议继承。

注意:你也可以用协议组合类型来合并多个协议的遵循性要求, 如协议组合类型 和协议组合所述。

你可以通过在该类型的扩展声明中采纳某个协议,为先前已声明的类型添加协议遵循性。在扩展中,你必须实现所采纳协议的全部要求。如果该类型已经实现了全部要求,你可以让扩展声明的主体为空。

默认情况下,遵循某个协议的类型必须实现该协议中声明的所有属性、方法和下标。话虽如此,你可以用 optional 声明修饰符标记这些协议成员声明,以表明遵循类型的实现是可选的。optional 修饰符只能用于标记了 objc 特性的成员,并且只能用于标记了 objc 特性的协议中的成员。因此,只有类类型才能采纳并遵循包含可选成员要求的协议。关于如何使用 optional 声明修饰符的更多信息,以及如何访问可选协议成员的指导——例如当你并不确定某个遵循类型是否实现了它们时——参见可选的协议要求。

枚举的情况可以满足针对类型成员的协议要求。具体来说,不带任何关联值的枚举情况可以满足针对类型为 Self 的只读类型变量的协议要求;带关联值的枚举情况可以满足针对返回 Self 的函数的协议要求,只要该函数的形参及其实参标签与该情况的关联值匹配。例如:

1
2
3
4
5
6
7
8
protocol SomeProtocol {
    static var someValue: Self { get }
    static func someFunction(x: Int) -> Self
}
enum MyEnum: SomeProtocol {
    case someValue
    case someFunction(x: Int)
}

要把协议的采纳限制为仅限类类型,在冒号后的所继承的协议列表中包含 AnyObject 协议。例如,下面这个协议只能被类类型采纳:

1
2
3
protocol SomeProtocol: AnyObject {
    /* Protocol members go here */
}

任何继承自带有 AnyObject 要求的协议的协议,同样也只能被类类型采纳。

注意:如果某个协议用 objc 特性标记, 那么 AnyObject 要求会隐式应用于该协议; 无需再用 AnyObject 要求显式标记该协议。

协议是具名类型,因此它们可以像其他具名类型一样出现在代码中的各个位置,如把协议用作类型所述。不过,你不能构造协议的实例,因为协议实际上并不为其规定的要求提供实现。

你可以用协议来声明类或结构体的委托应该实现哪些方法,如委托所述。

Grammar of a protocol declaration:

protocol-declaration → attributes? access-level-modifier? protocol protocol-name type-inheritance-clause? generic-where-clause? protocol-body
protocol-name → identifier
protocol-body → { protocol-members? }

protocol-members → protocol-member protocol-members?
protocol-member → protocol-member-declaration | compiler-control-statement

protocol-member-declaration → protocol-property-declaration
protocol-member-declaration → protocol-method-declaration
protocol-member-declaration → protocol-initializer-declaration
protocol-member-declaration → protocol-subscript-declaration
protocol-member-declaration → protocol-associated-type-declaration
protocol-member-declaration → typealias-declaration

协议属性声明

协议通过在协议声明的主体中写入协议属性声明,来声明遵循类型必须实现某个属性。协议属性声明是变量声明的一种特殊形式:

1
var <#property name#>: <#type#> { get set }

与其他协议成员声明一样,这些属性声明只声明遵循该协议的类型所需的 getter 和 setter 要求。因此,你不需要在声明它的协议中直接实现 getter 或 setter。

遵循类型可以用多种方式满足 getter 和 setter 要求。如果属性声明同时包含 get 和 set 关键字,遵循类型可以用存储变量属性来实现它,也可以用既可读又可写的计算属性(也就是说,同时实现 getter 和 setter 的属性)来实现它。不过,该属性声明不能用常量属性或只读计算属性来实现。如果属性声明只包含 get 关键字,它可以用任何种类的属性来实现。关于实现协议属性要求的遵循类型的示例,参见属性要求。

要在协议声明中声明类型属性要求,用 static 关键字标记该属性声明。遵循该协议的结构体和枚举用 static 关键字声明该属性,遵循该协议的类则用 static 或 class 关键字声明该属性。为结构体、枚举或类添加协议遵循性的扩展,使用与其所扩展类型相同的关键字。为类型属性要求提供默认实现的扩展使用 static 关键字。

另见变量声明。

Grammar of a protocol property declaration:

protocol-property-declaration → variable-declaration-head variable-name type-annotation getter-setter-keyword-block

协议方法声明

协议通过在协议声明的主体中写入协议方法声明,来声明遵循类型必须实现某个方法。协议方法声明与函数声明形式相同,但有两个例外:它们不包含函数体,而且你不能在函数声明中提供任何默认形参值。关于实现协议方法要求的遵循类型的示例,参见方法要求。

要在协议声明中声明类方法或静态方法要求,用 static 声明修饰符标记该方法声明。遵循该协议的结构体和枚举用 static 关键字声明该方法,遵循该协议的类则用 static 或 class 关键字声明该方法。为结构体、枚举或类添加协议遵循性的扩展,使用与其所扩展类型相同的关键字。为类型方法要求提供默认实现的扩展使用 static 关键字。

另见函数声明。

Grammar of a protocol method declaration:

protocol-method-declaration → function-head function-name generic-parameter-clause? function-signature generic-where-clause?

协议构造器声明

协议通过在协议声明的主体中写入协议构造器声明,来声明遵循类型必须实现某个构造器。协议构造器声明与构造器声明形式相同,只是不包含构造器的体。

遵循类型可以通过实现一个非可失败构造器或一个 init! 可失败构造器,来满足非可失败协议构造器要求。遵循类型可以通过实现任何种类的构造器,来满足可失败协议构造器要求。

当类实现某个构造器以满足协议的构造器要求时,如果该类尚未用 final 声明修饰符标记,则该构造器必须用 required 声明修饰符标记。

另见构造器声明。

Grammar of a protocol initializer declaration:

protocol-initializer-declaration → initializer-head generic-parameter-clause? parameter-clause throws-clause? generic-where-clause?
protocol-initializer-declaration → initializer-head generic-parameter-clause? parameter-clause rethrows generic-where-clause?

协议下标声明

协议通过在协议声明的主体中写入协议下标声明,来声明遵循类型必须实现某个下标。协议下标声明是下标声明的一种特殊形式:

1
subscript (<#parameters#>) -> <#return type#> { get set }

下标声明只声明遵循该协议的类型所需的最低 getter 和 setter 实现要求。如果下标声明同时包含 get 和 set 关键字,遵循类型必须同时实现 getter 和 setter 子句。如果下标声明只包含 get 关键字,遵循类型必须至少实现一个 getter 子句,并可以选择实现一个 setter 子句。

要在协议声明中声明静态下标要求,用 static 声明修饰符标记该下标声明。遵循该协议的结构体和枚举用 static 关键字声明该下标,遵循该协议的类则用 static 或 class 关键字声明该下标。为结构体、枚举或类添加协议遵循性的扩展,使用与其所扩展类型相同的关键字。为静态下标要求提供默认实现的扩展使用 static 关键字。

另见下标声明。

Grammar of a protocol subscript declaration:

protocol-subscript-declaration → subscript-head subscript-result generic-where-clause? getter-setter-keyword-block

协议关联类型声明

协议用 associatedtype 关键字声明关联类型。关联类型为协议声明中使用的某个类型提供别名。关联类型类似于泛型形参子句中的类型形参,但它们与声明它们的协议中的 Self 相关联。在那个上下文中,Self 指最终遵循该协议的类型。关于更多信息和示例,参见关联类型。

你可以在协议声明中使用泛型 where 子句,为从另一个协议继承来的关联类型添加约束,而无需重新声明这些关联类型。例如,下面 SubProtocol 的两种声明是等价的:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
protocol SomeProtocol {
    associatedtype SomeType
}

protocol SubProtocolA: SomeProtocol {
    // 这种写法会产生一个警告。
    associatedtype SomeType: Equatable
}

// 更推荐这种写法。
protocol SubProtocolB: SomeProtocol where SomeType: Equatable { }

另见类型别名声明。

Grammar of a protocol associated type declaration:

protocol-associated-type-declaration → attributes? access-level-modifier? associatedtype typealias-name type-inheritance-clause? typealias-assignment? generic-where-clause?

构造器声明

构造器声明把类、结构体或枚举的构造器引入你的程序。构造器声明用 init 关键字声明,有两种基本形式。

结构体、枚举和类类型可以有任意数量的构造器,但类构造器的规则和相关行为有所不同。与结构体和枚举不同,类有两种构造器:指定构造器和便利构造器,如初始化所述。

下面的形式为结构体、枚举以及类的指定构造器声明构造器:

1
2
3
init(<#parameters#>) {
   <#statements#>
}

类的指定构造器直接初始化该类的所有属性。它不能调用同一个类的任何其他构造器;如果该类有超类,它必须调用超类的某个指定构造器。如果类从超类继承了任何属性,那么在当前类中设置或修改这些属性之前,必须先调用超类的某个指定构造器。

指定构造器只能在类声明的上下文中声明,因此不能用扩展声明添加到类中。

结构体和枚举中的构造器可以调用其他已声明的构造器,以委托部分或全部初始化过程。

要为类声明便利构造器,用 convenience 声明修饰符标记该构造器声明。

1
2
3
convenience init(<#parameters#>) {
   <#statements#>
}

便利构造器可以把初始化过程委托给另一个便利构造器,或者委托给该类的某个指定构造器。话虽如此,初始化过程最终必须落在对某个指定构造器的调用上,由它来初始化类的属性。便利构造器不能调用超类的构造器。

你可以用 required 声明修饰符标记指定构造器和便利构造器,以要求每个子类都实现该构造器。子类对该构造器的实现也必须用 required 声明修饰符标记。

默认情况下,超类中声明的构造器不会被子类继承。话虽如此,如果子类用默认值初始化了它所有的存储属性,并且没有定义自己的任何构造器,那么它会继承超类的全部构造器。如果子类覆盖了超类的所有指定构造器,它会继承超类的便利构造器。

与方法、属性和下标一样,你需要用 override 声明修饰符标记被覆盖的指定构造器。

注意:如果你用 required 声明修饰符标记了某个构造器, 那么在子类中覆盖这个必需构造器时, 不需要再用 override 修饰符标记它。

与函数和方法一样,构造器可以抛出或重新抛出错误。同样与函数和方法一样,你在构造器的形参之后使用 throws 或 rethrows 关键字来表明相应行为。同样,构造器可以是异步的,你用 async 关键字来表明这一点。

关于各种类型声明中构造器的示例,参见初始化。

可失败构造器

可失败构造器是一类构造器,它产生所声明类型的可选实例或隐式解包可选实例。因此,可失败构造器可以返回 nil 来表示初始化失败。

要声明产生可选实例的可失败构造器,在构造器声明的 init 关键字后加一个问号(init?)。要声明产生隐式解包可选实例的可失败构造器,改为加一个感叹号(init!)。下面的例子展示了一个 init? 可失败构造器,它产生结构体的可选实例。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
struct SomeStruct {
    let property: String
    // 产生 'SomeStruct' 的可选实例
    init?(input: String) {
        if input.isEmpty {
            // 丢弃 'self' 并返回 'nil'
            return nil
        }
        property = input
    }
}

调用 init? 可失败构造器的方式与调用非可失败构造器相同,只是你必须处理结果的可选性。

1
2
3
4
5
if let actualInstance = SomeStruct(input: "Hello") {
    // 对 'SomeStruct' 的实例做点什么
} else {
    // 'SomeStruct' 初始化失败,构造器返回了 'nil'
}

可失败构造器可以在构造器体实现中的任何位置返回 nil。

可失败构造器可以委托给任何种类的构造器。非可失败构造器可以委托给另一个非可失败构造器,或者委托给 init! 可失败构造器。非可失败构造器可以通过强制解包超类构造器的结果来委托给 init? 可失败构造器——例如写成 super.init()!。

初始化失败会沿构造器委托链传播。具体来说,如果某个可失败构造器委托给一个失败并返回 nil 的构造器,那么进行委托的那个构造器也会失败,并隐式返回 nil。如果某个非可失败构造器委托给一个失败并返回 nil 的 init! 可失败构造器,则会引发运行时错误(就好像你用 ! 运算符去解包一个值为 nil 的可选量)。

可失败指定构造器可以在子类中由任何种类的指定构造器覆盖。非可失败指定构造器在子类中只能由非可失败指定构造器覆盖。

关于可失败构造器的更多信息和示例,参见可失败构造器。

Grammar of an initializer declaration:

initializer-declaration → initializer-head generic-parameter-clause? parameter-clause async? throws-clause? generic-where-clause? initializer-body
initializer-declaration → initializer-head generic-parameter-clause? parameter-clause async? rethrows generic-where-clause? initializer-body
initializer-head → attributes? declaration-modifiers? init
initializer-head → attributes? declaration-modifiers? init ?
initializer-head → attributes? declaration-modifiers? init !
initializer-body → code-block

反初始化器声明

反初始化器声明为类类型声明一个反初始化器。反初始化器不接受形参,形式如下:

1
2
3
deinit {
   <#statements#>
}

当不再有任何引用指向某个类对象时,会在该对象被释放之前自动调用反初始化器。反初始化器只能在类声明的主体中声明——不能在类的扩展中声明——而且每个类最多只能有一个。

子类会继承超类的反初始化器,该反初始化器会在子类对象被释放之前被隐式调用。只有当继承链中所有反初始化器都执行完毕后,子类对象才会被释放。

反初始化器不会被直接调用。

关于如何在类声明中使用反初始化器的示例,参见反初始化。

Grammar of a deinitializer declaration:

deinitializer-declaration → attributes? deinit code-block

扩展声明

扩展声明允许你扩展已有类型的行为。扩展声明用 extension 关键字声明,形式如下:

1
2
3
extension <#type name#> where <#requirements#> {
   <#declarations#>
}

扩展声明的主体包含零个或多个声明。这些声明可以包括计算属性、计算类型属性、实例方法、类型方法、构造器、下标声明,甚至类、结构体和枚举声明。扩展声明不能包含反初始化器声明或协议声明、存储属性、属性观察器,也不能包含其他扩展声明。协议扩展中的声明不能用 final 标记。关于包含各种声明的扩展的讨论和若干示例,参见扩展。

如果类型名是类、结构体或枚举类型,该扩展就扩展这个类型。如果类型名是协议类型,该扩展就扩展遵循该协议的所有类型。

扩展泛型类型或带关联类型的协议的扩展声明可以包含要求。如果被扩展类型(或遵循被扩展协议的类型)的某个实例满足这些要求,该实例就会获得声明中所指定的行为。

扩展声明可以包含构造器声明。话虽如此,如果你要扩展的类型定义在另一个模块中,那么构造器声明必须委托给该模块中已定义的构造器,以确保该类型的成员被正确初始化。

已有类型的属性、方法和构造器不能在该类型的扩展中被覆盖。

扩展声明可以通过指定所采纳的协议,为已有的类、结构体或枚举类型添加协议遵循性:

1
2
3
extension <#type name#>: <#adopted protocols#> where <#requirements#> {
   <#declarations#>
}

扩展声明不能为已有类添加类继承,因此在类型名和冒号之后只能指定协议列表。

条件遵循

你可以扩展一个泛型类型,让它有条件地遵循某个协议,这样只有当满足某些要求时,该类型的实例才遵循该协议。你通过在扩展声明中包含要求,来为某个协议添加条件遵循。

在某些泛型上下文中不会使用被覆盖的要求

在某些泛型上下文中,通过条件遵循某个协议而获得行为的类型,并不总是使用该协议要求的特化实现。为了说明这种行为,下面的例子定义了两个协议,以及一个有条件地遵循这两个协议的泛型类型。

 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
33
34
35
36
37
38
protocol Loggable {
    func log()
}
extension Loggable {
    func log() {
        print(self)
    }
}

protocol TitledLoggable: Loggable {
    static var logTitle: String { get }
}
extension TitledLoggable {
    func log() {
        print("\(Self.logTitle): \(self)")
    }
}

struct Pair<T>: CustomStringConvertible {
    let first: T
    let second: T
    var description: String {
        return "(\(first), \(second))"
    }
}

extension Pair: Loggable where T: Loggable { }
extension Pair: TitledLoggable where T: TitledLoggable {
    static var logTitle: String {
        return "Pair of '\(T.logTitle)'"
    }
}

extension String: TitledLoggable {
    static var logTitle: String {
        return "String"
    }
}

只要 Pair 的泛型类型分别遵循 Loggable 或 TitledLoggable,Pair 结构体就遵循 Loggable 和 TitledLoggable。在下面的例子中,oneAndTwo 是 Pair<String> 的实例,它遵循 TitledLoggable,因为 String 遵循 TitledLoggable。当直接对 oneAndTwo 调用 log() 方法时,使用的是包含标题字符串的特化版本。

1
2
3
let oneAndTwo = Pair(first: "one", second: "two")
oneAndTwo.log()
// 打印 "Pair of 'String': (one, two)"。

然而,当 oneAndTwo 用在泛型上下文中,或者当作 Loggable 协议的实例使用时,就不会使用特化版本。Swift 只根据 Pair 要遵循 Loggable 所需的最低要求,来选择调用 log() 的哪个实现。因此,这里使用的是 Loggable 协议提供的默认实现。

1
2
3
4
5
func doSomething<T: Loggable>(with x: T) {
    x.log()
}
doSomething(with: oneAndTwo)
// 打印 "(one, two)"。

当对传给 doSomething(_:) 的实例调用 log() 时,输出的字符串中就省略了自定义的标题。

协议遵循性不能冗余

一个具体类型只能遵循某个特定协议一次。Swift 会把冗余的协议遵循性标记为错误。你很可能在两类情形下遇到这种错误。第一类是显式地多次遵循同一个协议,但每次的要求不同。第二类是隐式地多次继承同一个协议。下面各节将讨论这些情形。

解决显式冗余

同一个具体类型上的多个扩展不能添加对同一个协议的遵循性,即使这些扩展的要求互斥也不行。下面的例子演示了这一限制。两个扩展声明都试图为 Serializable 协议添加条件遵循,一个用于元素为 Int 的数组,另一个用于元素为 String 的数组。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
protocol Serializable {
    func serialize() -> Any
}

extension Array: Serializable where Element == Int {
    func serialize() -> Any {
        // 实现
    }
}
extension Array: Serializable where Element == String {
    func serialize() -> Any {
        // 实现
    }
}
// 错误:'Array<Element>' 对协议 'Serializable' 的遵循性是冗余的。

如果你需要基于多个具体类型添加条件遵循,可以创建一个新协议,让每个类型都能遵循它,并在声明条件遵循时把该协议作为要求使用。

1
2
3
4
5
6
7
8
9
protocol SerializableInArray { }
extension Int: SerializableInArray { }
extension String: SerializableInArray { }

extension Array: Serializable where Element: SerializableInArray {
    func serialize() -> Any {
        // 实现
    }
}

解决隐式冗余

当某个具体类型有条件地遵循某个协议时,该类型会隐式遵循所有具有相同要求的父协议。

如果你需要让某个类型有条件地遵循两个继承自同一个父协议的协议,请显式声明对父协议的遵循性。这样可以避免因要求不同而隐式地两次遵循父协议。

下面的例子显式声明了 Array 对 Loggable 的条件遵循,以避免在同时声明对 TitledLoggable 和新协议 MarkedLoggable 的条件遵循时产生冲突。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
protocol MarkedLoggable: Loggable {
    func markAndLog()
}

extension MarkedLoggable {
    func markAndLog() {
        print("----------")
        log()
    }
}

extension Array: Loggable where Element: Loggable { }
extension Array: TitledLoggable where Element: TitledLoggable {
    static var logTitle: String {
        return "Array of '\(Element.logTitle)'"
    }
}
extension Array: MarkedLoggable where Element: MarkedLoggable { }

如果没有这个显式声明对 Loggable 条件遵循的扩展,其他 Array 扩展就会隐式地创建下面这些声明,从而导致错误:

1
2
3
extension Array: Loggable where Element: TitledLoggable { }
extension Array: Loggable where Element: MarkedLoggable { }
// 错误:'Array<Element>' 对协议 'Loggable' 的遵循性是冗余的。

Grammar of an extension declaration:

extension-declaration → attributes? access-level-modifier? extension type-identifier type-inheritance-clause? generic-where-clause? extension-body
extension-body → { extension-members? }

extension-members → extension-member extension-members?
extension-member → declaration | compiler-control-statement

下标声明

下标声明允许你为特定类型的对象添加下标支持,通常用于为访问集合、列表或序列中的元素提供便捷语法。下标声明用 subscript 关键字声明,形式如下:

1
2
3
4
5
6
7
8
subscript (<#parameters#>) -> <#return type#> {
   get {
      <#statements#>
   }
   set(<#setter name#>) {
      <#statements#>
   }
}

下标声明只能出现在类、结构体、枚举、扩展或协议声明的上下文中。

形参指定一个或多个用于在下标表达式中访问相应类型元素的索引(例如表达式 object[i] 中的 i)。虽然用于访问元素的索引可以是任意类型,但每个形参都必须包含类型注解,以指定每个索引的类型。返回类型指定被访问元素的类型。

与计算属性一样,下标声明支持读取和写入被访问元素的值。getter 用于读取值,setter 用于写入值。setter 子句是可选的;只需要 getter 时,你可以把两个子句都省略,直接返回所需的值。话虽如此,如果你提供了 setter 子句,就必须同时提供 getter 子句。

setter 名及其外面的圆括号是可选的。如果提供了 setter 名,它会被用作 setter 参数的名称。如果没有提供 setter 名,setter 的默认参数名是 newValue。setter 参数的类型与返回类型相同。

只要形参或返回类型与被重载的下标不同,你就可以在下标声明所在的类型中重载它。你也可以覆盖从超类继承来的下标声明。这样做时,必须用 override 声明修饰符标记被覆盖的下标声明。

下标形参遵循与函数形参相同的规则,但有两个例外。首先,与函数、方法和构造器不同,下标默认使用的形参不带实参标签。不过,你可以用与函数、方法和构造器相同的语法提供显式实参标签。此外,下标不能有输入输出形参。下标形参可以有默认值,使用特殊种类的形参中描述的语法。

你也可以在协议声明的上下文中声明下标,如协议下标声明所述。

关于下标的更多信息以及下标声明的示例,参见下标。

类型下标声明

要声明由类型本身(而不是类型的实例)公开的下标,用 static 声明修饰符标记该下标声明。类可以把类型计算属性改用 class 声明修饰符标记,以允许子类覆盖父类的实现。在类声明中,static 关键字的效果等同于用 class 和 final 两个声明修饰符标记该声明。

Grammar of a subscript declaration:

subscript-declaration → subscript-head subscript-result generic-where-clause? code-block
subscript-declaration → subscript-head subscript-result generic-where-clause? getter-setter-block
subscript-declaration → subscript-head subscript-result generic-where-clause? getter-setter-keyword-block
subscript-head → attributes? declaration-modifiers? subscript generic-parameter-clause? parameter-clause
subscript-result → -> attributes? type

宏声明

宏声明引入一个新的宏。它以 macro 关键字开头,形式如下:

1
macro <#name#> = <#macro implementation#>

宏实现是另一个宏,它指出执行该宏展开的代码所在的位置。执行宏展开的代码是一个独立的 Swift 程序,它使用 SwiftSyntax 模块与 Swift 代码交互。调用 Swift 标准库中的 externalMacro(module:type:) 宏,传入包含该宏实现的类型名,以及包含该类型的模块名。

宏可以被重载,遵循与函数相同的模型。宏声明只能出现在文件作用域。

关于 Swift 中宏的概览,参见宏。

Grammar of a macro declaration:

macro-declaration → macro-head identifier generic-parameter-clause? macro-signature macro-definition? generic-where-clause
macro-head → attributes? declaration-modifiers? macro
macro-signature → parameter-clause macro-function-signature-result?
macro-function-signature-result → -> type
macro-definition → = expression

运算符声明

运算符声明把一个新的中缀、前缀或后缀运算符引入你的程序,用 operator 关键字声明。

你可以声明三种不同位置性质的运算符:中缀、前缀和后缀。运算符的位置性质指出运算符相对于其操作数的位置。

运算符声明有三种基本形式,分别对应一种位置性质。运算符的位置性质通过在 operator 关键字之前用 infix、prefix 或 postfix 声明修饰符标记运算符声明来指定。在每种形式中,运算符的名称只能包含运算符中定义的运算符字符。

下面的形式声明一个新的中缀运算符:

1
infix operator <#operator name#>: <#precedence group#>

中缀运算符是写在两个操作数之间的二元运算符,例如表达式 1 + 2 中我们熟悉的加法运算符(+)。

中缀运算符可以选择指定一个优先级组。如果你省略运算符的优先级组,Swift 会使用默认优先级组 DefaultPrecedence,它指定的优先级仅高于 TernaryPrecedence。更多信息参见优先级组声明。

下面的形式声明一个新的前缀运算符:

1
prefix operator <#operator name#>

前缀运算符是紧写在操作数之前的一元运算符,例如表达式 !a 中的前缀逻辑非运算符(!)。

前缀运算符声明不指定优先级。前缀运算符是非结合的。

下面的形式声明一个新的后缀运算符:

1
postfix operator <#operator name#>

后缀运算符是紧写在操作数之后的一元运算符,例如表达式 a! 中的后缀强制解包运算符(!)。

与前缀运算符一样,后缀运算符声明也不指定优先级。后缀运算符是非结合的。

声明一个新运算符之后,你通过声明一个与该运算符同名的静态方法来实现它。该静态方法是运算符接收其某个实参值的那些类型之一的成员——例如,把 Double 乘以 Int 的运算符,实现为 Double 或 Int 结构体上的静态方法。如果你实现的是前缀或后缀运算符,还必须用相应的 prefix 或 postfix 声明修饰符标记该方法声明。关于如何创建和实现新运算符的示例,参见自定义运算符。

Grammar of an operator declaration:

operator-declaration → prefix-operator-declaration | postfix-operator-declaration | infix-operator-declaration

prefix-operator-declaration → prefix operator operator
postfix-operator-declaration → postfix operator operator
infix-operator-declaration → infix operator operator infix-operator-group?

infix-operator-group → : precedence-group-name

优先级组声明

优先级组声明把中缀运算符优先级的一个新分组引入你的程序。运算符的优先级指出在没有分组圆括号时,运算符与其操作数结合的紧密程度。

优先级组声明的形式如下:

1
2
3
4
5
6
precedencegroup <#precedence group name#> {
    higherThan: <#lower group names#>
    lowerThan: <#higher group names#>
    associativity: <#associativity#>
    assignment: <#assignment#>
}

较低组名和较高组名列表指定新优先级组与已有优先级组之间的关系。lowerThan 优先级组特性只能用来引用在当前模块之外声明的优先级组。当两个运算符争夺它们的操作数时,例如在表达式 2 + 3 * 5 中,相对优先级较高的运算符会更紧密地与操作数结合。

注意:用较低组名和较高组名相互关联的优先级组 必须构成单一的相对层级关系, 但它们不必形成线性层级。 这意味着可能存在相对优先级未定义的优先级组。 来自这些优先级组的运算符 在没有分组圆括号的情况下不能相邻使用。

Swift 定义了许多优先级组,与 Swift 标准库提供的运算符相配套。例如,加法(+)和减法(-)运算符属于 AdditionPrecedence 组,乘法(*)和除法(/)运算符属于 MultiplicationPrecedence 组。Swift 标准库提供的优先级组的完整列表,参见运算符声明。

运算符的结合性指出在没有分组圆括号时,一串同级优先级的运算符如何分组。你通过写上下文相关关键字 left、right 或 none 之一来指定运算符的结合性——如果省略结合性,默认是 none。左结合的运算符从左到右分组。例如,减法运算符(-)是左结合的,因此表达式 4 - 5 - 6 分组为 (4 - 5) - 6,求值结果是 -7。右结合的运算符从右到左分组,而结合性指定为 none 的运算符完全不结合。同一优先级下非结合的运算符不能彼此相邻出现。例如,< 运算符的结合性是 none,这意味着 1 < 2 < 3 不是有效表达式。

优先级组的 assignment 指定运算符在包含可选链的运算中使用时的优先级。当设置为 true 时,相应优先级组中的运算符在可选链期间使用与 Swift 标准库中赋值运算符相同的分组规则。否则,当设置为 false 或省略时,该优先级组中的运算符遵循与不执行赋值的运算符相同的可选链规则。

Grammar of a precedence group declaration:

precedence-group-declaration → precedencegroup precedence-group-name { precedence-group-attributes? }

precedence-group-attributes → precedence-group-attribute precedence-group-attributes?
precedence-group-attribute → precedence-group-relation
precedence-group-attribute → precedence-group-assignment
precedence-group-attribute → precedence-group-associativity

precedence-group-relation → higherThan : precedence-group-names
precedence-group-relation → lowerThan : precedence-group-names

precedence-group-assignment → assignment : boolean-literal

precedence-group-associativity → associativity : left
precedence-group-associativity → associativity : right
precedence-group-associativity → associativity : none

precedence-group-names → precedence-group-name | precedence-group-name , precedence-group-names
precedence-group-name → identifier

声明修饰符

声明修饰符是用于修改声明行为或含义的关键字或上下文相关关键字。你可以在声明的属性特性(如果有)与引入声明的关键字之间,写入相应的关键字或上下文相关关键字,来指定声明修饰符。

  • class:把这个修饰符应用于类的成员,以表明该成员是类本身的成员,而不是类实例的成员。带有这个修饰符且不带 final 修饰符的超类成员可以被子类覆盖。

  • dynamic:把这个修饰符应用于类中任何可以由 Objective-C 表示的成员。当你用 dynamic 修饰符标记某个成员声明时,对该成员的访问总是使用 Objective-C 运行时进行动态派发。编译器绝不会对该成员的访问做内联或去虚化。

    由于用 dynamic 修饰符标记的声明使用 Objective-C 运行时派发,它们必须用 objc 特性标记。

  • final:把这个修饰符应用于一个类,或者类的某个属性、方法或下标成员。应用于类时,表明该类不能被继承。应用于类的属性、方法或下标时,表明该类成员不能在任何子类中被覆盖。关于如何使用 final 特性的示例,参见阻止覆盖。

  • lazy:把这个修饰符应用于类或结构体的某个存储变量属性,以表明该属性的初始值最多计算并存储一次,即在该属性第一次被访问时。关于如何使用 lazy 修饰符的示例,参见延迟存储属性。

  • optional:把这个修饰符应用于协议的属性、方法或下标成员,以表明遵循类型不必实现这些成员。

    你只能把 optional 修饰符应用于用 objc 特性标记的协议。因此,只有类类型才能采纳并遵循包含可选成员要求的协议。关于如何使用 optional 修饰符的更多信息,以及如何访问可选协议成员的指导——例如当你并不确定某个遵循类型是否实现了它们时——参见可选的协议要求。

  • required:把这个修饰符应用于类的某个指定构造器或便利构造器,以表明每个子类都必须实现该构造器。子类对该构造器的实现也必须用 required 修饰符标记。

  • static:把这个修饰符应用于结构体、类、枚举或协议的成员,以表明该成员是类型的成员,而不是该类型实例的成员。在类声明的作用域内,在成员声明上写 static 修饰符,效果等同于在该成员声明上写 class 和 final 修饰符。不过,类的常量类型属性是一个例外:在那里 static 保持其通常的非类含义,因为你不能在这些声明上写 class 或 final。

  • unowned:把这个修饰符应用于存储变量、常量或存储属性,以表明该变量或属性对它存储的对象持无主引用。如果你在对象被释放之后访问该变量或属性,会引发运行时错误。与弱引用一样,该属性或值的类型必须是类类型;与弱引用不同,该类型是非可选的。关于 unowned 修饰符的示例和更多信息,参见无主引用。

  • unowned(safe):unowned 的显式写法。

  • unowned(unsafe):把这个修饰符应用于存储变量、常量或存储属性,以表明该变量或属性对它存储的对象持无主引用。如果你在对象被释放之后访问该变量或属性,你访问的将是该对象原所在位置的内存,这是一种内存不安全的操作。与弱引用一样,该属性或值的类型必须是类类型;与弱引用不同,该类型是非可选的。关于 unowned 修饰符的示例和更多信息,参见无主引用。

  • weak:把这个修饰符应用于存储变量或存储变量属性,以表明该变量或属性对它存储的对象持弱引用。该变量或属性的类型必须是可选类类型。如果你在对象被释放之后访问该变量或属性,它的值是 nil。关于 weak 修饰符的示例和更多信息,参见弱引用。

访问控制级别

Swift 提供五个访问控制级别:open、public、internal、file private 和 private。你可以用下面的某个访问级别修饰符标记声明,来指定该声明的访问级别。访问控制在访问控制中详细讨论。

  • open:把这个修饰符应用于声明,以表明该声明可以被与它在同一模块中的代码访问和继承。用 open 访问级别修饰符标记的声明,也可以被导入了包含该声明的模块的代码访问和继承。

  • public:把这个修饰符应用于声明,以表明该声明可以被与它在同一模块中的代码访问和继承。用 public 访问级别修饰符标记的声明,也可以被导入了包含该声明的模块的代码访问(但不能继承)。

  • package:把这个修饰符应用于声明,以表明该声明只能被与它在同一个包中的代码访问。包是你在你所使用的构建系统中定义的代码分发单位。构建系统编译代码时,会通过向 Swift 编译器传入 -package-name 标志来指定包名。如果构建系统在构建两个模块时指定了相同的包名,它们就属于同一个包。

  • internal:把这个修饰符应用于声明,以表明该声明只能被与它在同一模块中的代码访问。默认情况下,大多数声明都隐式标记了 internal 访问级别修饰符。

  • fileprivate:把这个修饰符应用于声明,以表明该声明只能被与它在同一源文件中的代码访问。

  • private:把这个修饰符应用于声明,以表明该声明只能被该声明直接所在作用域内的代码访问。

出于访问控制的目的,扩展的行为如下:

  • 如果同一个文件中有多个扩展,并且这些扩展都扩展同一个类型,那么这些扩展具有相同的访问控制作用域。扩展和它们所扩展的类型可以位于不同文件中。

  • 如果有扩展与它们所扩展的类型位于同一个文件,那么这些扩展具有与该类型相同的访问控制作用域。

  • 在类型的声明中声明的私有成员,可以从该类型的扩展中访问。在一个扩展中声明的私有成员,可以从其他扩展以及被扩展类型的声明中访问。

上面每个访问级别修饰符都可以选择接受一个实参,该实参由括在圆括号中的 set 关键字组成——例如 private(set)。当你想为变量或下标指定一个不高于(小于或等于)变量或下标自身访问级别的 setter 访问级别时,使用这种形式的访问级别修饰符,如 getter 与 setter所述。

Grammar of a declaration modifier:

declaration-modifier → class | convenience | dynamic | final | infix | lazy | optional | override | postfix | prefix | required | static | unowned | unowned ( safe ) | unowned ( unsafe ) | weak
declaration-modifier → access-level-modifier
declaration-modifier → mutation-modifier
declaration-modifier → actor-isolation-modifier
declaration-modifiers → declaration-modifier declaration-modifiers?

access-level-modifier → private | private ( set )
access-level-modifier → fileprivate | fileprivate ( set )
access-level-modifier → internal | internal ( set )
access-level-modifier → package | package ( set )
access-level-modifier → public | public ( set )
access-level-modifier → open | open ( set )

mutation-modifier → mutating | nonmutating

actor-isolation-modifier → nonisolated