(轉)Swift中的Protocol與繼承

原文鏈接:http://www.lxweimin.com/p/26be1e04ea1a

Swift中的Protocol

眾所周知,Swift是一門面向協議編程(Protocol Oriented Programming 以下簡稱POP)的語言,其中許多標準庫均是基于此來實現的。由于以往使用面向對象的語言的慣性,以至于實際開發中并沒有養成面向協議編程的思維習慣。本文將簡單來聊聊Swift中的Protocol,以及我們為什么要面向protocol編程,以加深對其的印象和了解。

Swift協議的基本功能

協議方法

協議可以要求遵循協議的類型實現某些指定的實例方法或類方法。不支持為協議中的方法的參數提供默認值。功能和Objective-C中基本一致

protocol CoinProtocol {

    func tradingPlatform() -> String

    func sell()

    func buy()
}

如果你想定義為可選方法

@objc protocol CoinProtocol {

    @objc optional func tradingPlatform() -> String

    @objc optional func sell()

    @objc optional func buy()
}

相比Objective-CSwift中的協議提供了一些更加豐富的功能

協議屬性

協議可以要求遵循協議的類型提供特定名稱和類型的實例屬性或類型屬性,它只指定屬性的名稱和類型,協議還指定屬性是可讀的還是可讀可寫的。

protocol CoinProtocol {
    var name: String {get}
    var price: Double {get set}
}

協議作為類型

協議可以像其他普通類型一樣使用,使用場景如下:

  • 作為函數方法的參數或者返回值類型
  • 作為常量變量或者屬性的類型
  • 作為集合中元素的類型

代理模式

代理模式,很常用的一種設計模式;不管是Cocoa還是日常開發中都能??吹?/p>

協議支持繼承、聚合

協議能夠繼承一個或多個其他協議,可以在繼承的協議的基礎上增加新的要求。協議的繼承語法與類的繼承相似,多個被繼承的協議間用逗號分隔:

protocol InheritingProtocol: SomeProtocol, AnotherProtocol {
    // 這里是協議的定義部分
}

有時候需要同時遵循多個協議,你可以將多個協議采用 SomeProtocol & AnotherProtocol 這樣的格式進行組合,稱為 協議合成(protocol composition)。你可以羅列任意多個你想要遵循的協議,以與符號(&)分隔。

protocol InheritingProtocol: SomeProtocol & AnotherProtocol {
    // 這里是協議的定義部分
}

關聯類型(associatedtype)

使用associatedtype來定義一個在協議中使用的關聯類型(可以理解為協議中的泛型)
此類型需要在實現協議的類中定義和指明

protocol Unitable {
    associatedtype Unit

    func calculatingUnit() -> Unit
}

class People: Unitable {
    typealias Unit = Int

    func calculatingUnit() -> Int {
        return 1
    }
}

class RMB: Unitable {
    typealias Unit = Double

    func calculatingUnit() -> Double {
        return 1.0
    }
}

上面是一個比較簡陋的例子,定義了一個單位計算協議,當People類遵循協議時,計算單位為Int,當RMB類遵循協議時,計算單位為Double

通過擴展遵循協議

可以通過擴展類型來遵循協議,可以為已有類型添加方法和屬性

class BTC {
    // ....
}

extension BTC: CoinType {

    func tradingPlatform() -> String {
        return "Binance"
    }

    func sell() {
        // sell
    }

    func buy() {
        // buy
    }
}

協議擴展

協議可以通過擴展來為遵循協議的類型提供屬性、方法以及下標的實現。通過這種方式,你可以基于協議本身來實現這些功能,而無需在每個遵循協議的類型中都重復同樣的實現,從而達到了協議的默認實現的功能,并且在協議擴展中還可以為協議添加限制條件

extension CoinType where Self: BTC {
    func tradingPlatform() -> String {
        return "default platform"
    }

    func sell() {
        print("sell all coin")
    }

