Swift 程式語言 — Protocols (3)

讓我們來看看協議的繼承、組成以及檢查遵循協議。

Jeremy Xue
Jeremy Xue ‘s Blog
9 min readNov 20, 2019

--

Photo by Luis Quintero on Unsplash

前言:

如果還沒看過前面協議的文章的讀者,可以先閱讀我前兩篇文章:

|協議繼承

協議可以繼承一個或多個其他協議,並在繼承的要求上添加其他要求。協議繼承的語法類似於繼承的語法,但是可以選擇列出多個繼承的協議,並用逗號分隔:

這邊是從上面繼承 TextRepresentable 協議的範例:

這個範例定義了一個新的協議 — PrettyTextRepresentable,該協議繼承 TextRepresentable。任何採用 PrettyTextRepresentable 必須滿足 TextRepresentable 的強制要求,再加上 PrettyTextRepresentable 的強制要求。這個範例中,PrettyTextRepresentable 添加了單個要求來提供一個稱為 prettyTextualDescription 的可讀屬性,該屬性返回一個 String

SnakeAndLadders 可以使用擴展來採用且遵循 PrettyTextRepresentable

該擴展宣告其採用了 PrettyTextRepresentable 協議,並且為 SnakesAndLadders 類型提供了 prettyTextualDescription 屬性的實現。任何 PrettyTextRepresentable 的內容也必須是 TextRepresentable,因為 prettyTextualDescription 的實現開始於 TextRepresentable 協議訪問 textualDescription 屬性來開始輸出字串。它添加一個冒號和換行符號,將其作用漂亮文本表示的開始。然後它遍歷棋盤的陣列,並且添加一個幾何圖形來表示每個方塊的內容:

  • 如果方塊的值大於 0,則他是梯子的底部,用 ▲ 來表示。
  • 如果方塊的值小於 0,則他是蛇的頭部,用 ▼ 來表示。
  • 否則,該方塊的值為 0,使用 ○ 來表示一個自由的方塊。

現在 prettyTextualDescription 屬性可以用來印出任何 SnakesAndLadders 實例的漂亮文本描述:

看看結果:

|Class-Only 的協議

你可以透過將 AnyObject 協議添加到協議的繼承列表中,來限制協議採用 class 類型:

上面的範例中,SomeClassOnlyProtocol 只能由 class 類型採用。編寫試圖採用 SomeClassOnlyProtocol 的 struct 或 enum 的定義將會編譯時錯誤。

|協議組合

在同一時間要求一個類型遵循多個協議可能很有用。你可以將多個協議結合到具有協議組成的單個需求。協議組合的行為就像你定義了一個臨時本地協議,而該協議具有組合中所有協議的組合要求。協議組成沒有定義任何新的協議類型。

協議組合具有 SomeProtocolAnotherProtocol 的形式。你可以根據需求列出任意數量的協議,並使用 & 分隔。除了協議列表之外,協議組合還可以包含一個 class 類型,你還可以使用該 class 類型來指定所需的 superclass。

這邊有一個範例其結合了兩個協議 — NamedAged,將兩個協議組合為一個函數參數的單個協議組成要求:

看看結果:

在這個範例中,Named 協議具有名為 named 的可獲取 String 屬性的單個要求。而 Aged 協議具有名為 age 的可獲取 Int 屬性的單個要求。兩種協議都被名為 Personstruct 採用。

該範例還定義了一個 wishHappyBirthday(to:) 的方法,其 celebrator 參數的類型為 Named & Aged,表示 “同時遵循 NamedAged 兩個協議的任何類型”。哪種特定類型傳遞給函數並不重要,只要他符合必須的協議即可。

最後,該範例創建一個名為 birthdayPersonPerson 實例,並且將該實例傳遞給 wishHappyBirthday(to:) 函數。因為 Person 遵循這兩種協議,所以此調用有效,並且 wishHappyBirthday(to:) 函數可以印出其生日問候。

這裡是一個結合上個範例中的 Named 協議和 Location class 的範例:

看看結果:

上面的 beginConcert(in:) 函數採用的函數類型為 Location & Named,表示“任何屬於 Location 的 subclass 且符合 Named 協議的類型”。在這種情況下,City 滿足了這些需求。

BirthdayPerson 傳遞給 beginConcert(in:) 函數無效,因為 Person 不是 Location 的 subclass。同樣的,如果你創建 Location 的 subclass 沒有遵循 Named 協議,則使用該類型的實例調用 beginConcert(in:) 也是無效的。

|檢查遵循協議

你可以使用類型轉換中提及的 isas 運算符來檢查遵循的協議,並且轉換為特定協議。“檢查和轉換為協議”與“檢查和轉換為類型”完全使用相同的語法:

  • 如果實例遵循協議,則 is 運算符將返回 true,反之為 false
  • as? 向下轉換運算符會返回協議類型的 optional 值,如果實例沒有遵循該協議,則該值為 nil
  • as! 向下轉換運算符會強制向下轉換為該協議類型,如果向下轉換失敗,則會觸發 runtime error。

這個範例定義了一個名為 HasArea 的協議,具有一個名為 area 的可獲取的 Double 屬性的單一屬性要求:

這裡有 CircleCountry 兩個 class,它們都遵循 HasArea 協議:

Circle 基於存儲的 radius 屬性,將 area 屬性要求實現為計算屬性。而 Country 直接將 area 要求實現為儲存屬性。這兩個 class 皆正確的遵循 HasArea 協議。

這是一個名為 Animalclass,它不遵循 HasArea 協議:

CircleCountryAnimal 沒有共享的 base class。儘管如此,它們都是 class,因此可以使用這三種類型的實例來初始化儲存類型為 AnyObject 的陣列:

objects 陣列使用包含兩個圓角半徑的 Circle 實例、以平方公里為單位的英國面積的 Country 實例、還有一個四隻腳的 Animal 實例的陣列字面量初始化。

現在 objects 陣列可以被迭代了,並且可以檢查陣列中的每個對象是否遵循 HasArea 協議:

看看結果:

每當陣列中的對象遵循 HasArea 協議時,as? 運算符返回的 optional 值透過 optional binding 到名為 objectWithArea 的常數中。已知 objectWithArea 常數的類型為 HasArea,因此可以使用類型安全的方式印出其 area 屬性。

注意,轉換過程並不會更改基礎對象,它們仍然是 CircleCountryAnimal。但是,由於它們儲存在 objectWithArea 常數中。因此只知道它們是 HasArea 類型,所以只能訪問他們的 area 屬性。

--

--

Jeremy Xue
Jeremy Xue ‘s Blog

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