iT邦幫忙

2018 iT 邦幫忙鐵人賽
DAY 21
0

可失敗初始化器 (Failable Initializers)

定義初始化可能會失敗的 Class 、Struct、Enum 有時用很有用,這種失敗可能是由無效的初始化參數值,缺少所需外部資源或是其他一些阻止初始化成功的情況。為了妥善處理這種可能會失敗的情況,在 Class 、Struct、Enum 中定義一個或多個可失敗的初始化器。通過在 init 關鍵字後面添加問號(init?)來寫。

你不能定義可失敗和非可失敗的初始化器為相同的參數類型和名稱。

可失敗的初始化器創建了一個初始化類型的可選值。你透過在可失敗初始化器寫上 return nil ,來表明可失敗初始化器在何種情況下會觸發初始化失敗。嚴格來說,初始化器不會有返回值。相反,它們的角色是確保在初始化結束時, self 能夠被正確初始化。雖然你寫了 return nil 來觸發初始化失敗,但是你不能使用 return 來表示初始化成功了。

舉個例子,可失敗初始化器為數字類型轉換做實現。我們轉換 number 中 Double 型別的值,試著將它轉成 Int 類型,並比較兩個數字的差異:

let number1 = 87.0
if let valueChange = Int(exactly: number1){
    print("數字 1 : \(number1) 可以保持原有的值,轉換成 Int 。")
}

let number2 = 87.2
let valueNil = Int(exactly: number2)
if valueNil == nil {
    print("數字 2 : \(number2) 無法保持原值轉換類型。")
}

結果如下:
https://ithelp.ithome.com.tw/upload/images/20180109/20107701ucFBNuoEKH.png

上面的 number 經由我們 Int(exactly: number1) 初始化器分別會得到 number1 值為 87 ,number2 則為 nil ,因為 number2 進行轉換後沒辦法保持原有的值,所以他的初始化失敗,得到一個 nil 值。

下面我們在來透過可失敗初始化器來做學生姓名的範例,並且在可失敗初始化器中,如果 name 為空字串,則回傳一個 nil 值給他,如果有輸入值,則把值傳遞給 name:

struct Student {
    let name:String
    init?(name:String) {
        if name.isEmpty {return nil}
        self.name = name
    }
}

https://ithelp.ithome.com.tw/upload/images/20180109/20107701Wb6t8BOkQf.png


枚舉的可失敗初始化器

你可以使用一個可失敗初始化器來在帶一個或多個形式參數的枚舉中選擇合適的情況。如果提供的形式參數沒有匹配合適的情況初始化器就可能失敗。

我們下面舉例一個餐廳中擁有的項目,並用可失敗初始化器來判斷餐廳是否有這個選項,如果有則印出選項,如果沒有就回傳一個 nil 值:

enum Restaurant {
    case food, soup, drink
    init?(item: String){
        switch item {
        case "食物":
            self = .food
        case "湯品":
            self = .soup
        case "飲料":
            self = .drink
        default:
            return nil
        }
    }
}

這邊就不用 if 去確認,直接看實例中的值,因為炸物沒有相匹配的 String ,所以初始化失敗:
https://ithelp.ithome.com.tw/upload/images/20180109/20107701OJoa25WpmN.png


帶有原始值枚舉的可失敗初始化器

帶有原始值的 enum 會自動擁有一個可失敗初始化器 init?(rawValue:) ,該可失敗初始化器接收一個名為 rawValue 的合適的原始值類型參數如果找到了匹配的枚舉 case 就選擇,沒有找到匹配的值則初始化失敗。

我們使用這個方式修改上述範例:

enum Restaurant: String {
    case food = "食物", soup = "湯品", drink = "飲料"
}

之後我們可以透過 rawValue 來輸入 String 值,如果有匹配的值則接收,如果沒有匹配的結果則初始化失敗,回傳 nil 。

https://ithelp.ithome.com.tw/upload/images/20180109/20107701lMDR9M1277.png

https://ithelp.ithome.com.tw/upload/images/20180109/20107701GoKINc41nw.png


初始化失敗的傳遞

Class,Struct 或 enum 的可失敗初始化器可以橫向委託到同一個 Class 中、Struct 或 enum 裡的另一個可失敗初始化器。同樣的,子類的可失敗初始化器可以向上委託到父類的可失敗初始化器。無論哪種情況,如果你委託到另一個初始化器導致了初始化失敗,那麼整個初始化過程也會跟著失敗,並且之後任何初始化都不會執行。

