WWDC2015 -- protocol-oriented programming in swift(Session 408)(面向協(xié)議編程)

標簽(空格分隔): WWDC


Classes Are Awesome

  • 封裝
  • 訪問控制
  • 抽象
  • 命名空間
  • 語法表達
  • 延伸性

Type Are Awesome

  • 訪問控制
  • 抽象
  • 命名空間

這些是讓程序員管理復雜事件的要素。
但是用structs與enums可以實現(xiàn)以上功能。


Three Beefs about Class

1.Implicit Sharing


當A與B同時指向一個對象的時候,經(jīng)常會發(fā)生錯誤。

  1. 為了減少在代碼中的錯誤瘋狂的使用copy。
  2. 使用過多的copy帶來的性能上的影響。
  3. 當使用Dispatch_queue時,提供了一個比賽場景,因為線程共享了一個的可變狀態(tài)。
  4. 所以你需要為了保護你的常量加上lock。
  5. 這個lock使得性能更加低下。
  6. 甚至造成死鎖。
  7. 產(chǎn)生Bug!

NOTE
It is not safe to modify a mutable collection while enumerating through it. Some enumerators may currently allow enumeration of a collection that is modified, but this behavior is not guaranteed to be supported in the future.

官方的說明。
但是這種情況不會在Swift上出現(xiàn),因為Swift的所有集合都是值類型。


2.Class Inheritance

  1. 需要正確選擇一個好的父類。
  2. 單一的繼承,得到父類中所有的信息。
  3. 必須在Class創(chuàng)建時繼承,而不是在之后拓展。
  4. 如果父類儲存了許多屬性。
  5. 子類也必須要儲存屬性。
  6. 初始化負擔加重。
  7. 注意不能修改父類中的常量。
  8. ovewride時,需要知道是什么方法,或者怎么去寫(什么時候不能使用override)。

3.Lost Type Relationships

class Ordered {
  func precedes(other: Ordered) -> Bool { fatalError("implement me!") }
}

首先類必須實現(xiàn)方法,不然會報錯。

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 }

這是一個二叉樹的搜索方法,類型為Order。

class Number : Ordered {
  var value: Double = 0
  override func precedes(other: Ordered) -> Bool {
    return value < other.value
  }
}

class Label : Ordered { var text: String = "" ... } 

現(xiàn)在創(chuàng)建一個Number類,以及一個Label類,他們都集成了Ordered。
在Number類中重載使用方法precedes不能使用子類參數(shù)value。
因為這里不能不是所有的Ordered的子類都會有value屬性。

所以需要改為

  override func precedes(other: Ordered) -> Bool 
  {   
  return value < (other as! Number).value 
  } 

這樣的問題來自于Class之間對于自身和其他類的類型并沒有建立關系。
類型關系的缺失經(jīng)常是由于抽象地使用類。


一個更好的抽象機制

支持值類型 (and classes)
支持靜態(tài)類型關系 (和動態(tài)調(diào)度) 非完全統(tǒng)一的
支持逆襲建模
不在模型上引入實例數(shù)據(jù)
不在模型上引入初始化負擔
使實現(xiàn)的內(nèi)容更加清晰

這不就是Protocal的優(yōu)點嗎?


用Protocal開始編碼

首先將上面的代碼進行轉(zhuǎn)換

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

這樣就再也不需要一個基類,實現(xiàn)方法的時候也不需要override,同時不希望number作為一個類去使用,改成了struct類型。

 func precedes(other: Number) -> Bool 
 {    
 return self.value < other.value  
 } 

此時需要解決潛在的靜態(tài)類型的安全漏洞,因為other參數(shù)可能是另外的類型,所以現(xiàn)在設置為Number類型,這樣就不需要再做類型判斷,但是這樣又與協(xié)議中的Ordered類型相互矛盾。于是我們現(xiàn)在將Ordered中的類型設置為self。

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

這種設計叫做Self-requirement,當你在protocol中看到self時,它是作為一個遵守了這種模型類型的占位符。所以現(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 
} 

回到剛才的二叉樹搜索方法上,這種方法在Ordered是Class的時候是有效的,在protocol中加入Self前也是有效的,現(xiàn)在加入self后會報錯,protocol 'Ordered' can only be used as a generic constraint because it has Self or associated type requirements。本來我們可以處理由numbers和label組成的[Ordered]數(shù)組,但是現(xiàn)在編譯器要求我們的類型一致,就像這樣:

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 }

這么處理后,只能對單一的Ordered類型處理,看起來特別嚴格,好像缺失了很多靈活性。但是想想看,對于不同類型同時存在的處理我們往往是去阻止,而不是真正意義上的處理。事實上,單一類型的數(shù)組才是我們想要的東西。


