簡單聊聊Swift中的Protocol

Swift中的Protocol

眾所周知雾狈,Swift是一門面向協(xié)議編程(Protocol Oriented Programming 以下簡稱POP)的語言,其中許多標(biāo)準(zhǔn)庫均是基于此來實(shí)現(xiàn)的。由于以往使用面向?qū)ο蟮恼Z言的慣性缘眶,以至于實(shí)際開發(fā)中并沒有養(yǎng)成面向協(xié)議編程的思維習(xí)慣翰舌。本文將簡單來聊聊Swift中的Protocol,以及我們?yōu)槭裁匆嫦騪rotocol編程唤锉,以加深對其的印象和了解世囊。

Swift協(xié)議的基本功能

協(xié)議方法

協(xié)議可以要求遵循協(xié)議的類型實(shí)現(xiàn)某些指定的實(shí)例方法或類方法。不支持為協(xié)議中的方法的參數(shù)提供默認(rèn)值窿祥。功能和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-C株憾,Swift中的協(xié)議提供了一些更加豐富的功能

協(xié)議屬性

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

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

協(xié)議作為類型

協(xié)議可以像其他普通類型一樣使用墙歪,使用場景如下:

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

代理模式

代理模式,很常用的一種設(shè)計(jì)模式贝奇;不管是Cocoa還是日常開發(fā)中都能澈绶疲看到

協(xié)議支持繼承、聚合

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

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

有時(shí)候需要同時(shí)遵循多個協(xié)議陕习,你可以將多個協(xié)議采用 SomeProtocol & AnotherProtocol 這樣的格式進(jìn)行組合霎褐,稱為 協(xié)議合成(protocol composition)。你可以羅列任意多個你想要遵循的協(xié)議该镣,以與符號(&)分隔冻璃。

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

關(guān)聯(lián)類型(associatedtype)

使用associatedtype來定義一個在協(xié)議中使用的關(guān)聯(lián)類型(可以理解為協(xié)議中的泛型)
此類型需要在實(shí)現(xiàn)協(xié)議的類中定義和指明

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
    }
}

上面是一個比較簡陋的例子,定義了一個單位計(jì)算協(xié)議损合,當(dāng)People類遵循協(xié)議時(shí)省艳,計(jì)算單位為Int,當(dāng)RMB類遵循協(xié)議時(shí)塌忽,計(jì)算單位為Double

通過擴(kuò)展遵循協(xié)議

可以通過擴(kuò)展類型來遵循協(xié)議拍埠,可以為已有類型添加方法和屬性

class BTC {
    // ....
}

extension BTC: CoinType {

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

協(xié)議擴(kuò)展

協(xié)議可以通過擴(kuò)展來為遵循協(xié)議的類型提供屬性、方法以及下標(biāo)的實(shí)現(xiàn)土居。通過這種方式枣购,你可以基于協(xié)議本身來實(shí)現(xiàn)這些功能,而無需在每個遵循協(xié)議的類型中都重復(fù)同樣的實(shí)現(xiàn)擦耀,從而達(dá)到了協(xié)議的默認(rèn)實(shí)現(xiàn)的功能棉圈,并且在協(xié)議擴(kuò)展中還可以為協(xié)議添加限制條件

extension CoinType where Self: BTC {
    func tradingPlatform() -> String {
        return "default platform"
    }
    
    func sell() {
        print("sell all coin")
    }
    
    func buy() {
        print("buy BTC?")
    }
}

協(xié)議擴(kuò)展中需要注意的兩點(diǎn)是:

1.通過協(xié)議擴(kuò)展為協(xié)議要求提供的默認(rèn)實(shí)現(xiàn)和可選的協(xié)議要求不同。雖然在這兩種情況下眷蜓,遵循協(xié)議的類型都無需自己實(shí)現(xiàn)這些要求分瘾,但是通過擴(kuò)展提供的默認(rèn)實(shí)現(xiàn)可以直接調(diào)用,而無需使用可選鏈?zhǔn)秸{(diào)用吁系。

2.如果多個協(xié)議擴(kuò)展都為同一個協(xié)議要求提供了默認(rèn)實(shí)現(xiàn)德召,而遵循協(xié)議的類型又同時(shí)滿足這些協(xié)議擴(kuò)展的限制條件,那么將會使用限制條件最多的那個協(xié)議擴(kuò)展提供的默認(rèn)實(shí)現(xiàn)汽纤。


部分摘抄自官方文檔上岗,更詳細(xì)的參見Swift-Protocol

簡單介紹完基本概念,我們來看看協(xié)議在Swift中的一些基礎(chǔ)庫中的應(yīng)用

在講之前蕴坪,我們大體可以把標(biāo)準(zhǔn)庫中的協(xié)議類型分為三種

  • Can do
  • Is a
  • Can be

1.Can do

表示的是協(xié)議能夠做某件事或者實(shí)現(xiàn)某些功能肴掷,最常見的一個例子是Hashable敬锐,遵循此協(xié)議的類型表示具有可hash的功能,這表示你可以得到這個類的整型散列值呆瞻,把它當(dāng)做一個字典的Key值等等台夺。這種協(xié)議大都以able結(jié)尾,這也比較符合它的語義

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

其實(shí)我們也可以使用基礎(chǔ)庫的一些協(xié)議來實(shí)現(xiàn)一些功能明郭,比如使用RawRepresentable來規(guī)范和管理Storyboard中的界面跳轉(zhuǎn)
正常情況下我們的segue跳轉(zhuǎn)時(shí)一個controller會對應(yīng)到一個identifier买窟,而這個identifier由于多次使用分散在各處,很容易拼寫錯誤然后導(dǎo)致crash薯定,
可以利用枚舉來嘗試下解決這個問題

首先我們定義一個Segueable的協(xié)議

protocol Segueable {
    associatedtype CustomSegueIdentifier: RawRepresentable
    
    func performCustomSegue(_ segue: CustomSegueIdentifier, sender: Any?)
    
    func customSegueIdentifier(forSegue segue: UIStoryboardSegue) -> CustomSegueIdentifier
}

定義了一個遵循RawRepresentable協(xié)議的關(guān)聯(lián)類型,兩個方法瞳购,一個跳轉(zhuǎn)的话侄,一個獲取identifier的。

我們在擴(kuò)展中給這兩個方法提供下默認(rèn)實(shí)現(xiàn)学赛,順便約束一下協(xié)議實(shí)現(xiàn)的類型

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")
        }
    }
}