下面的我們定義了 Classes 類的子類 Student。我們希望可以看見每個班級裡面有多少人, Student 引入了一個名為 student 常數存儲屬性,並設定他在小於 1 的時候回傳 nil ,因為班級人數不會少於1人。

class Classes {
    let className:String
    init?(className:String) {
        if className.isEmpty {return nil}
        self.className = className
    }
}

class Student:Classes {
    let student:Int
    init?(className:String, student:Int) {
        if student < 1 { return nil}
        self.student = student
        super.init(className: className)
    }
}

在 Student 中的可失敗初始化器會檢測它是否接受了一個 student 是否大於 1。如果 student < 1 或不合法,整個初始化過程會立即失敗並且後來的初始化都不會執行。同樣地, Classese 的可失敗初始化器檢查 className 的值,初始化器會在 name 為空字符串時直接失敗,結果如下:

https://ithelp.ithome.com.tw/upload/images/20180109/20107701CMRnfeh2tO.png


重寫可失敗初始化器

你可以在子類裡重寫父類的可失敗初始化器。就好比其他的初始化器那樣。或者,你可以用子類的非可失敗初始化器來重寫父類可失敗初始化器。這樣允許你定義一個初始化不會失敗的子類,儘管父類的初始化允許失敗。如果你用非可失敗的子類初始化器重寫了一個可失敗初始化器,向上委託到父類初始化器的唯一的辦法是強制展開父類可失敗初始化器的結果。
你可以用一個非可失敗初始化器重寫一個可失敗初始化器,但反過來是不行的。

首先我們定義一個 file 的 Class , 我們希望這個文件是給我們一個名字或是 nil,但是不能為空字串:

class File {
    var name:String?
    init() {}
    init?(name:String) {
        self.name = name
        if name.isEmpty {return nil}
    }
}

結果如下:
https://ithelp.ithome.com.tw/upload/images/20180109/201077012Jik1LFA2K.png

下面我們定義了一個名為 AutoName 的 File 類的子類。我們重寫了從父類引入的兩個指定初始化器。這些重寫確保了 AutoName 實例在初始化時沒有名字或者傳給 init(name:) 初始化器一個空字符串時 name 的初始值就會變成未命名文件:

class AutoName:File {
    override init() {
        super.init()
        self.name = "未命名文件"
    }
    override init?(name: String) {
        super.init()
        if name.isEmpty {
            self.name = "未命名文件"
        } else {
            self.name = name
        }
    }
}

結果如下:
https://ithelp.ithome.com.tw/upload/images/20180109/201077019H4SchIHTg.png

我們在上面用非可失敗初始化器 init(name:) 重寫了父類的可失敗 init?(name:) 初始化器。因為 AutoName 用不同的方式處理空字符串的情況,它的初始化器不會失敗,所以它提供了非可失敗初始化器來代替。

你可以在初始化器裡使用強制展開來從父類調用一個可失敗初始化器作為子類非可失敗初始化器的一部分:

class Unknow:File {
    override init() {
        super.init(name: "未命名文件")!
    }
}

結果如下:
https://ithelp.ithome.com.tw/upload/images/20180109/201077015PGNeciLDb.png

如果父類的 init (name:) 初始化器曾以空字符串做名字,強制展開操作會產生錯誤。由於這個範例它調用了一個 String ,那麼這個初始化器不會失敗,所以不會有運行時錯誤發生。


可失敗初始化器init!

我們可以透過在 init 關鍵字後添加問號的來定義一個可失敗初始化器以創建一個合適類型的可選項實例。另外,你也可以使用可失敗初始化器創建一個隱式展開具有合適類型的可選項實例。通過在 init 後面添加驚嘆號(init!)是不是問號。

你可以在 init? 初始化器中委託調用 init! 初始化器。反之,你也可以用 init! 重寫 init?。你還可以用 init 委託調用 init! ,儘管當 init! 初始化器導致初始化失敗時會觸發斷言。

必要初始化器

可以在 class 的初始化器前加上 required 修飾符來表示所有這個 Class 的子類都必須實現該初始化器:

class SomeClass {
    required init() {
    }
}

當子類重寫父類的必要初始化器時,必須在子類的初始化器前也需加上 required 修飾符以確保當其它類繼承該子類時,該初始化器一樣為必要初始化器。在重寫父類的必要初始化器時,不需要加上 override:

class SomeSubclass: SomeClass {
    required init() {
        // subclass implementation of the required initializer goes here
    }
}

上一篇
Day-20 Swift 語法(16) - Class 與 初始化器
下一篇
Day-22 Swift 語法(18) - 反初始化 Deinitialization
系列文
Swift 菜鳥的30天30

尚未有邦友留言

立即登入留言