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變成了Error,CollectionType變成了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)系的一個需求
我們使用傳統(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)管理。新增名單為以下幾位
于是我們立馬簡單明了的按照了動物種類來做了以下區(qū)分
如圖我們分別引入了哺乳動物绰咽、鳥類和魚類這幾個細(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é)議寞肖,分別是Runable和Swimable,具有某種特性的動物只要實(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 的邂逅 (下)
參考: