swift下标

分類 编程语言, swift

下标

下标可以定义在类、结构体和枚举中,是访问集合、列表或序列中元素的快捷方式。可以使用下标的索引,设置和获取值,而不需要再调用对应的存取方法。举例来说,用下标访问一个 Array 实例中的元素可以写作 someArray[index],访问 Dictionary 实例中的元素可以写作 someDictionary[key]

一个类型可以定义多个下标,通过不同索引类型进行重载。下标不限于一维,你可以定义具有多个入参的下标满足自定义类型的需求。

下标语法

下标允许你通过在实例名称后面的方括号中传入一个或者多个索引值来对实例进行存取。语法类似于实例方法语法和计算型属性语法的混合。与定义实例方法类似,定义下标使用 subscript 关键字,指定一个或多个输入参数和返回类型;与实例方法不同的是,下标可以设定为读写或只读。这种行为由 getter 和 setter 实现,有点类似计算型属性:

1
2
3
4
5
6
7
8
subscript(index: Int) -> Int {
get {
// 返回一个适当的 Int 类型的值
}
set(newValue) {
// 执行适当的赋值操作
}
}

newValue 的类型和下标的返回类型相同。如同计算型属性,可以不指定 setter 的参数(newValue)。如果不指定参数,setter 会提供一个名为 newValue 的默认参数。

如同只读计算型属性,可以省略只读下标的 get 关键字:

1
2
3
subscript(index: Int) -> Int {
// 返回一个适当的 Int 类型的值
}

下面代码演示了只读下标的实现,这里定义了一个 TimesTable 结构体,用来表示传入整数的乘法表:

1
2
3
4
5
6
7
8
9
struct TimesTable {
let multiplier: Int
subscript(index: Int) -> Int {
return multiplier * index
}
}
let threeTimesTable = TimesTable(multiplier: 3)
print("six times three is \(threeTimesTable[6])")
// 打印“six times three is 18”

在上例中,创建了一个 TimesTable 实例,用来表示整数 3 的乘法表。数值 3 被传递给结构体的构造函数,作为实例成员 multiplier 的值。

你可以通过下标访问 threeTimesTable 实例,例如上面演示的 threeTimesTable[6]。这条语句查询了 3 的乘法表中的第六个元素,返回 36 倍即 18

注意

TimesTable 例子基于一个固定的数学公式,对 threeTimesTable[someIndex] 进行赋值操作并不合适,因此下标定义为只读的。

下标用法

下标的确切含义取决于使用场景。下标通常作为访问集合,列表或序列中元素的快捷方式。你可以针对自己特定的类或结构体的功能来自由地以最恰当的方式实现下标。

例如,Swift 的 Dictionary 类型实现下标用于对其实例中储存的值进行存取操作。为字典设值时,在下标中使用和字典的键类型相同的键,并把一个和字典的值类型相同的值赋给这个下标:

1
2
var numberOfLegs = ["spider": 8, "ant": 6, "cat": 4]
numberOfLegs["bird"] = 2

上例定义一个名为 numberOfLegs 的变量,并用一个包含三对键值的字典字面量初始化它。numberOfLegs 字典的类型被推断为 [String: Int]。字典创建完成后,该例子通过下标将 String 类型的键 birdInt 类型的值 2 添加到字典中。

更多关于 Dictionary 下标的信息请参考 读取和修改字典

注意

Swift 的 Dictionary 类型的下标接受并返回可选类型的值。上例中的 numberOfLegs 字典通过下标返回的是一个 Int? 或者说“可选的 int”。Dictionary 类型之所以如此实现下标,是因为不是每个键都有个对应的值,同时这也提供了一种通过键删除对应值的方式,只需将键对应的值赋值为 nil 即可。

下标选项

下标可以接受任意数量的入参,并且这些入参可以是任意类型。下标的返回值也可以是任意类型。下标可以使用可变参数,并且可以提供默认参数数值,但是不能使用输入输出参数。

一个类或结构体可以根据自身需要提供多个下标实现,使用下标时将通过入参的数量和类型进行区分,自动匹配合适的下标,这就是下标的重载

虽然接受单一入参的下标是最常见的,但也可以根据情况定义接受多个入参的下标。例如下例定义了一个 Matrix 结构体,用于表示一个 Double 类型的二维矩阵。Matrix 结构体的下标接受两个整型参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
struct Matrix {
let rows: Int, columns: Int
var grid: [Double]
init(rows: Int, columns: Int) {
self.rows = rows
self.columns = columns
grid = Array(repeating: 0.0, count: rows * columns)
}
func indexIsValid(row: Int, column: Int) -> Bool {
return row >= 0 && row < rows && column >= 0 && column < columns
}
subscript(row: Int, column: Int) -> Double {
get {
assert(indexIsValid(row: row, column: column), "Index out of range")
return grid[(row * columns) + column]
}
set {
assert(indexIsValid(row: row, column: column), "Index out of range")
grid[(row * columns) + column] = newValue
}
}
}

Matrix 提供了一个接受两个入参的构造方法,入参分别是 rowscolumns,创建了一个足够容纳 rows * columnsDouble 类型的值的数组。通过传入数组长度和初始值 0.0 到数组的构造器,将矩阵中每个位置的值初始化为 0.0。关于数组的这种构造方法请参考 创建一个带有默认值的数组

你可以通过传入合适的 rowcolumn 的数量来构造一个新的 Matrix 实例:

1
var matrix = Matrix(rows: 2, columns: 2)

上例中创建了一个 Matrix 实例来表示两行两列的矩阵。该 Matrix 实例的 grid 数组按照从左上到右下的阅读顺序将矩阵扁平化存储:

rowcolumn 的值传入下标来为矩阵设值,下标的入参使用逗号分隔:

1
2
matrix[0, 1] = 1.5
matrix[1, 0] = 3.2

上面两条语句分别调用下标的 setter 将矩阵右上角位置(即 row0column1 的位置)的值设置为 1.5,将矩阵左下角位置(即 row1column0 的位置)的值设置为 3.2

Matrix 下标的 getter 和 setter 中都含有断言,用来检查下标入参 rowcolumn 的值是否有效。为了方便进行断言,Matrix 包含了一个名为 indexIsValid(row:column:) 的便利方法,用来检查入参 rowcolumn 的值是否在矩阵范围内:

1
2
3
func indexIsValid(row: Int, column: Int) -> Bool {
return row >= 0 && row < rows && column >= 0 && column < columns
}

断言在下标越界时触发:

1
2
let someValue = matrix[2, 2]
// 断言将会触发,因为 [2, 2] 已经超过了 matrix 的范围

类型下标

正如上节所述,实例下标是在特定类型的一个实例上调用的下标。你也可以定义一种在这个类型本身上调用的下标。这种下标的类型被称作类型下标。你可以通过在 subscript 关键字之前写下 static 关键字的方式来表示一个类型下标。类可以使用 class 关键字来允许子类重写父类中对那个下标的实现。下面的例子展示了如何定义和调用一个类型下标:

1
2
3
4
5
6
7
8
enum Planet: Int {
case mercury = 1, venus, earth, mars, jupiter, saturn, uranus, neptune
static subscript(n: Int) -> Planet {
return Planet(rawValue: n)!
}
}
let mars = Planet[4]
print(mars)

留言與分享

swift方法

分類 编程语言, swift

方法

方法是与某些特定类型相关联的函数。类、结构体、枚举都可以定义实例方法;实例方法为给定类型的实例封装了具体的任务与功能。类、结构体、枚举也可以定义类型方法;类型方法与类型本身相关联。类型方法与 Objective-C 中的类方法(class methods)相似。

结构体和枚举能够定义方法是 Swift 与 C/Objective-C 的主要区别之一。在 Objective-C 中,类是唯一能定义方法的类型。但在 Swift 中,你不仅能选择是否要定义一个类/结构体/枚举,还能灵活地在你创建的类型(类/结构体/枚举)上定义方法。

实例方法(Instance Methods)

实例方法是属于某个特定类、结构体或者枚举类型实例的方法。实例方法提供访问和修改实例属性的方法或提供与实例目的相关的功能,并以此来支撑实例的功能。实例方法的语法与函数完全一致,详情参见 函数

实例方法要写在它所属的类型的前后大括号之间。实例方法能够隐式访问它所属类型的所有的其他实例方法和属性。实例方法只能被它所属的类的某个特定实例调用。实例方法不能脱离于现存的实例而被调用。

下面的例子,定义一个很简单的 Counter 类,Counter 能被用来对一个动作发生的次数进行计数:

1
2
3
4
5
6
7
8
9
10
11
12
class Counter {
var count = 0
func increment() {
count += 1
}
func increment(by amount: Int) {
count += amount
}
func reset() {
count = 0
}
}

Counter 类定义了三个实例方法:

  • increment 让计数器按一递增;
  • increment(by: Int) 让计数器按一个指定的整数值递增;
  • reset 将计数器重置为0。

Counter 这个类还声明了一个可变属性 count,用它来保持对当前计数器值的追踪。

和调用属性一样,用点语法(dot syntax)调用实例方法:

1
2
3
4
5
6
7
8
let counter = Counter()
// 初始计数值是0
counter.increment()
// 计数值现在是1
counter.increment(by: 5)
// 计数值现在是6
counter.reset()
// 计数值现在是0

函数参数可以同时有一个局部名称(在函数体内部使用)和一个外部名称(在调用函数时使用),详情参见 指定外部参数名。方法参数也一样,因为方法就是函数,只是这个函数与某个类型相关联了。

self 属性

类型的每一个实例都有一个隐含属性叫做 selfself 完全等同于该实例本身。你可以在一个实例的实例方法中使用这个隐含的 self 属性来引用当前实例。

上面例子中的 increment 方法还可以这样写:

1
2
3
func increment() {
self.count += 1
}

实际上,你不必在你的代码里面经常写 self。不论何时,只要在一个方法中使用一个已知的属性或者方法名称,如果你没有明确地写 self,Swift 假定你是指当前实例的属性或者方法。这种假定在上面的 Counter 中已经示范了:Counter 中的三个实例方法中都使用的是 count(而不是 self.count)。

使用这条规则的主要场景是实例方法的某个参数名称与实例的某个属性名称相同的时候。在这种情况下,参数名称享有优先权,并且在引用属性时必须使用一种更严格的方式。这时你可以使用 self 属性来区分参数名称和属性名称。

下面的例子中,self 消除方法参数 x 和实例属性 x 之间的歧义:

1
2
3
4
5
6
7
8
9
10
11
struct Point {
var x = 0.0, y = 0.0
func isToTheRightOf(x: Double) -> Bool {
return self.x > x
}
}
let somePoint = Point(x: 4.0, y: 5.0)
if somePoint.isToTheRightOf(x: 1.0) {
print("This point is to the right of the line where x == 1.0")
}
// 打印“This point is to the right of the line where x == 1.0”

如果不使用 self 前缀,Swift会认为 x 的两个用法都引用了名为 x 的方法参数。

在实例方法中修改值类型

结构体和枚举是值类型。默认情况下,值类型的属性不能在它的实例方法中被修改。

但是,如果你确实需要在某个特定的方法中修改结构体或者枚举的属性,你可以为这个方法选择 可变(mutating)行为,然后就可以从其方法内部改变它的属性;并且这个方法做的任何改变都会在方法执行结束时写回到原始结构中。方法还可以给它隐含的 self 属性赋予一个全新的实例,这个新实例在方法结束时会替换现存实例。

要使用 可变方法,将关键字 mutating 放到方法的 func 关键字之前就可以了:

1
2
3
4
5
6
7
8
9
10
11
struct Point {
var x = 0.0, y = 0.0
mutating func moveBy(x deltaX: Double, y deltaY: Double) {
x += deltaX
y += deltaY
}
}
var somePoint = Point(x: 1.0, y: 1.0)
somePoint.moveBy(x: 2.0, y: 3.0)
print("The point is now at (\(somePoint.x), \(somePoint.y))")
// 打印“The point is now at (3.0, 4.0)”

上面的 Point 结构体定义了一个可变方法 moveBy(x:y :) 来移动 Point 实例到给定的位置。该方法被调用时修改了这个点,而不是返回一个新的点。方法定义时加上了 mutating 关键字,从而允许修改属性。

注意,不能在结构体类型的常量(a constant of structure type)上调用可变方法,因为其属性不能被改变,即使属性是变量属性,详情参见 常量结构体的存储属性

1
2
3
let fixedPoint = Point(x: 3.0, y: 3.0)
fixedPoint.moveBy(x: 2.0, y: 3.0)
// 这里将会报告一个错误

在可变方法中给 self 赋值

可变方法能够赋给隐含属性 self 一个全新的实例。上面 Point 的例子可以用下面的方式改写:

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)
}
}

新版的可变方法 moveBy(x:y:) 创建了一个新的结构体实例,它的 x 和 y 的值都被设定为目标值。调用这个版本的方法和调用上个版本的最终结果是一样的。

枚举的可变方法可以把 self 设置为同一枚举类型中不同的成员:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
enum TriStateSwitch {
case off, low, high
mutating func next() {
switch self {
case .off:
self = .low
case .low:
self = .high
case .high:
self = .off
}
}
}
var ovenLight = TriStateSwitch.low
ovenLight.next()
// ovenLight 现在等于 .high
ovenLight.next()
// ovenLight 现在等于 .off

上面的例子中定义了一个三态切换的枚举。每次调用 next() 方法时,开关在不同的电源状态(off, low, high)之间循环切换。

类型方法

实例方法是被某个类型的实例调用的方法。你也可以定义在类型本身上调用的方法,这种方法就叫做类型方法。在方法的 func 关键字之前加上关键字 static,来指定类型方法。类还可以用关键字 class 来指定,从而允许子类重写父类该方法的实现。

注意

在 Objective-C 中,你只能为 Objective-C 的类类型(classes)定义类型方法(type-level methods)。在 Swift 中,你可以为所有的类、结构体和枚举定义类型方法。每一个类型方法都被它所支持的类型显式包含。

类型方法和实例方法一样用点语法调用。但是,你是在类型上调用这个方法,而不是在实例上调用。下面是如何在 SomeClass 类上调用类型方法的例子:

1
2
3
4
5
6
class SomeClass {
class func someTypeMethod() {
// 在这里实现类型方法
}
}
SomeClass.someTypeMethod()

在类型方法的方法体(body)中,self 属性指向这个类型本身,而不是类型的某个实例。这意味着你可以用 self 来消除类型属性和类型方法参数之间的歧义(类似于我们在前面处理实例属性和实例方法参数时做的那样)。

一般来说,在类型方法的方法体中,任何未限定的方法和属性名称,可以被本类中其他的类型方法和类型属性引用。一个类型方法可以直接通过类型方法的名称调用本类中的其它类型方法,而无需在方法名称前面加上类型名称。类似地,在结构体和枚举中,也能够直接通过类型属性的名称访问本类中的类型属性,而不需要前面加上类型名称。

下面的例子定义了一个名为 LevelTracker 结构体。它监测玩家的游戏发展情况(游戏的不同层次或阶段)。这是一个单人游戏,但也可以存储多个玩家在同一设备上的游戏信息。

游戏初始时,所有的游戏等级(除了等级 1)都被锁定。每次有玩家完成一个等级,这个等级就对这个设备上的所有玩家解锁。LevelTracker 结构体用类型属性和方法监测游戏的哪个等级已经被解锁。它还监测每个玩家的当前等级。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
struct LevelTracker {
static var highestUnlockedLevel = 1
var currentLevel = 1

static func unlock(_ level: Int) {
if level > highestUnlockedLevel { highestUnlockedLevel = level }
}

static func isUnlocked(_ level: Int) -> Bool {
return level <= highestUnlockedLevel
}

@discardableResult
mutating func advance(to level: Int) -> Bool {
if LevelTracker.isUnlocked(level) {
currentLevel = level
return true
} else {
return false
}
}
}

LevelTracker 监测玩家已解锁的最高等级。这个值被存储在类型属性 highestUnlockedLevel 中。

