Swift Tour Learn (九) -- Swift 語法(自動引用計數、可選鏈、錯誤處理)

本章將會介紹

自動引用計數的工作機制
自動引用計數實踐
類實例之間的循環強引用
解決實例之間的循環強引用
閉包引起的循環強引用
解決閉包引起的循環強引用
使用可選鏈式調用代替強制展開
為可選鏈式調用定義模型類
通過可選鏈式調用訪問屬性
通過可選鏈式調用調用方法
通過可選鏈式調用訪問下標
連接多層可選鏈式調用
在方法的可選返回值上進行可選鏈式調用
表示并拋出錯誤
處理錯誤
指定清理操作

自動引用計數

Swift 使用自動引用計數(ARC)機制來跟蹤和管理你的應用程序的內存。通常情況下,Swift 內存管理機制會一直起作用,你無須自己來考慮內存的管理。ARC 會在類的實例不再被使用時,自動釋放其占用的內存。

然而在少數情況下,為了能幫助你管理內存,ARC 需要更多的,代碼之間關系的信息。本章描述了這些情況,并且為你示范怎樣才能使 ARC 來管理你的應用程序的所有內存。在 Swift 使用 ARC 與在 Obejctive-C 中使用 ARC 非常類似。

注意
引用計數僅僅應用于類的實例。結構體和枚舉類型是值類型,不是引用類型,也不是通過引用的方式存儲和傳遞。

1.自動引用計數的工作機制

當你每次創建一個類的新的實例的時候,ARC 會分配一塊內存來儲存該實例信息。內存中會包含實例的類型信息,以及這個實例所有相關的存儲型屬性的值。

此外,當實例不再被使用時,ARC 釋放實例所占用的內存,并讓釋放的內存能挪作他用。這確保了不再被使用的實例,不會一直占用內存空間。

然而,當 ARC 收回和釋放了正在被使用中的實例,該實例的屬性和方法將不能再被訪問和調用。實際上,如果你試圖訪問這個實例,你的應用程序很可能會崩潰。

為了確保使用中的實例不會被銷毀,ARC 會跟蹤和計算每一個實例正在被多少屬性,常量和變量所引用。哪怕實例的引用數為1,ARC都不會銷毀這個實例。

為了使上述成為可能,無論你將實例賦值給屬性、常量或變量,它們都會創建此實例的強引用。之所以稱之為“強”引用,是因為它會將實例牢牢地保持住,只要強引用還在,實例是不允許被銷毀的。

2.自動引用計數實踐

下面的例子展示了自動引用計數的工作機制。例子以一個簡單的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”

應當注意到當你調用Person類的構造函數的時候,“John Appleseed is being initialized”會被打印出來。由此可以確定構造函數被執行。

由于Person類的新實例被賦值給了reference1變量,所以reference1到Person類的新實例之間建立了一個強引用。正是因為這一個強引用,ARC 會保證Person實例被保持在內存中不被銷毀。

如果你將同一個Person實例也賦值給其他兩個變量,該實例又會多出兩個強引用:

reference2 = reference1
reference3 = reference1

現在這一個Person實例已經有三個強引用了。

如果你通過給其中兩個變量賦值nil的方式斷開兩個強引用(包括最先的那個強引用),只留下一個強引用,Person實例不會被銷毀:

reference1 = nil
reference2 = nil

在你清楚地表明不再使用這個Person實例時,即第三個也就是最后一個強引用被斷開時,ARC 會銷毀它:

reference3 = nil
// 打印 “John Appleseed is being deinitialized”
3.類實例之間的循環強引用

在上面的例子中,ARC 會跟蹤你所新創建的Person實例的引用數量,并且會在Person實例不再被需要時銷毀它。

然而,我們可能會寫出一個類實例的強引用數永遠不能變成0的代碼。如果兩個類實例互相持有對方的強引用,因而每個實例都讓對方一直存在,就是這種情況。這就是所謂的循環強引用。

你可以通過定義類之間的關系為弱引用或無主引用,以替代強引用,從而解決循環強引用的問題。具體的過程在解決類實例之間的循環強引用中有描述。不管怎樣,在你學習怎樣解決循環強引用之前,很有必要了解一下它是怎樣產生的。

下面展示了一個不經意產生循環強引用的例子。例子定義了兩個類: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實例有一個類型為String,名字為name的屬性,并有一個可選的初始化為nil的apartment屬性。apartment屬性是可選的,因為一個人并不總是擁有公寓。

類似的,每個Apartment實例有一個叫unit,類型為String的屬性,并有一個可選的初始化為nil的tenant屬性。tenant屬性是可選的,因為一棟公寓并不總是有居民。

這兩個類都定義了析構函數,用以在類實例被析構的時候輸出信息。這讓你能夠知曉Person和Apartment的實例是否像預期的那樣被銷毀。

接下來的代碼片段定義了兩個可選類型的變量john和unit4A,并分別被設定為下面的Apartment和Person的實例。這兩個變量都被初始化為nil,這正是可選類型的優點:

var john: Person?
var unit4A: Apartment?

現在你可以創建特定的Person和Apartment實例并將賦值給john和unit4A變量:

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后,強引用關系如下圖:

Person和Apartment實例之間的強引用關系保留了下來并且不會被斷開。

4.解決實例之間的循環強引用

Swift 提供了兩種辦法用來解決你在使用類的屬性時所遇到的循環強引用問題:弱引用(weak reference)和無主引用(unowned reference)。

弱引用和無主引用允許循環引用中的一個實例引用而另外一個實例不保持強引用。這樣實例能夠互相引用而不產生循環強引用。

當其他的實例有更短的生命周期時,使用弱引用,也就是說,當其他實例析構在先時。在上面公寓的例子中,很顯然一個公寓在它的生命周期內會在某個時間段沒有它的主人,所以一個弱引用就加在公寓類里面,避免循環引用。相比之下,當其他實例有相同的或者更長生命周期時,請使用無主引用。

  • 弱引用

弱引用不會對其引用的實例保持強引用,因而不會阻止 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變量所保持的強引用時,再也沒有指向Person實例的強引用了:

由于再也沒有指向Person實例的強引用,該實例會被銷毀:

john = nil
// 打印 “John Appleseed is being deinitialized”

唯一剩下的指向Apartment實例的強引用來自于變量unit4A。如果你斷開這個強引用,再也沒有指向Apartment實例的強引用了:

由于再也沒有指向Apartment實例的強引用,該實例也會被銷毀:

unit4A = nil
// 打印 “Apartment 4A is being deinitialized”

上面的兩段代碼展示了變量john和unit4A在被賦值為nil后,Person實例和Apartment實例的析構函數都打印出“銷毀”的信息。這證明了引用循環被打破了。

注意
在使用垃圾收集的系統里,弱指針有時用來實現簡單的緩沖機制,因為沒有強引用的對象只會在內存壓力觸發垃圾收集時才被銷毀。但是在 ARC 中,一旦值的最后一個強引用被移除,就會被立即銷毀,這導致弱引用并不適合上面的用途。

  • 無主引用

和弱引用類似,無主引用不會牢牢保持住引用的實例。和弱引用不同的是,無主引用在其他實例有相同或者更長的生命周期時使用。你可以在聲明屬性或者變量時,在前面加上關鍵字unowned表示這是一個無主引用。

無主引用通常都被期望擁有值。不過 ARC 無法在實例被銷毀后將無主引用設為nil,因為非可選類型的變量不允許被賦值為nil。

重要
使用無主引用,你必須確保引用始終指向一個未銷毀的實例。
如果你試圖在實例被銷毀后,訪問該實例的無主引用,會觸發運行時錯誤。

下面的例子定義了兩個類,Customer和CreditCard,模擬了銀行客戶和客戶的信用卡。這兩個類中,每一個都將另外一個類的實例作為自身的屬性。這種關系可能會造成循環強引用。

Customer和CreditCard之間的關系與前面弱引用例子中Apartment和Person的關系略微不同。在這個數據模型中,一個客戶可能有或者沒有信用卡,但是一張信用卡總是關聯著一個客戶。為了表示這種關系,Customer類有一個可選類型的card屬性,但是CreditCard類有一個非可選類型的customer屬性。

此外,只能通過將一個number值和customer實例傳遞給CreditCard構造函數的方式來創建CreditCard實例。這樣可以確保當創建CreditCard實例時總是有一個customer實例與之關聯。

由于信用卡總是關聯著一個客戶,因此將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 位的卡號。

下面的代碼片段定義了一個叫john的可選類型Customer變量,用來保存某個特定客戶的引用。由于是可選類型,所以變量被初始化為nil:

var john: Customer?

現在你可以創建Customer類的實例,用它初始化CreditCard實例,并將新創建的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”

最后的代碼展示了在john變量被設為nil后Customer實例和CreditCard實例的構造函數都打印出了“銷毀”的信息。

注意
上面的例子展示了如何使用安全的無主引用。對于需要禁用運行時的安全檢查的情況(例如,出于性能方面的原因),Swift還提供了不安全的無主引用。與所有不安全的操作一樣,你需要負責檢查代碼以確保其安全性。 你可以通過unowned(unsafe)來聲明不安全無主引用。如果你試圖在實例被銷毀后,訪問該實例的不安全無主引用,你的程序會嘗試訪問該實例之前所在的內存地址,這是一個不安全的操作。

  • 無主引用以及隱式解析可選屬性

上面弱引用和無主引用的例子涵蓋了兩種常用的需要打破循環強引用的場景。

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屬性。

Country的構造函數調用了City的構造函數。然而,只有Country的實例完全初始化后,Country的構造函數才能把self傳給City的構造函數。在兩段式構造過程中有具體描述。

為了滿足這種需求,通過在類型結尾處加上感嘆號(City!)的方式,將Country的capitalCity屬性聲明為隱式解析可選類型的屬性。這意味著像其他可選類型一樣,capitalCity屬性的默認值為nil,但是不需要展開它的值就能訪問它。在隱式解析可選類型中有描述。

由于capitalCity默認值為nil,一旦Country的實例在構造函數中給name屬性賦值后,整個初始化過程就完成了。這意味著一旦name屬性被賦值后,Country的構造函數就能引用并傳遞隱式的self。Country的構造函數在賦值capitalCity時,就能將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屬性在初始化完成后,能像非可選值一樣使用和存取,同時還避免了循環強引用。

