Swift — 面試題挑戰賽(2)

讓我們一起來討論以及研究一些 iOS 面試題目吧。

Jeremy Xue
Jeremy Xue ‘s Blog
15 min readJun 3, 2019

--

Photo by Nik MacMillan on Unsplash

# 本期題目:

— Swift:

  1. Final class
  2. Struct vs Class
  3. Swift Standard Library Protocol
  4. Downcasting
  5. Explain [weak self] and [unowned self]?
  6. What are failable and throwing initializers?

# Final class

編寫 final 修飾符的主要目的是 — 防止覆寫(override)

你可以透過將方法、屬性或下標標記為 final防止其被覆寫。在方法、屬性及下標的之前編寫 final 修飾符來達成此操作( 像是: final varfinal funcfinal class funcfinal subscript)。

任何嘗試覆寫 subClass 中 final 方法、屬性或是下標都會表示編譯錯誤。你添加到擴展(extension)中的 class 的方法、屬性和下標也可以在擴展定義中標記為 final

你可以標記整個 class 為 final(final class)。,透過編寫 final 修飾符在 class 關鍵字之前任何對 final class 做 subclass 的嘗試都會表示編輯錯誤。

# Struct vs Class

Swift 中的 struct 和 class 有許多共同點,其兩者都能夠:

  • 定義屬性用來儲存值
  • 定義方法用來提供功能
  • 定義下標用來提供使用下標語法訪問其值
  • 定義初始化器用於設置其初始狀態
  • 擴展用來擴展其功能,超出默認的實現
  • 符合協議(Protocols)用於提供某些標準功能

而 class 還具有 struct 不具備的功能:

  • 繼承(Inheritance)使某個 class 去繼承另一個 class 的特性
  • 類型轉換(Type casting)使你能夠在運行時檢查和解釋 class 實例的類型
  • 反初始化器(Deinitializers)使一個 class 的實例能夠被釋放任何它被分配的資源
  • 引用技術(Reference counting)允許對一個 class 實例的多個引用

而 class 支援的額外功能增加複雜性為代價。作爲一般準則,更偏向使用 struct,因為他們更容易推理,並在適當或是必要時使用 class。實際上,這意味著你定義的大多數自定義的資料類型都會是 struct 和 enum。

還有一個比較大的差別兩者的傳遞方式, struct 和 enum 為 pass by value,而 class 是 pass by reference

  • 值類型( Value Type )是一種類型,其值在被賦值給變數或常數時被複製,或者在傳遞給函數時被複製。
  • 不同於值類型,引用類型( Reference Types )再分配給常數或變數或是傳遞給函數時不會被複製。使用對同一個現有的實例的引用,而不是副本。

關於 class 與 struct 之間的選擇問題,可以查看我這篇官方翻譯文章,或是這篇官方文件。

# Swift Standard Library Protocol

官方說過 Swift 設計核心是兩個非常強大的想法:協議導向( Protocol-oriented programing)以及第一類物件( first class value)語義。

而 Apple 在 Swift 標準庫(Standard Libary)中廣泛的使用協議。我們可以看到標準庫構成的最佳資源是 http://swiftdoc.org,其中展示了構成標準庫的類型、協議、運算符以及全局變數。

你可以在上方連結點入任何一個類型,進到該類型的畫面之後滾動到畫面中間時,可以看到這個類型所繼承、關聯類型、嵌套類型以及 Import 的內容:

你還能夠點擊繼承中的 VIEW PROTOCOL HIERARCHY -> 連結來查看其中協議階層結構的圖形化表示:

使用協議,你可以擺脫某些多重繼承的困擾,對於 struct 以及 enum 也能使其與協議組合產生更多效用。接下來我們簡單介紹 Swift 中的三種協議:

# “Can do” protocols

這個類型的協議通常描述一件 “可以做” 或是 “已完成” 的事情,它們通常以 -able 做為其結尾,像是: HashableEquatableComparable 等等。例如:符合 Hashable 協議的能夠將一件事物 hash 到一個 Int 中,這意味著你可以將其存儲到一個 Set 中,它可以是 Dictionarykey 等等。你還獲得了 EquatableComparable 協議,這意味著你能夠使用 Swift 中提供的各種相等和比較運算符來比較某些值中的兩個實例。