LevelTracker 还定义了两个类型方法与 highestUnlockedLevel 配合工作。第一个类型方法是 unlock(_:),一旦新等级被解锁,它会更新 highestUnlockedLevel 的值。第二个类型方法是 isUnlocked(_:),如果某个给定的等级已经被解锁,它将返回 true。(注意,尽管我们没有使用类似 LevelTracker.highestUnlockedLevel 的写法,这个类型方法还是能够访问类型属性 highestUnlockedLevel

除了类型属性和类型方法,LevelTracker 还监测每个玩家的进度。它用实例属性 currentLevel 来监测每个玩家当前的等级。

为了便于管理 currentLevel 属性,LevelTracker 定义了实例方法 advance(to:)。这个方法会在更新 currentLevel 之前检查所请求的新等级是否已经解锁。advance(to:) 方法返回布尔值以指示是否能够设置 currentLevel。因为允许在调用 advance(to:) 时候忽略返回值,不会产生编译警告,所以函数被标注为 @discardableResult 属性,更多关于属性信息,请参考 特性章节。

下面,Player 类使用 LevelTracker 来监测和更新每个玩家的发展进度:

1
2
3
4
5
6
7
8
9
10
11
class Player {
var tracker = LevelTracker()
let playerName: String
func complete(level: Int) {
LevelTracker.unlock(level + 1)
tracker.advance(to: level + 1)
}
init(name: String) {
playerName = name
}
}

Player 类创建一个新的 LevelTracker 实例来监测这个用户的进度。它提供了 complete(level:) 方法,一旦玩家完成某个指定等级就调用它。这个方法为所有玩家解锁下一等级,并且将当前玩家的进度更新为下一等级。(我们忽略了 advance(to:) 返回的布尔值,因为之前调用 LevelTracker.unlock(_:) 时就知道了这个等级已经被解锁了)。

你还可以为一个新的玩家创建一个 Player 的实例,然后看这个玩家完成等级一时发生了什么:

1
2
3
4
var player = Player(name: "Argyrios")
player.complete(level: 1)
print("highest unlocked level is now \(LevelTracker.highestUnlockedLevel)")
// 打印“highest unlocked level is now 2”

如果你创建了第二个玩家,并尝试让他开始一个没有被任何玩家解锁的等级,那么试图设置玩家当前等级将会失败:

1
2
3
4
5
6
7
player = Player(name: "Beto")
if player.tracker.advance(to: 6) {
print("player is now on level 6")
} else {
print("level 6 has not yet been unlocked")
}
// 打印“level 6 has not yet been unlocked”

留言與分享

swift属性

分類 编程语言, swift

属性

属性将值与特定的类、结构体或枚举关联。存储属性会将常量和变量存储为实例的一部分,而计算属性则是直接计算(而不是存储)值。计算属性可以用于类、结构体和枚举,而存储属性只能用于类和结构体。

存储属性和计算属性通常与特定类型的实例关联。但是,属性也可以直接与类型本身关联,这种属性称为类型属性。

另外,还可以定义属性观察器来监控属性值的变化,以此来触发自定义的操作。属性观察器可以添加到类本身定义的存储属性上,也可以添加到从父类继承的属性上。

存储属性

简单来说,一个存储属性就是存储在特定类或结构体实例里的一个常量或变量。存储属性可以是变量存储属性(用关键字 var 定义),也可以是常量存储属性(用关键字 let 定义)。

可以在定义存储属性的时候指定默认值,请参考 默认构造器 一节。也可以在构造过程中设置或修改存储属性的值,甚至修改常量存储属性的值,请参考 构造过程中常量属性的修改 一节。

下面的例子定义了一个名为 FixedLengthRange 的结构体,该结构体用于描述整数的区间,且这个范围值在被创建后不能被修改。

1
2
3
4
5
6
7
8
struct FixedLengthRange {
var firstValue: Int
let length: Int
}
var rangeOfThreeItems = FixedLengthRange(firstValue: 0, length: 3)
// 该区间表示整数 0,1,2
rangeOfThreeItems.firstValue = 6
// 该区间现在表示整数 6,7,8

FixedLengthRange 的实例包含一个名为 firstValue 的变量存储属性和一个名为 length 的常量存储属性。在上面的例子中,length 在创建实例的时候被初始化,且之后无法修改它的值,因为它是一个常量存储属性。

常量结构体实例的存储属性

如果创建了一个结构体实例并将其赋值给一个常量,则无法修改该实例的任何属性,即使被声明为可变属性也不行:

1
2
3
4
let rangeOfFourItems = FixedLengthRange(firstValue: 0, length: 4)
// 该区间表示整数 0,1,2,3
rangeOfFourItems.firstValue = 6
// 尽管 firstValue 是个可变属性,但这里还是会报错

因为 rangeOfFourItems 被声明成了常量(用 let 关键字),所以即使 firstValue 是一个可变属性,也无法再修改它了。

这种行为是由于结构体属于值类型。当值类型的实例被声明为常量的时候,它的所有属性也就成了常量。

属于引用类型的类则不一样。把一个引用类型的实例赋给一个常量后,依然可以修改该实例的可变属性。

延时加载存储属性

延时加载存储属性是指当第一次被调用的时候才会计算其初始值的属性。在属性声明前使用 lazy 来标示一个延时加载存储属性。

注意

必须将延时加载属性声明成变量(使用 var 关键字),因为属性的初始值可能在实例构造完成之后才会得到。而常量属性在构造过程完成之前必须要有初始值,因此无法声明成延时加载。

当属性的值依赖于一些外部因素且这些外部因素只有在构造过程结束之后才会知道的时候,延时加载属性就会很有用。或者当获得属性的值因为需要复杂或者大量的计算,而需要采用需要的时候再计算的方式,延时加载属性也会很有用。

下面的例子使用了延时加载存储属性来避免复杂类中不必要的初始化工作。例子中定义了 DataImporterDataManager 两个类,下面是部分代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class DataImporter {
/*
DataImporter 是一个负责将外部文件中的数据导入的类。
这个类的初始化会消耗不少时间。
*/
var fileName = "data.txt"
// 这里会提供数据导入功能
}

class DataManager {
lazy var importer = DataImporter()
var data = [String]()
// 这里会提供数据管理功能
}

let manager = DataManager()
manager.data.append("Some data")
manager.data.append("Some more data")
// DataImporter 实例的 importer 属性还没有被创建

DataManager 类包含一个名为 data 的存储属性,初始值是一个空的字符串数组。这里没有给出全部代码,只需知道 DataManager 类的目的是管理和提供对这个字符串数组的访问即可。

DataManager 的一个功能是从文件中导入数据。这个功能由 DataImporter 类提供,DataImporter 完成初始化需要消耗不少时间:因为它的实例在初始化时可能需要打开文件并读取文件中的内容到内存中。

DataManager 管理数据时也可能不从文件中导入数据。所以当 DataManager 的实例被创建时,没必要创建一个 DataImporter 的实例,更明智的做法是第一次用到 DataImporter 的时候才去创建它。

由于使用了 lazyDataImporter 的实例 importer 属性只有在第一次被访问的时候才被创建。比如访问它的属性 fileName 时:

1
2
3
print(manager.importer.fileName)
// DataImporter 实例的 importer 属性现在被创建了
// 输出“data.txt”

注意

如果一个被标记为 lazy 的属性在没有初始化时就同时被多个线程访问,则无法保证该属性只会被初始化一次。

存储属性和实例变量

如果您有过 Objective-C 经验,应该知道 Objective-C 为类实例存储值和引用提供两种方法。除了属性之外,还可以使用实例变量作为一个备份存储将变量值赋值给属性。

Swift 编程语言中把这些理论统一用属性来实现。Swift 中的属性没有对应的实例变量,属性的备份存储也无法直接访问。这就避免了不同场景下访问方式的困扰,同时也将属性的定义简化成一个语句。属性的全部信息——包括命名、类型和内存管理特征——作为类型定义的一部分,都定义在一个地方。

计算属性

除存储属性外,类、结构体和枚举可以定义计算属性。计算属性不直接存储值,而是提供一个 getter 和一个可选的 setter,来间接获取和设置其他属性或变量的值。

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
struct Point {
var x = 0.0, y = 0.0
}
struct Size {
var width = 0.0, height = 0.0
}
struct Rect {
var origin = Point()
var size = Size()
var center: Point {
get {
let centerX = origin.x + (size.width / 2)
let centerY = origin.y + (size.height / 2)
return Point(x: centerX, y: centerY)
}
set(newCenter) {
origin.x = newCenter.x - (size.width / 2)
origin.y = newCenter.y - (size.height / 2)
}
}
}
var square = Rect(origin: Point(x: 0.0, y: 0.0),
size: Size(width: 10.0, height: 10.0))
let initialSquareCenter = square.center
square.center = Point(x: 15.0, y: 15.0)
print("square.origin is now at (\(square.origin.x), \(square.origin.y))")
// 打印“square.origin is now at (10.0, 10.0)”

这个例子定义了 3 个结构体来描述几何形状:

  • Point 封装了一个 (x, y) 的坐标
  • Size 封装了一个 width 和一个 height
  • Rect 表示一个有原点和尺寸的矩形

Rect 也提供了一个名为 center 的计算属性。一个 Rect 的中心点可以从 origin(原点)和 size(大小)算出,所以不需要将中心点以 Point 类型的值来保存。Rect 的计算属性 center 提供了自定义的 getter 和 setter 来获取和设置矩形的中心点,就像它有一个存储属性一样。

上述例子中创建了一个名为 squareRect 实例,初始值原点是 (0, 0),宽度高度都是 10。如下图中蓝色正方形所示。

squarecenter 属性可以通过点运算符(square.center)来访问,这会调用该属性的 getter 来获取它的值。跟直接返回已经存在的值不同,getter 实际上通过计算然后返回一个新的 Point 来表示 square 的中心点。如代码所示,它正确返回了中心点 (5, 5)

center 属性之后被设置了一个新的值 (15, 15),表示向右上方移动正方形到如下图橙色正方形所示的位置。设置属性 center 的值会调用它的 setter 来修改属性 originxy 的值,从而实现移动正方形到新的位置。

Computed Properties sample

简化 Setter 声明

如果计算属性的 setter 没有定义表示新值的参数名,则可以使用默认名称 newValue。下面是使用了简化 setter 声明的 Rect 结构体代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct AlternativeRect {
var origin = Point()
var size = Size()
var center: Point {
get {
let centerX = origin.x + (size.width / 2)
let centerY = origin.y + (size.height / 2)
return Point(x: centerX, y: centerY)
}
set {
origin.x = newValue.x - (size.width / 2)
origin.y = newValue.y - (size.height / 2)
}
}
}

简化 Getter 声明

如果整个 getter 是单一表达式,getter 会隐式地返回这个表达式结果。下面是另一个版本的 Rect 结构体,用到了简化的 getter 和 setter 声明:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct CompactRect {
var origin = Point()
var size = Size()
var center: Point {
get {
Point(x: origin.x + (size.width / 2),
y: origin.y + (size.height / 2))
}
set {
origin.x = newValue.x - (size.width / 2)
origin.y = newValue.y - (size.height / 2)
}
}
}

在 getter 中忽略 return 与在函数中忽略 return 的规则相同,请参考 隐式返回的函数

只读计算属性

只有 getter 没有 setter 的计算属性叫只读计算属性。只读计算属性总是返回一个值,可以通过点运算符访问,但不能设置新的值。

注意

必须使用 var 关键字定义计算属性,包括只读计算属性,因为它们的值不是固定的。let 关键字只用来声明常量属性,表示初始化后再也无法修改的值。

只读计算属性的声明可以去掉 get 关键字和花括号:

1
2
3
4
5
6
7
8
9
struct Cuboid {
var width = 0.0, height = 0.0, depth = 0.0
var volume: Double {
return width * height * depth
}
}
let fourByFiveByTwo = Cuboid(width: 4.0, height: 5.0, depth: 2.0)
print("the volume of fourByFiveByTwo is \(fourByFiveByTwo.volume)")
// 打印“the volume of fourByFiveByTwo is 40.0”

这个例子定义了一个名为 Cuboid 的结构体,表示三维空间的立方体,包含 widthheightdepth 属性。结构体还有一个名为 volume 的只读计算属性用来返回立方体的体积。为 volume 提供 setter 毫无意义,因为无法确定如何修改 widthheightdepth 三者的值来匹配新的 volume。然而,Cuboid 提供一个只读计算属性来让外部用户直接获取体积是很有用的。

属性观察器

属性观察器监控和响应属性值的变化,每次属性被设置值的时候都会调用属性观察器,即使新值和当前值相同的时候也不例外。

你可以为除了延时加载存储属性之外的其他存储属性添加属性观察器,你也可以在子类中通过重写属性的方式为继承的属性(包括存储属性和计算属性)添加属性观察器。你不必为非重写的计算属性添加属性观察器,因为你可以直接通过它的 setter 监控和响应值的变化。属性重写请参考 重写

可以为属性添加其中一个或两个观察器:

  • willSet 在新的值被设置之前调用
  • didSet 在新的值被设置之后调用

willSet 观察器会将新的属性值作为常量参数传入,在 willSet 的实现代码中可以为这个参数指定一个名称,如果不指定则参数仍然可用,这时使用默认名称 newValue 表示。

同样,didSet 观察器会将旧的属性值作为参数传入,可以为该参数指定一个名称或者使用默认参数名 oldValue。如果在 didSet 方法中再次对该属性赋值,那么新值会覆盖旧的值。

注意

在父类初始化方法调用之后,在子类构造器中给父类的属性赋值时,会调用父类属性的 willSetdidSet 观察器。而在父类初始化方法调用之前,给子类的属性赋值时不会调用子类属性的观察器。

有关构造器代理的更多信息,请参考 值类型的构造器代理类的构造器代理

下面是一个 willSetdidSet 实际运用的例子,其中定义了一个名为 StepCounter 的类,用来统计一个人步行时的总步数。这个类可以跟计步器或其他日常锻炼的统计装置的输入数据配合使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class StepCounter {
var totalSteps: Int = 0 {
willSet(newTotalSteps) {
print("将 totalSteps 的值设置为 \(newTotalSteps)")
}
didSet {
if totalSteps > oldValue {
print("增加了 \(totalSteps - oldValue) 步")
}
}
}
}
let stepCounter = StepCounter()
stepCounter.totalSteps = 200
// 将 totalSteps 的值设置为 200
// 增加了 200 步
stepCounter.totalSteps = 360
// 将 totalSteps 的值设置为 360
// 增加了 160 步
stepCounter.totalSteps = 896
// 将 totalSteps 的值设置为 896
// 增加了 536 步

StepCounter 类定义了一个叫 totalStepsInt 类型的属性。它是一个存储属性,包含 willSetdidSet 观察器。

totalSteps 被设置新值的时候,它的 willSetdidSet 观察器都会被调用,即使新值和当前值完全相同时也会被调用。

例子中的 willSet 观察器将表示新值的参数自定义为 newTotalSteps,这个观察器只是简单的将新的值输出。

didSet 观察器在 totalSteps 的值改变后被调用,它把新值和旧值进行对比,如果总步数增加了,就输出一个消息表示增加了多少步。didSet 没有为旧值提供自定义名称,所以默认值 oldValue 表示旧值的参数名。

注意

如果将带有观察器的属性通过 in-out 方式传入函数,willSetdidSet 也会调用。这是因为 in-out 参数采用了拷入拷出内存模式:即在函数内部使用的是参数的 copy,函数结束后,又对参数重新赋值。关于 in-out 参数详细的介绍,请参考 输入输出参数

全局变量和局部变量

计算属性和观察属性所描述的功能也可以用于全局变量局部变量。全局变量是在函数、方法、闭包或任何类型之外定义的变量。局部变量是在函数、方法或闭包内部定义的变量。

前面章节提到的全局或局部变量都属于存储型变量,跟存储属性类似,它为特定类型的值提供存储空间,并允许读取和写入。

另外,在全局或局部范围都可以定义计算型变量和为存储型变量定义观察器。计算型变量跟计算属性一样,返回一个计算结果而不是存储值,声明格式也完全一样。

注意

全局的常量或变量都是延迟计算的,跟 延时加载存储属性 相似,不同的地方在于,全局的常量或变量不需要标记 lazy 修饰符。

局部范围的常量和变量从不延迟计算。

类型属性

实例属性属于一个特定类型的实例,每创建一个实例,实例都拥有属于自己的一套属性值,实例之间的属性相互独立。

你也可以为类型本身定义属性,无论创建了多少个该类型的实例,这些属性都只有唯一一份。这种属性就是类型属性

类型属性用于定义某个类型所有实例共享的数据,比如所有实例都能用的一个常量(就像 C 语言中的静态常量),或者所有实例都能访问的一个变量(就像 C 语言中的静态变量)。

存储型类型属性可以是变量或常量,计算型类型属性跟实例的计算型属性一样只能定义成变量属性。

注意

跟实例的存储型属性不同,必须给存储型类型属性指定默认值,因为类型本身没有构造器,也就无法在初始化过程中使用构造器给类型属性赋值。

存储型类型属性是延迟初始化的,它们只有在第一次被访问的时候才会被初始化。即使它们被多个线程同时访问,系统也保证只会对其进行一次初始化,并且不需要对其使用 lazy 修饰符。

类型属性语法

在 C 或 Objective-C 中,与某个类型关联的静态常量和静态变量,是作为 global(全局)静态变量定义的。但是在 Swift 中,类型属性是作为类型定义的一部分写在类型最外层的花括号内,因此它的作用范围也就在类型支持的范围内。

使用关键字 static 来定义类型属性。在为类定义计算型类型属性时,可以改用关键字 class 来支持子类对父类的实现进行重写。下面的例子演示了存储型和计算型类型属性的语法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
struct SomeStructure {
static var storedTypeProperty = "Some value."
static var computedTypeProperty: Int {
return 1
}
}
enum SomeEnumeration {
static var storedTypeProperty = "Some value."
static var computedTypeProperty: Int {
return 6
}
}
class SomeClass {
static var storedTypeProperty = "Some value."
static var computedTypeProperty: Int {
return 27
}
class var overrideableComputedTypeProperty: Int {
return 107
}
}

注意

例子中的计算型类型属性是只读的,但也可以定义可读可写的计算型类型属性,跟计算型实例属性的语法相同。

获取和设置类型属性的值

跟实例属性一样,类型属性也是通过点运算符来访问。但是,类型属性是通过类型本身来访问,而不是通过实例。比如:

1
2
3
4
5
6
7
8
9
print(SomeStructure.storedTypeProperty)
// 打印“Some value.”
SomeStructure.storedTypeProperty = "Another value."
print(SomeStructure.storedTypeProperty)
// 打印“Another value.”
print(SomeEnumeration.computedTypeProperty)
// 打印“6”
print(SomeClass.computedTypeProperty)
// 打印“27”

下面的例子定义了一个结构体,使用两个存储型类型属性来表示两个声道的音量,每个声道具有 010 之间的整数音量。

下图展示了如何把两个声道结合来模拟立体声的音量。当声道的音量是 0,没有一个灯会亮;当声道的音量是 10,所有灯点亮。本图中,左声道的音量是 9,右声道的音量是 7

Static Properties VUMeter

上面所描述的声道模型使用 AudioChannel 结构体的实例来表示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct AudioChannel {
static let thresholdLevel = 10
static var maxInputLevelForAllChannels = 0
var currentLevel: Int = 0 {
didSet {
if currentLevel > AudioChannel.thresholdLevel {
// 将当前音量限制在阈值之内
currentLevel = AudioChannel.thresholdLevel
}
if currentLevel > AudioChannel.maxInputLevelForAllChannels {
// 存储当前音量作为新的最大输入音量
AudioChannel.maxInputLevelForAllChannels = currentLevel
}
}
}
}

AudioChannel 结构定义了 2 个存储型类型属性来实现上述功能。第一个是 thresholdLevel,表示音量的最大上限阈值,它是一个值为 10 的常量,对所有实例都可见,如果音量高于 10,则取最大上限值 10(见后面描述)。

第二个类型属性是变量存储型属性 maxInputLevelForAllChannels,它用来表示所有 AudioChannel 实例的最大输入音量,初始值是 0

AudioChannel 也定义了一个名为 currentLevel 的存储型实例属性,表示当前声道现在的音量,取值为 010

属性 currentLevel 包含 didSet 属性观察器来检查每次设置后的属性值,它做如下两个检查:

  • 如果 currentLevel 的新值大于允许的阈值 thresholdLevel,属性观察器将 currentLevel 的值限定为阈值 thresholdLevel
  • 如果修正后的 currentLevel 值大于静态类型属性 maxInputLevelForAllChannels 的值,属性观察器就将新值保存在 maxInputLevelForAllChannels 中。

注意

在第一个检查过程中,didSet 属性观察器将 currentLevel 设置成了不同的值,但这不会造成属性观察器被再次调用。

可以使用结构体 AudioChannel 创建两个声道 leftChannelrightChannel,用以表示立体声系统的音量:

1
2
var leftChannel = AudioChannel()
var rightChannel = AudioChannel()

如果将左声道的 currentLevel 设置成 7,类型属性 maxInputLevelForAllChannels 也会更新成 7

1
2
3
4
5
leftChannel.currentLevel = 7
print(leftChannel.currentLevel)
// 输出“7”
print(AudioChannel.maxInputLevelForAllChannels)
// 输出“7”

如果试图将右声道的 currentLevel 设置成 11,它会被修正到最大值 10,同时 maxInputLevelForAllChannels 的值也会更新到 10

1
2
3
4
5
rightChannel.currentLevel = 11
print(rightChannel.currentLevel)
// 输出“10”
print(AudioChannel.maxInputLevelForAllChannels)
// 输出“10”

留言與分享

swift类和结构

分類 编程语言, swift

结构体和类

结构体作为一种通用而又灵活的结构,成为了人们构建代码的基础。你可以使用定义常量、变量和函数的语法,为你的结构体和类定义属性、添加方法。

与其他编程语言所不同的是,Swift 并不要求你为自定义的结构体和类的接口与实现代码分别创建文件。你只需在单一的文件中定义一个结构体或者类,系统将会自动生成面向其它代码的外部接口。

注意

通常一个的实例被称为对象。然而相比其他语言,Swift 中结构体和类的功能更加相近,本章中所讨论的大部分功能都可以用在结构体或者类上。因此,这里会使用实例这个更通用的术语。

结构体和类对比

Swift 中结构体和类有很多共同点。两者都可以:

  • 定义属性用于存储值
  • 定义方法用于提供功能
  • 定义下标操作用于通过下标语法访问它们的值
  • 定义构造器用于设置初始值
  • 通过扩展以增加默认实现之外的功能
  • 遵循协议以提供某种标准功能

更多信息请参见 属性方法下标构造过程扩展协议

与结构体相比,类还有如下的附加功能:

  • 继承允许一个类继承另一个类的特征
  • 类型转换允许在运行时检查和解释一个类实例的类型
  • 析构器允许一个类实例释放任何其所被分配的资源
  • 引用计数允许对一个类的多次引用

更多信息请参见 继承类型转换析构过程自动引用计数

类支持的附加功能是以增加复杂性为代价的。作为一般准则,优先使用结构体,因为它们更容易理解,仅在适当或必要时才使用类。实际上,这意味着你的大多数自定义数据类型都会是结构体和枚举。更多详细的比较参见 在结构和类之间进行选择

类型定义的语法

结构体和类有着相似的定义方式。你通过 struct 关键字引入结构体,通过 class 关键字引入类,并将它们的具体定义放在一对大括号中:

1
2
3
4
5
6
struct SomeStructure {
// 在这里定义结构体
}
class SomeClass {
// 在这里定义类
}

注意

每当你定义一个新的结构体或者类时,你都是定义了一个新的 Swift 类型。请使用 UpperCamelCase 这种方式来命名类型(如这里的 SomeClassSomeStructure),以便符合标准 Swift 类型的大写命名风格(如 StringIntBool)。请使用 lowerCamelCase 这种方式来命名属性和方法(如 framerateincrementCount),以便和类型名区分。

以下是定义结构体和定义类的示例:

1
2
3
4
5
6
7
8
9
10
struct Resolution {
var width = 0
var height = 0
}
class VideoMode {
var resolution = Resolution()
var interlaced = false
var frameRate = 0.0
var name: String?
}

在上面的示例中定义了一个名为 Resolution 的结构体,用来描述基于像素的分辨率。这个结构体包含了名为 widthheight 的两个存储属性。存储属性是与结构体或者类绑定的,并存储在其中的常量或变量。当这两个属性被初始化为整数 0 的时候,它们会被推断为 Int 类型。

在上面的示例还定义了一个名为 VideoMode 的类,用来描述视频显示器的某个特定视频模式。这个类包含了四个可变的存储属性。第一个, resolution,被初始化为一个新的 Resolution 结构体的实例,属性类型被推断为 Resolution。新 VideoMode 实例同时还会初始化其它三个属性,它们分别是初始值为 falseinterlaced(意为“非隔行视频”),初始值为 0.0frameRate,以及值为可选 Stringname。因为 name 是一个可选类型,它会被自动赋予一个默认值 nil,意为“没有 name 值”。

结构体和类的实例

Resolution 结构体和 VideoMode 类的定义仅描述了什么是 ResolutionVideoMode。它们并没有描述一个特定的分辨率(resolution)或者视频模式(video mode)。为此,你需要创建结构体或者类的一个实例。

创建结构体和类实例的语法非常相似:

1
2
let someResolution = Resolution()
let someVideoMode = VideoMode()

结构体和类都使用构造器语法来创建新的实例。构造器语法的最简单形式是在结构体或者类的类型名称后跟随一对空括号,如 Resolution()VideoMode()。通过这种方式所创建的类或者结构体实例,其属性均会被初始化为默认值。构造过程 章节会对类和结构体的初始化进行更详细的讨论。

属性访问

你可以通过使用点语法访问实例的属性。其语法规则是,实例名后面紧跟属性名,两者以点号(.)分隔,不带空格:

1
2
print("The width of someResolution is \(someResolution.width)")
// 打印 "The width of someResolution is 0"

在上面的例子中,someResolution.width 引用 someResolutionwidth 属性,返回 width 的初始值 0

你也可以访问子属性,如 VideoModeresolution 属性的 width 属性:

1
2
print("The width of someVideoMode is \(someVideoMode.resolution.width)")
// 打印 "The width of someVideoMode is 0"

你也可以使用点语法为可变属性赋值:

1
2
3
someVideoMode.resolution.width = 1280
print("The width of someVideoMode is now \(someVideoMode.resolution.width)")
// 打印 "The width of someVideoMode is now 1280"

结构体类型的成员逐一构造器

所有结构体都有一个自动生成的成员逐一构造器,用于初始化新结构体实例中成员的属性。新实例中各个属性的初始值可以通过属性的名称传递到成员逐一构造器之中:

1
let vga = Resolution(width: 640, height: 480)

与结构体不同,类实例没有默认的成员逐一构造器。构造过程 章节会对构造器进行更详细的讨论。

结构体和枚举是值类型

值类型是这样一种类型,当它被赋值给一个变量、常量或者被传递给一个函数的时候,其值会被拷贝

在之前的章节中,你已经大量使用了值类型。实际上,Swift 中所有的基本类型:整数(integer)、浮点数(floating-point number)、布尔值(boolean)、字符串(string)、数组(array)和字典(dictionary),都是值类型,其底层也是使用结构体实现的。

Swift 中所有的结构体和枚举类型都是值类型。这意味着它们的实例,以及实例中所包含的任何值类型的属性,在代码中传递的时候都会被复制。

注意

标准库定义的集合,例如数组,字典和字符串,都对复制进行了优化以降低性能成本。新集合不会立即复制,而是跟原集合共享同一份内存,共享同样的元素。在集合的某个副本要被修改前,才会复制它的元素。而你在代码中看起来就像是立即发生了复制。

请看下面这个示例,其使用了上一个示例中的 Resolution 结构体:

1
2
let hd = Resolution(width: 1920, height: 1080)
var cinema = hd

在以上示例中,声明了一个名为 hd 的常量,其值为一个初始化为全高清视频分辨率(1920 像素宽,1080 像素高)的 Resolution 实例。

然后示例中又声明了一个名为 cinema 的变量,并将 hd 赋值给它。因为 Resolution 是一个结构体,所以会先创建一个现有实例的副本,然后将副本赋值给 cinema 。尽管 hdcinema 有着相同的宽(width)和高(height),但是在幕后它们是两个完全不同的实例。

下面,为了符合数码影院放映的需求(2048 像素宽,1080 像素高),cinemawidth 属性被修改为稍微宽一点的 2K 标准:

1
cinema.width = 2048

查看 cinemawidth 属性,它的值确实改为了 2048

1
2
print("cinema is now  \(cinema.width) pixels wide")
// 打印 "cinema is now 2048 pixels wide"

然而,初始的 hd 实例中 width 属性还是 1920

1
2
print("hd is still \(hd.width) pixels wide")
// 打印 "hd is still 1920 pixels wide"

hd 赋值给 cinema 时,hd 中所存储的会拷贝到新的 cinema 实例中。结果就是两个完全独立的实例包含了相同的数值。由于两者相互独立,因此将 cinemawidth 修改为 2048 并不会影响 hd 中的 width 的值,如下图所示:

sharedStateStruct_2x

枚举也遵循相同的行为准则:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
enum CompassPoint {
case north, south, east, west
mutating func turnNorth() {
self = .north
}
}
var currentDirection = CompassPoint.west
let rememberedDirection = currentDirection
currentDirection.turnNorth()

print("The current direction is \(currentDirection)")
print("The remembered direction is \(rememberedDirection)")
// 打印 "The current direction is north"
// 打印 "The remembered direction is west"

rememberedDirection 被赋予了 currentDirection 的值,实际上它被赋予的是值的一个拷贝。赋值过程结束后再修改 currentDirection 的值并不影响 rememberedDirection 所储存的原始值的拷贝。

类是引用类型

与值类型不同,引用类型在被赋予到一个变量、常量或者被传递到一个函数时,其值不会被拷贝。因此,使用的是已存在实例的引用,而不是其拷贝。

请看下面这个示例,其使用了之前定义的 VideoMode 类:

1
2
3
4
5
let tenEighty = VideoMode()
tenEighty.resolution = hd
tenEighty.interlaced = true
tenEighty.name = "1080i"
tenEighty.frameRate = 25.0

以上示例中,声明了一个名为 tenEighty 的常量,并让其引用一个 VideoMode 类的新实例。它的视频模式(video mode)被赋值为之前创建的 HD 分辨率(1920*1080)的一个拷贝。然后将它设置为隔行视频,名字设为 “1080i”,并将帧率设置为 25.0 帧每秒。

接下来,将 tenEighty 赋值给一个名为 alsoTenEighty 的新常量,并修改 alsoTenEighty 的帧率:

1
2
let alsoTenEighty = tenEighty
alsoTenEighty.frameRate = 30.0

因为类是引用类型,所以 tenEightalsoTenEight 实际上引用的是同一个 VideoMode 实例。换句话说,它们是同一个实例的两种叫法,如下图所示:

sharedStateClass_2x

通过查看 tenEightyframeRate 属性,可以看到它正确地显示了底层的 VideoMode 实例的新帧率 30.0

1
2
print("The frameRate property of tenEighty is now \(tenEighty.frameRate)")
// 打印 "The frameRate property of theEighty is now 30.0"

这个例子也显示了为何引用类型更加难以理解。如果 tenEightyalsoTenEighty 在你代码中的位置相距很远,那么就很难找到所有修改视频模式的地方。无论在哪使用 tenEighty,你都要考虑使用 alsoTenEighty 的代码,反之亦然。相反,值类型就更容易理解了,因为你的源码中与同一个值交互的代码都很近。

需要注意的是 tenEightyalsoTenEighty 被声明为常量而不是变量。然而你依然可以改变 tenEighty.frameRatealsoTenEighty.frameRate,这是因为 tenEightyalsoTenEighty 这两个常量的值并未改变。它们并不“存储”这个 VideoMode 实例,而仅仅是对 VideoMode 实例的引用。所以,改变的是底层 VideoMode 实例的 frameRate 属性,而不是指向 VideoMode 的常量引用的值。

恒等运算符

因为类是引用类型,所以多个常量和变量可能在幕后同时引用同一个类实例。(对于结构体和枚举来说,这并不成立。因为它们作为值类型,在被赋予到常量、变量或者传递到函数时,其值总是会被拷贝。)

判定两个常量或者变量是否引用同一个类实例有时很有用。为了达到这个目的,Swift 提供了两个恒等运算符:

  • 相同(===
  • 不相同(!==

使用这两个运算符检测两个常量或者变量是否引用了同一个实例:

1
2
3
4
if tenEighty === alsoTenEighty {
print("tenEighty and alsoTenEighty refer to the same VideoMode instance.")
}
// 打印 "tenEighty and alsoTenEighty refer to the same VideoMode instance."

请注意,“相同”(用三个等号表示,===)与“等于”(用两个等号表示,==)的不同。“相同”表示两个类类型(class type)的常量或者变量引用同一个类实例。“等于”表示两个实例的值“相等”或“等价”,判定时要遵照设计者定义的评判标准。

当在定义你的自定义结构体和类的时候,你有义务来决定判定两个实例“相等”的标准。在章节 等价操作符 中将会详细介绍实现自定义 == 和 !== 运算符的流程。

指针

如果你有 C,C++ 或者 Objective-C 语言的经验,那么你也许会知道这些语言使用指针来引用内存中的地址。Swift 中引用了某个引用类型实例的常量或变量,与 C 语言中的指针类似,不过它并不直接指向某个内存地址,也不要求你使用星号(*)来表明你在创建一个引用。相反,Swift 中引用的定义方式与其它的常量或变量的一样。如果需要直接与指针交互,你可以使用标准库提供的指针和缓冲区类型 —— 参见 手动管理内存

留言與分享

swift枚举

分類 编程语言, swift

枚举

枚举为一组相关的值定义了一个共同的类型,使你可以在你的代码中以类型安全的方式来使用这些值。

如果你熟悉 C 语言,你会知道在 C 语言中,枚举会为一组整型值分配相关联的名称。Swift 中的枚举更加灵活,不必给每一个枚举成员提供一个值。如果给枚举成员提供一个值(称为原始值),则该值的类型可以是字符串、字符,或是一个整型值或浮点数。

此外,枚举成员可以指定任意类型的关联值存储到枚举成员中,就像其他语言中的联合体(unions)和变体(variants)。你可以在一个枚举中定义一组相关的枚举成员,每一个枚举成员都可以有适当类型的关联值。

在 Swift 中,枚举类型是一等(first-class)类型。它们采用了很多在传统上只被类(class)所支持的特性,例如计算属性(computed properties),用于提供枚举值的附加信息,实例方法(instance methods),用于提供和枚举值相关联的功能。枚举也可以定义构造函数(initializers)来提供一个初始值;可以在原始实现的基础上扩展它们的功能;还可以遵循协议(protocols)来提供标准的功能。

想了解更多相关信息,请参见 属性方法构造过程扩展协议

枚举语法

使用 enum 关键词来创建枚举并且把它们的整个定义放在一对大括号内:

1
2
3
enum SomeEnumeration {
// 枚举定义放在这里
}

下面是用枚举表示指南针四个方向的例子:

1
2
3
4
5
6
enum CompassPoint {
case north
case south
case east
case west
}

枚举中定义的值(如 northsoutheastwest)是这个枚举的成员值(或成员)。你可以使用 case 关键字来定义一个新的枚举成员值。

注意

与 C 和 Objective-C 不同,Swift 的枚举成员在被创建时不会被赋予一个默认的整型值。在上面的 CompassPoint 例子中,northsoutheastwest 不会被隐式地赋值为 0123。相反,这些枚举成员本身就是完备的值,这些值的类型是已经明确定义好的 CompassPoint 类型。

多个成员值可以出现在同一行上,用逗号隔开:

1
2
3
enum Planet {
case mercury, venus, earth, mars, jupiter, saturn, uranus, neptune
}

每个枚举定义了一个全新的类型。像 Swift 中其他类型一样,它们的名字(例如 CompassPointPlanet)以一个大写字母开头。给枚举类型起一个单数名字而不是复数名字,以便于:

1
var directionToHead = CompassPoint.west

directionToHead 的类型可以在它被 CompassPoint 的某个值初始化时推断出来。一旦 directionToHead 被声明为 CompassPoint 类型,你可以使用更简短的点语法将其设置为另一个 CompassPoint 的值:

1
directionToHead = .east

directionToHead 的类型已知时,再次为其赋值可以省略枚举类型名。在使用具有显式类型的枚举值时,这种写法让代码具有更好的可读性。

使用 Switch 语句匹配枚举值

你可以使用 switch 语句匹配单个枚举值:

1
2
3
4
5
6
7
8
9
10
11
12
directionToHead = .south
switch directionToHead {
case .north:
print("Lots of planets have a north")
case .south:
print("Watch out for penguins")
case .east:
print("Where the sun rises")
case .west:
print("Where the skies are blue")
}
// 打印“Watch out for penguins”

你可以这样理解这段代码:

“判断 directionToHead 的值。当它等于 .north,打印 “Lots of planets have a north”。当它等于 .south,打印 “Watch out for penguins”。”

……以此类推。

正如在 控制流 中介绍的那样,在判断一个枚举类型的值时,switch 语句必须穷举所有情况。如果忽略了 .west 这种情况,上面那段代码将无法通过编译,因为它没有考虑到 CompassPoint 的全部成员。强制穷举确保了枚举成员不会被意外遗漏。

当不需要匹配每个枚举成员的时候,你可以提供一个 default 分支来涵盖所有未明确处理的枚举成员:

1
2
3
4
5
6
7
8
let somePlanet = Planet.earth
switch somePlanet {
case .earth:
print("Mostly harmless")
default:
print("Not a safe place for humans")
}
// 打印“Mostly harmless”

枚举成员的遍历

在一些情况下,你会需要得到一个包含枚举所有成员的集合。可以通过如下代码实现:

令枚举遵循 CaseIterable 协议。Swift 会生成一个 allCases 属性,用于表示一个包含枚举所有成员的集合。下面是一个例子:

1
2
3
4
5
6
enum Beverage: CaseIterable {
case coffee, tea, juice
}
let numberOfChoices = Beverage.allCases.count
print("\(numberOfChoices) beverages available")
// 打印“3 beverages available”

在前面的例子中,通过 Beverage.allCases 可以访问到包含 Beverage 枚举所有成员的集合。allCases 的使用方法和其它一般集合一样——集合中的元素是枚举类型的实例,所以在上面的情况中,这些元素是 Beverage 值。在前面的例子中,统计了总共有多少个枚举成员。而在下面的例子中,则使用 for 循环来遍历所有枚举成员。

1
2
3
4
5
6
for beverage in Beverage.allCases {
print(beverage)
}
// coffee
// tea
// juice

在前面的例子中,使用的语法表明这个枚举遵循 CaseIterable 协议。想了解 protocols 相关信息,请参见 协议

关联值

枚举语法那一小节的例子演示了如何定义和分类枚举的成员。你可以为 Planet.earth 设置一个常量或者变量,并在赋值之后查看这个值。然而,有时候把其他类型的值和成员值一起存储起来会很有用。这额外的信息称为关联值,并且你每次在代码中使用该枚举成员时,还可以修改这个关联值。

你可以定义 Swift 枚举来存储任意类型的关联值,如果需要的话,每个枚举成员的关联值类型可以各不相同。枚举的这种特性跟其他语言中的可识别联合(discriminated unions),标签联合(tagged unions),或者变体(variants)相似。

例如,假设一个库存跟踪系统需要利用两种不同类型的条形码来跟踪商品。有些商品上标有使用 09 的数字的 UPC 格式的一维条形码。每一个条形码都有一个代表数字系统的数字,该数字后接五位代表厂商代码的数字,接下来是五位代表“产品代码”的数字。最后一个数字是检查位,用来验证代码是否被正确扫描:

其他商品上标有 QR 码格式的二维码,它可以使用任何 ISO 8859-1 字符,并且可以编码一个最多拥有 2,953 个字符的字符串:

这便于库存跟踪系统用包含四个整型值的元组存储 UPC 码,以及用任意长度的字符串储存 QR 码。

在 Swift 中,使用如下方式定义表示两种商品条形码的枚举:

1
2
3
4
enum Barcode {
case upc(Int, Int, Int, Int)
case qrCode(String)
}

以上代码可以这么理解:

“定义一个名为 Barcode 的枚举类型,它的一个成员值是具有 (Int,Int,Int,Int) 类型关联值的 upc,另一个成员值是具有 String 类型关联值的 qrCode。”

这个定义不提供任何 IntString 类型的关联值,它只是定义了,当 Barcode 常量和变量等于 Barcode.upcBarcode.qrCode 时,可以存储的关联值的类型。

然后你可以使用任意一种条形码类型创建新的条形码,例如:

1
var productBarcode = Barcode.upc(8, 85909, 51226, 3)

上面的例子创建了一个名为 productBarcode 的变量,并将 Barcode.upc 赋值给它,关联的元组值为 (8, 85909, 51226, 3)

同一个商品可以被分配一个不同类型的条形码,例如:

1
productBarcode = .qrCode("ABCDEFGHIJKLMNOP")

这时,原始的 Barcode.upc 和其整数关联值被新的 Barcode.qrCode 和其字符串关联值所替代。Barcode 类型的常量和变量可以存储一个 .upc 或者一个 .qrCode(连同它们的关联值),但是在同一时间只能存储这两个值中的一个。

你可以使用一个 switch 语句来检查不同的条形码类型,和之前使用 Switch 语句来匹配枚举值的例子一样。然而,这一次,关联值可以被提取出来作为 switch 语句的一部分。你可以在 switch 的 case 分支代码中提取每个关联值作为一个常量(用 let 前缀)或者作为一个变量(用 var 前缀)来使用:

1
2
3
4
5
6
7
switch productBarcode {
case .upc(let numberSystem, let manufacturer, let product, let check):
print("UPC: \(numberSystem), \(manufacturer), \(product), \(check).")
case .qrCode(let productCode):
print("QR code: \(productCode).")
}
// 打印“QR code: ABCDEFGHIJKLMNOP.”

如果一个枚举成员的所有关联值都被提取为常量,或者都被提取为变量,为了简洁,你可以只在成员名称前标注一个 let 或者 var

1
2
3
4
5
6
7
switch productBarcode {
case let .upc(numberSystem, manufacturer, product, check):
print("UPC: \(numberSystem), \(manufacturer), \(product), \(check).")
case let .qrCode(productCode):
print("QR code: \(productCode).")
}
// 打印“QR code: ABCDEFGHIJKLMNOP.”

原始值

关联值 小节的条形码例子中,演示了如何声明存储不同类型关联值的枚举成员。作为关联值的替代选择,枚举成员可以被默认值(称为原始值)预填充,这些原始值的类型必须相同。

这是一个使用 ASCII 码作为原始值的枚举:

1
2
3
4
5
enum ASCIIControlCharacter: Character {
case tab = "\t"
case lineFeed = "\n"
case carriageReturn = "\r"
}

枚举类型 ASCIIControlCharacter 的原始值类型被定义为 Character,并设置了一些比较常见的 ASCII 控制字符。Character 的描述详见 字符串和字符 部分。

原始值可以是字符串、字符,或者任意整型值或浮点型值。每个原始值在枚举声明中必须是唯一的。

注意

原始值和关联值是不同的。原始值是在定义枚举时被预先填充的值,像上述三个 ASCII 码。对于一个特定的枚举成员,它的原始值始终不变。关联值是创建一个基于枚举成员的常量或变量时才设置的值,枚举成员的关联值可以变化。

原始值的隐式赋值

在使用原始值为整数或者字符串类型的枚举时,不需要显式地为每一个枚举成员设置原始值,Swift 将会自动为你赋值。

例如,当使用整数作为原始值时,隐式赋值的值依次递增 1。如果第一个枚举成员没有设置原始值,其原始值将为 0

下面的枚举是对之前 Planet 这个枚举的一个细化,利用整型的原始值来表示每个行星在太阳系中的顺序:

1
2
3
enum Planet: Int {
case mercury = 1, venus, earth, mars, jupiter, saturn, uranus, neptune
}

在上面的例子中,Plant.mercury 的显式原始值为 1Planet.venus 的隐式原始值为 2,依次类推。

当使用字符串作为枚举类型的原始值时,每个枚举成员的隐式原始值为该枚举成员的名称。

下面的例子是 CompassPoint 枚举的细化,使用字符串类型的原始值来表示各个方向的名称:

1
2
3
enum CompassPoint: String {
case north, south, east, west
}

上面例子中,CompassPoint.south 拥有隐式原始值 south,依次类推。

使用枚举成员的 rawValue 属性可以访问该枚举成员的原始值:

1
2
3
4
5
let earthsOrder = Planet.earth.rawValue
// earthsOrder 值为 3

let sunsetDirection = CompassPoint.west.rawValue
// sunsetDirection 值为 "west"

使用原始值初始化枚举实例

如果在定义枚举类型的时候使用了原始值,那么将会自动获得一个初始化方法,这个方法接收一个叫做 rawValue 的参数,参数类型即为原始值类型,返回值则是枚举成员或 nil。你可以使用这个初始化方法来创建一个新的枚举实例。

这个例子利用原始值 7 创建了枚举成员 Uranus

1
2
let possiblePlanet = Planet(rawValue: 7)
// possiblePlanet 类型为 Planet? 值为 Planet.uranus

然而,并非所有 Int 值都可以找到一个匹配的行星。因此,原始值构造器总是返回一个可选的枚举成员。在上面的例子中,possiblePlanetPlanet? 类型,或者说“可选的 Planet”。

注意

原始值构造器是一个可失败构造器,因为并不是每一个原始值都有与之对应的枚举成员。更多信息请参见 可失败构造器

如果你试图寻找一个位置为 11 的行星,通过原始值构造器返回的可选 Planet 值将是 nil

1
2
3
4
5
6
7
8
9
10
11
12
let positionToFind = 11
if let somePlanet = Planet(rawValue: positionToFind) {
switch somePlanet {
case .earth:
print("Mostly harmless")
default:
print("Not a safe place for humans")
}
} else {
print("There isn't a planet at position \(positionToFind)")
}
// 打印“There isn't a planet at position 11”

这个例子使用了可选绑定(optional binding),试图通过原始值 11 来访问一个行星。if let somePlanet = Planet(rawValue: 11) 语句创建了一个可选 Planet,如果可选 Planet 的值存在,就会赋值给 somePlanet。在这个例子中,无法检索到位置为 11 的行星,所以 else 分支被执行。

递归枚举

递归枚举是一种枚举类型,它有一个或多个枚举成员使用该枚举类型的实例作为关联值。使用递归枚举时,编译器会插入一个间接层。你可以在枚举成员前加上 indirect 来表示该成员可递归。

例如,下面的例子中,枚举类型存储了简单的算术表达式:

1
2
3
4
5
enum ArithmeticExpression {
case number(Int)
indirect case addition(ArithmeticExpression, ArithmeticExpression)
indirect case multiplication(ArithmeticExpression, ArithmeticExpression)
}

你也可以在枚举类型开头加上 indirect 关键字来表明它的所有成员都是可递归的:

1
2
3
4
5
indirect enum ArithmeticExpression {
case number(Int)
case addition(ArithmeticExpression, ArithmeticExpression)
case multiplication(ArithmeticExpression, ArithmeticExpression)
}

上面定义的枚举类型可以存储三种算术表达式:纯数字、两个表达式相加、两个表达式相乘。枚举成员 additionmultiplication 的关联值也是算术表达式——这些关联值使得嵌套表达式成为可能。例如,表达式 (5 + 4) * 2,乘号右边是一个数字,左边则是另一个表达式。因为数据是嵌套的,因而用来存储数据的枚举类型也需要支持这种嵌套——这意味着枚举类型需要支持递归。下面的代码展示了使用 ArithmeticExpression 这个递归枚举创建表达式 (5 + 4) * 2

1
2
3
4
let five = ArithmeticExpression.number(5)
let four = ArithmeticExpression.number(4)
let sum = ArithmeticExpression.addition(five, four)
let product = ArithmeticExpression.multiplication(sum, ArithmeticExpression.number(2))

要操作具有递归性质的数据结构,使用递归函数是一种直截了当的方式。例如,下面是一个对算术表达式求值的函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
func evaluate(_ expression: ArithmeticExpression) -> Int {
switch expression {
case let .number(value):
return value
case let .addition(left, right):
return evaluate(left) + evaluate(right)
case let .multiplication(left, right):
return evaluate(left) * evaluate(right)
}
}

print(evaluate(product))
// 打印“18”

该函数如果遇到纯数字,就直接返回该数字的值。如果遇到的是加法或乘法运算,则分别计算左边表达式和右边表达式的值,然后相加或相乘。

留言與分享

swift闭包

分類 编程语言, swift

闭包

闭包是自包含的函数代码块,可以在代码中被传递和使用。Swift 中的闭包与 C 和 Objective-C 中的代码块(blocks)以及其他一些编程语言中的匿名函数(Lambdas)比较相似。

闭包可以捕获和存储其所在上下文中任意常量和变量的引用。被称为包裹常量和变量。 Swift 会为你管理在捕获过程中涉及到的所有内存操作。

注意

如果你不熟悉捕获(capturing)这个概念也不用担心,在 值捕获 章节有它更详细的介绍。

函数 章节中介绍的全局和嵌套函数实际上也是特殊的闭包,闭包采用如下三种形式之一:

  • 全局函数是一个有名字但不会捕获任何值的闭包
  • 嵌套函数是一个有名字并可以捕获其封闭函数域内值的闭包
  • 闭包表达式是一个利用轻量级语法所写的可以捕获其上下文中变量或常量值的匿名闭包

Swift 的闭包表达式拥有简洁的风格,并鼓励在常见场景中进行语法优化,主要优化如下:

  • 利用上下文推断参数和返回值类型
  • 隐式返回单表达式闭包,即单表达式闭包可以省略 return 关键字
  • 参数名称缩写
  • 尾随闭包语法

闭包表达式

嵌套函数 作为复杂函数的一部分时,它自包含代码块式的定义和命名形式在使用上带来了方便。当然,编写未完整声明和没有函数名的类函数结构代码是很有用的,尤其是在编码中涉及到函数作为参数的那些方法时。

闭包表达式是一种构建内联闭包的方式,它的语法简洁。在保证不丢失它语法清晰明了的同时,闭包表达式提供了几种优化的语法简写形式。下面通过对 sorted(by:) 这一个案例的多次迭代改进来展示这个过程,每次迭代都使用了更加简明的方式描述了相同功能。。

排序方法

Swift 标准库提供了名为 sorted(by:) 的方法,它会基于你提供的排序闭包表达式的判断结果对数组中的值(类型确定)进行排序。一旦它完成排序过程,sorted(by:) 方法会返回一个与旧数组类型大小相同类型的新数组,该数组的元素有着正确的排序顺序。原数组不会被 sorted(by:) 方法修改。

下面的闭包表达式示例使用 sorted(by:) 方法对一个 String 类型的数组进行字母逆序排序。以下是初始数组:

1
let names = ["Chris", "Alex", "Ewa", "Barry", "Daniella"]

sorted(by:) 方法接受一个闭包,该闭包函数需要传入与数组元素类型相同的两个值,并返回一个布尔类型值来表明当排序结束后传入的第一个参数排在第二个参数前面还是后面。如果第一个参数值出现在第二个参数值前面,排序闭包函数需要返回 true,反之返回 false

该例子对一个 String 类型的数组进行排序,因此排序闭包函数类型需为 (String, String) -> Bool

提供排序闭包函数的一种方式是撰写一个符合其类型要求的普通函数,并将其作为 sorted(by:) 方法的参数传入:

1
2
3
4
5
func backward(_ s1: String, _ s2: String) -> Bool {
return s1 > s2
}
var reversedNames = names.sorted(by: backward)
// reversedNames 为 ["Ewa", "Daniella", "Chris", "Barry", "Alex"]

如果第一个字符串(s1)大于第二个字符串(s2),backward(_:_:) 函数会返回 true,表示在新的数组中 s1 应该出现在 s2 前。对于字符串中的字符来说,“大于”表示“按照字母顺序较晚出现”。这意味着字母 "B" 大于字母 "A" ,字符串 "Tom" 大于字符串 "Tim"。该闭包将进行字母逆序排序,"Barry" 将会排在 "Alex" 之前。

然而,以这种方式来编写一个实际上很简单的表达式(a > b),确实太过繁琐了。对于这个例子来说,利用闭包表达式语法可以更好地构造一个内联排序闭包。

闭包表达式语法

闭包表达式语法有如下的一般形式:

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

闭包表达式参数 可以是 in-out 参数,但不能设定默认值。如果你命名了可变参数,也可以使用此可变参数。元组也可以作为参数和返回值。

下面的例子展示了之前 backward(_:_:) 函数对应的闭包表达式版本的代码:

1
2
3
reversedNames = names.sorted(by: { (s1: String, s2: String) -> Bool in
return s1 > s2
})

需要注意的是内联闭包参数和返回值类型声明与 backward(_:_:) 函数类型声明相同。在这两种方式中,都写成了 (s1: String, s2: String) -> Bool。然而在内联闭包表达式中,函数和返回值类型都写在大括号内,而不是大括号外。

闭包的函数体部分由关键字 in 引入。该关键字表示闭包的参数和返回值类型定义已经完成,闭包函数体即将开始。

由于这个闭包的函数体部分如此短,以至于可以将其改写成一行代码:

1
reversedNames = names.sorted(by: { (s1: String, s2: String) -> Bool in return s1 > s2 } )

该例中 sorted(by:) 方法的整体调用保持不变,一对圆括号仍然包裹住了方法的整个参数。然而,参数现在变成了内联闭包。

根据上下文推断类型

因为排序闭包函数是作为 sorted(by:) 方法的参数传入的,Swift 可以推断其参数和返回值的类型。sorted(by:) 方法被一个字符串数组调用,因此其参数必须是 (String, String) -> Bool 类型的函数。这意味着 (String, String)Bool 类型并不需要作为闭包表达式定义的一部分。因为所有的类型都可以被正确推断,返回箭头(->)和围绕在参数周围的括号也可以被省略:

1
reversedNames = names.sorted(by: { s1, s2 in return s1 > s2 } )

实际上,通过内联闭包表达式构造的闭包作为参数传递给函数或方法时,总是能够推断出闭包的参数和返回值类型。这意味着闭包作为函数或者方法的参数时,你几乎不需要利用完整格式构造内联闭包。

尽管如此,你仍然可以明确写出有着完整格式的闭包。如果完整格式的闭包能够提高代码的可读性,则我们更鼓励采用完整格式的闭包。而在 sorted(by:) 方法这个例子里,显然闭包的目的就是排序。由于这个闭包是为了处理字符串数组的排序,因此读者能够推测出这个闭包是用于字符串处理的。

单表达式闭包的隐式返回

单行表达式闭包可以通过省略 return 关键字来隐式返回单行表达式的结果,如上版本的例子可以改写为:

1
reversedNames = names.sorted(by: { s1, s2 in s1 > s2 } )

在这个例子中,sorted(by:) 方法的参数类型明确了闭包必须返回一个 Bool 类型值。因为闭包函数体只包含了一个单一表达式(s1 > s2),该表达式返回 Bool 类型值,因此这里没有歧义,return 关键字可以省略。

参数名称缩写

Swift 自动为内联闭包提供了参数名称缩写功能,你可以直接通过 $0$1$2 来顺序调用闭包的参数,以此类推。

如果你在闭包表达式中使用参数名称缩写,你可以在闭包定义中省略参数列表,并且对应参数名称缩写的类型会通过函数类型进行推断。in 关键字也同样可以被省略,因为此时闭包表达式完全由闭包函数体构成:

1
reversedNames = names.sorted(by: { $0 > $1 } )

在这个例子中,$0$1 表示闭包中第一个和第二个 String 类型的参数。

运算符方法

实际上还有一种更简短的方式来编写上面例子中的闭包表达式。Swift 的 String 类型定义了关于大于号(>)的字符串实现,其作为一个函数接受两个 String 类型的参数并返回 Bool 类型的值。而这正好与 sorted(by:) 方法的参数需要的函数类型相符合。因此,你可以简单地传递一个大于号,Swift 可以自动推断找到系统自带的那个字符串函数的实现:

1
reversedNames = names.sorted(by: >)

更多关于运算符方法的内容请查看 运算符方法

尾随闭包

如果你需要将一个很长的闭包表达式作为最后一个参数传递给函数,将这个闭包替换成为尾随闭包的形式很有用。尾随闭包是一个书写在函数圆括号之后的闭包表达式,函数支持将其作为最后一个参数调用。在使用尾随闭包时,你不用写出它的参数标签:

1
2
3
4
5
6
7
8
9
10
11
12
13
func someFunctionThatTakesAClosure(closure: () -> Void) {
// 函数体部分
}

// 以下是不使用尾随闭包进行函数调用
someFunctionThatTakesAClosure(closure: {
// 闭包主体部分
})

// 以下是使用尾随闭包进行函数调用
someFunctionThatTakesAClosure() {
// 闭包主体部分
}

闭包表达式语法 上章节中的字符串排序闭包可以作为尾随包的形式改写在 sorted(by:) 方法圆括号的外面:

1
reversedNames = names.sorted() { $0 > $1 }

如果闭包表达式是函数或方法的唯一参数,则当你使用尾随闭包时,你甚至可以把 () 省略掉:

1
reversedNames = names.sorted { $0 > $1 }

当闭包非常长以至于不能在一行中进行书写时,尾随闭包变得非常有用。举例来说,Swift 的 Array 类型有一个 map(_:) 方法,这个方法获取一个闭包表达式作为其唯一参数。该闭包函数会为数组中的每一个元素调用一次,并返回该元素所映射的值。具体的映射方式和返回值类型由闭包来指定。

当提供给数组的闭包应用于每个数组元素后,map(_:) 方法将返回一个新的数组,数组中包含了与原数组中的元素一一对应的映射后的值。

下例介绍了如何在 map(_:) 方法中使用尾随闭包将 Int 类型数组 [16, 58, 510] 转换为包含对应 String 类型的值的数组 ["OneSix", "FiveEight", "FiveOneZero"]

1
2
3
4
5
let digitNames = [
0: "Zero", 1: "One", 2: "Two", 3: "Three", 4: "Four",
5: "Five", 6: "Six", 7: "Seven", 8: "Eight", 9: "Nine"
]
let numbers = [16, 58, 510]

如上代码创建了一个整型数位和它们英文版本名字相映射的字典。同时还定义了一个准备转换为字符串数组的整型数组。

你现在可以通过传递一个尾随闭包给 numbers 数组的 map(_:) 方法来创建对应的字符串版本数组:

1
2
3
4
5
6
7
8
9
10
11
12
let strings = numbers.map {
(number) -> String in
var number = number
var output = ""
repeat {
output = digitNames[number % 10]! + output
number /= 10
} while number > 0
return output
}
// strings 常量被推断为字符串类型数组,即 [String]
// 其值为 ["OneSix", "FiveEight", "FiveOneZero"]

map(_:) 为数组中每一个元素调用了一次闭包表达式。你不需要指定闭包的输入参数 number 的类型,因为可以通过要映射的数组类型进行推断。

在该例中,局部变量 number 的值由闭包中的 number 参数获得,因此可以在闭包函数体内对其进行修改,(闭包或者函数的参数总是常量),闭包表达式指定了返回类型为 String,以表明存储映射值的新数组类型为 String

闭包表达式在每次被调用的时候创建了一个叫做 output 的字符串并返回。其使用求余运算符(number % 10)计算最后一位数字并利用 digitNames 字典获取所映射的字符串。这个闭包能够用于创建任意正整数的字符串表示。

注意

字典 digitNames 下标后跟着一个叹号(!),因为字典下标返回一个可选值(optional value),表明该键不存在时会查找失败。在上例中,由于可以确定 number % 10 总是 digitNames 字典的有效下标,因此叹号可以用于强制解包(force-unwrap)存储在下标的可选类型的返回值中的 String 类型的值。

digitNames 字典中获取的字符串被添加到 output前部,逆序建立了一个字符串版本的数字。(在表达式 number % 10 中,如果 number16,则返回 658 返回 8510 返回 0。)

number 变量之后除以 10。因为其是整数,在计算过程中未除尽部分被忽略。因此 16 变成了 158 变成了 5510 变成了 51

整个过程重复进行,直到 number /= 100,这时闭包会将字符串 output 返回,而 map(_:) 方法则会将字符串添加到映射数组中。

在上面的例子中,通过尾随闭包语法,优雅地在函数后封装了闭包的具体功能,而不再需要将整个闭包包裹在 map(_:) 方法的括号内。

值捕获

闭包可以在其被定义的上下文中捕获常量或变量。即使定义这些常量和变量的原作用域已经不存在,闭包仍然可以在闭包函数体内引用和修改这些值。

Swift 中,可以捕获值的闭包的最简单形式是嵌套函数,也就是定义在其他函数的函数体内的函数。嵌套函数可以捕获其外部函数所有的参数以及定义的常量和变量。

举个例子,这有一个叫做 makeIncrementer 的函数,其包含了一个叫做 incrementer 的嵌套函数。嵌套函数 incrementer() 从上下文中捕获了两个值,runningTotalamount。捕获这些值之后,makeIncrementerincrementer 作为闭包返回。每次调用 incrementer 时,其会以 amount 作为增量增加 runningTotal 的值。

1
2
3
4
5
6
7
8
func makeIncrementer(forIncrement amount: Int) -> () -> Int {
var runningTotal = 0
func incrementer() -> Int {
runningTotal += amount
return runningTotal
}
return incrementer
}

makeIncrementer 返回类型为 () -> Int。这意味着其返回的是一个函数,而非一个简单类型的值。该函数在每次调用时不接受参数,只返回一个 Int 类型的值。关于函数返回其他函数的内容,请查看 函数类型作为返回类型

makeIncrementer(forIncrement:) 函数定义了一个初始值为 0 的整型变量 runningTotal,用来存储当前总计数值。该值为 incrementer 的返回值。

makeIncrementer(forIncrement:) 有一个 Int 类型的参数,其外部参数名为 forIncrement,内部参数名为 amount,该参数表示每次 incrementer 被调用时 runningTotal 将要增加的量。makeIncrementer 函数还定义了一个嵌套函数 incrementer,用来执行实际的增加操作。该函数简单地使 runningTotal 增加 amount,并将其返回。

如果我们单独考虑嵌套函数 incrementer(),会发现它有些不同寻常:

1
2
3
4
func incrementer() -> Int {
runningTotal += amount
return runningTotal
}

incrementer() 函数并没有任何参数,但是在函数体内访问了 runningTotalamount 变量。这是因为它从外围函数捕获了 runningTotalamount 变量的引用。捕获引用保证了 runningTotalamount 变量在调用完 makeIncrementer 后不会消失,并且保证了在下一次执行 incrementer 函数时,runningTotal 依旧存在。

注意

为了优化,如果一个值不会被闭包改变,或者在闭包创建后不会改变,Swift 可能会改为捕获并保存一份对值的拷贝。

Swift 也会负责被捕获变量的所有内存管理工作,包括释放不再需要的变量。

下面是一个使用 makeIncrementer 的例子:

1
let incrementByTen = makeIncrementer(forIncrement: 10)

该例子定义了一个叫做 incrementByTen 的常量,该常量指向一个每次调用会将其 runningTotal 变量增加 10incrementer 函数。调用这个函数多次可以得到以下结果:

1
2
3
4
5
6
incrementByTen()
// 返回的值为10
incrementByTen()
// 返回的值为20
incrementByTen()
// 返回的值为30

如果你创建了另一个 incrementer,它会有属于自己的引用,指向一个全新、独立的 runningTotal 变量:

1
2
3
let incrementBySeven = makeIncrementer(forIncrement: 7)
incrementBySeven()
// 返回的值为7

再次调用原来的 incrementByTen 会继续增加它自己的 runningTotal 变量,该变量和 incrementBySeven 中捕获的变量没有任何联系:

1
2
incrementByTen()
// 返回的值为40

注意

如果你将闭包赋值给一个类实例的属性,并且该闭包通过访问该实例或其成员而捕获了该实例,你将在闭包和该实例间创建一个循环强引用。Swift 使用捕获列表来打破这种循环强引用。更多信息,请参考 闭包引起的循环强引用

闭包是引用类型

上面的例子中,incrementBySevenincrementByTen 都是常量,但是这些常量指向的闭包仍然可以增加其捕获的变量的值。这是因为函数和闭包都是引用类型

无论你将函数或闭包赋值给一个常量还是变量,你实际上都是将常量或变量的值设置为对应函数或闭包的引用。上面的例子中,指向闭包的引用 incrementByTen 是一个常量,而并非闭包内容本身。

这也意味着如果你将闭包赋值给了两个不同的常量或变量,两个值都会指向同一个闭包:

1
2
3
let alsoIncrementByTen = incrementByTen
alsoIncrementByTen()
// 返回的值为50

逃逸闭包

当一个闭包作为参数传到一个函数中,但是这个闭包在函数返回之后才被执行,我们称该闭包从函数中逃逸。当你定义接受闭包作为参数的函数时,你可以在参数名之前标注 @escaping,用来指明这个闭包是允许“逃逸”出这个函数的。

一种能使闭包“逃逸”出函数的方法是,将这个闭包保存在一个函数外部定义的变量中。举个例子,很多启动异步操作的函数接受一个闭包参数作为 completion handler。这类函数会在异步操作开始之后立刻返回,但是闭包直到异步操作结束后才会被调用。在这种情况下,闭包需要“逃逸”出函数,因为闭包需要在函数返回之后被调用。例如:

1
2
3
4
var completionHandlers: [() -> Void] = []
func someFunctionWithEscapingClosure(completionHandler: @escaping () -> Void) {
completionHandlers.append(completionHandler)
}

someFunctionWithEscapingClosure(_:) 函数接受一个闭包作为参数,该闭包被添加到一个函数外定义的数组中。如果你不将这个参数标记为 @escaping,就会得到一个编译错误。

将一个闭包标记为 @escaping 意味着你必须在闭包中显式地引用 self。比如说,在下面的代码中,传递到 someFunctionWithEscapingClosure(_:) 中的闭包是一个逃逸闭包,这意味着它需要显式地引用 self。相对的,传递到 someFunctionWithNonescapingClosure(_:) 中的闭包是一个非逃逸闭包,这意味着它可以隐式引用 self

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
func someFunctionWithNonescapingClosure(closure: () -> Void) {
closure()
}

class SomeClass {
var x = 10
func doSomething() {
someFunctionWithEscapingClosure { self.x = 100 }
someFunctionWithNonescapingClosure { x = 200 }
}
}

let instance = SomeClass()
instance.doSomething()
print(instance.x)
// 打印出“200”

completionHandlers.first?()
print(instance.x)
// 打印出“100”

自动闭包

自动闭包是一种自动创建的闭包,用于包装传递给函数作为参数的表达式。这种闭包不接受任何参数,当它被调用的时候,会返回被包装在其中的表达式的值。这种便利语法让你能够省略闭包的花括号,用一个普通的表达式来代替显式的闭包。

我们经常会调用采用自动闭包的函数,但是很少去实现这样的函数。举个例子来说,assert(condition:message:file:line:) 函数接受自动闭包作为它的 condition 参数和 message 参数;它的 condition 参数仅会在 debug 模式下被求值,它的 message 参数仅当 condition 参数为 false 时被计算求值。

自动闭包让你能够延迟求值,因为直到你调用这个闭包,代码段才会被执行。延迟求值对于那些有副作用(Side Effect)和高计算成本的代码来说是很有益处的,因为它使得你能控制代码的执行时机。下面的代码展示了闭包如何延时求值。

1
2
3
4
5
6
7
8
9
10
11
12
var customersInLine = ["Chris", "Alex", "Ewa", "Barry", "Daniella"]
print(customersInLine.count)
// 打印出“5”

let customerProvider = { customersInLine.remove(at: 0) }
print(customersInLine.count)
// 打印出“5”

print("Now serving \(customerProvider())!")
// Prints "Now serving Chris!"
print(customersInLine.count)
// 打印出“4”

尽管在闭包的代码中,customersInLine 的第一个元素被移除了,不过在闭包被调用之前,这个元素是不会被移除的。如果这个闭包永远不被调用,那么在闭包里面的表达式将永远不会执行,那意味着列表中的元素永远不会被移除。请注意,customerProvider 的类型不是 String,而是 () -> String,一个没有参数且返回值为 String 的函数。

将闭包作为参数传递给函数时,你能获得同样的延时求值行为。

1
2
3
4
5
6
// customersInLine is ["Alex", "Ewa", "Barry", "Daniella"]
func serve(customer customerProvider: () -> String) {
print("Now serving \(customerProvider())!")
}
serve(customer: { customersInLine.remove(at: 0) } )
// 打印出“Now serving Alex!”

上面的 serve(customer:) 函数接受一个返回顾客名字的显式的闭包。下面这个版本的 serve(customer:) 完成了相同的操作,不过它并没有接受一个显式的闭包,而是通过将参数标记为 @autoclosure 来接收一个自动闭包。现在你可以将该函数当作接受 String 类型参数(而非闭包)的函数来调用。customerProvider 参数将自动转化为一个闭包,因为该参数被标记了 @autoclosure 特性。

1
2
3
4
5
6
// customersInLine is ["Ewa", "Barry", "Daniella"]
func serve(customer customerProvider: @autoclosure () -> String) {
print("Now serving \(customerProvider())!")
}
serve(customer: customersInLine.remove(at: 0))
// 打印“Now serving Ewa!”

注意

过度使用 autoclosures 会让你的代码变得难以理解。上下文和函数名应该能够清晰地表明求值是被延迟执行的。

如果你想让一个自动闭包可以“逃逸”,则应该同时使用 @autoclosure@escaping 属性。@escaping 属性的讲解见上面的 逃逸闭包

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// customersInLine i= ["Barry", "Daniella"]
var customerProviders: [() -> String] = []
func collectCustomerProviders(_ customerProvider: @autoclosure @escaping () -> String) {
customerProviders.append(customerProvider)
}
collectCustomerProviders(customersInLine.remove(at: 0))
collectCustomerProviders(customersInLine.remove(at: 0))

print("Collected \(customerProviders.count) closures.")
// 打印“Collected 2 closures.”
for customerProvider in customerProviders {
print("Now serving \(customerProvider())!")
}
// 打印“Now serving Barry!”
// 打印“Now serving Daniella!”

在上面的代码中,collectCustomerProviders(_:) 函数并没有调用传入的 customerProvider 闭包,而是将闭包追加到了 customerProviders 数组中。这个数组定义在函数作用域范围外,这意味着数组内的闭包能够在函数返回之后被调用。因此,customerProvider 参数必须允许“逃逸”出函数作用域。

留言與分享

swift函数

分類 编程语言, swift

函数

函数是一段完成特定任务的独立代码片段。你可以通过给函数命名来标识某个函数的功能,这个名字可以被用来在需要的时候“调用”这个函数来完成它的任务。

Swift 统一的函数语法非常的灵活,可以用来表示任何函数,包括从最简单的没有参数名字的 C 风格函数,到复杂的带局部和外部参数名的 Objective-C 风格函数。参数可以提供默认值,以简化函数调用。参数也可以既当做传入参数,也当做传出参数,也就是说,一旦函数执行结束,传入的参数值将被修改。

在 Swift 中,每个函数都有一个由函数的参数值类型和返回值类型组成的类型。你可以把函数类型当做任何其他普通变量类型一样处理,这样就可以更简单地把函数当做别的函数的参数,也可以从其他函数中返回函数。函数的定义可以写在其他函数定义中,这样可以在嵌套函数范围内实现功能封装。

函数的定义与调用

当你定义一个函数时,你可以定义一个或多个有名字和类型的值,作为函数的输入,称为参数,也可以定义某种类型的值作为函数执行结束时的输出,称为返回类型

每个函数有个函数名,用来描述函数执行的任务。要使用一个函数时,用函数名来“调用”这个函数,并传给它匹配的输入值(称作实参)。函数的实参必须与函数参数表里参数的顺序一致。

下面例子中的函数的名字是 greet(person:),之所以叫这个名字,是因为这个函数用一个人的名字当做输入,并返回向这个人问候的语句。为了完成这个任务,你需要定义一个输入参数——一个叫做 personString 值,和一个包含给这个人问候语的 String 类型的返回值:

1
2
3
4
func greet(person: String) -> String {
let greeting = "Hello, " + person + "!"
return greeting
}

所有的这些信息汇总起来成为函数的定义,并以 func 作为前缀。指定函数返回类型时,用返回箭头 ->(一个连字符后跟一个右尖括号)后跟返回类型的名称的方式来表示。

该定义描述了函数的功能,它期望接收什么作为参数和执行结束时它返回的结果是什么类型。这样的定义使得函数可以在别的地方以一种清晰的方式被调用:

1
2
3
4
print(greet(person: "Anna"))
// 打印“Hello, Anna!”
print(greet(person: "Brian"))
// 打印“Hello, Brian!”

调用 greet(person:) 函数时,在圆括号中传给它一个 String 类型的实参,例如 greet(person: "Anna")。正如上面所示,因为这个函数返回一个 String 类型的值,所以 greet 可以被包含在 print(_:separator:terminator:) 的调用中,用来输出这个函数的返回值。

注意

print(_:separator:terminator:) 函数的第一个参数并没有设置一个标签,而其他的参数因为已经有了默认值,因此是可选的。关于这些函数语法上的变化详见下方关于 函数参数标签和参数名以及默认参数值。

greet(person:) 的函数体中,先定义了一个新的名为 greetingString 常量,同时,把对 personName 的问候消息赋值给了 greeting 。然后用 return 关键字把这个问候返回出去。一旦 return greeting 被调用,该函数结束它的执行并返回 greeting 的当前值。

你可以用不同的输入值多次调用 greet(person:)。上面的例子展示的是用 "Anna""Brian" 调用的结果,该函数分别返回了不同的结果。

为了简化这个函数的定义,可以将问候消息的创建和返回写成一句:

1
2
3
4
5
func greetAgain(person: String) -> String {
return "Hello again, " + person + "!"
}
print(greetAgain(person: "Anna"))
// 打印“Hello again, Anna!”

函数参数与返回值

函数参数与返回值在 Swift 中非常的灵活。你可以定义任何类型的函数,包括从只带一个未名参数的简单函数到复杂的带有表达性参数名和不同参数选项的复杂函数。

无参数函数

函数可以没有参数。下面这个函数就是一个无参数函数,当被调用时,它返回固定的 String 消息:

1
2
3
4
5
func sayHelloWorld() -> String {
return "hello, world"
}
print(sayHelloWorld())
// 打印“hello, world”

尽管这个函数没有参数,但是定义中在函数名后还是需要一对圆括号。当被调用时,也需要在函数名后写一对圆括号。

多参数函数

函数可以有多种输入参数,这些参数被包含在函数的括号之中,以逗号分隔。

下面这个函数用一个人名和是否已经打过招呼作为输入,并返回对这个人的适当问候语:

1
2
3
4
5
6
7
8
9
func greet(person: String, alreadyGreeted: Bool) -> String {
if alreadyGreeted {
return greetAgain(person: person)
} else {
return greet(person: person)
}
}
print(greet(person: "Tim", alreadyGreeted: true))
// 打印“Hello again, Tim!”

你可以通过在括号内使用逗号分隔来传递一个 String 参数值和一个标识为 alreadyGreetedBool 值,来调用 greet(person:alreadyGreeted:) 函数。注意这个函数和上面 greet(person:) 是不同的。虽然它们都有着同样的名字 greet,但是 greet(person:alreadyGreeted:) 函数需要两个参数,而 greet(person:) 只需要一个参数。

无返回值函数

函数可以没有返回值。下面是 greet(person:) 函数的另一个版本,这个函数直接打印一个 String 值,而不是返回它:

1
2
3
4
5
func greet(person: String) {
print("Hello, \(person)!")
}
greet(person: "Dave")
// 打印“Hello, Dave!”

因为这个函数不需要返回值,所以这个函数的定义中没有返回箭头(->)和返回类型。

注意

严格地说,即使没有明确定义返回值,该 greet(Person:) 函数仍然返回一个值。没有明确定义返回类型的函数的返回一个 Void 类型特殊值,该值为一个空元组,写成 ()。

调用函数时,可以忽略该函数的返回值:

1
2
3
4
5
6
7
8
9
10
11
func printAndCount(string: String) -> Int {
print(string)
return string.count
}
func printWithoutCounting(string: String) {
let _ = printAndCount(string: string)
}
printAndCount(string: "hello, world")
// 打印“hello, world”,并且返回值 12
printWithoutCounting(string: "hello, world")
// 打印“hello, world”,但是没有返回任何值

第一个函数 printAndCount(string:),输出一个字符串并返回 Int 类型的字符数。第二个函数 printWithoutCounting(string:) 调用了第一个函数,但是忽略了它的返回值。当第二个函数被调用时,消息依然会由第一个函数输出,但是返回值不会被用到。

注意

返回值可以被忽略,但定义了有返回值的函数必须返回一个值,如果在函数定义底部没有返回任何值,将导致编译时错误。

多重返回值函数

你可以用元组(tuple)类型让多个值作为一个复合值从函数中返回。

下例中定义了一个名为 minMax(array:) 的函数,作用是在一个 Int 类型的数组中找出最小值与最大值。

1
2
3
4
5
6
7
8
9
10
11
12
func minMax(array: [Int]) -> (min: Int, max: Int) {
var currentMin = array[0]
var currentMax = array[0]
for value in array[1..<array.count] {
if value < currentMin {
currentMin = value
} else if value > currentMax {
currentMax = value
}
}
return (currentMin, currentMax)
}

minMax(array:) 函数返回一个包含两个 Int 值的元组,这些值被标记为 minmax ,以便查询函数的返回值时可以通过名字访问它们。

minMax(array:) 的函数体中,在开始的时候设置两个工作变量 currentMincurrentMax 的值为数组中的第一个数。然后函数会遍历数组中剩余的值并检查该值是否比 currentMincurrentMax 更小或更大。最后数组中的最小值与最大值作为一个包含两个 Int 值的元组返回。

因为元组的成员值已被命名,因此可以通过 . 语法来检索找到的最小值与最大值:

1
2
3
let bounds = minMax(array: [8, -6, 2, 109, 3, 71])
print("min is \(bounds.min) and max is \(bounds.max)")
// 打印“min is -6 and max is 109”

需要注意的是,元组的成员不需要在元组从函数中返回时命名,因为它们的名字已经在函数返回类型中指定了。

可选元组返回类型

如果函数返回的元组类型有可能整个元组都“没有值”,你可以使用可选的 元组返回类型反映整个元组可以是 nil 的事实。你可以通过在元组类型的右括号后放置一个问号来定义一个可选元组,例如 (Int, Int)?(String, Int, Bool)?

注意

可选元组类型如 (Int, Int)? 与元组包含可选类型如 (Int?, Int?) 是不同的。可选的元组类型,整个元组是可选的,而不只是元组中的每个元素值。

前面的 minMax(array:) 函数返回了一个包含两个 Int 值的元组。但是函数不会对传入的数组执行任何安全检查,如果 array 参数是一个空数组,如上定义的 minMax(array:) 在试图访问 array[0] 时会触发一个运行时错误。

为了安全地处理这个“空数组”问题,将 minMax(array:) 函数改写为使用可选元组返回类型,并且当数组为空时返回 nil

1
2
3
4
5
6
7
8
9
10
11
12
13
func minMax(array: [Int]) -> (min: Int, max: Int)? {
if array.isEmpty { return nil }
var currentMin = array[0]
var currentMax = array[0]
for value in array[1..<array.count] {
if value < currentMin {
currentMin = value
} else if value > currentMax {
currentMax = value
}
}
return (currentMin, currentMax)
}

你可以使用可选绑定来检查 minMax(array:) 函数返回的是一个存在的元组值还是 nil

1
2
3
4
if let bounds = minMax(array: [8, -6, 2, 109, 3, 71]) {
print("min is \(bounds.min) and max is \(bounds.max)")
}
// 打印“min is -6 and max is 109”

隐式返回的函数

如果一个函数的整个函数体是一个单行表达式,这个函数可以隐式地返回这个表达式。举个例子,以下的函数有着同样的作用:

1
2
3
4
5
6
7
8
9
10
11
func greeting(for person: String) -> String {
"Hello, " + person + "!"
}
print(greeting(for: "Dave"))
// 打印 "Hello, Dave!"

func anotherGreeting(for person: String) -> String {
return "Hello, " + person + "!"
}
print(anotherGreeting(for: "Dave"))
// 打印 "Hello, Dave!"

greeting(for:) 函数的完整定义是打招呼内容的返回,这就意味着它能使用隐式返回这样更简短的形式。anothergreeting(for:) 函数返回同样的内容,却因为 return 关键字显得函数更长。任何一个可以被写成一行 return 语句的函数都可以忽略 return

正如你将会在 简略的 Getter 声明 里看到的, 一个属性的 getter 也可以使用隐式返回的形式。

函数参数标签和参数名称

每个函数参数都有一个参数标签(argument label)以及一个参数名称(parameter name)。参数标签在调用函数的时候使用;调用的时候需要将函数的参数标签写在对应的参数前面。参数名称在函数的实现中使用。默认情况下,函数参数使用参数名称来作为它们的参数标签。

1
2
3
4
func someFunction(firstParameterName: Int, secondParameterName: Int) {
// 在函数体内,firstParameterName 和 secondParameterName 代表参数中的第一个和第二个参数值
}
someFunction(firstParameterName: 1, secondParameterName: 2)

所有的参数都必须有一个独一无二的名字。虽然多个参数拥有同样的参数标签是可能的,但是一个唯一的函数标签能够使你的代码更具可读性。

指定参数标签

你可以在参数名称前指定它的参数标签,中间以空格分隔:

1
2
3
func someFunction(argumentLabel parameterName: Int) {
// 在函数体内,parameterName 代表参数值
}

这个版本的 greet(person:) 函数,接收一个人的名字和他的家乡,并且返回一句问候:

1
2
3
4
5
func greet(person: String, from hometown: String) -> String {
return "Hello \(person)! Glad you could visit from \(hometown)."
}
print(greet(person: "Bill", from: "Cupertino"))
// 打印“Hello Bill! Glad you could visit from Cupertino.”

参数标签的使用能够让一个函数在调用时更有表达力,更类似自然语言,并且仍保持了函数内部的可读性以及清晰的意图。

忽略参数标签

如果你不希望为某个参数添加一个标签,可以使用一个下划线(_)来代替一个明确的参数标签。

1
2
3
4
func someFunction(_ firstParameterName: Int, secondParameterName: Int) {
// 在函数体内,firstParameterName 和 secondParameterName 代表参数中的第一个和第二个参数值
}
someFunction(1, secondParameterName: 2)

如果一个参数有一个标签,那么在调用的时候必须使用标签来标记这个参数。

默认参数值

你可以在函数体中通过给参数赋值来为任意一个参数定义默认值(Deafult Value)。当默认值被定义后,调用这个函数时可以忽略这个参数。

1
2
3
4
5
func someFunction(parameterWithoutDefault: Int, parameterWithDefault: Int = 12) {
// 如果你在调用时候不传第二个参数,parameterWithDefault 会值为 12 传入到函数体中。
}
someFunction(parameterWithoutDefault: 3, parameterWithDefault: 6) // parameterWithDefault = 6
someFunction(parameterWithoutDefault: 4) // parameterWithDefault = 12

将不带有默认值的参数放在函数参数列表的最前。一般来说,没有默认值的参数更加的重要,将不带默认值的参数放在最前保证在函数调用时,非默认参数的顺序是一致的,同时也使得相同的函数在不同情况下调用时显得更为清晰。

可变参数

一个*可变参数(variadic parameter)*可以接受零个或多个值。函数调用时,你可以用可变参数来指定函数参数可以被传入不确定数量的输入值。通过在变量类型名后面加入(...)的方式来定义可变参数。

可变参数的传入值在函数体中变为此类型的一个数组。例如,一个叫做 numbersDouble... 型可变参数,在函数体内可以当做一个叫 numbers[Double] 型的数组常量。

下面的这个函数用来计算一组任意长度数字的 算术平均数(arithmetic mean)

1
2
3
4
5
6
7
8
9
10
11
func arithmeticMean(_ numbers: Double...) -> Double {
var total: Double = 0
for number in numbers {
total += number
}
return total / Double(numbers.count)
}
arithmeticMean(1, 2, 3, 4, 5)
// 返回 3.0, 是这 5 个数的平均数。
arithmeticMean(3, 8.25, 18.75)
// 返回 10.0, 是这 3 个数的平均数。

注意

一个函数最多只能拥有一个可变参数。

输入输出参数

函数参数默认是常量。试图在函数体中更改参数值将会导致编译错误。这意味着你不能错误地更改参数值。如果你想要一个函数可以修改参数的值,并且想要在这些修改在函数调用结束后仍然存在,那么就应该把这个参数定义为输入输出参数(In-Out Parameters)

定义一个输入输出参数时,在参数定义前加 inout 关键字。一个 输入输出参数有传入函数的值,这个值被函数修改,然后被传出函数,替换原来的值。想获取更多的关于输入输出参数的细节和相关的编译器优化,请查看 输入输出参数 一节。

你只能传递变量给输入输出参数。你不能传入常量或者字面量,因为这些量是不能被修改的。当传入的参数作为输入输出参数时,需要在参数名前加 & 符,表示这个值可以被函数修改。

注意

输入输出参数不能有默认值,而且可变参数不能用 inout 标记。

下例中,swapTwoInts(_:_:) 函数有两个分别叫做 ab 的输入输出参数:

1
2
3
4
5
func swapTwoInts(_ a: inout Int, _ b: inout Int) {
let temporaryA = a
a = b
b = temporaryA
}

swapTwoInts(_:_:) 函数简单地交换 ab 的值。该函数先将 a 的值存到一个临时常量 temporaryA 中,然后将 b 的值赋给 a,最后将 temporaryA 赋值给 b

你可以用两个 Int 型的变量来调用 swapTwoInts(_:_:)。需要注意的是,someIntanotherInt 在传入 swapTwoInts(_:_:) 函数前,都加了 & 的前缀:

1
2
3
4
5
var someInt = 3
var anotherInt = 107
swapTwoInts(&someInt, &anotherInt)
print("someInt is now \(someInt), and anotherInt is now \(anotherInt)")
// 打印“someInt is now 107, and anotherInt is now 3”

从上面这个例子中,我们可以看到 someIntanotherInt 的原始值在 swapTwoInts(_:_:) 函数中被修改,尽管它们的定义在函数体外。

注意

输入输出参数和返回值是不一样的。上面的 swapTwoInts 函数并没有定义任何返回值,但仍然修改了 someIntanotherInt 的值。输入输出参数是函数对函数体外产生影响的另一种方式。

函数类型

每个函数都有种特定的函数类型,函数的类型由函数的参数类型和返回类型组成。

例如:

1
2
3
4
5
6
func addTwoInts(_ a: Int, _ b: Int) -> Int {
return a + b
}
func multiplyTwoInts(_ a: Int, _ b: Int) -> Int {
return a * b
}

这个例子中定义了两个简单的数学函数:addTwoIntsmultiplyTwoInts。这两个函数都接受两个 Int 值, 返回一个 Int 值。

这两个函数的类型是 (Int, Int) -> Int,可以解读为:

“这个函数类型有两个 Int 型的参数并返回一个 Int 型的值”。

下面是另一个例子,一个没有参数,也没有返回值的函数:

1
2
3
func printHelloWorld() {
print("hello, world")
}

这个函数的类型是:() -> Void,或者叫“没有参数,并返回 Void 类型的函数”。

使用函数类型

在 Swift 中,使用函数类型就像使用其他类型一样。例如,你可以定义一个类型为函数的常量或变量,并将适当的函数赋值给它:

1
var mathFunction: (Int, Int) -> Int = addTwoInts

这段代码可以被解读为:

”定义一个叫做 mathFunction 的变量,类型是‘一个有两个 Int 型的参数并返回一个 Int 型的值的函数’,并让这个新变量指向 addTwoInts 函数”。

addTwoIntsmathFunction 有同样的类型,所以这个赋值过程在 Swift 类型检查(type-check)中是允许的。

现在,你可以用 mathFunction 来调用被赋值的函数了:

1
2
print("Result: \(mathFunction(2, 3))")
// Prints "Result: 5"

有相同匹配类型的不同函数可以被赋值给同一个变量,就像非函数类型的变量一样:

1
2
3
mathFunction = multiplyTwoInts
print("Result: \(mathFunction(2, 3))")
// Prints "Result: 6"

就像其他类型一样,当赋值一个函数给常量或变量时,你可以让 Swift 来推断其函数类型:

1
2
let anotherMathFunction = addTwoInts
// anotherMathFunction 被推断为 (Int, Int) -> Int 类型

函数类型作为参数类型

你可以用 (Int, Int) -> Int 这样的函数类型作为另一个函数的参数类型。这样你可以将函数的一部分实现留给函数的调用者来提供。

下面是另一个例子,正如上面的函数一样,同样是输出某种数学运算结果:

1
2
3
4
5
func printMathResult(_ mathFunction: (Int, Int) -> Int, _ a: Int, _ b: Int) {
print("Result: \(mathFunction(a, b))")
}
printMathResult(addTwoInts, 3, 5)
// 打印“Result: 8”

这个例子定义了 printMathResult(_:_:_:) 函数,它有三个参数:第一个参数叫 mathFunction,类型是 (Int, Int) -> Int,你可以传入任何这种类型的函数;第二个和第三个参数叫 ab,它们的类型都是 Int,这两个值作为已给出的函数的输入值。

printMathResult(_:_:_:) 被调用时,它被传入 addTwoInts 函数和整数 35。它用传入 35 调用 addTwoInts,并输出结果:8

printMathResult(_:_:_:) 函数的作用就是输出另一个适当类型的数学函数的调用结果。它不关心传入函数是如何实现的,只关心传入的函数是不是一个正确的类型。这使得 printMathResult(_:_:_:) 能以一种类型安全(type-safe)的方式将一部分功能转给调用者实现。

函数类型作为返回类型

你可以用函数类型作为另一个函数的返回类型。你需要做的是在返回箭头(->)后写一个完整的函数类型。

下面的这个例子中定义了两个简单函数,分别是 stepForward(_:)stepBackward(_:)stepForward(_:) 函数返回一个比输入值大 1 的值。stepBackward(_:) 函数返回一个比输入值小 1 的值。这两个函数的类型都是 (Int) -> Int

1
2
3
4
5
6
func stepForward(_ input: Int) -> Int {
return input + 1
}
func stepBackward(_ input: Int) -> Int {
return input - 1
}

如下名为 chooseStepFunction(backward:) 的函数,它的返回类型是 (Int) -> Int 类型的函数。chooseStepFunction(backward:) 根据布尔值 backwards 来返回 stepForward(_:) 函数或 stepBackward(_:) 函数:

1
2
3
func chooseStepFunction(backward: Bool) -> (Int) -> Int {
return backward ? stepBackward : stepForward
}

你现在可以用 chooseStepFunction(backward:) 来获得两个函数其中的一个:

1
2
3
var currentValue = 3
let moveNearerToZero = chooseStepFunction(backward: currentValue > 0)
// moveNearerToZero 现在指向 stepBackward() 函数。

上面这个例子中计算出从 currentValue 逐渐接近到0是需要向正数走还是向负数走。currentValue 的初始值是 3,这意味着 currentValue > 0 为真(true),这将使得 chooseStepFunction(_:) 返回 stepBackward(_:) 函数。一个指向返回的函数的引用保存在了 moveNearerToZero 常量中。

现在,moveNearerToZero 指向了正确的函数,它可以被用来数到零:

1
2
3
4
5
6
7
8
9
10
11
print("Counting to zero:")
// Counting to zero:
while currentValue != 0 {
print("\(currentValue)... ")
currentValue = moveNearerToZero(currentValue)
}
print("zero!")
// 3...
// 2...
// 1...
// zero!

嵌套函数

到目前为止本章中你所见到的所有函数都叫全局函数(global functions),它们定义在全局域中。你也可以把函数定义在别的函数体中,称作 嵌套函数(nested functions)

默认情况下,嵌套函数是对外界不可见的,但是可以被它们的外围函数(enclosing function)调用。一个外围函数也可以返回它的某一个嵌套函数,使得这个函数可以在其他域中被使用。

你可以用返回嵌套函数的方式重写 chooseStepFunction(backward:) 函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func chooseStepFunction(backward: Bool) -> (Int) -> Int {
func stepForward(input: Int) -> Int { return input + 1 }
func stepBackward(input: Int) -> Int { return input - 1 }
return backward ? stepBackward : stepForward
}
var currentValue = -4
let moveNearerToZero = chooseStepFunction(backward: currentValue > 0)
// moveNearerToZero now refers to the nested stepForward() function
while currentValue != 0 {
print("\(currentValue)... ")
currentValue = moveNearerToZero(currentValue)
}
print("zero!")
// -4...
// -3...
// -2...
// -1...
// zero!

留言與分享

swift控制流

分類 编程语言, swift

控制流

Swift 提供了多种流程控制结构,包括可以多次执行任务的 while 循环,基于特定条件选择执行不同代码分支的 ifguardswitch 语句,还有控制流程跳转到其他代码位置的 breakcontinue 语句。

Swift 还提供了 for-in 循环,用来更简单地遍历数组(Array),字典(Dictionary),区间(Range),字符串(String)和其他序列类型。

Swift 的 switch 语句比许多类 C 语言要更加强大。case 还可以匹配很多不同的模式,包括范围匹配,元组(tuple)和特定类型匹配。switch 语句的 case 中匹配的值可以声明为临时常量或变量,在 case 作用域内使用,也可以配合 where 来描述更复杂的匹配条件。

For-In 循环

你可以使用 for-in 循环来遍历一个集合中的所有元素,例如数组中的元素、范围内的数字或者字符串中的字符。

以下例子使用 for-in 遍历一个数组所有元素:

1
2
3
4
5
6
7
8
let names = ["Anna", "Alex", "Brian", "Jack"]
for name in names {
print("Hello, \(name)!")
}
// Hello, Anna!
// Hello, Alex!
// Hello, Brian!
// Hello, Jack!

你也可以通过遍历一个字典来访问它的键值对。遍历字典时,字典的每项元素会以 (key, value) 元组的形式返回,你可以在 for-in 循环中使用显式的常量名称来解读 (key, value) 元组。下面的例子中,字典的键声明会为 animalName 常量,字典的值会声明为 legCount 常量:

1
2
3
4
5
6
7
let numberOfLegs = ["spider": 8, "ant": 6, "cat": 4]
for (animalName, legCount) in numberOfLegs {
print("\(animalName)s have \(legCount) legs")
}
// cats have 4 legs
// ants have 6 legs
// spiders have 8 legs

字典的内容理论上是无序的,遍历元素时的顺序是无法确定的。将元素插入字典的顺序并不会决定它们被遍历的顺序。关于数组和字典的细节,参见 集合类型

for-in 循环还可以使用数字范围。下面的例子用来输出乘法表的一部分内容:

1
2
3
4
5
6
7
8
for index in 1...5 {
print("\(index) times 5 is \(index * 5)")
}
// 1 times 5 is 5
// 2 times 5 is 10
// 3 times 5 is 15
// 4 times 5 is 20
// 5 times 5 is 25

例子中用来进行遍历的元素是使用闭区间操作符(...)表示的从 15 的数字区间。index 被赋值为闭区间中的第一个数字(1),然后循环中的语句被执行一次。在本例中,这个循环只包含一个语句,用来输出当前 index 值所对应的乘 5 乘法表的结果。该语句执行后,index 的值被更新为闭区间中的第二个数字(2),之后 print(_:separator:terminator:) 函数会再执行一次。整个过程会进行到闭区间结尾为止。

上面的例子中,index 是一个每次循环遍历开始时被自动赋值的常量。这种情况下,index 在使用前不需要声明,只需要将它包含在循环的声明中,就可以对其进行隐式声明,而无需使用 let 关键字声明。

如果你不需要区间序列内每一项的值,你可以使用下划线(_)替代变量名来忽略这个值:

1
2
3
4
5
6
7
8
let base = 3
let power = 10
var answer = 1
for _ in 1...power {
answer *= base
}
print("\(base) to the power of \(power) is \(answer)")
// 输出“3 to the power of 10 is 59049”

这个例子计算 base 这个数的 power 次幂(本例中,是 310 次幂),从 130 次幂)开始做 3 的乘法, 进行 10 次,使用 110 的闭区间循环。这个计算并不需要知道每一次循环中计数器具体的值,只需要执行了正确的循环次数即可。下划线符号 _ (替代循环中的变量)能够忽略当前值,并且不提供循环遍历时对值的访问。

在某些情况下,你可能不想使用包括两个端点的闭区间。想象一下,你在一个手表上绘制分钟的刻度线。总共 60 个刻度,从 0 分开始。使用半开区间运算符(..<)来表示一个左闭右开的区间。有关区间的更多信息,请参阅 区间运算符

1
2
3
4
let minutes = 60
for tickMark in 0..<minutes {
// 每一分钟都渲染一个刻度线(60次)
}

一些用户可能在其 UI 中可能需要较少的刻度。他们可以每 5 分钟作为一个刻度。使用 stride(from:to:by:) 函数跳过不需要的标记。

1
2
3
4
let minuteInterval = 5
for tickMark in stride(from: 0, to: minutes, by: minuteInterval) {
// 每5分钟渲染一个刻度线(0, 5, 10, 15 ... 45, 50, 55)
}

可以在闭区间使用 stride(from:through:by:) 起到同样作用:

1
2
3
4
5
let hours = 12
let hourInterval = 3
for tickMark in stride(from: 3, through: hours, by: hourInterval) {
// 每3小时渲染一个刻度线(3, 6, 9, 12)
}

While 循环

while 循环会一直运行一段语句直到条件变成 false。这类循环适合使用在第一次迭代前,迭代次数未知的情况下。Swift 提供两种 while 循环形式:

  • while 循环,每次在循环开始时计算条件是否符合;
  • repeat-while 循环,每次在循环结束时计算条件是否符合。

While

while 循环从计算一个条件开始。如果条件为 true,会重复运行一段语句,直到条件变为 false

下面是 while 循环的一般格式:

1
2
3
while condition {
statements
}

下面的例子来玩一个叫做蛇和梯子(也叫做滑道和梯子)的小游戏:

image

游戏的规则如下:

  • 游戏盘面包括 25 个方格,游戏目标是达到或者超过第 25 个方格;
  • 每一轮,你通过掷一个六面体骰子来确定你移动方块的步数,移动的路线由上图中横向的虚线所示;
  • 如果在某轮结束,你移动到了梯子的底部,可以顺着梯子爬上去;
  • 如果在某轮结束,你移动到了蛇的头部,你会顺着蛇的身体滑下去。

游戏盘面可以使用一个 Int 数组来表达。数组的长度由一个 finalSquare 常量储存,用来初始化数组和检测最终胜利条件。游戏盘面由 26 个 Int 0 值初始化,而不是 25 个(由 025,一共 26 个):

1
2
let finalSquare = 25
var board = [Int](repeating: 0, count: finalSquare + 1)

一些方格被设置成特定的值来表示有蛇或者梯子。梯子底部的方格是一个正值,使你可以向上移动,蛇头处的方格是一个负值,会让你向下移动:

1
2
board[03] = +08; board[06] = +11; board[09] = +09; board[10] = +02
board[14] = -10; board[19] = -11; board[22] = -02; board[24] = -08

3 号方格是梯子的底部,会让你向上移动到 11 号方格,我们使用 board[03] 等于 +08(来表示 113 之间的差值)。为了对齐语句,这里使用了一元正运算符(+i)和一元负运算符(-i),并且小于 10 的数字都使用 0 补齐(这些语法的技巧不是必要的,只是为了让代码看起来更加整洁)。

玩家由左下角空白处编号为 0 的方格开始游戏。玩家第一次掷骰子后才会进入游戏盘面:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var square = 0
var diceRoll = 0
while square < finalSquare {
// 掷骰子
diceRoll += 1
if diceRoll == 7 { diceRoll = 1 }
// 根据点数移动
square += diceRoll
if square < board.count {
// 如果玩家还在棋盘上,顺着梯子爬上去或者顺着蛇滑下去
square += board[square]
}
}
print("Game over!")

本例中使用了最简单的方法来模拟掷骰子。diceRoll 的值并不是一个随机数,而是以 0 为初始值,之后每一次 while 循环,diceRoll 的值增加 1 ,然后检测是否超出了最大值。当 diceRoll 的值等于 7 时,就超过了骰子的最大值,会被重置为 1。所以 diceRoll 的取值顺序会一直是 12345612 等。

掷完骰子后,玩家向前移动 diceRoll 个方格,如果玩家移动超过了第 25 个方格,这个时候游戏将会结束,为了应对这种情况,代码会首先判断 square 的值是否小于 boardcount 属性,只有小于才会在 board[square] 上增加 square,来向前或向后移动(遇到了梯子或者蛇)。

注意

如果没有这个检测(square < board.count),board[square] 可能会越界访问 board 数组,导致运行时错误。

当本轮 while 循环运行完毕,会再检测循环条件是否需要再运行一次循环。如果玩家移动到或者超过第 25 个方格,循环条件结果为 false,此时游戏结束。

while 循环比较适合本例中的这种情况,因为在 while 循环开始时,我们并不知道游戏要跑多久,只有在达成指定条件时循环才会结束。

Repeat-While

while 循环的另外一种形式是 repeat-while,它和 while 的区别是在判断循环条件之前,先执行一次循环的代码块。然后重复循环直到条件为 false

注意

Swift 语言的 repeat-while 循环和其他语言中的 do-while 循环是类似的。

下面是 repeat-while 循环的一般格式:

1
2
3
repeat {
statements
} while condition

还是蛇和梯子的游戏,使用 repeat-while 循环来替代 while 循环。finalSquareboardsquarediceRoll 的值初始化同 while 循环时一样:

1
2
3
4
5
6
let finalSquare = 25
var board = [Int](repeating: 0, count: finalSquare + 1)
board[03] = +08; board[06] = +11; board[09] = +09; board[10] = +02
board[14] = -10; board[19] = -11; board[22] = -02; board[24] = -08
var square = 0
var diceRoll = 0

repeat-while 的循环版本,循环中第一步就需要去检测是否在梯子或者蛇的方块上。没有梯子会让玩家直接上到第 25 个方格,所以玩家不会通过梯子直接赢得游戏。这样在循环开始时先检测是否踩在梯子或者蛇上是安全的。

游戏开始时,玩家在第 0 个方格上,board[0] 一直等于 0, 不会有什么影响:

1
2
3
4
5
6
7
8
9
10
repeat {
// 顺着梯子爬上去或者顺着蛇滑下去
square += board[square]
// 掷骰子
diceRoll += 1
if diceRoll == 7 { diceRoll = 1 }
// 根据点数移动
square += diceRoll
} while square < finalSquare
print("Game over!")

检测完玩家是否踩在梯子或者蛇上之后,开始掷骰子,然后玩家向前移动 diceRoll 个方格,本轮循环结束。

循环条件(while square < finalSquare)和 while 方式相同,但是只会在循环结束后进行计算。在这个游戏中,repeat-while 表现得比 while 循环更好。repeat-while 方式会在条件判断 square 没有超出后直接运行 square += board[square],这种方式可以比起前面 while 循环的版本,可以省去数组越界的检查。

条件语句

根据特定的条件执行特定的代码通常是十分有用的。当错误发生时,你可能想运行额外的代码;或者,当值太大或太小时,向用户显示一条消息。要实现这些功能,你就需要使用条件语句

Swift 提供两种类型的条件语句:if 语句和 switch 语句。通常,当条件较为简单且可能的情况很少时,使用 if 语句。而 switch 语句更适用于条件较复杂、有更多排列组合的时候。并且 switch 在需要用到模式匹配(pattern-matching)的情况下会更有用。

If

if 语句最简单的形式就是只包含一个条件,只有该条件为 true 时,才执行相关代码:

1
2
3
4
5
var temperatureInFahrenheit = 30
if temperatureInFahrenheit <= 32 {
print("It's very cold. Consider wearing a scarf.")
}
// 输出“It's very cold. Consider wearing a scarf.”

上面的例子会判断温度是否小于等于 32 华氏度(水的冰点)。如果是,则打印一条消息;否则,不打印任何消息,继续执行 if 块后面的代码。

当然,if 语句允许二选一执行,叫做 else 从句。也就是当条件为 false 时,执行 else 语句

1
2
3
4
5
6
7
temperatureInFahrenheit = 40
if temperatureInFahrenheit <= 32 {
print("It's very cold. Consider wearing a scarf.")
} else {
print("It's not that cold. Wear a t-shirt.")
}
// 输出“It's not that cold. Wear a t-shirt.”

显然,这两条分支中总有一条会被执行。由于温度已升至 40 华氏度,不算太冷,没必要再围围巾。因此,else 分支就被触发了。

你可以把多个 if 语句链接在一起,来实现更多分支:

1
2
3
4
5
6
7
8
9
temperatureInFahrenheit = 90
if temperatureInFahrenheit <= 32 {
print("It's very cold. Consider wearing a scarf.")
} else if temperatureInFahrenheit >= 86 {
print("It's really warm. Don't forget to wear sunscreen.")
} else {
print("It's not that cold. Wear a t-shirt.")
}
// 输出“It's really warm. Don't forget to wear sunscreen.”

在上面的例子中,额外的 if 语句用于判断是不是特别热。而最后的 else 语句被保留了下来,用于打印既不冷也不热时的消息。

实际上,当不需要完整判断情况的时候,最后的 else 语句是可选的:

1
2
3
4
5
6
temperatureInFahrenheit = 72
if temperatureInFahrenheit <= 32 {
print("It's very cold. Consider wearing a scarf.")
} else if temperatureInFahrenheit >= 86 {
print("It's really warm. Don't forget to wear sunscreen.")
}

在这个例子中,由于既不冷也不热,所以不会触发 ifelse if 分支,也就不会打印任何消息。

Switch

switch 语句会尝试把某个值与若干个模式(pattern)进行匹配。根据第一个匹配成功的模式,switch 语句会执行对应的代码。当有可能的情况较多时,通常用 switch 语句替换 if 语句。

switch 语句最简单的形式就是把某个值与一个或若干个相同类型的值作比较:

1
2
3
4
5
6
7
8
9
switch some value to consider {
case value 1:
respond to value 1
case value 2,
value 3:
respond to value 2 or 3
default:
otherwise, do something else
}

switch 语句由多个 case 构成,每个由 case 关键字开始。为了匹配某些更特定的值,Swift 提供了几种方法来进行更复杂的模式匹配,这些模式将在本节的稍后部分提到。

if 语句类似,每一个 case 都是代码执行的一条分支。switch 语句会决定哪一条分支应该被执行,这个流程被称作根据给定的值切换(switching)

switch 语句必须是完备的。这就是说,每一个可能的值都必须至少有一个 case 分支与之对应。在某些不可能涵盖所有值的情况下,你可以使用默认(default)分支来涵盖其它所有没有对应的值,这个默认分支必须在 switch 语句的最后面。

下面的例子使用 switch 语句来匹配一个名为 someCharacter 的小写字符:

1
2
3
4
5
6
7
8
9
10
let someCharacter: Character = "z"
switch someCharacter {
case "a":
print("The first letter of the alphabet")
case "z":
print("The last letter of the alphabet")
default:
print("Some other character")
}
// 输出“The last letter of the alphabet”

在这个例子中,第一个 case 分支用于匹配第一个英文字母 a,第二个 case 分支用于匹配最后一个字母 z。因为 switch 语句必须有一个 case 分支用于覆盖所有可能的字符,而不仅仅是所有的英文字母,所以 switch 语句使用 default 分支来匹配除了 az 外的所有值,这个分支保证了 swith 语句的完备性。

不存在隐式的贯穿

与 C 和 Objective-C 中的 switch 语句不同,在 Swift 中,当匹配的 case 分支中的代码执行完毕后,程序会终止 switch 语句,而不会继续执行下一个 case 分支。这也就是说,不需要在 case 分支中显式地使用 break 语句。这使得 switch 语句更安全、更易用,也避免了漏写 break 语句导致多个语言被执行的错误。

注意

虽然在 Swift 中 break 不是必须的,但你依然可以在 case 分支中的代码执行完毕前使用 break 跳出,详情请参见 Switch 语句中的 break

每一个 case 分支都必须包含至少一条语句。像下面这样书写代码是无效的,因为第一个 case 分支是空的:

1
2
3
4
5
6
7
8
9
let anotherCharacter: Character = "a"
switch anotherCharacter {
case "a": // 无效,这个分支下面没有语句
case "A":
print("The letter A")
default:
print("Not the letter A")
}
// 这段代码会报编译错误

不像 C 语言里的 switch 语句,在 Swift 中,switch 语句不会一起匹配 "a""A"。相反的,上面的代码会引起编译期错误:case "a": 不包含任何可执行语句——这就避免了意外地从一个 case 分支贯穿到另外一个,使得代码更安全、也更直观。

为了让单个 case 同时匹配 aA,可以将这个两个值组合成一个复合匹配,并且用逗号分开:

1
2
3
4
5
6
7
8
let anotherCharacter: Character = "a"
switch anotherCharacter {
case "a", "A":
print("The letter A")
default:
print("Not the letter A")
}
// 输出“The letter A”

为了可读性,符合匹配可以写成多行形式,详情请参考 复合匹配

注意

如果想要显式贯穿 case 分支,请使用 fallthrough 语句,详情请参考 贯穿

区间匹配

case 分支的模式也可以是一个值的区间。下面的例子展示了如何使用区间匹配来输出任意数字对应的自然语言格式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
let approximateCount = 62
let countedThings = "moons orbiting Saturn"
let naturalCount: String
switch approximateCount {
case 0:
naturalCount = "no"
case 1..<5:
naturalCount = "a few"
case 5..<12:
naturalCount = "several"
case 12..<100:
naturalCount = "dozens of"
case 100..<1000:
naturalCount = "hundreds of"
default:
naturalCount = "many"
}
print("There are \(naturalCount) \(countedThings).")
// 输出“There are dozens of moons orbiting Saturn.”

在上例中,approximateCount 在一个 switch 声明中被评估。每一个 case 都与之进行比较。因为 approximateCount 落在了 12 到 100 的区间,所以 naturalCount 等于 "dozens of" 值,并且此后的执行跳出了 switch 语句。

元组

我们可以使用元组在同一个 switch 语句中测试多个值。元组中的元素可以是值,也可以是区间。另外,使用下划线(_)来匹配所有可能的值。

下面的例子展示了如何使用一个 (Int, Int) 类型的元组来分类下图中的点 (x, y):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
let somePoint = (1, 1)
switch somePoint {
case (0, 0):
print("\(somePoint) is at the origin")
case (_, 0):
print("\(somePoint) is on the x-axis")
case (0, _):
print("\(somePoint) is on the y-axis")
case (-2...2, -2...2):
print("\(somePoint) is inside the box")
default:
print("\(somePoint) is outside of the box")
}
// 输出“(1, 1) is inside the box”

image

在上面的例子中,switch 语句会判断某个点是否是原点 (0, 0),是否在红色的 x 轴上,是否在橘黄色的 y 轴上,是否在一个以原点为中心的4x4的蓝色矩形里,或者在这个矩形外面。

不像 C 语言,Swift 允许多个 case 匹配同一个值。实际上,在这个例子中,点 (0, 0)可以匹配所有四个 case。但是,如果存在多个匹配,那么只会执行第一个被匹配到的 case 分支。考虑点 (0, 0)会首先匹配 case (0, 0),因此剩下的能够匹配的分支都会被忽视掉。

值绑定(Value Bindings)

case 分支允许将匹配的值声明为临时常量或变量,并且在 case 分支体内使用 —— 这种行为被称为值绑定(value binding),因为匹配的值在 case 分支体内,与临时的常量或变量绑定。

下面的例子将下图中的点 (x, y),使用 (Int, Int) 类型的元组表示,然后分类表示:

1
2
3
4
5
6
7
8
9
10
let anotherPoint = (2, 0)
switch anotherPoint {
case (let x, 0):
print("on the x-axis with an x value of \(x)")
case (0, let y):
print("on the y-axis with a y value of \(y)")
case let (x, y):
print("somewhere else at (\(x), \(y))")
}
// 输出“on the x-axis with an x value of 2”

image

在上面的例子中,switch 语句会判断某个点是否在红色的 x 轴上,是否在橘黄色的 y 轴上,或者不在坐标轴上。

这三个 case 都声明了常量 xy 的占位符,用于临时获取元组 anotherPoint 的一个或两个值。第一个 case ——case (let x, 0) 将匹配一个纵坐标为 0 的点,并把这个点的横坐标赋给临时的常量 x。类似的,第二个 case ——case (0, let y) 将匹配一个横坐标为 0 的点,并把这个点的纵坐标赋给临时的常量 y

一旦声明了这些临时的常量,它们就可以在其对应的 case 分支里使用。在这个例子中,它们用于打印给定点的类型。

请注意,这个 switch 语句不包含默认分支。这是因为最后一个 case ——case let(x, y) 声明了一个可以匹配余下所有值的元组。这使得 switch 语句已经完备了,因此不需要再书写默认分支。

Where

case 分支的模式可以使用 where 语句来判断额外的条件。

下面的例子把下图中的点 (x, y)进行了分类:

1
2
3
4
5
6
7
8
9
10
let yetAnotherPoint = (1, -1)
switch yetAnotherPoint {
case let (x, y) where x == y:
print("(\(x), \(y)) is on the line x == y")
case let (x, y) where x == -y:
print("(\(x), \(y)) is on the line x == -y")
case let (x, y):
print("(\(x), \(y)) is just some arbitrary point")
}
// 输出“(1, -1) is on the line x == -y”

image

在上面的例子中,switch 语句会判断某个点是否在绿色的对角线 x == y 上,是否在紫色的对角线 x == -y 上,或者不在对角线上。

这三个 case 都声明了常量 xy 的占位符,用于临时获取元组 yetAnotherPoint 的两个值。这两个常量被用作 where 语句的一部分,从而创建一个动态的过滤器(filter)。当且仅当 where 语句的条件为 true 时,匹配到的 case 分支才会被执行。

就像是值绑定中的例子,由于最后一个 case 分支匹配了余下所有可能的值,switch 语句就已经完备了,因此不需要再书写默认分支。

复合型 Cases

当多个条件可以使用同一种方法来处理时,可以将这几种可能放在同一个 case 后面,并且用逗号隔开。当 case 后面的任意一种模式匹配的时候,这条分支就会被匹配。并且,如果匹配列表过长,还可以分行书写:

1
2
3
4
5
6
7
8
9
10
11
let someCharacter: Character = "e"
switch someCharacter {
case "a", "e", "i", "o", "u":
print("\(someCharacter) is a vowel")
case "b", "c", "d", "f", "g", "h", "j", "k", "l", "m",
"n", "p", "q", "r", "s", "t", "v", "w", "x", "y", "z":
print("\(someCharacter) is a consonant")
default:
print("\(someCharacter) is not a vowel or a consonant")
}
// 输出“e is a vowel”

这个 switch 语句中的第一个 case,匹配了英语中的五个小写元音字母。相似的,第二个 case 匹配了英语中所有的小写辅音字母。最终,default 分支匹配了其它所有字符。

复合匹配同样可以包含值绑定。复合匹配里所有的匹配模式,都必须包含相同的值绑定。并且每一个绑定都必须获取到相同类型的值。这保证了,无论复合匹配中的哪个模式发生了匹配,分支体内的代码,都能获取到绑定的值,并且绑定的值都有一样的类型。

1
2
3
4
5
6
7
8
let stillAnotherPoint = (9, 0)
switch stillAnotherPoint {
case (let distance, 0), (0, let distance):
print("On an axis, \(distance) from the origin")
default:
print("Not on an axis")
}
// 输出“On an axis, 9 from the origin”

上面的 case 有两个模式:(let distance, 0) 匹配了在 x 轴上的值,(0, let distance) 匹配了在 y 轴上的值。两个模式都绑定了 distance,并且 distance 在两种模式下,都是整型——这意味着分支体内的代码,只要 case 匹配,都可以获取到 distance 值。

控制转移语句

控制转移语句改变你代码的执行顺序,通过它可以实现代码的跳转。Swift 有五种控制转移语句:

  • continue
  • break
  • fallthrough
  • return
  • throw

我们将会在下面讨论 continuebreakfallthrough 语句。return 语句将会在 函数 章节讨论,throw 语句会在 错误抛出 章节讨论。

Continue

continue 语句告诉一个循环体立刻停止本次循环,重新开始下次循环。就好像在说“本次循环我已经执行完了”,但是并不会离开整个循环体。

下面的例子把一个小写字符串中的元音字母和空格字符移除,生成了一个含义模糊的短句:

1
2
3
4
5
6
7
8
9
10
11
12
let puzzleInput = "great minds think alike"
var puzzleOutput = ""
for character in puzzleInput {
switch character {
case "a", "e", "i", "o", "u", " ":
continue
default:
puzzleOutput.append(character)
}
}
print(puzzleOutput)
// 输出“grtmndsthnklk”

在上面的代码中,只要匹配到元音字母或者空格字符,就调用 continue 语句,使本次循环结束,重新开始下次循环。这种行为使 switch 匹配到元音字母和空格字符时不做处理,而不是让每一个匹配到的字符都被打印。

Break

break 语句会立刻结束整个控制流的执行。break 可以在 switch 或循环语句中使用,用来提前结束 switch 或循环语句。

循环语句中的 break

当在一个循环体中使用 break 时,会立刻中断该循环体的执行,然后跳转到表示循环体结束的大括号(})后的第一行代码。不会再有本次循环的代码被执行,也不会再有下次的循环产生。

Switch 语句中的 break

当在一个 switch 代码块中使用 break 时,会立即中断该 switch 代码块的执行,并且跳转到表示 switch 代码块结束的大括号(})后的第一行代码。

