Swift开发技巧Swift Tips首页投稿(暂停使用,暂停投稿)

WWDC 2015 - Session 408 - Protoc

2016-03-14  本文已影响630人  NinthDay

由 Dave Abrahams Professor 演讲,这货是C++牛人,自行wiki。

笔记先从 Object Oriented Programming - OOP(面向对象编程)说起,自然少不了 Class 这个主角,当初我入坑 iOS 时就问过一个问题:Class 和 Struct 的区别以及应用场景?

当然本文中不会详尽地去比较孰优孰劣,只是简单阐述下,仅供参考。

Classes Are Awesome

其中“Access Control”、“Abstraction”和“Namespace” 指明是 Class 令人头疼之处。

先说说继承性,Class 完胜 Structure,记住Structure是不具有继承性的!

类可以将对象抽象成多个描述属性和方法,可以通过继承得到子类,也就是传统意义上的superclass 和 subclass。 子类可以从父类继承一系列方法,当然也可以通过 override 重写父类的方法。

The Three Beef

第一点:Class 实例需要在堆上分配内存,此时有两个对象A和B要操作该实例,那么传递给A和B类实例的指针(内存地址)即可,只有一个线程时不会有任何问题,凡是有个先来后到,A操作完实例后B再接手操作对象;说说多线程情况,假设A在线程1对实例进行读操作,B在线程2对实例进行写操作,来个巧合吧!假设A在读取实例中的数据量较大的数组(指向读取旧数据),而B此刻却在修改old数据,用new数据覆盖,美名其曰更新。此时意外就产生了!A宝宝心里苦啊,我只是想读取旧数据,你却马不停蹄地覆盖旧数据。所以喽,以上情况想要解决就是复制一份实例! 但是过多的copy是否会让你在操作时提心吊胆?

第二点:Class 只能有一个父类,意味着只能从一个父类继承,而不能继承多个类。打个比方吧,现在具有类A和类B,C具有A的所有特性,所以喽Class C:A{},C是A的子类;巧了!C具有B的所有特性,所以喽Class C:B{}; 但是却没有类似Class C:A,B{}这种多继承写法。

关于第三点,视频举了个binary Search(二分法查找) 例子:

// 对于有序队列的一个抽象类 关于队列元素的类型可以是Int String Double 等等
// 所以我说这是一个抽象类!
class Ordered {
  func precedes(other: Ordered) -> Bool{
    fatalError("implement me!")
  }
}
// 全局函数 传入一个排序好的数组 以及要查找的key值 通过二分法搜索返回索引值
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: 为什么不实现Ordered类中precedes方法?

首先已经强调了Ordered是抽象类,当然这并不是站得住脚的理由,看看下面两个继承自它的子类。

// 这是一个标签 所以包含String类型的text属性
class Label:Ordered{
  var text:String = ""
  ...
}
// 这是一个数字 所以包含Double类型的value属性
class Number : Ordered {
  var value: Double = 0
  override func precedes(other: Ordered) -> Bool {
    return value < other.value
  }
}
// 还有其他类型的Ordered 子类

Label 严格意义上来说是一个Ordered类,同理 Number 也是,因为他们继承自 Ordered,具有其所有的特性;刚才说到你要为Ordered类实现precedes方法?请你告诉我对于传入的 Ordered 类到底是 Label 呢,还是 Number 呢,亦或是其他呢? 要知道你根本无法确定!所以我们在Ordered类中是不能实现的,而是让子类去重写实现。理解了这点继续下面的内容。

你会注意上面的 Number 类中的 precedes 方法有点问题:

class Number : Ordered {
  var value: Double = 0
  override func precedes(other: Ordered) -> Bool {
    // 报错!!! 很好理解,传入的 other 为 Ordered 类即可
    // 但是 Ordered 可没有 value属性! 所以我们需要进行向下(父类->子类)cast
    return value < other.value
  }
}

修改如下:

class Number : Ordered {
  var value: Double = 0
  // 实现也有缺陷 见下
  override func precedes(other: Ordered) -> Bool {
    return value < (other as! Number).value
  }
}

这里又有一个问题,代码要求传入 Ordered 类进行处理,那么Number 和Label 严格意义上来说都是 Ordered 类(这里要理解因为它们都是Ordered的子类),所以往代码中传入 Number 是Ok的, 而传入Label类时就很有问题了, 因为向下cast会出问题(人家明明是Label ,你想要 as! 到 Number 类,绝壁失败 程序crash)。所以喽 问题多多!

