WWDC 2015 - Session 408 - Protocol Oriented Programming in Swift

由 Dave Abrahams Professor 演講,這貨是C++牛人律罢,自行wiki。

筆記先從 Object Oriented Programming - OOP(面向?qū)ο缶幊蹋┱f(shuō)起棍丐,自然少不了 Class 這個(gè)主角误辑,當(dāng)初我入坑 iOS 時(shí)就問(wèn)過(guò)一個(gè)問(wèn)題:Class 和 Struct 的區(qū)別以及應(yīng)用場(chǎng)景?

當(dāng)然本文中不會(huì)詳盡地去比較孰優(yōu)孰劣歌逢,只是簡(jiǎn)單闡述下巾钉,僅供參考。

Classes Are Awesome

  • Encapsulation(封裝)
  • Access Control(訪問(wèn)控制)
  • Abstraction(抽象)
  • Namespace(命名空間)
  • Expressive Syntax(表達(dá)語(yǔ)法)
  • Extensibility(拓展性)

其中“Access Control”秘案、“Abstraction”和“Namespace” 指明是 Class 令人頭疼之處砰苍。

先說(shuō)說(shuō)繼承性,Class 完勝 Structure阱高,記住Structure是不具有繼承性的赚导!

類(lèi)可以將對(duì)象抽象成多個(gè)描述屬性和方法,可以通過(guò)繼承得到子類(lèi)赤惊,也就是傳統(tǒng)意義上的superclass 和 subclass吼旧。 子類(lèi)可以從父類(lèi)繼承一系列方法,當(dāng)然也可以通過(guò) override 重寫(xiě)父類(lèi)的方法未舟。

The Three Beef

  • Implicit Sharing
  • Inheritance All Up In Your Business
  • Lost Type relationShips

第一點(diǎn):Class 實(shí)例需要在堆上分配內(nèi)存圈暗,此時(shí)有兩個(gè)對(duì)象A和B要操作該實(shí)例,那么傳遞給A和B類(lèi)實(shí)例的指針(內(nèi)存地址)即可裕膀,只有一個(gè)線(xiàn)程時(shí)不會(huì)有任何問(wèn)題员串,凡是有個(gè)先來(lái)后到,A操作完實(shí)例后B再接手操作對(duì)象昼扛;說(shuō)說(shuō)多線(xiàn)程情況寸齐,假設(shè)A在線(xiàn)程1對(duì)實(shí)例進(jìn)行讀操作,B在線(xiàn)程2對(duì)實(shí)例進(jìn)行寫(xiě)操作,來(lái)個(gè)巧合吧访忿!假設(shè)A在讀取實(shí)例中的數(shù)據(jù)量較大的數(shù)組(指向讀取舊數(shù)據(jù)),而B(niǎo)此刻卻在修改old數(shù)據(jù)斯稳,用new數(shù)據(jù)覆蓋海铆,美名其曰更新。此時(shí)意外就產(chǎn)生了挣惰!A寶寶心里苦啊卧斟,我只是想讀取舊數(shù)據(jù),你卻馬不停蹄地覆蓋舊數(shù)據(jù)憎茂。所以嘍珍语,以上情況想要解決就是復(fù)制一份實(shí)例! 但是過(guò)多的copy是否會(huì)讓你在操作時(shí)提心吊膽竖幔?

第二點(diǎn):Class 只能有一個(gè)父類(lèi)板乙,意味著只能從一個(gè)父類(lèi)繼承,而不能繼承多個(gè)類(lèi)拳氢。打個(gè)比方吧募逞,現(xiàn)在具有類(lèi)A和類(lèi)B,C具有A的所有特性馋评,所以嘍Class C:A{}放接,C是A的子類(lèi);巧了留特!C具有B的所有特性纠脾,所以嘍Class C:B{}; 但是卻沒(méi)有類(lèi)似Class C:A,B{}這種多繼承寫(xiě)法蜕青。

關(guān)于第三點(diǎn)苟蹈,視頻舉了個(gè)binary Search(二分法查找) 例子:

// 對(duì)于有序隊(duì)列的一個(gè)抽象類(lèi) 關(guān)于隊(duì)列元素的類(lèi)型可以是Int String Double 等等
// 所以我說(shuō)這是一個(gè)抽象類(lèi)!
class Ordered {
  func precedes(other: Ordered) -> Bool{
    fatalError("implement me!")
  }
}
// 全局函數(shù) 傳入一個(gè)排序好的數(shù)組 以及要查找的key值 通過(guò)二分法搜索返回索引值
func binarySearch(sortedKeys: [Ordered], forKey k: Ordered) -> Int {
  var lo = 0, hi = sortedKeys.count
  while hi > lo {
    let mid = lo + (hi - lo) / 2
    if sortedKeys[mid].precedes(k) { lo = mid + 1 }
    else { hi = mid }
}
  return lo 
}