    func buy() {
        print("buy BTC?")
    }
}

協議擴展中需要注意的兩點是:

1.通過協議擴展為協議要求提供的默認實現和可選的協議要求不同。雖然在這兩種情況下,遵循協議的類型都無需自己實現這些要求,但是通過擴展提供的默認實現可以直接調用,而無需使用可選鏈式調用。

2.如果多個協議擴展都為同一個協議要求提供了默認實現,而遵循協議的類型又同時滿足這些協議擴展的限制條件,那么將會使用限制條件最多的那個協議擴展提供的默認實現。


部分摘抄自官方文檔,更詳細的參見Swift-Protocol

簡單介紹完基本概念,我們來看看協議在Swift中的一些基礎庫中的應用

在講之前,我們大體可以把標準庫中的協議類型分為三種

  • Can do
  • Is a
  • Can be

1.Can do

表示的是協議能夠做某件事或者實現某些功能,最常見的一個例子是Hashable,遵循此協議的類型表示具有可hash的功能,這表示你可以得到這個類的整型散列值,把它當做一個字典的Key值等等。這種協議大都以able結尾,這也比較符合它的語義

類似的還有RawRepresentable這個協議,它能夠讓遵循它的類獲得類似于枚舉中的初始值的功能,可以從一個原始值來初始化,或者獲得類型對象的原始值

其實我們也可以使用基礎庫的一些協議來實現一些功能,比如使用RawRepresentable來規范和管理Storyboard中的界面跳轉
正常情況下我們的segue跳轉時一個controller會對應到一個identifier,而這個identifier由于多次使用分散在各處,很容易拼寫錯誤然后導致crash,
可以利用枚舉來嘗試下解決這個問題

首先我們定義一個Segueable的協議

protocol Segueable {
    associatedtype CustomSegueIdentifier: RawRepresentable

    func performCustomSegue(_ segue: CustomSegueIdentifier, sender: Any?)

    func customSegueIdentifier(forSegue segue: UIStoryboardSegue) -> CustomSegueIdentifier
}

定義了一個遵循RawRepresentable協議的關聯類型,兩個方法,一個跳轉的,一個獲取identifier的。

我們在擴展中給這兩個方法提供下默認實現,順便約束一下協議實現的類型

extension KYXSegueable where Self: UIViewController, CustomSegueIdentifier.RawValue == String {

    func performCustomSegue(_ segue: CustomSegueIdentifier, sender: Any?) {
        performSegue(withIdentifier: segue.rawValue, sender: sender)
    }

    func customSegueIdentifier(forSegue segue: UIStoryboardSegue) -> CustomSegueIdentifier {
        guard let identifier = segue.identifier, let customSegueIndentifier = CustomSegueIdentifier(rawValue: identifier) else {
            fatalError("Cannot get custom segue indetifier for segue: \(segue.identifier ?? "")")
        }

        return customSegueIndentifier
    }
}

我們可以這樣使用

class SegueTestViewController: UIViewController, KYXSegueable {

    typealias CustomSegueIdentifier = SegueType

    enum SegueType: String {
        case login = "loginSegue"
        case regist = "registSegue"
        case other = "otherSegue"
    }

    override func viewDidLoad() {
        super.viewDidLoad()

        // Do any additional setup after loading the view.
    }

    @IBAction func handleLoginButtonAction() {
        self.performCustomSegue(.login, sender: nil)
    }

    override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
        let segueType = self.customSegueIdentifier(forSegue: segue)

        switch segueType {
        case .login:
            print("login")

        case .regist:
            print("regist")

        default:
            print("other")
        }
    }
}

這樣我們就可以在提供的關聯類型中定義我們跳轉的identifier,使用枚舉的Switch來匹配和判斷不同的跳轉

2. Is a

這類在基礎庫中占大部分,大都基本上以Type結尾,簡單可以理解為是某種類型,表明遵守它的類具有某種身份, 擁有這種身份后可以擁有身份所具有的一些特征和功能。當然一個類型可以擁有多種身份,由于Swift中不支持多繼承,使用這種協議可以實現一些多繼承的場景。