所以Abrahams提出了几个不错的抽象机制:

面向协议编程

英文 Protocol-Oriented Programming ,面向协议编程自然少不了协议。简单来说,首先将对象属性(property)和行为(behavior)抽象成实例属性和方法;将这些准则整合成协议;最后让对象遵循这个协议并实现协议中的内容即可。

改写上面例子:

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

现在 Ordered 是一个协议,它的准则只有一个 precedes ,因为是协议,所以不需要具体实现。

现在我们说 Number 类是有序的,所以只需要遵循这个协议并实现要求的内容即算满足。

protocol Ordered {
  func precedes(other: Ordered) -> Bool
}
// 既然是POP 而非OOP,摒弃类吧,改成Struct 值类型哦!
struct Number : Ordered {
  var value: Double = 0
  // 去掉override 因为我们不再是重写父类的方法 而是遵循协议
  func precedes(other: Ordered) -> Bool {
    return value < (other as! Number).value
  }
}

还没完,对于传入的other类型为 Ordered 类型,早前因为我们是重写父类方法,所以类型上要保持一致,但是现在是POP,所以我们现在可以任性了!改成Number,去掉as! Number 这难看的转换。

修改如下:

protocol Ordered {
  func precedes(other: Ordered) -> Bool
}
// 既然是POP 而非OOP,摒弃类吧,改成Struct 值类型哦!
struct Number : Ordered {
  var value: Double = 0
  // 去掉override 因为我们不再是重写父类的方法 而是遵循协议
  func precedes(other: Number) -> Bool {
    return value < other.value
  }
}

不幸地是,编译器报错“protocol requires function 'precedes' with type'(Ordered)->Bool' candidate has non-matching type '(Number)->Bool'”,不难理解,协议方法要求传入 Ordered 类型,而我们在实现时却传入 Number 类型,严格来说我们并未遵循Ordered协议。

值得引起注意,以及值得思考和牢记的地方:将协议中的Ordered类型改写成 Self,这叫 “Self” requirement, Self 代表任何遵循这个协议的类自身,譬如 Number遵循 Ordered协议,那么实现的方法中 Self 会替换成 Number; Label 遵循Ordered协议,那么其实现的方法中 Self 会替换成 Label,以此类推。

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

再来看看二分法查找全局函数的实现:

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 
}

编译器又报错拉! “protocol 'Ordered' can only be used as a generic constraint because it has Self or associated type requirements”。 因为我们使用 Self 替换了明确的类,所以我们只能通过添加泛型约束才能解决问题。ps:出错原因并非是函数传入参数类型为Ordered协议!! 很好奇为什么使用Self 或 关联类型就只能使用泛型约束了....2016/07/09 update: 感谢way的回答,实际上这样的,尽管sortedKeysforKey k 类型都是Ordered,但这并不能保证两个变量类型保持一致,可能前者是Number,后者是 Label呢;而我们期望 xx.preceseds(yy)这种方式调用时 xx 和 yy 的类型是相同的,这也是协议明确说明的,说直白一些我们希望 xx 和 yy 的类型都是 T,但是类型 T 必须遵循(实现) Ordered 协议。这也是为什么要用泛型约束了。

修改如下:

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 的绘图例子

画图自然逃不了移动点,描绘线和圆弧等基本操作,所以声明一个描绘器 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))")
  } 
}

接下来,思考我们要绘制的图形可不是简单的一个点或一条直线,有可能是多边形,复合图形等等。但终究逃不出“画”这个动词以及需要一个描绘器Renderer。所以喽,指定一个协议吧,它要求传入一个renderer进行绘图:

protocol Drawable {
  func draw(renderer: Renderer)
}

接下来想想如何描绘一个多边形?貌似只要告知它起始点位置,要连接的下一个点位置就可以了吧!所以声明一个Polygon结构体(值类型)如下:

struct Polygon : Drawable {
  func draw(renderer: Renderer) {
    // 移动到指定点
    renderer.moveTo(corners.last!)
    // 只要按顺序连接点即可
    for p in corners {
      renderer.lineTo(p)
    }
  }
  // 一系列有循序的链接点
  var corners: [CGPoint] = []
}

绘制一个圆需要知道圆心位置和半径长度即可:

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 ,仅仅只是一个图形罢了。现在我们要更进一步,告诉我要绘制的所有图形(多边形,圆形,椭圆等等),然后我将所有图形都绘制到一张图表上:

// 依旧遵循 Drawable 协议
struct Diagram : Drawable {
  func draw(renderer: Renderer) {
    for f in elements {
      f.draw(renderer)
    } 
  }
  // 传入要描绘的图形
  var elements: [Drawable] = []
}

Test It!

是时候测试下了!

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)
$
*/

面向对象更进一步

前面的 Renderer 作为结构体存在,负责移动点、描线、画圆弧等等,这是一个实例喽,但注意到这个Renderer实例中的三个基本绘图方法执行结果仅仅是打印信息,而不是真实地在屏幕上进行绘图!太桑心了,所以喽我们现在要依靠 CGContext 来进行真实操作,首先要做的抽象一个描绘器要做的工作——实际我们已经做好了,只需要将Renderer结构体改成协议即可。如下:

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

要求 CGContext 遵循Renderer协议并进行实现:

extension CGContext : Renderer {
  // 移动点 self 即CGContext实例本身 Self 值类型本身
  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)
  }
}

效果图:

graphic.png

协议扩展和泛型

声明Bubble形状(Bubble气泡,即一个圆中包含另外一个小圆):

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

回忆早前的Circle形状:

struct Circle : Drawable {
  func draw(r: Renderer) {
    // 画圆
    r.arcAt(center, radius: radius, startAngle: 0.0, endAngle: twoPi)
  } 
}

画圆,画圆,画圆!! 看来我们的描绘器renderer是时候新增一个画圆操作了!

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

// 随之而来的改动自然就是TestRenderer要实现这个方法喽
extension TestRenderer {
  func circleAt(center: CGPoint, radius: CGFloat) {
    arcAt(center, radius: radius, startAngle: 0, endAngle: twoPi)
  }
}
// 当然别忘了 CGContext
extension CGContext {
  func circleAt(center: CGPoint, radius: CGFloat) {
     arcAt(center, radius: radius, startAngle: 0, endAngle: twoPi)
  }
}

看似挺好,但是注意TestRender和 CGContext的circleAt实现仅仅是调用arcAt方法罢了,说白了就是复制代码,显然不可取。

这时我们要用到Swift中的Protocol Extension 特性拉,为协议加上默认实现,这次我们是对Renderer协议进行Extension,加上默认的circleAt实现:

extension Renderer {
  // 这意味着所有遵循Renderer协议的对象都具有一个circleAt默认实现方法
  func circleAt(center: CGPoint, radius: CGFloat) {
    arcAt(center, radius: radius, startAngle: 0, endAngle: twoPi)
  }
}

这样所有只要遵循了Renderer协议的对象都具有circleAt实现方法拉!

接着我们再为 Renderer 协议Extension一个默认实现方法:rectangleAt(edges: CGRect)。ps:强调这里并未在Renderer协议中添加rectangleAt声明,而是在Extension中添加了默认实现方法,这很重要。

现在有如下代码:

// 对协议进行extension 是为其实现默认方法
extension Renderer {
  func circleAt(center: CGPoint, radius: CGFloat) { ... }
  func rectangleAt(edges: CGRect) { ... }
}
// 对类进行extension协议 是表明遵循协议 两者有本质区别
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. 调用的自然是TestRenderer的circleAt方法
  2. 调用的自然是TestRenderer的rectangleAt方法

现在修改let r : Renderer = TestRenderer() ,编译器只知道r是Renderer类型,而非TestRenderer,此时1、2调用的是谁的方法呢?ps:swift以后要有笔试题 这绝壁会有!

  1. 调用TestRenderer的circleAt方法,因为这在Renderer协议中是required的
  2. 调用Renderer的默认实现方法rectangleAt。

表示现在略感迷茫。-.-! 之后补充吧。

关于协议的遵循:

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

道理我都懂,使用 '==' 操作符进行前要确认两边对象怎么样才算相等(返回true),怎样算不相等(返回false)。举例来说,Int类型数据 2 和 3 进行比较,显然不相等喽;String类型数据“PMST” 和 “PMST” 比较是相等的喽,那如果是个自定义类型MyClass的实例对象 a 和 b 进行比较呢? 怎么样才算相等? 这个比较行为需要我们来自定义对吧,我的地盘我做主!