這樣我們就可以在提供的關(guān)聯(lián)類型中定義我們跳轉(zhuǎn)的identifier年堆,使用枚舉的Switch來匹配和判斷不同的跳轉(zhuǎn)

2. Is a

這類在基礎(chǔ)庫中占大部分,大都基本上以Type結(jié)尾盏浇,簡單可以理解為是某種類型变丧,表明遵守它的類具有某種身份, 擁有這種身份后可以擁有身份所具有的一些特征和功能。當(dāng)然一個類型可以擁有多種身份绢掰,由于Swift中不支持多繼承痒蓬,使用這種協(xié)議可以實(shí)現(xiàn)一些多繼承的場景。

常見的如ErrorType滴劲,表明當(dāng)前類型具有可出現(xiàn)Error的身份攻晒,也就相應(yīng)的具有處理error的功能和一些Error的特征。

值得注意的是在Swift3.0之后基礎(chǔ)庫中所有以Type結(jié)尾的 “is a”類型的協(xié)議班挖,都統(tǒng)一去除了type字段鲁捏,如ErrorType變成了ErrorCollectionType變成了Collection萧芙,這樣也更符合Swift語法簡練的特點(diǎn)和理念

3. Can be

可以成為** 可以轉(zhuǎn)換成给梅,例如A可以轉(zhuǎn)換成為B,一般以 Convertible結(jié)尾双揪。
如常見的
CustomStringConvertible**动羽,實(shí)現(xiàn)以后可以自定義當(dāng)前類的輸出

class Rectangle: CustomStringConvertible {
    var length = 10
    var width = 20
    
    var description: String {
        return "\(width * length)"
    }
    
    func log() {
        print(self)
        // 輸出面積 200
    }
}

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

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"

或者擴(kuò)展一下盟榴,這樣來操作一下

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

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

我們可以根據(jù)基礎(chǔ)庫協(xié)議庫中的幾個大的分類來選擇在我們業(yè)務(wù)開發(fā)中使用協(xié)議的場景和姿勢


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

Why is Protocol

舉個栗子羽德,有如下的繼承關(guān)系的一個需求


E843D966-49C9-401C-9CDB-C0A982D03DF6.png

我們使用傳統(tǒng)的面向?qū)ο蟮姆绞饺ソ鉀Q這個問題時(shí)几莽,思路大都如下


class Animal {
    var age: Int { return 0 }
    var gender: Bool { return true } //假設(shè)為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")
    }
}


如上,我們的Cat姨夹、Eagle纤垂、Shark分別通過基類的方式獲得了基類的屬性和一些方法,然后在子類里根據(jù)自身擴(kuò)充一些屬性和方法磷账。這么一看確實(shí)是沒什么問題峭沦。

于是接下來園長說,我們動物園的動物太少了逃糟,需要新增一批動物吼鱼,而且還要和原來的一起按照動物的種類來進(jìn)行合理的分區(qū)飼養(yǎng)管理。新增名單為以下幾位

76EC9504-C3F5-4B28-9082-BF6EB7949AFC.png

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

02CDBE2B-1618-41D2-B834-252E8FD1BBA9.png

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

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

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