Question: 為什么不實(shí)現(xiàn)Ordered類(lèi)中precedes方法右核?

首先已經(jīng)強(qiáng)調(diào)了Ordered是抽象類(lèi)汉操,當(dāng)然這并不是站得住腳的理由,看看下面兩個(gè)繼承自它的子類(lèi)蒙兰。

// 這是一個(gè)標(biāo)簽 所以包含String類(lèi)型的text屬性
class Label:Ordered{
  var text:String = ""
  ...
}
// 這是一個(gè)數(shù)字 所以包含Double類(lèi)型的value屬性
class Number : Ordered {
  var value: Double = 0
  override func precedes(other: Ordered) -> Bool {
    return value < other.value
  }
}
// 還有其他類(lèi)型的Ordered 子類(lèi)

Label 嚴(yán)格意義上來(lái)說(shuō)是一個(gè)Ordered類(lèi)磷瘤,同理 Number 也是,因?yàn)樗麄兝^承自 Ordered搜变,具有其所有的特性采缚;剛才說(shuō)到你要為Ordered類(lèi)實(shí)現(xiàn)precedes方法?請(qǐng)你告訴我對(duì)于傳入的 Ordered 類(lèi)到底是 Label 呢挠他,還是 Number 呢扳抽,亦或是其他呢? 要知道你根本無(wú)法確定!所以我們?cè)贠rdered類(lèi)中是不能實(shí)現(xiàn)的贸呢,而是讓子類(lèi)去重寫(xiě)實(shí)現(xiàn)镰烧。理解了這點(diǎn)繼續(xù)下面的內(nèi)容。

你會(huì)注意上面的 Number 類(lèi)中的 precedes 方法有點(diǎn)問(wèn)題:

class Number : Ordered {
  var value: Double = 0
  override func precedes(other: Ordered) -> Bool {
    // 報(bào)錯(cuò)@阆荨U睢! 很好理解固蛾,傳入的 other 為 Ordered 類(lèi)即可
    // 但是 Ordered 可沒(méi)有 value屬性结执! 所以我們需要進(jìn)行向下(父類(lèi)->子類(lèi))cast
    return value < other.value
  }
}

修改如下:

class Number : Ordered {
  var value: Double = 0
  // 實(shí)現(xiàn)也有缺陷 見(jiàn)下
  override func precedes(other: Ordered) -> Bool {
    return value < (other as! Number).value
  }
}

這里又有一個(gè)問(wèn)題,代碼要求傳入 Ordered 類(lèi)進(jìn)行處理艾凯,那么Number 和Label 嚴(yán)格意義上來(lái)說(shuō)都是 Ordered 類(lèi)(這里要理解因?yàn)樗鼈兌际荗rdered的子類(lèi))献幔,所以往代碼中傳入 Number 是Ok的, 而傳入Label類(lèi)時(shí)就很有問(wèn)題了趾诗, 因?yàn)橄蛳耤ast會(huì)出問(wèn)題(人家明明是Label 蜡感,你想要 as! 到 Number 類(lèi),絕壁失敗 程序crash)恃泪。所以嘍 問(wèn)題多多铸敏!

所以Abrahams提出了幾個(gè)不錯(cuò)的抽象機(jī)制:

  • Supports value types (and classes)
  • Supports static type relationships (and dynamic dispatch) Non-monolithic
  • Supports retroactive modeling
  • Doesn’t impose instance data on models
  • Doesn’t impose initialization burdens on models
  • Makes clear what to implement

面向協(xié)議編程

英文 Protocol-Oriented Programming ,面向協(xié)議編程自然少不了協(xié)議悟泵。簡(jiǎn)單來(lái)說(shuō)杈笔,首先將對(duì)象屬性(property)和行為(behavior)抽象成實(shí)例屬性和方法;將這些準(zhǔn)則整合成協(xié)議糕非;最后讓對(duì)象遵循這個(gè)協(xié)議并實(shí)現(xiàn)協(xié)議中的內(nèi)容即可蒙具。

改寫(xiě)上面例子:

protocol Ordered {
  func precedes(other: Ordered) -> Bool
}

現(xiàn)在 Ordered 是一個(gè)協(xié)議,它的準(zhǔn)則只有一個(gè) precedes 朽肥,因?yàn)槭菂f(xié)議禁筏,所以不需要具體實(shí)現(xiàn)。

現(xiàn)在我們說(shuō) Number 類(lèi)是有序的衡招,所以只需要遵循這個(gè)協(xié)議并實(shí)現(xiàn)要求的內(nèi)容即算滿(mǎn)足篱昔。

protocol Ordered {
  func precedes(other: Ordered) -> Bool
}
// 既然是POP 而非OOP,摒棄類(lèi)吧始腾,改成Struct 值類(lèi)型哦州刽!
struct Number : Ordered {
  var value: Double = 0
  // 去掉override 因?yàn)槲覀儾辉偈侵貙?xiě)父類(lèi)的方法 而是遵循協(xié)議
  func precedes(other: Ordered) -> Bool {
    return value < (other as! Number).value
  }
}