5.閉包引起的循環強引用

前面我們看到了循環強引用是在兩個類實例屬性互相保持對方的強引用時產生的,還知道了如何用弱引用和無主引用來打破這些循環強引用。

循環強引用還會發生在當你將一個閉包賦值給類實例的某個屬性,并且這個閉包體中又使用了這個類實例時。這個閉包體中可能訪問了實例的某個屬性,例如self.someProperty,或者閉包中調用了實例的某個方法,例如self.someMethod()。這兩種情況都導致了閉包“捕獲”self,從而產生了循環強引用。

循環強引用的產生,是因為閉包和類相似,都是引用類型。當你把一個閉包賦值給某個屬性時,你是將這個閉包的引用賦值給了屬性。實質上,這跟之前的問題是一樣的——兩個強引用讓彼此一直有效。但是,和兩個類實例不同,這次一個是類實例,另一個是閉包。

Swift 提供了一種優雅的方法來解決這個問題,稱之為閉包捕獲列表(closure capture list)。同樣的,在學習如何用閉包捕獲列表打破循環強引用之前,先來了解一下這里的循環強引用是如何產生的,這對我們很有幫助。

下面的例子為你展示了當一個閉包引用了self后是如何產生一個循環強引用的。例子中定義了一個叫HTMLElement的類,用一種簡單的模型表示 HTML 文檔中的一個單獨的元素:

class HTMLElement {

    let name: String
    let text: String?