有無Protocols的兩個世界

Without Self Requirement With Self Requirement
func precedes(other: Ordered) -> Bool func precedes(other: Self) -> Bool
作為一種類型使用 只能作為一種泛型約束使用
func sort(inout a: [Ordered]) func sort<T : Ordered>(inout a: [T])
Think “多樣化” Think “單一化”
每一個模型都與其他類型有關聯(lián) 模型在交互上是自由的
動態(tài)調(diào)度 靜態(tài)調(diào)度
有更少的優(yōu)化度 有更多的優(yōu)化度

挑戰(zhàn)!將使用Class改為使用Protocol

準備

首先設定一個struct類型的Renderer,用print方式直觀實現(xiàn)方法。

struct Renderer { 
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))")   } 
} 

然后創(chuàng)建一個Drawable協(xié)議為我們的元素提供一個共同的接口。

protocol Drawable {   func draw(renderer: Renderer) } 

創(chuàng)建一個Polygon形狀,因為是值類型,所以使用了struct,并包含了一個點的數(shù)組。通過調(diào)用renderer的方法,遍歷數(shù)組中所有的點畫出形狀。

struct Polygon : Drawable { 
func draw(renderer: Renderer) 
    {   
    renderer.moveTo(corners.last!)    
    for p in corners 
            {    
                renderer.lineTo(p)    
            }  
    }  
var corners: [CGPoint] = [] 
}

同理,創(chuàng)建一個Circle。需要中心點和半徑。也通過調(diào)用renderer的方法。

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

最后創(chuàng)建一個Diagram,其中的elements類型是Drawable。因為所有的Darwable都是值類型,所以這里的Darwable類型的數(shù)組也是值類型。

struct Diagram : Drawable { 
    func draw(renderer: Renderer) {    
    for f in elements
         {     
    f.draw(renderer)  
         }   
    }  
    var elements: [Drawable] = [] 
}

根據(jù)以上內(nèi)容,我們可以進行測試。

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())

先將三個值初始化,再講Renderer()傳入diagram的方法中。


Renderer的小修改

首先復制一份Renderer

struct Renderer { 
    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))")  } 
} 
struct Renderer { 
    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))")  } 
} 

將第一個Renderer從struct改為protocol

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

將第二個Renderer修改為TestRenderer ,遵守Renderer協(xié)議。

struct TestRenderer:Renderer { 
    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))")  } 
} 

以上只是一個簡單對Renderer部分做了處理。
最后再修改一下調(diào)用方式即可。

diagram.draw(TestRenderer())

用CGContext測試

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

現(xiàn)在可以為CGContext拓展協(xié)議。
如果這個Renderer是class,就不能這么做。
這樣做之后CGContext就擁有了Renderer中所以基礎的東西,并且需要全部是實現(xiàn)。

extension CGContext : Renderer {
  func moveTo(p: CGPoint) {
    CGContextMoveToPoint(self, position.x, position.y)
  }
  func lineTo(p: CGPoint) {
    CGContextAddLineToPoint(self, position.x, position.y)
  }
  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)
  }
}

詳解在Building Better Apps with Value Types in Swift中。


易測的協(xié)議與泛型

如果用協(xié)議去解耦,任何東西都能夠變得容易測試。
這中測試與使用模型很接近。但是模型本身是比較脆弱的。
你必須去將你的測試代碼與在測試下的實現(xiàn)代碼聯(lián)系起來,因為模型很脆弱,所以不能在Swift的強靜態(tài)類型系統(tǒng)下很好的運行。

struct Bubble : Drawable {
  func draw(r: Renderer) {
    r.arcAt(center, radius: radius, startAngle: 0, endAngle: twoPi)
    r.arcAt(highlightCenter, radius: highlightRadius,
        startAngle: 0, endAngle: twoPi)
  }
}
struct Circle : Drawable {
  func draw(r: Renderer) {
    r.arcAt(center, radius: radius, startAngle: 0.0, endAngle: twoPi)
} }

代碼中startAngle: 0, endAngle: twoPi每個方法都用到了,如果想簡化成這樣:

struct Bubble : Drawable {
  func draw(r: Renderer) {
    r.circleAt(center, radius: radius)
    r.circleAt(highlightCenter, radius: highlightRadius)
  }
}
struct Circle : Drawable {
  func draw(r: Renderer) {
    r.circleAt(center, radius: radius)
  }
}

我們需要在協(xié)議中加上circleAt,直接將startAngle與Angle去除。

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

在有遵守Renderer協(xié)議的地方,我們可以用extension補充上去。

