標簽(空格分隔): WWDC
Classes Are Awesome
- 封裝
- 訪問控制
- 抽象
- 命名空間
- 語法表達
- 延伸性
Type Are Awesome
- 訪問控制
- 抽象
- 命名空間
這些是讓程序員管理復雜事件的要素。
但是用structs與enums可以實現(xiàn)以上功能。
Three Beefs about Class
1.Implicit Sharing
當A與B同時指向一個對象的時候,經(jīng)常會發(fā)生錯誤。
- 為了減少在代碼中的錯誤瘋狂的使用copy。
- 使用過多的copy帶來的性能上的影響。
- 當使用Dispatch_queue時,提供了一個比賽場景,因為線程共享了一個的可變狀態(tài)。
- 所以你需要為了保護你的常量加上lock。
- 這個lock使得性能更加低下。
- 甚至造成死鎖。
- 產(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
- 需要正確選擇一個好的父類。
- 單一的繼承,得到父類中所有的信息。
- 必須在Class創(chuàng)建時繼承,而不是在之后拓展。
- 如果父類儲存了許多屬性。
- 子類也必須要儲存屬性。
- 初始化負擔加重。
- 注意不能修改父類中的常量。
- 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
} }
- 在Drawable協(xié)議中添加了isEqualTo方法,
$0==($1)改為$0.isEqualTo($1)。 - 將isEqualTo參數(shù)類型與Diagram中的elements一致,都是繼承Drawable協(xié)議。
- 拓展Drawable協(xié)議,并加入約束條件。
- 先確定傳入的參數(shù)是否是self類型,因為有了Equatable的限定,就可以使用就使用 == 操作符去判斷,不是就返回false。
結(jié)尾
什么時候使用class?
當你想要implicit sharing 時
- 拷貝或者比較實例沒有意義的是有(如,window)
- 實例生命周期與外部影響(如,臨時文件)
- 實例就像通過只寫的方式流到外部狀態(tài)(如,CGContext)
final class StringRenderer : Renderer {
var result: String
...
}
比如這個。
- final。
- 繼承的不是class。
不要與系統(tǒng)作對
- 如果一個框架需要你傳遞一個對象或者子類,就需要。
也要謹慎
- 在軟件中不應該有太大的東西
- 當納入了class之外的元素,考慮不要用class
總結(jié)
使用協(xié)議而不是超類
拓展協(xié)議等于魔法