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-C,Swift中的協議提供了一些更加豐富的功能
協議屬性
協議可以要求遵循協議的類型提供特定名稱和類型的實例屬性或類型屬性,它只指定屬性的名稱和類型,協議還指定屬性是可讀的還是可讀可寫的。
protocol CoinProtocol {
var name: String {get}
var price: Double {get set}
}
協議作為類型
協議可以像其他普通類型一樣使用,使用場景如下:
- 作為函數方法的參數或者返回值類型
- 作為常量變量或者屬性的類型
- 作為集合中元素的類型
代理模式
代理模式,很常用的一種設計模式;不管是Cocoa還是日常開發中都能常看到
協議支持繼承、聚合
協議能夠繼承一個或多個其他協議,可以在繼承的協議的基礎上增加新的要求。協議的繼承語法與類的繼承相似,多個被繼承的協議間用逗號分隔:
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變成了Error,CollectionType變成了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
舉個栗子,有如下的繼承關系的一個需求
我們使用傳統的面向對象的方式去解決這個問題時,思路大都如下
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")
}
}
如上,我們的Cat、Eagle、Shark分別通過基類的方式獲得了基類的屬性和一些方法,然后在子類里根據自身擴充一些屬性和方法。這么一看確實是沒什么問題。
于是接下來園長說,我們動物園的動物太少了,需要新增一批動物,而且還要和原來的一起按照動物的種類來進行合理的分區飼養管理。新增名單為以下幾位
于是我們立馬簡單明了的按照了動物種類來做了以下區分
如圖我們分別引入了哺乳動物、鳥類和魚類這幾個細分的基類,來做更加細分的處理,比如這樣
//鳥類
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()
}
我們定義了兩個協議,分別是Runable和Swimable,具有某種特性的動物只要實現對應的協議就可以擁有其相應的行為。比如
//鳥類
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來實現怎么樣?
關于Protocol的更進一步進階的使用例子請前往喵神的面向協議編程與 Cocoa 的邂逅 (下)
參考: