3.4 表达式

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

3.4 表达式

访问、修改和赋值。

在 Swift 中,表达式有四类:前置表达式、中置表达式、主表达式和后置表达式。对表达式求值会返回一个值、产生副作用,或者两者兼有。

前置表达式和中置表达式让你把运算符应用于更小的表达式。主表达式在概念上是最简单的一类表达式,它们提供访问值的方式。后置表达式与前置、中置表达式一样,让你用函数调用、成员访问之类的后置部分构建更复杂的表达式。下面各节将详细描述每一类表达式。

Grammar of an expression:

expression → try-operator? await-operator? prefix-expression infix-expressions?

前置表达式

前置表达式把一个可选的前置运算符与一个表达式组合起来。前置运算符接受一个实参,即紧跟其后的表达式。

关于这些运算符行为的内容,参见基本运算符和高级运算符。

关于 Swift 标准库提供的运算符,参见 Operator Declarations。

Grammar of a prefix expression:

prefix-expression → prefix-operator? postfix-expression
prefix-expression → in-out-expression

输入输出表达式

输入输出表达式标记作为实参以输入输出方式传给函数调用表达式的变量。

1
&<#expression#>

关于输入输出参数的更多内容以及示例,参见输入输出参数。

在需要指针的上下文中提供非指针实参时,也会使用输入输出表达式,详见到指针类型的隐式转换。

Grammar of an in-out expression:

in-out-expression → & primary-expression

try 运算符

try 表达式由 try 运算符后跟一个可能抛出错误的表达式构成,其形式如下:

1
try <#expression#>

try 表达式的值就是该表达式的值。

可选 try 表达式由 try? 运算符后跟一个可能抛出错误的表达式构成,其形式如下:

1
try? <#expression#>

如果该表达式没有抛出错误,可选 try 表达式的值就是一个包含该表达式值的可选值;否则,可选 try 表达式的值是 nil。

强制 try 表达式由 try! 运算符后跟一个可能抛出错误的表达式构成,其形式如下:

1
try! <#expression#>

强制 try 表达式的值就是该表达式的值;如果该表达式抛出错误,就会产生运行时错误。

当中置运算符左侧的表达式被标记了 try、try? 或 try! 时,该运算符作用于整个中置表达式。话虽如此,你可以用圆括号明确指定该运算符的作用范围。

1
2
3
4
5
6
7
8
// 写在前面的 'try' 作用于两次函数调用。
sum = try someThrowingFunction() + anotherThrowingFunction()

// 写在前面的 'try' 作用于两次函数调用。
sum = try (someThrowingFunction() + anotherThrowingFunction())

// 错误:这样写的 'try' 只作用于第一次函数调用。
sum = (try someThrowingFunction()) + anotherThrowingFunction()

try 表达式不能出现在中置运算符的右侧,除非该中置运算符是赋值运算符,或者该 try 表达式被圆括号括起来。

如果某个表达式同时包含 try 和 await 运算符,try 运算符必须写在前面。

关于如何使用 try、try? 和 try! 的更多内容与示例,参见错误处理。

Grammar of a try expression:

try-operator → try | try ? | try !

await 运算符

await 表达式由 await 运算符后跟一个使用异步操作结果的表达式构成,其形式如下:

1
await <#expression#>

await 表达式的值就是该表达式的值。

被标记了 await 的表达式称为潜在挂起点。异步函数的执行可能在每个被标记 await 的表达式处挂起;此外,并发代码的执行不会在任何其他位置被挂起。这意味着,潜在挂起点之间的代码可以安全地更新那些需要临时破坏不变式的状态,只要在下一次潜在挂起点之前完成更新即可。

await 表达式只能出现在异步上下文中,例如传给 async(priority:operation:) 函数的尾随闭包。它不能出现在 defer 语句体中,也不能出现在同步函数类型的自动闭包中。

当中置运算符左侧的表达式被标记了 await 运算符时,该运算符作用于整个中置表达式。话虽如此,你可以用圆括号明确指定该运算符的作用范围。

1
2
3
4
5
6
7
8
// 写在前面的 'await' 作用于两次函数调用。
sum = await someAsyncFunction() + anotherAsyncFunction()

// 写在前面的 'await' 作用于两次函数调用。
sum = await (someAsyncFunction() + anotherAsyncFunction())

// 错误:这样写的 'await' 只作用于第一次函数调用。
sum = (await someAsyncFunction()) + anotherAsyncFunction()

await 表达式不能出现在中置运算符的右侧,除非该中置运算符是赋值运算符,或者该 await 表达式被圆括号括起来。

如果某个表达式同时包含 await 和 try 运算符,try 运算符必须写在前面。

Grammar of an await expression:

await-operator → await

中置表达式

中置表达式把一个中置二元运算符与它作为左右实参接受的表达式组合起来,其形式如下:

1
<#left-hand argument#> <#operator#> <#right-hand argument#>

关于这些运算符行为的内容,参见基本运算符和高级运算符。

关于 Swift 标准库提供的运算符,参见 Operator Declarations。

注意:在解析阶段, 由中置运算符构成的表达式被表示为一张扁平的列表, 随后通过应用运算符优先级把它变换成一棵树。 例如表达式 2 + 3 * 5 最初被理解成由 2、+、3、*、5 五项组成的扁平列表, 经过这一过程后被变换成树 (2 + (3 * 5))。

Grammar of an infix expression:

infix-expression → infix-operator prefix-expression
infix-expression → assignment-operator try-operator? await-operator? prefix-expression
infix-expression → conditional-operator try-operator? await-operator? prefix-expression
infix-expression → type-casting-operator
infix-expressions → infix-expression infix-expressions?

赋值运算符

赋值运算符为给定的表达式设置新值,其形式如下:

1
<#expression#> = <#value#>

该表达式的值被设为对值求值所得的结果。如果该表达式是元组,值必须是元素个数相同的元组(允许嵌套元组)。赋值会从值的每一部分执行到该表达式的对应部分。例如:

1
2
(a, _, (b, c)) = ("test", 9.45, (12, 3))
// a 是 "test",b 是 12,c 是 3,9.45 被忽略

赋值运算符不返回任何值。

Grammar of an assignment operator:

assignment-operator → =

三元条件运算符

三元条件运算符根据条件的取值求值为两个给定值之一,其形式如下:

1
<#condition#> ? <#expression used if true#> : <#expression used if false#>

如果条件求值为 true,条件运算符就对第一个表达式求值并返回其值;否则对第二个表达式求值并返回其值。没有被用到的那一个表达式不会被求值。

使用三元条件运算符的示例参见三元条件运算符。

Grammar of a conditional operator:

conditional-operator → ? expression :

类型转换运算符

类型转换运算符有四个:is 运算符、as 运算符、as? 运算符和 as! 运算符。

它们的形式如下:

1
2
3
4
<#expression#> is <#type#>
<#expression#> as <#type#>
<#expression#> as? <#type#>
<#expression#> as! <#type#>

is 运算符在运行时检查该表达式能否转换成指定的类型:可以转换就返回 true,否则返回 false。

当编译期已知转换总会成功时(例如向上转换或桥接),as 运算符执行转换。向上转换让你把表达式当作其类型的父类型的实例使用,而无需中间变量。下面几种做法是等价的:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
func f(_ any: Any) { print("Function for Any") }
func f(_ int: Int) { print("Function for Int") }
let x = 10
f(x)
// 输出 "Function for Int"。

let y: Any = x
f(y)
// 输出 "Function for Any"。

f(x as Any)
// 输出 "Function for Any"。

桥接让你可以把 String 这样的 Swift 标准库类型表达式当作其对应的 Foundation 类型(例如 NSString)使用,而不必创建新实例。关于桥接的更多内容,参见 Working with Foundation Types。

as? 运算符对该表达式执行到指定类型的条件转换,返回指定类型的可选值:运行时如果转换成功,该表达式的值被包在可选值中返回;否则返回 nil。如果转换成指定类型必定失败或必定成功,就会产生编译期错误。

as! 运算符对该表达式执行到指定类型的强制转换,返回指定类型的值(不是可选类型)。如果转换失败,就会产生运行时错误。x as! T 的行为与 (x as? T)! 相同。

关于类型转换的更多内容与使用类型转换运算符的示例,参见类型转换。

Grammar of a type-casting operator:

type-casting-operator → is type
type-casting-operator → as type
type-casting-operator → as ? type
type-casting-operator → as ! type

主表达式

主表达式是最基本的一类表达式:它们既可以单独作为表达式使用,也可以与其他词法单元组合,构成前置表达式、中置表达式和后置表达式。

Grammar of a primary expression:

primary-expression → identifier generic-argument-clause?
primary-expression → literal-expression
primary-expression → self-expression
primary-expression → superclass-expression
primary-expression → conditional-expression
primary-expression → closure-expression
primary-expression → parenthesized-expression
primary-expression → tuple-expression
primary-expression → implicit-member-expression
primary-expression → wildcard-expression
primary-expression → macro-expansion-expression
primary-expression → key-path-expression
primary-expression → selector-expression
primary-expression → key-path-string-expression

字面表达式

字面表达式由普通字面量(例如字符串或数字)、数组或字典字面量,或者 playground 字面量构成。

注意: 在 Swift 5.9 之前, 下列特殊字面量是被认可的: #column、 #dsohandle、 #fileID、 #filePath、 #file、 #function 和 #line。 它们现在作为宏实现在 Swift 标准库中: column()、 dsohandle()、 fileID()、 filePath()、 file()、 function() 和 line()。

数组字面量是一个有序的值集合,其形式如下:

1
[<#value 1#>, <#value 2#>, <#...#>]

数组中最后一个表达式后面可以跟一个可选的逗号。数组字面量的类型是 [T],其中 T 是其中各表达式的类型;如果存在多种类型的表达式,T 是它们最近的公共父类型。空数组字面量用一对空方括号书写,可以用来创建指定类型的空数组。

1
var emptyArray: [Double] = []

字典字面量是一个无序的键值对集合,其形式如下:

1
[<#key 1#>: <#value 1#>, <#key 2#>: <#value 2#>, <#...#>]

字典中最后一个表达式后面可以跟一个可选的逗号。字典字面量的类型是 [Key: Value],其中 Key 是其键表达式的类型,Value 是其值表达式的类型;如果存在多种类型的表达式,Key 和 Value 分别是各自取值最近的公共父类型。空字典字面量写作方括号里的一个冒号([:]),以区别于空数组字面量。你可以用空字典字面量创建指定键和值类型的空字典。

1
var emptyDictionary: [String: Double] = [:]

playground 字面量由 Xcode 用来在程序编辑器中创建颜色、文件或图片的交互式表示。在 Xcode 之外的纯文本中,playground 字面量用一种特殊的字面量语法表示。

关于在 Xcode 中使用 playground 字面量的内容,参见 Xcode Help 中的 Add a color, file, or image literal。

Grammar of a literal expression:

literal-expression → literal
literal-expression → array-literal | dictionary-literal | playground-literal

array-literal → [ array-literal-items? ,? ]
array-literal-items → array-literal-item | array-literal-item , array-literal-items
array-literal-item → expression

dictionary-literal → [ dictionary-literal-items ,? ] | [ : ]
dictionary-literal-items → dictionary-literal-item | dictionary-literal-item , dictionary-literal-items
dictionary-literal-item → expression : expression

playground-literal → #colorLiteral ( red : expression , green : expression , blue : expression , alpha : expression )
playground-literal → #fileLiteral ( resourceName : expression )
playground-literal → #imageLiteral ( resourceName : expression )

self 表达式

self 表达式显式引用它所在的当前类型或该类型的当前实例,其形式如下:

1
2
3
4
5
self
self.<#member name#>
self[<#subscript index#>]
self(<#initializer arguments#>)
self.init(<#initializer arguments#>)

在构造器、下标或实例方法中,self 指向它所在的类型的当前实例;在类型方法中,self 指向它所在的当前类型。

访问成员时,self 表达式用来限定作用域:当作用域中存在同名的其他变量(例如函数参数)时,它提供了消除歧义的手段。例如:

1
2
3
4
5
6
class SomeClass {
    var greeting: String
    init(greeting: String) {
        self.greeting = greeting
    }
}

在值类型的可变方法中,你可以把该值类型的新实例赋给 self。例如:

1
2
3
4
5
6
struct Point {
    var x = 0.0, y = 0.0
    mutating func moveBy(x deltaX: Double, y deltaY: Double) {
        self = Point(x: x + deltaX, y: y + deltaY)
    }
}

Grammar of a self expression:

self-expression → self | self-method-expression | self-subscript-expression | self-initializer-expression

self-method-expression → self . identifier
self-subscript-expression → self [ function-call-argument-list ]
self-initializer-expression → self . init

父类表达式

父类表达式让类与其父类交互,其形式为下列之一:

1
2
3
super.<#member name#>
super[<#subscript index#>]
super.init(<#initializer arguments#>)

第一种形式用于访问父类的成员,第二种形式用于访问父类的下标实现,第三种形式用于访问父类的构造器。

子类可以在其成员、下标和构造器的实现中使用父类表达式,以利用父类中的实现。

Grammar of a superclass expression:

superclass-expression → superclass-method-expression | superclass-subscript-expression | superclass-initializer-expression

superclass-method-expression → super . identifier
superclass-subscript-expression → super [ function-call-argument-list ]
superclass-initializer-expression → super . init

条件表达式

条件表达式根据条件的取值求值为若干给定值之一,其形式为下列之一:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
if <#condition 1#> {
   <#expression used if condition 1 is true#>
} else if <#condition 2#> {
   <#expression used if condition 2 is true#>
} else {
   <#expression used if both conditions are false#>
}

switch <#expression#> {
case <#pattern 1#>:
    <#expression 1#>
case <#pattern 2#> where <#condition#>:
    <#expression 2#>
default:
    <#expression 3#>
}

条件表达式在行为和语法上与 if 语句或 switch 语句相同,只有下面几段所描述的差别。

条件表达式只出现在下列上下文中:

  • 作为赋给变量的值。
  • 作为变量或常量声明中的初始值。
  • 作为 throw 表达式所抛出的错误。
  • 作为函数、闭包或属性取值器返回的值。
  • 作为条件表达式某个分支内部的值。

条件表达式的各个分支是穷尽的,这保证了无论条件如何,该表达式总能产生一个值。这意味着每个 if 分支都需要有对应的 else 分支。

每个分支包含单个表达式(当该分支的条件为真时,它被用作条件表达式的值)、一条 throw 语句,或者对永不返回的函数的调用。

每个分支产生的值必须是同一类型。由于各分支的类型检查是相互独立的,有时你需要显式指定值的类型,例如分支中包含不同种类的字面量时,或者某个分支的值是 nil 时。需要提供这些信息时,可以给接收结果的变量加类型标注,或者给各分支的值加 as 转换。

1
2
let number: Double = if someCondition { 10 } else { 12.34 }
let number = if someCondition { 10 as Double } else { 12.34 }

在结果构建器内部,条件表达式只能作为变量或常量的初始值出现。这意味着当你在结果构建器中写 if 或 switch 时——在变量或常量声明之外——那段代码会被理解为分支语句,由结果构建器的某个方法来变换它。

不要把条件表达式放进 try 表达式,即使条件表达式的某个分支是抛出错误的分支也不行。

Grammar of a conditional expression:

conditional-expression → if-expression | switch-expression

if-expression → if condition-list { statement } if-expression-tail
if-expression-tail → else if-expression
if-expression-tail → else { statement }

switch-expression → switch expression { switch-expression-cases }
switch-expression-cases → switch-expression-case switch-expression-cases?
switch-expression-case → case-label statement
switch-expression-case → default-label statement

闭包表达式

闭包表达式创建一个闭包,在其他编程语言中也称为lambda或匿名函数。与函数声明一样,闭包包含语句,并捕获其外围作用域中的常量和变量,其形式如下:

1
2
3
{ (<#parameters#>) -> <#return type#> in
   <#statements#>
}

其中参数的形式与函数声明中的参数相同,详见函数声明。

在闭包表达式中写 throws 或 async,就显式地把闭包标记为抛出错误或异步。

1
2
3
{ (<#parameters#>) async throws -> <#return type#> in
   <#statements#>
}

如果闭包体包含 throws 语句,或者包含没有嵌套在具有穷尽错误处理的 do 语句内部的 try 表达式,就认为该闭包是抛出错误的闭包。如果抛出错误的闭包只抛出单一类型的错误,就认为该闭包抛出该错误类型;否则认为它抛出 any Error。同样,如果闭包体包含 await 表达式,就认为它是异步的。

有几种特殊形式可以让闭包写得更简洁:

  • 闭包可以省略参数类型、返回类型,或两者都省略。如果同时省略参数名和两种类型,就省略语句之前的 in 关键字。如果省略的类型无法推断出来,就会产生编译期错误。
  • 闭包可以省略参数名,此时参数被隐式命名为 $ 加上其位置:$0、$1、$2,依此类推。
  • 只由单个表达式组成的闭包被认为返回该表达式的值。对周围表达式做类型推断时,也会考虑这个表达式的内容。

下面这些闭包表达式是等价的:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
myFunction { (x: Int, y: Int) -> Int in
    return x + y
}

myFunction { x, y in
    return x + y
}

myFunction { return $0 + $1 }

myFunction { $0 + $1 }

关于把闭包作为实参传给函数的内容,参见函数调用表达式。

闭包表达式可以在不被存入变量或常量的情况下使用,例如把它立即用在函数调用中。上面代码中传给 myFunction 的那些闭包表达式就是这种立即使用。因此,一个闭包表达式是逃逸的还是非逃逸的,取决于该表达式周围的上下文:如果闭包被立即调用,或者作为非逃逸的函数实参传入,它就是非逃逸的;否则,该闭包表达式就是逃逸的。

关于逃逸闭包的更多内容,参见逃逸闭包。

捕获列表

默认情况下,闭包表达式会以强引用捕获其外围作用域中的常量和变量。你可以用捕获列表显式控制闭包中值的捕获方式。

捕获列表写在参数列表之前,是用方括号括起来的逗号分隔表达式列表。如果你使用捕获列表,即使省略参数名、参数类型和返回类型,也必须使用 in 关键字。捕获列表中的最后一个表达式后面可以跟一个可选逗号。

捕获列表中的条目在闭包创建时初始化。对于捕获列表中的每个条目,都会用一个常量初始化为外围作用域中同名常量或变量的值。例如在下面的代码中,a 被包含在捕获列表里而 b 没有,这使它们的行为不同。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
var a = 0
var b = 0
let closure = { [a] in
 print(a, b)
}

a = 10
b = 10
closure()
// 输出 "0 10"。

这里有两个不同的东西都叫 a:外围作用域中的变量和闭包作用域中的常量;而名为 b 的变量只有一个。闭包创建时,内层作用域中的 a 用外层 a 的值初始化,但两者的值之间没有任何特殊的关联。这意味着修改外层的 a 不会影响内层的 a,在闭包内部修改 a 也不会影响闭包外部的 a。相比之下,名为 b 的变量只有一个——也就是外层作用域中的那个 b——因此无论是从闭包内部还是外部修改,两边看到的都是同一个值。

当被捕获变量的类型具有引用语义时,这种区别就看不出来了。例如下面的代码中有两个都叫 x 的东西:外层作用域中的变量和内层作用域中的常量;但由于引用语义,它们都指向同一个对象。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
class SimpleClass {
    var value: Int = 0
}
var x = SimpleClass()
var y = SimpleClass()
let closure = { [x] in
    print(x.value, y.value)
}

x.value = 10
y.value = 10
closure()
// 输出 "10 10"。

如果表达式值的类型是类,你可以在捕获列表中给该表达式加上 weak 或 unowned,以捕获对该表达式值的弱引用或无主引用。

1
2
3
4
myFunction { print(self.title) }                    // 隐式强捕获
myFunction { [self] in print(self.title) }          // 显式强捕获
myFunction { [weak self] in print(self!.title) }    // 弱捕获
myFunction { [unowned self] in print(self.title) }  // 无主捕获

你还可以在捕获列表中把任意表达式绑定到具名的值上。该表达式在闭包创建时求值,其值以指定的引用强度被捕获。例如:

1
2
// 以弱引用把 "self.parent" 捕获为 "parent"
myFunction { [weak parent = self.parent] in print(parent!.title) }

关于闭包表达式的更多内容与示例,参见闭包表达式;关于捕获列表的更多内容与示例,参见解决闭包的强引用循环。

Grammar of a closure expression:

closure-expression → { attributes? closure-signature? statements? }

closure-signature → capture-list? closure-parameter-clause async? throws-clause? function-result? in
closure-signature → capture-list in

closure-parameter-clause → ( ) | ( closure-parameter-list ,? ) | identifier-list
closure-parameter-list → closure-parameter | closure-parameter , closure-parameter-list
closure-parameter → closure-parameter-name type-annotation?
closure-parameter → closure-parameter-name type-annotation ...
closure-parameter-name → identifier

capture-list → [ capture-list-items ,? ]
capture-list-items → capture-list-item | capture-list-item , capture-list-items
capture-list-item → capture-specifier? identifier
capture-list-item → capture-specifier? identifier = expression
capture-list-item → capture-specifier? self-expression
capture-specifier → weak | unowned | unowned(safe) | unowned(unsafe)

隐式成员表达式

隐式成员表达式是在类型推断能够确定隐含类型的上下文中,访问某个类型成员(例如枚举成员或类型方法)的简写方式,其形式如下:

1
.<#member name#>

例如:

1
2
var x = MyEnumeration.someValue
x = .anotherValue

如果推断出的类型是可选类型,你也可以在隐式成员表达式中使用非可选类型的成员。

1
var someOptional: MyEnumeration? = .someValue

隐式成员表达式后面可以跟后置运算符或后置表达式中列出的其他后置语法。这称为链式隐式成员表达式。虽然链式后置表达式通常都同属一个类型,但唯一的要求是:整个链式隐式成员表达式必须可以转换为其上下文所隐含的类型。具体来说,如果隐含类型是可选类型,你可以使用非可选类型的值;如果隐含类型是类类型,你可以使用它的某个子类的值。例如:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
class SomeClass {
    static var shared = SomeClass()
    static var sharedSubclass = SomeSubclass()
    var a = AnotherClass()
}
class SomeSubclass: SomeClass { }
class AnotherClass {
    static var s = SomeClass()
    func f() -> SomeClass { return AnotherClass.s }
}
let x: SomeClass = .shared.a.f()
let y: SomeClass? = .shared
let z: SomeClass = .sharedSubclass

在上面的代码中,x 的类型与其上下文隐含的类型完全一致,y 的类型可以从 SomeClass 转换为 SomeClass?,而 z 的类型可以从 SomeSubclass 转换为 SomeClass。

Grammar of an implicit member expression:

implicit-member-expression → . identifier
implicit-member-expression → . identifier . postfix-expression

括号表达式

括号表达式由一对圆括号和其中的表达式构成。你可以用圆括号显式地对表达式分组,从而指定运算的优先级。分组用的圆括号不会改变表达式的类型——例如 (1) 的类型就是 Int。

Grammar of a parenthesized expression:

parenthesized-expression → ( expression )

元组表达式

元组表达式由一对圆括号和其中以逗号分隔的表达式列表构成。每个表达式之前可以有一个可选的标识符,用冒号(:)分隔,其形式如下:

1
(<#identifier 1#>: <#expression 1#>, <#identifier 2#>: <#expression 2#>, <#...#>)

元组表达式中的每个标识符在该元组表达式作用域内必须唯一。在嵌套的元组表达式中,同一嵌套层级上的标识符必须唯一。例如 (a: 10, a: 20) 不合法,因为标签 a 在同一层级出现了两次;而 (a: 10, b: (a: 1, x: 2)) 是合法的——虽然 a 出现了两次,但一次在外层元组、一次在内层元组。

元组表达式可以包含零个表达式,也可以包含两个或更多表达式。圆括号内只含单个表达式时,它是括号表达式。

注意:在 Swift 中,空元组表达式和空元组类型都写作 ()。 由于 Void 是 () 的类型别名, 你可以用它来书写空元组类型。 不过,与所有类型别名一样,Void 始终是类型—— 你不能用它书写空元组表达式。

元组中最后一个表达式后面可以跟一个可选逗号。

Grammar of a tuple expression:

tuple-expression → ( ) | ( tuple-element , tuple-element-list ,? )
tuple-element-list → tuple-element | tuple-element , tuple-element-list
tuple-element → expression | identifier : expression

通配符表达式

通配符表达式用于在赋值时显式忽略某个值。例如在下面的赋值中,10 被赋给 x,20 被忽略:

1
2
(x, _) = (10, 20)
// x 是 10,20 被忽略

Grammar of a wildcard expression:

wildcard-expression → _

宏展开表达式

宏展开表达式由宏的名字后跟一对圆括号构成,其中是以逗号分隔的宏实参列表。宏在编译期展开。宏展开表达式的形式如下:

1
<#macro name#>(<#macro argument 1#>, <#macro argument 2#>)

如果宏不接受任何实参,宏展开表达式会省略宏名之后的圆括号。

宏展开表达式可以作为参数的默认值。当用作函数或方法参数的默认值时,宏会用调用处的源代码位置求值,而不是它们在函数定义中出现的位置。不过,如果默认值是同时包含宏和其他代码的更大表达式,这些宏会在函数定义中出现的位置求值。

1
2
3
4
func f(a: Int = #line, b: Int = (#line), c: Int = 100 + #line) {
    print(a, b, c)
}
f()  // 输出 "4 1 101"

在上面的函数中,a 的默认值是一个单独的宏表达式,因此该宏使用调用 f(a:b:c:) 的源代码位置求值;相比之下,b 和 c 的值是包含宏的表达式——这些表达式中的宏使用定义 f(a:b:c:) 的源代码位置求值。

把宏用作默认值时,会在不展开该宏的情况下做类型检查,以验证下列要求:

  • 宏的访问级别与使用它的函数相同或更宽松。
  • 宏要么不接受实参,要么其实参是不含字符串插值的字面量。
  • 宏的返回类型与参数类型匹配。

你用宏表达式调用独立宏。要调用附加宏,请使用属性特性中介绍的自定义特性语法。独立宏和附加宏的展开过程都如下:

  1. Swift 解析源代码,生成抽象语法树(AST)。

  2. 宏实现以 AST 节点作为输入,执行该宏所需的变换。

  3. 宏实现所产生的变换后 AST 节点被加入原始 AST。

每个宏的展开都是独立自包含的。不过作为性能优化,Swift 可能启动一个实现宏的外部进程,并复用同一个进程来展开多个宏。实现宏时,其代码不能依赖你的代码此前展开过哪些宏,也不能依赖当前时间之类的任何其他外部状态。

对于嵌套宏以及具有多个角色的附加宏,展开过程会重复进行。嵌套的宏展开表达式从外向内展开。例如在下面的代码中,outerMacro(_:) 先展开,而 innerMacro(_:) 那次尚未展开的调用会出现在 outerMacro(_:) 作为输入收到的抽象语法树中。

1
#outerMacro(12, #innerMacro(34), "some text")

具有多个角色的附加宏会为每个角色各展开一次。每次展开都接收同一棵原始 AST 作为输入。Swift 收集所有生成的 AST 节点,把它们放入 AST 中相应的位置,从而形成整体展开结果。

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

Grammar of a macro-expansion expression:

macro-expansion-expression → # identifier generic-argument-clause? function-call-argument-clause? trailing-closures?

键路径表达式

键路径表达式指向某个类型的属性或下标。你在键值观察等动态编程任务中使用键路径表达式,其形式如下:

1
\<#type name#>.<#path#>

类型名是具象类型的名字,可以带泛型形参,例如 String、[Int] 或 Set<Int>。

路径由属性名、下标、可选链表达式和强制解包表达式构成。每个键路径组成部分都可以按需重复任意次,顺序也不限。

在编译期,键路径表达式会被替换为 KeyPath 类的实例。

要用键路径访问值,就把键路径传给所有类型都可用的 subscript(keyPath:) 下标。例如:

1
2
3
4
5
6
7
8
9
struct SomeStructure {
    var someValue: Int
}

let s = SomeStructure(someValue: 12)
let pathToProperty = \SomeStructure.someValue

let value = s[keyPath: pathToProperty]
// value 是 12

在类型推断能够确定隐含类型的上下文中,可以省略类型名。下面的代码用 \.someProperty 代替 \SomeClass.someProperty:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
class SomeClass: NSObject {
    @objc dynamic var someProperty: Int
    init(someProperty: Int) {
        self.someProperty = someProperty
    }
}

let c = SomeClass(someProperty: 10)
c.observe(\.someProperty) { object, change in
    // ...
}

路径可以指向 self,从而创建恒等键路径(\.self)。恒等键路径指向整个实例,因此你可以用它一步访问并修改存放在某个变量中的全部数据。例如:

1
2
3
var compoundValue = (a: 1, b: 2)
// 等价于 compoundValue = (a: 10, b: 20)
compoundValue[keyPath: \.self] = (a: 10, b: 20)

路径可以包含多个用句点分隔的属性名,用来指向某个属性之值的属性。下面这段代码用键路径表达式 \OuterStructure.outer.someValue 访问 OuterStructure 类型的 outer 属性中的 someValue 属性:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
struct OuterStructure {
    var outer: SomeStructure
    init(someValue: Int) {
        self.outer = SomeStructure(someValue: someValue)
    }
}

let nested = OuterStructure(someValue: 24)
let nestedKeyPath = \OuterStructure.outer.someValue

let nestedValue = nested[keyPath: nestedKeyPath]
// nestedValue 是 24

路径可以用方括号包含下标,只要下标的参数类型遵循 Hashable 协议即可。下面这个例子在键路径中使用下标访问数组的第二个元素:

1
2
3
let greetings = ["hello", "hola", "bonjour", "안녕"]
let myGreeting = greetings[keyPath: \[String].[1]]
// myGreeting 是 'hola'

下标中使用的值可以是具名值或字面量。值以值语义被捕获进键路径。下面的代码在键路径表达式和闭包中都使用变量 index,用来访问 greetings 数组的第三个元素。当 index 被修改后,键路径表达式仍然指向第三个元素,而闭包使用新的下标值。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
var index = 2
let path = \[String].[index]
let fn: ([String]) -> String = { strings in strings[index] }

print(greetings[keyPath: path])
// 输出 "bonjour"。
print(fn(greetings))
// 输出 "bonjour"。

// 给 index 设一个新值不会影响 path
index += 1
print(greetings[keyPath: path])
// 输出 "bonjour"。

// 由于 fn 捕获了 index,它使用的是新值
print(fn(greetings))
// 输出 "안녕"。

路径可以使用可选链和强制解包。下面这段代码在键路径中使用可选链访问可选字符串的属性:

1
2
3
4
5
6
7
8
let firstGreeting: String? = greetings.first
print(firstGreeting?.count as Any)
// 输出 "Optional(5)"。

// 用键路径做同样的事。
let count = greetings[keyPath: \[String].first?.count]
print(count as Any)
// 输出 "Optional(5)"。

你可以把键路径的各个组成部分混合搭配,以访问类型中嵌套很深的值。下面这段代码通过组合这些组成部分的键路径表达式,访问由数组构成的字典中的不同取值和属性。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
let interestingNumbers = ["prime": [2, 3, 5, 7, 11, 13, 17],
                          "triangular": [1, 3, 6, 10, 15, 21, 28],
                          "hexagonal": [1, 6, 15, 28, 45, 66, 91]]
print(interestingNumbers[keyPath: \[String: [Int]].["prime"]] as Any)
// 输出 "Optional([2, 3, 5, 7, 11, 13, 17])"。
print(interestingNumbers[keyPath: \[String: [Int]].["prime"]![0]])
// 输出 "2"。
print(interestingNumbers[keyPath: \[String: [Int]].["hexagonal"]!.count])
// 输出 "7"。
print(interestingNumbers[keyPath: \[String: [Int]].["hexagonal"]!.count.bitWidth])
// 输出 "64"。

在通常要提供函数或闭包的地方,你可以改用键路径表达式。具体来说,如果某个键路径表达式的根类型是 SomeType、其路径产生的值类型是 Value,你就可以用它代替 (SomeType) -> Value 类型的函数或闭包。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
struct Task {
    var description: String
    var completed: Bool
}
var toDoList = [
    Task(description: "Practice ping-pong.", completed: false),
    Task(description: "Buy a pirate costume.", completed: true),
    Task(description: "Visit Boston in the Fall.", completed: false),
]

// 下面两种写法等价。
let descriptions = toDoList.filter(\.completed).map(\.description)
let descriptions2 = toDoList.filter { $0.completed }.map { $0.description }

键路径表达式的任何副作用只在求值该表达式时发生一次。例如,如果你在键路径表达式的下标中调用函数,该函数只会在求值该表达式时被调用一次,而不是每次使用该键路径时都调用。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
func makeIndex() -> Int {
    print("Made an index")
    return 0
}
// 下面这行会调用 makeIndex()。
let taskKeyPath = \[Task][makeIndex()]
// 输出 "Made an index"。

// 使用 taskKeyPath 不会再次调用 makeIndex()。
let someTask = toDoList[keyPath: taskKeyPath]

关于在与 Objective-C API 交互的代码中使用键路径的更多内容,参见 Using Objective-C Runtime Features in Swift。关于键值编码与键值观察的内容,参见 Key-Value Coding Programming Guide 和 Key-Value Observing Programming Guide。

Grammar of a key-path expression:

key-path-expression → \ type? . key-path-components
key-path-components → key-path-component | key-path-component . key-path-components
key-path-component → identifier key-path-postfixes? | key-path-postfixes

key-path-postfixes → key-path-postfix key-path-postfixes?
key-path-postfix → ? | ! | self | [ function-call-argument-list ]

选择器表达式

选择器表达式让你访问在 Objective-C 中用来指向某个方法,或者某个属性的取值器或设值器的选择器。其形式如下:

1
2
3
#selector(<#method name#>)
#selector(getter: <#property name#>)
#selector(setter: <#property name#>)

方法名和属性名必须指向 Objective-C 运行时中可用的方法或属性。选择器表达式的值是 Selector 类型的实例。例如:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
class SomeClass: NSObject {
    @objc let property: String

    @objc(doSomethingWithInt:)
    func doSomething(_ x: Int) { }

    init(property: String) {
        self.property = property
    }
}
let selectorForMethod = #selector(SomeClass.doSomething(_:))
let selectorForPropertyGetter = #selector(getter: SomeClass.property)

为属性的取值器创建选择器时,属性名可以指向变量属性或常量属性;相比之下,为属性的设值器创建选择器时,属性名只能指向变量属性。

方法名可以包含用于分组的圆括号,也可以用 as 运算符在同名但类型签名不同的方法之间消除歧义。例如:

1
2
3
4
5
extension SomeClass {
    @objc(doSomethingWithString:)
    func doSomething(_ x: String) { }
}
let anotherSelector = #selector(SomeClass.doSomething(_:) as (SomeClass) -> (String) -> Void)

由于选择器是在编译期而不是运行时创建的,编译器能够检查该方法或属性确实存在,并且已暴露给 Objective-C 运行时。

注意:虽然方法名和属性名是表达式, 但它们永远不会被求值。

关于在与 Objective-C API 交互的 Swift 代码中使用选择器的更多内容,参见 Using Objective-C Runtime Features in Swift。

Grammar of a selector expression:

selector-expression → #selector ( expression )
selector-expression → #selector ( getter: expression )
selector-expression → #selector ( setter: expression )

键路径字符串表达式

键路径字符串表达式让你访问在 Objective-C 中用来指向某个属性的字符串,供键值编码和键值观察 API 使用。其形式如下:

1
#keyPath(<#property name#>)

属性名必须指向 Objective-C 运行时中可用的属性。在编译期,键路径字符串表达式会被替换为一个字符串字面量。例如:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
class SomeClass: NSObject {
    @objc var someProperty: Int
    init(someProperty: Int) {
       self.someProperty = someProperty
    }
}

let c = SomeClass(someProperty: 12)
let keyPath = #keyPath(SomeClass.someProperty)

if let value = c.value(forKey: keyPath) {
    print(value)
}
// 输出 "12"。

在类内部使用键路径字符串表达式时,你可以只写属性名(而不写类名)来指向该类的属性。

1
2
3
4
5
6
7
extension SomeClass {
    func getSomeKeyPath() -> String {
        return #keyPath(someProperty)
    }
}
print(keyPath == c.getSomeKeyPath())
// 输出 "true"。

由于键路径字符串是在编译期而不是运行时创建的,编译器能够检查该属性确实存在,并且已暴露给 Objective-C 运行时。

关于在与 Objective-C API 交互的 Swift 代码中使用键路径的更多内容,参见 Using Objective-C Runtime Features in Swift。关于键值编码与键值观察的内容,参见 Key-Value Coding Programming Guide 和 Key-Value Observing Programming Guide。

注意:虽然属性名是表达式,但它永远不会被求值。

Grammar of a key-path string expression:

key-path-string-expression → #keyPath ( expression )

后置表达式

后置表达式是通过把后置运算符或其他后置语法应用于某个表达式而形成的。从语法上说,每个主表达式也都是后置表达式。

关于这些运算符行为的内容,参见基本运算符和高级运算符。

关于 Swift 标准库提供的运算符,参见 Operator Declarations。

Grammar of a postfix expression:

postfix-expression → primary-expression
postfix-expression → postfix-expression postfix-operator
postfix-expression → function-call-expression
postfix-expression → initializer-expression
postfix-expression → explicit-member-expression
postfix-expression → postfix-self-expression
postfix-expression → subscript-expression
postfix-expression → forced-value-expression
postfix-expression → optional-chaining-expression

函数调用表达式

函数调用表达式由函数名后跟一对圆括号构成,其中是以逗号分隔的函数实参列表。函数调用表达式的形式如下:

1
<#function name#>(<#argument value 1#>, <#argument value 2#>)

函数名可以是任何值为函数类型的表达式。

如果函数定义中为参数起了名字,那么函数调用必须在实参值之前写上这些名字,并用冒号(:)分隔。这类函数调用表达式的形式如下:

1
<#function name#>(<#argument name 1#>: <#argument value 1#>, <#argument name 2#>: <#argument value 2#>)

圆括号中最后一个实参后面可以跟一个可选逗号。

函数调用表达式可以在右圆括号之后紧跟闭包表达式形式的尾随闭包。尾随闭包被理解为该函数的实参,接在最后一个带括号的实参之后传入:第一个闭包表达式没有标签,其他闭包表达式前面要写各自的实参标签。下面的例子展示了使用和不使用尾随闭包语法时等价的函数调用写法:

1
2
3
4
5
6
7
// someFunction 接受一个整数和一个闭包作为实参
someFunction(x: x, f: { $0 == 13 })
someFunction(x: x) { $0 == 13 }

// anotherFunction 接受一个整数和两个闭包作为实参
anotherFunction(x: x, f: { $0 == 13 }, g: { print(99) })
anotherFunction(x: x) { $0 == 13 } g: { print(99) }

如果尾随闭包是该函数唯一的实参,你可以省略圆括号。

1
2
3
// someMethod 只接受一个闭包作为实参
myData.someMethod() { $0 == 13 }
myData.someMethod { $0 == 13 }

为了把尾随闭包纳入实参,编译器按如下方式从左到右检查函数的参数:

尾随闭包参数动作
带标签带标签如果标签相同,闭包匹配该参数;否则跳过该参数。
带标签不带标签跳过该参数。
不带标签带标签或不带标签如果该参数在结构上类似函数类型(定义见下),闭包匹配该参数;否则跳过该参数。

尾随闭包会作为它所匹配参数的实参传入。扫描过程中被跳过的参数不会收到实参——例如它们可以使用默认参数。找到匹配之后,扫描继续处理下一个尾随闭包和下一个参数。匹配过程结束时,所有尾随闭包都必须找到匹配。

如果参数不是输入输出参数,且属于下列之一,那么该参数在结构上类似函数类型:

  • 类型是函数类型的参数,例如 (Bool) -> Int
  • 自动闭包参数,且其被包装表达式的类型是函数类型,例如 @autoclosure () -> ((Bool) -> Int)
  • 可变参数,且其数组元素类型是函数类型,例如 ((Bool) -> Int)...
  • 类型被裹在一层或多层可选类型中的参数,例如 Optional<(Bool) -> Int>
  • 类型是这些允许类型之组合的参数,例如 (Optional<(Bool) -> Int>)...

当尾随闭包匹配到一个在结构上类似函数类型、但本身不是函数的参数时,闭包会按需被包装。例如,如果参数类型是可选类型,闭包会自动被包装进 Optional。

为了便于把 Swift 5.3 之前的代码迁移过来——那些版本是从右到左做这一匹配的——编译器会同时检查从左到右和从右到左两种顺序。如果两种扫描方向给出不同结果,就采用旧的从右到左顺序,并生成一条警告。未来的 Swift 版本将始终使用从左到右的顺序。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
typealias Callback = (Int) -> Int
func someFunction(firstClosure: Callback? = nil,
                secondClosure: Callback? = nil) {
    let first = firstClosure?(10)
    let second = secondClosure?(20)
    print(first ?? "-", second ?? "-")
}

someFunction()  // 输出 "- -"
someFunction { return $0 + 100 }  // 有歧义
someFunction { return $0 } secondClosure: { return $0 }  // 输出 "10 20"

在上面的例子里,标记为"有歧义"的那次函数调用会输出 “- 120”,并在 Swift 5.3 上产生一条编译器警告。未来的 Swift 版本会输出 “110 -"。

类、结构体或枚举类型可以通过声明若干方法之一来启用函数调用语法的语法糖,详见具有特殊名称的方法。

到指针类型的隐式转换

在函数调用表达式中,如果实参与参数类型不同,编译器会尝试按下列清单中的某一种隐式转换使它们的类型匹配:

  • inout SomeType 可以变成 UnsafePointer<SomeType> 或 UnsafeMutablePointer<SomeType>
  • inout Array<SomeType> 可以变成 UnsafePointer<SomeType> 或 UnsafeMutablePointer<SomeType>
  • Array<SomeType> 可以变成 UnsafePointer<SomeType>
  • String 可以变成 UnsafePointer<CChar>

下面两次函数调用是等价的:

1
2
3
4
5
6
7
func unsafeFunction(pointer: UnsafePointer<Int>) {
    // ...
}
var myNumber = 1234

unsafeFunction(pointer: &myNumber)
withUnsafePointer(to: myNumber) { unsafeFunction(pointer: $0) }

通过这些隐式转换创建的指针只在函数调用期间有效。为避免未定义行为,请确保你的代码在函数调用结束后绝不会继续保存该指针。

注意:把数组隐式转换为不安全指针时, Swift 会按需转换或复制数组,以确保数组的存储是连续的。 例如,你可以对来自 未就其存储做出任何 API 约定的 NSArray 子类、 并桥接到 Array 的数组使用这种语法。 如果你需要保证数组的存储本来就是连续的, 使这一隐式转换永远不必做这些工作, 请使用 ContiguousArray 而不是 Array。

在对底层 C 函数的调用中,用 & 代替 withUnsafePointer(to:) 这样的显式函数可以让调用更易读,尤其是当函数接受多个指针实参时。不过,调用其他 Swift 代码中的函数时,请避免用 & 代替显式的不安全 API。

Grammar of a function call expression:

function-call-expression → postfix-expression function-call-argument-clause
function-call-expression → postfix-expression function-call-argument-clause? trailing-closures

function-call-argument-clause → ( ) | ( function-call-argument-list ,? )
function-call-argument-list → function-call-argument | function-call-argument , function-call-argument-list
function-call-argument → expression | identifier : expression
function-call-argument → operator | identifier : operator

trailing-closures → closure-expression labeled-trailing-closures?
labeled-trailing-closures → labeled-trailing-closure labeled-trailing-closures?
labeled-trailing-closure → identifier : closure-expression

构造器表达式

构造器表达式提供对某个类型构造器的访问,其形式如下:

1
<#expression#>.init(<#initializer arguments#>)

你在函数调用表达式中使用构造器表达式来初始化某个类型的新实例,也用构造器表达式委托给父类的构造器。

1
2
3
4
5
6
class SomeSubClass: SomeSuperClass {
    override init() {
        // 子类初始化写在这里
        super.init()
    }
}

和函数一样,构造器也可以当作值使用。例如:

1
2
3
4
5
// 需要类型标注,因为 String 有多个构造器。
let initializer: (Int) -> String = String.init
let oneTwoThree = [1, 2, 3].map(initializer).reduce("", +)
print(oneTwoThree)
// 输出 "123"。

如果你按名字指定类型,就可以不使用构造器表达式来访问该类型的构造器;其他所有情况下都必须使用构造器表达式。

1
2
3
4
5
let s1 = SomeType.init(data: 3)  // 合法
let s2 = SomeType(data: 1)       // 也合法

let s3 = type(of: someValue).init(data: 7)  // 合法
let s4 = type(of: someValue)(data: 5)       // 错误

Grammar of an initializer expression:

initializer-expression → postfix-expression . init
initializer-expression → postfix-expression . init ( argument-names )

显式成员表达式

显式成员表达式用于访问具名类型、元组或模块的成员:它由项目与其成员标识符之间的一个句点(.)构成。

1
<#expression#>.<#member name#>

具名类型的成员在类型声明或其扩展中被命名。例如:

1
2
3
4
5
class SomeClass {
    var someProperty = 42
}
let c = SomeClass()
let y = c.someProperty  // 成员访问

元组的成员按出现顺序从零开始用整数隐式命名。例如:

1
2
3
var t = (10, 20, 30)
t.0 = t.1
// 现在 t 是 (20, 20, 30)

模块的成员就是对该模块中顶层声明的访问。

用 dynamicMemberLookup 特性声明的类型包含在运行时查找的成员,详见属性特性。

要区分仅实参名不同的方法或构造器,就在圆括号里写上实参名,每个实参名后跟一个冒号(:);没有名字的实参写一个下划线(_)。要区分重载方法,请使用类型标注。例如:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
class SomeClass {
    func someMethod(x: Int, y: Int) {}
    func someMethod(x: Int, z: Int) {}
    func overloadedMethod(x: Int, y: Int) {}
    func overloadedMethod(x: Int, y: Bool) {}
}
let instance = SomeClass()

let a = instance.someMethod              // 有歧义
let b = instance.someMethod(x:y:)        // 无歧义

let d = instance.overloadedMethod        // 有歧义
let d = instance.overloadedMethod(x:y:)  // 仍有歧义
let d: (Int, Bool) -> Void  = instance.overloadedMethod(x:y:)  // 无歧义

如果句点出现在行首,它被理解为显式成员表达式的一部分,而不是隐式成员表达式。例如下面这段清单展示了跨多行拆分的链式方法调用:

1
2
3
4
let x = [10, 3, 20, 15, 4]
    .sorted()
    .filter { $0 > 5 }
    .map { $0 * 100 }

你可以把这种多行链式语法与编译器控制语句结合,控制每个方法何时被调用。例如下面这段代码在 iOS 上使用不同的过滤规则:

1
2
3
4
5
6
let numbers = [10, 20, 33, 43, 50]
#if os(iOS)
    .filter { $0 < 40 }
#else
    .filter { $0 > 25 }
#endif

在 #if、#endif 以及其他编译指令之间,条件编译块可以包含一个隐式成员表达式后跟零个或多个后置部分,构成一个后置表达式;它也可以包含另一个条件编译块,或者这些表达式与块的组合。

只要可以书写显式成员表达式,就可以使用这一语法,而不限于顶层代码。

在条件编译块中,#if 编译指令对应的分支必须至少包含一个表达式;其他分支可以为空。

Grammar of an explicit member expression:

explicit-member-expression → postfix-expression . decimal-digits
explicit-member-expression → postfix-expression . identifier generic-argument-clause?
explicit-member-expression → postfix-expression . identifier ( argument-names )
explicit-member-expression → postfix-expression conditional-compilation-block

argument-names → argument-name argument-names?
argument-name → identifier :

后置 self 表达式

后置 self 表达式由一个表达式或类型名紧跟 .self 构成,其形式如下:

1
2
<#expression#>.self
<#type#>.self

第一种形式求值为该表达式的值。例如 x.self 求值为 x。

第二种形式求值为该类型的值。用这种形式可以把类型当作值来访问:例如,由于 SomeClass.self 求值为 SomeClass 类型本身,你可以把它传给接受类型层面实参的函数或方法。

Grammar of a postfix self expression:

postfix-self-expression → postfix-expression . self

下标表达式

下标表达式使用相应下标声明的取值器和设值器提供下标访问,其形式如下:

1
<#expression#>[<#index expressions#>]

要对下标表达式求值,会以下标表达式作为下标参数调用该表达式所属类型的下标取值器;要设置其值,则以同样方式调用下标设值器。

关于下标声明的内容,参见协议下标声明。

Grammar of a subscript expression:

subscript-expression → postfix-expression [ function-call-argument-list ]

强制取值表达式

强制取值表达式解包一个你确定不是 nil 的可选值,其形式如下:

1
<#expression#>!

如果该表达式的值不是 nil,该可选值会被解包,并以相应的非可选类型返回;否则会产生运行时错误。

强制取值表达式解包出来的值可以被修改:既可以修改该值本身,也可以给它某个成员赋值。例如:

1
2
3
4
5
6
7
var x: Int? = 0
x! += 1
// x 现在是 1

var someDictionary = ["a": [1, 2, 3], "b": [10, 20]]
someDictionary["a"]![0] = 100
// someDictionary 现在是 ["a": [100, 2, 3], "b": [10, 20]]

Grammar of a forced-value expression:

forced-value-expression → postfix-expression !

可选链表达式

可选链表达式为在后置表达式中使用可选值提供了简化语法,其形式如下:

1
<#expression#>?

后置 ? 运算符把表达式变成可选链表达式,而不改变该表达式的值。

可选链表达式必须出现在后置表达式之中,它们会让该后置表达式以一种特殊方式求值:如果可选链表达式的值是 nil,那么该后置表达式中其他所有操作都被忽略,整个后置表达式求值为 nil;如果可选链表达式的值不是 nil,该值会被解包,并用于求值该后置表达式的其余部分。无论哪种情况,该后置表达式的值都仍然是可选类型。

如果包含可选链表达式的后置表达式嵌套在其他后置表达式之中,只有最外层的表达式返回可选类型。在下面的例子里,当 c 不是 nil 时,它的值会被解包并用于求值 .property,再用 .property 的值求值 .performAction();整个表达式 c?.property.performAction() 的值是可选类型。

1
2
var c: SomeClass?
var result: Bool? = c?.property.performAction()

下面的例子展示了不使用可选链时上面例子对应的行为。

1
2
3
4
var result: Bool?
if let unwrappedC = c {
    result = unwrappedC.property.performAction()
}

可选链表达式解包出来的值可以被修改:既可以修改该值本身,也可以给它某个成员赋值。如果可选链表达式的值是 nil,赋值运算符右侧的表达式不会被求值。例如:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
func someFunctionWithSideEffects() -> Int {
    return 42  // 实际上没有副作用。
}
var someDictionary = ["a": [1, 2, 3], "b": [10, 20]]

someDictionary["not here"]?[0] = someFunctionWithSideEffects()
// someFunctionWithSideEffects 不会被求值
// someDictionary 仍然是 ["a": [1, 2, 3], "b": [10, 20]]

someDictionary["a"]?[0] = someFunctionWithSideEffects()
// someFunctionWithSideEffects 被求值并返回 42
// someDictionary 现在 是 ["a": [42, 2, 3], "b": [10, 20]]

Grammar of an optional-chaining expression:

optional-chaining-expression → postfix-expression ?