AbsoluteValuable, 這聽起來很重要,只因為它在結尾處有 valuable 這個詞,但不巧的是它聽起來比它更重要,這意味著它支持絕對值功能。其他例子也是相同概念,RawRepresentable 意味著類型可以表示為某種原始值,然後您可以將該原始值轉換回實際實例的值。

# “Is a” protocols

這類型的協議通常描述了這類型的事物,與上述的 “Can do” 的協議相比,這類協議更基於身份,這意味著符合著多種協議感覺最接近 Swift 中的多重繼承。你可以在標準庫看見許多這種類型,它們以 -type 做為結尾。例如:CollectionType 其中ArrayDictionarySet 都符合 CollectionType

然後有部分協議用於覆蓋一些原語,例如: IntegerTypeBooleanTypeFloatingPointType 等等,你可以將這些協議視為分組的概念。所以我們有多種整數類型,像是 unsigned int、signed int,16-bit int 等等。但他們都組合在一起,因為它們符合通用的整數類型協議。

# “Can be” protocols

最後一種協議不僅僅是同一事物的替代觀點,這些更多是關於直接轉換,從 A類型 轉換成 B類型,它們通常以 -Convertible 做為結尾,這也意味著該類型可以轉換或轉換成其他類型。

讓我們來幾個範例。我們有簡單的初始化器樣式,像是FloatLiteralConvertibleIntegerLiteralConvertibleArrayLiteralConvertible 等。如果你的類型符合 FloatLiteralConvertible,那麼這意味著你需要一個初始化器,它需要某種 floatLiteral,默認情況下是一個 double,然後由它構建你的類型。所以轉換是從 float 到你的類型的那個方向。

相比之下,有一些協議,像是每個人最好的朋友 CustomStringConvertible,或者以前稱為 Printable 的協議。這指定您的類型可以轉換為字串,因此轉換將轉向另一個方向 — 您將從您的類型轉變為將其轉換為字串。

想看更多協議的資訊可以查看這篇文章。

# Downcasting

某個 class 類型中的常數或變數實際上可能是指背後 subclass 的實例。當你認為是這種情況時,你可以嘗試使用轉換運算符(as?as!)去向下轉換(downcast)為 subclass 類型。

因為向下轉換可能會失敗,因此其類型轉換運算符有兩種不同的形式:

  • as?:返回嘗試向下轉換的類型的 Optional value。
  • as!:嘗試強制向下轉換並且將結果強制展開作為單個複合操作。

當你不確定下向轉換是否成功時,採用 as? 運算符,這種形式的轉換始終返回一個 Optional value 或是 nil ;當你確定向下轉換必定成功時,採用 as! 運算符強制轉換,但是如果向下轉換為不正確的類型,則將觸發運行錯誤。

需要特別注意,類型轉換是轉換為它原有或是有繼承關係的類型,而不是將某個類型轉換成另一個全新的類型,其類型必定始終是相同的,下面我們簡單示範一個範例:

as 運算符是表示 A type 是不是能夠作為 B type,而不是表示 A type 能不能轉換為 B type

Cast from ‘Int’ to unrelated type ‘String’ always fails

不要把類型實例化概念 String(number) 與類型轉換 number as? String 的概念搞混。as 不是代表他能不能被轉換成該類型。

下面簡單示範一個向下轉換範例,首先定義 MediaItem class:

接著定義兩個 class 分別為 MovieSong ,並且兩者都繼承 MediaItem

接著我們宣告一個稱為 library 的常數,裡面會放著 MovieSong 類型的時,這邊 Swift 類型推斷會判斷 library 的類型為 [MediaItem]

接著我們使用 as? 類型轉換符號以及 Optional binding 來判斷是否能夠轉換為該類型,若轉換成功則印出其資訊:

而當你轉型成必定成功的情況,像是相同類型轉換或是轉型為 superclass 類型時,其轉型一定成功,這時候可以將 as! 的驚嘆號移除,改為 as 即可。

# Type casting 額外補充

你也能夠使用 is 檢查運算符來判斷其類型,若為該類型則會返回 true,反之為 false

你還能夠透過 type(of:) 來看出該實例為什麼類型。

之後我們會在類型推斷的文章中介紹更多。

# Explain weak self and unowned self?

當你使用 class 類型的屬性時,Swift 提供了兩種解決引用強循環的方法,弱引用(weak reference)無主引用(unowned reference),其兩者能夠引用循環中的一個實例來引用另外一個實例時,不保持強引用。這樣實例能夠互相引用而不產生循環強引用。

當另一個實例的生命週期更短時,即在可以首先釋放另一個實例時,使用弱引用。相反,當另一個實例具有相同的生命週期或更長的生命週期時,請使用無主引用。

# Weak reference

弱引用不會對其引用的實例保持強引用,所以不會阻止 ARC 釋放被引用的實例。這種特性阻止了循環強引用。宣告屬性或變數時,在前面加上 weak 表名為弱引用。

由於它不會保持對實例的引用,所以說實例被釋放了弱引用仍舊引用著這個實例也是有可能的。因此,ARC 會再被引用的實例被釋放時自動設置弱引用為 nil。由於弱引用需要允許它們的值為 nil。所以他們必須為 optional type。

您可以檢查弱引用中是否存在值,就像任何其他 optional value 一樣,並且您永遠不會得到對不再存在的無效實例的引用。

# Unowned reference

與弱引用相似,無主引用不會對其引用的實例保持強引用。不同的是,當另一個實例具有相同的生命週期或更長的生命週期時,則使用無主引用。透過在屬性或是變數宣告之前加上 unowned 關鍵字來表示為無主引用。

一個無主參考應該總是有值。因此,ARC 永遠不會將無主引用的值設置為 nil,這意味著非可選類型( non-optional type )定義為無主類型。

只有當你確認引用始終引用尚未釋放的實例時,才使用無主引用。如果您嘗試實例訪問已被釋放後無主參考的值,你會得到一個運行時錯誤。

# What are failable and throwing initializers?

# failable initializer

定義初始化可能失敗的 class、struct 和 enum 有時很有用。這個失敗可能由無效的初始化參數值、缺少必須的外部資源或阻止初始化的成功的某些條件觸發。

要處理可能失敗的初始化條件時,定義一個或多個的可失敗初始化器作為 class、struct 和 enum 定義的一部分。你可以透過在 init 關鍵字後面加上 ? 來編寫可失敗的初始化器。

你不能定義一個可失敗和不可失敗的初始化器使用相同的參數名稱跟類型。

可失敗的初始化器創建了一個初始化類型的 optional value。你透過在可失敗初始化器寫 return nil 語句,來表示可失敗初始化器在何種情況下會觸發初始化失敗。

嚴格來說,初始化器並不會返回值。相反的,他們的作用是確保在初始化結束時完全正確的初始化 self。雖然你編寫了返回 nil 來觸發初始化失敗,但不能使用 return 關鍵字來表示初始化成功。

這邊簡單示範一個可失敗初始化器的選項,我們定義一個 Animal 的 struct,其中有一個可失敗的初始化器:

接著我們分別丟入 cat 和空字串來初始化兩個 Animal 實例,接著透過 Optional binding 判斷是否有值:

透過上面可失敗初始化的 Animal 實例,該類型皆為 Animal?

# throwing initializer

拋出初始化器可以像拋出函數一樣傳播錯誤,它透過將它們錯誤傳遞給調用者來處理它遇到的任何錯誤。

一樣透過一個簡單的範例來表示,這邊我們定義一個 FileStruct 的 struct ,而其中在初始化會根據路徑的內容來初始化 String 並且賦給 text

或是也能夠省略 do-catch 區塊。

--

--

Jeremy Xue
Jeremy Xue ‘s Blog

Hi, I’m Jeremy. [好想工作室 — iOS Developer]