欢迎来到 Swift 世界
关于 Swift
理解这门语言的高层目标。
Swift 是为手机、平板电脑、桌面电脑、服务器或其他运行代码的平台编写软件的绝佳方式。它是一种安全和快速的编程语言,结合了现代编程语言思维和多样化开源社区的智慧。
Swift 对新人程序员很友好,但也没有牺牲经验丰富的程序员所需要的功能和灵活性。它是一种具有工业质量的编程语言,与脚本语言一样富有表现力和令人愉快。编译器是为性能而优化的,语言是为开发而优化的,两者都不会相互影响。
Swift 通过现代编程模式消除了大量常见的编程错误:
-
变量总是在使用之前初始化。
-
检查数组索引是否有越界错误。
-
检查整数是否溢出。
-
可空类型确保显式地处理
nil
值。 -
内存是自动管理的。
-
错误处理机制允许从意外故障中进行受控恢复。
Swift 代码经过编译和优化以充分利用现代硬件。Swift 语法和标准库的设计基于“编写代码的明显方式也应具有最佳性能”的指导原则。安全和速度的结合使 Swift 成为从 “Hello, world!” 编程到整个操作系统编程的绝佳选择。
Swift 结合了其他流行编程语言的开发人员所熟悉的现代轻量级语法,并具有强大的功能,如类型推断和模式匹配,允许以清晰简洁的方式表达复杂的思想。因此 Swift 代码更易于读写与维护。
Swift 在继续发展,提供周到的新特性和强大的功能。Swift 的目标是宏伟的。我们迫不及待地想看看你用它创造出什么。
版本兼容性
了解在旧的语言模式中可用的功能。
本书介绍的是 Swift 5.9.2,这是 Xcode 15.1 中默认的 Swift 版本。你可以使用 Xcode 15.1 构建用 5.9.2、 Swift 4.2 或 Swift 4 编写的目标。
当你使用 Xcode 15.1 来构建 Swift 4 和 Swift 4.2 代码时,大多数 Swift 5.9.2 的功能都是可用的。以下更改仅对使用 5.9.2 或更高版本的代码有效:
-
返回不透明类型的函数需要 Swift 5.1 运行时支持。
-
try?
表达式不会为已经返回可空类型的表达式增加额外的可选性。 -
大整型的字面量初始化表达式被推断为正确的整数类型。例如,
UInt64(0xffff_ffff_ffff_ffff)
的计算结果是正确的而不是溢出的。
并发特性要求 5.9.2 或更高版本以及提供相应并发类型的 Swift 标准库版本。在苹果平台上,至少将部署目标设置为 iOS 13、macOS 10.15、tvOS 13 或 watchOS 6。
使用 5.9.2 编写的目标可以依赖于使用 Swift 4.2 或 Swift 4 编写的目标,反之亦然。这意味着,如果你有一个分成多个框架的大型项目,你可以将代码从 Swift 4 框架一次迁移到 5.9.2 框架。
Swift 指南
探索 Swift 的特性和语法。
按照传统,新语言的第一个程序应该是打印 “Hello, world!” 到屏幕上。在 Swift 中,这可以在一行代码中完成:
print("Hello, world!")
// Prints "Hello, world!"
如果你学习过其他语言,这段语法应该很熟悉——在 Swift 中,这行代码是一个完整的程序。你不需要为输出文本或处理字符串等功能导入单独的库。在全局作用域中编写的代码被用作程序的入口点,因此不需要 main()
函数。你也不需要在每个语句末尾都加上分号。
本教程通过展示如何完成各种编程任务为您提供足够的信息,以开始使用 Swift 编写代码。如果你有不明白的地方,不要担心——本书的其余部分会详细解释本指南中介绍的所有内容。
简单的值
使用 let
创建常量,使用 var
创建变量。编译时不需要知道常量的值,但必须给它赋值一次。这意味着你可以使用常量来命名一个只确定一次但会在很多地方使用的值。
var myVariable = 42
myVariable = 50
let myConstant = 42
常量或变量必须与要赋给它的值具有相同的类型。然而,你并不总是需要显式地编写类型。创建常量或变量时提供值可以让编译器推断其类型。在上面的例子中,编译器推断 myVariable
是一个整型,因为它的初始值是一个整型。
如果初始值没有提供足够的信息(或者没有初始值),可以在变量后面写上类型,用冒号分隔。
let implicitInteger = 70
let implicitDouble = 70.0
let explicitDouble: Double = 70
实验
创建一个显式类型为
Float
的常量,值为4
。let explicitFloat: Float = 4
值永远不会隐式转换为其他类型。如果需要将值转换为其他类型,请显式地创建所需类型的实例。
let label = "The width is "
let width = 94
let widthLabel = label + String(width)
实验
尝试删除最后一行的字符串转换。得到什么错误?
error: binary operator '+' cannot be applied to operands of type 'String' and 'Int'
在字符串中包含值还有一种更简单的方法:将值放在括号中,并在括号前加一个反斜杠 \
,例如:
let apples = 3
let oranges = 5
let appleSummary = "I have \(apples) apples."
let fruitSummary = "I have \(apples + oranges) pieces of fruit."
实验
使用
\()
在字符串中包含浮点数计算,并在问候语中包含某人的名字。let num1 = 2.5 let num2 = 3.5 let name = "Hatcher" print("2.5 + 3.5 = \(num1 + num2)") print("Hello, \(name)!")
使用三个双引号 """
输入多行字符串。Swift 会删除每一行开头的缩进,直到它与结束引号的缩进一致。例如:
let quotation = """
Even though there's whitespace to the left,
the actual lines aren't indented.
Except for this line.
Double quotes (") can appear without being escaped.
I still have \(apples + oranges) pieces of fruit.
"""
使用方括号 []
创建数组和字典,并通过方括号中的索引或键来访问其中的元素。最后一个元素后可以加逗号。
var fruits = ["strawberries", "limes", "tangerines"]
fruits[1] = "grapes"
var occupations = [
"Malcolm": "Captain",
"Kaylee": "Mechanic",
]
occupations["Jayne"] = "Public Relations"
数组会随着元素的增加而自动增长。
fruits.append("blueberries")
print(fruits)
// Prints "["strawberries", "grapes", "tangerines", "blueberries"]"
还可以使用方括号来实现空数组或空字典。对于数组,使用 write[]
;对于字典,使用 write[:]
。
fruits = []
occupations = [:]
如果要将空数组或字典赋值给新变量,或者其他没有任何类型信息的地方,就需要指定类型。
let emptyArray: [String] = []
let emptyDictionary: [String: Float] = [:]
控制流
使用 if
和 switch
创建条件语句,使用 for-in
、 while
和 repeat-while
创建循环。条件变量或循环变量的括号是可选的。语句体用花括号包围。
let individualScores = [75, 43, 103, 87, 12]
var teamScore = 0
for score in individualScores {
if score > 50 {
teamScore += 3
} else {
teamScore += 1
}
}
print(teamScore)
// Prints "11"
在 if
语句中,条件语句必须是布尔表达式——这意味着代码如 if score { … }
是错误的,而不是与 0
的隐式比较。
你可以在赋值语句的等号 =
后面或 return
后面写 if
或 switch
,以根据条件选择一个值。
let scoreDecoration = if teamScore > 10 {
"🎉"
} else {
""
}
print("Score:", teamScore, scoreDecoration)
// Prints "Score: 11 🎉"
可以同时使用 if
和 let
来处理可能缺失的值。这些值都是可空类型。可空类型的值要么包含值,要么包含 nil
,表示缺少一个值。在值的类型后面加一个问号 ?
,将值标记为可空类型。
var optionalString: String? = "Hello"
print(optionalString == nil)
// Prints "false"
var optionalName: String? = "John Appleseed"
var greeting = "Hello!"
if let name = optionalName {
greeting = "Hello, \(name)"
}
实验
将
optionalName
改为nil
。你收到了什么问候语?添加一个else
子句,在optionalName
为nil
时设置不同的问候语。
var optionalName: String? = nil var greeting = "Hello!" if let name = optionalName { greeting = "Hello, \(name)" } print(greeting) // 输出 Hello!
2. ```swift var optionalName: String? = nil var greeting = "Hello!" if let name = optionalName { greeting = "Hello, \(name)" } else { greeting = "Hola!" } print(greeting) // 输出 Hola!
如果可空类型值为 nil
,条件语句为 false
,跳过花括号中的代码。否则,可空类型值会解包并在 let
之后赋值给常量,这样解包后的值就可以在代码块中使用了。
另一种处理可空类型值的方法是使用 ??
操作符。如果可空类型的值不存在,则使用默认值。
let nickname: String? = nil
let fullName: String = "John Appleseed"
let informalGreeting = "Hi \(nickname ?? fullName)"
可以使用更短的拼写来解包值,对解包值使用相同的名称。
if let nickname {
print("Hey, \(nickname)")
}
// Doesn't print anything, because nickname is nil.
switch
语句支持任何类型的数据和各种各样的比较操作——不仅限于整数和相等测试。
let vegetable = "red pepper"
switch vegetable {
case "celery":
print("Add some raisins and make ants on a log.")
case "cucumber", "watercress":
print("That would make a good tea sandwich.")
case let x where x.hasSuffix("pepper"):
print("Is it a spicy \(x)?")
default:
print("Everything tastes good in soup.")
}
// Prints "Is it a spicy red pepper?"
实验
尝试删除默认分支。得到什么错误?
error: switch must be exhaustive
注意如何在模式中使用 let
,将匹配模式的值赋给常量。
在执行匹配的 switch 分支内的代码后,程序退出 switch 语句。不会继续执行下一分支,因此不需要在每个分支的代码末尾显式地跳出 switch。
通过为键值对提供名称对,可以使用 for-in
遍历字典中的元素。字典是无序集合,因此键和值的迭代顺序是任意的。
let interestingNumbers = [
"Prime": [2, 3, 5, 7, 11, 13],
"Fibonacci": [1, 1, 2, 3, 5, 8],
"Square": [1, 4, 9, 16, 25],
]
var largest = 0
for (_, numbers) in interestingNumbers {
for number in numbers {
if number > largest {
largest = number
}
}
}
print(largest)
// Prints "25"
实验
将
_
替换为变量名,并记录哪种数最大。let interestingNumbers = [ "Prime": [2, 3, 5, 7, 11, 13], "Fibonacci": [1, 1, 2, 3, 5, 8], "Square": [1, 4, 9, 16, 25], ] var answer = "" var largest = 0 for (sequence, numbers) in interestingNumbers { for number in numbers { if number > largest { largest = number answer = sequence } } } print(answer) // Prints "Square"
使用 while
重复执行代码块,直到条件发生变化。循环的条件可以放在循环的末尾,确保循环至少运行一次。
var n = 2
while n < 100 {
n *= 2
}
print(n)
// Prints "128"
var m = 2
repeat {
m *= 2
} while m < 100
print(m)
// Prints "128"
实验
将条件由
m < 100
修改为m < 0
来查看while
和repeat-while
在循环条件为真时的行为有何不同。var n = 2 while n < 0 { n *= 2 } print(n) // Prints "2" var m = 2 repeat { m *= 2 } while m < 0 print(m) // Prints "4"
你可以使用 ..<
在循环中创建一个索引范围。
var total = 0
for i in 0..<4 {
total += i
}
print(total)
// Prints "6"
使用 ..<
创建忽略上限的范围,并使用 ...
创建包含上下界的范围。
函数和闭包
使用 func
声明函数。调用函数时,在函数名后面加上一对括号,括号内是参数列表。使用 ->
将参数名和类型与函数返回值类型分开。
func greet(person: String, day: String) -> String {
return "Hello \(person), today is \(day)."
}
greet(person: "Bob", day: "Tuesday")
实验
删除
day
参数。添加一个参数,在问候语中包含今天的特价午餐。func greet(person: String, lunchSpecial: String) -> String { return "Hello \(person), today's lunch special is \(lunchSpecial)." } print(greet(person: "Bob", lunchSpecial: "Tortellini"))
默认情况下,函数使用它们的形参名作为实参的标签。在参数名之前写一个自定义参数标签,或者写 _
不使用参数标签。
func greet(_ person: String, on day: String) -> String {
return "Hello \(person), today is \(day)."
}
greet("John", on: "Wednesday")
使用元组创建复合值——例如,从一个函数返回多个值。元组中的元素可以通过名称或数字引用。
func calculateStatistics(scores: [Int]) -> (min: Int, max: Int, sum: Int) {
var min = scores[0]
var max = scores[0]
var sum = 0
for score in scores {
if score > max {
max = score
} else if score < min {
min = score
}
sum += score
}
return (min, max, sum)
}
let statistics = calculateStatistics(scores: [5, 3, 100, 3, 9])
print(statistics.sum)
// Prints "120"
print(statistics.2)
// Prints "120"
函数可以嵌套。嵌套函数可以访问在外部函数中声明的变量。你可以使用嵌套函数来组织冗长或复杂的函数中的代码。
func returnFifteen() -> Int {
var y = 10
func add() {
y += 5
}
add()
return y
}
returnFifteen()
函数是一级类型。这意味着一个函数可以将另一个函数作为返回值。
func makeIncrementer() -> ((Int) -> Int) {
func addOne(number: Int) -> Int {
return 1 + number
}
return addOne
}
var increment = makeIncrementer()
increment(7)
一个函数可以接受另一个函数作为它的参数。
func hasAnyMatches(list: [Int], condition: (Int) -> Bool) -> Bool {
for item in list {
if condition(item) {
return true
}
}
return false
}
func lessThanTen(number: Int) -> Bool {
return number < 10
}
var numbers = [20, 19, 7, 12]
hasAnyMatches(list: numbers, condition: lessThanTen)
函数实际上是闭包的一种特殊情况:可以稍后调用的代码块。闭包中的代码可以访问在创建闭包的作用域中可用的变量和函数等,即使闭包在执行时处于不同的作用域中——你已经看到了一个嵌套函数的例子。你可以用括号 ({})
将代码包裹起来,这样就可以编写一个匿名闭包。使用 in
将参数和返回值类型与函数体分开。
numbers.map({ (number: Int) -> Int in
let result = 3 * number
return result
})
实验
重写这个闭包,让它对所有奇数都返回零。
var numbers = [20, 19, 7, 12] numbers = numbers.map({ (number: Int) -> Int in return if number % 2 == 0 { number } else { 0 } }) print(numbers) // Prints "[20, 0, 0, 12]"
有几种方法可以让闭包的代码更简洁。如果闭包的类型是已知的,例如委托的回调,则可以省略其参数类型或返回值类型,或两者都省略。单语句闭包隐式地返回其唯一语句的值。
let mappedNumbers = numbers.map({ number in 3 * number })
print(mappedNumbers)
// Prints "[60, 57, 21, 36]"
你可以通过数字而不是名称来引用参数——这种方法在非常短的闭包中特别有用。作为函数最后一个参数的闭包可以紧跟在圆括号后面。当闭包是函数的唯一参数时,可以完全省略括号。
let sortedNumbers = numbers.sorted { $0 > $1 }
print(sortedNumbers)
// Prints "[20, 19, 12, 7]"
对象和类
使用 class
后跟类名创建类。在类中声明属性与声明常量或变量的方式相同,不同之处在于它是在类的上下文中声明的。同样,方法和函数声明的写法也是一样的。
class Shape {
var numberOfSides = 0
func simpleDescription() -> String {
return "A shape with \(numberOfSides) sides."
}
}
实验
使用
let
添加一个常量属性,再添加一个接受参数的方法。class Shape { var numberOfSides = 0 let PI = 3.14 func simpleDescription() -> String { return "A shape with \(numberOfSides) sides." } func area(radius: Double) -> Double { return PI * radius * radius } }
类名后加一对括号可以创建类的实例。使用点语法访问实例的属性和方法。
var shape = Shape()
shape.numberOfSides = 7
var shapeDescription = shape.simpleDescription()
这个版本的 Shape
类缺少了一些重要的东西:创建实例时用于设置类的初始化方法。使用 init
创建。
class NamedShape {
var numberOfSides: Int = 0
var name: String
init(name: String) {
self.name = name
}
func simpleDescription() -> String {
return "A shape with \(numberOfSides) sides."
}
}
注意 self
是如何用来区分 name
属性和初始化方法的 name
参数的。初始化方法的参数在创建类的实例时像函数调用一样传递。每个属性都需要赋值——无论是在声明中(如 numberOfSides
)还是在初始化方法中(如 name
)。
如果需要在释放对象之前执行一些清理操作,可以使用 deinit
来创建一个反初始化方法。
子类在类名后面加上超类名,用冒号分隔。没有必须继承的标准根类,所以你可以根据需要包含或省略超类。
覆盖超类实现的子类的方法被标记为 override
——如果不小心覆盖了方法而没有使用 override
,编译器会将其检测为错误。编译器还会检测带有 override
的方法,这些方法实际上不会覆盖超类中的任何方法。
class Square: NamedShape {
var sideLength: Double
init(sideLength: Double, name: String) {
self.sideLength = sideLength
super.init(name: name)
numberOfSides = 4
}
func area() -> Double {
return sideLength * sideLength
}
override func simpleDescription() -> String {
return "A square with sides of length \(sideLength)."
}
}
let test = Square(sideLength: 5.2, name: "my test square")
test.area()
test.simpleDescription()
实验
创建
NamedShape
的另一个子类Circle
,让它接受半径和名字作为初始化方法的参数。在Circle
类上实现area()
方法和simpleDescription()
方法。class NamedShape { var numberOfSides: Int = 0 var name: String init(name: String) { self.name = name } func simpleDescription() -> String { return "A shape with \(numberOfSides) sides." } } class Circle: NamedShape { let PI = 3.14 var radius: Double init(radius: Double, name: String) { self.radius = radius super.init(name: name) numberOfSides = 1 } func area() -> Double { return PI * radius * radius } override func simpleDescription() -> String { return "A circle with radius of lenth \(radius)" } } let test = Circle(radius: 5, name: "my test circle") print(test.area()) print(test.simpleDescription())
除了存储的简单属性之外,属性还可以有 getter
和 setter
。
class EquilateralTriangle: NamedShape {
var sideLength: Double = 0.0
init(sideLength: Double, name: String) {
self.sideLength = sideLength
super.init(name: name)
numberOfSides = 3
}
var perimeter: Double {
get {
return 3.0 * sideLength
}
set {
sideLength = newValue / 3.0
}
}
override func simpleDescription() -> String {
return "An equilateral triangle with sides of length \(sideLength)."
}
}
var triangle = EquilateralTriangle(sideLength: 3.1, name: "a triangle")
print(triangle.perimeter)
// Prints "9.3"
triangle.perimeter = 9.9
print(triangle.sideLength)
// Prints "3.3000000000000003"
在 perimeter
的 setter 中,新值的隐式名称是 newValue
。你可以在 set
后面的括号中提供一个明确的名称。
注意,EquilateralTriangle
类的初始化方法有三个不同的步骤。
-
设置子类声明的属性的值。
-
调用超类的初始化方法。
-
修改超类定义的属性的值。任何使用方法、getter 或 setter 的额外设置工作也可以在这里完成。
如果你不需要计算属性,但仍然需要提供设置新值之前和之后运行的代码,请使用 willSet
和 didSet
。只要值在初始化方法之外发生变化,你提供的代码就会运行。例如,下面的类确保其三角形的边长始终与其正方形的边长相等。
class TriangleAndSquare {
var triangle: EquilateralTriangle {
willSet {
square.sideLength = newValue.sideLength
}
}
var square: Square {
willSet {
triangle.sideLength = newValue.sideLength
}
}
init(size: Double, name: String) {
square = Square(sideLength: size, name: name)
triangle = EquilateralTriangle(sideLength: size, name: name)
}
}
var triangleAndSquare = TriangleAndSquare(size: 10, name: "another test shape")
print(triangleAndSquare.square.sideLength)
// Prints "10.0"
print(triangleAndSquare.triangle.sideLength)
// Prints "10.0"
triangleAndSquare.square = Square(sideLength: 50, name: "larger square")
print(triangleAndSquare.triangle.sideLength)
// Prints "50.0"
当涉及可空类型值时,你可以在方法、属性和下标等操作之前使用 ?
。如果 ?
前的值是 nil
,?
后的部分将被忽略,而且整个表达式的值为 nil
。否则,可空类型的值将被解包,而 ?
后的部分作用于解包的值。在这两种情况下,整个表达式的值都是可空类型。
let optionalSquare: Square? = Square(sideLength: 2.5, name: "optional square")
let sideLength = optionalSquare?.sideLength
枚举和结构
使用 enum
创建枚举。与类和所有其他命名类型一样,枚举也可以关联方法。
enum Rank: Int {
case ace = 1
case two, three, four, five, six, seven, eight, nine, ten
case jack, queen, king
func simpleDescription() -> String {
switch self {
case .ace:
return "ace"
case .jack:
return "jack"
case .queen:
return "queen"
case .king:
return "king"
default:
return String(self.rawValue)
}
}
}
let ace = Rank.ace
let aceRawValue = ace.rawValue
实验
编写一个函数,通过比较两个
Rank
的原始值来比较它们。func compare(x:Rank, y:Rank) { if x.rawValue < y.rawValue { print("\(x.simpleDescription())<\(y.simpleDescription())") } else if x.rawValue > y.rawValue { print("\(x.simpleDescription())>\(y.simpleDescription())") } else { print("\(x.simpleDescription())=\(y.simpleDescription())") } }
默认情况下,Swift 从零开始赋值原始值,每次加一,但你可以通过显式指定值来改变这种行为。在上面的例子中,Ace
显式地给出了原始值 1
,其余的原始值按顺序赋值。枚举的原始类型也可以是字符串或浮点数。使用 rawValue
属性可以访问枚举实例的原始值。
使用初始化方法 init?(rawValue:)
从原始值创建枚举实例。它返回与原始值匹配的枚举分支,如果没有匹配的 Rank
,则返回 nil
。
if let convertedRank = Rank(rawValue: 3) {
let threeDescription = convertedRank.simpleDescription()
}
枚举的 case
值是实际值,而不是原始值的一种写法。事实上,原始值没有意义的情况下,你不必提供原始值。
enum Suit {
case spades, hearts, diamonds, clubs
func simpleDescription() -> String {
switch self {
case .spades:
return "spades"
case .hearts:
return "hearts"
case .diamonds:
return "diamonds"
case .clubs:
return "clubs"
}
}
}
let hearts = Suit.hearts
let heartsDescription = hearts.simpleDescription()
实验
为
Suit
添加一个color()
方法,让它对黑桃和梅花返回 "black ",对红心和方块返回 "red "。func color() -> String { switch self { case .spades: return "black" case .hearts: return "red" case .diamonds: return "red" case .clubs: return "black" } }
注意上面使用 hearts
枚举项的两种方式:给常量 hearts
赋值时,枚举项 Suit.hearts
是用全名表示的,因为该常量没有显式指定类型。在 switch 语句中,枚举项用缩写形式 .hearts
表示,因为 self
的值已知是 Suit 类型。只要已知值的类型,就可以使用缩写形式。
如果枚举有原始值,这些值将作为声明的一部分确定,这意味着特定枚举项的每个实例始终具有相同的原始值。枚举项的另一种选择是将值与项关联——这些值在创建实例时确定,对于枚举项的不同实例,它们可能是不同的。你可以把关联的值想象成枚举项的存储属性。例如,考虑从服务器请求日出和日落时间的情况。服务器要么返回请求的信息,要么返回错误描述。
enum ServerResponse {
case result(String, String)
case failure(String)
}
let success = ServerResponse.result("6:00 am", "8:09 pm")
let failure = ServerResponse.failure("Out of cheese.")
switch success {
case let .result(sunrise, sunset):
print("Sunrise is at \(sunrise) and sunset is at \(sunset).")
case let .failure(message):
print("Failure... \(message)")
}
// Prints "Sunrise is at 6:00 am and sunset is at 8:09 pm."
实验
在
ServerResponse
和 switch 中添加第三个分支。enum ServerResponse { case result(String, String) case failure(String) case zone(String) } let zone = ServerResponse.zone("Shanghai") switch zone { case let .result(sunrise, sunset): print("Sunrise is at \(sunrise) and sunset is at \(sunset).") case let .failure(message): print("Failure... \(message)") case let .zone(name): print("Zone is \(name).") } // Prints "Zone is Shanghai."
注意日出和日落时间是如何作为值与 switch 项匹配的一部分从 ServerResponse
值中提取出的。
使用 struct
创建一个结构体。结构体支持很多与类相同的行为,包括方法和初始化方法。结构体和类之间最重要的区别之一是,在代码中传递结构体时总是复制结构体,而类通过引用传递。
struct Card {
var rank: Rank
var suit: Suit
func simpleDescription() -> String {
return "The \(rank.simpleDescription()) of \(suit.simpleDescription())"
}
}
let threeOfSpades = Card(rank: .three, suit: .spades)
let threeOfSpadesDescription = threeOfSpades.simpleDescription()
实验
编写一个函数,它返回一个数组,其中包含一副扑克牌,由 rank 和 suit 的各种组合组成。
func fullDeckCard() -> [Card] { var cards:[Card] = [] for rankRawValue in 1...13 { for suitRawValue in 0...3 { let rank = Rank(rawValue: rankRawValue)! let suit = Suit(rawValue: suitRawValue)! let card = Card(rank: rank, suit: suit) cards.append(card) } } return cards }
并发
使用 async
标记一个异步运行的函数。
func fetchUserID(from server: String) async -> Int {
if server == "primary" {
return 97
}
return 501
}
你可以在异步函数的调用前面写 await
来标记它。
func fetchUsername(from server: String) async -> String {
let userID = await fetchUserID(from: server)
if userID == 501 {
return "John Appleseed"
}
return "Guest"
}
使用 async let
调用异步函数,让它与其他异步代码并行运行。当你使用它返回的值时,写 await
。
func connectUser(to server: String) async {
async let userID = fetchUserID(from: server)
async let username = fetchUsername(from: server)
let greeting = await "Hello \(username), user ID \(userID)"
print(greeting)
}
使用 Task
从同步代码中调用异步函数,而无需等待它们返回。
Task {
await connectUser(to: "primary")
}
// Prints "Hello Guest, user ID 97"
使用任务组来组织并发代码。
let userIDs = await withTaskGroup(of: Int.self) { group in
for server in ["primary", "secondary", "development"] {
group.addTask {
return await fetchUserID(from: server)
}
}
var results: [Int] = []
for await result in group {
results.append(result)
}
return results
}
角色类似于类,不同之处在于角色可以确保不同的异步函数可以同时安全地与同一个角色的实例交互。
actor ServerConnection {
var server: String = "primary"
private var activeUsers: [Int] = []
func connect() async -> Int {
let userID = await fetchUserID(from: server)
// ... communicate with server ...
activeUsers.append(userID)
return userID
}
}
当调用角色的方法或访问其属性时,将代码标记为 await
表示它必须等待角色上已运行的其他代码完成。
let server = ServerConnection()
let userID = await server.connect()
协议和扩展
使用 protocol
声明一个协议。
protocol ExampleProtocol {
var simpleDescription: String { get }
mutating func adjust()
}
类、枚举和结构体都可以采用协议。
class SimpleClass: ExampleProtocol {
var simpleDescription: String = "A very simple class."
var anotherProperty: Int = 69105
func adjust() {
simpleDescription += " Now 100% adjusted."
}
}
var a = SimpleClass()
a.adjust()
let aDescription = a.simpleDescription
struct SimpleStructure: ExampleProtocol {
var simpleDescription: String = "A simple structure"
mutating func adjust() {
simpleDescription += " (adjusted)"
}
}
var b = SimpleStructure()
b.adjust()
let bDescription = b.simpleDescription
实验
向
ExampleProtocol
添加另一个需求。需要对SimpleClass
和SimpleStructure
做哪些修改,才能让它们仍然符合协议?需要在
SimpleClass
和SimpleStructure
添加该需求的实现。
注意,在 SimpleStructure
的声明中使用了 mutating
关键字来标记修改结构的方法。SimpleClass
的声明不需要将任何方法标记为 mutating
,因为类中的方法总是可以修改类。
使用 extension
为现有类型添加功能,如新方法和计算属性。你可以使用扩展为在别处声明的类型添加协议一致性,甚至为从库或框架导入的类型添加协议一致性。
extension Int: ExampleProtocol {
var simpleDescription: String {
return "The number \(self)"
}
mutating func adjust() {
self += 42
}
}
print(7.simpleDescription)
// Prints "The number 7"
实验
为
Double
类型编写一个扩展以添加absoluteValue
属性。extension Double { var absoluteValue: Double { return if self >= 0 { self } else { -1 * self } } } print((-6.1).absoluteValue) // Prints "6.1"
你可以像使用其他命名类型一样使用协议名——例如,创建一组具有不同类型但都遵循同一个协议的对象。使用装箱协议类型的值时,协议定义之外的方法不可用。
let protocolValue: any ExampleProtocol = a
print(protocolValue.simpleDescription)
// Prints "A very simple class. Now 100% adjusted."
// print(protocolValue.anotherProperty) // Uncomment to see the error
即使变量 protocolValue
的运行时类型是 SimpleClass
,编译器也会将其视为 ExampleProtocol
的给定类型。这意味着除了符合协议之外,你不能意外访问类实现的方法或属性。
错误处理
可以使用任何采用了 Error
协议的类型来表示错误。
enum PrinterError: Error {
case outOfPaper
case noToner
case onFire
}
使用 throw
抛出错误,使用 throws
标记可能抛出错误的函数。如果在函数中抛出错误,函数会立即返回,调用函数的代码会处理这个错误。
func send(job: Int, toPrinter printerName: String) throws -> String {
if printerName == "Never Has Toner" {
throw PrinterError.noToner
}
return "Job sent"
}
有几种方法可以处理错误。一种方法是使用 do-catch
。在 do
代码块中,通过在代码前写 try
来标记可能抛出错误的代码。在 catch
块中,错误自动被命名为 error
,除非你给它起了其他的名字。
do {
let printerResponse = try send(job: 1040, toPrinter: "Bi Sheng")
print(printerResponse)
} catch {
print(error)
}
// Prints "Job sent"
实验
将打印机名称改为
“Never Has Toner”
,这样send(job: topprinter:)
函数就会抛出一个错误。
你可以提供多个 catch
块来处理特定的错误。在 catch
后面编写模式,就像 switch 语句中在 case
后面编写模式一样。
do {
let printerResponse = try send(job: 1440, toPrinter: "Gutenberg")
print(printerResponse)
} catch PrinterError.onFire {
print("I'll just put this over here, with the rest of the fire.")
} catch let printerError as PrinterError {
print("Printer error: \(printerError).")
} catch {
print(error)
}
// Prints "Job sent"
实验
在
do
块中添加抛出错误的代码。需要抛出什么错误才能让第一个catch
块处理?第二个和第三个呢?
PrinterError.onFire
PrinterError
中除了onFile
的其他- 除了
PrinterError
的其他
另一种处理错误的方法是使用 try?
将结果转换为可空类型。如果该函数抛出错误,则丢弃该错误,结果为 nil
。否则,结果是一个可空对象,包含函数返回的值。
let printerSuccess = try? send(job: 1884, toPrinter: "Mergenthaler")
let printerFailure = try? send(job: 1885, toPrinter: "Never Has Toner")
使用 defer
编写在函数中所有其他代码之后、函数返回之前执行的代码块。不管函数是否抛出错误,代码都会执行。你可以使用 defer
将设置和清理代码放在一起编写,即使它们需要在不同的时间执行。
var fridgeIsOpen = false
let fridgeContent = ["milk", "eggs", "leftovers"]
func fridgeContains(_ food: String) -> Bool {
fridgeIsOpen = true
defer {
fridgeIsOpen = false
}
let result = fridgeContent.contains(food)
return result
}
if fridgeContains("banana") {
print("Found a banana")
}
print(fridgeIsOpen)
// Prints "false"
范型
在尖括号中写一个名称,以创建一个泛型函数或类。
func makeArray<Item>(repeating item: Item, numberOfTimes: Int) -> [Item] {
var result: [Item] = []
for _ in 0..<numberOfTimes {
result.append(item)
}
return result
}
makeArray(repeating: "knock", numberOfTimes: 4)
可以创建泛型形式的函数和方法,也可以创建类、枚举和结构。
// Reimplement the Swift standard library's optional type
enum OptionalValue<Wrapped> {
case none
case some(Wrapped)
}
var possibleInteger: OptionalValue<Int> = .none
possibleInteger = .some(100)
在函数体之前使用 where
指定一系列需求——例如,要求类型实现一个协议,要求两个类型相同,或者要求一个类有一个特定的超类。
func anyCommonElements<T: Sequence, U: Sequence>(_ lhs: T, _ rhs: U) -> Bool
where T.Element: Equatable, T.Element == U.Element
{
for lhsItem in lhs {
for rhsItem in rhs {
if lhsItem == rhsItem {
return true
}
}
}
return false
}
anyCommonElements([1, 2, 3], [3])
实验
修改
anyCommonElements(_:_:)
函数,让它返回一个包含任意两个序列共有元素的数组。func anyCommonElements<T: Sequence, U: Sequence>(_ lhs: T, _ rhs: U) -> [T.Element] where T.Element: Equatable, T.Element == U.Element { var elements:[T.Element] = [] for lhsItem in lhs { for rhsItem in rhs { if lhsItem == rhsItem { elements.append(lhsItem) break } } } return elements } print(anyCommonElements([1, 2, 3], [3])) // Prints "[3]"
写作 <T: Equatable>
与写作 <T> ... where T: Equatable
的实现相同。