Swift进阶十二:协议

一:对比Swift协议与其他语言

泛型可以帮助我们写出动态的程序。协议可以与函数和泛型协同工作,让我们代码的动态特性更加强大.

Swift 协议既可以被用作代理,也可以让你对接口进行抽象,比如Sequence,和OC协议的最大不同在于我们可以让结构体和枚举类型满足协议。除此之外,还可以有关联类型,可以通过协议扩展的方式为协议添加方法实现等等.

在面向对象编程中,子类是在多个类之间共享代码的有效方式。一个子类将从它的父类继承所有的方法,然后选择重写其中的某些方法.
在 Swift 中,代码共享也可以通过协议和协议扩展来实现。Sequence 协议和它的扩展在结构体和枚举这样的值类型中依然可用,而这些值类型是不支持子类继承的.

不再依赖于子类让类型系统更加灵活。在 Swift (以及其他大多数面向对象的语言) 中,一个类只能有一个父类。当我们创建一个类时,我们必须同时选择父类,而且我们只能选择一个父类,而C++这种可以多重继承的语言,又会有新的问题,相比多继承,实现多个协议并没有那些问题.

协议扩展是一种可以在不共享基类的前提下共享代码的方法。协议定义了一组最小可行的方法集合,以供类型进行实现。而类型通过扩展的方式在这些最小方法上实现更多更复杂的特性.

要实现一个对任意序列进行排序的泛型算法,你需要两件事情。首先,你需要知道如何对要排序的元素进行迭代。其次,你需要能够比较这些元素的大小,就这么多。没有必要知道元素是如何被存储的,也没有必要规定这些元素到底是什么只要你在类型系统中提供了前面提到的那两个约束,我们就能实现 sort 函数.

extension Sequence where Element: Comparable { 
      func sorted() -> [Self.Element] 
}

通过父类来添加共享特性就没那么灵活了,在开发过程进行到一半的时候再决定为很多不同的类添加一个共同基类往往是很困难的,这需要大量的重构。而且如果你不是这些子类的拥有者的话,就更做不到了.
子类必须知道哪些方法是它们能够重写而不会破坏父类行为的。比如,当一个方法被重写时,子类可能会需要在合适的时机调用父类的方法,这个时机可能是方法开头,也可能是中间某个地方,又或者是在方法最后。通常这个调用时机是不可预估和指定的。另外,如果重写了错误的方法,子类还可能破坏父类的行为,却不会收到任何来自编译器的警告.

通过协议进行代码共享相比与通过继承的共享,有这几个优势:
1.我们不需要被强制使用某个父类。
2.我们可以让已经存在的类型满足协议 (比如我们让 CGContext 满足了 Drawing)。子类就没那么灵活了,如果 CGContext 是一个类的话,我们无法以追溯的方式去变更它的父类。
3.协议既可以用于类,也可以用于结构体,而父类就无法和结构体一起使用了。
4.当处理协议时,我们无需担心方法重写或者在正确的时间调用 super 这样的问题。

二:面向协议编程

1.追溯建模

协议的最强大的特性之一就是我们可以以追溯的方式来修改任意类型,让它们满足协议.这也是最常用的特性.

protocol Drawing { 
      mutating func addEllipse(rect: CGRect, fill: UIColor)
      mutating func addRectangle(rect: CGRect, fill: UIColor) 
}

extension CGRect: Drawing {
    func addEllipse(rect: CGRect, fill: UIColor) {
       print("addEllipse CGRect")
    }
    
    func addRectangle(rect: CGRect, fill fillColor: UIColor) {
        print("addRectangle CGRect")
    }
}

2.协议扩展

作为协议的作者,当你想在扩展中添加一个协议方法,你有两种方法。首先,你可以只在扩展中进行添加。或者,你还可以在协议定义本身中添加这个方法的声明,让它成为协议要求的方法。协议要求的方法是动态派发的,而仅定义在扩展中的方法是静态派发的。

添加在扩展中的方法

extension Drawing {
    mutating func addCircle(center: CGPoint, radius: CGFloat, fill: UIColor) {
        print("addCircle Drawing")
    }
}

另外也可以重写这个方法,编译器将选择 addCircle 的最具体的版本,也就是定义在 CGRect 扩展上的版本

func addCircle(center: CGPoint, radius: CGFloat, fill: UIColor) {
        print("addCircle CGRect")
}
var rect = CGRect.zero
rect.addCircle(center: .zero, radius: 0, fill: .clear)
// addCircle CGRect

如果把声明的类型改为Drawing,编译器会使用了协议扩展中的addCircle 方法,而没有用 CGRect 扩展中的