還沒(méi)完,對(duì)于傳入的other類(lèi)型為 Ordered 類(lèi)型浪箭,早前因?yàn)槲覀兪侵貙?xiě)父類(lèi)方法穗椅,所以類(lèi)型上要保持一致,但是現(xiàn)在是POP奶栖,所以我們現(xiàn)在可以任性了匹表!改成Number门坷,去掉as! Number 這難看的轉(zhuǎn)換。

修改如下:

protocol Ordered {
  func precedes(other: Ordered) -> Bool
}
// 既然是POP 而非OOP袍镀,摒棄類(lèi)吧默蚌,改成Struct 值類(lèi)型哦!
struct Number : Ordered {
  var value: Double = 0
  // 去掉override 因?yàn)槲覀儾辉偈侵貙?xiě)父類(lèi)的方法 而是遵循協(xié)議
  func precedes(other: Number) -> Bool {
    return value < other.value
  }
}

不幸地是苇羡,編譯器報(bào)錯(cuò)“protocol requires function 'precedes' with type'(Ordered)->Bool' candidate has non-matching type '(Number)->Bool'”绸吸,不難理解,協(xié)議方法要求傳入 Ordered 類(lèi)型宣虾,而我們?cè)趯?shí)現(xiàn)時(shí)卻傳入 Number 類(lèi)型,嚴(yán)格來(lái)說(shuō)我們并未遵循Ordered協(xié)議温数。

值得引起注意绣硝,以及值得思考和牢記的地方:將協(xié)議中的Ordered類(lèi)型改寫(xiě)成 Self,這叫 “Self” requirement撑刺, Self 代表任何遵循這個(gè)協(xié)議的類(lèi)自身鹉胖,譬如 Number遵循 Ordered協(xié)議,那么實(shí)現(xiàn)的方法中 Self 會(huì)替換成 Number够傍; Label 遵循Ordered協(xié)議甫菠,那么其實(shí)現(xiàn)的方法中 Self 會(huì)替換成 Label,以此類(lèi)推冕屯。

protocol Ordered {
  func precedes(other: Self) -> Bool
}
struct Number : Ordered {
  var value: Double = 0
  func precedes(other: Number) -> Bool {
    return self.value < other.value
  }
}

再來(lái)看看二分法查找全局函數(shù)的實(shí)現(xiàn):

func binarySearch(sortedKeys: [Ordered], forKey k: Ordered) -> Int {
  var lo = 0
  var hi = sortedKeys.count
  while hi > lo {
    let mid = lo + (hi - lo) / 2
    if sortedKeys[mid].precedes(k) { lo = mid + 1 }
    else { hi = mid }
}
return lo 
}

編譯器又報(bào)錯(cuò)拉寂诱! “protocol 'Ordered' can only be used as a generic constraint because it has Self or associated type requirements”。 因?yàn)槲覀兪褂?Self 替換了明確的類(lèi)安聘,所以我們只能通過(guò)添加泛型約束才能解決問(wèn)題痰洒。ps:出錯(cuò)原因并非是函數(shù)傳入?yún)?shù)類(lèi)型為Ordered協(xié)議!浴韭! 很好奇為什么使用Self 或 關(guān)聯(lián)類(lèi)型就只能使用泛型約束了....2016/07/09 update: 感謝way的回答丘喻,實(shí)際上這樣的,盡管sortedKeysforKey k 類(lèi)型都是Ordered念颈,但這并不能保證兩個(gè)變量類(lèi)型保持一致泉粉,可能前者是Number,后者是 Label呢榴芳;而我們期望 xx.preceseds(yy)這種方式調(diào)用時(shí) xx 和 yy 的類(lèi)型是相同的嗡靡,這也是協(xié)議明確說(shuō)明的,說(shuō)直白一些我們希望 xx 和 yy 的類(lèi)型都是 T窟感,但是類(lèi)型 T 必須遵循(實(shí)現(xiàn)) Ordered 協(xié)議叽躯。這也是為什么要用泛型約束了。

修改如下:

func binarySearch<T : Ordered>(sortedKeys: [T], forKey k: T) -> Int {
  var lo = 0
  var hi = sortedKeys.count
  while hi > lo {
    let mid = lo + (hi - lo) / 2
    if sortedKeys[mid].precedes(k) { lo = mid + 1 }
    else { hi = mid }
}
return lo 
}

基于Protocol Oriented Programming 的繪圖例子

畫(huà)圖自然逃不了移動(dòng)點(diǎn)肌括,描繪線(xiàn)和圓弧等基本操作点骑,所以聲明一個(gè)描繪器 Renderer 合情合理吧:

struct Renderer {
  // 以下幾個(gè)基本繪圖操作
  func moveTo(p: CGPoint) { print("moveTo(\(p.x), \(p.y))") }
  func lineTo(p: CGPoint) { print("lineTo(\(p.x), \(p.y))") }
  func arcAt(center: CGPoint, radius: CGFloat,
             startAngle: CGFloat, endAngle: CGFloat) {
      print("arcAt(\(center), radius: \(radius),"
        + " startAngle: \(startAngle), endAngle: \(endAngle))")
  } 
}

接下來(lái)酣难,思考我們要繪制的圖形可不是簡(jiǎn)單的一個(gè)點(diǎn)或一條直線(xiàn),有可能是多邊形黑滴,復(fù)合圖形等等憨募。但終究逃不出“畫(huà)”這個(gè)動(dòng)詞以及需要一個(gè)描繪器Renderer。所以嘍袁辈,指定一個(gè)協(xié)議吧菜谣,它要求傳入一個(gè)renderer進(jìn)行繪圖:

protocol Drawable {
  func draw(renderer: Renderer)
}

接下來(lái)想想如何描繪一個(gè)多邊形?貌似只要告知它起始點(diǎn)位置晚缩,要連接的下一個(gè)點(diǎn)位置就可以了吧尾膊!所以聲明一個(gè)Polygon結(jié)構(gòu)體(值類(lèi)型)如下:

struct Polygon : Drawable {
  func draw(renderer: Renderer) {
    // 移動(dòng)到指定點(diǎn)
    renderer.moveTo(corners.last!)
    // 只要按順序連接點(diǎn)即可
    for p in corners {
      renderer.lineTo(p)
    }
  }
  // 一系列有循序的鏈接點(diǎn)
  var corners: [CGPoint] = []
}

繪制一個(gè)圓需要知道圓心位置和半徑長(zhǎng)度即可:

struct Circle : Drawable {
  func draw(renderer: Renderer) {
    renderer.arcAt(center, radius: radius,
      startAngle: 0.0, endAngle: twoPi)
  }
  var center: CGPoint
  var radius: CGFloat
}

不管是繪制 Polygon 還是 Circle ,僅僅只是一個(gè)圖形罷了≤癖耍現(xiàn)在我們要更進(jìn)一步冈敛,告訴我要繪制的所有圖形(多邊形,圓形鸣皂,橢圓等等)抓谴,然后我將所有圖形都繪制到一張圖表上:

// 依舊遵循 Drawable 協(xié)議
struct Diagram : Drawable {
  func draw(renderer: Renderer) {
    for f in elements {
      f.draw(renderer)
    } 
  }
  // 傳入要描繪的圖形
  var elements: [Drawable] = []
}

Test It!

是時(shí)候測(cè)試下了!

var circle = Circle(center:
  CGPoint(x: 187.5, y: 333.5),
  radius: 93.75)
var triangle = Polygon(corners: [
  CGPoint(x: 187.5, y: 427.25),
  CGPoint(x: 268.69, y: 286.625),
  CGPoint(x: 106.31, y: 286.625)])
var diagram = Diagram(elements: [circle, triangle])
diagram.draw(Renderer())
/// 終端打印信息
/*
$ ./test
arcAt((187.5, 333.5),
radius: 93.75, startAngle: 0.0,
endAngle: 6.28318530717959)
moveTo(106.310118395209, 286.625)
lineTo(187.5, 427.25)
lineTo(268.689881604791, 286.625)
lineTo(106.310118395209, 286.625)
$
*/

面向?qū)ο蟾M(jìn)一步

前面的 Renderer 作為結(jié)構(gòu)體存在寞缝,負(fù)責(zé)移動(dòng)點(diǎn)癌压、描線(xiàn)、畫(huà)圓弧等等荆陆,這是一個(gè)實(shí)例嘍滩届,但注意到這個(gè)Renderer實(shí)例中的三個(gè)基本繪圖方法執(zhí)行結(jié)果僅僅是打印信息,而不是真實(shí)地在屏幕上進(jìn)行繪圖被啼!太桑心了丐吓,所以嘍我們現(xiàn)在要依靠 CGContext 來(lái)進(jìn)行真實(shí)操作,首先要做的抽象一個(gè)描繪器要做的工作——實(shí)際我們已經(jīng)做好了趟据,只需要將Renderer結(jié)構(gòu)體改成協(xié)議即可券犁。如下:

protocol Renderer {
  func moveTo(p: CGPoint)
  func lineTo(p: CGPoint)
  func arcAt(center: CGPoint, radius: CGFloat,
             startAngle: CGFloat, endAngle: CGFloat)
}

要求 CGContext 遵循Renderer協(xié)議并進(jìn)行實(shí)現(xiàn):