常見的如ErrorType,表明當前類型具有可出現Error的身份,也就相應的具有處理error的功能和一些Error的特征。

值得注意的是在Swift3.0之后基礎庫中所有以Type結尾的 “is a”類型的協議,都統一去除了type字段,如ErrorType變成了ErrorCollectionType變成了Collection,這樣也更符合Swift語法簡練的特點和理念

3. Can be

可以成為** 可以轉換成,例如A可以轉換成為B,一般以 Convertible結尾。
如常見的
CustomStringConvertible**,實現以后可以自定義當前類的輸出

class Rectangle: CustomStringConvertible {
    var length = 10
    var width = 20

    var description: String {
        return "\(width * length)"
    }

    func log() {
        print(self)
        // 輸出面積 200
    }
}

再如CustomStringConvertible現在棄用改成了ExpressibleByStringLiteral,實現此協議的類型可以通過字面量的形式賦值初始化

struct People {
    var name: String = ""
    var age: Int = 0
    var gender: Int = 0
}

extension People: ExpressibleByStringLiteral {
    typealias StringLiteralType = String

    public init(stringLiteral value: String) {
        self = People()
        self.name = value
    }
}

這樣我們就可以直接通過字符串(人名)的方式來直接初始化一個People對象了

let xiaoming: People = "xiaoming"

或者擴展一下,這樣來操作一下

extension URL: ExpressibleByStringLiteral {
    public init(stringLiteral value: String) {
        guard let url = URL(string: value) else {
           preconditionFailure("url transform is failure")
        }
        self = url
    }
}

這樣我們就可以直接通過字面量的形式來創建和使用URL了

我們可以根據基礎庫協議庫中的幾個大的分類來選擇在我們業務開發中使用協議的場景和姿勢


那么為什么我們要使用協議呢,或者說使用協議編程能給我們帶來什么,能夠解決哪些痛點和問題呢,下面我們就來簡單的探討一下

Why is Protocol

舉個栗子,有如下的繼承關系的一個需求

image

我們使用傳統的面向對象的方式去解決這個問題時,思路大都如下


class Animal {
    var age: Int { return 0 }
    var gender: Bool { return true } //假設為true為雄性
    //.....等其他一些共有特征

    func eat() {
        print("eat food")
    }

    func excrete() {
        print("lababa")
    }
}

定義了一個動物的基類,定義了動物的一些共有屬性(動物特征)和一些共有方法(動物行為),我們的子類都要繼承于此基類,如下

class Cat: Animal {
    override var age: Int { return 1 }
    override var gender: Bool { return false }

    var legNum: Int = 4 //四條腿

    override func eat() {
        print("eat fish")
    }

    func run() {
        print("runing cat")
    }

    func catchMouse() {
        print("捉老鼠")
    }
}

class Eagle: Animal {
    override var age: Int { return 2 }

    var leg: Int = 2 //兩條腿
    var wing: Int = 2 //兩只翅膀

    override func eat() {
        print("eat meat")
    }

    func fly() {
        print("flying eagle")
    }
}

class Shark: Animal {
    override var age: Int { return 3 }
    override var gender: Bool { return false }

    var tooth: Int = 100 //反正很多...

    override func eat() {
        print("eat other fish")
    }

    func swim() {
        print("swimming shark")
    }
}

如上,我們的CatEagle、Shark分別通過基類的方式獲得了基類的屬性和一些方法,然后在子類里根據自身擴充一些屬性和方法。這么一看確實是沒什么問題。

于是接下來園長說,我們動物園的動物太少了,需要新增一批動物,而且還要和原來的一起按照動物的種類來進行合理的分區飼養管理。新增名單為以下幾位

image

于是我們立馬簡單明了的按照了動物種類來做了以下區分

image