这种特性可以被用来匹配或者忽略一个或多个分支。因为 Swift 的 switch 需要包含所有的分支而且不允许有为空的分支,有时为了使你的意图更明显,需要特意匹配或者忽略某个分支。那么当你想忽略某个分支时,可以在该分支内写上 break 语句。当那个分支被匹配到时,分支内的 break 语句立即结束 switch 代码块。

注意

当一个 switch 分支仅仅包含注释时,会被报编译时错误。注释不是代码语句而且也不能让 switch 分支达到被忽略的效果。你应该使用 break 来忽略某个分支。

下面的例子通过 switch 来判断一个 Character 值是否代表下面四种语言之一。为了简洁,多个值被包含在了同一个分支情况中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
let numberSymbol: Character = "三"  // 简体中文里的数字 3
var possibleIntegerValue: Int?
switch numberSymbol {
case "1", "١", "一", "๑":
possibleIntegerValue = 1
case "2", "٢", "二", "๒":
possibleIntegerValue = 2
case "3", "٣", "三", "๓":
possibleIntegerValue = 3
case "4", "٤", "四", "๔":
possibleIntegerValue = 4
default:
break
}
if let integerValue = possibleIntegerValue {
print("The integer value of \(numberSymbol) is \(integerValue).")
} else {
print("An integer value could not be found for \(numberSymbol).")
}
// 输出“The integer value of 三 is 3.”

