声明
引入类型、运算符、变量以及其他名称和构造。
声明将在程序中引入一个新的名称或构造例如,你使用声明来引入函数和方法,引入变量和常量,以及定义枚举、结构体、类和协议类型。你还可以使用声明来扩展现有具名类型的行为,并将其他地方声明的符号引入到你的程序中。
在 Swift 中,大多数声明也是定义,因为它们在声明的同时被实现或初始化。但由于协议不实现它的成员,所以协议成员在此仅仅是声明。为了方便起见,而且这种区别在 Swift 中没那么重要,所以术语声明涵盖了声明和定义两种含义。
顶级代码
Swift 源文件中的顶级代码由零个或多个语句、声明和表达式组成。默认情况下,在源文件顶层声明的变量、常量和其他具名声明可以被同一模块中每个源文件的代码访问。你可以使用访问级别修饰符来重写此默认行为,具体说明见
有两种类型的顶级代码:顶级声明和可执行的顶级代码。顶级声明仅由声明组成,允许出现在所有 Swift 源文件中。可执行的顶级代码包含语句和表达式,而不仅仅是声明,仅允许作为程序的顶级入口点。
编译 Swift 代码生成可执行文件时,无论文件和模块中的代码如何组织,都只能通过以下方法之一来指定顶级入口点:main 特性、NSApplicationMain 特性、UIApplicationMain 特性、main.swift 文件,或包含顶级可执行代码的文件。
代码块
代码块被各种声明和控制结构用来将语句组合在一起。它具有以下形式:
{
<#statements#>
}
代码块中的语句包括声明、表达式和其他类型的语句,并按它们在源代码中出现的顺序执行。
导入声明
导入声明允许你访问在当前文件之外声明的符号。基本形式是导入整个模块;它由 import 关键字后跟模块名称组成。
import <#module#>
提供更多细节可以限制导入哪些符号——可以指定特定的子模块,也可以指定模块或子模块中特定的声明。使用这种限制后,在当前作用域中,只有被导入的符号是可用的,而不是整个模块中的所有声明。
import <#import kind#> <#module#>.<#symbol name#>
import <#module#>.<#submodule#>
常量声明
常量声明会在你的程序中引入一个具名的常量值。常量声明使用 let 关键字,形式如下:
let <#constant name#>: <#type#> = <#expression#>
常量声明定义了常量名称与构造器表达式的值之间的不可变绑定;一旦常量的被赋值,就无法更改。也就是说,如果常量是用类对象初始化的,对象本身可以改变,但常量名称与它所指向的对象之间的绑定不能改变。
当常量声明在全局作用域时,常量必须赋值。当常量声明在函数或者方法的上下文中时,可以稍后初始化,只要保证在第一次读取其值之前已为其赋值。如果编译器能够证明常量的值从未被读取,则不要求该常量必须赋值。此分析称为确定初始化——编译器保证一个值在读取之前值已被赋值。
当常量声明出现在类或结构体声明的上下文中时,它被视为一个常量属性。常量声明不是计算属性,因此没有 getter 或 setter。
如果常量名称是元组形式,元组中每一项的名称都会和初始化表达式中对应的值进行绑定。
let (firstNumber, secondNumber) = (10, 42)
在这个例子中,firstNumber 是值 10 的具名常量,而 secondNumber 是值 42 的具名常量。现在这两个常量可以独立使用:
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 声明修饰符实现允许或禁止被子类重写的目的。类型属性的讨论请参见
有关常量的更多信息以及何时使用它们的指导,请参见
变量声明
变量声明会在你的程序中引入一个具名的变量值,并使用 var 关键字进行声明。
变量声明有多种形式,用于定义各种有名称的、可变的值,包括存储变量和计算变量及属性、存储变量和属性观察者,以及静态变量属性。使用哪种形式取决于变量声明的作用域以及你打算声明的变量类型。
你可以通过在子类的属性声明中标记 override 声明修饰符来重写属性,参见
存储变量和存储变量属性
以下形式声明了一个存储变量或存储变量属性:
var <#variable name#>: <#type#> = <#expression#>
你可以在全局作用域、函数的局部作用域,类或结构体声明的上下文中定义这种形式的变量声明。当这种形式的变量声明在全局作用域或函数的全局作用域内声明时,它被称为存储变量。当它在类或结构体声明的上下文中声明时,它被称为存储变量属性。
构造器表达式不能出现在协议声明中,但在其他场景下,构造器表达式是可选的。也就是说,如果没有构造器表达式,变量声明必须包含显式类型注释(: type)。
与常量声明一样,如果变量声明省略了构造器表达式,则在第一次读取该变量之前必须为其设置一个值。同样,如果变量名是一个元组模式,则元组中每个项的名称都绑定到构造器表达式中的相应值。
如其名称所示,存储变量或存储变量属性的值存储在内存中。
计算变量和计算属性
以下形式声明了一个计算变量或计算属性:
var <#variable name#>: <#type#> {
get {
<#statements#>
}
set(<#setter name#>) {
<#statements#>
}
}
你可以在全局作用域、函数的全局作用域或类、结构体、枚举或扩展声明的上下文中定义这种形式的变量声明。当这种形式的变量声明在全局作用域或函数的全局作用域内声明时,它被称为计算变量。当它在类、结构体或扩展声明的上下文中声明时,它被称为计算属性。
getter 用于读取值,setter 用于写入值。setter 子句是可选的,当只需要 getter 时,可以省略两个子句,直接返回请求的值,参见
setter 名称和括号是可选的。如果你提供了 setter 名称,它将用作 setter 参数的名称。如果你不提供 setter 名称,setter 的默认参数名称是 newValue,参见
与存储的具名值和存储的变量属性不同,计算的具名值或计算属性的值并不会存储在内存中。
有关更多信息以及查看计算属性的示例,请参见
存储变量观察者和属性观察者
你还可以使用 willSet 和 didSet 观察者声明一个存储变量或属性。带有观察者的存储变量或属性具有以下形式:
var <#variable name#>: <#type#> = <#expression#> {
willSet(<#setter name#>) {
<#statements#>
}
didSet(<#setter name#>) {
<#statements#>
}
}
你可以在全局作用域、函数的全局作用域或类或结构体声明的上下文中定义这种变量声明形式。当这种形式的变量声明在全局作用域或函数的全局作用域内声明时,观察者被称为存储变量观察者。当它在类或结构体声明的上下文中声明时,观察者被称为属性观察者。
你可以为任何存储属性添加属性观察者。你还可以通过在子类中重写属性,为任何继承自父类的属性(无论是存储的还是计算的)添加属性观察者,参见
构造器表达式在类或结构体声明的上下文中是可选的,但在其他地方是必须的。如果能通过构造器表达式推断出类型,则类型标注是可选的。通常,表达式的类型推断发生在首次读取属性时。如果在读取属性之前,初值已经被重写,则推断发生在首次写入属性时。
willSet 和 didSet 观察者提供了一种在变量或属性被赋值时的观察(和响应)方式。当变量或属性首次初始化时,观察者不会被调用。相反,它们仅在初始化上下文之外的情况下,值被设置时被调用。
willSet 观察者在变量或属性的值被设置之前被调用。新值作为常量传递给 willSet 观察者,因此新值在 willSet 子句的实现中无法更改。didSet 观察者在新值被设置后立即被调用。与 willSet 观察者不同,变量或属性的旧值会传递给 didSet 观察者,以防你仍然需要访问它。也就是说,如果你在其自己的 didSet 观察者子句中给变量或属性赋值,那么你赋的这个新值将替代刚刚设置并传递给 willSet 观察者的值。
setter 名称和 willSet 与 didSet 子句中的括号是可选的。如果提供了 setter 名称,它们将作为 willSet 和 didSet 观察者的参数名称。如果不提供 setter 名称,willSet 观察者的默认参数名称是 newValue,而 didSet 观察者的默认参数名称是 oldValue。
didSet 子句在提供 willSet 子句时是可选的。同样,在提供 didSet 子句时,willSet 子句也是可选的。
如果在 didSet 主体中引用了旧值,为了使旧值可用,在调用 didSet 之前,会先调用 getter。否则,新的值会被存储,而不调用超类的 getter。下面的示例显示了一个由超类定义并被其子类重写以添加观察者的计算属性。
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 声明修饰符标记类型计算属性,以允许子类重写超类的实现。类型属性的讨论请参见
类型别名声明
类型别名声明将现有类型的具名别名引入到你的程序中。类型别名声明使用 typealias 关键字声明,具有以下形式:
typealias <#name#> = <#existing type#>
在声明类型别名后,别名名称可以在程序中的任何地方替代现有类型使用。现有类型可以是具名类型或复合类型。类型别名不会创建新类型;它们只是允许一个名称引用现有类型。
类型别名声明可以使用泛型参数为现有的泛型类型命名。类型别名可以为现有类型的某些或所有泛型参数提供具体类型。例如:
typealias StringDictionary<Value> = Dictionary<String, Value>
// 接下来的两个字典是同一类型
var dictionary1: StringDictionary<Int> = [:]
var dictionary2: Dictionary<String, Int> = [:]
当声明一个带有泛型参数的类型别名时,这些参数的约束必须与现有类型的泛型参数的约束完全匹配。例如:
typealias DictionaryOfInts<Key: Hashable> = Dictionary<Key, Int>
因为类型别名和现有类型可以互换使用,类型别名不能引入额外的泛型约束。
类型别名可以通过省略声明中的所有泛型参数来转发现有类型的泛型参数。例如,这里声明的 Diccionario 类型别名具有与 Dictionary 相同的泛型参数和约束。
typealias Diccionario = Dictionary
在协议声明中,类型别名可以为经常使用的类型提供一个更短和更方便的名称。例如:
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。
另见
函数声明
函数声明将一个函数或方法引入到你的程序中。在类、结构体、枚举或协议的上下文中声明的函数被称为方法。函数声明使用 func 关键字声明,具有以下形式:
func <#function name#>(<#parameters#>) -> <#return type#> {
<#statements#>
}
如果函数的返回类型是 Void ,则可以省略返回类型,如下所示:
func <#function name#>(<#parameters#>) {
<#statements#>
}
因为参数类型无法被推断出来,所以函数的每个参数必须明确指定类型。如果在参数类型前写上 inout,则该参数可以在函数的作用域内被修改。关于 in-out 参数的详细讨论请参见下面的
如果一个函数声明中的语句只包含一个表达式,则默认返回该表达式的值。只有在表达式的类型和函数的返回类型不是 Void,并且不是像 Never 那样没有任何枚举值的枚举时,才会考虑这种隐式返回语法。
函数可以使用元组类型作为返回类型来返回多个值。
在函数内部,可以定义另一个函数。这种定义在函数内部的函数,被称为嵌套函数。
如果一个嵌套函数捕获了一个保证不会逃逸的值——例如一个 in-out 参数——或者作为一个非逃逸函数参数传递,那么这个嵌套函数就是非逃逸的。否则,嵌套函数就是逃逸的。
有关嵌套函数的讨论,请参见
参数名称
函数的参数是一个以逗号分隔的列表,每个参数都可以是不同的类型。函数调用时的参数顺序必须和函数声明时的参数顺序一致。最简单的参数列表有着如下的形式:
<#parameter name#>: <#parameter type#>
参数有名称和标签,名称用于在函数内访问参数,标签用于在调用函数时指定参数。默认情况下,参数的名称也可以作为标签。例如:
func f(x: Int, y: Int) -> Int { return x + y }
f(x: 1, y: 2) // x 和 y 都带有标签
你可以使用以下形式(选其一),重写参数标签的默认行为:
<#argument label#> <#parameter name#>: <#parameter type#>
_ <#parameter name#>: <#parameter type#>
在参数名称前的名称会作为这个参数的显式实参标签,它可以和参数名称不同。在函数或方法调用时,相对应的参数必须使用这个实参标签。
参数名称前的下划线(_)可以去除参数的实参标签。在函数或方法调用时,相对应的参数必须去除标签。
func repeatGreeting(_ greeting: String, count n: Int) { /* 打招呼 n 次 */ }
repeatGreeting("Hello, world!", count: 2) // count 带有标签,greeting 没有
参数修饰符
参数修饰符改变了参数传递给函数的方式。
<#argument label#> <#parameter name#>: <#parameter modifier#> <#parameter type#>
要使用参数修饰符,请在参数类型之前写 inout、borrowing 或 consuming。
func someFunction(a: inout A, b: consuming B, c: C) { ... }
In-Out 参数
默认情况下,Swift 中的函数参数是按值传递的:在函数内进行的任何更改对调用者都是不可见的。如果需要传入一个 in-out 参数,可以使用 inout 参数修饰符。
func someFunction(a: inout Int) {
a += 1
}
当调用包含 in-out 参数的函数时,必须在 in-out 参数前加上 &(与符号),以表明函数调用可以更改该参数的值。
var x = 7
someFunction(&x)
print(x) // 打印 "8"
In-out 参数的传递方式如下:
这种行为被称为拷入拷出(copy-in copy-out) 或值结果调用(call by value result)。例如,当一个计算属性或一个带观察者的属性作为 in-out 参数传递时,它的 getter 在函数调用中被调用,而它的 setter 在函数返回时被调用。
作为一种优化手段,当参数值存储在内存中的物理地址时,在函数体内部和外部均会使用同一内存位置。这种优化行为被称为引用调用(call by reference); 它满足了 copy-in copy-out 模型的所有要求,同时消除了复制的开销。请使用 copy-in copy-out 给出的模型编写代码,而不依赖于引用传递优化,以便在有或没有优化的情况下都能正确运行。
在函数内,不要访问作为 in-out 参数传递的值,即使原始值在当前作用域中可用。访问原始值是对该值的同时访问,这违反了内存独占性。
var someValue: Int
func someFunction(a: inout Int) {
a += someValue
}
// 错误:这会导致运行时排他性违规
someFunction(&someValue)
出于同样的原因,你不能将相同的值传递给多个 in-out 参数。
var someValue: Int
func someFunction(a: inout Int, b: inout Int) {
a += b
b += 1
}
// 错误:不能将同一个值传递给多个 in-out 参数
someFunction(&someValue, &someValue)
有关内存安全和内存独占的更多信息,请参见
捕获 in-out 参数的闭包或嵌套函数必须是非逃逸的。如果你需要捕获一个 in-out 参数而不对其进行修改,请使用捕获列表显式地以不可变方式捕获该参数。
func someFunction(a: inout Int) -> () -> Int {
return { [a] in return a + 1 }
}
如果你需要捕获并修改一个 in-out 参数,请使用一个显式的局部副本,例如在多线程代码中,确保所有修改在函数返回之前都已完成。
func multithreadedFunction(queue: DispatchQueue, x: inout Int) {
// 创建一个本地副本,并在函数结束时手动将其复制回去。
var localX = x
defer { x = localX }
// 异步操作 localX,然后在返回之前等待。
queue.async { someMutatingOperation(&localX) }
queue.sync {}
}
有关 in-out 参数的更多讨论和示例,请参见
借用和消费参数
默认情况下,Swift 使用一套规则在函数调用之间自动管理对象生命周期,在需要时复制值。默认规则旨在在大多数情况下最小化开销——如果你想要更具体的控制,可以应用 borrowing 或 consuming 参数修饰符。在这种情况下,使用 copy 显式标记复制操作。
无论你是否使用默认规则,Swift 确保在所有情况下对象的生命周期和所有权都得到正确管理。这些参数修饰符仅影响特定使用模式的相对效率,而不影响正确性。
borrowing 修饰函数参数时,函数不会保留参数的值。在这种情况下,调用者保留对象的所有权,并负责对象的生命周期管理。所以当函数只是临时使用对象时,用 borrowing 修饰可以最大限度地减少开销。
// `isLessThan` 不会保留任一参数
func isLessThan(lhs: borrowing A, rhs: borrowing A) -> Bool {
...
}
如果函数需要保留参数的值,例如,通过将其存储在全局变量中——你可以使用 copy 显式地复制该值。
// 同样是 `isLessThan` 函数,这个 `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 参数修饰符表示该函数拥有该值的所有权,负责在函数返回之前存储或销毁它。
// `store` 会保留它的参数,因此将其标记为 `consuming`
func store(a: consuming A) {
someGlobalVariable = a
}
使用 consuming 可以在调用者在函数调用后不再需要使用该对象时,最小化开销。
// 通常,这就是最后一次使用 value 了
store(a: value)
如果在函数调用后继续使用可复制对象,编译器会在函数调用之前自动复制该对象。
// 编译器会在这里插入一个隐式副本
store(a: someValue) // 此函数消费 someValue
print(someValue) // 这里使用的是 someValue 的副本
与 inout 不同,borrowing 和 consuming 参数在调用函数时不需要任何特殊标记:
func someFunction(a: borrowing A, b: consuming B) { ... }
someFunction(a: someA, b: someB)
显式使用 borrowing 或 consuming 表示你希望更严格地控制运行时所有权管理的开销。因为复制可能导致意外的运行时所有权操作,所以标记为这两种修饰符的参数在没有使用显式的 copy 关键字的情况下不能被复制:
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
}
特殊类型的参数
参数可以被忽略,数量可以不固定,还可以为其提供默认值,使用形式如下
_ : <#parameter type#>
<#parameter name#>: <#parameter type#>...
<#parameter name#>: <#parameter type#> = <#default argument value#>
下划线 (_) 参数被显式忽略,无法在函数体内访问。
带有基本类型名称后面紧跟三个点(...)的参数被理解为可变参数。紧跟在可变参数后面的参数必须有一个实参标签。一个函数可以有多个可变参数。可变参数被视为包含基本类型名称元素的数组。例如,可变参数 Int... 被视为 [Int]。有关使用可变参数的示例,请参见
带有等号(=)且在类型后跟随一个表达式的参数,表示该参数有一个默认值。这个给定的表达式会在函数调用时进行求值。如果在调用函数时省略了该参数,则会使用默认值。
func f(x: Int = 42) -> Int { return x }
f() // 有效,使用默认值
f(x: 7) // 有效,使用提供的值
f(7) // 无效,缺少实参标签
特殊类型的方法
枚举或结构体的方法,如果修改了 self,必须标记为 mutating 声明修饰符。
重写超类方法的方法必须标记为 override 声明修饰符。没有 override 修饰符而重写方法,或者在不重写超类方法的情况下使用 override 修饰符,都是编译时错误。
与类型相关的方法,而不是与类型实例相关的方法,必须使用 static 声明修饰符来标记(枚举和结构体使用 static,类可以使用 static 或 class 声明修饰符)。用 class 声明修饰符标记的类类型方法可以被子类的实现重写;用 class final 或 static 标记的类类型方法则不能被重写。
特殊名称的方法
一些具有特殊名称的方法为函数调用语法提供了语法糖。如果一个类型定义了这些方法之一,该类型的实例就可以使用函数调用语法。此时的函数调用会被理解为对该实例上某个特殊命名方法的调用
类、结构体或枚举类型可以通过定义一个 dynamicallyCall(withArguments:) 方法或一个 dynamicallyCall(withKeywordArguments:) 方法来支持函数调用语法,参见 dynamicCallable 特性使用的其中一个方法,则在可以使用任一方法的情况下,编译器优先选择作为函数调用的方法。
作为函数调用方法的名称为 callAsFunction(),或其他以 callAsFunction( 开头并带有有标签或无标签参数的名称——例如,callAsFunction(::) 和 callAsFunction(something:) 也是有效的作为函数调用方法名称。
以下函数调用是等效的:
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 特性的方法,并不允许你在函数调用表达式以外的任何上下文中将该类型的实例用作函数。例如:
let someFunction1: (Int, Int) -> Void = callable(_:scale:) // 错误
let someFunction2: (Int, Int) -> Void = callable.callAsFunction(_:scale:)
subscript(dynamicMember:) 下标为成员查找提供了语法糖,参见
抛出函数和方法
可以抛出错误的函数和方法必须标记 throws 关键字。这些函数和方法被称为抛出函数和抛出方法。它们具有以下形式:
func <#function name#>(<#parameters#>) throws -> <#return type#> {
<#statements#>
}
抛出特定错误类型的函数具有以下形式:
func <#function name#>(<#parameters#>) throws(<#error type#>) -> <#return type#> {
<#statements#>
}
调用抛出函数或方法的必须被包裹在一个 try 或 try! 表达式中(即,在 try 或 try! 操作符的作用域内)。
函数的类型包括:它是否会抛出错误,以及它抛出的错误类型。非抛出函数是抛出函数的子类型。所以,可以在使用抛出函数的地方使用非抛出函数。有关抛出错误函数类型的更多信息,请参阅
你不能仅根据函数是否会抛出错误来重载一个函数。不过,你可以根据函数的参数是否会抛出错误来重载函数。
抛出方法不能重写非抛出方法,且抛出方法也不能满足协议中对非抛出方法的要求。不过,非抛出方法可以重写抛出方法,且非抛出方法也可以满足协议中对会抛出方法的要求。
再抛出函数和方法
函数或方法可以使用 rethrows 关键字声明,表示它只在其某个函数参数抛出错误时才会抛出错误。这样的函数和方法被称为再抛出函数(rethrowing functions)和再抛出方法(rethrowing methods)。再抛出函数和方法必须至少有一个会抛出错误的函数参数。
func someFunction(callback: () throws -> Void) rethrows {
try callback()
}
再抛出的函数或方法只能在 catch 子句中包含 throw 语句。这使得你可以在 do-catch 语句中调用抛出函数,并通过抛出不同的错误在 catch 子句中处理错误。此外,catch 子句必须仅处理由再抛出函数的抛出参数抛出的错误。例如,以下是无效的,因为 catch 子句将处理由 alwaysThrows() 抛出的错误。
func alwaysThrows() throws {
throw SomeError.error
}
func someFunction(callback: () throws -> Void) rethrows {
do {
try callback()
try alwaysThrows() // 无效,alwaysThrows() 不是一个抛出参数
} catch {
throw AnotherError.error
}
}
抛出方法不能重写再抛出方法,抛出方法也不能满足再抛出方法的协议要求。也就是说,再抛出方法可以重写抛出方法,再抛出方法可以满足抛出方法的协议要求。
在泛型代码中,抛出特定错误类型是再抛出的替代方案。例如:
func someFunction<E: Error>(callback: () throws(E) -> Void) throws(E) {
try callback()
}
这种传播错误的方法保留了错误的类型信息。然而,与标记一个函数 rethrows 不同,这种方法并不阻止该函数抛出相同类型的错误。
异步函数和方法
以异步方式运行的函数和方法必须使用 async 关键字标记。这类函数和方法被称为异步函数和异步方法。它们的形式如下:
func <#function name#>(<#parameters#>) async -> <#return type#> {
<#statements#>
}
对异步函数或方法的调用必须包装在一个 await 表达式中——也就是说,它们必须在 await 操作符的作用域内。
async 关键字是函数类型的一部分,且同步函数是异步函数的子类型。因此,你可以在需要异步函数的上下文中使用同步函数。例如,你可以用同步方法重写异步方法,且同步方法可以满足对异步方法的协议要求。
你可以根据函数是否为异步来重载一个函数。在调用时,由上下文决定使用哪个重载:在异步上下文中,会使用异步函数,而在同步上下文中,会使用同步函数。
异步方法不能重写同步方法,且异步方法不能满足对同步方法的协议要求。不过,同步方法可以重写异步方法,且同步方法可以满足对异步方法的协议要求。
永不返回的函数
Swift 定义了一个 [Never][] 类型,表示一个函数或方法不会返回给调用者。返回类型为 Never 的函数和方法被称为非返回。非返回的函数和方法要么导致不可恢复的错误,要么开始一个无限进行的工作序列。这意味着在调用后立即运行的代码永远不会被执行。即使抛出错误的函数和再抛出错误的函数是非返回类型,它们仍然可以将程序控制权转移到相应的 catch 块。
[Never]: https://developer.apple.com/documentation/swift/never
非返回的函数或方法可以在 guard 语句的 else 分支中调用,以结束该分支,见
你可以重写一个非返回的方法,但新方法必须保持其返回类型和非返回的行为。
枚举声明
枚举声明将一个具名的枚举类型引入到你的程序中。
枚举声明有两种基本形式,使用 enum 关键字进行声明。使用任一形式声明的枚举的主体包含零个或多个值——称为枚举用例——以及任意数量的声明,包括计算属性、实例方法、类型方法、构造器、类型别名,甚至其他枚举、结构体、类和协议类型。枚举声明不能包含析构器或协议声明。
枚举类型可以采用任意数量的协议,但不能从类、结构体或其他枚举继承。
与类和结构体不同,枚举类型没有隐式提供的默认构造器;所有构造器必须显式声明。构造器可以委托给枚举中的其他构造器,但初始化过程只有在构造器将枚举的一个用例赋值给 self 后才完成。
像结构体但不同于类,枚举是值类型;当枚举的实例被赋值给变量或常量,或作为参数传递给函数调用时,会被复制。有关值类型的信息,请参见
你可以通过扩展声明扩展枚举类型的行为,如
任意类型的枚举成员
如下的形式声明了一个包含任意类型枚举用例的枚举变量:
enum <#enumeration name#>: <#adopted protocols#> {
case <#enumeration case 1#>
case <#enumeration case 2#>(<#associated value types#>)
}
这种形式的枚举声明在其他语言中有时被叫做可识别联合。
在这种形式中,每个用例块由 case 关键字开始,后面跟着一个或多个枚举用例,用逗号分隔。每个用例的名称必须是唯一的。每个用例还可以指定它存储特定类型的值。这些类型在关联值类型元组中指定,紧接在用例名称之后。
存储关联值的枚举成员可以用作函数,这些函数创建具有指定关联值的枚举实例。就像函数一样,你可以获取对枚举成员的引用,并在代码中稍后应用它。
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 声明修饰符进行标记。间接枚举成员必须具有关联值。
enum Tree<T> {
case empty
indirect case node(value: T, left: Tree, right: Tree)
}
要让一个枚举类型的所有用例都支持递归,请使用 indirect 修饰符标记整个枚举——当枚举包含许多需要标记为 indirect 修饰符的用例时,这样做非常方便。
使用 indirect 修饰符标记的枚举类型可以既包含有关联值的用例,同时还可包含没有关联值的用例。但是,它不能再单独使用 indirect 修饰符来标记某个用例。
带有原始值类型的枚举
以下形式声明了一个枚举类型,其中包含相同基本类型的枚举成员:
enum <#enumeration name#>: <#raw-value type#>, <#adopted protocols#> {
case <#enumeration case 1#> = <#raw value 1#>
case <#enumeration case 2#> = <#raw value 2#>
}
在这种形式中,每个用例块由 case 关键字开始,后面跟着一个或多个枚举用例,用逗号分隔。与第一种形式中的用例不同,每个用例都有一个基础值,称为原始值,其基本类型相同。这些值的类型在原始值类型中指定,必须表示整数、浮点数、字符串或单个字符。特别是,原始值类型必须遵循 Equatable 协议,并且遵循以下协议之一:ExpressibleByIntegerLiteral 用于整型字面量,ExpressibleByFloatLiteral 用于浮点型字面量,ExpressibleByStringLiteral 用于包含任意数量字符的字符串字面量,以及 ExpressibleByUnicodeScalarLiteral 或 ExpressibleByExtendedGraphemeClusterLiteral 用于仅包含单个字符的字符串字面量。每一个用例的名字和原始值必须唯一。
如果原始值类型被指定为 Int,并且你没有显式地为这些用例分配值,它们将隐式地被分配值 0、1、2,依此类推。每个未分配的 Int 类型的用例将隐式地被分配一个原始值,该值是从前一个用例的原始值自动递增的。
enum ExampleEnum: Int {
case a, b, c = 5, d
}
在上述示例中,ExampleEnum.a 的原始值为 0,而 ExampleEnum.b 的值为 1。由于 ExampleEnum.c 的值被显式设置为 5,因此 ExampleEnum.d 的值自动从 5 增加,结果为 6。
如果原始值类型被指定为 String,并且你没有显式地为各个用例分配值,则每个未分配的用例会隐式地分配一个与该成员名称相同文本的字符串。
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 语句的用例分支中,枚举类型会与枚举用例进行模式匹配,详见
结构体声明
结构体声明将一个具名的结构体类型引入到你的程序中。结构体声明使用 struct 关键字声明,具有以下形式:
struct <#structure name#>: <#adopted protocols#> {
<#declarations#>
}
结构体的主体包含零个或多个声明。这些声明可以包括存储属性和计算属性、类型属性、实例方法、类型方法、构造器、下标、类型别名,甚至其他结构体、类、actor 和枚举声明。结构体声明不能包含析构器或协议声明。有关包含各种类型声明的结构体的讨论和多个示例,请参见
结构体类型可以采用任意数量的协议,但不能从类、枚举或其他结构体继承。
有三种方法可以创建先前声明的结构体的实例:
初始化结构体中声明属性的过程在
结构体实例的属性可以使用点 (.) 语法访问,参见
结构体是值类型;当结构体的实例被赋值给变量或常量,或作为参数传递给函数调用时,会被复制。有关值类型的信息,请参见
你可以通过扩展声明来扩展结构体类型的行为,参见
类声明
类声明将一个具名的类类型引入到你的程序中。类声明使用 class 关键字声明,具有以下形式:
class <#class name#>: <#superclass#>, <#adopted protocols#> {
<#declarations#>
}
类的主体包含零个或多个声明。这些声明可以包括存储属性和计算属性、实例方法、类型方法、构造器、一个析构器、下标、类型别名,甚至其他类、结构体、actor 和枚举声明。类声明不能包含协议声明。有关包含各种类型声明的类的讨论和多个示例,请参见
类类型只能继承自一个父类,即它的超类,但可以采用任意数量的协议。超类在类名和冒号之后首先出现,后面跟着任何采用的协议。泛型类可以继承其他泛型和非泛型类,但非泛型类只能继承其他非泛型类。当你在冒号后写泛型超类的名称时,必须包括该泛型类的全名,包括其泛型参数子句。
如在
类可以重写其超类的属性、方法、下标和构造器。重写的属性、方法、下标和指定构造器必须标记为 override 声明修饰符。
要要求子类实现超类的构造器,请使用 required 声明修饰符标记超类的构造器。子类对该构造器的实现也必须使用 required 声明修饰符进行标记。
尽管在超类中声明的属性和方法被当前类继承,但在超类中声明的指定构造器仅在子类满足
有两种方法可以创建一个先前声明的类的实例:
使用点(.)语法访问类实例的属性,参见
类是引用类型;当类的实例被赋值给变量或常量,或作为参数传递给函数调用时,是引用而不是复制。有关引用类型的信息,请参见
你可以通过扩展声明扩展类类型的行为,参见
Actor 声明
actor 声明将一个具名的 actor 类型引入到你的程序中。actor 声明使用 actor 关键字声明,具有以下形式:
actor <#actor name#>: <#adopted protocols#> {
<#declarations#>
}
actor 的主体包含零个或多个声明。这些声明可以包括存储属性和计算属性、实例方法、类型方法、构造器、一个析构器、下标、类型别名,甚至其他类、结构体和枚举声明。有关包含各种声明的 actor 的讨论和多个示例,请参见
actor 类型可以采用任意数量的协议,但不能从类、枚举、结构体或其他 actor 继承。然而,标记为 @objc 特性的 actor 隐式地遵循 NSObjectProtocol 协议,并作为 NSObject 的子类型暴露给 Objective-C 运行时。
有两种方法可以创建一个先前声明的 actor 的实例:
默认情况下,actor 的成员是与该 actor 隔离的。方法体或属性的 getter 等代码是在该 actor 上执行的。actor 内部的代码可以同步地与这些成员交互,因为代码已经在同一个 actor 上运行;但 actor 外部的代码必须使用 await 标记,以表明该代码是异步地在另一个 actor 上运行的。键路径不能引用 actor 的隔离成员。actor 隔离的存储属性可以作为 in-out 参数传递给同步函数,但不能传递给异步函数。
actor 还可以拥有非隔离成员,其声明使用 nonisolated 关键字标记。非隔离成员的执行方式类似于 actor 外部的代码:它无法与 actor 的任何隔离状态交互,调用者在使用时也不需要使用 await 进行标记。
actor 的成员只有在它们是非隔离或异步的情况下才能标记为 @objc 属性。
初始化 actor 中声明的属性的过程,参见
actor 实例的属性可以使用点 (.) 语法访问,参见
actor 是引用类型;当分配给变量或常量,或作为参数传递给函数调用时,actor 的实例是被引用而不是复制。有关引用类型的信息,请参见
你可以通过扩展声明扩展 actor 类型的行为,参见
协议声明
协议声明将一个具名的协议类型引入到你的程序中。协议声明使用 protocol 关键字声明,具有以下形式:
protocol <#protocol name#>: <#inherited protocols#> {
<#protocol member declarations#>
}
协议声明可以出现在全局作用域内,或嵌套在非泛型类型或非泛型函数内部。
协议的主体包含零个或多个协议成员声明,这些声明描述了任何采用该协议的类型必须满足的遵循性要求。特别是,协议可以声明遵循的类型必须实现某些属性、方法、构造器和下标。协议还可以声明特殊类型的类型别名,称为关联类型,可以指定协议中各种声明之间的关系。协议声明不能包含类、结构体、枚举或其他协议声明。协议成员声明将在下面详细讨论。
协议类型可以从任意数量的其他协议继承。当一个协议类型从其他协议继承时,这些其他协议的要求集合会被聚合,任何从当前协议继承的类型必须遵循所有这些要求。有关如何使用协议继承的示例,请参见
你可以通过在该类型的扩展声明中采用协议,为先前声明的类型添加协议遵循性。在扩展中,你必须实现所采用协议的所有要求。如果该类型已经实现了所有要求,你可以将扩展声明的主体留空。
默认情况下,遵循协议的类型必须实现协议中声明的所有属性、方法和下标。也就是说,你可以使用 optional 声明修饰符来标记这些协议成员声明,以指定遵循类型的实现是可选的。optional 修饰符只能应用于标记为 objc 特性的成员,并且只能应用于标记为 objc 特性的协议成员。因此,只有类类型可以采用并遵循包含可选成员要求的协议。有关如何使用 optional 声明修饰符的信息,以及如何访问可选协议成员的指导——例如,当你不确定遵循类型是否实现它们时——请参见
枚举的用例可以满足类型成员的协议要求。具体来说,没有任何关联值的枚举用例满足类型 Self 的只读类型变量的协议要求,而具有关联值的枚举成员满足返回 Self 的函数的协议要求,该函数的参数及其实参标签与枚举成员的关联值匹配。例如:
protocol SomeProtocol {
static var someValue: Self { get }
static func someFunction(x: Int) -> Self
}
enum MyEnum: SomeProtocol {
case someValue
case someFunction(x: Int)
}
要将协议的采用限制为类类型,只需在冒号后将 AnyObject 协议包含在继承协议列表中。例如,以下协议只能被类类型采用:
protocol SomeProtocol: AnyObject {
/* 协议成员写在这里 */
}
任何从标记为 AnyObject 要求的协议继承的协议,也只能被类类型采用。
协议是具名类型,因此它们可以出现在代码中与其他具名类型相同的位置,如
你可以使用协议来声明类或结构体的代理应该实现哪些方法,参见
协议属性声明
协议通过在协议声明体中包含一个协议属性声明,规定遵循该协议的类型必须实现一个属性。协议属性声明是一种特殊形式的变量声明,格式如下:
var <#property name#>: <#type#> { get set }
与其他协议成员声明一样,这些属性声明仅声明遵循该协议的类型的 getter 和 setter 要求。因此,你不会在声明它的协议中直接实现 getter 或 setter。
遵循协议的类型可以通过多种方式满足 getter 和 setter 的要求。如果属性声明同时包含 get 和 set 关键字,遵循类型可以用存储变量属性或可读写的计算属性(即实现了 getter 和 setter 的属性)来实现。然而,这样的属性声明不能被实现为常量属性或只读计算属性。如果属性声明只包含 get 关键字,则可以实现为任何类型的属性。关于符合协议类型如何实现属性要求的示例,参见
在协议声明中声明类型属性要求时,使用 static 关键字标记属性声明。遵循该协议的结构体和枚举使用 static 关键字声明属性,而遵循该协议的类则可以使用 static 或 class 关键字声明属性。为结构体、枚举或类添加协议遵循的扩展使用与其扩展的类型相同的关键字。为类型属性要求提供默认实现的扩展使用 static 关键字。
另见
协议方法声明
协议通过在协议声明体中包含一个协议方法声明,规定遵循该协议的类型必须实现一个方法。协议方法声明的形式与函数声明相同,但有两个例外:它们不包含函数体,且不能在函数声明中提供任何默认参数值。关于遵循协议类型如何实现方法要求的示例,参见
在协议声明中声明类或静态方法的要求时,使用 static 修饰符标记方法声明。遵循该协议的结构体和枚举使用 static 关键字声明该方法,而遵循该协议的类则使用 static 或 class 关键字声明该方法。为结构体、枚举或类添加协议遵循的扩展使用与其扩展的类型相同的关键字。为类型方法要求提供默认实现的扩展使用 static 关键字。
另见
协议构造器声明
协议通过在协议声明的主体中包含协议构造器声明,要求遵循的类型必须实现一个构造器。协议构造器声明的形式与构造器声明相同,只是不包括构造器的主体。
遵循类型可以通过实现一个非可失败构造器或一个 init! 可失败构造器来满足非可失败协议构造器的要求。一个遵循类型可以通过实现任何类型的构造器来满足可失败协议构造器的要求。
当一个类实现一个构造器以满足协议的构造器要求时,如果该类尚未标记为 final 声明修饰符,则构造器必须标记为 required 声明修饰符。
另见
协议下标声明
协议声明遵循的类型必须通过在协议声明的主体中包含协议下标声明来实现下标。协议下标声明具有下标声明的特殊形式:
subscript (<#parameters#>) -> <#return type#> { get set }
下标声明仅声明遵循协议的类型所需的最小 getter 和 setter 实现要求。如果下标声明同时包含 get 和 set 关键字,则遵循的类型必须实现 getter 和 setter 子句。如果下标声明仅包含 get 关键字,则遵循的类型必须实现至少一个 getter 子句,并且可以选择性地实现一个 setter 子句。
在协议声明中声明静态下标要求时,使用 static 声明修饰符标记下标声明。遵循该协议的结构体和枚举使用 static 关键字声明下标,而遵循该协议的类则使用 static 或 class 关键字声明下标。为结构体、枚举或类添加协议遵循性的扩展使用与其扩展的类型相同的关键字。为静态下标要求提供默认实现的扩展使用 static 关键字。
另见
协议关联类型声明
协议使用 associatedtype 关键字声明关联类型。关联类型为作为协议声明一部分使用的类型提供了别名。关联类型类似于泛型参数子句中的类型参数,但它们与声明它们的协议中的 Self 相关联。在该上下文中,Self 指的是遵循该协议的最终类型。有关更多信息和示例,请参见
你在协议声明中使用通用的 where 子句,以便为从另一个协议继承的关联类型添加约束,而无需重新声明关联类型。以下 SubProtocol 的声明是等效的:
protocol SomeProtocol {
associatedtype SomeType
}
protocol SubProtocolA: SomeProtocol {
// 此语法会产生警告。
associatedtype SomeType: Equatable
}
// 推荐使用此语法。
protocol SubProtocolB: SomeProtocol where SomeType: Equatable { }
另见
构造器声明
构造器声明在你的程序中引入了一个类、结构体或枚举的构造器。构造器声明使用 init 关键字声明,有两种基本形式。
结构体、枚举和类类型可以有任意数量的构造器,但是类的构造器具有不同的规则和行为。与结构体和枚举不同,类有两种类型的构造器:指定构造器和便利构造器,参见
以下形式声明了结构体和枚举的构造器,以及类的指定构造器::
init(<#parameters#>) {
<#statements#>
}
类的指定构造器直接初始化类的所有属性。它不能调用当前类的其他构造器,如果该类有一个超类,则必须调用超类的一个指定构造器。如果该类从其超类继承了任何属性,则在当前类中设置或修改这些属性之前,必须调用超类的一个指定构造器。
指定构造器只能在类声明中声明,因此不能通过扩展声明添加到类中。
结构体和枚举中的构造器可以调用其他已声明的构造器,以委托部分或全部初始化过程。
要为一个类声明便利构造器,请使用 convenience 声明修饰符标记构造器声明。
convenience init(<#parameters#>) {
<#statements#>
}
便利构造器可以将构造过程委托给另一个便利构造器或一个指定构造器。但是,类的构造过程必须以一个将类中所有属性完全初始化的指定构造器的调用作为结束。便利构造器不能调用超类的构造器
你可以使用 required 声明修饰符标记指定和便利构造器,以要求每个子类实现该构造器。子类对该构造器的实现也必须标记为 required 声明修饰符。
默认情况下,超类中声明的构造器不会被子类继承。也就是说,如果子类用默认值初始化了所有存储属性,并且没有定义自己的构造器,它将继承超类的所有构造器。如果子类重写了超类的所有指定构造器,它将继承超类的便利构造器。
与方法、属性和下标一样,你需要使用 override 声明修饰符标记重写的指定构造器。
就像函数和方法一样,构造器可以抛出或再抛出错误。与函数和方法一样,你在构造器的参数后使用 throws 或 rethrows 关键字来指示适当的行为。同样,构造器可以是异步的,你使用 async 关键字来指示这一点。
要查看各种类型声明中构造器的示例,请参见
可失败的构造器
可失败的构造器是一种生成一个可选实例或一个隐式解包的可选实例的构造器,具体取决于构造器声明的类型。因此,可失败的构造器可以返回 nil 以表示初始化失败。
要声明一个可失败的构造器并生成一个可选实例,需要在构造器声明中的 init 关键字后面加上问号(init?)。要声明一个可失败构造器并生成一个隐式解包的可选实例,则需要加上感叹号(init!)。下面的示例展示了一个 init? 可失败的构造器,它生成了一个结构体的可选实例。
struct SomeStruct {
let property: String
// 生成一个 `SomeStruct` 可选实例
init?(input: String) {
if input.isEmpty {
// 丢弃 'self' 并返回 'nil'
return nil
}
property = input
}
}
调用 init? 可失败的构造器与调用不可失败的构造器的方式相同,只是你必须处理结果的可选性。
if let actualInstance = SomeStruct(input: "Hello") {
// 使用 'SomeStruct' 的实例执行操作
} else {
// 'SomeStruct' 的初始化失败,初始化器返回了 'nil'
}
可失败的构造器可以在构造器主体的实现中的任何时刻返回 nil。
可失败的构造器可以委托给任何类型的构造器。不可失败的构造器可以委托给另一个不可失败的构造器或一个 init! 可失败的构造器。不可失败的构造器可以通过强制解包超类构造器的结果来委托给一个 init? 可失败的构造器——例如,通过写 super.init()!。
初始化失败会通过构造器委托传播。具体来说,如果一个可失败的构造器委托给一个失败并返回 nil 的构造器,那么委托的构造器也会失败并隐式返回 nil。如果一个不可失败的构造器委托给一个失败并返回 nil 的 init! 可失败构造器,那么会引发运行时错误(就像你使用!运算符来解包一个值为 nil的可选值一样)。
可失败的指定构造器可以在子类中被任何类型的指定构造器重写。不可失败的指定构造器只能在子类中被不可失败的指定构造器重写。
有关更多信息以及可失败构造器的示例,请参见
析构器声明
析构器声明为类类型声明一个析构器。析构器不接受参数,具有以下形式:
deinit {
<#statements#>
}
析构器在一个类对象不再有任何引用时,会在该对象被释放之前自动调用。析构器只能在类声明的主体内声明,不能在类的扩展中声明,并且每个类最多只能有一个析构器
子类继承其超类的析构器,该析构器在子类对象被释放之前隐式调用。子类对象在其继承链中的所有析构器执行完毕之前不会被释放。
析构器不会被直接调用。
在类声明中如何使用析构器的示例,请参见
扩展声明
扩展声明允许你扩展现有类型的行为。扩展声明使用 extension 关键字声明,具有以下形式:
extension <#type name#> where <#requirements#> {
<#declarations#>
}
扩展声明的主体包含零个或多个声明。这些声明可以包括计算属性、计算类型属性、实例方法、类型方法、构造器、下标声明,甚至类、结构体和枚举声明。扩展声明不能包含析构器或协议声明、存储属性、属性观察者或其他扩展声明。协议扩展中的声明不能标记为 final。有关包含各种类型声明的扩展的讨论和多个示例,请参见
如果类型名称是类、结构体或枚举类型,则扩展该类型。如果类型名称是协议类型,则扩展所有遵循该协议的类型。
扩展声明可以扩展具有关联类型的泛型类型或协议,并可以包含要求。如果扩展类型的实例或遵循扩展协议的类型的实例满足要求,则该实例获得声明中指定的行为。
扩展声明可以包含构造器声明。也就是说,如果你正在扩展的类型在另一个模块中定义,则构造器声明必须委托给该模块中已定义的构造器,以确保该类型的成员得到正确初始化。
现有类型的属性、方法和构造器不能在该类型的扩展中被重写。
扩展声明可以通过指定采用的协议,为现有的类、结构体或枚举类型添加协议遵循:
extension <#type name#>: <#adopted protocols#> where <#requirements#> {
<#declarations#>
}
扩展声明不能为现有类添加类继承,因此你只能在类型名称和冒号后指定协议列表。
条件遵循
你可以扩展一个泛型类型以有条件地遵循一个协议,从而使该类型的实例仅在满足某些要求时遵循该协议。你通过在扩展声明中包含要求来添加对协议的条件遵循。
重写的要求在某些泛型上下文中不会被使用
在某些泛型上下文中,通过条件遵循协议而获得行为的类型,并不总是使用该协议要求的特定实现。为了说明这种行为,以下示例定义了两个协议和一个有条件地遵循这两个协议的泛型类型。
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 时,也会相应地遵循 Loggable 和 TitledLoggable。在下面的示例中,oneAndTwo 是 Pair 的一个实例,由于 String 遵循 TitledLoggable,因此 oneAndTwo 也遵循 TitledLoggable。当直接调用 oneAndTwo 的 log() 方法时,将使用包含标题字符串的特定版本。
let oneAndTwo = Pair(first: "one", second: "two")
oneAndTwo.log()
// 打印 "Pair of 'String': (one, two)"
然而,当在泛型上下文中使用 oneAndTwo 或将其作为 Loggable 协议的一个实例时,特定的实现版本不会被使用。Swift 在选择调用哪个 log() 实现时,只参考 Pair 遵循 Loggable 所需的最低要求。因此,使用的是 Loggable 协议提供的默认实现。
func doSomething<T: Loggable>(with x: T) {
x.log()
}
doSomething(with: oneAndTwo)
// 打印 "(one, two)"
当在传递给 doSomething(_:) 的实例上调用 log() 时,自定义标题会从日志字符串中省略。
协议的遵循不应冗余
具体类型只能遵从某个协议一次。Swift 会将多余的协议遵从标记为错误。你可能会在两种情况下遇到这种错误。第一种情况是,当你以不同的要求多次显式地遵从同一个协议。第二种情况是,当你多次隐式地继承同一个协议。以下部分将讨论这些情况。
解决显式冗余
对一个具体类型的多个扩展不能添加对同一协议的遵循,即使这些扩展的要求是互斥的。以下示例展示了这一限制。两个扩展声明试图为 Serializable 协议添加条件遵循,一个是针对包含 Int 元素的数组,另一个是针对包含 String 元素的数组。
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' 的遵循是多余的
如果你需要根据多个具体类型添加条件遵循,请创建一个每个类型都可以遵循的新协议,并在声明条件遵循时使用该协议作为要求。
protocol SerializableInArray { }
extension Int: SerializableInArray { }
extension String: SerializableInArray { }
extension Array: Serializable where Element: SerializableInArray {
func serialize() -> Any {
// 实现
}
}
解决隐式冗余
当一个具体类型有条件地遵循一个协议时,该类型隐式地遵循任何具有相同要求的父协议。
如果你需要一个类型有条件地遵循两个继承自单一父协议的协议,请显式声明对父协议的遵循。这可以避免以不同的要求隐式地两次遵循父协议。
以下示例显式声明了 Array 对 Loggable 的条件遵循,以避免在声明其对 TitledLoggable 和新的 MarkedLoggable 协议的条件遵循时发生冲突。
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 扩展会隐式创建这些声明,从而导致错误:
extension Array: Loggable where Element: TitledLoggable { }
extension Array: Loggable where Element: MarkedLoggable { }
// 错误:'Array<Element>' 对协议 'Loggable' 的遵循是多余的
下标声明
下标声明允许你为特定类型的对象添加下标支持,通常用于提供一种方便的语法来访问集合、列表或序列中的元素。下标声明使用 subscript 关键字声明,具有以下形式:
subscript (<#parameters#>) -> <#return type#> {
get {
<#statements#>
}
set(<#setter name#>) {
<#statements#>
}
}
下标声明只能出现在类、结构体、枚举、扩展或协议声明的上下文中。
参数指定了在下标表达式中用于访问对应类型元素的一个或多个索引(例如,在表达式 object[i] 中的 i)。尽管用于访问元素的索引可以是任意类型,但每个参数都必须包含一个类型注释,以指定每个索引的类型。返回类型指定了被访问元素的类型。
与计算属性一样,下标声明支持读取和写入所访问元素的值。getter 用于读取值,setter 用于写入值。setter 子句是可选的,当只需要 getter 时,可以省略两个子句,直接返回请求的值。也就是说,如果提供了 setter 子句,则必须同时提供 getter 子句。
setter 名称和括号是可选的。如果你提供了 setter 名称,它将用作 setter 的参数名称。如果你不提供 setter 名称,setter 的默认参数名称是 value。setter 的参数类型与返回类型相同。
你可以在声明其类型的地方重载下标声明,只要参数或返回类型与要重载的下标不同。你也可以重写从超类继承的下标声明。在这样做时,必须使用 override 声明修饰符标记被重写的下标声明。
下标参数遵循与函数参数相同的规则,但有两个例外。默认情况下,使用下标的参数没有参数标签,这与函数、方法和构造器不同。然而,你可以使用与函数、方法和构造器相同的语法提供显式参数标签。此外,下标不能有 in-out 参数。下标参数可以具有默认值,参见
你还可以在协议声明的上下文中声明下标,参见
有关下标的更多信息以及下标声明的示例,请参见
类型下标声明
要声明由类型本身而非类型实例公开的下标,可以在下标声明中使用 static 声明修饰符。类可以使用 class 声明修饰符来标记类型计算属性,以允许子类重写超类的实现。在类声明中,static 关键字的效果与将声明标记为 class 和 final 声明修饰符相同。
宏声明
宏声明引入一个新的宏。它以 macro 关键字开始,具有以下形式:
macro <#name#> = <#macro implementation#>
宏实现是另一个宏,用于指示执行此宏扩展的代码位置。执行宏扩展的代码是一个独立的 Swift 程序,该程序使用 [SwiftSyntax][] 模块与 Swift 代码进行交互。调用 Swift 标准库中的 externalMacro(module:type:) 宏,并传入包含宏实现的类型名称以及包含该类型的模块名称。
[SwiftSyntax]: https://github.com/swiftlang/swift-syntax
宏可以被重载,遵循与函数相同的模型。宏声明仅在文件作用域内出现。
有关 Swift 中宏的概述,请参见
运算符声明
运算符声明将新的中缀、前缀或后缀运算符引入到你的程序中,并使用 operator 关键字进行声明。
你可以声明三种不同优先级的运算符:中缀、前缀和后缀。运算符的优先级指定了运算符相对于其操作数的相对位置。
运算符声明有三种基本形式,每种形式对应一种结合性。运算符的结合性通过在 operator 关键字之前标注 infix、prefix 或 postfix 声明修饰符来指定。在每种形式中,运算符的名称只能包含
以下形式声明了一个新的中缀运算符:
infix operator <#operator name#>: <#precedence group#>
中缀运算符是一个二元运算符,它写在两个操作数之间,例如在表达式 1 + 2 中熟悉的加法运算符+。
中缀运算符可以选择性地指定优先级组。如果你省略运算符的优先级组,Swift 将使用默认优先级组 DefaultPrecedence,该组的优先级仅高于 TernaryPrecedence。有关更多信息,请参见
以下形式声明了一个新的前缀运算符:
prefix operator <#operator name#>
前缀运算符是一种一元运算符,它直接写在操作数之前,例如表达式 !a 中的前缀逻辑非运算符(!)。
前缀运算符声明不指定优先级。前缀运算符是非结合的。
以下形式声明了一个新的后缀运算符:
postfix operator <#operator name#>
后缀运算符是一种一元运算符,它紧跟在操作数后面,例如在表达式 a! 中的后缀强制解包运算符 !。
与前缀运算符一样,后缀运算符声明不指定优先级。后缀运算符是非结合的。
在声明一个新运算符后,你通过声明一个与运算符同名的静态方法来实现它。这个静态方法是运算符作为参数所接受的类型之一的成员——例如,一个将 Double 乘以 Int 的运算符是作为 Double 或 Int 结构上的静态方法实现的。如果你在实现前缀或后缀运算符,你还必须在方法声明中标记相应的 prefix 或 postfix 声明修饰符。要查看如何创建和实现新运算符的示例,请参见
优先级组声明
优先级组声明在程序中引入了一个新的中缀运算符优先级分组。运算符的优先级指定了在没有分组括号的情况下,运算符与其操作数的绑定紧密程度。
优先级组声明具有以下形式:
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 不是一个有效的表达式。
赋值优先级组的设置指定了运算符在包含可选链操作中的优先级。当设置为 true 时,对应优先级组中的运算符在可选链操作期间使用与 Swift 标准库中的赋值运算符相同的分组规则。否则,当设置为 false 或省略时,该优先级组中的运算符将遵循与不执行赋值的运算符相同的可选链规则。
声明修饰符
声明修饰符 是用于修改声明行为或意义的关键字或上下文相关关键字。你通过在声明的属性(如果有的话)和引入声明的关键字之间写入相应的关键字或上下文相关关键字来指定声明修饰符。
因为带有 dynamic 修饰符的声明是通过 Objective-C 运行时进行调度的,因此它们必须标记为 objc 属性。
你只能将 optional 修饰符应用于带有 objc 属性的协议。因此,只有类类型可以采用并遵循包含可选成员要求的协议。有关如何使用 optional 修饰符的更多信息,以及在不确定遵循类型是否实现了这些成员时如何访问可选协议成员的指导,请参见
访问控制级别
Swift 提供五种访问控制级别:open、public、internal、file private 和 private。你可以使用以下访问级别修饰符之一标记声明,以指定声明的访问级别。访问控制的详细信息请参见
出于访问控制的目的,扩展的行为如下:
每个上述访问级别修饰符可选择性地接受一个参数,该参数由括号中包含的 set 关键字组成——例如,private(set)。当你想要为变量或下标的setter指定一个小于或等于变量或下标本身的访问级别时,请使用这种形式的访问级别修饰符,如