Swift 4.0 编程语言(六)
126.析构器
在一个类实例销毁前,一个析构器会立即调用。使用deinit 关键字来表示析构器, 跟构造器写法类似。析构器只能用在类类型上。 types.
析构器如何工作
Swift 在实例不需要时会自动析构它, 去释放资源。Swift 通过自动引用计数来管理实例的内存(ARC). 当实例销毁时你不需要手动执行清理操作。不过, 当你使用自己的资源时, 你可能需要额外执行一些清理。例如, 如果你创建一个类去读写文件, 在类的实例销毁前你可能需要去关闭这个文件。
类最多只能有一个析构器。析构器没有任何参数也没有括号:
deinit {
// perform the deinitialization
}
析构器会自动调用, 在实例销毁发生前。禁止自己去调用析构器。子类会继承超类的析构器, 超类析构器在子类析构器实现后调用。 即使子类不提供自己的析构器,超类析构器也会调用。
因为实例直到析构器调用才会销毁, 一个析构器可以访问调用实例的所有属性,同时也可以改变基于这些属性的行为 (例如寻找需要关闭的文件名).
析构器的活动
这里有一个析构器活动的例子。这个例子定义了两个新类型, Bank 和 Player, Bank 类管理一个虚拟货币, 它的流通货币不会超过 10,000 硬币。游戏里只有能有一个银行, 所以 Bank 用类实现,带有类型属性和方法来存储和管理它的当前状态:
class Bank {
static var coinsInBank = 10_000
static func distribute(coins numberOfCoinsRequested: Int) -> Int {
let numberOfCoinsToVend = min(numberOfCoinsRequested, coinsInBank)
coinsInBank -= numberOfCoinsToVend
return numberOfCoinsToVend
}
static func receive(coins: Int) {
coinsInBank += coins
}
}
Bank 用coinsInBank 属性跟中当前硬币的数量。它同时提供了两个方法s—distribute(coins:) 和 receive(coins:)—来管理硬币的发行和回收。
distribute(coins:) 方法判断在发行前银行是否有足够的硬币。如果没有足够的硬币, Bank 返回一个比要求数量少的数量 (如果银行没有硬币了就返回0). 它返回一个整数来表示实际提供的硬币数量。
receive(coins:) 方法简单加上收到的硬币数量。
Player 类描述了游戏中的一个玩家。 每一个玩家在任何时间都有一定数量的硬币在钱包里。这个用玩家的coinsInPurse 来表示:
class Player {
var coinsInPurse: Int
init(coins: Int) {
coinsInPurse = Bank.distribute(coins: coins)
}
func win(coins: Int) {
coinsInPurse += Bank.distribute(coins: coins)
}
deinit {
Bank.receive(coins: coinsInPurse)
}
}
在初始化过程中,每个玩家都会用一定数量的硬币来初始化, 尽管玩家可能在银行硬币不足的时候获取较少的数量。
Player 类定义了一个方法 win(coins:), 它从银行获取一定数量的硬币然后把它们加入玩家的钱包。Player 类同时实现了一个去构造器, 它在 Player 实例销毁前调用。在这里, 这个构造器简单把玩家的硬币返还给银行:
var playerOne: Player? = Player(coins: 100)
print("A new player has joined the game with \(playerOne!.coinsInPurse) coins")
// 打印 "A new player has joined the game with 100 coins"
print("There are now \(Bank.coinsInBank) coins left in the bank")
// 打印 "There are now 9900 coins left in the bank"
一个新的 Player 实例被创建, 如果有硬币就请求100个硬币。这个Player 实例存在一个可选的 Player 变量 playerOne 中。可选变量用在这里, 因为玩家可能在任何时间离开游戏。可选项让你跟踪玩家当前是否还在游戏中。
因为 playerOne 是一个可选项, 当它的 coinsInPurse 属性被访问来打印硬币数量时, 需要使用感叹号。一旦它的 winCoins(_:) 方法被调用:
playerOne!.win(coins: 2_000)
print("PlayerOne won 2000 coins & now has \(playerOne!.coinsInPurse) coins")
// 打印 "PlayerOne won 2000 coins & now has 2100 coins"
print("The bank now only has \(Bank.coinsInBank) coins left")
// 打印 "The bank now only has 7900 coins left"
在这里, 玩家已经赢得了 2,000 个硬币。玩家的钱包现在有 2,100 个硬币, 银行还有 7,900 个硬币。
playerOne = nil
print("PlayerOne has left the game")
// 打印 "PlayerOne has left the game"
print("The bank now has \(Bank.coinsInBank) coins")
// 打印 "The bank now has 10000 coins"
玩家现在离开游戏。设置可选playerOne变量为nil 说明了这点。意思是 “没有Player 实例。” 此时, playerOne 变量引用的Player 实例被销毁。没有其他属性或者变量再引用 Player 实例, 所以它被销毁来释放内存。在这个发生之前, 它的析构器自动调用, 然后它的硬币全部归还给银行。
127.自动引用计数
Swift 使用自动引用计数来跟踪和管理你的应用的存储使用。在大多数情况里, 这就意味着内存管理 “仅仅工作” 在 Swift, 你自己不需要管理内存。类的实例不再需要的时候,ARC 会自动释放实例使用的内存。
不过, 在一些少见的情况下,为了替你管理内存,ARC 需要更多关于你代码关系的信息。这个章节描述这些场景并且向你展示你怎么使用 ARC 来管理应用的内存。
备注:引用计数只适用于类的实例。结构体和枚举是值类型, 而非引用类型, 不能用引用存储和传递。
ARC 如何工作
每次你创建一个新的类实例, ARC 就会分配一块内存来存储实例的信息。这个内存掌握了实例类型的信息, 还有对应实例的存储属性的值。
另外, 当一个实例不再需要的时候, ARC 会释放实例使用的内存,让内存另有他用。这个确保类的实例不需要的时候,不会继续占用内存空间。
不过, 如果 ARC 将要销毁在使用的实例, 它将不能访问实例的属性, 或者是实例的方法。 事实上, 如果你访问这个实例, 你的应用可能会崩溃。
为了确保实例在使用时不会消失, ARC 跟踪有多少属性, 常量和变量正在引用类的每个实例。只要有一个对实例的引用,ARC 都不会销毁这个实例。
为了实现这种可能, 一旦你把类赋给一个属性, 常量或者变量, 这些属性,常量或者变量就会强引用这个实例。 这个引用之所以叫强引用,是因为它牢牢掌握了这个实例。只要强引用还在,就不会允许它被销毁。
ARC 的活动
下面有个自动引用计数的例子,来展示它如何工作。这个例子以一个简单的类 Person 开始, 它定义了一个存储常量属性 name:
class Person {
let name: String
init(name: String) {
self.name = name
print("\(name) is being initialized")
}
deinit {
print("\(name) is being deinitialized")
}
}
Person 类有一个构造器,用来设置实例的 name 属性,并且打印一条信息来表明构造在进行。Person 类同时也有一个析构器,在类的实例被销毁时打印一条信息。
下面代码定义了三个 Person? 类型的变量, 在后续代码中,它们用来设置多个对新Person 实例的引用。因为这些变量是可选类型(Person?, 不是 Person), 它们会自动初始化为nil, 并且当前不会引用一个Person 实例。
var reference1: Person?
var reference2: Person?
var reference3: Person?
你现在可以创建一个新的 Person 实例,并且把它赋值给三个变量的其中一个:
reference1 = Person(name: "John Appleseed")
// 打印 "John Appleseed is being initialized"
注意这条信息 “John Appleseed is being initialized” 在调用Person 来的构造器时打印。这个确认了初始化已经发生。
因为新的 Person 实例已经指定给 reference1 变量, 现在从reference1 到新的Person 实例有了一个强引用。因为有了至少一个强引用, ARC 确保 Person 保持在内存里不被销毁。
如果你把相同的 Person 实例赋值给另外两个变量, 指向这个实例的两个强引用就建立了:
reference2 = reference1
reference3 = reference1
现在有三个强引用指向这个单独的 Person 实例。
通过给其中两个变量赋值为nil,你可以打破这些强引用, 还剩一个强引用, 但是Person 实例不会被销毁:
reference1 = nil
reference2 = nil
ARC 直到最后一个强引用被打破才会销毁 Person 实例, 在这个时刻很清楚你无法再继续使用这个 Person 实例:
reference3 = nil
// 打印 "John Appleseed is being deinitialized"
类实例的强引用循环
上面的例子中, ARC 可以跟踪你创建的实例的引用数量,并且在实例不需要的时候销毁它。
不过, 有可能写下这种代码,在代码里一个类实例不知道什么时候强引用数量为0. 如果两个类实例互相强引用就会出现这个问题, 这样每个实例都保有其他活动的实例。这就是众所周知的强引用循环。
使用weak 或者 unowned 来代替强引用, 这样你可以解决强引用循环的问题。不过, 在你学习如何解决强引用问题前, 有必要先学习循环引用是怎么产生的。
下面的例子展示强引用循环是如何偶发的。这个例子定义了两个类 Person 和 Apartment, 它模拟了一组公寓和居民:
class Person {
let name: String
init(name: String) { self.name = name }
var apartment: Apartment?
deinit { print("\(name) is being deinitialized") }
}
class Apartment {
let unit: String
init(unit: String) { self.unit = unit }
var tenant: Person?
deinit { print("Apartment \(unit) is being deinitialized") }
}
每个 Person 实例有一个字符串类型的name属性和一个可选的 apartment 属性,后者初始化为nil. apartment 属性是可选的, 因为一个人有可能没有公寓。
相似的, 每个 Apartment 实例有一个字符串类型的 unit属性和一个可选的 tenant 属性,后者初始化为nil nil. tenant 属性是可选的,因为一个公寓有可能没有租客。
两个类都定义了析构器, 它们打印类实例被销毁的事实。这个可以帮你查看两个类的实例是否按照预期销毁了。
下面的代码片段定义了两个可选类型的变量 john 和 unit4A, 他们将会被设置为特定的 Apartment 和 Person 实例。这两个变量初始值都是 nil, 因为它们是可选的:
var john: Person?
var unit4A: Apartment?
john = Person(name: "John Appleseed")
unit4A = Apartment(unit: "4A")
这里展示创建和赋值这两个实例后,强引用的样子。john 变量现在强引用这个新的 Person 实例, unit4A 变量现在强引用这个新的 Apartment 实例:
现在可以联系两个实例到一起,现在人有了公寓,公寓也有了一个租客。注意这里使用了感叹号来拆包和访问 john 和 unit4A 可选变量, 所以这些实例的属性可以被设置:
john!.apartment = unit4A
unit4A!.tenant = john
下面是连接两个实例之后的强引用的样子:
不幸运的是, 联系这两个实例,导致在它们之间建立了强引用循环。Person 实例现在有了一个强引用到 Apartment 实例, 然后 Apartment 实例有一个强引用到 Person 实例。因此, 当你打破 john 和 unit4A 变量拥有的强引用, 引用计数也不会降为0, 实例也不会被ARC销毁:
john = nil
unit4A = nil
注意当你设置两个变量为nil时,两个析构器都不会调用。强引用循环阻止了 Person 和 Apartment 实例的销毁。导致你的应用内存泄露。
这里展示设置john 和 unit4A 变量为 nil后强引用的样子:
128.解决实例间的强引用循环
针对类类型的属性, Swift 提供了两个方法来解决强引用循环问题: weak 引用 和 unowned 引用。
Weak 和 unowned references 可以使一个实例在引用循环中去调用另外一个实例,而不会强引用它。然后实例之间可以互相引用而不会出现强引用循环。
其他实例有更短的生命期的时候使用一个弱引用—就是说, 当其他实例首先被销毁的时候。 上面的 Apartment 例子, 在公寓的生命周期内, 在某个时刻没有租客是正常的。所以在这个情况下,使用弱引用来打破强引用是合适的方式。相反, 如果其他实例有着相同或者更长的生命周期,就使用无主引用。
弱引用
弱引用不会强持有它引用的实例, 并且不会阻止ARC处理引用的实例。这个行为防止出现强引用循环。你可以在属性或者变量声明前加上weak关键字来声明一个弱引用。
因为弱引用不会强持有它引用的实例, 在弱引用仍然引用实例的时候,有可能这个实例将要被销毁了。因此, 在引用实例销毁时, ARC 自动把弱引用设置为nil. 并且, 由于弱引用要在运行时允许它们的值修改为nil, 它们总是声明成可选类型变量, 而不是常量。
你可以判断在弱引用中一个值的存在与否, 就像其他可选值一样, 实例不存后,你不会得到一个无效的实例引用。
备注:ARC 设置一个弱引用为nil时, 属性观察者不会调用。
下面的例子和上面 Person 和 Apartment 例子是一样的, 只有一个重要的不同。这一次, Apartment 类型的 tenant 属性声明为弱引用:
class Person {
let name: String
init(name: String) { self.name = name }
var apartment: Apartment?
deinit { print("\(name) is being deinitialized") }
}
class Apartment {
let unit: String
init(unit: String) { self.unit = unit }
weak var tenant: Person?
deinit { print("Apartment \(unit) is being deinitialized") }
}
两个变量的强引用它们之间的联系创建如前 (john 和 unit4A):
var john: Person?
var unit4A: Apartment?
john = Person(name: "John Appleseed")
unit4A = Apartment(unit: "4A")
john!.apartment = unit4A
unit4A!.tenant = john
这里展示联系两个实例后,引用的样子:
Person 实例依然强引用 Apartment 实例, 不过 Apartment 实例现在是弱引用 Person 实例了。这就意味着, 当你设置john为nil来打破这个强引用的时候, 就不会再有对 Person 实例的强引用:
john = nil
// 打印 "John Appleseed is being deinitialized"
因为没有更多对Person实例的强引用, 它被销毁而且tenant 属性被设置为 nil:
现在只剩下unit4A 变量对 Apartment 实例的强引用。如果你打破这个强引用, 就不会再有对Apartment实例的强引用:
unit4A = nil
// 打印 "Apartment 4A is being deinitialized"
因为没有更多对 Apartment 实例的强引用, 它也被销毁了:
备注:在使用垃圾回收的系统里, 弱指针有时用来实现一个简单的缓存机制,因为没有强引用的对象只有在内存紧张的时候才会触发垃圾回收。不过, 使用 ARC, 只要值的强引用被移除了,它们就会被销毁, 为了这个目的使用弱引用是不合适的。
无主引用
很像弱引用, 无主引用不会强持有它引用的实例。跟弱引用不同的是, 当其他实例有相同或者更久的生命周期时才使用无主引用。通过在属性或者变量声明的前面加上unowned 关键字来声明无主引用。
一个无主引用总希望有值。结果是, ARC 永远不会把无主引用的值设为nil, 这意味着无主引用使用非可选类型来定义。
重要
使用无主引用前提是,只在你能确保引用的实例不会被销毁情况下使用。
如果你在实例销毁后尝试调用无主引用的值,你会得到一个运行时错误。
下面的例子定义了两个类, Customer 和 CreditCard, 它模拟了一个银行用户和用户的信用卡。这两个类都存储了对方的实例作为属性。这种关系有产生强引用循环的可能。
Customer 和 CreditCard 之间的关系与上面的Apartment 和 Person之间的关系略有不同。在这个数据模型里, 一个用户可能有或者没有一张信用卡, 但是一张信用卡总有对应的用户。一个 CreditCard 实例永远不会比它调用的用户活的更久。为了表示这个, Customer 类有一个可选的 card 属性, CreditCard 类有一个无主的 (非可选) customer 属性。
此外, 一个新的 CreditCard 实例创建时, 需要传递一个number 值和一个 customer 实例给自定义的 CreditCard 构造器。这个确保了一个 CreditCard 实例总会有一个对应的用户。
因为一张信用卡总会有一个用户, 你定义customer 属性为一个无主引用, 可以避免强引用循环:
class Customer {
let name: String
var card: CreditCard?
init(name: String) {
self.name = name
}
deinit { print("\(name) is being deinitialized") }
}
class CreditCard {
let number: UInt64
unowned let customer: Customer
init(number: UInt64, customer: Customer) {
self.number = number
self.customer = customer
}
deinit { print("Card #\(number) is being deinitialized") }
}
备注:CreditCard 的number属性定义成 UInt64 类型而不是 Int, 是为了保证 number 属性的容量做够大可以在32位和64位系统上存储一个16位的信用卡号。
下面的代码片段定义了一个可选的 Customer 变量 john, 用来存储指向特定用户的引用。这个变量初始值是 nil, 因为它是可选的:
var john: Customer?
现在你创建了一个 Customer 实例, 然后用它初始化和分配一个新的CreditCard 实例作为用户的card 属性:
john = Customer(name: "John Appleseed")
john!.card = CreditCard(number: 1234_5678_9012_3456, customer: john!)
这里是引用的样子, 现在你把两个实例联系起来了:
Customer 实例现在强引用 CreditCard 实例, CreditCard 实例则是无主引用 Customer 实例。
由于无主的 customer 引用, 当你打破john 变量持有的强引用, 就不会有其他对 Customer 的强用了:
因为没有其他对 Customer 实例的强引用, 它会被销毁,然后, 对CreditCard 实例的强引用也没有了, 所以它也被销毁了:
john = nil
// 打印 "John Appleseed is being deinitialized"
// 打印 "Card #1234567890123456 is being deinitialized"
最后的代码片段展示了Customer 实例和 CreditCard 实例析构器都打印出析构的信息。
备注:上面的例子展示了如何使用安全的无主引用。 Swift 同时提供了不安全的无主引用, 使用场景是你需要去禁用运行时安全检查—例如, 性能原因。使用所有的不安全操作, 你都有责任检查代码的安全问题。
通过写上unowned(unsafe)来表明这是一个不安全的无主引用。如果在实例销毁后尝试访问一个不安全的无主引用, 你的程序会访问实例原来存在的内存位置, 这是不安全的操作。
128.无主引用和隐式拆包可选属性
上面弱引用和无主引用的例子,覆盖了更多常见情况的两种,在这种场景下需要打破一个强引用循环。
Person 和 Apartment 例子展示了一个情况, 有两个属性都可以设置为nil, 有导致一个强引用循环的可能。这种情况最好使用弱引用来解决。
Customer 和 CreditCard 例子展示了一个情况,有一个属性允许设置为nil,而另外一个属性不能设置成nil,也有产生强引用循环的可能。这个情况最好用无主引用来解决。
不过, 存在第三种情况, 这种情况下两个属性都应该有值, 一旦初始化完成两个属性都不能为nil. 在这个情况下, 把一个类的无主属性和其他类的隐式拆包可选属性合并是很有用的。
一旦初始化完成,这个可以保证两个属性都可以直接访问 (没有可选拆包), 同时避免了一个引用循环。这部分内容介绍如何建立这种关系。
下面的例子定义了两个类, Country 和 City, 它们互相存储对方类的实例作为属性。在这个数据模型中, 每个国家必须有一个首都,每个城市必须属于一个国家。为了表示这个, Country 有一个 capitalCity 属性, City 有一个 country 属性:
class Country {
let name: String
var capitalCity: City!
init(name: String, capitalName: String) {
self.name = name
self.capitalCity = City(name: capitalName, country: self)
}
}
class City {
let name: String
unowned let country: Country
init(name: String, country: Country) {
self.name = name
self.country = country
}
}
为了建立这两个类的关联, City 构造器接收一个Country 实例, 然后把这个实例存储到它的 country 属性。
City 的构造器在 Country 构造器中调用。不过, Country 构造器直到一个新的Country 实例完成构造后才可以传给 City 构造器。
为了应对这个需求, 你把Country 的capitalCity 属性声明成一个隐式拆包可选属性, 通过在类型后加上感叹号来表明 (City!). 这就意味着 capitalCity 属性默认值为 nil, 不需要拆包就可以访问它的值。
因为 capitalCity 默认值是 nil, 一个新的 Country 实例在name属性在构造器被设置后会被完全初始化。这就意味着 Country 构造器开始引用和传递隐式的 self 属性,当name属性设置的时候。当Country 构造器开始设置它的capitalCity 属性时,Country 构造器可以把self作为参数传给 City 构造器。
所有这一切意味着,你可以在一条语句里创建 Country 和 City 实例, 而不会产生强引用循环, capitalCity 属性可以直接访问, 不需要使用感叹号去拆包它的可选值:
var country = Country(name: "Canada", capitalName: "Ottawa")
print("\(country.name)'s capital city is called \(country.capitalCity.name)")
// 打印 "Canada's capital city is called Ottawa"
上面例子里, 隐式拆包可选意味着,所有两阶段类构造需求都满足了。一旦初始化完成, capitalCity 属性可以像非可选值一样使用和访问, 同时避免了一个强引用循环。
闭包强引用循环
你可以看到上面展示的,实例互相引用产生的强引用循环。你也看到如何使用弱引用和无主引用来打破这种循环。
如果你把一个闭包赋值给类属性,也可能产生一个强引用循环, 包体捕获了这个实例。因为包体访问了实例属性, 比如 self.someProperty, 或者因为闭包调用了实例方法, 例如 self.someMethod(). 在这两种情况下, 这些访问会导致闭包捕获self, 这就产生了一个强引用循环。
这个强引用循环是因为闭包产生,像类一样是引用类型。当你把闭包赋值给属性时, 你就赋值了一个引用给这个闭包。本质上, 它的问题和上面是一样的—两个强引用互相保持。不过, 相比两个类实例, 这次是一个类实例和一个闭包互相保有。
Swift 提供一个优雅的方案来解决这个问题, 就是大家熟知的捕获列表。不过, 在你知道怎么用捕获列表来打破一个强引用循环前,有必要知道它是怎么产生的。
下面的例子展示使用闭包调用self时, 是如何产生强引用循环的。这个例子定义了一个类 HTMLElement, 它提供了一个简单的模拟HTML文档里的单个元素:
class HTMLElement {
let name: String
let text: String?
lazy var asHTML: () -> String = {
if let text = self.text {
return "<\(self.name)>\(text)"
} else {
return "<\(self.name)>"
}
}
init(name: String, text: String? = nil) {
self.name = name
self.text = text
}
deinit {
print("\(name) is being deinitialized")
}
}
HTMLElement 类定义了一个name属性, 用来表示元素名, 比如头部元素 “h1” , 段落原色”p”, 或者换行符”br”。HTMLElement 同时定义了一个可选text属性, 可以设置成字符串来表示渲染的文本。
除了这两个简单的属性, HTMLElement 类定义了一个lazy属性asHTML. 这个属性引用了一个闭包来把name和text合并成一个HTML片段。asHTML 属性类型是 () -> String, 或者 “没有参数的函数,返回一个字符串”。
默认情况, asHTML 属性被指定了一个闭包,这个闭包返回字符串来表示一个 HTML 标签。如果文本存在,这个标签就包含可选文本值,否则不包含文本内容。对于一个段落元素, 闭包会返回 “
some text
“ 或者 “
“, 取决于text 属性是否等于 “some text” 或者 nil.
asHTML 属性命名和使用稍微有点像实例方法。不过, 因为 asHTML 是一个闭包而不是实例方法, 你可以用自定义闭包来替换它的默认值, 如果你想为一个特殊HTML元素改变HTML渲染的话。
例如, asHTML 属性可以设置成一个闭包, 如果text属性为nil就默认设置成一些文本, 以防返回一个空的HTML标签:
let heading = HTMLElement(name: "h1")
let defaultText = "some default text"
heading.asHTML = {
return "<\(heading.name)>\(heading.text ?? defaultText)"
}
print(heading.asHTML())
// 打印 "
some default text
"
备注:asHTML 属性声明成lazy属性, 因为它只有在元素需要作为字符串值进行渲染的时候才需要。asHTML 是一个lazy属性意味着你可以在默认闭包里调用self, 因为初始化完成同时self存在后,lazy属性才能访问。
HTMLElement 类只提供了一个构造器,它有一个name参数和一个text参数。这个类同时定义了一个析构器, 它打印一条信息来表示HTMLElement 实例被销毁了。
这里是如何使用 HTMLElement 类来创建和打印一个新的实例:
var paragraph: HTMLElement? = HTMLElement(name: "p", text: "hello, world")
print(paragraph!.asHTML())
// 打印 "
hello, world
"
注意:paragraph 变量定义成一个可选的 HTMLElement 类型, 所以它可以设置成nil。下面展示一个强引用循环的存在。
不幸的是, HTMLElement 类, 在 HTMLElement 实例和用作asHTML 值的闭包之间产生了一个强引用循环。看起来是这个样子:
实例的asHTML 属性强引用它的闭包。然而, 因为这个闭包在它的包体里调用了self(self.name 和 self.text), 闭包捕获了self, 这以为着它又强引用回HTMLElement 实例。两者之间就产生了一个强引用循环。
备注
尽管这个闭包多次调用self, 它只会捕获一个对 HTMLElement 实例的强引用。
如果你把paragraph 变量设置成nil来打破这个强引用, HTMLElement 实例和它的闭包都不会销毁, 就是因为这个强引用循环:
paragraph = nil
注意 HTMLElement 析构器的信息没有打印, 这就说明 HTMLElement 实例没有销毁。
解决闭包强引用循环
通过在闭包里定义一个捕获列表,你可以解决闭包和类实例之间的强引用循环的问题。捕获列表定义了在包体里捕获一个或者多个引用类型时的使用规则。就像两个类实例间的强引用, 你声明每个捕获的引用为弱引用或者无主引用而不是强引用。弱引用和无主引用的选择取决于你代码不同部分的联系。
备注:当你在闭包中调用self 的成员时, Swift 要求你写成 self.someProperty 或者 self.someMethod() (而不是只有someProperty 或者 someMethod())。这个帮你记住, 闭包偶尔会捕获self.
定义捕获列表
捕获列表中的每一项都是一对 weak 或者 unowned 关键字,带有对一个类实例或者变量的引用或者使用一些值初始化的变量。这些写在一堆方括号内,用逗号分开。
把捕获列表放在闭包参数列表前,如果它们提供则返回类型:
lazy var someClosure: (Int, String) -> String = {
[unowned self, weak delegate = self.delegate!] (index: Int, stringToProcess: String) -> String in
// closure body goes here
}
如果一个闭包没有指定参数列表或者返回类型, 就把捕获列表放在闭包最开始,后面跟着in关键字:
lazy var someClosure: () -> String = {
[unowned self, weak delegate = self.delegate!] in
// closure body goes here
}
弱引用和无主引用
当闭包和它捕获的实例将要互相引用, 并且会同时销毁时,定义成捕获无主引用。
相反, 当捕获的实例在某个时间会变成nil时,定义成捕获弱引用。弱引用总是一个可选类型, 当引用的实例销毁后自动变成nil. 这个确保你可以在包体内判断它们是否存在。
备注:如果捕获的引用永不会变成nil, 它应该总是捕获为一个无主引用, 而不是一个弱引用。
无主引用比较适合解决上面 HTMLElement 例子里的强引用循环的问题。这里是如何写 HTMLElement 类来避免这种循环:
class HTMLElement {
let name: String
let text: String?
lazy var asHTML: () -> String = {
[unowned self] in
if let text = self.text {
return "<\(self.name)>\(text)"
} else {
return "<\(self.name)>"
}
}
init(name: String, text: String? = nil) {
self.name = name
self.text = text
}
deinit {
print("\(name) is being deinitialized")
}
}
这个版本的实现和前面版本的实现是一样的, 除了asHTML闭包里的捕获列表。在这种情况下, 捕获列表是 [unowned self], 意思是 “捕获self作为无主引用而不是强引用”。
你可以跟以前一样创建和打印一个 HTMLElement 实例:
var paragraph: HTMLElement? = HTMLElement(name: "p", text: "hello, world")
print(paragraph!.asHTML())
// 打印 "
hello, world
"
这里是使用捕获列表后引用的样子:
这次, 闭包捕获的self是一个无主引用, 不会强引用 HTMLElement 实例。如果你把 paragraph 变量的引用设置为nil, HTMLElement 实例就会被销毁, 可以看到析构器打印的信息在下面:
paragraph = nil
// 打印 "p is being deinitialized"
更多捕获列表信息, 参见捕获列表。
129.可选链
可选链是一个过程,用来查询和调用属性, 方法和下标用一个可选项。如果可选项包含一个值, 属性, 方法或者下标调用成功; 如果可选项为nil, 属性, 方法或者下标调用返回nil. 多次调用可以写在一起, 如果任何链中任何联系为nil,整个链就会失败。
备注:Swift 中的可选链和Objective-C中的消息传递nil很像, 但是它对任何类型都一样, 可以判断失败或者成功。
作为强制拆包替代方案的可选链
在可选值后面加上问号就可以指定可选链, 如果可选值不为nil,你可以调用一个属性,函数或者下标。这个跟在可选值后放置感叹号来强制拆包很相似。区别是,当可选项为nil时,可选链接就会失败。如果可选项为nil,强制拆包会触发运行时错误。
为了反映可选链接可以被nil值调用的事实, 一个可选链接调用的结果总是一个可选值, 即使你查询的属性,方法或者下标返回一个非可选的值。你使用返回值来判断可选链接调用是否成功, 或者判断是否因为有nil存在导致了失败。
特别指出, 可选连接调用的结果和预期返回值的类型是一样的, 但是包在一个可选项中。如果通过可选链接访问, 正常要返回Int的属性会返回Int?
下面的代码片段展示了可选链接和强制拆包的区别, 确保你可以用来判断成功与否。
首先, 定义两个类Person 和 Residence:
class Person {
var residence: Residence?
}
class Residence {
var numberOfRooms = 1
}
Residence 实例只有一个整型属性 numberOfRooms, 默认值是 1. Person 实例有一个可选Residence?类型属性 residence.
如果你创建一个新的 Person 实例, 它的residence 属性默认初始化为nil,因为它是可选的。下面的代码里, john 有一个值为nil的属性residence:
let john = Person()
如果你想访问person属性 residence的属性 numberOfRooms, 可以在residence 后面放置一个感叹号来强制拆包它的值, 由于还没有值可以拆包,所以你会触发一个运行时错误:
let roomCount = john.residence!.numberOfRooms
// 触发运行时错误
当john.residence有一个非nil值时,上面的代码会成功,然后会把合适的房间数量设置给 roomCount. 不过, 当residence 为nil时,这个代码总会触发运行时错误。
可选链接提供了一个替代方式来访问 numberOfRooms 的值。通过在感叹号的位置使用问号来使用可选链接:
if let roomCount = john.residence?.numberOfRooms {
print("John's residence has \(roomCount) room(s).")
} else {
print("Unable to retrieve the number of rooms.")
}
// 打印 "Unable to retrieve the number of rooms."
这会告诉 Swift 去链接可选的residence 属性,如果它存在就返回 numberOfRooms 的值。
由于尝试访问 numberOfRooms 有可能失败, 可选链接尝试放回一个 Int? 类型值, 或者“可选整型”。当 residence 为nil, 这个可选整型将会是nil, 这样就不能访问 numberOfRooms. 可选整型通过可选绑定来访问,然后去拆包这个整数同时把非空值赋值给roomCount 变量。
注意,尽管numberOfRooms 是一个非空的整型,这个也是真的。通过可选链接查询的事实意味着调用numberOfRooms 总是返回 Int? 而不是 Int.
你可以把一个 Residence 实例赋值给john.residence, 这样它就不会是一个空值了:
john.residence = Residence()
john.residence 现在有了一个实际的 Residence 实例, 而不是nil. 如果你用之前一样的可选链接访问 numberOfRooms, 它将放回 Int? , 包含一个默认值为1的 numberOfRooms:
if let roomCount = john.residence?.numberOfRooms {
print("John's residence has \(roomCount) room(s).")
} else {
print("Unable to retrieve the number of rooms.")
}
// 打印 "John's residence has 1 room(s)."
为可选链接定义模型类
你可以用可选链接调用多个层次的属性,方法或者下标。这使得你可以在复合相关类型内,向下获取子属性。然后判断是否可以在这些子属性上访问属性,方法或者下标。
下面的代码片段定义了四个模型类,用于下面的例子。包括多层次可选链接的例子。这些类在 the Person 和 Residence 模型上展开, 加入了两个类 Room 和 Address, 带有对应的属性,方法和下标。
Person 类定义和以前一样:
class Person {
var residence: Residence?
}
Residence 类比以前复杂。这次, Residence 类定义了一个变量属性rooms, 用一个空数组[Room]初始化:
class Residence {
var rooms = [Room]()
var numberOfRooms: Int {
return rooms.count
}
subscript(i: Int) -> Room {
get {
return rooms[i]
}
set {
rooms[i] = newValue
}
}
func printNumberOfRooms() {
print("The number of rooms is \(numberOfRooms)")
}
var address: Address?
}
因为这个版本的 Residence 存储了一组 Room 实例, 它的numberOfRooms 属性实现成一个计算属性, 而不是一个存储属性。numberOfRooms 属性简单返回了rooms 数组的个数。
作为访问romms 数组的简写, 这个版本的 Residence 提供了一个读写下标,来访问rooms数组指定索引下的房间。
这个版本的 Residence 同时提供了一个方法printNumberOfRooms, 它只是简单打印住宅中的房间数。
最后, Residence 定义了一个可选属性 address, 类型是 Address?. Address 类下面定义。
用于rooms数组的Room类是带有一个属性name的简单类, 还有一个构造器,用来设置这个房间的名字:
class Room {
let name: String
init(name: String) { self.name = name }
}
模型里最后一个类是 Address. 这个类有三个类型为String?的可选属性。前面两个属性, buildingName 和 buildingNumber, 是两种方案来识别某个建筑的地址部分。第三个属性 street, 用来命名地址里的街道:
class Address {
var buildingName: String?
var buildingNumber: String?
var street: String?
func buildingIdentifier() -> String? {
if buildingNumber != nil && street != nil {
return "\(buildingNumber) \(street)"
} else if buildingName != nil {
return buildingName
} else {
return nil
}
}
}
Address 类同时提供了一个方法 buildingIdentifier(), 它有一个String?类型的返回值。这个方法判断地址的属性,如果有值就返回 buildingName, 如果buildingNumber 和 street 都有值,就返回两者的连接, 否则返回nil.
通过可选链接访问属性
可选链接中描述它可以作为强制拆包的替代方案, 你可以在一个可选值上使用可选链接来访问一个属性, 然后判断属性访问是否成功。
使用上面定义的类创建一个新的 Person 实例, 然后像以前一样访问它的属性numberOfRooms:
let john = Person()
if let roomCount = john.residence?.numberOfRooms {
print("John's residence has \(roomCount) room(s).")
} else {
print("Unable to retrieve the number of rooms.")
}
// 打印 "Unable to retrieve the number of rooms."
因为 john.residence 为nil, 可选链接像以前一样会调用失败。
你也可以通过可选链接给属性设置一个值:
let someAddress = Address()
someAddress.buildingNumber = "29"
someAddress.street = "Acacia Road"
john.residence?.address = someAddress
在这个例子里, 尝试设置john.residence的属性 address 将会失败, 因为 john.residence 现在是nil.
赋值是可选链接的一部分, 意味者=运算符的右侧代码不会执行。在上一个例子, 容易看到 someAddress 不会执行, 因为访问一个常量不会有任何边界影响。下面的代码做了同样的赋值, 不过它使用一个函数去创建地址。在返回值前函数会打印 “Function was called”, 这个会让你看到=运算符右侧代码是否执行了。
func createAddress() -> Address {
print("Function was called.")
let someAddress = Address()
someAddress.buildingNumber = "29"
someAddress.street = "Acacia Road"
return someAddress
}
john.residence?.address = createAddress()
你可以看到createAddress() 函数不会执行, 因为什么也没有打印。
130.通过可选链接调用方法
你可以在一个可选值上通过可选链接调用一个方法, 然后判断调用是否成功。即使这个方法没有定义返回值,你也可以这么做。
Residence 类的 printNumberOfRooms() 方法打印numberOfRooms的当前值。这里是方法的样子:
func printNumberOfRooms() {
print("The number of rooms is \(numberOfRooms)")
}
这个方法没有指定返回值。不过, 没有返回值的方法和函数有一个隐式的返回值Void. 这就说它们返回了(), 或者说一个空元组。
如果你使用可选链接,在一个可选值上调用这个方法。这个方法的返回类型会是 Void?, 而不是Void, 因为通过可选链接调用总是返回一个可选值。这让你可以使用if语句来判断能否调用printNumberOfRooms() 方法, 尽管这个方法本身没有定义返回值。把printNumberOfRooms 返回值和 nil 比较来查看方法调用是否成功:
if john.residence?.printNumberOfRooms() != nil {
print("It was possible to print the number of rooms.")
} else {
print("It was not possible to print the number of rooms.")
}
// 打印 "It was not possible to print the number of rooms."
如果你尝试通过可选链接设置属性也是一样。上面的例子尝试为 john.residence 设置地址值, 尽管residence 属性是nil. 通过可选链接设置属性值都会返回一个Void?, 让你可以跟nil比较,来判断设置属性是否成功:
if (john.residence?.address = someAddress) != nil {
print("It was possible to set the address.")
} else {
print("It was not possible to set the address.")
}
// 打印 "It was not possible to set the address."
通过可选链接访问下标
你可以在可选值上使用可选链接来获取或者设置一个下标值, 然后判断下标调用是否成功。
备注:当你通过可选链接访问可选值的下标时, 你要把问号放置在下标括号的前面而不是后面。这个问号总是直接跟在可选表达式的后面。
下面的例子尝试获取john.residence的房子数组中第一个房子的名字,使用的是定义在Residence类里的下标。因为 john.residence 现在是 nil, 下标调用会失败:
if let firstRoomName = john.residence?[0].name {
print("The first room name is \(firstRoomName).")
} else {
print("Unable to retrieve the first room name.")
}
// 打印 "Unable to retrieve the first room name."
下标调用中的可选链接问号直接放在john.residence后面, 在下标括号前面, 因为 john.residence 是可选值。
相似的, 你可以使用可选链接,通过下标来设置新值:
john.residence?[0] = Room(name: "Bathroom")
这个下标设置的尝试也会失败, 因为residence 当前值为nil. 如果你创建一个实际的 Residence 实例并把它赋值给 john.residence, 同时带有一个或者多个 Room 实例存于rooms 数组, 你可以通过可选链接,使用 Residence 下标访问rooms数组中的实际项:
let johnsHouse = Residence()
johnsHouse.rooms.append(Room(name: "Living Room"))
johnsHouse.rooms.append(Room(name: "Kitchen"))
john.residence = johnsHouse
if let firstRoomName = john.residence?[0].name {
print("The first room name is \(firstRoomName).")
} else {
print("Unable to retrieve the first room name.")
}
// 打印 "The first room name is Living Room."
访问可选类型下标
如果一个下标返回一个可选类型值—比如Swift 字典类型的key下标—在下标方括号后面加上问号来链接它的可选返回值:
var testScores = ["Dave": [86, 82, 84], "Bev": [79, 94, 81]]
testScores["Dave"]?[0] = 91
testScores["Bev"]?[0] += 1
testScores["Brian"]?[0] = 72
// the "Dave" array is now [91, 82, 84] and the "Bev" array is now [80, 94, 81]
上面的例子定义了一个字典 testScores, 它包含了两个键值对,把一个字符串键映射到一个整数数组。这个例子用可选链接把Dave数组的第一项设置成 91; 把Bev数组的第一项值增加1; 并且尝试为键为Brian的数组设置第一项的值。前两个调用成功, 因为testScores 字典包含 “Dave” 和 “Bev”. 第三个调用会失败, 因为 testScores 字典并不包含 “Brian” 键。
多层链接
你可以使用多层链接来访问模型的属性,方法和下标。不过, 多层链接不会给返回值添加更多可选性。
换句话说:
如果你获取的类型不是可选的, 由于可选链接,它会变成可选类型。
如果你获取的类型已经是可选的了, 它不会因为可选链接而变的更可选。
因此:
如果你通过可选链接去获取一个Int值, 会返回一个Int?, 不管使用了多少层的链接。
类似的, 如果你使用可选链接去获取一个 Int?值, 会返回一个Int?, 不管使用了多少层的连接。
下面的例子尝试访问john的属性residence 的属性address 的属性street. 这里用了两层可选链接, 来链接 residence 和 address 属性, 它们都是可选类型:
if let johnsStreet = john.residence?.address?.street {
print("John's street name is \(johnsStreet).")
} else {
print("Unable to retrieve the address.")
}
// 打印 "Unable to retrieve the address."
john.residence 的值现在有一个有效的 Residence 实例。不过, john.residence.address 的值现在是 nil. 所以, john.residence?.address?.street 调用失败。
注意在上面的例子里, 你尝试获取street属性的值。这个属性的类型是 String?. 因此 john.residence?.address?.street 也是 String? 类型, 尽管使用了两层可选链接。
如果你为john.residence?.address?设置一个实际的地址, 并且为address的属性street设置一个实际值, 你就可以使用多层链接访问street属性的值了:
let johnsAddress = Address()
johnsAddress.buildingName = "The Larches"
johnsAddress.street = "Laurel Street"
john.residence?.address = johnsAddress
if let johnsStreet = john.residence?.address?.street {
print("John's street name is \(johnsStreet).")
} else {
print("Unable to retrieve the address.")
}
// 打印 "John's street name is Laurel Street."
在这个例子里, 尝试设置john.residence的address属性会成功, 因为john.residence现在包含了一个有效的 Residence 实例。
使用可选返回值链接方法
前面的例子展示如何通过可选链接获取一个可选类型的属性值。你也可以使用可选链接,调用返回值是可选类型的方法, 如果需要可以链接方法的返回值。
下面的例子使用可选链接调用 Address 类的 buildingIdentifier() 方法。这个方法返回值类型是 String?. 就像上面描述的, 可选链接最后的返回类型也是 String?:
if let buildingIdentifier = john.residence?.address?.buildingIdentifier() {
print("John's building identifier is \(buildingIdentifier).")
}
// 打印 "John's building identifier is The Larches."
如果你想执行更深的操作链接方法的返回值, 就在方法括号的后面放上可选链接的问号:
if let beginsWithThe =
john.residence?.address?.buildingIdentifier()?.hasPrefix("The") {
if beginsWithThe {
print("John's building identifier begins with \"The\".")
} else {
print("John's building identifier does not begin with \"The\".")
}
}
// 打印 "John's building identifier begins with "The"."
备注:在上面的例子里, 你在括号后面放上了可选链接问号, 因为你链接的可选值是 buildingIdentifier() 方法的返回值, 并不是buildingIdentifier() 方法本身。
错误处理
错误处理是在你的程序中响应错误条件并恢复的过程。Swift 为在运行时抛出,捕获,传递和恢复错误提供了一级支持。
有些操作不能保证总能顺利执行或者产生有用的输出。可选用来代表缺值, 但是当一个操作失败了, 知道失败怎么产生的通常很有用, 所以你的代码可以相应作出响应。
例如, 考虑从磁盘读取和处理数据的任务。这个任务有很多方式失败, 包括指定位置文件不存在, 文件没有读的权限, 或者文件不能以兼容的格式进行编码。这些不同场景的区分允许程序去解决一些错误,和用户沟通它不能解决的任何错误。
表示和抛出错误
在 Swift里, 错误用符合错误协议的类型值表示。这个空协议表明一个类型可以用来错误处理。
Swift 枚举类型特别适合用来模拟一组相关的错误条件, 带有对应的值来传达错误的额外信息。例如, 这里有一组,在一个游戏中操作自动售货机的错误条件:
enum VendingMachineError: Error {
case invalidSelection
case insufficientFunds(coinsNeeded: Int)
case outOfStock
}
抛出一个错误,让你表明异常发生了,正常执行语句不能继续了。例如, 下面的代码抛出一个错误,表明自动售货机需要五个以上的硬币:
throw VendingMachineError.insufficientFunds(coinsNeeded: 5)
处理错误
当一个错误抛出来后, 一些包围的代码要负责处理这个错误—比如, 通过修正这个问题, 尝试替代方法或者通知用户执行失败。
Swift 里有四种方法来处理错误。你可以把错误从函数传递到调用函数的代码里;使用一个 do-catch 语句来处理错误,;把错误当做一个可选值来处理;或者断言这个错误不会发生。
当一个函数抛出一个错误时, 它改变了你的程序流, 所以快速判断抛出错误代码的位置很重要。为了在你的代码里判断这些位置,在调用会抛出错误的函数,方法或者构造器的代码前写上try 关键字—或者它的变体 try? 或者 try!.
备注:Swift 的错误处理像其他语言的异常处理, 也是使用try, catch 和throw 关键字。给很多语言不同的是—包括 Objective-C— Swift 的错误处理不涉及解除调用堆栈, 这个过程花费太多的计算。像这样, 抛出异常语句的性能可以媲美这些返回语句。
用抛出函数传递错误
为了表示一个函数,方法或者构造器可以抛出错误, 在函数定义里,把throws 关键字写在函数括号的后面。用throws 标记的函数就是一个抛出函数。如果函数指定了一个返回值, 把 throws 关键字写在返回箭头之前 (->).
func canThrowErrors() throws -> String
func cannotThrowErrors() -> String
一个抛出函数把错误传递到它被调用的地方。
备注
只有抛出函数可以传递错误。非抛出函数内部的错误只能在函数内部处理。
在下面的代码里, VendingMachine 类有一个方法 vend(itemNamed:),如果请求项目不合理,它会抛出一个对应的VendingMachineError, 超出存货, 或者有一笔消费超过了当前的存量:
struct Item {
var price: Int
var count: Int
}
class VendingMachine {
var inventory = [
"Candy Bar": Item(price: 12, count: 7),
"Chips": Item(price: 10, count: 4),
"Pretzels": Item(price: 7, count: 11)
]
var coinsDeposited = 0
func vend(itemNamed name: String) throws {
guard let item = inventory[name] else {
throw VendingMachineError.invalidSelection
}
guard item.count > 0 else {
throw VendingMachineError.outOfStock
}
guard item.price <= coinsdeposited="" else="" {="" throw="" vendingmachineerror.insufficientfunds(coinsneeded:="" item.price="" -="" coinsdeposited)="" }="" var="" newitem="item" newitem.count="" inventory[name]="newItem" print("dispensing="" \(name)")="" <="" code="">
vend(itemNamed:) 方法使用 guard 语句,如果任何购买小吃的要求不能满足,就尽早退出和抛出错误。因为一个throw 语句会立即转移程序控制, 只有所有的要求都满足才会出售货品。
因为 vend(itemNamed:) 方法会传递所有抛出的错误, 任何调用这个方法的代码要么处理这个错误—用 do-catch 语句, try?, 或者 try!—要么继续传递这些错误。例如, 这个例子里的buyFavoriteSnack(person:vendingMachine:) 方法也是一个抛出函数, vend(itemNamed:) 方法抛出的错误会传递到 buyFavoriteSnack(person:vendingMachine:) 函数被调用的地方。
let favoriteSnacks = [
"Alice": "Chips",
"Bob": "Licorice",
"Eve": "Pretzels",
]
func buyFavoriteSnack(person: String, vendingMachine: VendingMachine) throws {
let snackName = favoriteSnacks[person] ?? "Candy Bar"
try vendingMachine.vend(itemNamed: snackName)
}
在这个例子里,buyFavoriteSnack(person: vendingMachine:) 函数查询给定客户最喜欢的小吃,然后调用vend(itemNamed:)方法来购买。由于 vend(itemNamed:) 方法可以抛出错误, 所以在它前面加上try关键字来调用。
抛出构造器和抛出函数一样可以传递错误。例如, 下面PurchasedSnack 结构体的构造器调用了一个抛出函数作为构造过程的一部分, 通过传递错误给调用者,它处理任何它遇见的错误。
struct PurchasedSnack {
let name: String
init(name: String, vendingMachine: VendingMachine) throws {
try vendingMachine.vend(itemNamed: name)
self.name = name
}
}
131.使用 Do-Catch 处理错误
你可以用一个 do-catch 语句来处理错误。如果一个错误被do字句的代码抛出来, 它匹配 catch 字句来决定哪一个来处理这个错误。
这里是 do-catch 语句的一般样式:
do {
try expression
statements
} catch pattern 1 {
statements
} catch pattern 2 where condition {
statements
}
在catch后面写上模式来表明这个字句可以处理什么错误。如果一个 catch 字句没有一个模式, 字句就会匹配所有的错误并且把错误绑定到一个本地的常量 error. 更多模式匹配,参见模式。
catch 字句并不需要处理所有do字句抛出来的错误。如果没有 catch 字句处理这个错误, 这个错误就会传递到周围代码。不过, 这个错误必须被周围代码处理—或者被一个封闭的 do-catch 字句或者在一个抛出函数内部处理。例如, 下面的代码处理了VendingMachineError 列举的所有三种情况, 但是所有其他的错我必须被周围代码处理:
var vendingMachine = VendingMachine()
vendingMachine.coinsDeposited = 8
do {
try buyFavoriteSnack(person: "Alice", vendingMachine: vendingMachine)
} catch VendingMachineError.invalidSelection {
print("Invalid Selection.")
} catch VendingMachineError.outOfStock {
print("Out of Stock.")
} catch VendingMachineError.insufficientFunds(let coinsNeeded) {
print("Insufficient funds. Please insert an additional \(coinsNeeded) coins.")
}
// 打印 "Insufficient funds. Please insert an additional 2 coins."
在上面的例子里, buyFavoriteSnack(person:vendingMachine:) 在一个try表达式里调用, 因为它可以抛出一个错误。如果一个错误抛出了, 语句执行立即转到 catch 字句, 它觉得是否允许传递继续。如果没有错误抛出来, do 语句剩下的语句会继续执行。