由 Dave Abrahams Professor 演讲,这货是C++牛人,自行wiki。
笔记先从 Object Oriented Programming - OOP(面向对象编程)说起,自然少不了 Class 这个主角,当初我入坑 iOS 时就问过一个问题:Class 和 Struct 的区别以及应用场景?
当然本文中不会详尽地去比较孰优孰劣,只是简单阐述下,仅供参考。
Classes Are Awesome
- Encapsulation(封装)
- Access Control(访问控制)
- Abstraction(抽象)
- Namespace(命名空间)
- Expressive Syntax(表达语法)
- Extensibility(拓展性)
其中“Access Control”、“Abstraction”和“Namespace” 指明是 Class 令人头疼之处。
先说说继承性,Class 完胜 Structure,记住Structure是不具有继承性的!
类可以将对象抽象成多个描述属性和方法,可以通过继承得到子类,也就是传统意义上的superclass 和 subclass。 子类可以从父类继承一系列方法,当然也可以通过 override 重写父类的方法。
The Three Beef
- Implicit Sharing
- Inheritance All Up In Your Business
- Lost Type relationShips
第一点: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提出了几个不错的抽象机制:
- 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
面向协议编程
英文 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的回答,实际上这样的,尽管sortedKeys
和 forKey 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)
}
}
效果图:
协议扩展和泛型
声明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);
- 调用的自然是TestRenderer的circleAt方法
- 调用的自然是TestRenderer的rectangleAt方法
现在修改let r : Renderer = TestRenderer()
,编译器只知道r是Renderer类型,而非TestRenderer
,此时1、2调用的是谁的方法呢?ps:swift以后要有笔试题 这绝壁会有!
- 调用TestRenderer的circleAt方法,因为这在Renderer协议中是required的
- 调用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语言实现原理。
首先认识几个问题:
- 要使用二分法查找的数组一定是有序的!
- 数组元素类型可以是Int,Double,String等!
实现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 }
}
什么? 无法比较self
和other
?加个约束呗!
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)
相比较我更喜欢后者!