关于Swift 的一点认识:值类型和引用类型,Any 和AnyObject

Swift 1.2对值类型的处理做了很多优化,使其执行效率得到很大提高。国外很多评测文章也指出新的Swift 编译器编译出的代码明显比之前版本编译出的代码快。受其影响,决定将自己项目里的数据结构全部换成struct。经过这一番重构,感觉对Swift 这门语言有了点新的认识。

之前没用过有现代语法的其它语言,像Ruby、Scala 这类。于是看到Swift 那诸多眼花缭乱的“新”特性很是兴奋,一直坚持练习。但对其的理解始终停留在这种语言的表象层面或者说语法层面上,比如强大的enum,很流畅的closure 表达方式、函数式/范型思想的大量运用等等。只是觉得它比起其它古老的语言(Java、Objective-C)等要好用,却没有进行深入了解。

##值类型和引用类型

比如它一直宣扬的值类型安全,我一直觉得struct 没有多大用处,而且其官方文档上也建议“绝大多数情况下使用class”:

In all other cases, define a class, and create instances of that class to be managed and passed by reference. In practice, this means that most custom data constructs should be classes, not structures.

我觉得class 的一大优越之处正是在于其可以被“引用”,只要保持一份实体,剩下的操作都可以通过引用来解决,空间上可以节省好多。

但经过这次重构、大量使用struct 来保存、传递数据后,发现值类型确实有着非常“好”的地方,而且这是一种思维方式上的改变,我觉得没有什么比这更让我兴奋的了。

使用值类型后,最大的变化在于写代码的时候可以专注于当前代码段了。之前的习惯是收到数据后,会去考虑这个数据哪里来,确保数据的源头后再做相应的调整、操作;如果发现数据需要被修改,那么需要考虑这个修改是临时性的还是永久性的,以及是否需要把数据做备份来保证不会干扰到源头。而变成值类型后,所有的东西都是不变的了,如果做了改动,那系统会自动的给你做一份copy,你的所有修改都发生在这份copy 之上。所以不需要再去考虑这份数据的源头在哪儿,只需要想如果有修改是否要保持住这份修改,而这些操作是只发生在当前函数内部的,也就是如何设计这个函数的问题了。举个栗子,某个函数本身不会产生任何数据,而是需要修改传进来的数据,那么可以把函数的参数加上inout 关键字即可,这样所有的改变都会反应到参数里;而如果函数本身是会产生数据的,那么参数的作用就变成了参考,所以没必要动参数的内容,相反如果在写这个函数的时候发现要改参数数据的内容,很明显需要停下来想想是不是设计上哪里有问题了。

这种安全的方式让程序整个变的更可靠,也更简单,因为你可以大胆放心的使用传进来的参数,完全无毒副作用。至于多线程环境下自然的便利那就不用多说了。

不过相比引用类型来说,空间上确实会有一定的开销,比如传进来一个数组,一旦改了其中一个元素,那整个数组都会发生一次copy。但这些可以通过一定的设计方案来解决,比如上面说的inout 方式(但个人觉得inout 还是尽量少用,因为会破坏值类型安全的特性),或者借用一些临时变量。

##Any 和AnyObject

另外一个认识的变化是关于Swift 的Any 和AnyObject类型。这个算是上面说的值类型/引用类型的一点延伸。

之前用[AnyObject]时发现的一个问题,就是往[AnyObject]里放Bool、Int 后,它们会全部变成数值,然后在接收端用switch case 像下面一样来把每个数据还原时,如果先判断Bool,那么Int 也会被当作Bool --非0为true,0为false;但如果先判断Int,那么Bool 会变成Int--1或者0。换句话说就是无法把数据还原回来了。

1
2
3
4
5
6
7
8
9
10
for item in arr {
case let intValue as Int:
// process as Int
case let boolValue as Bool:
// process as Bool
case let strValue as String:
// process as String
default:
// Not supported type
}

当时不明白是怎么回事,以为是Swift 设计上的缺陷,但这次Google 一下后,了解到这背后的原因:因为AnyObject 是对类对象也就是引用类型的抽象,并非Swift 的值类型,所以String、Int 等Swift 的类型会被转成Cocoa 里相应的对象,比如String 会转成NSString,Int 会转成NSNumber。如果数据要返回,那会再把Cocoa 的对象转回String 的类型(Swift 1.2里已经不再支持这一种类型的隐式转换,还是挺不错的)。Int、Bool 都变成了NSNumber,原来的类型信息自然就不见了。

当时写这段代码是由于对这些事情不了解,以为AnyObject 就可以代表所有类型,而Any 不过相当于是AnyObject 的一个父类,所以用了[AnyObject]
试着用[Any]替代[AnyObject]后发现这些Bool、Int 都可以被很好的识别还原了。

当然最后没有使用[Any],因为这种过于抽象的东西做不到精确控制,用了值类型后,我需要让所有东西都为我所控:)
另外一个原因在于Swift 编译器的bug,在我的机器上当Any 数组里放到超过20个数据时,编译器会疯狂的吃内存,21个时会用到25G左右来完成编译,而到达22个时,编译器吃到60G+的内存后被系统杀死,编译无法完成。