这个例子检查 numberSymbol 是否是拉丁,阿拉伯,中文或者泰语中的 14 之一。如果被匹配到,该 switch 分支语句给 Int? 类型变量 possibleIntegerValue 设置一个整数值。

switch 代码块执行完后,接下来的代码通过使用可选绑定来判断 possibleIntegerValue 是否曾经被设置过值。因为是可选类型的缘故,possibleIntegerValue 有一个隐式的初始值 nil,所以仅仅当 possibleIntegerValue 曾被 switch 代码块的前四个分支中的某个设置过一个值时,可选的绑定才会被判定为成功。

在上面的例子中,想要把 Character 所有的的可能性都枚举出来是不现实的,所以使用 default 分支来包含所有上面没有匹配到字符的情况。由于这个 default 分支不需要执行任何动作,所以它只写了一条 break 语句。一旦落入到 default 分支中后,break 语句就完成了该分支的所有代码操作,代码继续向下,开始执行 if let 语句。

贯穿(Fallthrough)

在 Swift 里,switch 语句不会从上一个 case 分支跳转到下一个 case 分支中。相反,只要第一个匹配到的 case 分支完成了它需要执行的语句,整个 switch 代码块完成了它的执行。相比之下,C 语言要求你显式地插入 break 语句到每个 case 分支的末尾来阻止自动落入到下一个 case 分支中。Swift 的这种避免默认落入到下一个分支中的特性意味着它的 switch 功能要比 C 语言的更加清晰和可预测,可以避免无意识地执行多个 case 分支从而引发的错误。

如果你确实需要 C 风格的贯穿的特性,你可以在每个需要该特性的 case 分支中使用 fallthrough 关键字。下面的例子使用 fallthrough 来创建一个数字的描述语句。

1
2
3
4
5
6
7
8
9
10
11
let integerToDescribe = 5
var description = "The number \(integerToDescribe) is"
switch integerToDescribe {
case 2, 3, 5, 7, 11, 13, 17, 19:
description += " a prime number, and also"
fallthrough
default:
description += " an integer."
}
print(description)
// 输出“The number 5 is a prime number, and also an integer.”

这个例子定义了一个 String 类型的变量 description 并且给它设置了一个初始值。函数使用 switch 逻辑来判断 integerToDescribe 变量的值。当 integerToDescribe 的值属于列表中的质数之一时,该函数在 description 后添加一段文字,来表明这个数字是一个质数。然后它使用 fallthrough 关键字来“贯穿”到 default 分支中。default 分支在 description 的最后添加一段额外的文字,至此 switch 代码块执行完了。

如果 integerToDescribe 的值不属于列表中的任何质数,那么它不会匹配到第一个 switch 分支。而这里没有其他特别的分支情况,所以 integerToDescribe 匹配到 default 分支中。

switch 代码块执行完后,使用 print(_:separator:terminator:) 函数打印该数字的描述。在这个例子中,数字 5 被准确的识别为了一个质数。