    lazy var asHTML: (Void) -> String = {
        if let text = self.text {
            return "<\(self.name)>\(text)</\(self.name)>"
        } 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,用來設置 HTML 元素呈現的文本。

除了上面的兩個屬性,HTMLElement還定義了一個lazy屬性asHTML。這個屬性引用了一個將name和text組合成 HTML 字符串片段的閉包。該屬性是Void -> String類型,或者可以理解為“一個沒有參數,返回String的函數”。

默認情況下,閉包賦值給了asHTML屬性,這個閉包返回一個代表 HTML 標簽的字符串。如果text值存在,該標簽就包含可選值text;如果text不存在,該標簽就不包含文本。對于段落元素,根據text是“some text”還是nil,閉包會返回"<p>some text</p>"或者"<p />"。

可以像實例方法那樣去命名、使用asHTML屬性。然而,由于asHTML是閉包而不是實例方法,如果你想改變特定 HTML 元素的處理方式的話,可以用自定義的閉包來取代默認值。

例如,可以將一個閉包賦值給asHTML屬性,這個閉包能在text屬性是nil時使用默認文本,這是為了避免返回一個空的 HTML 標簽:

let heading = HTMLElement(name: "h1")
let defaultText = "some default text"
heading.asHTML = {
    return "<\(heading.name)>\(heading.text ?? defaultText)</\(heading.name)>"
}
print(heading.asHTML())
// 打印 “<h1>some default text</h1>”

注意
asHTML聲明為lazy屬性,因為只有當元素確實需要被處理為 HTML 輸出的字符串時,才需要使用asHTML。也就是說,在默認的閉包中可以使用self,因為只有當初始化完成以及self確實存在后,才能訪問lazy屬性。

HTMLElement類只提供了一個構造函數,通過name和text(如果有的話)參數來初始化一個新元素。該類也定義了一個析構函數,當HTMLElement實例被銷毀時,打印一條消息。

下面的代碼展示了如何用HTMLElement類創建實例并打印消息:

var paragraph: HTMLElement? = HTMLElement(name: "p", text: "hello, world")
print(paragraph!.asHTML())
// 打印 “<p>hello, world</p>”

注意
上面的paragraph變量定義為可選類型的HTMLElement,因此我們可以賦值nil給它來演示循環強引用。

不幸的是,上面寫的HTMLElement類產生了類實例和作為asHTML默認值的閉包之間的循環強引用。循環強引用如下圖所示:

實例的asHTML屬性持有閉包的強引用。但是,閉包在其閉包體內使用了self(引用了self.name和self.text),因此閉包捕獲了self,這意味著閉包又反過來持有了HTMLElement實例的強引用。這樣兩個對象就產生了循環強引用。

注意
雖然閉包多次使用了self,它只捕獲HTMLElement實例的一個強引用。

如果設置paragraph變量為nil,打破它持有的HTMLElement實例的強引用,HTMLElement實例和它的閉包都不會被銷毀,也是因為循環強引用:

paragraph = nil

注意,HTMLElement的析構函數中的消息并沒有被打印,證明了HTMLElement實例并沒有被銷毀。

6.解決閉包引起的循環強引用

在定義閉包時同時定義捕獲列表作為閉包的一部分,通過這種方式可以解決閉包和類實例之間的循環強引用。捕獲列表定義了閉包體內捕獲一個或者多個引用類型的規則。跟解決兩個類實例間的循環強引用一樣,聲明每個捕獲的引用為弱引用或無主引用,而不是強引用。應當根據代碼關系來決定使用弱引用還是無主引用。

注意
Swift 有如下要求:只要在閉包內使用self的成員,就要用self.someProperty或者self.someMethod()(而不只是someProperty或someMethod())。這提醒你可能會一不小心就捕獲了self。

  • 定義捕獲列表

捕獲列表中的每一項都由一對元素組成,一個元素是weak或unowned關鍵字,另一個元素是類實例的引用(例如self)或初始化過的變量(如delegate = self.delegate!)。這些項在方括號中用逗號分開。

如果閉包有參數列表和返回類型,把捕獲列表放在它們前面:

lazy var someClosure: (Int, String) -> String = {
    [unowned self, weak delegate = self.delegate!] (index: Int, stringToProcess: String) -> String in
    // 這里是閉包的函數體
}

如果閉包沒有指明參數列表或者返回類型,即它們會通過上下文推斷,那么可以把捕獲列表和關鍵字in放在閉包最開始的地方:

lazy var someClosure: Void -> String = {
    [unowned self, weak delegate = self.delegate!] in
    // 這里是閉包的函數體
}
  • 弱引用和無主引用

在閉包和捕獲的實例總是互相引用并且總是同時銷毀時,將閉包內的捕獲定義為無主引用。

相反的,在被捕獲的引用可能會變為nil時,將閉包內的捕獲定義為弱引用。弱引用總是可選類型,并且當引用的實例被銷毀后,弱引用的值會自動置為nil。這使我們可以在閉包體內檢查它們是否存在。

注意
如果被捕獲的引用絕對不會變為nil,應該用無主引用,而不是弱引用。

前面的HTMLElement例子中,無主引用是正確的解決循環強引用的方法。這樣編寫HTMLElement類來避免循環強引用:

class HTMLElement {

    let name: String
    let text: String?

    lazy var asHTML: (Void) -> String = {
        [unowned self] in
        if let text = self.text {
            return "<\(self.name)>\(text)</\(self.name)>"
        } else {
            return "<\(self.name) />"
        }
    }

    init(name: String, text: String? = nil) {
        self.name = name
        self.text = text
    }

    deinit {
        print("\(name) is being deinitialized")
    }
}

上面的HTMLElement實現和之前的實現一致,除了在asHTML閉包中多了一個捕獲列表。這里,捕獲列表是[unowned self],表示“將self捕獲為無主引用而不是強引用”。

和之前一樣,我們可以創建并打印HTMLElement實例:

var paragraph: HTMLElement? = HTMLElement(name: "p", text: "hello, world")
print(paragraph!.asHTML())
// 打印 “<p>hello, world</p>”

使用捕獲列表后引用關系如下圖所示

這一次,閉包以無主引用的形式捕獲self,并不會持有HTMLElement實例的強引用。如果將paragraph賦值為nil,HTMLElement實例將會被銷毀,并能看到它的析構函數打印出的消息:

paragraph = nil
// 打印 “p is being deinitialized”
7.自動引用計數總結
自動引用計數

// 示例
class Person0 {
    let name: String
    init(name: String) {
        self.name = name
        print("\(name) is being initialized")
    }
    deinit {
        print("\(name) is being deinitialized")
    }
}

var reference1: Person0?
var reference2: Person0?
var reference3: Person0?
reference1 = Person0(name: "John")
reference2 = reference1
reference3 = reference1
// 現在Person實例有三個強引用了

reference1 = nil // 引用實例變成兩個
reference2 = nil // 引用實例變成1個
reference3 = nil // 引用實例沒有了 調用析構函數

// 類實例之間的循環強引用以及解決方法
// 弱引用 Person和Apartment的例子展示了兩個屬性的值都允許為nil,并會潛在的產生循環強引用。這種場景最適合用弱引用來解決。
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")
    }
}

var john: Person?
var unit4A: Apartment?
john = Person(name: "John Appleased")
unit4A = Apartment(unit: "4A")

john!.apartment = unit4A
unit4A!.tenant = john

john = nil
unit4A = nil

// 無主引用 Customer和CreditCard的例子展示了一個屬性的值允許為nil,而另一個屬性的值不允許為nil,這也可能會產生循環強引用。這種場景最適合通過無主引用來解決。
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 deinitilized")
    }
}

var henry: Customer?
henry = Customer(name: "Henry Appleased")
henry!.card = CreditCard(number: 1234_5678_9087_3456, customer: henry!)

henry = nil

// 無主引用以及隱式解析可選屬性 兩個屬性都必須有值,并且初始化完成后永遠不會為nil 在這種場景中,需要一個類使用無主屬性,而另外一個類使用隱式解析可選屬性。
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
    }
}

var country = Country(name: "Canada", capitalName: "Ottawa")
print("\(country.name)'s capital city is called \(country.capitalCity.name)")


// 閉包引起的循環強引用
class HTMLElement {

    let name: String
    let text: String?

    lazy var asHTML: (Void) -> String = {
        [unowned self] in
        if let text = self.text {
            return "<\(self.name)>\(text)</\(self.name)>"
        } else {
            return "<\(self.name) />"
        }
    }

    init(name: String, text: String? = nil) {
        self.name = name
        self.text = text
    }

