Swift 程式語言 — Protocols (3)
讓我們來看看協議的繼承、組成以及檢查遵循協議。
前言:
如果還沒看過前面協議的文章的讀者,可以先閱讀我前兩篇文章:
|協議繼承
協議可以繼承一個或多個其他協議,並在繼承的要求上添加其他要求。協議繼承的語法類似於繼承的語法,但是可以選擇列出多個繼承的協議,並用逗號分隔:
這邊是從上面繼承 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 的定義將會編譯時錯誤。
當協議的要求定義行為假設或要求遵循類型具有引用語意而非值語意十。請使用 class-only 協議。有關引用和值語意的更多資訊,請參考 Structures and Enumerations Are Value Types 和 Classes Are Reference Types。
|協議組合
在同一時間要求一個類型遵循多個協議可能很有用。你可以將多個協議結合到具有協議組成的單個需求。協議組合的行為就像你定義了一個臨時本地協議,而該協議具有組合中所有協議的組合要求。協議組成沒有定義任何新的協議類型。
協議組合具有 SomeProtocol
和 AnotherProtocol
的形式。你可以根據需求列出任意數量的協議,並使用 &
分隔。除了協議列表之外,協議組合還可以包含一個 class 類型,你還可以使用該 class 類型來指定所需的 superclass。
這邊有一個範例其結合了兩個協議 — Named
和 Aged
,將兩個協議組合為一個函數參數的單個協議組成要求:
看看結果:
在這個範例中,Named
協議具有名為 named
的可獲取 String
屬性的單個要求。而 Aged
協議具有名為 age
的可獲取 Int
屬性的單個要求。兩種協議都被名為 Person
的 struct
採用。
該範例還定義了一個 wishHappyBirthday(to:)
的方法,其 celebrator
參數的類型為 Named & Aged
,表示 “同時遵循 Named
和 Aged
兩個協議的任何類型”。哪種特定類型傳遞給函數並不重要,只要他符合必須的協議即可。
最後,該範例創建一個名為 birthdayPerson
的 Person
實例,並且將該實例傳遞給 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:)
也是無效的。
|檢查遵循協議
你可以使用類型轉換中提及的 is
和 as
運算符來檢查遵循的協議,並且轉換為特定協議。“檢查和轉換為協議”與“檢查和轉換為類型”完全使用相同的語法:
- 如果實例遵循協議,則
is
運算符將返回true
,反之為false
。 as?
向下轉換運算符會返回協議類型的 optional 值,如果實例沒有遵循該協議,則該值為nil
。as!
向下轉換運算符會強制向下轉換為該協議類型,如果向下轉換失敗,則會觸發 runtime error。
這個範例定義了一個名為 HasArea
的協議,具有一個名為 area
的可獲取的 Double
屬性的單一屬性要求:
這裡有 Circle
和 Country
兩個 class
,它們都遵循 HasArea
協議:
Circle
基於存儲的 radius
屬性,將 area
屬性要求實現為計算屬性。而 Country
直接將 area
要求實現為儲存屬性。這兩個 class
皆正確的遵循 HasArea
協議。
這是一個名為 Animal
的 class
,它不遵循 HasArea
協議:
Circle
、Country
和 Animal
沒有共享的 base class。儘管如此,它們都是 class
,因此可以使用這三種類型的實例來初始化儲存類型為 AnyObject
的陣列:
objects
陣列使用包含兩個圓角半徑的 Circle
實例、以平方公里為單位的英國面積的 Country
實例、還有一個四隻腳的 Animal
實例的陣列字面量初始化。
現在 objects
陣列可以被迭代了,並且可以檢查陣列中的每個對象是否遵循 HasArea
協議:
看看結果:
每當陣列中的對象遵循 HasArea
協議時,as?
運算符返回的 optional 值透過 optional binding 到名為 objectWithArea
的常數中。已知 objectWithArea
常數的類型為 HasArea
,因此可以使用類型安全的方式印出其 area
屬性。
注意,轉換過程並不會更改基礎對象,它們仍然是 Circle
、Country
和 Animal
。但是,由於它們儲存在 objectWithArea
常數中。因此只知道它們是 HasArea
類型,所以只能訪問他們的 area
屬性。