承接上文,已经简明阐述了使用Struct代替Class的好处,使用Class会使我们的程序出现“意外的共享”以及“循环引用”之类的危险,传统面向对象开发中对Class的依赖主要来自于我们对“继承”的依赖。Swift2.0引入协议扩展后,之前的“类-继承”所能实现的功能使用“结构体(枚举)-协议-协议扩展”都可以实现,并且更加高效和灵活。回到主题上来,首先回顾下“幽灵架构”中的两个主体:View和Model所对应的协议:
//视图使用的协议
protocol ViewType{
func getData<M:ModelType>(model:M)
}
//数据使用的协议
protocol ModelType{
}
//定义默认方法giveData
extension ModelType{
func giveData<V:ViewType>(view:V){
view.getData(self)
}
}
在控制器中生成了Model和View的实例之后,需要将二者绑定起来:
dataList[indexPath.row].giveData(cell)
如果你尝试修改顺序使用:
cell.getData(dataList[indexPath.row])
在dataList是[ModelType]类型的情况下编译无法通过,但是对于[Festival]或者[Event]这样同构的数组,在捆绑时调用getData方法是可行的,这是因为getData的参数是泛型,而数组中的元素是ModelType类型的,不符合泛型的使用规范,所以编译器会报错,但是为什么同样是泛型方法,反过来调用giveData就可以呢?giveData其实有两层类型检查的“关口”,第一层是giveData要求参数是遵守ViewType的泛型类型,而Demo中的每一个cell实例都是具体的类型,所以cell可以作为giveData的参数,而一旦giveData的参数通过考验,那么在giveData默认实现的方法体中,调用了参数的getData方法,这个方法也要求传入一个遵守协议的具体类型,这里传入了self属性,这个属性返回的是实例本身,所以可以顺利通过编译器的检测。所以真正进行数据绑定的是getData方法,giveData方法的作用是避免getData的参数是协议类型的,从而产生不必要的开销,使得即使数据源是异构的,依然可以通过giveData的方式传递真实的数据类型。
这里不得不再提一句泛型的好处,泛型保证了原始数据的传递,而不会像协议类型那样多一步寻址的过程,保证了高效。所以“幽灵架构”中的数据绑定虽然被移出了控制器代码,但是其实和在控制器中直接进行数据绑定同样高效,为了证明这一点,我们在TableViewCell的子类的getData方法中加入一个sizeOfValue方法,检查参数model的长度:
func getData<M : ModelType>(model: M) {
print(sizeofValue(model))
//这里不能写成guard let dateModel = model as? DateViewModel else{}令我有些意外
guard let dateModel = model as? hasDate else{
return
}
//处理相同属性
dateLabel.text = dateModel.date
//处理数据源异构
if let event = dateModel as? Event{
MixLabel.text = event.eventTitle
backgroundColor = UIColor.redColor()
} else if let festival = dateModel as? Festival{
MixLabel.text = festival.festivalName
}
}
运行,打印结果:
因为我们定义的Festival和Event中有两个String类型的属性,每个String的大小是24字节,所以Festival和Event都是48字节,现在我们把Event改成一个Class:
class是引用类型的,所以它在栈上的长度是8个字节(一个指针的长度),可以看到在“幽灵架构”体系中,Model和View的绑定不会产生中间层,二者是直接绑定的,那么问题来了,这种互相绑定会不会产生“循环引用”?
首先,如果Model都是结构体的话,是不会产生循环引用的,每个值类型都只有一个拥有者(因为Copy),在值类型的拥有者执行完毕后,值类型随着拥有者一同销毁。虽然struct可以定义init,但是你会发现当你想在struct中定义一个deinit方法时,编译器提示你deinit只能被定义在类中:
所以你不用关心值类型的生命周期,那么已经被改成了class的Event呢?为了制造可能造成循环引用的场景,我们在事件节日提醒控制器前面再加一个控制器,把新加的控制器和事件节日提醒控制器用导航控制器连接起来,现在通过导航栏返回的时候事件节日提醒控制器会被系统回收,所有的Model和View实例也应该被回收,此时可以检验有没有出现“循环引用”,在Event中定义一个deinit:
class Event:DateViewModel{
var date = ""
var eventTitle = ""
init(date:String,eventTitle:String){
self.date = date
self.eventTitle = eventTitle
}
deinit{
print("deinit")
}
}
现在运行程序,点击按钮来到事件节日提醒列表页面,然后点击返回,可以看到中控台打印:
谢天谢地^ ^。现在来解释一下,Swift中的实例方法其实并不会被保存在实例中,实例方法和全局方法不同的地方是多了一个命名空间,就好像类方法那样的格式,举个例子:
struct Test{
var a = ""
var b = ""
var c = ""
}
使用sizeof查看这个结构体的大小,显示为72,然后向其中增加一个方法:
struct Test{
var a = ""
var b = ""
var c = ""
func ab(str:String){
print(str)
}
}
再次查看Test的大小,仍旧是72。在调用这个方法的时候我们常用的格式是:
a.ab("111")
但其实我们也可以这样用:
Test.ab(a)("111")
这里用到了柯里化,对于实例方法来说,先传入一个实例a,返回一个接受String类型的参数的方法,再传入我们定义的参数类型。所以使用方法进行数据和视图的绑定是安全的,因为二者是同等关系的,并不存在依赖关系。这也解释了为什么在官方文档中讲解capture list的时候,普通的闭包和方法不会产生循环引用,只有把闭包作为参数的时候才会产生循环引用,如上面看到的,因为实例只会持有自己的参数而不会持有方法。