    deinit {
        print("hahahah \(name) is being deinitialized")
    }
}
let heading = HTMLElement(name: "h1")
let defaultText = "some default text"
heading.asHTML = {
    return "<\(heading.name)>\(heading.text ?? defaultText)</\(heading.name)>"
}
print(heading.asHTML())

var paragraph: HTMLElement? = HTMLElement(name: "p", text: "hello, world")
print(paragraph!.asHTML())
// 打印 “<p>hello, world</p>”

paragraph = nil


可選鏈

可選鏈式調用是一種可以在當前值可能為nil的可選值上請求和調用屬性、方法及下標的方法。如果可選值有值,那么調用就會成功;如果可選值是nil,那么調用將返回nil。多個調用可以連接在一起形成一個調用鏈,如果其中任何一個節點為nil,整個調用鏈都會失敗,即返回nil。

注意
Swift 的可選鏈式調用和 Objective-C 中向nil發送消息有些相像,但是 Swift 的可選鏈式調用可以應用于任意類型,并且能檢查調用是否成功。

1.使用可選鏈式調用代替強制展開

通過在想調用的屬性、方法、或下標的可選值后面放一個問號(?),可以定義一個可選鏈。這一點很像在可選值后面放一個嘆號(!)來強制展開它的值。它們的主要區別在于當可選值為空時可選鏈式調用只會調用失敗,然而強制展開將會觸發運行時錯誤。

為了反映可選鏈式調用可以在空值(nil)上調用的事實,不論這個調用的屬性、方法及下標返回的值是不是可選值,它的返回結果都是一個可選值。你可以利用這個返回值來判斷你的可選鏈式調用是否調用成功,如果調用有返回值則說明調用成功,返回nil則說明調用失敗。

特別地,可選鏈式調用的返回結果與原本的返回結果具有相同的類型,但是被包裝成了一個可選值。例如,使用可選鏈式調用訪問屬性,當可選鏈式調用成功時,如果屬性原本的返回結果是Int類型,則會變為Int?類型。

下面幾段代碼將解釋可選鏈式調用和強制展開的不同。

首先定義兩個類Person和Residence:

class Person {
    var residence: Residence?
}

class Residence {
    var numberOfRooms = 1
}

Residence有一個Int類型的屬性numberOfRooms,其默認值為1。Person具有一個可選的residence屬性,其類型為Residence?。

假如你創建了一個新的Person實例,它的residence屬性由于是是可選型而將初始化為nil,在下面的代碼中,john有一個值為nil的residence屬性:

let john = Person()

如果使用嘆號(!)強制展開獲得這個john的residence屬性中的numberOfRooms值,會觸發運行時錯誤,因為這時residence沒有可以展開的值:

let roomCount = john.residence!.numberOfRooms
// 這會引發運行時錯誤

john.residence為非nil值的時候,上面的調用會成功,并且把roomCount設置為Int類型的房間數量。正如上面提到的,當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.”

在residence后面添加問號之后,Swift 就會在residence不為nil的情況下訪問numberOfRooms。

因為訪問numberOfRooms有可能失敗,可選鏈式調用會返回Int?類型,或稱為“可選的 Int”。如上例所示,當residence為nil的時候,可選的Int將會為nil,表明無法訪問numberOfRooms。訪問成功時,可選的Int值會通過可選綁定展開,并賦值給非可選類型的roomCount常量。

要注意的是,即使numberOfRooms是非可選的Int時,這一點也成立。只要使用可選鏈式調用就意味著numberOfRooms會返回一個Int?而不是Int。

可以將一個Residence的實例賦給john.residence,這樣它就不再是nil了:

john.residence = Residence()

john.residence現在包含一個實際的Residence實例,而不再是nil。如果你試圖使用先前的可選鏈式調用訪問numberOfRooms,它現在將返回值為1的Int?類型的值:

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).”
2.為可選鏈式調用定義模型類

通過使用可選鏈式調用可以調用多層屬性、方法和下標。這樣可以在復雜的模型中向下訪問各種子屬性,并且判斷能否訪問子屬性的屬性、方法或下標。

下面這段代碼定義了四個模型類,這些例子包括多層可選鏈式調用。為了方便說明,在Person和Residence的基礎上增加了Room類和Address類,以及相關的屬性、方法以及下標。

Person類的定義基本保持不變:

class Person {
    var 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數組的count屬性的值。

Residence還提供了訪問rooms數組的快捷方式,即提供可讀寫的下標來訪問rooms數組中指定位置的元素。

此外,Residence還提供了printNumberOfRooms方法,這個方法的作用是打印numberOfRooms的值。

最后,Residence還定義了一個可選屬性address,其類型為Address?。Address類的定義在下面會說明。

Room類是一個簡單類,其實例被存儲在rooms數組中。該類只包含一個屬性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 buildingName != nil {
            return buildingName
        } else if buildingNumber != nil && street != nil {
            return "\(buildingNumber) \(street)"
        } else {
            return nil
        }
    }
}

Address類提供了buildingIdentifier()方法,返回值為String?。 如果buildingName有值則返回buildingName。或者,如果buildingNumber和street均有值則返回buildingNumber。否則,返回nil。

3.通過可選鏈式調用訪問屬性