extension CGContext : Renderer {
  // 移動(dòng)點(diǎn) self 即CGContext實(shí)例本身 Self 值類(lèi)型本身
  func moveTo(p: CGPoint) {
    CGContextMoveToPoint(self, position.x, position.y)
  }
  // 連線(xiàn)
  func lineTo(p: CGPoint) {
    CGContextAddLineToPoint(self, position.x, position.y)
  }
  // 畫(huà)圓弧
  func arcAt(center: CGPoint, radius: CGFloat,
             startAngle: CGFloat, endAngle: CGFloat) {
    let arc = CGPathCreateMutable()
    CGPathAddArc(arc, nil, c.x, c.y, radius, startAngle, endAngle, true)
    CGContextAddPath(self, arc)
  }
}

效果圖:

graphic.png

協(xié)議擴(kuò)展和泛型

聲明Bubble形狀(Bubble氣泡,即一個(gè)圓中包含另外一個(gè)小圓):

struct Bubble : Drawable {
  func draw(r: Renderer) {
    // 描畫(huà)一個(gè)圓
    r.arcAt(center, radius: radius, startAngle: 0, endAngle: twoPi)
    // 描畫(huà)另外一個(gè)圓
    r.arcAt(highlightCenter, radius: highlightRadius,
        startAngle: 0, endAngle: twoPi)
  }
}

回憶早前的Circle形狀:

struct Circle : Drawable {
  func draw(r: Renderer) {
    // 畫(huà)圓
    r.arcAt(center, radius: radius, startAngle: 0.0, endAngle: twoPi)
  } 
}

畫(huà)圓汹碱,畫(huà)圓粘衬,畫(huà)圓!咳促! 看來(lái)我們的描繪器renderer是時(shí)候新增一個(gè)畫(huà)圓操作了稚新!

protocol Renderer {
  func moveTo(p: CGPoint)
  func lineTo(p: CGPoint)
  // 新增方法
  func circleAt(center: CGPoint, radius: CGFloat)
  func arcAt(
    center: CGPoint, radius: CGFloat, startAngle: CGFloat, endAngle: CGFloat)
}

// 隨之而來(lái)的改動(dòng)自然就是TestRenderer要實(shí)現(xiàn)這個(gè)方法嘍
extension TestRenderer {
  func circleAt(center: CGPoint, radius: CGFloat) {
    arcAt(center, radius: radius, startAngle: 0, endAngle: twoPi)
  }
}
// 當(dāng)然別忘了 CGContext
extension CGContext {
  func circleAt(center: CGPoint, radius: CGFloat) {
     arcAt(center, radius: radius, startAngle: 0, endAngle: twoPi)
  }
}

看似挺好,但是注意TestRender和 CGContext的circleAt實(shí)現(xiàn)僅僅是調(diào)用arcAt方法罷了跪腹,說(shuō)白了就是復(fù)制代碼褂删,顯然不可取。

這時(shí)我們要用到Swift中的Protocol Extension 特性拉冲茸,為協(xié)議加上默認(rèn)實(shí)現(xiàn)屯阀,這次我們是對(duì)Renderer協(xié)議進(jìn)行Extension缅帘,加上默認(rèn)的circleAt實(shí)現(xiàn):

extension Renderer {
  // 這意味著所有遵循Renderer協(xié)議的對(duì)象都具有一個(gè)circleAt默認(rèn)實(shí)現(xiàn)方法
  func circleAt(center: CGPoint, radius: CGFloat) {
    arcAt(center, radius: radius, startAngle: 0, endAngle: twoPi)
  }
}

這樣所有只要遵循了Renderer協(xié)議的對(duì)象都具有circleAt實(shí)現(xiàn)方法拉!

接著我們?cè)贋?Renderer 協(xié)議Extension一個(gè)默認(rèn)實(shí)現(xiàn)方法:rectangleAt(edges: CGRect)难衰。ps:強(qiáng)調(diào)這里并未在Renderer協(xié)議中添加rectangleAt聲明钦无,而是在Extension中添加了默認(rèn)實(shí)現(xiàn)方法,這很重要盖袭。

現(xiàn)在有如下代碼:

// 對(duì)協(xié)議進(jìn)行extension 是為其實(shí)現(xiàn)默認(rèn)方法
extension Renderer {
  func circleAt(center: CGPoint, radius: CGFloat) { ... }
  func rectangleAt(edges: CGRect) { ... }
}
// 對(duì)類(lèi)進(jìn)行extension協(xié)議 是表明遵循協(xié)議 兩者有本質(zhì)區(qū)別
extension TestRenderer : Renderer {
  func circleAt(center: CGPoint, radius: CGFloat) { ... }
  func rectangleAt(edges: CGRect) { ... }
}
let r = TestRenderer()
// 1 
r.circleAt(origin, radius: 1);
// 2
r.rectangleAt(edges);
  1. 調(diào)用的自然是TestRenderer的circleAt方法
  2. 調(diào)用的自然是TestRenderer的rectangleAt方法