如圖我們分別引入了哺乳動物、鳥類魚類這幾個細分的基類,來做更加細分的處理,比如這樣

//鳥類
class Birds: Animal {
    //鳥類的一些特征定義
}

//鴕鳥
class Ostrich: Birds {
    //..
}

于是問題就來了,按照如圖來進行區分和管理真的可靠嗎?我們知道大都哺乳動物是Runable的,但是很抱歉海豚是swim的,而不是Run;鴕鳥是Run的,而不是像大多數鳥類那樣是Fly的。我們的前輩們為了能夠對真實世界的對象進行建模,發展出了面向對象編程的概念,但是這套理念有一些缺陷。雖然我們努力用這套抽象和繼承的方法進行建模,但是實際的事物往往是一系列特質的組合,而不單單是以一脈相承并逐漸擴展的方式構建的。我們不能在哺乳動物中定義通用的Run方法,因為它并不適用于所有的哺乳動物比如海豚,并且它還可以能適用于其他類型的對象,比如鴕鳥。那么我們怎么才能夠在相同的繼承關系(但是代碼并不通用)和不同的繼承關系的對象間共用代碼呢。

傳統的做法是

1.粘貼/復制:當然這種做法方便快捷,但是方式非常糟糕

2.引入一個基類,在基類中定義通用的屬性和方法;這種做法稍微靠譜點,但是基類會變得愈加臃腫,部分子類還會獲得一些本身不需要的屬性和方法。以后管理起來也是個大包袱

3.多繼承:遺憾的是在iOS的世界里并不支持。

4.引入帶有相關屬性和方法的依賴對象,好像引入額外的依賴也并不是合適的方式

那么我們如何使用面向協議的姿勢來解決上面的問題呢

@objc protocol Runable {
   @objc optional func run()
}

@objc protocol Swimable {
   @objc optional func swim()
}

@objc protocol Flyable {
    @objc optional func fly()
}

我們定義了兩個協議,分別是RunableSwimable,具有某種特性的動物只要實現對應的協議就可以擁有其相應的行為。比如

//鳥類
class Birds: Animal, Flyable {
    //鳥類的一些特征定義
}

//鴕鳥
class Ostrich: Birds, Runable {
    func run() {
        print("i am ostrich, i can run")
    }
}

//老鷹
class Eagle: Birds {
    override var age: Int { return 2 }

    var leg: Int = 2 //兩條腿
    var wing: Int = 2 //兩只翅膀

    override func eat() {
        print("eat meat")
    }

    func fly() {
        print("flying eagle")
    }
}

再或者我們做的更干脆一點,拋掉Animal基類,來定義一個Animal的協議,任何滿足此協議的對象都可以理解為是一個Animal,如下

@objc protocol Animal {
    @objc optional var age: Int { get set }
    @objc optional var gender: Bool { get set }

    @objc optional func eat()
    @objc optional func gender()
}

于是我們上面的代碼可以變成這樣

//鳥類
class Birds: Animal, Flyable {
    //鳥類的一些特征定義
}

//鴕鳥
class Ostrich: Birds, Runable {
    func run() {
        print("i am ostrich, i can run")
    }
}

//老鷹
class Eagle: Birds {
    var age: Int = 2

    var leg: Int = 2 //兩條腿
    var wing: Int = 2 //兩只翅膀

    func eat() {
        print("eat meat")
    }

    func fly() {
        print("flying eagle")
    }
}

以上基本解決了我們面向對象編程時所面臨的一些問題,而且具有高度的靈活性和更低的耦合性。

記得下次有新的需求時,先想想用Protocol來實現怎么樣?

原文鏈接:http://www.lxweimin.com/p/26be1e04ea1a

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,546評論 6 533
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,570評論 3 418
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 176,505評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,017評論 1 313
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,786評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,219評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,287評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,438評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,971評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,796評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,995評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,540評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,230評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,662評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,918評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,697評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,991評論 2 374