正如使用可選鏈式調用代替強制展開中所述,可以通過可選鏈式調用在一個可選值上訪問它的屬性,并判斷訪問是否成功。

下面的代碼創建了一個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。

上面代碼中的賦值過程是可選鏈式調用的一部分,這意味著可選鏈式調用失敗時,等號右側的代碼不會被執行。對于上面的代碼來說,很難驗證這一點,因為像這樣賦值一個常量沒有任何副作用。下面的代碼完成了同樣的事情,但是它使用一個函數來創建Address實例,然后將該實例返回用于賦值。該函數會在返回前打印“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()函數并未被執行。

4.通過可選鏈式調用調用方法

可以通過可選鏈式調用來調用方法,并判斷是否調用成功,即使這個方法沒有返回值。

Residence類中的printNumberOfRooms()方法打印當前的numberOfRooms值,如下所示:

func printNumberOfRooms() {
    print("The number of rooms is \(numberOfRooms)")
}

這個方法沒有返回值。然而,沒有返回值的方法具有隱式的返回類型Void,如無返回值函數中所述。這意味著沒有返回值的方法也會返回(),或者說空的元組。

如果在可選值上通過可選鏈式調用來調用這個方法,該方法的返回類型會是Void?,而不是Void,因為通過可選鏈式調用得到的返回值都是可選的。這樣我們就可以使用if語句來判斷能否成功調用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中的address屬性賦值,即使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.”
5.通過可選鏈式調用訪問下標

通過可選鏈式調用,我們可以在一個可選值上訪問下標,并且判斷下標調用是否成功。

注意
通過可選鏈式調用訪問可選值的下標時,應該將問號放在下標方括號的前面而不是后面。可選鏈式調用的問號一般直接跟在可選表達式的后面。

下面這個例子用下標訪問john.residence屬性存儲的Residence實例的rooms數組中的第一個房間的名稱,因為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實例,并為其rooms數組添加一些Room實例,然后將Residence實例賦值給john.residence,那就可以通過可選鏈和下標來訪問數組中的元素:

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 中Dictionary類型的鍵的下標,可以在下標的結尾括號后面放一個問號來在其可選返回值上進行可選鏈式調用:

var testScores = ["Dave": [86, 82, 84], "Bev": [79, 94, 81]]
testScores["Dave"]?[0] = 91
testScores["Bev"]?[0] += 1
testScores["Brian"]?[0] = 72
// "Dave" 數組現在是 [91, 82, 84],"Bev" 數組現在是 [80, 94, 81]

上面的例子中定義了一個testScores數組,包含了兩個鍵值對,把String類型的鍵映射到一個Int值的數組。這個例子用可選鏈式調用把"Dave"數組中第一個元素設為91,把"Bev"數組的第一個元素+1,然后嘗試把"Brian"數組中的第一個元素設為72。前兩個調用成功,因為testScores字典中包含"Dave"和"Bev"這兩個鍵。但是testScores字典中沒有"Brian"這個鍵,所以第三個調用失敗。

6.連接多層可選鏈式調用

可以通過連接多個可選鏈式調用在更深的模型層級中訪問屬性、方法以及下標。然而,多層可選鏈式調用不會增加返回值的可選層級。

也就是說:

  • 如果你訪問的值不是可選的,可選鏈式調用將會返回可選值。
  • 如果你訪問的值就是可選的,可選鏈式調用不會讓可選返回值變得“更可選”。

因此:

  • 通過可選鏈式調用訪問一個Int值,將會返回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實例,并且為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包含一個有效的Residence實例,所以對john.residence的address屬性賦值將會成功。

7.在方法的可選返回值上進行可選鏈式調用

上面的例子展示了如何在一個可選值上通過可選鏈式調用來獲取它的屬性值。我們還可以在一個可選值上通過可選鏈式調用來調用方法,并且可以根據需要繼續在方法的可選返回值上進行可選鏈式調用。

在下面的例子中,通過可選鏈式調用來調用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()方法的可選返回值上進行可選鏈式調用,而不是方法本身。

8.可選鏈總結
可選鏈

class Person {
    var residence: Residence?
}

class Residence {
    var rooms = [Room]()
    var numbweOfRooms: Int {
        return rooms.count
    }
    subscript(i: Int) -> Room {
        get {
            return rooms[i]
        }
        set {
            rooms[i] = newValue
        }
    }
    func printNumberOfRooms() {
        print("The number of rooms is \(numbweOfRooms)")
    }
    var address: Address?
}

class Room {
    let name: String
    init(name: String) { self.name = name }
}

class Address {
    var buildingName: String?
    var buildingNumber: String?
    var street: String?
    func buildingIdentifier() -> String? {
        if buildingName != nil {
            return buildingName
        } else if buildingNumber != nil && street != nil {
            return "\(buildingNumber) \(street)"
        } else {
            return nil
        }
    }
}



let john = Person()
if let roomCount = john.residence?.numbweOfRooms {
    print("John住宅有\(roomCount)個房間")
} else {
    print("無法獲取房間數")
}

let someAddress = Address()
someAddress.buildingNumber = "29"
someAddress.street = "Acacid Road"
john.residence?.address = someAddress

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