現(xiàn)在修改let r : Renderer = TestRenderer() 失暂,編譯器只知道r是Renderer類(lèi)型,而非TestRenderer鳄虱,此時(shí)1弟塞、2調(diào)用的是誰(shuí)的方法呢?ps:swift以后要有筆試題 這絕壁會(huì)有拙已!

  1. 調(diào)用TestRenderer的circleAt方法决记,因?yàn)檫@在Renderer協(xié)議中是required的
  2. 調(diào)用Renderer的默認(rèn)實(shí)現(xiàn)方法rectangleAt。

表示現(xiàn)在略感迷茫悠栓。-.-! 之后補(bǔ)充吧霉涨。

關(guān)于協(xié)議的遵循:

extension CollectionType {
  public func indexOf(element: Generator.Element) -> Index? {
    for i in self.indices {
    
      // 報(bào)錯(cuò)信息: binary operator '==' cannot be applied two Generator.Element operands
      if self[i] == element {
        return i 
      }
    }
    return nil 
   }
}

道理我都懂按价,使用 '==' 操作符進(jìn)行前要確認(rèn)兩邊對(duì)象怎么樣才算相等(返回true)惭适,怎樣算不相等(返回false)。舉例來(lái)說(shuō)楼镐,Int類(lèi)型數(shù)據(jù) 2 和 3 進(jìn)行比較癞志,顯然不相等嘍;String類(lèi)型數(shù)據(jù)“PMST” 和 “PMST” 比較是相等的嘍框产,那如果是個(gè)自定義類(lèi)型MyClass的實(shí)例對(duì)象 a 和 b 進(jìn)行比較呢凄杯? 怎么樣才算相等? 這個(gè)比較行為需要我們來(lái)自定義對(duì)吧秉宿,我的地盤(pán)我做主戒突!

而這里我們是在為CollectionType 協(xié)議實(shí)現(xiàn)一個(gè)默認(rèn)方法IndexOf,只需要加個(gè)約束即可:

extension CollectionType where Generator.Element : Equatable {
  public func indexOf(element: Generator.Element) -> Index? {
    for i in self.indices {
      if self[i] == element {
          return i 
      }
    }
    return nil 
   }
}

先講到這里描睦,更多內(nèi)容請(qǐng)見(jiàn)官方Video膊存。

Why Coding Like This? 詳述Ordered一例

Binary search 二分法不是本文的重點(diǎn),因此不會(huì)這里不會(huì)詳述忱叭,但是你可以簡(jiǎn)單先看下C語(yǔ)言實(shí)現(xiàn)原理隔崎。

首先認(rèn)識(shí)幾個(gè)問(wèn)題:

  • 要使用二分法查找的數(shù)組一定是有序的!
  • 數(shù)組元素類(lèi)型可以是Int韵丑,Double爵卒,String等!

實(shí)現(xiàn)myBinarySearch函數(shù)撵彻,函數(shù)傳入已經(jīng)排序好的數(shù)組sortedKeys以及要查找的鍵值k钓株,返回鍵值的索引值实牡。譬如傳入[2,3,4,5,6,7]key = 7,那么返回索引號(hào)為5享幽。

先以Int數(shù)據(jù)類(lèi)型為例:

func myBinarySearch(sortedKeys:[Int],forKey k : Int) -> Int{
    
    var lo = 0,hi = sortedKeys.count
    while hi > lo{
        let mid = lo + (hi - lo) / 2
        if sortedKeys[mid] < k {lo = mid + 1}
        else{ hi = mid}
    }
    return lo
}

myBinarySearch([2,3,4,5,6], forKey: 4) // 索引值為2 

考慮到數(shù)組元素類(lèi)型還有Double String等等铲掐,所以使用泛型來(lái)實(shí)現(xiàn):

func myBinarySearch<T>(sortedKeys:[T],forKey k : T) -> Int{
    
    var lo = 0,hi = sortedKeys.count
    while hi > lo{
        let mid = lo + (hi - lo) / 2
        // 報(bào)錯(cuò)
        if sortedKeys[mid] < k {lo = mid + 1}
        else{ hi = mid}
    }
    return lo
}

很遺憾,編譯器報(bào)錯(cuò)了“Binary operator'< cannot ...'”值桩,主要是傳入的泛型是否具有比較性無(wú)從得知摆霉,因此出現(xiàn)了報(bào)錯(cuò)。

我們只需要為泛型T加上Comparable約束奔坟,問(wèn)題迎刃而解携栋。

func myBinarySearch<T:Comparable>(sortedKeys:[T],forKey k : T) -> Int{
    
    var lo = 0,hi = sortedKeys.count
    while hi > lo{
        let mid = lo + (hi - lo) / 2
        if sortedKeys[mid] < k {lo = mid + 1}
        else{ hi = mid}
    }
    return lo
}

myBinarySearch([2,3,4,5,6], forKey: 4)

是時(shí)候加些面向協(xié)議的“佐料”了。有序即具有先后之分咳秉,那么對(duì)象自身(self)和其他(other)必須有個(gè)排序問(wèn)題婉支,因此協(xié)議制定了 func precedes(other:Self)->Bool協(xié)議方法,用于判斷對(duì)象自身和其他對(duì)象的排序澜建。