于是問題就來了取募,按照如圖來進(jìn)行區(qū)分和管理真的可靠嗎琐谤?我們知道大都哺乳動物是Runable的,但是很抱歉海豚是swim的玩敏,而不是Run斗忌;鴕鳥是Run的,而不是像大多數(shù)鳥類那樣是Fly的聊品。我們的前輩們?yōu)榱四軌驅(qū)φ鎸?shí)世界的對象進(jìn)行建模飞蹂,發(fā)展出了面向?qū)ο缶幊痰母拍睿沁@套理念有一些缺陷翻屈。雖然我們努力用這套抽象和繼承的方法進(jìn)行建模陈哑,但是實(shí)際的事物往往是一系列特質(zhì)的組合,而不單單是以一脈相承并逐漸擴(kuò)展的方式構(gòu)建的伸眶。我們不能在哺乳動物中定義通用的Run方法惊窖,因?yàn)樗⒉贿m用于所有的哺乳動物比如海豚,并且它還可以能適用于其他類型的對象厘贼,比如鴕鳥界酒。那么我們怎么才能夠在相同的繼承關(guān)系(但是代碼并不通用)和不同的繼承關(guān)系的對象間共用代碼呢。

傳統(tǒng)的做法是

1.粘貼/復(fù)制:當(dāng)然這種做法方便快捷嘴秸,但是方式非常糟糕

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

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

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

那么我們?nèi)绾问褂妹嫦騾f(xié)議的姿勢來解決上面的問題呢

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

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

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

我們定義了兩個協(xié)議寞肖,分別是RunableSwimable,具有某種特性的動物只要實(shí)現(xiàn)對應(yīng)的協(xié)議就可以擁有其相應(yīng)的行為衰腌。比如

//鳥類
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")
    }
}

再或者我們做的更干脆一點(diǎn)新蟆,拋掉Animal基類,來定義一個Animal的協(xié)議右蕊,任何滿足此協(xié)議的對象都可以理解為是一個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")
    }
}

以上基本解決了我們面向?qū)ο缶幊虝r(shí)所面臨的一些問題,而且具有高度的靈活性和更低的耦合性尤泽。

記得下次有新的需求時(shí)欣簇,先想想用Protocol來實(shí)現(xiàn)怎么樣?

關(guān)于Protocol的更進(jìn)一步進(jìn)階的使用例子請前往喵神的面向協(xié)議編程與 Cocoa 的邂逅 (下)


參考:

面向協(xié)議編程與 Cocoa 的邂逅 (上)

我從55個Swift標(biāo)準(zhǔn)庫協(xié)議中學(xué)到了什么?

Swift中協(xié)議的簡單介紹

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末坯约,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子莫鸭,更是在濱河造成了極大的恐慌闹丐,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,188評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件被因,死亡現(xiàn)場離奇詭異卿拴,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)梨与,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,464評論 3 395
  • 文/潘曉璐 我一進(jìn)店門堕花,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人粥鞋,你說我怎么就攤上這事缘挽。” “怎么了呻粹?”我有些...
    開封第一講書人閱讀 165,562評論 0 356
  • 文/不壞的土叔 我叫張陵壕曼,是天一觀的道長。 經(jīng)常有香客問我等浊,道長腮郊,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,893評論 1 295
  • 正文 為了忘掉前任筹燕,我火速辦了婚禮轧飞,結(jié)果婚禮上衅鹿,老公的妹妹穿的比我還像新娘。我一直安慰自己过咬,他們只是感情好大渤,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,917評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著援奢,像睡著了一般兼犯。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上集漾,一...
    開封第一講書人閱讀 51,708評論 1 305
  • 那天切黔,我揣著相機(jī)與錄音,去河邊找鬼具篇。 笑死纬霞,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的驱显。 我是一名探鬼主播诗芜,決...
    沈念sama閱讀 40,430評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼埃疫!你這毒婦竟也來了伏恐?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,342評論 0 276
  • 序言:老撾萬榮一對情侶失蹤栓霜,失蹤者是張志新(化名)和其女友劉穎翠桦,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體胳蛮,經(jīng)...
    沈念sama閱讀 45,801評論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡销凑,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,976評論 3 337
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了仅炊。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片斗幼。...
    茶點(diǎn)故事閱讀 40,115評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖抚垄,靈堂內(nèi)的尸體忽然破棺而出蜕窿,到底是詐尸還是另有隱情,我是刑警寧澤督勺,帶...
    沈念sama閱讀 35,804評論 5 346
  • 正文 年R本政府宣布渠羞,位于F島的核電站,受9級特大地震影響智哀,放射性物質(zhì)發(fā)生泄漏次询。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,458評論 3 331
  • 文/蒙蒙 一瓷叫、第九天 我趴在偏房一處隱蔽的房頂上張望屯吊。 院中可真熱鬧送巡,春花似錦、人聲如沸盒卸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,008評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽蔽介。三九已至摘投,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間虹蓄,已是汗流浹背犀呼。 一陣腳步聲響...
    開封第一講書人閱讀 33,135評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留薇组,地道東北人外臂。 一個月前我還...
    沈念sama閱讀 48,365評論 3 373
  • 正文 我出身青樓,卻偏偏與公主長得像律胀,于是被迫代替她去往敵國和親宋光。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,055評論 2 355