注意

fallthrough 关键字不会检查它下一个将会落入执行的 case 中的匹配条件。fallthrough 简单地使代码继续连接到下一个 case 中的代码,这和 C 语言标准中的 switch 语句特性是一样的。

带标签的语句

在 Swift 中,你可以在循环体和条件语句中嵌套循环体和条件语句来创造复杂的控制流结构。并且,循环体和条件语句都可以使用 break 语句来提前结束整个代码块。因此,显式地指明 break 语句想要终止的是哪个循环体或者条件语句,会很有用。类似地,如果你有许多嵌套的循环体,显式指明 continue 语句想要影响哪一个循环体也会非常有用。

为了实现这个目的,你可以使用标签(statement label)来标记一个循环体或者条件语句,对于一个条件语句,你可以使用 break 加标签的方式,来结束这个被标记的语句。对于一个循环语句,你可以使用 break 或者 continue 加标签,来结束或者继续这条被标记语句的执行。

声明一个带标签的语句是通过在该语句的关键词的同一行前面放置一个标签,作为这个语句的前导关键字(introducor keyword),并且该标签后面跟随一个冒号。下面是一个针对 while 循环体的标签语法,同样的规则适用于所有的循环体和条件语句。

1
2
3
label name: while condition {
statements
}

下面的例子是前面章节中蛇和梯子的适配版本,在此版本中,我们将使用一个带有标签的 while 循环体中调用 breakcontinue 语句。这次,游戏增加了一条额外的规则:

  • 为了获胜,你必须刚好落在第 25 个方块中。

如果某次掷骰子使你的移动超出第 25 个方块,你必须重新掷骰子,直到你掷出的骰子数刚好使你能落在第 25 个方块中。

游戏的棋盘和之前一样:

image

finalSquareboardsquarediceRoll 值被和之前一样的方式初始化:

1
2
3
4
5
6
let finalSquare = 25
var board = [Int](repeating: 0, count: finalSquare + 1)
board[03] = +08; board[06] = +11; board[09] = +09; board[10] = +02
board[14] = -10; board[19] = -11; board[22] = -02; board[24] = -08
var square = 0
var diceRoll = 0

这个版本的游戏使用 while 循环和 switch 语句来实现游戏的逻辑。while 循环有一个标签名 gameLoop,来表明它是游戏的主循环。

while 循环体的条件判断语句是 while square !=finalSquare,这表明你必须刚好落在方格25中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
gameLoop: while square != finalSquare {
diceRoll += 1
if diceRoll == 7 { diceRoll = 1 }
switch square + diceRoll {
case finalSquare:
// 骰子数刚好使玩家移动到最终的方格里,游戏结束。
break gameLoop
case let newSquare where newSquare > finalSquare:
// 骰子数将会使玩家的移动超出最后的方格,那么这种移动是不合法的,玩家需要重新掷骰子
continue gameLoop
default:
// 合法移动,做正常的处理
square += diceRoll
square += board[square]
}
}
print("Game over!")

每次循环迭代开始时掷骰子。与之前玩家掷完骰子就立即移动不同,这里使用了 switch 语句来考虑每次移动可能产生的结果,从而决定玩家本次是否能够移动。

  • 如果骰子数刚好使玩家移动到最终的方格里,游戏结束。break gameLoop 语句跳转控制去执行 while 循环体后的第一行代码,意味着游戏结束。
  • 如果骰子数将会使玩家的移动超出最后的方格,那么这种移动是不合法的,玩家需要重新掷骰子。continue gameLoop 语句结束本次 while 循环,开始下一次循环。
  • 在剩余的所有情况中,骰子数产生的都是合法的移动。玩家向前移动 diceRoll 个方格,然后游戏逻辑再处理玩家当前是否处于蛇头或者梯子的底部。接着本次循环结束,控制跳转到 while 循环体的条件判断语句处,再决定是否需要继续执行下次循环。

注意

如果上述的 break 语句没有使用 gameLoop 标签,那么它将会中断 switch 语句而不是 while 循环。使用 gameLoop 标签清晰的表明了 break 想要中断的是哪个代码块。

同时请注意,当调用 continue gameLoop 去跳转到下一次循环迭代时,这里使用 gameLoop 标签并不是严格必须的。因为在这个游戏中,只有一个循环体,所以 continue 语句会影响到哪个循环体是没有歧义的。然而,continue 语句使用 gameLoop 标签也是没有危害的。这样做符合标签的使用规则,同时参照旁边的 break gameLoop,能够使游戏的逻辑更加清晰和易于理解。

提前退出

if 语句一样,guard 的执行取决于一个表达式的布尔值。我们可以使用 guard 语句来要求条件必须为真时,以执行 guard 语句后的代码。不同于 if 语句,一个 guard 语句总是有一个 else 从句,如果条件不为真则执行 else 从句中的代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
func greet(person: [String: String]) {
guard let name = person["name"] else {
return
}

print("Hello \(name)!")

guard let location = person["location"] else {
print("I hope the weather is nice near you.")
return
}

print("I hope the weather is nice in \(location).")
}

greet(person: ["name": "John"])
// 输出“Hello John!”
// 输出“I hope the weather is nice near you.”
greet(person: ["name": "Jane", "location": "Cupertino"])
// 输出“Hello Jane!”
// 输出“I hope the weather is nice in Cupertino.”

如果 guard 语句的条件被满足,则继续执行 guard 语句大括号后的代码。将变量或者常量的可选绑定作为 guard 语句的条件,都可以保护 guard 语句后面的代码。

如果条件不被满足,在 else 分支上的代码就会被执行。这个分支必须转移控制以退出 guard 语句出现的代码段。它可以用控制转移语句如 returnbreakcontinue 或者 throw 做这件事,或者调用一个不返回的方法或函数,例如 fatalError()

相比于可以实现同样功能的 if 语句,按需使用 guard 语句会提升我们代码的可读性。它可以使你的代码连贯的被执行而不需要将它包在 else 块中,它可以使你在紧邻条件判断的地方,处理违规的情况。

检测 API 可用性

Swift 内置支持检查 API 可用性,这可以确保我们不会在当前部署机器上,不小心地使用了不可用的 API。

编译器使用 SDK 中的可用信息来验证我们的代码中使用的所有 API 在项目指定的部署目标上是否可用。如果我们尝试使用一个不可用的 API,Swift 会在编译时报错。

我们在 ifguard 语句中使用 可用性条件(availability condition)去有条件的执行一段代码,来在运行时判断调用的 API 是否可用。编译器使用从可用性条件语句中获取的信息去验证,在这个代码块中调用的 API 是否可用。

1
2
3
4
5
if #available(iOS 10, macOS 10.12, *) {
// 在 iOS 使用 iOS 10 的 API, 在 macOS 使用 macOS 10.12 的 API
} else {
// 使用先前版本的 iOS 和 macOS 的 API
}

以上可用性条件指定,if 语句的代码块仅仅在 iOS 10 或 macOS 10.12 及更高版本才运行。最后一个参数,*,是必须的,用于指定在所有其它平台中,如果版本号高于你的设备指定的最低版本,if 语句的代码块将会运行。

在它一般的形式中,可用性条件使用了一个平台名字和版本的列表。平台名字可以是 iOSmacOSwatchOStvOS——请访问 声明属性 来获取完整列表。除了指定像 iOS 8 或 macOS 10.10 的大版本号,也可以指定像 iOS 11.2.6 以及 macOS 10.13.3 的小版本号。

1
2
3
4
5
if #available(平台名称 版本号, ..., *) {
APIs 可用,语句将执行
} else {
APIs 不可用,语句将不执行
}

留言與分享

swift集合类型

分類 编程语言, swift

集合类型

Swift 语言提供 ArraysSetsDictionaries 三种基本的集合类型用来存储集合数据。数组(Arrays)是有序数据的集。集合(Sets)是无序无重复数据的集。字典(Dictionaries)是无序的键值对的集。

Swift 语言中的 ArraysSetsDictionaries 中存储的数据值类型必须明确。这意味着我们不能把错误的数据类型插入其中。同时这也说明你完全可以对取回值的类型非常放心。

注意

Swift 的 ArraysSetsDictionaries 类型被实现为泛型集合。更多关于泛型类型和集合,参见 泛型 章节。

集合的可变性

如果创建一个 ArraysSetsDictionaries 并且把它分配成一个变量,这个集合将会是可变的。这意味着你可以在创建之后添加更多或移除已存在的数据项,或者改变集合中的数据项。如果我们把 ArraysSetsDictionaries 分配成常量,那么它就是不可变的,它的大小和内容都不能被改变。

注意

在我们不需要改变集合的时候创建不可变集合是很好的实践。如此 Swift 编译器可以优化我们创建的集合。

数组(Arrays)

数组使用有序列表存储同一类型的多个值。相同的值可以多次出现在一个数组的不同位置中。

注意

Swift 的 Array 类型被桥接到 Foundation 中的 NSArray 类。更多关于在 FoundationCocoa 中使用 Array 的信息,参见 Using Swift with Cocoa and Obejective-C(Swift 4.1)使用 Cocoa 数据类型 部分。

数组的简单语法

写 Swift 数组应该遵循像 Array<Element> 这样的形式,其中 Element 是这个数组中唯一允许存在的数据类型。我们也可以使用像 [Element] 这样的简单语法。尽管两种形式在功能上是一样的,但是推荐较短的那种,而且在本文中都会使用这种形式来使用数组。

创建一个空数组

我们可以使用构造语法来创建一个由特定数据类型构成的空数组:

1
2
3
var someInts = [Int]()
print("someInts is of type [Int] with \(someInts.count) items.")
// 打印“someInts is of type [Int] with 0 items.”

注意,通过构造函数的类型,someInts 的值类型被推断为 [Int]

或者,如果代码上下文中已经提供了类型信息,例如一个函数参数或者一个已经定义好类型的常量或者变量,我们可以使用空数组语句创建一个空数组,它的写法很简单:[](一对空方括号):

1
2
3
4
someInts.append(3)
// someInts 现在包含一个 Int 值
someInts = []
// someInts 现在是空数组,但是仍然是 [Int] 类型的。

创建一个带有默认值的数组

Swift 中的 Array 类型还提供一个可以创建特定大小并且所有数据都被默认的构造方法。我们可以把准备加入新数组的数据项数量(count)和适当类型的初始值(repeating)传入数组构造函数:

1
2
var threeDoubles = Array(repeating: 0.0, count: 3)
// threeDoubles 是一种 [Double] 数组,等价于 [0.0, 0.0, 0.0]

通过两个数组相加创建一个数组

我们可以使用加法操作符(+)来组合两种已存在的相同类型数组。新数组的数据类型会被从两个数组的数据类型中推断出来:

1
2
3
4
5
var anotherThreeDoubles = Array(repeating: 2.5, count: 3)
// anotherThreeDoubles 被推断为 [Double],等价于 [2.5, 2.5, 2.5]

var sixDoubles = threeDoubles + anotherThreeDoubles
// sixDoubles 被推断为 [Double],等价于 [0.0, 0.0, 0.0, 2.5, 2.5, 2.5]

用数组字面量构造数组

我们可以使用数组字面量来进行数组构造,这是一种用一个或者多个数值构造数组的简单方法。数组字面量是一系列由逗号分割并由方括号包含的数值:

[value 1, value 2, value 3]

下面这个例子创建了一个叫做 shoppingList 并且存储 String 的数组:

1
2
var shoppingList: [String] = ["Eggs", "Milk"]
// shoppingList 已经被构造并且拥有两个初始项。

shoppingList 变量被声明为“字符串值类型的数组“,记作 [String]。 因为这个数组被规定只有 String 一种数据结构,所以只有 String 类型可以在其中被存取。 在这里,shoppingList 数组由两个 String 值("Eggs""Milk")构造,并且由数组字面量定义。

注意

shoppingList 数组被声明为变量(var 关键字创建)而不是常量(let 创建)是因为以后可能会有更多的数据项被插入其中。

在这个例子中,字面量仅仅包含两个 String 值。匹配了该数组的变量声明(只能包含 String 的数组),所以这个字面量的分配过程可以作为用两个初始项来构造 shoppingList 的一种方式。

由于 Swift 的类型推断机制,当我们用字面量构造只拥有相同类型值数组的时候,我们不必把数组的类型定义清楚。shoppingList 的构造也可以这样写:

1
var shoppingList = ["Eggs", "Milk"]

因为所有数组字面量中的值都是相同的类型,Swift 可以推断出 [String]shoppingList 中变量的正确类型。

访问和修改数组

我们可以通过数组的方法和属性来访问和修改数组,或者使用下标语法。

可以使用数组的只读属性 count 来获取数组中的数据项数量:

1
2
print("The shopping list contains \(shoppingList.count) items.")
// 输出“The shopping list contains 2 items.”(这个数组有2个项)

使用布尔属性 isEmpty 作为一个缩写形式去检查 count 属性是否为 0

1
2
3
4
5
6
if shoppingList.isEmpty {
print("The shopping list is empty.")
} else {
print("The shopping list is not empty.")
}
// 打印“The shopping list is not empty.”(shoppinglist 不是空的)

也可以使用 append(_:) 方法在数组后面添加新的数据项:

1
2
shoppingList.append("Flour")
// shoppingList 现在有3个数据项,有人在摊煎饼

除此之外,使用加法赋值运算符(+=)也可以直接在数组后面添加一个或多个拥有相同类型的数据项:

1
2
3
4
shoppingList += ["Baking Powder"]
// shoppingList 现在有四项了
shoppingList += ["Chocolate Spread", "Cheese", "Butter"]
// shoppingList 现在有七项了

可以直接使用下标语法来获取数组中的数据项,把我们需要的数据项的索引值放在直接放在数组名称的方括号中:

1
2
var firstItem = shoppingList[0]
// 第一项是“Eggs”

注意

第一项在数组中的索引值是 0 而不是 1。 Swift 中的数组索引总是从零开始。

我们也可以用下标来改变某个已有索引值对应的数据值:

1
2
shoppingList[0] = "Six eggs"
// 其中的第一项现在是“Six eggs”而不是“Eggs”

还可以利用下标来一次改变一系列数据值,即使新数据和原有数据的数量是不一样的。下面的例子把 "Chocolate Spread""Cheese""Butter" 替换为 "Bananas""Apples"

1
2
shoppingList[4...6] = ["Bananas", "Apples"]
// shoppingList 现在有6项

注意

不可以用下标访问的形式去在数组尾部添加新项。

调用数组的 insert(_:at:) 方法来在某个具体索引值之前添加数据项:

1
2
3
shoppingList.insert("Maple Syrup", at: 0)
// shoppingList 现在有7项
// 现在是这个列表中的第一项是“Maple Syrup”

这次 insert(_:at:) 方法调用把值为 "Maple Syrup" 的新数据项插入列表的最开始位置,并且使用 0 作为索引值。

类似的我们可以使用 remove(at:) 方法来移除数组中的某一项。这个方法把数组在特定索引值中存储的数据项移除并且返回这个被移除的数据项(我们不需要的时候就可以无视它):

1
2
3
4
let mapleSyrup = shoppingList.remove(at: 0)
// 索引值为0的数据项被移除
// shoppingList 现在只有6项,而且不包括 Maple Syrup
// mapleSyrup 常量的值等于被移除数据项“Maple Syrup”的值

注意

如果我们试着对索引越界的数据进行检索或者设置新值的操作,会引发一个运行期错误。我们可以使用索引值和数组的 count 属性进行比较来在使用某个索引之前先检验是否有效。除了当 count 等于 0 时(说明这是个空数组),最大索引值一直是 count - 1,因为数组都是零起索引。

数据项被移除后数组中的空出项会被自动填补,所以现在索引值为 0 的数据项的值再次等于 "Six eggs"

1
2
firstItem = shoppingList[0]
// firstItem 现在等于“Six eggs”

如果我们只想把数组中的最后一项移除,可以使用 removeLast() 方法而不是 remove(at:) 方法来避免我们需要获取数组的 count 属性。就像后者一样,前者也会返回被移除的数据项:

1
2
3
4
let apples = shoppingList.removeLast()
// 数组的最后一项被移除了
// shoppingList 现在只有5项,不包括 Apples
// apples 常量的值现在等于“Apples”字符串

数组的遍历

我们可以使用 for-in 循环来遍历所有数组中的数据项:

1
2
3
4
5
6
7
8
for item in shoppingList {
print(item)
}
// Six eggs
// Milk
// Flour
// Baking Powder
// Bananas

如果我们同时需要每个数据项的值和索引值,可以使用 enumerated() 方法来进行数组遍历。enumerated() 返回一个由每一个数据项索引值和数据值组成的元组。我们可以把这个元组分解成临时常量或者变量来进行遍历:

1
2
3
4
5
6
7
8
for (index, value) in shoppingList.enumerated() {
print("Item \(String(index + 1)): \(value)")
}
// Item 1: Six eggs
// Item 2: Milk
// Item 3: Flour
// Item 4: Baking Powder
// Item 5: Bananas

更多关于 for-in 循环的介绍请参见 For 循环

集合(Sets)

*集合(Set)*用来存储相同类型并且没有确定顺序的值。当集合元素顺序不重要时或者希望确保每个元素只出现一次时可以使用集合而不是数组。

注意 Swift 的 Set 类型被桥接到 Foundation 中的 NSSet 类。

关于使用 FoundationCocoaSet 的知识,参见 Using Swift with Cocoa and Obejective-C(Swift 4.1)使用 Cocoa 数据类型部分。

集合类型的哈希值

一个类型为了存储在集合中,该类型必须是可哈希化的——也就是说,该类型必须提供一个方法来计算它的哈希值。一个哈希值是 Int 类型的,相等的对象哈希值必须相同,比如 a==b,因此必须 a.hashValue == b.hashValue

Swift 的所有基本类型(比如 StringIntDoubleBool)默认都是可哈希化的,可以作为集合的值的类型或者字典的键的类型。没有关联值的枚举成员值(在 枚举 有讲述)默认也是可哈希化的。

注意

你可以使用你自定义的类型作为集合的值的类型或者是字典的键的类型,但你需要使你的自定义类型遵循 Swift 标准库中的 Hashable 协议。遵循 Hashable 协议的类型需要提供一个类型为 Int 的可读属性 hashValue。由类型的 hashValue 属性返回的值不需要在同一程序的不同执行周期或者不同程序之间保持相同。

因为 Hashable 协议遵循 Equatable 协议,所以遵循该协议的类型也必须提供一个“是否相等”运算符(==)的实现。这个 Equatable 协议要求任何遵循 == 实现的实例间都是一种相等的关系。也就是说,对于 a,b,c 三个值来说,== 的实现必须满足下面三种情况:

  • a == a(自反性)
  • a == b 意味着 b == a(对称性)
  • a == b && b == c 意味着 a == c(传递性)

关于遵循协议的更多信息,请看 协议

集合类型语法

Swift 中的 Set 类型被写为 Set<Element>,这里的 Element 表示 Set 中允许存储的类型,和数组不同的是,集合没有等价的简化形式。

创建和构造一个空的集合

你可以通过构造器语法创建一个特定类型的空集合:

1
2
3
var letters = Set<Character>()
print("letters is of type Set<Character> with \(letters.count) items.")
// 打印“letters is of type Set<Character> with 0 items.”

注意

通过构造器,这里的 letters 变量的类型被推断为 Set<Character>

此外,如果上下文提供了类型信息,比如作为函数的参数或者已知类型的变量或常量,我们可以通过一个空的数组字面量创建一个空的 Set

1
2
3
4
letters.insert("a")
// letters 现在含有1个 Character 类型的值
letters = []
// letters 现在是一个空的 Set,但是它依然是 Set<Character> 类型

用数组字面量创建集合

你可以使用数组字面量来构造集合,并且可以使用简化形式写一个或者多个值作为集合元素。

下面的例子创建一个称之为 favoriteGenres 的集合来存储 String 类型的值:

1
2
var favoriteGenres: Set<String> = ["Rock", "Classical", "Hip hop"]
// favoriteGenres 被构造成含有三个初始值的集合

这个 favoriteGenres 变量被声明为“一个 String 值的集合”,写为 Set<String>。由于这个特定的集合含有指定 String 类型的值,所以它只允许存储 String 类型值。这里的 favoriteGenres 变量有三个 String 类型的初始值("Rock""Classical""Hip hop"),并以数组字面量的方式出现。

注意

favoriteGenres 被声明为一个变量(拥有 var 标示符)而不是一个常量(拥有 let 标示符),因为它里面的元素将会在下面的例子中被增加或者移除。

一个 Set 类型不能从数组字面量中被单独推断出来,因此 Set 类型必须显式声明。然而,由于 Swift 的类型推断功能,如果你想使用一个数组字面量构造一个 Set 并且该数组字面量中的所有元素类型相同,那么你无须写出 Set 的具体类型。favoriteGenres 的构造形式可以采用简化的方式代替:

1
var favoriteGenres: Set = ["Rock", "Classical", "Hip hop"]

由于数组字面量中的所有元素类型相同,Swift 可以推断出 Set<String> 作为 favoriteGenres 变量的正确类型。

访问和修改一个集合

你可以通过 Set 的属性和方法来访问和修改一个 Set

为了找出一个 Set 中元素的数量,可以使用其只读属性 count

1
2
print("I have \(favoriteGenres.count) favorite music genres.")
// 打印“I have 3 favorite music genres.”

使用布尔属性 isEmpty 作为一个缩写形式去检查 count 属性是否为 0

1
2
3
4
5
6
if favoriteGenres.isEmpty {
print("As far as music goes, I'm not picky.")
} else {
print("I have particular music preferences.")
}
// 打印“I have particular music preferences.”

你可以通过调用 Setinsert(_:) 方法来添加一个新元素:

1
2
favoriteGenres.insert("Jazz")
// favoriteGenres 现在包含4个元素

你可以通过调用 Setremove(_:) 方法去删除一个元素,如果该值是该 Set 的一个元素则删除该元素并且返回被删除的元素值,否则如果该 Set 不包含该值,则返回 nil。另外,Set 中的所有元素可以通过它的 removeAll() 方法删除。

1
2
3
4
5
6
if let removedGenre = favoriteGenres.remove("Rock") {
print("\(removedGenre)? I'm over it.")
} else {
print("I never much cared for that.")
}
// 打印“Rock? I'm over it.”

使用 contains(_:) 方法去检查 Set 中是否包含一个特定的值:

1
2
3
4
5
6
if favoriteGenres.contains("Funk") {
print("I get up on the good foot.")
} else {
print("It's too funky in here.")
}
// 打印“It's too funky in here.”

遍历一个集合

你可以在一个 for-in 循环中遍历一个 Set 中的所有值。

1
2
3
4
5
6
for genre in favoriteGenres {
print("\(genre)")
}
// Classical
// Jazz
// Hip hop

更多关于 for-in 循环的信息,参见 For 循环

Swift 的 Set 类型没有确定的顺序,为了按照特定顺序来遍历一个 Set 中的值可以使用 sorted() 方法,它将返回一个有序数组,这个数组的元素排列顺序由操作符’<'对元素进行比较的结果来确定。

1
2
3
4
5
6
for genre in favoriteGenres.sorted() {
print("\(genre)")
}
// Classical
// Hip hop
// Jazz

集合操作

你可以高效地完成 Set 的一些基本操作,比如把两个集合组合到一起,判断两个集合共有元素,或者判断两个集合是否全包含,部分包含或者不相交。

基本集合操作

下面的插图描述了两个集合 ab,以及通过阴影部分的区域显示集合各种操作的结果。

  • 使用 intersection(_:) 方法根据两个集合中都包含的值创建的一个新的集合。
  • 使用 symmetricDifference(_:) 方法根据在一个集合中但不在两个集合中的值创建一个新的集合。
  • 使用 union(_:) 方法根据两个集合的值创建一个新的集合。
  • 使用 subtracting(_:) 方法根据不在该集合中的值创建一个新的集合。
1
2
3
4
5
6
7
8
9
10
11
12
let oddDigits: Set = [1, 3, 5, 7, 9]
let evenDigits: Set = [0, 2, 4, 6, 8]
let singleDigitPrimeNumbers: Set = [2, 3, 5, 7]

oddDigits.union(evenDigits).sorted()
// [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
oddDigits.intersection(evenDigits).sorted()
// []
oddDigits.subtracting(singleDigitPrimeNumbers).sorted()
// [1, 9]
oddDigits.symmetricDifference(singleDigitPrimeNumbers).sorted()
// [1, 2, 9]

集合成员关系和相等

下面的插图描述了三个集合 abc,以及通过重叠区域表述集合间共享的元素。集合 a 是集合 b 的父集合,因为 a 包含了 b 中所有的元素,相反的,集合 b 是集合 a 的子集合,因为属于 b 的元素也被 a 包含。集合 b 和集合 c 彼此不关联,因为它们之间没有共同的元素。

  • 使用“是否相等”运算符(==)来判断两个集合是否包含全部相同的值。
  • 使用 isSubset(of:) 方法来判断一个集合中的值是否也被包含在另外一个集合中。
  • 使用 isSuperset(of:) 方法来判断一个集合中包含另一个集合中所有的值。
  • 使用 isStrictSubset(of:) 或者 isStrictSuperset(of:) 方法来判断一个集合是否是另外一个集合的子集合或者父集合并且两个集合并不相等。
  • 使用 isDisjoint(with:) 方法来判断两个集合是否不含有相同的值(是否没有交集)。
1
2
3
4
5
6
7
8
9
10
let houseAnimals: Set = ["🐶", "🐱"]
let farmAnimals: Set = ["🐮", "🐔", "🐑", "🐶", "🐱"]
let cityAnimals: Set = ["🐦", "🐭"]

houseAnimals.isSubset(of: farmAnimals)
// true
farmAnimals.isSuperset(of: houseAnimals)
// true
farmAnimals.isDisjoint(with: cityAnimals)
// true

字典

字典是一种存储多个相同类型的值的容器。每个值(value)都关联唯一的键(key),键作为字典中的这个值数据的标识符。和数组中的数据项不同,字典中的数据项并没有具体顺序。我们在需要通过标识符(键)访问数据的时候使用字典,这种方法很大程度上和我们在现实世界中使用字典查字义的方法一样。

注意

Swift 的 Dictionary 类型被桥接到 FoundationNSDictionary 类。

更多关于在 FoundationCocoa 中使用 Dictionary 类型的信息,参见 Using Swift with Cocoa and Obejective-C(Swift 4.1)使用 Cocoa 数据类型 部分。

字典类型简化语法

Swift 的字典使用 Dictionary<Key, Value> 定义,其中 Key 是字典中键的数据类型,Value 是字典中对应于这些键所存储值的数据类型。

注意

一个字典的 Key 类型必须遵循 Hashable 协议,就像 Set 的值类型。

我们也可以用 [Key: Value] 这样简化的形式去创建一个字典类型。虽然这两种形式功能上相同,但是后者是首选,并且这本指导书涉及到字典类型时通篇采用后者。

创建一个空字典

我们可以像数组一样使用构造语法创建一个拥有确定类型的空字典:

1
2
var namesOfIntegers = [Int: String]()
// namesOfIntegers 是一个空的 [Int: String] 字典

这个例子创建了一个 [Int: String] 类型的空字典来储存整数的英语命名。它的键是 Int 型,值是 String 型。

如果上下文已经提供了类型信息,我们可以使用空字典字面量来创建一个空字典,记作 [:](中括号中放一个冒号):

1
2
3
4
namesOfIntegers[16] = "sixteen"
// namesOfIntegers 现在包含一个键值对
namesOfIntegers = [:]
// namesOfIntegers 又成为了一个 [Int: String] 类型的空字典

用字典字面量创建字典

我们可以使用字典字面量来构造字典,这和我们刚才介绍过的数组字面量拥有相似语法。字典字面量是一种将一个或多个键值对写作 Dictionary 集合的快捷途径。

一个键值对是一个 key 和一个 value 的结合体。在字典字面量中,每一个键值对的键和值都由冒号分割。这些键值对构成一个列表,其中这些键值对由方括号包含、由逗号分割:

1
[key 1: value 1, key 2: value 2, key 3: value 3]

下面的例子创建了一个存储国际机场名称的字典。在这个字典中键是三个字母的国际航空运输相关代码,值是机场名称:

1
var airports: [String: String] = ["YYZ": "Toronto Pearson", "DUB": "Dublin"]

airports 字典被声明为一种 [String: String] 类型,这意味着这个字典的键和值都是 String 类型。

注意

airports 字典被声明为变量(用 var 关键字)而不是常量(let 关键字)因为后来更多的机场信息会被添加到这个示例字典中。

airports 字典使用字典字面量初始化,包含两个键值对。第一对的键是 YYZ,值是 Toronto Pearson。第二对的键是 DUB,值是 Dublin

这个字典语句包含了两个 String: String 类型的键值对。它们对应 airports 变量声明的类型(一个只有 String 键和 String 值的字典)所以这个字典字面量的任务是构造拥有两个初始数据项的 airport 字典。

和数组一样,我们在用字典字面量构造字典时,如果它的键和值都有各自一致的类型,那么就不必写出字典的类型。 airports 字典也可以用这种简短方式定义:

1
var airports = ["YYZ": "Toronto Pearson", "DUB": "Dublin"]

因为这个语句中所有的键和值都各自拥有相同的数据类型,Swift 可以推断出 Dictionary<String, String>airports 字典的正确类型。

访问和修改字典

我们可以通过字典的方法和属性来访问和修改字典,或者通过使用下标语法。

和数组一样,我们可以通过字典的只读属性 count 来获取某个字典的数据项数量:

1
2
print("The dictionary of airports contains \(airports.count) items.")
// 打印“The dictionary of airports contains 2 items.”(这个字典有两个数据项)

使用布尔属性 isEmpty 作为一个缩写形式去检查 count 属性是否为 0

1
2
3
4
5
6
if airports.isEmpty {
print("The airports dictionary is empty.")
} else {
print("The airports dictionary is not empty.")
}
// 打印“The airports dictionary is not empty.”

我们也可以在字典中使用下标语法来添加新的数据项。可以使用一个恰当类型的键作为下标索引,并且分配恰当类型的新值:

1
2
airports["LHR"] = "London"
// airports 字典现在有三个数据项

我们也可以使用下标语法来改变特定键对应的值:

1
2
airports["LHR"] = "London Heathrow"
// “LHR”对应的值被改为“London Heathrow”

作为另一种下标方法,字典的 updateValue(_:forKey:) 方法可以设置或者更新特定键对应的值。就像上面所示的下标示例,updateValue(_:forKey:) 方法在这个键不存在对应值的时候会设置新值或者在存在时更新已存在的值。和上面的下标方法不同的,updateValue(_:forKey:) 这个方法返回更新值之前的原值。这样使得我们可以检查更新是否成功。

updateValue(_:forKey:) 方法会返回对应值的类型的可选值。举例来说:对于存储 String 值的字典,这个函数会返回一个 String? 或者“可选 String”类型的值。

如果有值存在于更新前,则这个可选值包含了旧值,否则它将会是 nil

1
2
3
4
if let oldValue = airports.updateValue("Dublin Airport", forKey: "DUB") {
print("The old value for DUB was \(oldValue).")
}
// 输出“The old value for DUB was Dublin.”

我们也可以使用下标语法来在字典中检索特定键对应的值。因为有可能请求的键没有对应的值存在,字典的下标访问会返回对应值的类型的可选值。如果这个字典包含请求键所对应的值,下标会返回一个包含这个存在值的可选值,否则将返回 nil

1
2
3
4
5
6
if let airportName = airports["DUB"] {
print("The name of the airport is \(airportName).")
} else {
print("That airport is not in the airports dictionary.")
}
// 打印“The name of the airport is Dublin Airport.”

我们还可以使用下标语法来通过给某个键的对应值赋值为 nil 来从字典里移除一个键值对:

1
2
3
4
airports["APL"] = "Apple Internation"
// “Apple Internation”不是真的 APL 机场,删除它
airports["APL"] = nil
// APL 现在被移除了

此外,removeValue(forKey:) 方法也可以用来在字典中移除键值对。这个方法在键值对存在的情况下会移除该键值对并且返回被移除的值或者在没有值的情况下返回 nil

1
2
3
4
5
6
if let removedValue = airports.removeValue(forKey: "DUB") {
print("The removed airport's name is \(removedValue).")
} else {
print("The airports dictionary does not contain a value for DUB.")
}
// 打印“The removed airport's name is Dublin Airport.”

字典遍历

我们可以使用 for-in 循环来遍历某个字典中的键值对。每一个字典中的数据项都以 (key, value) 元组形式返回,并且我们可以使用临时常量或者变量来分解这些元组:

1
2
3
4
5
for (airportCode, airportName) in airports {
print("\(airportCode): \(airportName)")
}
// YYZ: Toronto Pearson
// LHR: London Heathrow

更多关于 for-in 循环的信息,参见 For 循环

通过访问 keys 或者 values 属性,我们也可以遍历字典的键或者值:

1
2
3
4
5
6
7
8
9
10
11
for airportCode in airports.keys {
print("Airport code: \(airportCode)")
}
// Airport code: YYZ
// Airport code: LHR

for airportName in airports.values {
print("Airport name: \(airportName)")
}
// Airport name: Toronto Pearson
// Airport name: London Heathrow

如果我们只是需要使用某个字典的键集合或者值集合来作为某个接受 Array 实例的 API 的参数,可以直接使用 keys 或者 values 属性构造一个新数组:

1
2
3
4
5
let airportCodes = [String](airports.keys)
// airportCodes 是 ["YYZ", "LHR"]

let airportNames = [String](airports.values)
// airportNames 是 ["Toronto Pearson", "London Heathrow"]

Swift 的字典类型是无序集合类型。为了以特定的顺序遍历字典的键或值,可以对字典的 keysvalues 属性使用 sorted() 方法。

留言與分享

swift字符串和字符

分類 编程语言, swift

字符串和字符

字符串是是一系列字符的集合,例如 "hello, world""albatross"。Swift 的字符串通过 String 类型来表示。而 String 内容的访问方式有多种,例如以 Character 值的集合。

Swift 的 StringCharacter 类型提供了一种快速且兼容 Unicode 的方式来处理代码中的文本内容。创建和操作字符串的语法与 C 语言中字符串操作相似,轻量并且易读。通过 + 符号就可以非常简单的实现两个字符串的拼接操作。与 Swift 中其他值一样,能否更改字符串的值,取决于其被定义为常量还是变量。你可以在已有字符串中插入常量、变量、字面量和表达式从而形成更长的字符串,这一过程也被成为字符串插值。尤其是在为显示、存储和打印创建自定义字符串值时,字符串插值操作尤其有用。

尽管语法简易,但 Swift 中的 String 类型的实现却很快速和现代化。每一个字符串都是由编码无关的 Unicode 字符组成,并支持访问字符的多种 Unicode 表示形式。

注意

Swift 的 String 类型与 Foundation NSString 类进行了无缝桥接。Foundation 还对 String 进行扩展使其可以访问 NSString 类型中定义的方法。这意味着调用那些 NSString 的方法,你无需进行任何类型转换。

更多关于在 Foundation 和 Cocoa 中使用 String 的信息请查看 Bridging Between String and NSString

字符串字面量

你可以在代码里使用一段预定义的字符串值作为字符串字面量。字符串字面量是由一对双引号包裹着的具有固定顺序的字符集。

字符串字面量可以用于为常量和变量提供初始值:

1
let someString = "Some string literal value"

注意,Swift 之所以推断 someString 常量为字符串类型,是因为它使用了字面量方式进行初始化。

多行字符串字面量

如果你需要一个字符串是跨越多行的,那就使用多行字符串字面量 — 由一对三个双引号包裹着的具有固定顺序的文本字符集:

1
2
3
4
5
6
7
let quotation = """
The White Rabbit put on his spectacles. "Where shall I begin,
please your Majesty?" he asked.

"Begin at the beginning," the King said gravely, "and go on
till you come to the end; then stop."
"""

一个多行字符串字面量包含了所有的在开启和关闭引号(""")中的行。这个字符从开启引号(""")之后的第一行开始,到关闭引号(""")之前为止。这就意味着字符串开启引号之后(""")或者结束引号(""")之前都没有换行符号。(译者:下面两个字符串其实是一样的,虽然第二个使用了多行字符串的形式)

1
2
3
4
let singleLineString = "These are the same."
let multilineString = """
These are the same.
"""

如果你的代码中,多行字符串字面量包含换行符的话,则多行字符串字面量中也会包含换行符。如果你想换行,以便加强代码的可读性,但是你又不想在你的多行字符串字面量中出现换行符的话,你可以用在行尾写一个反斜杠(\)作为续行符。

1
2
3
4
5
6
7
let softWrappedQuotation = """
The White Rabbit put on his spectacles. "Where shall I begin, \
please your Majesty?" he asked.

"Begin at the beginning," the King said gravely, "and go on \
till you come to the end; then stop."
"""

为了让一个多行字符串字面量开始和结束于换行符,请将换行写在第一行和最后一行,例如:

1
2
3
4
5
6
let lineBreaks = """

This string starts with a line break.
It also ends with a line break.

"""

一个多行字符串字面量能够缩进来匹配周围的代码。关闭引号(""")之前的空白字符串告诉 Swift 编译器其他各行多少空白字符串需要忽略。然而,如果你在某行的前面写的空白字符串超出了关闭引号(""")之前的空白字符串,则超出部分将被包含在多行字符串字面量中。

在上面的例子中,尽管整个多行字符串字面量都是缩进的(源代码缩进),第一行和最后一行没有以空白字符串开始(实际的变量值)。中间一行的缩进用空白字符串(源代码缩进)比关闭引号(""")之前的空白字符串多,所以,它的行首将有4个空格。

字符串字面量的特殊字符

字符串字面量可以包含以下特殊字符:

  • 转义字符 \0(空字符)、\\(反斜线)、\t(水平制表符)、\n(换行符)、\r(回车符)、\"(双引号)、\'(单引号)。
  • Unicode 标量,写成 \u{n}(u 为小写),其中 n 为任意一到八位十六进制数且可用的 Unicode 位码。

下面的代码为各种特殊字符的使用示例。 wiseWords 常量包含了两个双引号。 dollarSignblackHeartsparklingHeart 常量演示了三种不同格式的 Unicode 标量:

1
2
3
4
5
let wiseWords = "\"Imagination is more important than knowledge\" - Einstein"
// "Imageination is more important than knowledge" - Enistein
let dollarSign = "\u{24}" // $,Unicode 标量 U+0024
let blackHeart = "\u{2665}" // ♥,Unicode 标量 U+2665
let sparklingHeart = "\u{1F496}" // 💖,Unicode 标量 U+1F496