protocol Ordered{
    func precedes(other:Self) -> Bool
}
// 報(bào)錯(cuò)
func binarySearch(sortedKeys:[Ordered],forKey k: Ordered) -> Int{
    var lo = 0,hi = sortedKeys.count
    while hi > lo{
        let mid = lo + (hi - lo) / 2
        if sortedKeys[mid].precedes(k){lo = mid + 1}
        else{ hi = mid}
    }
    return lo
}

這里同樣有個(gè)問(wèn)題“protocol 'Ordered' can only be used as a generic constraint because it has Self or associated type requirements”向挖,前文已經(jīng)提及,改動(dòng)方法是使用泛型+約束炕舵。

現(xiàn)在完整代碼如下:

protocol Ordered{
    func precedes(other:Self) -> Bool
}

func binarySearch<T : Ordered>(sortedKeys:[T],forKey k: T) -> Int{
    var lo = 0,hi = sortedKeys.count
    while hi > lo{
        let mid = lo + (hi - lo) / 2
        if sortedKeys[mid].precedes(k){lo = mid + 1}
        else{ hi = mid}
    }
    return lo
}

滿(mǎn)心歡喜去測(cè)試何之,結(jié)果顯然很悲傷:

// 報(bào)錯(cuò)cannot invoke binarySearch with an argument...
let position = binarySearch(["2", "3", "5", "7"], forKey: "5")

原因很簡(jiǎn)單,函數(shù)要求傳入的參數(shù)是遵循Ordered協(xié)議的類(lèi)型T數(shù)組咽筋,而String顯然沒(méi)有遵循Ordered協(xié)議溶推。因此我們接下來(lái)要做的是extension String:Ordered{},顯然除了String奸攻,還有Int等蒜危,一并寫(xiě)了?

// 以下擴(kuò)展的作用是遵循Ordered協(xié)議 
extension Int : Ordered {
  func precedes(other: Int) -> Bool { return self < other }
}
extension String : Ordered {
  func precedes(other: String) -> Bool { return self < other }
}
// 其他...

難道為每一個(gè)類(lèi)型都進(jìn)行extension實(shí)現(xiàn)嗎睹耐?顯然有更好的方法辐赞,還記得前文講得協(xié)議擴(kuò)展嗎?為Comparable協(xié)議增加默認(rèn)實(shí)現(xiàn)硝训!當(dāng)然Int朵逝,String類(lèi)型逃脫不了遵循Ordered的命運(yùn)(因?yàn)楹瘮?shù)傳入的必須是實(shí)現(xiàn)Ordered協(xié)議的對(duì)象)狮杨,但是真的節(jié)省了很多重復(fù)的代碼,不是嗎?

extension Comparable {
  func precedes(other: Self) -> Bool { return self < other }
}
extension Int : Ordered {}
extension String : Ordered {}

這里的亮點(diǎn)是為已有協(xié)議Comparable進(jìn)行Extension喉祭,添加了precedes的默認(rèn)實(shí)現(xiàn)汞贸。這也意味著所有只要遵循了Comparable協(xié)議的已有類(lèi)型,同樣也是遵循Ordered協(xié)議的彰导。但是倘若你沒(méi)有使用extension Int:Ordered{}方式明確告知Int類(lèi)型實(shí)現(xiàn)Ordered協(xié)議的話(huà)敲茄,那么即使Int類(lèi)型擴(kuò)展中實(shí)現(xiàn)有precedes方法,也不能說(shuō)Int類(lèi)型遵循Ordered協(xié)議堰燎!

舉個(gè)例子吧:

protocol myProtocol{
  func sayHello()
}

class MyClass{
    func sayHello(){
        print("hello")
    }
}
let mc = MyClass()
mc is myProtocol // false

盡管MyClass實(shí)現(xiàn)了協(xié)議的要求,但是它沒(méi)有告知我遵循了myProtocol協(xié)議笋轨,所以很遺憾秆剪,然并卵。

回到先前的話(huà)題:

extension Comparable {
  func precedes(other: Self) -> Bool { return self < other }
}
extension Int : Ordered {}
extension String : Ordered {}

let truth = 3.14.precedes(98.6) // 編譯通過(guò)

Double類(lèi)型遵循Comparable協(xié)議爵政,因此也具有precedes方法仅讽!所以可以調(diào)用precedes方法,貌似這不是我們期望的<匦洁灵! 更糟糕的是Double并未遵循Ordered協(xié)議!因此以下調(diào)用是錯(cuò)誤的掺出!

let position = binarySearch([2.0, 3.0, 5.0, 7.0], forKey: 5.0)//報(bào)錯(cuò)

當(dāng)然我們可以繼續(xù)使用extension Double:Ordered{}來(lái)解決問(wèn)題徽千,但是說(shuō)實(shí)話(huà)真的不咋地,是時(shí)候思考下了汤锨!

