标签(空格分隔): WWDC
Classes Are Awesome
- 封装
- 访问控制
- 抽象
- 命名空间
- 语法表达
- 延伸性
Type Are Awesome
- 访问控制
- 抽象
- 命名空间
这些是让程序员管理复杂事件的要素。
但是用structs与enums可以实现以上功能。
Three Beefs about Class
1.Implicit Sharing
当A与B同时指向一个对象的时候,经常会发生错误。
- 为了减少在代码中的错误疯狂的使用copy。
- 使用过多的copy带来的性能上的影响。
- 当使用Dispatch_queue时,提供了一个比赛场景,因为线程共享了一个的可变状态。
- 所以你需要为了保护你的常量加上lock。
- 这个lock使得性能更加低下。
- 甚至造成死锁。
- 产生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上出现,因为Swift的所有集合都是值类型。
2.Class Inheritance
- 需要正确选择一个好的父类。
- 单一的继承,得到父类中所有的信息。
- 必须在Class创建时继承,而不是在之后拓展。
- 如果父类储存了许多属性。
- 子类也必须要储存属性。
- 初始化负担加重。
- 注意不能修改父类中的常量。
- ovewride时,需要知道是什么方法,或者怎么去写(什么时候不能使用override)。
3.Lost Type Relationships
class Ordered {
func precedes(other: Ordered) -> Bool { fatalError("implement me!") }
}
首先类必须实现方法,不然会报错。
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 = "" ... }
现在创建一个Number类,以及一个Label类,他们都集成了Ordered。
在Number类中重载使用方法precedes不能使用子类参数value。
因为这里不能不是所有的Ordered的子类都会有value属性。
所以需要改为
override func precedes(other: Ordered) -> Bool
{
return value < (other as! Number).value
}
这样的问题来自于Class之间对于自身和其他类的类型并没有建立关系。
类型关系的缺失经常是由于抽象地使用类。
一个更好的抽象机制
支持值类型 (and classes)
支持静态类型关系 (和动态调度) 非完全统一的
支持逆袭建模
不在模型上引入实例数据
不在模型上引入初始化负担
使实现的内容更加清晰
这不就是Protocal的优点吗?
用Protocal开始编码
首先将上面的代码进行转换
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
}
}
这样就再也不需要一个基类,实现方法的时候也不需要override,同时不希望number作为一个类去使用,改成了struct类型。
func precedes(other: Number) -> Bool
{
return self.value < other.value
}
此时需要解决潜在的静态类型的安全漏洞,因为other参数可能是另外的类型,所以现在设置为Number类型,这样就不需要再做类型判断,但是这样又与协议中的Ordered类型相互矛盾。于是我们现在将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时,它是作为一个遵守了这种模型类型的占位符。所以现在代码也变得有效了。
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前也是有效的,现在加入self后会报错,protocol 'Ordered' can only be used as a generic constraint because it has Self or associated type requirements。本来我们可以处理由numbers和label组成的[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 }
这么处理后,只能对单一的Ordered类型处理,看起来特别严格,好像缺失了很多灵活性。但是想想看,对于不同类型同时存在的处理我们往往是去阻止,而不是真正意义上的处理。事实上,单一类型的数组才是我们想要的东西。
有无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 “单一化” |
每一个模型都与其他类型有关联 | 模型在交互上是自由的 |
动态调度 | 静态调度 |
有更少的优化度 | 有更多的优化度 |
挑战!将使用Class改为使用Protocol
准备
首先设定一个struct类型的Renderer,用print方式直观实现方法。
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))") }
}
然后创建一个Drawable协议为我们的元素提供一个共同的接口。
protocol Drawable { func draw(renderer: Renderer) }
创建一个Polygon形状,因为是值类型,所以使用了struct,并包含了一个点的数组。通过调用renderer的方法,遍历数组中所有的点画出形状。
struct Polygon : Drawable {
func draw(renderer: Renderer)
{
renderer.moveTo(corners.last!)
for p in corners
{
renderer.lineTo(p)
}
}
var corners: [CGPoint] = []
}
同理,创建一个Circle。需要中心点和半径。也通过调用renderer的方法。
struct Circle : Drawable {
func draw(renderer: Renderer)
{
renderer.arcAt(center, radius: radius, startAngle: 0.0, endAngle: twoPi)
}
var center: CGPoint
var radius: CGFloat
}
最后创建一个Diagram,其中的elements类型是Drawable。因为所有的Darwable都是值类型,所以这里的Darwable类型的数组也是值类型。
struct Diagram : Drawable {
func draw(renderer: Renderer) {
for f in elements
{
f.draw(renderer)
}
}
var elements: [Drawable] = []
}
根据以上内容,我们可以进行测试。
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协议。
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部分做了处理。
最后再修改一下调用方式即可。
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) { }
}
现在可以为CGContext拓展协议。
如果这个Renderer是class,就不能这么做。
这样做之后CGContext就拥有了Renderer中所以基础的东西,并且需要全部是实现。
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中。
易测的协议与泛型
如果用协议去解耦,任何东西都能够变得容易测试。
这中测试与使用模型很接近。但是模型本身是比较脆弱的。
你必须去将你的测试代码与在测试下的实现代码联系起来,因为模型很脆弱,所以不能在Swift的强静态类型系统下很好的运行。
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)
}
}
我们需要在协议中加上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协议的地方,我们可以用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)
}
}
但是这么做特别奇怪,因为每一个地方都要补充相同的类容,很复杂。于是我们直接对协议做了扩展。
extension Renderer {
func circleAt(center: CGPoint, radius: CGFloat) {
arcAt(center, radius: radius, startAngle: 0, endAngle: twoPi)
}
}
这么完成之后就不需要再对其他准守了Renderer协议的地方再继续扩展。
协议的扩展
刚刚看到
extension Renderer {
func circleAt(center: CGPoint, radius: CGFloat) {
arcAt(center, radius: radius, startAngle: 0, endAngle: twoPi)
}
}
是对已经存在在Renderer协议中的方法进行了拓展,如果这个方法不存在在Renderer中呢?拓展了不存在在协议中的方法,那这个方法和拓展本来在协议中的方法的区别是什么?
现在我来做一个简单模型的示范:
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")
}
}
创建好之后我创建一个test对象,并且进行测试。
let test = testStruct()
test.a()
test.b()
test.c()
结果为:
a3
b2
c3
这看上去没什么奇怪的,甚至我们直接把extension testProtocol去除也没关系,但是我们再这么修改一下,如果swift知道它遵守了testProtocol呢?
let test:testProtocol = testStruct()
test.a()
test.b()
test.c()
结果为:
a3
b2
c1
为什么?因为a方式是必须的,所以调用了被定制的方法。而c方法不是必须的,所以在testStruct中只是覆盖了testProtocol的拓展实现内容。而现在,swift只知道test是testProtocol而不是testStruct,所以调用了testProtocol中实现的结果。
是不是说每一个方法都是必须的呢?对于大部分API来说是不一定的,所以最好的做法是覆盖协议中存在的必须实现的方法,而不是覆盖模型上的方法。
更多协议扩展的技巧
拓展的约束
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类型的数字,但是编译器也会报错,因为int类型并没有遵守Ordered,这时候我们为了解决这个问题,加上了:
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协议,我们可以直接拓展Compareble协议。
extension Comparable {
func precedes(other: Self) -> Bool { return self < other }
}
extension Int : Ordered {}
extension String : Ordered {}
省略了在Int和String中实现precedes方法的字段。
现在如果要查找Double类型呢?是否也要在加一次扩展?是事实就算不进行拓展,Double类型的对象也能实现precedes方法,事实上它就算能实现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。
现在请看下面这段代码,如果不遵守Equatable协议:
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之间,所以现在没有相等的操作符用于这两个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协议中的 == 。
protocol Equatable {
func == (Self, Self) -> Bool
}
它有Self-requirements,这就意味着现在Drawable现在也有Self-requirements的特征。Self-requirements直接将Drawable放在了单一性,静态调度的世界,但是Diagram确实需要多态性的Drawable类型的数组,因为我们需要将polygons和circles放在相同的Diagram中,所以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.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
} }
- 在Drawable协议中添加了isEqualTo方法,
$0==($1)
改为$0.isEqualTo($1)
。 - 将isEqualTo参数类型与Diagram中的elements一致,都是继承Drawable协议。
- 拓展Drawable协议,并加入约束条件。
- 先确定传入的参数是否是self类型,因为有了Equatable的限定,就可以使用就使用 == 操作符去判断,不是就返回false。
结尾
什么时候使用class?
当你想要implicit sharing 时
- 拷贝或者比较实例没有意义的是有(如,window)
- 实例生命周期与外部影响(如,临时文件)
- 实例就像通过只写的方式流到外部状态(如,CGContext)
final class StringRenderer : Renderer {
var result: String
...
}
比如这个。
- final。
- 继承的不是class。
不要与系统作对
- 如果一个框架需要你传递一个对象或者子类,就需要。
也要谨慎
- 在软件中不应该有太大的东西
- 当纳入了class之外的元素,考虑不要用class
总结
使用协议而不是超类
拓展协议等于魔法