extension TestRenderer {
func circleAt(center: CGPoint, radius: CGFloat) {
  arcAt(center, radius: radius, startAngle: 0, endAngle: twoPi)
    }
}
extension CGContext {
func circleAt(center: CGPoint, radius: CGFloat) {
  arcAt(center, radius: radius, startAngle: 0, endAngle: twoPi)
    }
}

但是這么做特別奇怪,因為每一個地方都要補充相同的類容,很復雜。于是我們直接對協(xié)議做了擴展。

extension Renderer {
  func circleAt(center: CGPoint, radius: CGFloat) {
    arcAt(center, radius: radius, startAngle: 0, endAngle: twoPi)
  }
}

這么完成之后就不需要再對其他準守了Renderer協(xié)議的地方再繼續(xù)擴展。


協(xié)議的擴展

剛剛看到

extension Renderer {
  func circleAt(center: CGPoint, radius: CGFloat) {
    arcAt(center, radius: radius, startAngle: 0, endAngle: twoPi)
  }
}

是對已經(jīng)存在在Renderer協(xié)議中的方法進行了拓展,如果這個方法不存在在Renderer中呢?拓展了不存在在協(xié)議中的方法,那這個方法和拓展本來在協(xié)議中的方法的區(qū)別是什么?

現(xiàn)在我來做一個簡單模型的示范:


protocol testProtocol{
    func a()
    func b()
}
extension testProtocol{
    func a(){
        print("a1")
    }
    func c(){
        print("c1")
    }
}

struct testStruct{

    func b(){
        print("b2")
    }
}
extension testStruct:testProtocol{
    func a(){
        print("a3")
    }
    func c(){
        print("c3")
    }
}

創(chuàng)建好之后我創(chuàng)建一個test對象,并且進行測試。

let test = testStruct()
test.a()
test.b()
test.c()

結(jié)果為:

a3
b2
c3

這看上去沒什么奇怪的,甚至我們直接把extension testProtocol去除也沒關系,但是我們再這么修改一下,如果swift知道它遵守了testProtocol呢?

let test:testProtocol = testStruct()
test.a()
test.b()
test.c()

結(jié)果為:

a3
b2
c1

為什么?因為a方式是必須的,所以調(diào)用了被定制的方法。而c方法不是必須的,所以在testStruct中只是覆蓋了testProtocol的拓展實現(xiàn)內(nèi)容。而現(xiàn)在,swift只知道test是testProtocol而不是testStruct,所以調(diào)用了testProtocol中實現(xiàn)的結(jié)果。
是不是說每一個方法都是必須的呢?對于大部分API來說是不一定的,所以最好的做法是覆蓋協(xié)議中存在的必須實現(xiàn)的方法,而不是覆蓋模型上的方法。


更多協(xié)議擴展的技巧

拓展的約束

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

這樣寫會出錯,因為在兩個Generator.Element之間不能使用 == ,這時候只需要修改拓展條件,改為:

extension CollectionType where Generator.Element : Equatable

就可以解決。


protocol Ordered {
    func precedes(other: Self) -> Bool
}
func binarySearch<T : Ordered>(sortedKeys: [T], forKey k: T) -> Int { return 1
}

let position = binarySearch([2, 3, 5, 7], forKey: 5)

我們尋找需要查找一個int類型的數(shù)字,但是編譯器也會報錯,因為int類型并沒有遵守Ordered,這時候我們?yōu)榱私鉀Q這個問題,加上了:

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

萬一需要查找一個String類型呢?那又要加上

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

每一個類型都需要拓展一遍,而且再寫一遍方法。但是Int和String都準守Compareble協(xié)議,我們可以直接拓展Compareble協(xié)議。

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

省略了在Int和String中實現(xiàn)precedes方法的字段。
現(xiàn)在如果要查找Double類型呢?是否也要在加一次擴展?是事實就算不進行拓展,Double類型的對象也能實現(xiàn)precedes方法,事實上它就算能實現(xiàn)precedes方法也不能在沒有被擴展的情況下用二分查找。那么這個precede還有什么意義嗎?
為了解決這個問題,還是依然用了為拓展加上約束的方法。

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

這樣就能精確的知道我們到底想要的是什么。


泛型的美化

這是一個二分查找的使用,可以使用在任何集合中,在Swift1中是這么寫的。

func binarySearch<
  C : CollectionType where C.Index == RandomAccessIndexType,
  C.Generator.Element : Ordered
>(sortedKeys: C, forKey k: C.Generator.Element) -> Int {
  ...
}
let pos = binarySearch([2, 3, 5, 7, 11, 13, 17], forKey: 5)

看上去非常的糟糕而且丑陋,在Swift2中可以改成這樣:

extension CollectionType where Index == RandomAccessIndexType,
Generator.Element : Ordered {
  func binarySearch(forKey: Generator.Element) -> Int {
    ...
} }
let pos = [2, 3, 5, 7, 11, 13, 17].binarySearch(5)

哪一種寫法更好,這是可以一眼看出來的。


Building Better Apps with Value Types in Swift

func == (lhs: Polygon, rhs: Polygon) -> Bool {
  return lhs.corners == rhs.corners
}
extension Polygon : Equatable {}
func == (lhs: Circle, rhs: Circle) -> Bool {
  return lhs.center == rhs.center
    && lhs.radius == rhs.radius
}
extension Circle : Equatable {}

為什么需要所有的值類型都要能夠使用==?
具體請看Building Better Apps with Value Types in Swift Session。

現(xiàn)在請看下面這段代碼,如果不遵守Equatable協(xié)議:

struct Diagram : Drawable {
  func draw(renderer: Renderer) { ... }
  var elements: [Drawable] = []
}
func == (lhs: Diagram, rhs: Diagram) -> Bool {
  return lhs.elements == rhs.elements
}

這段代碼就會出錯,因為==不能作為操作符應用于兩個Drawable之間,但是如果我們展開呢?

struct Diagram : Drawable {
  func draw(renderer: Renderer) { ... }
  var elements: [Drawable] = []
}
func == (lhs: Diagram, rhs: Diagram) -> Bool {
  return lhs.elements.count == rhs.elements.count
    && !zip(lhs.elements, rhs.elements).contains { $0 != $1 }
}

首先確定他們有同樣多的元素,再講兩個array進行比較,好像是沒有問題了,但是需要注意的是 != 并不能使用,因為!=不能作為操作符應用于兩個Drawable之間,所以現(xiàn)在沒有相等的操作符用于這兩個Array之間了。

那我們能不能將Equatable用于所有的Drawable之中呢?

struct Diagram : Drawable {
  func draw(renderer: Renderer) { ... }
  var elements: [Drawable] = []
}
func == (lhs: Diagram, rhs: Diagram) -> Bool {
  return lhs.elements.count == rhs.elements.count
    && !zip(lhs.elements, rhs.elements).contains { $0 != $1 }
}
protocol Drawable : Equatable {
  func draw()
}

問題在于Equatable協(xié)議中的 == 。

protocol Equatable {
  func == (Self, Self) -> Bool
}

它有Self-requirements,這就意味著現(xiàn)在Drawable現(xiàn)在也有Self-requirements的特征。Self-requirements直接將Drawable放在了單一性,靜態(tài)調(diào)度的世界,但是Diagram確實需要多態(tài)性的Drawable類型的數(shù)組,因為我們需要將polygons和circles放在相同的Diagram中,所以Drawable又是在多態(tài)性,動態(tài)調(diào)度的世界中。這就產(chǎn)生了矛盾。

現(xiàn)在怎么辦?

struct Diagram : Drawable {
  func draw(renderer: Renderer) { ... }
  var elements: [Drawable] = []
}
func == (lhs: Diagram, rhs: Diagram) -> Bool {
  return lhs.elements.count == rhs.elements.count
    && !zip(lhs.elements, rhs.elements).contains { !$0.isEqualTo($1) }
}
protocol Drawable {
  func isEqualTo(other: Drawable) -> Bool
  func draw()
}
extension Drawable where Self : Equatable {
  func isEqualTo(other: Drawable) -> Bool {
    if let o = other as? Self { return self == o }
    return false
} }
  1. 在Drawable協(xié)議中添加了isEqualTo方法,$0==($1)改為 $0.isEqualTo($1)。
  2. 將isEqualTo參數(shù)類型與Diagram中的elements一致,都是繼承Drawable協(xié)議。
  3. 拓展Drawable協(xié)議,并加入約束條件。
  4. 先確定傳入的參數(shù)是否是self類型,因為有了Equatable的限定,就可以使用就使用 == 操作符去判斷,不是就返回false。

結(jié)尾

什么時候使用class?

當你想要implicit sharing 時

  • 拷貝或者比較實例沒有意義的是有(如,window)
  • 實例生命周期與外部影響(如,臨時文件)
  • 實例就像通過只寫的方式流到外部狀態(tài)(如,CGContext)
final class StringRenderer : Renderer {
  var result: String
  ...
}

比如這個。

  1. final。
  2. 繼承的不是class。

不要與系統(tǒng)作對

  • 如果一個框架需要你傳遞一個對象或者子類,就需要。

也要謹慎

  • 在軟件中不應該有太大的東西
  • 當納入了class之外的元素,考慮不要用class

總結(jié)

使用協(xié)議而不是超類
拓展協(xié)議等于魔法

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容