if john.residence?.address != nil {
    print("It was possible to set the address.")
} else {
    print("It was not possible to set the address.")
}

if let firstRoomName = john.residence?[0].name {
    print("The first room name is \(firstRoomName).")
} else {
    print("Unable to retrieve the first room name.")
}

john.residence?[0] = Room(name: "Bathroom")

let johnHouse = Residence()
johnHouse.rooms.append(Room(name: "Living Room"))
johnHouse.rooms.append(Room(name: "Kitchen"))
john.residence = johnHouse

if let firstRoomName = john.residence?[0].name {
    print("The first room name is \(firstRoomName).")
} else {
    print("Unable to retrieve the first room name.")
}

if let johnsStreet = john.residence?.address?.street {
    print("John's street name is \(johnsStreet).")
} else {
    print("Unable to retrieve the address.")
}

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

if let buildingIdentifier = john.residence?.address?.buildingIdentifier() {
    print("John's building identifier is \(buildingIdentifier).")
}

if let beginsWithThe = john.residence?.address?.buildingIdentifier()?.hasPrefix("The") {
    beginsWithThe ? print("John's building identifier begins with \"The\".") : print("John's building identifier does not begin with \"The\".")

//    if beginsWithThe {
//        print("John's building identifier begins with \"The\".")
//    } else {
//        print("John's building identifier does not begin with \"The\".")
//    }
}


錯誤處理

錯誤處理(Error handling)是響應錯誤以及從錯誤中恢復的過程。Swift 提供了在運行時對可恢復錯誤的拋出、捕獲、傳遞和操作的一等公民支持。

某些操作無法保證總是執行完所有代碼或總是生成有用的結果。可選類型可用來表示值缺失,但是當某個操作失敗時,最好能得知失敗的原因,從而可以作出相應的應對。

舉個例子,假如有個從磁盤上的某個文件讀取數據并進行處理的任務,該任務會有多種可能失敗的情況,包括指定路徑下文件并不存在,文件不具有可讀權限,或者文件編碼格式不兼容。區分這些不同的失敗情況可以讓程序解決并處理某些錯誤,然后把它解決不了的錯誤報告給用戶。

1.表示并拋出錯誤

在 Swift 中,錯誤用符合Error協議的類型的值來表示。這個空協議表明該類型可以用于錯誤處理。

Swift 的枚舉類型尤為適合構建一組相關的錯誤狀態,枚舉的關聯值還可以提供錯誤狀態的額外信息。例如,你可以這樣表示在一個游戲中操作自動販賣機時可能會出現的錯誤狀態:

enum VendingMachineError: Error {
    case invalidSelection                    //選擇無效
    case insufficientFunds(coinsNeeded: Int) //金額不足
    case outOfStock                          //缺貨
}

拋出一個錯誤可以讓你表明有意外情況發生,導致正常的執行流程無法繼續執行。拋出錯誤使用throw關鍵字。例如,下面的代碼拋出一個錯誤,提示販賣機還需要5個硬幣:

throw VendingMachineError. insufficientFunds(coinsNeeded: 5)
2.處理錯誤

某個錯誤被拋出時,附近的某部分代碼必須負責處理這個錯誤,例如糾正這個問題、嘗試另外一種方式、或是向用戶報告錯誤。

Swift 中有4種處理錯誤的方式。你可以把函數拋出的錯誤傳遞給調用此函數的代碼、用do-catch語句處理錯誤、將錯誤作為可選類型處理、或者斷言此錯誤根本不會發生。每種方式在下面的小節中都有描述。

當一個函數拋出一個錯誤時,你的程序流程會發生改變,所以重要的是你能迅速識別代碼中會拋出錯誤的地方。為了標識出這些地方,在調用一個能拋出錯誤的函數、方法或者構造器之前,加上try關鍵字,或者try?或try!這種變體。這些關鍵字在下面的小節中有具體講解。

注意
Swift 中的錯誤處理和其他語言中用try,catch和throw進行異常處理很像。和其他語言中(包括 Objective-C )的異常處理不同的是,Swift 中的錯誤處理并不涉及解除調用棧,這是一個計算代價高昂的過程。就此而言,throw語句的性能特性是可以和return語句相媲美的。

  • 用 throwing 函數傳遞錯誤

為了表示一個函數、方法或構造器可以拋出錯誤,在函數聲明的參數列表之后加上throws關鍵字。一個標有throws關鍵字的函數被稱作throwing 函數。如果這個函數指明了返回值類型,throws關鍵詞需要寫在箭頭(->)的前面。

func canThrowErrors() throws -> String
func cannotThrowErrors() -> String

一個 throwing 函數可以在其內部拋出錯誤,并將錯誤傳遞到函數被調用時的作用域。

注意
只有 throwing 函數可以傳遞錯誤。任何在某個非 throwing 函數內部拋出的錯誤只能在函數內部處理。

下面的例子中,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 dispenseSnack(snack: String) {
        print("Dispensing \(snack)")
    }

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

        coinsDeposited -= item.price

        var newItem = item
        newItem.count -= 1
        inventory[name] = newItem

        print("Dispensing \(name)")
    }
}

