引自Apple Swift團隊的一句話
At the heart of Swift’s design are two incredibly powerful ideas: protocol-oriented programming and first class value semantics. Each of these concepts benefit predictability, performance, and productivity, but together they can change the way we think about programming.
通過值類型和面相協議協議來編寫你的App,你會發現原來Swift對比Objective C來說,是如此的Powerful
首先,我們來看看OC中常見的需要Copy的類型,比如NSString,NSArray,NSDictionary…
通常,你需要將一個NSStrig聲明為Copy,so,why?
如果我們將一個NSString聲明為strong
@property (strong,nonatomic)NSString * str;
然後,這麼調用
NSMutableString * mutableStr = [@"Leo" mutableCopy]; self.str = mutableStr; [mutableStr appendString:@" MobileDeveloper"]; NSLog(@"%@",self.str); //Leo MobileDeveloper
所以,問題就在這了,本身NSString是一個不可變對象,但是我的str潛在的被修改了都不知道。所謂為了更安全,很多類型都采用了Copy來進行操作。
Swift中,Class是引用類型,Struct和Enum是值類型。引用類型在傳遞的時候,具有implict sharing. 也就是,默認是同一個內存對象的引用。而值類型在傳遞的時候,都是copy。
比如
class Person{ var name:String var age:UInt init(name:String,age:UInt){ self.name = name self.age = age } }
然後,當你進行傳遞,然後不小心在某一個地方修改了一個屬性,你會發現原始的對象被修改了(即使我將Person聲明為let)
let jack = Person(name: "Jack", age: 23) let temp = jack temp.age = 24 print(jack.age) //24
這樣做導致的結果就是:
你的實例在多個地方都可能被修改,導致了很多你注意不到的代碼耦合 你的實例依賴於外部狀態,所以,單元測試變的困難。甚至,你的代碼通過了單元測試,仍然會有Bug然後,我們再來看看值類型。
struct Person{ var name:String var age:UInt } let jack = Person(name: "Jack", age: 23) var temp = jack temp.age = 24 print(jack.age) //23
值類型的最大優勢是:不可變狀態,拿到值類型你不需要考慮別處會修改它。
如果你懂一點函數式編程語言,比如Haskell,你一定會了解不可變狀態帶來的代碼可讀性,可維護性,可測試行的好處。
如果你在網上搜索,那麼一定會有很多博客,很多問答告訴你:“值類型用在簡單的Model上”
網上別人的觀點一定是正確的嗎?
當然不是。
值類型不僅僅能用在Model上,從MVVM的角度,他也能用在View,Controller,和viewModel上。並且,帶來很多好處。WWDC 2016的這個視頻Protocol and Value Oriented Programming in UIKit Apps介紹了值類型用在View的Layout以及Controller的Undo。這裡我就不拾人牙慧,再寫一遍了。
引自Swift官方的描述,以下場景你可以優先考慮值類型
當這個數據結構主要是為了封裝一系列相關的值 當進行傳遞時候,應該傳遞的是Copy,而不是引用 這個數據結構本身存儲的類型也是值類型 這個數據結構不需要繼承通常值類型,你都要讓其遵循Equatable來保證可以用符號==來比較。
比如
struct Point:Equatable{
var x,y:CGFloat
}
func == (lhs:Point,rhs:Point)->Bool{
return lhs.x == rhs.x && lhs.y == rhs.y
}
值類型性能
看到這,有經驗的你一定在想,值類型每次都Copy,不會有性能問題嗎?
絕大部份時候,沒有性能問題。
並且相對於Class,Struct往往性能更好,因為:
通常Struct在棧上分配內存,Class在堆上分配內存。在堆上分配內存的性能是要低於棧上的,因為堆上不得不做很多鎖之類的操作來保證多線程的時候不會有問題。 Struct無需引用計數,而Classs需要。引用計數會造成隱式的額外開銷 Struct不能繼承,所有在方法執行的時候,是static dispatch.而Class是dynacmic dispatch。意味著Class需要通過virtual table來動態的找到執行的方法。
Tips:如果你希望深入了解Swift的性能問題,建議看看WWDC 2016的這個視頻 Understanding Swift Performance
協議的定義
A protocol defines a blueprint of methods, properties, and other requirements that suit a particular task or piece of functionality. (協議定義了一個功能需要的方法,屬性以及其它必要條件的藍圖)
也就是說,協議是一個功能抽象的體現,協議本身只提供一套規則。
Swift協議的優秀特性
協議是一種類型
Swift協議是一種類型。也就意味著你可以
把它作為函數的參數或者返回值 聲明一個常量,變量或者屬性 作為容器類型(Array/Dictionary)的Element
比如
protocol Drawable{
func draw()
}
struct Circle:Drawable{
var center:CGPoint
var radius:CGFloat
func draw(){
print("Draw cirlce at \(center) with radius \(radius)")
}
}
struct Line:Drawable{
var from,to:CGPoint
func draw(){
print("Draw line from \(from) to radius \(to)")
}
}
struct Chart:Drawable{
var items:[Drawable]
func draw() {
items.forEach{
$0.draw()
}
}
}
let circle = Circle(center: CGPointMake(10, 10), radius: 5)
let line = Line(from: CGPointMake(10, 10), to:CGPointMake(15, 10))
let chart = Chart(items: [circle,line])
chart.draw()
//Draw cirlce at (10.0, 10.0) with radius 5.0
//Draw line from (10.0, 10.0) to radius (15.0, 10.0)
因為協議本身是一種類型,你所需要關注的就是抽象的本身,比如上文的代碼裡我只需要關注
Drawable(可繪制),至於具體是怎麼實現的,我並不關心。
可繼承
protocol Drawable{
func draw()
}
protocol AntoherProtocol:Drawable{
func anotherFunc()
}
協議本身也是可擴展的
通過擴展,來提供協議的默認實現
比如,利用extension來擴展協議Drawable
extension Drawable{
func printSelf(){
print("This is a drawable protocol instance")
}
}
//然後,我們就可以這麼調用了
let circle = Circle(center: CGPointMake(10, 10), radius: 5)
circle.printSelf()
讓現有Class/Struct枚舉遵循協議
可以通過Extension的方式,來讓一個現有類實現某一個協議。
比如,以下是最簡單的Swift Model,Person和Room。
struct Person{
var name:String
var age:Int
var city:String
var district:String
}
struct Room{
var city:String
var district:String
}
然後,突然有一天,我們有個需求是需要Log出Person和Room的地址信息。這時候怎麼做?
方式一,通過修改Person和Room的實現。這樣其實就違背了“對擴展開放,對修改封閉的原則”。
方式二,通過Swift協議擴展
//定義新的需求的協議
protocol AddressPrintAble{
var addressDescription:String {get}
}
//通過擴展,讓Person和Room實現協議
extension Person:AddressPrintAble{
var addressDescription:String{
return "\(city) \(district)"
}
}
extension Room:AddressPrintAble{
var addressDescription:String{
return "\(city) \(district)"
}
}
然後,你就可以把Person,Room結構體的實例,當作AddressPrintAble(新的可打印地址)類型來使用了。因為協議是一種類型,你可以把他們存到一個數組裡。
let leo:AddressPrintAble = Person(name: "Leo", age: 24, city: "Shanghai", district: "XuHui")
let room:AddressPrintAble = Room(name: "XinSi", city: "Shanghai", district: "Xuhui")
var array = [AddressPrintAble]()
array.append(leo)
array.append(room)
array.forEach { (address) in
print(address.addressDescription)
}
通過協議來擴展Cocoa類是一個很常見也是很有用的一個技巧
條件擴展
可以在擴展協議的時候,用where語句進行約束,只對滿足制定條件的約束協議進行擴展。比如,我們在初始化一個NSObject子類的時候,我們希望有一些初始化配置,如果能夠讓NSObject提供一個操作符來讓我們進行初始化配置就好了
於是,我們可以這麼擴展
public protocol SetUp {}
extension SetUp where Self: AnyObject {
public func SetUp(@noescape closure: Self -> Void) -> Self {
closure(self)
return self
}
}
extension NSObject: SetUp {}
然後,你就可以這麼調用了
let textfield = UITextField().SetUp {
$0.frame = CGRectMake(0, 0,200, 30)
$0.textAlignment = .Center
$0.font = UIFont.systemFontOfSize(14)
$0.center = view.center
$0.placeholder = "Input some text"
$0.borderStyle = UITextBorderStyle.RoundedRect
}
其它
僅能用在Class上的協議
protocol myProtocol:class
協議組合Protocol Composition
同時遵循兩個協議
protocol Named {
var name: String { get }
}
protocol Aged {
var age: Int { get }
}
func wishHappyBirthday(celebrator: protocol) {
print("Happy birthday \(celebrator.name) - you're \(celebrator.age)!")
}
Self
協議方法中的Self,表示實際的遵循協議的類型。
protocol Orderable{
func compare(other:Self)->Bool
}
然後
protocol Orderable{
func compare(other:Self)->Bool
}
extension CGPoint:Orderable{
func compare(other: CGPoint) -> Bool { //注意,這裡的Other是CGPoint 類型了
return self.x > other.x && self.y > other.y
}
}
associatedtype
當你的協議中,有不確定類型的對象,你可以通過associatedtype來聲明
比如
protocol Listable{
associatedtype Content
var dataList:[Content]{get}
}
extension CGPoint:Listable{
typealias Content = CGFloat
var dataList:[Content]{
get{
return [x,y]
}
}
}
總結
協議會讓你的代碼變的前所未有的靈活,比如我在寫PullToRefreshKit的時候,就用協議增強了其可擴展性。
舉個例子,對於下拉刷新Header。
定義了Protocol RefreshableHeader來表示任意可刷新的View的抽象。
public protocol RefreshableHeader:class{
func distanceToRefresh()->CGFloat
func percentUpdateWhenNotRefreshing(percent:CGFloat)
func releaseWithRefreshingState()
func didBeginEndRefershingAnimation(result:RefreshResult)
func didCompleteEndRefershingAnimation(result:RefreshResult)
}
然後,利用協議和范型,任何遵循這個協議的UIView子類,都可以作為我的Refresh Header
public func setUpHeaderRefresh(header:T,action:()->())->T{
//...
}