这个坑是新开的一个有关 swift 6 的学习笔记系列,因为我近期需要在 Apple Vision OS 上进行具身遥操作应用的开发,需要用到 swift 和 swiftUI 这两个开发语言,在此之前我完全没有接触过 swift,也没有在 iOS 平台上进行过开发,因此这个系列博客是我个人的一个纯零基础笔记,和其他的几个系列博客相比,可能中间存在一些错误。
这篇博客的主要内容是官方教程中的《A Swift Tour》 部分。
该系列博客的前期主要是在 MacOS + VSCode 上学习,后面需要构建工程后就会转移到 MacOS + XCode,所以先确保自己的环境中已经安装了 Swift 6.1.2
版本,同时 VSCode 中安装了 Swift 插件。
【Note】:由于 Apple 会删除旧版本的官方教程,因此这个博客是基于当前最新的 Swift 版本推进的,5.X 和 6.X 在很多地方都存在差异,如果你明确需要使用 5.X 的版本建议买实体书。
可以使用下面的命令将 Swift 更新到最新版本:
$ curl -O https://download.swift.org/swiftly/darwin/swiftly.pkg && \
installer -pkg swiftly.pkg -target CurrentUserHomeDirectory && \
~/.swiftly/bin/swiftly init --quiet-shell-followup && \
. ${SWIFTLY_HOME_DIR:-~/.swiftly}/env.sh && \
hash -r
然后确定当前使用的 swift 版本:
$ swift --version
Apple Swift version 6.1.2 (swift-6.1.2-RELEASE)
Target: arm64-apple-macosx15.0
1. Hello World
所有编程语的第一个 case 都是 “Hello World”:
print("Hello World")
在 Swift 中,这行代码就是一个完整的程序,无需导入单独的库来实现输出文本或处理字符串等功能。在全局作用域中编写的代码将用作程序的入口点,因此不需要 main()
函数,而每个语句末尾添加分号也是可有可无的。
这里比较困惑的是官方教程中没有说怎么运行,估计 Apple 打算让所有人用 XCode 开发,点击 “Play” 按钮运行,但因为我这用的是 VSCode 编辑器,所以还可以通过终端来执行这个文件,将这个文件保存为 deom.swift
然后在终端执行下面的命令:
$ swift demo.swift
Hello World
后面的所有代码都用上面命令行方式运行。
2. 简单变量
2.1 常量与变量
使用 let
来创建常量,使用 var
来创建变量。常量的值在编译时无需确定,但必须赋值一次,并且在全局上都依旧可用。
var myVariable = 42 // 变量
myVariable = 50
let myConstant = 43 // 常量
myConstant = 40 // 报错:常量不可修改
常量或变量必须与要赋给它的值具有相同的类型,但不必总是明确指定类型,创建时提供值可以让编译器推断其类型,在上面的示例中,编译器推断 myVariable
是一个整数。
如果初始值没有提供足够的信息(或者没有初始值)需要在变量后写上类型,并用冒号分隔,如下面的代码
let implicitInteger = 70 // 自动推导为 Int 类型
let implicitDouble = 70.0 // 自动推导为 Double 类型
let explicitDouble: Double = 70 // 显式声明为 Double 类型
let var1:Int, var2:Int; // 定义好变量但暂时不使用
变量的值永远不会被隐式转换为其他类型,如果需要将值转换为其他类型,需要显式创建所需类型的实例。
let label = "The width is "
let width = 94
let widthLabel = label + String(width) // 需要显示进行变量转换
2.2 字符串中的运算
使用 ()
可以在字符串中包含涉及计算的部分,有点类似 python 语法中的字符串运算,但更直接:
let apples = 10 # 常量
var oranges = 5 # 变量
let quotation = """
I still have \(apples + oranges) pieces of fruit.
"""
print(quotation) // I still have 15 pieces of fruit.
2.3 数组与字典
使用方括号 []
创建数组或字典,并通过在方括号中写入索引或键来访问其元素。最后一个元素后可以使用逗号。
数组:
var fruits = ["strawberries", "limes", "tangerines"]
fruits[1] = "grapes" // 使用下标修改元素
fruits.append("blueberries") // 使用append追加元素
字典:
var occupations = [
"Malcolm": "Captain",
"Kaylee": "Mechanic",
]
occupations["Jayne"] = "Public Relations" // 向字典中追加元素
print(occupations) // ["Malcolm": "Captain", "Jayne": "Public Relations", "Kaylee": "Mechanic"]
清空元素:
fruits = []
occupations = [:]
如果先要给 空数组 或 空字典 分配新的值需要明确数据类型:
let new_fruits:[String] = []
let new_occupations:[String:String] = [:]
3. 流程控制
使用 if
和 switch
语句创建条件语句,使用 for-in
、while
和 repeat-while
语句创建循环。条件或循环变量周围的括号是可选的,但语句主体周围的括号是必需的。
3.1 for-in 循环
这个 for-in
循环很像 C++ 的形式,需要用 {}
限定其作用域。
let individualScores = [75, 43, 103, 87, 12]
var teamScore = 0
for score in individualScores{
if score > 50{
teamScore += 3
}else{
teamScore += 1
}
}
print(teamScore)
在 6.X 版本中支持直接在代码中使用 emoji 同样也支持变量的条件定义,但在 5.X 版本中不支持,下面的代码在实用条件定义的同时也用到了 emoji:
let teamScore = 15
let scoreDecortation = if teamScore > 10 {
"🎉"
}else{
""
}
print("Score: ", teamScore, scoreDecortation)
// Score: 15 🎉
3.2 if let 条件判断
下面这部分的官方 demo 在说明上有些不足,对初学者不太友好,没有很好的解释什么是 可选类型。Swift 明确区分了可选类型和明确类型,下面以字符串类型举例。
如果你学过 C++ 那么会发现所有对象都是一个明确类型,你不能给一个 std::string
对象赋值为 null
,但有时候又需要表示函数并没有获得字符串,如果直接用空字符串 ""
表示则会丢失一种情况:对方真的返回了一个空字符串,那么最常见的方法就是再给函数传入一个变量或者返回一个 pair:
std::string RequestFunction(std::string request, bool& is_responsed);
std::pair<bool, std::string> RequestFunction(std::string request);
而在 Swift 中就不用这么麻烦,你可以在定义变量的时候带上一个 ?
表示这个变量可以是一个空对象 nil
即 C++ 中的 null
,但不限于字符串,任何数据类型都可以通过这个方式明确其当前没有值:
var optionalString: String? = "Hello" // 定义一个可选对象,类型是 String
print(optionalString == nil) // false
optionalString = nil // 将这个对象赋值为空
print(optionalString == nil) // true
optionalString = "" // 赋值为空字符串
print(optionalString == nil) // false, 空字符串不是空对象
但是,如果数据类型后面没有 ?
则表示这是一个明确对象,不能进行 nil
操作:
var explicitString: String = ""
print(explicitString == nil) // Error 报错
explicitString = nil // Error 报错
同理,将上面的对象换成 Int
,Double
也是可以,有了上面的基础再去理解官方教程就轻松多了:
var optionalString: String? = "Hello"
print(optionalString == nil)
// false
var optionalName: String? = "John Appleseed"
var greeting = "Hello"
if let name = optionalName{ // 如果 optionalName 非 nil 则给 greeting 赋值
greeting = "Hello \(name)"
}
print(greeting)
// false
Hello John Appleseed
可以通过 ??
来判断可选值和明确值内容是否相同:
let nickname: String? = nil
let fullname: String = "John Appleseed"
let informalGreeting = "Hi \(nickname ?? fullname)"
print(informalGreeting)
// Hi John Appleseed
想要判断一个可选值是否为 nil
可以为其加上一个 let
,但一个明确值不能这做:
let nickname: String? = nil // 可选值
let fullname: String = "John" // 明确值
if let nickname{
print("Hey \(nickname)")
}
if let fullname { // 报错
print("Het \(fullname)")
}
3.3 switch 判断
对于 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.")
}
3.4 字典遍历
可以使用 for-in
循环遍历字典中的项,字典是无序集合,因此它们的键和值可以按任意顺序进行迭代。但是这里官方的demo又写的不好,体现不出 “无序” ,我将代码进行了一点修改,在实验时可以多运行几次就能发现当前扫描的对象顺序是不定的:
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 (label, numbers) in interestingNumbers{
print("Current is scaning \(label)")
for number in numbers{
if number > largest{
largest = number
}
}
}
print(largest)
我这里执行了两次可以看到第一个扫描的对象存在差别:
bogon:SwiftLearning gaohao$ swift demo.swift
Current is scaning Square
Current is scaning Prime
Current is scaning Fibonacci
25
bogon:SwiftLearning gaohao$ swift demo.swift
Current is scaning Prime
Current is scaning Fibonacci
Current is scaning Square
25
3.5 while 循环
使用 while 语句重复执行一段代码,直到条件发生变化。
var n = 2
while n < 100 {
n *= 2
}
print(n)
3.6 repeat-while 循环
将循环的条件也可以放在末尾,以确保循环至少运行一次。
var m = 2
repeat {
m *= 2
}while m < 100
print(m)
3.7 创建索引范围
可以在循环中用 start..<end
表示创建了一个索引范围:
var total = 0
for i in 0..<4{
total += i
}
print(total)
4. 函数与闭包
swift 中函数的使用和 python 比较相似,需要以 func
定义并且在调用的时候传入适当的参数:
func greet(person: String, day: String) -> String {
return "Hello \(person), today is \(day)"
}
print(greet(person: "Bob", day: "Tuesday"))
// Hello Bob, today is Tuesday
4.1 形参与形参标签
默认情况下,使用 形参名 作为参数函数调用是传入参数标签,swift 提供了一个机制可以让你自定义参数标签,或者使用 _
表示不使用标签:
下面这个函数就是一个很好的例子:
- 第一个参数
person
因为在前面用了_
进行修饰,所以在调用的时候不能写形参标签名; - 第二个参数在
day
前用了一个新的标签名on
进行修饰,所以在使用的时候只能用新的标签名on
; - 第三个参数
val
没有任何修饰,在调用时使用原始形参名;
func greet(_ person: String, on day: String, val: String) -> String{
return "Hello, \(person), today is \(day), value \(val)"
}
print(greet("John", on: "Wedensday", val: "100"))
print(greet("abc", day: "Wen", val: "50")) // 报错
print(greet(person: "abc", day: "Wen", val: "50")) // 报错
4.2 返回元组
如果函数需要返回多个值,可以以元组的方式返回,这个元组的个数没有上限,返回得到的元组也可以使用元素名和索引使用:
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
}
if score < min {
min = score
}
sum += score
}
return (min, max, sum)
}
let statistics = calculateStatistics(scores: [5,3,100,3,9])
print(statistics.sum) // 变量名方式使用
print(statistics.2) // 索引号方式使用
4.3 函数嵌套
在嵌套的函数中可以使用外边函数的变量。
func returnFiteen() -> Int{
var y = 10
func add(){ // 定义嵌套函数
y += 5
}
add() // 调用嵌套函数
return y
}
print(returnFiteen())
4.4 函数作为返回值
函数可以作为另一个函数的返回值,在 C++ 中这个就是函数指针,只不过swift中定义和用起来更方便:
func makeIncrementer() -> ((Int) -> Int) {
// 定义了一个参数和返回值匹配的函数
func addOne(number: Int) -> Int{
return 1 + number
}
return addOne // 将这个函数作为返回值
}
var increment = makeIncrementer() // 这里拿到的是一个函数
print(increment(7))
// 8
4.4 函数作为参数
同样,函数也可以作为参数传递给函数:
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]
print(hasAnyMatches(list: numbers, condition: lessThanTen))
// true
4.5 匿名函数闭包
可以使用括号 ({})
括起代码,从而编写一个没有名称的闭包。使用 in
将参数和返回类型与闭包主体分隔开。
var numbers = [20, 19, 7, 12]
let value = numbers.map({(number: Int) -> Int in
let result = 3 * number
return result
})
print(value)
// [60, 57, 21, 36]
和 python 中的 lambda 表达式一样,形参和返回值在简单的情况下可以省略:
var numbers = [20, 19, 7, 12]
let mappedNumbers = numbers.map({
number in 3 * number
})
print(mappedNumbers)
// [60, 57, 21, 36]
可以通过编号而来引用参数,这种方法在简单闭包中尤其有用。作为函数最后一个参数的闭包可以紧跟在括号之后。当闭包是函数的唯一参数时,可以完全省略括号。
var numbers = [20, 19, 7, 12]
let sortedNumbers = numbers.sorted {$0 > $1}
print(sortedNumbers)
// [20, 19, 12, 7]
5. 对象和类
使用 class
后跟类名来创建类。类中的属性声明与常量或变量声明的写法相同,方法和函数声明的写法也相同。
class Shape{
var numberOfSides = 0
func simpleDescription() -> String{
return "A shape with \(numberOfSides)"
}
}
var shape = Shape() // 初始化类对象
shape.numberOfSides = 10 // 修改类成员变量
print(shape.simpleDescription()) // 调用类成员函数
5.1 构造函数
初始化类其实就是构造函数,在对象实例化的时候自动调用,swift 中规定类的构造函数:
class NamedShape{
var numberOfSides: Int = 0
var name: String
init(name:String){
self.name = name
}
func simpleDescription() -> String{
return "A shape \(name) with \(numberOfSides) sides."
}
}
var namedShape = NamedShape(name: "8")
print(namedShape.simpleDescription())
5.2 成员函数覆写
使用 override
可以将父类的函数在子类进行覆写,同样 self
关键字可以明确使用的是当前类的成员:
// 父类
class NamedShape{
var numberOfSides: Int = 0
var name: String
init(name:String){
self.name = name
}
func simpleDescription() -> String{
return "A shape \(name) with \(numberOfSides) sides."
}
}
// 子类
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 side of length \(sideLength)"
}
}
let test = Square(sideLength: 5.2, name: "squre")
print(test.name)
print(test.area())
print(test.simpleDescription())
5.3 成员变量的 get 和 set
上面的例子中已经涉及到了类的成员变量使用 test.name
,你也可以显示定义每次获取/赋值成员变量时的操作:
【Note】:get
和 set
必须同时定义。
class EquilateralTriangle{
var sideLength: Double = 10
var name: String = ""
init(sideLength: Double, name: String){
self.sideLength = sideLength
self.name = name
}
var perimeter: Double{
get{
return 3.0 * sideLength // 每次获取perimeter变量时返回 3.0 * sideLength
}
set{
sideLength = newValue / 3.0 // 设置值时新值隐式命名为 newValue 这个变量
}
}
}
var trangle = EquilateralTriangle(sideLength: 3.1, name: "trangle")
print(trangle.perimeter)
trangle.perimeter = 10
print(trangle.sideLength)
5.4 willSet & didSet
在给对象成员变量赋值的时候可以使用 willSet
和 didSet
来对赋值前后进行操作,这里有一个关键概念 生效
,修改完变量之后可以通过这两个函数来控制生效的实际。
【Note】:willSet
和 didSet
是可以单独使用的。
class Square{
var sideLength: Double = 1.5
init(sideLength: Double){
self.sideLength = sideLength
}
}
class EquilateralTriangle{
var sideLength: Double = 2.0
init(sideLength: Double){
self.sideLength = sideLength
}
}
class TriangleAndSquare{
var triangle: EquilateralTriangle{
willSet{
print("triangle old value \(triangle.sideLength)")
triangle.sideLength = newValue.sideLength
}
didSet{
print("triangle new value \(triangle.sideLength)")
}
}
var square: Square{
willSet{
print("square old value \(square.sideLength)")
square.sideLength = newValue.sideLength
}
}
init(size: Double){
square = Square(sideLength: size)
triangle = EquilateralTriangle(sideLength: size)
}
}
var triangleAndSquare = TriangleAndSquare(size: 10)
print(triangleAndSquare.square.sideLength)
print(triangleAndSquare.triangle.sideLength)
triangleAndSquare.square = Square(sideLength: 20)
triangleAndSquare.triangle = EquilateralTriangle(sideLength: 18.2)
5.5 可选表达式
使用可选值时,可以在方法、属性和下标等操作前使用 ?
。如果可选值为 nil
,则后面的所有内容都将被忽略,并且整个表达式的值也为 nil
;否则可选值将被解包,并且之后的所有内容都将作用于解包后的值。在这两种情况下,整个表达式的值都是可选值。
class Square{
var sideLength: Double = 1.0
var name: String = ""
init(sideLength:Double, name:String){
self.sideLength = sideLength
self.name = name
}
}
// 可选值为实例
var optionalSquare: Square? = Square(sideLength:2.5, name:"optional square")
var sideLength = optionalSquare?.sideLength
// 可选值为 nil
optionalSquare = nil
sideLength = optionalSquare?.sideLength
6. 枚举类和结构体
使用 enum
创建枚举,与类和所有其他命名类型一样,枚举可以拥有与其关联的方法。但 swift 的枚举类功能相比 C++ 和 pyton 而言更全。
默认情况下,Swift 会从零开始每次递增 1 来分配原始值,但可以通过明确指定值来更改此行为。在上面的示例中,ace
被明确赋予了原始值 1
,其余原始值则按顺序分配,还可以使用字符串或浮点数作为枚举的原始类型。使用 rawValue
可以访问枚举成员的原始值。
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
let val = Rank.two
print(ace)
print(aceRawValue)
print(val)
print(Rank.eight)
6.1 原始值创建枚举对象
使用 init?(rawValue:)
初始化器可以从 原始值创建枚举实例,返回与原始值匹配的枚举成员,如果没有匹配的 Rank,则返回 nil
。
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)
}
}
}
if let convertedRank = Rank(rawValue: 3){
let threeDescription = convertedRank.simpleDescription()
print(threeDescription)
}
枚举的 case
值是实际值,而不仅仅是原始值的另一种写法,如果 case
中没有有意义的原始值,则无需提供。
6.2 枚举成员变量的引用
下面这个例子中引用枚举的 hearts
情况的两种方式:
- 在为变量
hearts
常量赋值时,Suit.hearts
使用其全名引用,因为该常量没有指定显式类型; - 在 switch 内部,枚举使用缩写形式
.hearts
引用,因为 self 的值已知。
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 heartDescription = hearts.simpleDescription()
print(hearts)
print(heartDescription)
6.3 枚举的关联行为
如果枚举具有原始值,则这些值在声明过程中确定,这意味着 特定枚举的每个实例始终具有相同的原始值;枚举的另一种选择是将这些值在创建实例时确定。可以将关联值的行为视为枚举情况实例的存储属性。例如,从服务器请求日出和日落时间的情况,服务器要么响应所请求的信息,要么响应出错的描述。
下面的例子中日出和日落时间是从 ServerResponse
值中提取出来的,并将其与 switch case
的值进行匹配。
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)")
}
6.4 结构体
使用 struct
创建一个结构体。结构体支持许多与类相同的行为,包括方法和初始化函数。结构体和类之间最重要的区别之一是,结构体在代码中传递时始终会被复制,而类则是通过引用传递的。
enum Rank: Int{
case ace = 1
case two, three, four, five, six, seven, eight, nine, ten
func simpleDescription() -> String{
return String(rawValue)
}
}
enum Suit {
case spades, hearts, diamonds, clubs
func simpleDescription() -> String{
switch self{
case .spades: return "spades"
case .hearts: return "hearts"
default: return "others"
}
}
}
struct Card{
var rank: Rank
var suit: Suit
func simpleDesctription() -> String{
return "The \(rank.simpleDescription()) of \(suit.simpleDescription())"
}
}
let threeOfSpades = Card(rank: .three, suit: .spades)
let threeOfSpadesDescription = threeOfSpades.simpleDesctription()
print(threeOfSpades)
print(threeOfSpadesDescription)
7. 并发
使用 async
标记异步运行的函数,在使用的时候用 await
关键字:
func fetchUserID(from server: String) async -> Int{
if server == "primary"{
return 97
}
return 501
}
var value = await fetchUserID(from: "192.168.0.1")
print(value)
// 501
7.1 Task 同步调用异步函数
下面这个例子就是一个复合型示例,使用 Task
从同步代码调用异步函数,而无需等待它们返回:
func fetchUserID(from server: String) async -> Int{
if server == "primary"{
return 97
}
return 501
}
func fetchUsername(from server: String) async -> String{
let userID = await fetchUserID(from: server)
if userID == 501{
return "John Appleseed"
}
return "Guest"
}
func connectUser(to server:String) async{
async let userID = fetchUserID(from: server)
async let username = fetchUserID(from: server)
let greeting = await "Hello \(username), user ID \(userID)"
print(greeting)
}
Task{
await connectUser(to: "primary")
}
7.2 任务组并发调用
同样可以用 “任务组” 的方式并发调用:
func fetchUserID(from server: String) async -> Int{
if server == "primary"{
return 97
}
return 501
}
func fetchUsername(from server: String) async -> String{
let userID = await fetchUserID(from: server)
if userID == 501{
return "John Appleseed"
}
return "Guest"
}
func connectUser(to server:String) async{
async let userID = fetchUserID(from: server)
async let username = fetchUserID(from: server)
let greeting = await "Hello \(username), user ID \(userID)"
print(greeting)
}
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
}
print(userIDs)
7.3 Actors
Actors 与类相似,不同之处在于它确保不同的异步函数可以同时安全地与同一 actor 的实例进行交互。
func fetchUserID(from server: String) async -> Int{
if server == "primary"{
return 97
}
return 501
}
func fetchUsername(from server: String) async -> String{
let userID = await fetchUserID(from: server)
if userID == 501{
return "John Appleseed"
}
return "Guest"
}
func connectUser(to server:String) async{
async let userID = fetchUserID(from: server)
async let username = fetchUserID(from: server)
let greeting = await "Hello \(username), user ID \(userID)"
print(greeting)
}
actor ServerConnection{
var server: String = "primary"
private var activeUsers: [Int] = []
func connect() async -> Int{
let userID = await fetchUserID(from: server)
activeUsers.append(userID)
return userID
}
}
let server = ServerConnection()
let userID = await server.connect()
print(server) // demo.ServerConnection
print(userID) // 97
8. 协议和扩展
8.1 协议 Protocols
用 protocos
来声明一个协议 Protocols,协议的功能类似于 Java 和 C++ 中的接口,
protocol ExampleProtocol{
var simpleDescription: String { get } // 表示实现这个协议的类型必须提供这个属性的“只读”访问
mutating func adject() // mutating 表示该方法可能修改self值,默认是不能修改的
}
类、枚举、结构体都可以继承于协议。
8.1.1 继承于协议的类
protocol ExampleProtocol {
var simpleDescription: String { get }
mutating func adjust()
}
class SimpleClass: ExampleProtocol{
var simpleDescription: String = "A simple class."
var anotherProperty: Int = 69105
func adjust() {
simpleDescription += " Now 100% adjusted."
}
}
var a = SimpleClass()
a.adjust()
print(a.simpleDescription)
8.1.2 继承于协议的结构体
【Note】:下面的代码中在实现 adjust()
函数时使用了 mutating
关键字来让结构体能够修改自身,而上面的类不需要,因为类本身就可以直接修改自身。
protocol ExampleProtocol {
var simpleDescription: String { get }
mutating func adjust()
}
struct SimpleStructure: ExampleProtocol{
var simpleDescription: String = "A simple struct"
mutating func adjust() {
simpleDescription += " (adjusted)"
}
}
var b = SimpleStructure()
b.adjust()
print(b.simpleDescription)
8.2 扩展 Extension
使用 extension
可以为现有类型添加功能,例如新方法和计算属性。用扩展为在其他地方声明的类型,甚至从库或框架导入的类型添加协议一致性。
protocol ExampleProtocol {
var simpleDescription: String { get }
mutating func adjust()
}
extension Int: ExampleProtocol{
var simpleDescription: String {
return "The number \(self)"
}
mutating func adjust() {
self += 42
}
}
print(7.simpleDescription) // A simple struct (adjusted)
9. 异常处理
可以对自带的 Error
协议进行适配异常类:
enum PrinterError: Error{
case outOfPaper
case noToner
case onFire
}
throw
来抛出错误,使用 throws
来标记一个可以抛出错误的函数。如果在函数中抛出错误,该函数会立即返回,并且调用该函数的代码会处理该错误。
enum PrinterError: Error{
case outOfPaper
case noToner
case onFire
}
func send(job: Int, toPrinter printerName: String) throws -> String{
if printerName == "Never Has Toner"{
throw PrinterError.noToner
}
return "Job sent"
}
9.1 do-catch try 捕获异常
处理错误的方法有很多种,其中一种方法是使用 do-catch
语句。在 do
语句块中,通过在可能抛出错误的代码前面添加 try
来标记它。在 catch
语句块中,除非指定其他名称,否则错误会自动被命名为 error
。
enum PrinterError: Error{
case outOfPaper
case noToner
case onFire
}
func send(job: Int, toPrinter printerName: String) throws -> String{
if printerName == "Never Has Toner"{
throw PrinterError.noToner
}
return "Job sent"
}
do{
let printerResponse = try send(job: 1040, toPrinter: "Bi Shen")
print(printerResponse)
}catch{
print(error)
}
// Job sent
可以提供多个 catch
块来处理特定的错误。在 catch
之后编写一个模式,就像在 switch
中的 case
之后编写一样。
enum PrinterError: Error{
case outOfPaper
case noToner
case onFire
}
func send(job: Int, toPrinter printerName: String) throws -> String{
if printerName == "Never Has Toner"{
throw PrinterError.noToner
}
return "Job sent"
}
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)
}
// Job sent
9.2 try? 捕获异常
处理错误的另一种方法是使用 try?
将结果转换为可选值。如果函数抛出错误,则丢弃特定的错误并将结果设置为 nil
。否则,结果将是一个包含函数返回值的可选值。
enum PrinterError: Error{
case outOfPaper
case noToner
case onFire
}
func send(job: Int, toPrinter printerName: String) throws -> String{
if printerName == "Never Has Toner"{
throw PrinterError.noToner
}
return "Job sent"
}
let printerSuccess = try? send(job: 1884, toPrinter: "Mergenthaler")
let printerFailure = try? send(job: 1885, toPrinter: "Never Has Toner")
print(printerSuccess == nil) // false
print(printerFailure == nil) // true
9.3 defer 返回
使用 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) // false
10. 泛型
在尖括号 <>
内写一个名称来创建通用函数或类型。
func makeArray<Item>(repeating item: Item, numberOfTimes:Int) -> [Item]{
var result: [Item] = []
for _ in 0..<numberOfTimes{
result.append(item)
}
return result
}
var res = makeArray(repeating: "knock", numberOfTimes: 4)
print(res) // ["knock", "knock", "knock", "knock"]
可以创建函数和方法的通用形式,以及类、枚举、结构体。
enum OptionalValue<Wrapped>{
case none
case some(Wrapped)
}
var possibleInteger: OptionalValue<Int> = .none
possibleInteger = .some(100)
print(possibleInteger) // some(100)
在主体之前使用 where
来指定一系列条件:要求类型实现协议、要求两种类型相同或要求类具有特定的超类。
下面这个代码定一了一个泛型函数 anyCommonElements
作用是:判断两个序列之间是否有共同的元素。官网在这里又没做解释而直接使用了关键字 where
,我在这里补充下:
where T.Element: Equatable
:T
的元素需要支持等号==
比较;T.Element == U.Element
:两个序列中的元素类型必须相同。
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
}
print(anyCommonElements([1, 2, 3], [3])) // true