在vend(itemNamed:)方法的實現中使用了guard語句來提前退出方法,確保在購買某個物品所需的條件中,有任一條件不滿足時,能提前退出方法并拋出相應的錯誤。由于throw語句會立即退出方法,所以物品只有在所有條件都滿足時才會被售出。

因為vend(itemNamed:)方法會傳遞出它拋出的任何錯誤,在你的代碼中調用此方法的地方,必須要么直接處理這些錯誤——使用do-catch語句,try?或try!;要么繼續將這些錯誤傳遞下去。例如下面例子中,buyFavoriteSnack(person:vendingMachine:)同樣是一個 throwing 函數,任何由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關鍵字。

throwing構造器能像throwing函數一樣傳遞錯誤.例如下面代碼中的PurchasedSnack構造器在構造過程中調用了throwing函數,并且通過傳遞到它的調用者來處理這些錯誤。

struct PurchasedSnack {
    let name: String
    init(name: String, vendingMachine: VendingMachine) throws {
        try vendingMachine.vend(itemNamed: name)
        self.name = name
    }
}
  • 用 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錯誤處理語句,要么是一個 throwing 函數的內部。舉例來說,下面的代碼處理了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子句中余下的語句就會被執行。

  • 將錯誤轉換成可選值

可以使用try?通過將錯誤轉換成一個可選值來處理錯誤。如果在評估try?表達式時一個錯誤被拋出,那么表達式的值就是nil。例如,在下面的代碼中,x和y有著相同的數值和等價的含義:

func someThrowingFunction() throws -> Int {
    // ...
}

let x = try? someThrowingFunction()

let y: Int?
do {
    y = try someThrowingFunction()
} catch {
    y = nil
}

如果someThrowingFunction()拋出一個錯誤,x和y的值是nil。否則x和y的值就是該函數的返回值。注意,無論someThrowingFunction()的返回值類型是什么類型,x和y都是這個類型的可選類型。例子中此函數返回一個整型,所以x和y是可選整型。

如果你想對所有的錯誤都采用同樣的方式來處理,用try?就可以讓你寫出簡潔的錯誤處理代碼。例如,下面的代碼用幾種方式來獲取數據,如果所有方式都失敗了則返回nil:

func fetchData() -> Data? {
    if let data = try? fetchDataFromDisk() { return data }
    if let data = try? fetchDataFromServer() { return data }
    return nil
}
  • 禁用錯誤傳遞

有時你知道某個throwing函數實際上在運行時是不會拋出錯誤的,在這種情況下,你可以在表達式前面寫try!來禁用錯誤傳遞,這會把調用包裝在一個不會有錯誤拋出的運行時斷言中。如果真的拋出了錯誤,你會得到一個運行時錯誤。

例如,下面的代碼使用了loadImage(atPath:)函數,該函數從給定的路徑加載圖片資源,如果圖片無法載入則拋出一個錯誤。在這種情況下,因為圖片是和應用綁定的,運行時不會有錯誤拋出,所以適合禁用錯誤傳遞:

let photo = try! loadImage(atPath: "./Resources/John Appleseed.jpg")
3.指定清理操作

可以使用defer語句在即將離開當前代碼塊時執行一系列語句。該語句讓你能執行一些必要的清理工作,不管是以何種方式離開當前代碼塊的——無論是由于拋出錯誤而離開,還是由于諸如return或者break的語句。例如,你可以用defer語句來確保文件描述符得以關閉,以及手動分配的內存得以釋放。

defer語句將代碼的執行延遲到當前的作用域退出之前。該語句由defer關鍵字和要被延遲執行的語句組成。延遲執行的語句不能包含任何控制轉移語句,例如break或是return語句,或是拋出一個錯誤。延遲執行的操作會按照它們被指定時的順序的相反順序執行——也就是說,第一條defer語句中的代碼會在第二條defer語句中的代碼被執行之后才執行,以此類推。

func processFile(filename: String) throws {
    if exists(filename) {
        let file = open(filename)
        defer {
            close(file)
        }
        while let line = try file.readline() {
            // 處理文件。
        }
        // close(file) 會在這里被調用,即作用域的最后。
    }
}

上面的代碼使用一條defer語句來確保open(_:)函數有一個相應的對close(_:)函數的調用。

注意
即使沒有涉及到錯誤處理,你也可以使用defer語句。

4.錯誤處理總結
錯誤處理

// 表示并拋出錯誤,錯誤用符合Error協議的類型的值來表示,這個空協議表明該類型可以用于錯誤處理
enum VendingMachineError: Error {
    case invalidSelection   // 選擇無效
    case insufficientFunds(coinsNeeded: Int)  // 金額不足
    case outOfStock         // 缺貨
}

// 錯誤處理 1 用throwing函數傳遞錯誤
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 dispenseSnack(snack: String) {
        print("Dispensing \(snack)")
    }

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

        coinsDeposited -= item.price

        var newItem = item
        newItem.count -= 1
        inventory[name] = newItem
        print("Dispensing \(name)")
    }
}

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

var vendingMachine = VendingMachine()
vendingMachine.coinsDeposited = 8
do {
    try buyFavoriteSnack(person: "Alice2", 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.")
}

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容