var rect:Drawing = CGRect.zero
rect.addCircle(center: .zero, radius: 0, fill: .clear)
//addCircle Drawing

当我们将 rect 定义为 Drawing 类型的变量时,编译器会自动将 CGRect 值封装到一个代表协议的类型中,这个封装被称作存在容器.
对存在容器调用 addCircle 时,方法是静态派发的,也就是说,它总是会使用 Drawing的扩展。如果它是动态派发,那么它肯定需要将方法的接收者 CGRect 类型考虑在内.

如果需要动态派发,就要把方法定义到协议本体中

protocol Drawing {
    mutating func addEllipse(rect: CGRect, fill: UIColor)
    mutating func addRectangle(rect: CGRect, fill: UIColor)
    mutating func addCircle(center: CGPoint, radius: CGFloat, fill: UIColor)
}

extension Drawing {
    mutating func addCircle(center: CGPoint, radius: CGFloat, fill: UIColor) {
        print("addCircle Drawing")
    }
}

并且不影响在扩展中添加一个默认实现,具体的类型还是可以自由地重写 addCircle。因为现在它是协议定义的一部分了,它将被动态派发。在运行时,根据方法接收者的动态类型的不同,存在容器将会在自定义实现存在时对其进行调用。如果自定义实现不存在,那么它将使用协议扩展中的默认实现。addCircle 方法变为了协议的一个自定义入口.标准库大量使用这种方法,比如Sequence定义的方法很多但基本都有默认实现.

三:特殊的协议类型

带有关联类型的协议,以及协议定义中在任何地方使用了 Self 的协议,这两种协议和普通的协议是不同的,这样的协议不能被当作独立的类型来使用.

1.带有关联类型的协议
IteratorProtocol包含一个关联类型 Element和一个返回该类型可选值的 next() 函数

public protocol IteratorProtocol { 
      associatedtype Element 
      public mutating func next() -> Element? 
}

假如我们写出 var a:IteratorProtocol
编译器就会报错:
"Protocol 'IteratorProtocol' can only be used as a generic constraint because it has Self or associated type requirements"
(IteratorProtocol’ 协议含有 Self 或者关联类型,因此它只能被当作泛型约束使用)

这就是说,将 IteratorProtocol 是一个不完整的类型。我们必须为它指明关联类型,否则单是关联类型的协议是没有意义的.
这样使用是可以的

func nextInt<I: IteratorProtocol>(iterator: inout I) -> Int? where I.Element == Int { 
      return iterator.next() 
}

这是可行方式,但是却有一个缺点,存储的迭代器的指定类型通过泛型参数 “泄漏” 出来了。在现有的类型系统中,我们无法表达 “元素类型是 Int 的任意迭代器” 这样一个概念。如果你想把多个 IteratorStore 放到一个数组里,这个限制就将带来问题。数组里的所有元素都必须有相同的类型,这也包括任何的泛型参数.

不过我们可以绕过这种限制,这种将 (迭代器这样的) 指定类型移除的过程,就被称为类型抹消.

实现一个封装类。我们不直接存储迭代器,而是让封装类存储迭代器的 next函数。要做到这一点,我们必须首先将 iterator 参数复制到一个本地的 var 变量中,这样我们就可以调用它的 mutating 的 next 方法了。接下来我们将 next() 的调用封装到闭包表达式中,然后将这个闭包赋值给属性。我们使用类来表征 IntIterator 具有引用语义.

class IntIterator { 
      var nextImpl: () -> Int?
      init<I: IteratorProtocol>(_ iterator: I) where I.Element == Int { 
            var iteratorCopy = iterator 
            self.nextImpl = { iteratorCopy.next() } 
      }
}

现在,在 IntIterator 中,迭代器的具体类型 (比如 ConstantIterator) 只在创建实例的时候被指定。在那之后,这个具体类型被隐藏起来,并被闭包捕获。我们可以使用任意类型且元素为整数的迭代器,来创建 IntIterator 实例。

var iter = IntIterator(ConstantIterator()) 
iter = IntIterator([1,2,3].makeIterator())

2.带有Self的协议
最简单的带有 Self 的协议是 Equatable。它有一个 (运算符形式的) 方法,用来比较两个元素

protocol Equatable { 
      static func ==(lhs: Self, rhs: Self) -> Bool 
}

我们不能简单地用 Equatable 来作为类型进行变量声明,这很好理解,任何类型都可以实现Equatable,但是两种类型显然不能分别写在"=="的两边.

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,457评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,837评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,696评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,183评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,057评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,105评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,520评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,211评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,482评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,574评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,353评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,213评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,576评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,897评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,174评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,489评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,683评论 2 335

推荐阅读更多精彩内容