而这里我们是在为CollectionType 协议实现一个默认方法IndexOf,只需要加个约束即可:

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

先讲到这里,更多内容请见官方Video。

Why Coding Like This? 详述Ordered一例

Binary search 二分法不是本文的重点,因此不会这里不会详述,但是你可以简单先看下C语言实现原理

首先认识几个问题:

实现myBinarySearch函数,函数传入已经排序好的数组sortedKeys以及要查找的键值k,返回键值的索引值。譬如传入[2,3,4,5,6,7]key = 7,那么返回索引号为5。

先以Int数据类型为例:

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 

考虑到数组元素类型还有Double String等等,所以使用泛型来实现:

func myBinarySearch<T>(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
}

很遗憾,编译器报错了“Binary operator'< cannot ...'”,主要是传入的泛型是否具有比较性无从得知,因此出现了报错。

我们只需要为泛型T加上Comparable约束,问题迎刃而解。

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)

是时候加些面向协议的“佐料”了。有序即具有先后之分,那么对象自身(self)和其他(other)必须有个排序问题,因此协议制定了 func precedes(other:Self)->Bool协议方法,用于判断对象自身和其他对象的排序。

protocol Ordered{
    func precedes(other:Self) -> Bool
}
// 报错
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
}

这里同样有个问题“protocol 'Ordered' can only be used as a generic constraint because it has Self or associated type requirements”,前文已经提及,改动方法是使用泛型+约束。

现在完整代码如下:

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
}

满心欢喜去测试,结果显然很悲伤:

// 报错cannot invoke binarySearch with an argument...
let position = binarySearch(["2", "3", "5", "7"], forKey: "5")

原因很简单,函数要求传入的参数是遵循Ordered协议的类型T数组,而String显然没有遵循Ordered协议。因此我们接下来要做的是extension String:Ordered{},显然除了String,还有Int等,一并写了?

// 以下扩展的作用是遵循Ordered协议 
extension Int : Ordered {
  func precedes(other: Int) -> Bool { return self < other }
}
extension String : Ordered {
  func precedes(other: String) -> Bool { return self < other }
}
// 其他...

难道为每一个类型都进行extension实现吗?显然有更好的方法,还记得前文讲得协议扩展吗?为Comparable协议增加默认实现!当然Int,String类型逃脱不了遵循Ordered的命运(因为函数传入的必须是实现Ordered协议的对象),但是真的节省了很多重复的代码,不是吗?

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

这里的亮点是为已有协议Comparable进行Extension,添加了precedes的默认实现。这也意味着所有只要遵循了Comparable协议的已有类型,同样也是遵循Ordered协议的。但是倘若你没有使用extension Int:Ordered{}方式明确告知Int类型实现Ordered协议的话,那么即使Int类型扩展中实现有precedes方法,也不能说Int类型遵循Ordered协议!

举个例子吧:

protocol myProtocol{
  func sayHello()
}

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

尽管MyClass实现了协议的要求,但是它没有告知我遵循了myProtocol协议,所以很遗憾,然并卵。

回到先前的话题:

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

let truth = 3.14.precedes(98.6) // 编译通过

Double类型遵循Comparable协议,因此也具有precedes方法!所以可以调用precedes方法,貌似这不是我们期望的!! 更糟糕的是Double并未遵循Ordered协议!因此以下调用是错误的!

let position = binarySearch([2.0, 3.0, 5.0, 7.0], forKey: 5.0)//报错

当然我们可以继续使用extension Double:Ordered{}来解决问题,但是说实话真的不咋地,是时候思考下了!

为何要给Comparable进行extension,为何不是给Ordered协议进行呢?就像这样:

extension Ordered {
  //报错
  func precedes(other: Self) -> Bool { return self < other }
}

什么? 无法比较selfother?加个约束呗!

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

还记得Self吗?其实就是遵循协议的类型的placeholder罢了!

再来看看 let truth = 3.14.precedes(98.6) 调用,已经编译报错了,确实本就不应该能够调用,去掉它吧!

最后美化下所写的二分法代码,你可以写成两种方式

方式一:全局函数

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)

方式二:扩展已有协议

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)

相比较我更喜欢后者!

上一篇下一篇

猜你喜欢

热点阅读