為何要給Comparable進(jìn)行extension双抽,為何不是給Ordered協(xié)議進(jìn)行呢?就像這樣:

extension Ordered {
  //報(bào)錯(cuò)
  func precedes(other: Self) -> Bool { return self < other }
}

什么泥畅? 無(wú)法比較selfother荠诬?加個(gè)約束唄琅翻!

extension Ordered where Self : Comparable {
  func precedes(other: Self) -> Bool { return self < other }
}
extension Double:Ordered{}

還記得Self嗎位仁?其實(shí)就是遵循協(xié)議的類(lèi)型的placeholder罷了!

再來(lái)看看 let truth = 3.14.precedes(98.6) 調(diào)用方椎,已經(jīng)編譯報(bào)錯(cuò)了聂抢,確實(shí)本就不應(yīng)該能夠調(diào)用,去掉它吧棠众!

最后美化下所寫(xiě)的二分法代碼琳疏,你可以寫(xiě)成兩種方式

方式一:全局函數(shù)

func binarySearch<
  C : CollectionType where C.Index == RandomAccessIndexType,
  C.Generator.Element : Ordered
>(sortedKeys: C, forKey k: C.Generator.Element) -> Int {
  ...
}
// 注意調(diào)用方式
let pos = binarySearch([2, 3, 5, 7, 11, 13, 17], forKey: 5)

方式二:擴(kuò)展已有協(xié)議

extension CollectionType where Index == RandomAccessIndexType,
Generator.Element : Ordered {
  func binarySearch(forKey: Generator.Element) -> Int {
    ...
} }
// 注意調(diào)用方式
let pos = [2, 3, 5, 7, 11, 13, 17].binarySearch(5)

相比較我更喜歡后者!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末闸拿,一起剝皮案震驚了整個(gè)濱河市空盼,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌新荤,老刑警劉巖揽趾,帶你破解...
    沈念sama閱讀 216,692評(píng)論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異篱瞎,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)牵素,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,482評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門(mén)笆呆,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)腰奋,“玉大人劣坊,你說(shuō)我怎么就攤上這事屈留」辔#” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 162,995評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵沫勿,是天一觀的道長(zhǎng)产雹。 經(jīng)常有香客問(wèn)我蔓挖,道長(zhǎng)瘟判,這世上最難降的妖魔是什么拷获? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,223評(píng)論 1 292
  • 正文 為了忘掉前任匆瓜,我火速辦了婚禮,結(jié)果婚禮上质欲,老公的妹妹穿的比我還像新娘嘶伟。我一直安慰自己九昧,他們只是感情好毕匀,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,245評(píng)論 6 388
  • 文/花漫 我一把揭開(kāi)白布蹋笼。 她就那樣靜靜地躺著躁垛,像睡著了一般教馆。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上胶滋,一...
    開(kāi)封第一講書(shū)人閱讀 51,208評(píng)論 1 299
  • 那天究恤,我揣著相機(jī)與錄音丁溅,去河邊找鬼探遵。 笑死箱季,一個(gè)胖子當(dāng)著我的面吹牛藏雏,可吹牛的內(nèi)容都是我干的掘殴。 我是一名探鬼主播,決...
    沈念sama閱讀 40,091評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼起意,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼揽咕!你這毒婦竟也來(lái)了套菜?” 一聲冷哼從身側(cè)響起逗柴,我...
    開(kāi)封第一講書(shū)人閱讀 38,929評(píng)論 0 274
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤戏溺,失蹤者是張志新(化名)和其女友劉穎于购,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體斑胜,經(jīng)...
    沈念sama閱讀 45,346評(píng)論 1 311
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡止潘,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,570評(píng)論 2 333
  • 正文 我和宋清朗相戀三年凭戴,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了么夫。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片档痪。...
    茶點(diǎn)故事閱讀 39,739評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡腐螟,死狀恐怖乐纸,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情泵督,我是刑警寧澤小腊,帶...
    沈念sama閱讀 35,437評(píng)論 5 344
  • 正文 年R本政府宣布秩冈,位于F島的核電站入问,受9級(jí)特大地震影響稀颁,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜棱烂,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,037評(píng)論 3 326
  • 文/蒙蒙 一颊糜、第九天 我趴在偏房一處隱蔽的房頂上張望衬鱼。 院中可真熱鬧鸟赫,春花似錦消别、人聲如沸妖啥。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,677評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)菜枷。三九已至,卻和暖如春岳瞭,著一層夾襖步出監(jiān)牢的瞬間瞳筏,已是汗流浹背牡昆。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,833評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工丢烘, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留播瞳,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,760評(píng)論 2 369
  • 正文 我出身青樓痒给,卻偏偏與公主長(zhǎng)得像苍柏,于是被迫代替她去往敵國(guó)和親试吁。 傳聞我的和親對(duì)象是個(gè)殘疾皇子楼咳,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,647評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容