由于多行字符串字面量使用了三个双引号,而不是一个,所以你可以在多行字符串字面量里直接使用双引号(")而不必加上转义符(\)。要在多行字符串字面量中使用 """ 的话,就需要使用至少一个转义符(\):

1
2
3
4
let threeDoubleQuotes = """
Escaping the first quote \"""
Escaping all three quotes \"\"\"
"""

扩展字符串分隔符

您可以将字符串文字放在扩展分隔符中,这样字符串中的特殊字符将会被直接包含而非转义后的效果。将字符串放在引号(")中并用数字符号()括起来。例如,打印字符串文字 #“Line 1 \ nLine 2”# 打印换行符转义序列(\n)而不是进行换行打印。

如果需要字符串文字中字符的特殊效果,请匹配转义字符(\)后面添加与起始位置个数相匹配的 # 符。 例如,如果您的字符串是 #“Line 1 \ nLine 2”# 并且您想要换行,则可以使用 #“Line 1 \ #nLine 2”# 来代替。 同样,###“Line1 \ ### nLine2”### 也可以实现换行效果。

扩展分隔符创建的字符串文字也可以是多行字符串文字。 您可以使用扩展分隔符在多行字符串中包含文本 “”",覆盖原有的结束文字的默认行为。例如:

1
2
3
let threeMoreDoubleQuotationMarks = #"""
Here are three more double quotes: """
"""#

初始化空字符串

要创建一个空字符串作为初始值,可以将空的字符串字面量赋值给变量,也可以初始化一个新的 String 实例:

1
2
3
var emptyString = ""               // 空字符串字面量
var anotherEmptyString = String() // 初始化方法
// 两个字符串均为空并等价。

你可以通过检查 Bool 类型的 isEmpty 属性来判断该字符串是否为空:

1
2
3
4
if emptyString.isEmpty {
print("Nothing to see here")
}
// 打印输出:“Nothing to see here”

字符串可变性

你可以通过将一个特定字符串分配给一个变量来对其进行修改,或者分配给一个常量来保证其不会被修改:

1
2
3
4
5
6
7
var variableString = "Horse"
variableString += " and carriage"
// variableString 现在为 "Horse and carriage"

let constantString = "Highlander"
constantString += " and another Highlander"
// 这会报告一个编译错误(compile-time error) - 常量字符串不可以被修改。

注意

在 Objective-C 和 Cocoa 中,需要通过选择两个不同的类(NSStringNSMutableString)来指定字符串是否可以被修改。

字符串是值类型

在 Swift 中 String 类型是值类型。如果你创建了一个新的字符串,那么当其进行常量、变量赋值操作,或在函数/方法中传递时,会进行值拷贝。在前述任一情况下,都会对已有字符串值创建新副本,并对该新副本而非原始字符串进行传递或赋值操作。值类型在 结构体和枚举是值类型 中进行了详细描述。

Swift 默认拷贝字符串的行为保证了在函数/方法向你传递的字符串所属权属于你,无论该值来自于哪里。你可以确信传递的字符串不会被修改,除非你自己去修改它。

在实际编译时,Swift 编译器会优化字符串的使用,使实际的复制只发生在绝对必要的情况下,这意味着你将字符串作为值类型的同时可以获得极高的性能。

使用字符

你可通过 for-in 循环来遍历字符串,获取字符串中每一个字符的值:

1
2
3
4
5
6
7
8
for character in "Dog!🐶" {
print(character)
}
// D
// o
// g
// !
// 🐶

for-in 循环在 For 循环 中进行了详细描述。

另外,通过标明一个 Character 类型并用字符字面量进行赋值,可以建立一个独立的字符常量或变量:

1
let exclamationMark: Character = "!"

字符串可以通过传递一个值类型为 Character 的数组作为自变量来初始化:

1
2
3
4
let catCharacters: [Character] = ["C", "a", "t", "!", "🐱"]
let catString = String(catCharacters)
print(catString)
// 打印输出:“Cat!🐱”

连接字符串和字符

字符串可以通过加法运算符(+)相加在一起(或称“连接”)创建一个新的字符串:

1
2
3
4
let string1 = "hello"
let string2 = " there"
var welcome = string1 + string2
// welcome 现在等于 "hello there"

你也可以通过加法赋值运算符(+=)将一个字符串添加到一个已经存在字符串变量上:

1
2
3
var instruction = "look over"
instruction += string2
// instruction 现在等于 "look over there"

你可以用 append() 方法将一个字符附加到一个字符串变量的尾部:

1
2
3
let exclamationMark: Character = "!"
welcome.append(exclamationMark)
// welcome 现在等于 "hello there!"

注意

你不能将一个字符串或者字符添加到一个已经存在的字符变量上,因为字符变量只能包含一个字符。

如果你需要使用多行字符串字面量来拼接字符串,并且你需要字符串每一行都以换行符结尾,包括最后一行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
let badStart = """
one
two
"""
let end = """
three
"""
print(badStart + end)
// 打印两行:
// one
// twothree

let goodStart = """
one
two

"""
print(goodStart + end)
// 打印三行:
// one
// two
// three

上面的代码,把 badStartend 拼接起来的字符串非我们想要的结果。因为 badStart 最后一行没有换行符,它与 end 的第一行结合到了一起。相反的,goodStart 的每一行都以换行符结尾,所以它与 end 拼接的字符串总共有三行,正如我们期望的那样。

字符串插值

字符串插值是一种构建新字符串的方式,可以在其中包含常量、变量、字面量和表达式。字符串字面量多行字符串字面量都可以使用字符串插值。你插入的字符串字面量的每一项都在以反斜线为前缀的圆括号中:

1
2
3
let multiplier = 3
let message = "\(multiplier) times 2.5 is \(Double(multiplier) * 2.5)"
// message 是 "3 times 2.5 is 7.5"

在上面的例子中,multiplier 作为 \(multiplier) 被插入到一个字符串常量量中。当创建字符串执行插值计算时此占位符会被替换为 multiplier 实际的值。

multiplier 的值也作为字符串中后面表达式的一部分。该表达式计算 Double(multiplier) * 2.5 的值并将结果(7.5)插入到字符串中。在这个例子中,表达式写为 \(Double(multiplier) * 2.5) 并包含在字符串字面量中。

注意

插值字符串中写在括号中的表达式不能包含非转义反斜杠(\),并且不能包含回车或换行符。不过,插值字符串可以包含其他字面量。

Unicode

Unicode是一个用于在不同书写系统中对文本进行编码、表示和处理的国际标准。它使你可以用标准格式表示来自任意语言几乎所有的字符,并能够对文本文件或网页这样的外部资源中的字符进行读写操作。Swift 的 StringCharacter 类型是完全兼容 Unicode 标准的。

Unicode 标量

Swift 的 String 类型是基于 Unicode 标量 建立的。Unicode 标量是对应字符或者修饰符的唯一的 21 位数字,例如 U+0061 表示小写的拉丁字母(LATIN SMALL LETTER A)(“a”),U+1F425 表示小鸡表情(FRONT-FACING BABY CHICK)(“🐥”)。

请注意,并非所有 21 位 Unicode 标量值都分配给字符,某些标量被保留用于将来分配或用于 UTF-16 编码。已分配的标量值通常也有一个名称,例如上面示例中的 LATIN SMALL LETTER A 和 FRONT-FACING BABY CHICK。

可扩展的字形群集

每一个 Swift 的 Character 类型代表一个可扩展的字形群。而一个可扩展的字形群构成了人类可读的单个字符,它由一个或多个(当组合时) Unicode 标量的序列组成。

举个例子,字母 é 可以用单一的 Unicode 标量 é(LATIN SMALL LETTER E WITH ACUTE, 或者 U+00E9)来表示。然而一个标准的字母 e(LATIN SMALL LETTER E 或者 U+0065) 加上一个急促重音(COMBINING ACTUE ACCENT)的标量(U+0301),这样一对标量就表示了同样的字母 é。 这个急促重音的标量形象的将 e 转换成了 é

在这两种情况中,字母 é 代表了一个单一的 Swift 的 Character 值,同时代表了一个可扩展的字形群。在第一种情况,这个字形群包含一个单一标量;而在第二种情况,它是包含两个标量的字形群:

1
2
3
let eAcute: Character = "\u{E9}"                         // é
let combinedEAcute: Character = "\u{65}\u{301}" // e 后面加上 ́
// eAcute 是 é, combinedEAcute 是 é

可扩展的字形集是一个将许多复杂的脚本字符表示为单个字符值的灵活方式。例如,来自朝鲜语字母表的韩语音节能表示为组合或分解的有序排列。在 Swift 都会表示为同一个单一的 Character 值:

1
2
3
let precomposed: Character = "\u{D55C}"                  // 한
let decomposed: Character = "\u{1112}\u{1161}\u{11AB}" // ᄒ, ᅡ, ᆫ
// precomposed 是 한, decomposed 是 한

可拓展的字符群集可以使包围记号(例如 COMBINING ENCLOSING CIRCLE 或者 U+20DD)的标量包围其他 Unicode 标量,作为一个单一的 Character 值:

1
2
let enclosedEAcute: Character = "\u{E9}\u{20DD}"
// enclosedEAcute 是 é⃝

地域性指示符号的 Unicode 标量可以组合成一个单一的 Character 值,例如 REGIONAL INDICATOR SYMBOL LETTER U(U+1F1FA)和 REGIONAL INDICATOR SYMBOL LETTER S(U+1F1F8):

1
2
let regionalIndicatorForUS: Character = "\u{1F1FA}\u{1F1F8}"
// regionalIndicatorForUS 是 🇺🇸

计算字符数量

如果想要获得一个字符串中 Character 值的数量,可以使用 count 属性:

1
2
3
let unusualMenagerie = "Koala 🐨, Snail 🐌, Penguin 🐧, Dromedary 🐪"
print("unusualMenagerie has \(unusualMenagerie.count) characters")
// 打印输出“unusualMenagerie has 40 characters”

注意在 Swift 中,使用可拓展的字符群集作为 Character 值来连接或改变字符串时,并不一定会更改字符串的字符数量。

例如,如果你用四个字符的单词 cafe 初始化一个新的字符串,然后添加一个 COMBINING ACTUE ACCENT(U+0301)作为字符串的结尾。最终这个字符串的字符数量仍然是 4,因为第四个字符是 é,而不是 e

1
2
3
4
5
6
7
8
var word = "cafe"
print("the number of characters in \(word) is \(word.count)")
// 打印输出“the number of characters in cafe is 4”

word += "\u{301}" // 拼接一个重音,U+0301

print("the number of characters in \(word) is \(word.count)")
// 打印输出“the number of characters in café is 4”

注意

可扩展的字形群可以由多个 Unicode 标量组成。这意味着不同的字符以及相同字符的不同表示方式可能需要不同数量的内存空间来存储。所以 Swift 中的字符在一个字符串中并不一定占用相同的内存空间数量。因此在没有获取字符串的可扩展的字符群的范围时候,就不能计算出字符串的字符数量。如果你正在处理一个长字符串,需要注意 count 属性必须遍历全部的 Unicode 标量,来确定字符串的字符数量。

另外需要注意的是通过 count 属性返回的字符数量并不总是与包含相同字符的 NSStringlength 属性相同。NSStringlength 属性是利用 UTF-16 表示的十六位代码单元数字,而不是 Unicode 可扩展的字符群集。

访问和修改字符串

你可以通过字符串的属性和方法来访问和修改它,当然也可以用下标语法完成。

字符串索引

每一个 String 值都有一个关联的索引(index)类型,String.Index,它对应着字符串中的每一个 Character 的位置。

前面提到,不同的字符可能会占用不同数量的内存空间,所以要知道 Character 的确定位置,就必须从 String 开头遍历每一个 Unicode 标量直到结尾。因此,Swift 的字符串不能用整数(integer)做索引。

使用 startIndex 属性可以获取一个 String 的第一个 Character 的索引。使用 endIndex 属性可以获取最后一个 Character 的后一个位置的索引。因此,endIndex 属性不能作为一个字符串的有效下标。如果 String 是空串,startIndexendIndex 是相等的。

通过调用 Stringindex(before:)index(after:) 方法,可以立即得到前面或后面的一个索引。你还可以通过调用 index(_:offsetBy:) 方法来获取对应偏移量的索引,这种方式可以避免多次调用 index(before:)index(after:) 方法。

你可以使用下标语法来访问 String 特定索引的 Character

1
2
3
4
5
6
7
8
9
10
let greeting = "Guten Tag!"
greeting[greeting.startIndex]
// G
greeting[greeting.index(before: greeting.endIndex)]
// !
greeting[greeting.index(after: greeting.startIndex)]
// u
let index = greeting.index(greeting.startIndex, offsetBy: 7)
greeting[index]
// a

试图获取越界索引对应的 Character,将引发一个运行时错误。

1
2
greeting[greeting.endIndex] // error
greeting.index(after: endIndex) // error

使用 indices 属性会创建一个包含全部索引的范围(Range),用来在一个字符串中访问单个字符。

1
2
3
4
for index in greeting.indices {
print("\(greeting[index]) ", terminator: "")
}
// 打印输出“G u t e n T a g ! ”

注意

你可以使用 startIndexendIndex 属性或者 index(before:)index(after:)index(_:offsetBy:) 方法在任意一个确认的并遵循 Collection 协议的类型里面,如上文所示是使用在 String 中,你也可以使用在 ArrayDictionarySet 中。

插入和删除

调用 insert(_:at:) 方法可以在一个字符串的指定索引插入一个字符,调用 insert(contentsOf:at:) 方法可以在一个字符串的指定索引插入一个段字符串。

1
2
3
4
5
6
var welcome = "hello"
welcome.insert("!", at: welcome.endIndex)
// welcome 变量现在等于 "hello!"

welcome.insert(contentsOf:" there", at: welcome.index(before: welcome.endIndex))
// welcome 变量现在等于 "hello there!"

调用 remove(at:) 方法可以在一个字符串的指定索引删除一个字符,调用 removeSubrange(_:) 方法可以在一个字符串的指定索引删除一个子字符串。

1
2
3
4
5
6
welcome.remove(at: welcome.index(before: welcome.endIndex))
// welcome 现在等于 "hello there"

let range = welcome.index(welcome.endIndex, offsetBy: -6)..<welcome.endIndex
welcome.removeSubrange(range)
// welcome 现在等于 "hello"

注意

你可以使用 insert(_:at:)insert(contentsOf:at:)remove(at:)removeSubrange(_:) 方法在任意一个确认的并遵循 RangeReplaceableCollection 协议的类型里面,如上文所示是使用在 String 中,你也可以使用在 ArrayDictionarySet 中。

子字符串

当你从字符串中获取一个子字符串 —— 例如,使用下标或者 prefix(_:) 之类的方法 —— 就可以得到一个 SubString 的实例,而非另外一个 String。Swift 里的 SubString 绝大部分函数都跟 String 一样,意味着你可以使用同样的方式去操作 SubStringString。然而,跟 String 不同的是,你只有在短时间内需要操作字符串时,才会使用 SubString。当你需要长时间保存结果时,就把 SubString 转化为 String 的实例:

1
2
3
4
5
6
7
let greeting = "Hello, world!"
let index = greeting.firstIndex(of: ",") ?? greeting.endIndex
let beginning = greeting[..<index]
// beginning 的值为 "Hello"

// 把结果转化为 String 以便长期存储。
let newString = String(beginning)

就像 String,每一个 SubString 都会在内存里保存字符集。而 StringSubString 的区别在于性能优化上,SubString 可以重用原 String 的内存空间,或者另一个 SubString 的内存空间(String 也有同样的优化,但如果两个 String 共享内存的话,它们就会相等)。这一优化意味着你在修改 StringSubString 之前都不需要消耗性能去复制内存。就像前面说的那样,SubString 不适合长期存储 —— 因为它重用了原 String 的内存空间,原 String 的内存空间必须保留直到它的 SubString 不再被使用为止。

上面的例子,greeting 是一个 String,意味着它在内存里有一片空间保存字符集。而由于 beginninggreetingSubString,它重用了 greeting 的内存空间。相反,newString 是一个 String —— 它是使用 SubString 创建的,拥有一片自己的内存空间。下面的图展示了他们之间的关系:

注意

StringSubString 都遵循 StringProtocol<//apple_ref/swift/intf/s:s14StringProtocolP> 协议,这意味着操作字符串的函数使用 StringProtocol 会更加方便。你可以传入 StringSubString 去调用函数。

比较字符串

Swift 提供了三种方式来比较文本值:字符串字符相等、前缀相等和后缀相等。

字符串/字符相等

字符串/字符可以用等于操作符(==)和不等于操作符(!=),详细描述在 比较运算符

1
2
3
4
5
6
let quotation = "We're a lot alike, you and I."
let sameQuotation = "We're a lot alike, you and I."
if quotation == sameQuotation {
print("These two strings are considered equal")
}
// 打印输出“These two strings are considered equal”

如果两个字符串(或者两个字符)的可扩展的字形群集是标准相等,那就认为它们是相等的。只要可扩展的字形群集有同样的语言意义和外观则认为它们标准相等,即使它们是由不同的 Unicode 标量构成。

例如,LATIN SMALL LETTER E WITH ACUTE(U+00E9)就是标准相等于 LATIN SMALL LETTER E(U+0065)后面加上 COMBINING ACUTE ACCENT(U+0301)。这两个字符群集都是表示字符 é 的有效方式,所以它们被认为是标准相等的:

1
2
3
4
5
6
7
8
9
10
// "Voulez-vous un café?" 使用 LATIN SMALL LETTER E WITH ACUTE
let eAcuteQuestion = "Voulez-vous un caf\u{E9}?"

// "Voulez-vous un café?" 使用 LATIN SMALL LETTER E and COMBINING ACUTE ACCENT
let combinedEAcuteQuestion = "Voulez-vous un caf\u{65}\u{301}?"

if eAcuteQuestion == combinedEAcuteQuestion {
print("These two strings are considered equal")
}
// 打印输出“These two strings are considered equal”

相反,英语中的 LATIN CAPITAL LETTER A(U+0041,或者 A)不等于俄语中的 CYRILLIC CAPITAL LETTER A(U+0410,或者 A)。两个字符看着是一样的,但却有不同的语言意义:

1
2
3
4
5
6
7
8
let latinCapitalLetterA: Character = "\u{41}"

let cyrillicCapitalLetterA: Character = "\u{0410}"

if latinCapitalLetterA != cyrillicCapitalLetterA {
print("These two characters are not equivalent")
}
// 打印“These two characters are not equivalent”

注意

在 Swift 中,字符串和字符并不区分地域(not locale-sensitive)。

前缀/后缀相等

通过调用字符串的 hasPrefix(_:)/hasSuffix(_:) 方法来检查字符串是否拥有特定前缀/后缀,两个方法均接收一个 String 类型的参数,并返回一个布尔值。

下面的例子以一个字符串数组表示莎士比亚话剧《罗密欧与朱丽叶》中前两场的场景位置:

1
2
3
4
5
6
7
8
9
10
11
12
13
let romeoAndJuliet = [
"Act 1 Scene 1: Verona, A public place",
"Act 1 Scene 2: Capulet's mansion",
"Act 1 Scene 3: A room in Capulet's mansion",
"Act 1 Scene 4: A street outside Capulet's mansion",
"Act 1 Scene 5: The Great Hall in Capulet's mansion",
"Act 2 Scene 1: Outside Capulet's mansion",
"Act 2 Scene 2: Capulet's orchard",
"Act 2 Scene 3: Outside Friar Lawrence's cell",
"Act 2 Scene 4: A street in Verona",
"Act 2 Scene 5: Capulet's mansion",
"Act 2 Scene 6: Friar Lawrence's cell"
]

你可以调用 hasPrefix(_:) 方法来计算话剧中第一幕的场景数:

1
2
3
4
5
6
7
8
var act1SceneCount = 0
for scene in romeoAndJuliet {
if scene.hasPrefix("Act 1 ") {
act1SceneCount += 1
}
}
print("There are \(act1SceneCount) scenes in Act 1")
// 打印输出“There are 5 scenes in Act 1”

相似地,你可以用 hasSuffix(_:) 方法来计算发生在不同地方的场景数:

1
2
3
4
5
6
7
8
9
10
11
var mansionCount = 0
var cellCount = 0
for scene in romeoAndJuliet {
if scene.hasSuffix("Capulet's mansion") {
mansionCount += 1
} else if scene.hasSuffix("Friar Lawrence's cell") {
cellCount += 1
}
}
print("\(mansionCount) mansion scenes; \(cellCount) cell scenes")
// 打印输出“6 mansion scenes; 2 cell scenes”

注意

hasPrefix(_:)hasSuffix(_:) 方法都是在每个字符串中逐字符比较其可扩展的字符群集是否标准相等,详细描述在 字符串/字符相等

字符串的 Unicode 表示形式

当一个 Unicode 字符串被写进文本文件或者其他储存时,字符串中的 Unicode 标量会用 Unicode 定义的几种 编码格式(encoding forms)编码。每一个字符串中的小块编码都被称 代码单元(code units)。这些包括 UTF-8 编码格式(编码字符串为 8 位的代码单元), UTF-16 编码格式(编码字符串位 16 位的代码单元),以及 UTF-32 编码格式(编码字符串32位的代码单元)。

Swift 提供了几种不同的方式来访问字符串的 Unicode 表示形式。你可以利用 for-in 来对字符串进行遍历,从而以 Unicode 可扩展的字符群集的方式访问每一个 Character 值。该过程在 使用字符 中进行了描述。

另外,能够以其他三种 Unicode 兼容的方式访问字符串的值:

  • UTF-8 代码单元集合(利用字符串的 utf8 属性进行访问)
  • UTF-16 代码单元集合(利用字符串的 utf16 属性进行访问)
  • 21 位的 Unicode 标量值集合,也就是字符串的 UTF-32 编码格式(利用字符串的 unicodeScalars 属性进行访问)

下面由 D,o,g,(DOUBLE EXCLAMATION MARK, Unicode 标量 U+203C)和 🐶(DOG FACE,Unicode 标量为 U+1F436)组成的字符串中的每一个字符代表着一种不同的表示:

1
let dogString = "Dog‼🐶"

UTF-8 表示

你可以通过遍历 Stringutf8 属性来访问它的 UTF-8 表示。其为 String.UTF8View 类型的属性,UTF8View 是无符号 8 位(UInt8)值的集合,每一个 UInt8 值都是一个字符的 UTF-8 表示:

CharacterD
U+0044
o
U+006F
g
U+0067

U+203C
🐶
U+1F436
UTF-8
Code Unit
68111103226128188240159144182
Position0123456789
1
2
3
4
5
for codeUnit in dogString.utf8 {
print("\(codeUnit) ", terminator: "")
}
print("")
// 68 111 103 226 128 188 240 159 144 182

上面的例子中,前三个 10 进制 codeUnit 值(68111103)代表了字符 Dog,它们的 UTF-8 表示与 ASCII 表示相同。接下来的三个 10 进制 codeUnit 值(226128188)是 DOUBLE EXCLAMATION MARK 的3字节 UTF-8 表示。最后的四个 codeUnit 值(240159144182)是 DOG FACE 的4字节 UTF-8 表示。

UTF-16 表示

你可以通过遍历 Stringutf16 属性来访问它的 UTF-16 表示。其为 String.UTF16View 类型的属性,UTF16View 是无符号16位(UInt16)值的集合,每一个 UInt16 都是一个字符的 UTF-16 表示:

CharacterD
U+0044
o
U+006F
g
U+0067

U+203C
🐶
U+1F436
UTF-16
Code Unit
6811110382525535756374
Position012345
1
2
3
4
5
for codeUnit in dogString.utf16 {
print("\(codeUnit) ", terminator: "")
}
print("")
// 68 111 103 8252 55357 56374

同样,前三个 codeUnit 值(68111103)代表了字符 Dog,它们的 UTF-16 代码单元和 UTF-8 完全相同(因为这些 Unicode 标量表示 ASCII 字符)。

第四个 codeUnit 值(8252)是一个等于十六进制 203C 的的十进制值。这个代表了 DOUBLE EXCLAMATION MARK 字符的 Unicode 标量值 U+203C。这个字符在 UTF-16 中可以用一个代码单元表示。

第五和第六个 codeUnit 值(5535756374)是 DOG FACE 字符的 UTF-16 表示。第一个值为 U+D83D(十进制值为 55357),第二个值为 U+DC36(十进制值为 56374)。

Unicode 标量表示

你可以通过遍历 String 值的 unicodeScalars 属性来访问它的 Unicode 标量表示。其为 UnicodeScalarView 类型的属性,UnicodeScalarViewUnicodeScalar 类型的值的集合。

每一个 UnicodeScalar 拥有一个 value 属性,可以返回对应的 21 位数值,用 UInt32 来表示:

CharacterD
U+0044
o
U+006F
g
U+0067

U+203C
🐶
U+1F436
Unicode Scalar
Code Unit
681111038252128054
Position01234
1
2
3
4
5
for scalar in dogString.unicodeScalars {
print("\(scalar.value) ", terminator: "")
}
print("")
// 68 111 103 8252 128054

前三个 UnicodeScalar 值(68111103)的 value 属性仍然代表字符 Dog

第四个 codeUnit 值(8252)仍然是一个等于十六进制 203C 的十进制值。这个代表了 DOUBLE EXCLAMATION MARK 字符的 Unicode 标量 U+203C

第五个 UnicodeScalar 值的 value 属性,128054,是一个十六进制 1F436 的十进制表示。其等同于 DOG FACE 的 Unicode 标量 U+1F436

作为查询它们的 value 属性的一种替代方法,每个 UnicodeScalar 值也可以用来构建一个新的 String 值,比如在字符串插值中使用:

1
2
3
4
5
6
7
8
for scalar in dogString.unicodeScalars {
print("\(scalar) ")
}
// D
// o
// g
// ‼
// 🐶

留言與分享

作者的圖片

Kein Chan

這是獨立全棧工程師Kein Chan的技術博客
分享一些技術教程,命令備忘(cheat-sheet)等


全棧工程師
資深技術顧問
數據科學家
Hit廣島觀光大使


Tokyo/Macau