Objective C转Swift注意事项(二)值类型和协议

来自Leo的原创博客,转载请著名出处

我的StackOverflow

profile for Leo on Stack Exchange, a network of free, community-driven Q&A sites

我的Github
https://github.com/LeoMobileDeveloper

注意:本文的代码是用Swift 2.2写的。


前言

引自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

在阅读本文之前,建议有时间的同学去看下WWDC 2015和WWDC 2016的三个视频,相信会很有收获的。


值类型

首先,我们来看看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

通常值类型,你都要让其遵循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<Named, Aged>) {
    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<T:UIView where T:RefreshableHeader>(header:T,action:()->())->T{
    //...
}

本来想把《面相协议编程》和《面相值类型编程》放到这一篇里来写的,写到这里发现篇幅有点过长了。这篇文章就当做是基础复习吧。后面再写一篇

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值