Comment on page
表达式
Swift 中存在四种表达式:前缀表达式,中缀表达式,基本表达式和后缀表达式。表达式在返回一个值的同时还可以引发副作用。
通过前缀表达式和中缀表达式可以对简单表达式使用各种运算符。基本表达式从概念上讲是最简单的一种表达式,它是一种访问值的方式。后缀表达式则允许你建立复杂的表达式,例如函数调用和成员访问。每种表达式都在下面有详细论述。
表达式语法
前缀表达式由可选的前缀运算符和表达式组成。前缀运算符只接收一个参数,表达式则紧随其后。
前缀表达式语法
输入输出表达式 将函数调用表达式传入的变量标记为输入输出实参。
&表达式
try 表达式由
try
运算符加上紧随 其后的可抛出错误的表达式组成,形式如下:try表达式
try
表达式的返回值是该表达式的值。可选 try 表达式由
try?
运算符加上紧随其后的可抛出错误的表达式组成,形式如下:try?表达式
如果表达式没有抛出错误,可选 try 表达式的返回值是可选的该表达式的值,否则,返回值为
nil
。强制 try 表达式由
try!
运算符加上紧随其后的可抛出错误的表达式组成,形式如下:try!表达式
强制 try 表达式的返回值是该表达式的值。如果该表达式抛出了错误,将会引发运行时错误。
在中缀运算符左侧的表达式被标记上
try
、try?
或者 try!
时,这个运算符对整个中缀表达式都产生作用。也就是说,你可以使用括号来明确运算符的作用范围。 // try 对两个函数调用都产生作用
sum = try someThrowingFunction() + anotherThrowingFunction()
// try 对两个函数调用都产生作用
sum = try (someThrowingFunction() + anotherThrowingFunction())
// 错误:try 只对第一个函数调用产生作用
sum = (try someThrowingFunction()) + anotherThrowingFunction()
try
表达式不能出现在中缀运算符的的右侧,除非中缀运算符是赋值运算符或者 try
表达式是被圆括号括起来的。如果表达式中同时包含
try
和 await
运算符,try
运算符必须在前面。Try 表达式语法
try 运算符 → try | try? | try!
await 表达式由
await
运算符加上紧随其后的异步操作结果的表达式。形式如下:await表达式
await
表达式返回值就是该表达式的值。被
await
标记的表达式被称为潜在的暂停点。异步函数的执行可以在每个标记
await
的表达式的位置暂停。除此之外,并发代码的执行永远不会在其他位置暂停。这意味着在潜在暂停点之间的代码可以暂时打破不变量的状态进行安全更新,只要更新在下一个潜在暂停点之前完成。await
表达式只能在异步的上下文中出现,比如传入 async(priority:operation:)
函数的尾随闭包中。它不能在 defer
语句的闭包中,或者在同步函数的自动闭包中出现。在中缀运算符左侧的表达式被标记上
await
运算符时,这个运算符对整个中缀表达式都产生作用。也就是说,你可以使用括号来明确运算符的作用范围。// await 对两个函数调用都产生作用
sum = await someAsyncFunction() + anotherAsyncFunction()
// await 对两个函数调用都产生作用
sum = await (someAsyncFunction() + anotherAsyncFunction())
// 错误:await 只对第一个函数调用产生作用
sum = (await someAsyncFunction()) + anotherAsyncFunction()
await
表达式不能出现在中缀运算符的的右侧,除非中缀运算符是赋值运算符或者 await
表达式是被圆括号括起来的。如果表达式中同时包含
try
和 await
运算符,try
运算符必须在前面。Await 表达式语法
await 运算符 → await
中缀表达式由中缀运算符和左右参数表达式组成。形式如下:
左侧参数
中缀运算符
右侧参数
注意在解析时,一个中缀表达式将作为一个扁平列表表示,然后根据运算符的优先级,再进一步进行组合。例如,2 + 3 * 5
首先被看作具有五个元素的列表,即2
、+
、3
、*
、5
,随后根据运算符优先级组合为(2 + (3 * 5))
。
赋值表达式会为某个给定的表达式赋值,形式如下;
表达式
=值
右边的值会被赋值给左边的表达式。如果左边表达式是一个元组,那么右边必须是一个具有同样元素个数的元组。(嵌套元组也是允许的。)右边的值中的每一部分都会被赋值给左边的表达式中的相应部分。例如:
(a, _, (b, c)) = ("test", 9.45, (12, 3))
// a 为 "test",b 为 12,c 为 3,9.45 会被忽略
赋值运算符不返回任何值。
赋值运算符语法
赋值运算符 → =
三元条件运算符会根据条件来对两个给定表达式中的一个进行求值,形式如下:
条件
?表达式(条件为真则使用)
:表达式(条件为假则使用)
如果条件为真,那么对第一个表达式进行求值并返回结果。否则,对第二个表达式进行求值并返回结果。未使用的表达式不会进行求值。
三元条件运算符语法
有 4 种类型转换运算符:
is
、as
、as?
和 as!
。它们有如下的形式:表达式
is类型
表达式
as类型
表达式
as?类型
表达式
as!类型
is
运算符在运行时检查表达式能否向下转化为指定的类型,如果可以则返回 ture
,否则返回 false
。as
运算符在编译时执行向上转换和桥接。向上转换可将表达式转换成父类的实例而无需使用任何中间变量。以下表达式是等价的: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”
桥接可将 Swift 标准库中的类型(例如
String
)作为一个与之相关的 Foundation 类型(例如 NSString
)来使用,而不需要新建一个实例。关于桥接的更多信息,请参阅 Working with Foundation Types。as?
运算符有条件地执行类型转换,返回目标类型的可选值。在运行时,如果转换成功,返回的可选值将包含转换后的值,否则返回 nil
。如果在编译时就能确定转换一定会成功或是失败,则会导致编译报错。as!
运算符执行强制类型转换,返回目标类型的非可选值。如果转换失败,则会导致运行时错误。表达式 x as! T
效果等同于 (x as? T)!
。基本表达式是最基本的表达式。它们可以单独使用,也可以跟前缀表达式、中置表达式、后缀表达式组合使用。
基本表达式语法
字面量表达式可由普通字面量(例如字符串或者数字),字典或者数组字面量,或者下面列表中的特殊字面量组成:
字面量 | 类型 | 值 |
---|---|---|
#file | String | 所在的文件名及模块 |
#filePath | String | 所在的文件路径 |
#line | Int | 所在的行数 |
#column | Int | 所在的列数 |
#function | String | 所在的声明的名字 |
#dsohandle | UnsafeRawPointer | 所使用的 DSO(动态共享对象)句柄 |
#file
表达式的值的格式是 module/file,file 是表达式所在的文件名,module 是文件所所在的模块 名。#filePath
表达式的字符串值是表达式所在的文件在整个文件系统里的路径。所有这些值可以被 #sourceLocation
改变,详见 行控制语句。注意要解析#file
表达式,第一个斜杠(/)之前的文本作为模块名,最后一个斜杠之后的文本作为文件名。将来,该字符串可能包含多个斜杠,例如MyModule/some/disambiguation/MyFile.swift
。
对于
#function
,在函数中会返回当前函数的名字,在方法中会返回当前方法的名字,在属性的存取器中会返回属性的名字,在特殊的成员如 init
或 subscript
中会返回这个关 键字的名字,在某个文件中会返回当前模块的名字。当其作为函数或者方法的默认参数值时,该字面量的值取决于函数或方法的调用环境。
func logFunctionName(string: String = #function) {
print(string)
}
func myFunction() {
logFunctionName()
}
myFunction() // 打印“myFunction()”
数组字面量是值的有序集合,形式如下:
[值 1
,值 2
,...
]
数组中的最后一个表达式可以紧跟一个逗号。数组字面量的类型是
[T]
,这个 T
就是数组中元素的类型。如果数组中包含多种类型,T
则是跟这些类型最近的的公共父类型。空数组字面量由一组方括号定义,可用来创建特定类型的空数组。var emptyArray: [Double] = []
字典字面量是一个包含无序键值对的集合,形式如下:
[键 1
:值 1
,键 2
:值 2
,...
]
字典中的最后一个表达式可以紧跟一个逗号。字典字面量的类型是
[Key : Value]
,Key
表示键的类型,Value
表示值的类型。如果字典中包含多种类型,那么 Key
表示的类型则为所有键最接近的公共父类型,Value
与之相似。一个空的字典字面量由方括号中加一个冒号组成([:]
),从而与空数组字面量区分开,可以使用空字典字面量来创建特定类型的字典。var emptyDictionary: [String : Double] = [:]
Xcode 使用 playground 字面量对程序编辑器中的颜色、文件或者图片创建可交互的展示。在 Xcode 之外的空白文本中,playground 字面量使用一种特殊的字面量语法来展示。
字面量表达式语法
self
表达式是对当前类型或者当前实例的显式引用,它有如下形式:selfself.成员名称
self[下标索引
]self(构造器参数
)self.init(构造器参数
)
如果在构造器、下标、实例方法中,
self
引用的是当前类型的实例。在一个类型方法中,self
引用的是当前的类型。当访问成员时,
self
可用来区分重名变量,例如函数的参数:class SomeClass {
var greeting: String
init(greeting: String) {
self.greeting = greeting
}
}
在
mutating
方法中,你可以对 self
重新赋值:struct Point {
var x = 0.0, y = 0.0
mutating func moveByX(deltaX: Double, y deltaY: Double) {
self = Point(x: x + deltaX, y: y + deltaY)
}
}
Self 表达式语法
self 构造器表达式 → self . init
父类表达式可以使我们在某个类中访问它的父类,它有如下形式:
super.成员名称
super[下标索引
]super.init(构造器参数
)
第一种形式用来访问父类的某个成员,第二种形式用来访问父类的下标,第三种形式用来访问父类的构造器。
子类可以通过父类表达式在它们的成员、下标和构造器中使用父类中的实现。
父类表达式语法
父类构造器表达式 → super . init
闭包表达式会创建一个闭包,在其他语言中也叫 lambda 或匿名函数。跟函数一样,闭包包含了待执行的代码,不同的是闭包还会捕获所在环境中的常量和变量。它的形式如下:
{ (parameters) -> return type in
statements
}
在闭包表达式中写入
throws
或 async
将显式地将闭包标记为丢掷或异步的。{ (parameters) async throws -> return type in
statements
}
如果闭包的主体 中含有 try 表达式,则认为该闭包会引发异常。同理,若闭包主体含有 await 表达式,则认为该闭包是异步的。
闭包还有几种特殊的形式,能让闭包使用起来更加简洁:
- 闭包可以省略它的参数和返回值的类型。如果省略了参数名和所有的类型,也要省略
in
关键字。如果被省略的类型无法被编译器推断,那么就会导致编译错误。 - 闭包可以省略参数名,参数会被隐式命名为
$
加上其索引位置,例如$0
、$1
、$2
分别表示第一个、第二个、第三个参数,以此类推。 - 如果闭包中只包含一个表达式,那么该表达式的结果就会被视为闭包的返回值。表达式结果的类型也会被推断为闭包的返回类型。
下面几个闭包表达式是等价的:
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
,这将导致这两个变量具有不同的行为。var a = 0
var b = 0
let closure = { [a] in
print(a, b)
}
a = 10
b = 10
closure()
// 打印“0 10”
在示例中,变量
b
只有一个,然而,变量 a
有两个,一个在闭包外,一个在闭包内。闭包内的变量 a
会在闭包创建时用闭包外的变量 a
的值来初始化,除此之外它们并无其他联系。这意味着在闭包创建后,改变某个 a
的值都不会对另一个 a
的值造成任何影响。与此相反,闭包内外都是同一个变量 b
,因此在闭包外改变其值,闭包内的值也会受影响。如果闭包捕获的值具有引用语义则有所不同。例如,下面示例中,有两个变量
x
,一个在闭包外,一个在闭包内,由于它们的值是引用语义,虽然这是两个不同的变量,它们却都引用着同一实例。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
来修饰它,闭包会分别用弱引用和无主引用来捕获该值。myFunction { print(self.title) } // 隐式强引用捕获
myFunction { [self] in print(self.title) } // 显式强引用捕获
myFunction { [weak self] in print(self!.title) } // 弱引用捕获
myFunction { [unowned self] in print(self.title) } // 无主引用捕获
在捕获列表中,也可以将任意表达式的值绑定到一个常量上。该表达式会在闭包被创建时进行求值,闭包会按照指定的引用类型来捕获表达式的值。例如:
// 以弱引用捕获 self.parent 并赋值给 parent
myFunction { [weak parent = self.parent] in print(parent!.title) }
闭包表达式语法closure-expressionclosure-signatureclosure-parameter-clauseclosure-parameter-listclosure-parameter{#closure-parameter}closure-parameter-name{#closure-parameter-name}closure-list{#closure-list}capture-list-itemscapture-list-itemcapture-specifier捕获说明符 → weak | unowned | unowned(safe) | unowned(unsafe)
若类型可被推断出来,可以使用隐式成员表达式来访问某个类型的成员(例如某个枚举成员或某个类型方法),形式如下:
.成员名称
例如:
var x = MyEnumeration.someValue
x = .anotherValue
如果推断的是可选类型,可以在隐式成员表达式里使用不可选类型的成员。
var someOptional: MyEnumeration? = .someValue
隐式成员表达式可以跟在后缀运算符或者其他在 后缀表达式 里介绍的语法后面。这被称为 链式隐式成员表达式。尽管链式后缀表达式大多都是相同类型,但其实只需要整个链式成员表达式可以转换为上下文的类型就行了。更具体的,如果隐式类型是可选的,则可以使用非可选类型的值,如果隐式类型是类类型,则可以使用其子类的值。例如:
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
。隐式成员表达式语法
圆括号表达式是由圆括号包围的表达式。你可以用圆括号说明成组的表达式的先后操作。成组的圆括号不会改变表达式的类型 - 例如
(1)
的类型就是简单的 Int
。圆括号表达式语法
元组表达式由圆括号和其中多个逗号分隔的子表达式组成。每个子表达式前面可以有一个标识符,用冒号隔开。元组表达式形式如下:
(标识符 1
:表达式 1
,标识符 2
:表达式 2
,...
)
元组表达式里的每一个标识符在表达式作用域里必须是唯一的。在嵌套的元组表达式中,同嵌套层级里的标识符也必须是唯一的。例如,
(a: 10, a: 20)
是不合法的,因为标签 a
在同一层级出现了两次。然而,(a: 10, b: (a: 1, x: 2))
是合法的,尽管 a
出现了两次,但有一次在外层元组里,一次在内层元组里。元组表达式可以一个表达式都没有,也可以包含两个或是更多的表达式。单个表达式用括号括起来就是括号表达式了。
注意在 Swift 中,空的元组表达式和空的元组类型都写作()
。由于Void
是()
的类型别名,因此可以使用它来表示空的元组类型。虽然如此,Void
就像所有的类型别名一样,永远是一个类型——不能表 示空的元组表达式。
元组表达式语法
通配符表达式可以在赋值过程中显式忽略某个值。例如下面的代码中,
10
被赋值给 x
,而 20
则被忽略:(x, _) = (10, 20)
// x 为 10,20 被忽略
通配符表达式语法
通配符表达式 → _
Key-path 表达式引用一个类型的属性或下标。在动态语言中使场景可以使用 Key-path 表达式,例如观察键 值对。格式为:
\类型名.路径
类型名是一个具体类型的名称,包含任何泛型参数,例如
String
、[Int]
或 Set<Int>
。路径可由属性名称、下标、可选链表达式或者强制解包表达式组成。以上任意 key-path 组件可以以任何顺序重复多次。
对于所有类型,都可以通过传递 key-path 参数到下标方法
subscript(keyPath:)
来访问它的值。例如:struct SomeStructure {
var someValue: Int
}
let s = SomeStructure(someValue: 12)
let pathToProperty = \SomeStructure.someValue
let value = s[keyPath: pathToProperty]
// 值为 12
在一些可以通过类型推断来确定所访问的具体类型的上下文中,可以省略 key-path 前的类型名字。下面的代码使用
\.someProperty
代替了 SomeClass.someProperty
: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
作为路径可以创建一个恒等 key path (\.self
)。恒等 key path 可以作为整个实例的引用,因此你仅 需一步操作便可以利用它来访问以及修改其存储的所有数据。例如:var compoundValue = (a: 1, b: 2)
// 等价于 compoundValue = (a: 10, b: 20)
compoundValue[keyPath: \.self] = (a: 10, b: 20)
通过点语法,可以让路径包含多个属性名称,以此来访问某实例的属性的属性。下面的代码使用 key-path 表达式
\OuterStructure.outer.someValue
来访问 OuterStructure
类型中 outer
属性的 someValue
属性。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
协议即可。下面的例子在 key path 中使用了下标来访问数组的第二个元素。let greetings = ["hello", "hola", "bonjour", "안녕"]
let myGreeting = greetings[keyPath: \[String].[1]]
// myGreeting 的值为 'hola'
下标访问中使用的值可以是一个变量或者字面量,并且 key-path 表达式会使用值语义来捕获此值。下面的代码在 key-path 表达式和闭包中都使用了
index
变量来访问 greetings
数组的第三个元素。当 index
被修改时,key-path 表达式仍旧引用数组第三个元素,而闭包则使用了新的索引值。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' 闭包使用了新值。
print(fn(greetings))
// 打印 "안녕"
路径可以使用可选链和强制解包。下面的代码在 key path 中使用了可选链来访问可选字符串的属性。
let firstGreeting: String? = greetings.first
print(firstGreeting?.count as Any)
// 打印 "Optional(5)"
// 使用 key path 实现同样的功能
let count = greetings[keyPath: \[String].first?.count]
print(count as Any)
// 打印 "Optional(5)"
可以混合使用各种 key path 组件来访问一些深度嵌套类型的值。下面的代码通过组合不同的组件,使用 key-path 表达式访问了一个字典数组中不同的值和属性。
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"
你可以在平时提供函数或者闭包的上下文里使用 key path 表达式。特别地,你可以用根类型是
SomeType
和路径产生 Value
类型值的 key path 表达式来替换类型是 (SomeType) -> Value
的函数或者闭包。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 }
任何 key path 表达式的副作用发生的关键在于表达式在哪里被执行。例如,如果你在 key path 表达式中 的一个下标里使用函数调用,该函数只会在表达式计算的时候调用一次,而不是每次这个 key path 被使用的时候。
func makeIndex() -> Int {
print("Made an index")
return 0
}
// 下面这行调用 makeIndex()。
let taskKeyPath = \[Task][makeIndex()]
// 打印 "Made an index"
// 使用 taskKeyPath 不会再次调用 makeIndex()。
let someTask = toDoList[keyPath: taskKeyPath]
关于更多如何使用 key path 与 Objective-C APIs 交互的信息,请参阅 在 Swift 中使用 Objective-C 运行时特性。关于更多 key-value 编程和 key-value 观察的信息,请参阅 Key-Value 编程 和 Key-Value 观察编程。
key-path 表达式语法