从周一 Swift 正式公布,到现在周五,这几天其实基本一直在关注和摸索 Swift 了。对于一门新语言来说,开荒阶段的探索自然是激动人心的,但是很多时候资料的缺失和细节的隐藏也让人着实苦恼。这一周,特别是最近几天的感受是,Swift 并不像我上一篇表达自己初步看法的文章里所说的那样,相对于 objc 来说有更好的学习曲线。甚至可以说 objc 在除了语法上比较特别以外,其概念还是比较容易的。而 Swift 在漂亮的语法之后其实隐藏了很多细节和实现,而如果无法理解这些细节和实现,就很难明白这门新语言在设计上的考虑。在实际编码中,也会有各种各样的为什么编译不通过,为什么运行时出错这样的问题。本文意在总结一下这几天看 Swift 时候遇到的自己觉得重要的一些概念,并重新整理一些对这门语言的想法。可能有些内容是需要您了解 Swift 的基本概念的,所以这并不是一篇教你怎么写 Swift 或者入门的文章,建议您先读读 Apple 官方给出的 Swift 的电子书,至少将第一章的 Tour 部分读完(这里也有质量很不错的但是暂时还没有完全翻译完成的中文版本)。不过因为自己也才接触一周不到,肯定说不上深入,还希望大家一起探讨。
类型?什么是类型?
这是一个基础的问题,类型 (Types) 在 Swift 中是非常重要的概念,在 Swift 中类型是用来描述和定义一组数据的有效值,以及指导它们如何进行操作的一个蓝图。这个概念和其他编程语言中“类”的概念很相似。Swift 的类型分为命名类型和复合类型两种;命名类型比较简单,就是我们日常用的类 (class)
,结构体 (struct)
,枚举 (enum)
以及接口 (protocol)
。在 Swift 中,这四种命名类型为我们定义了所有的基本结构,它们都可以有自己的成员变量和方法,这和其他一般的语言是不太一样的(比如很少有语言的enum可以有方法,protocol可以有变量)。另外一种类型是复合类型,包括函数 (func)
和 多元组 (tuple)
。它们在使用的时候不会被命名,而是由 Swift 内部自己定义。
我们在实际做开发时,一般会接触很多的命名类型。在 Swift 的世界中,一切看得到的东西,都一定属于某一种类型。在 PlayGround 或者是项目中,通过在某个实际的被命名的类型上 Cmd + 单击
,我们就能看到它的定义。比如在 Swift 世界中的所有基本型 Int
,String
,Array
,Dictionay
等等,其实它们都是结构体。而这些基本类型通过定义本身,以及众多的 extension
,实现了很多接口,共同提供了基本功能。这也正是 Swift 的类型的一种很常见的组织方式。
而相对的,Cocoa 框架中的类,基本都被映射为了 Swift 的 class
。如果你有比较深厚的 objc 功底的话,应该会听说过 objc 的类其实是一组包含了元数据 (metadata) 的结构体,而在 objc 中我们可以使用 +class
来拿到某个 Class 的 isa,从而确定类的组成和描述。而在 Swift 的 native 层面上,在 type safe 的基础上,不再需要 isa 来指导对象如何构建,而这个过程会通过确定的命名类型完成。正因为这个原因,Swift 中干脆把 NSObject 的 class
方法都拿掉,因为 Swift 和 ObjC 在这个根本问题上的分歧,最终导致了在使用 Swift 调用 Cocoa 框架时的各种麻烦和问题。
参照和值,Array和Dictionary背后的一些故事
2014 年 7 月 13 日更新 由于 beta 3 中 Array
被完全重写,这一节关于 Array
的一些行为和表述完全过时了。 关于 Array
的用法现在简化了很多,请参见新加的 “真 参照和值,Array和Dictionary背后的一些故事”
如果你坚持看到了这里,那么恭喜你...本文最无趣和枯燥的部分已经结束了(同时也应该吓走了不少抱着玩玩看的心态来看待 Swift 的读者吧..笑),那么开始说一些细节的东西吧。
首先要明白的概念是,参照和值。在 C 系语言里摸爬滚打过的同学都知道,我们在调用一个函数的时候,往里传的参数有两种可能。一种是传递类似一个数字或者结构体这样的基本元素,这时候这个整数的值会被在内存中复制一份然后传到函数内部;另一种情况是传递一个对象,为了性能和内存上的考虑,这时候一般不会去将对象的内容复制一遍,而是会传递的一个指向同一块内存的指针。
在 Swift 中一个与其他语言都不太一样的地方是,它的 Collection 类型,也就是 Array
和Dictionary
,并不是 class
类型,而是 struct
结构体。那么按照我们以往的经验,在传值或者赋值的时候应该是会复制一份。我们来试试看是不是这样的~
var dic = [0:0, 1:0, 2:0]
var newDic = dic
dic[0] = 1
dic
newDic
var arr = [0,0,0]
var newArr = arr
arr[0] = 1
arr
newArr
Dictionary
的值没有问题,我们改变了 dic
中的值,但是 newDic
保持了原来的值,说明newDic
确实被复制了一份。而当我们检查到 Array
的时候,发生了一点神奇的事情。虽然Array
是 struct
,但是当我们改变 arr
时,新的 newArr
也发生了改变,也就是说,arr
和newArr
其实是同一个参照。这里的原因其实在 Apple 的官方文档中有一些说明。Swift 考虑到实际使用的情景,对 Array
做了特殊的处理。除非需要(比如 Array
的大小发生改变,或者显式地要求进行复制),否则 Array
在传递的时候会使用参照。
在这里如果你想要只改变 arr
的值,而保持新赋予的 newArr
不变的话,你需要显式地对 arr
进行 copy()
,像下面这样。
var arr = [0,0,0]
var copiedArr = arr.copy()
arr[0] = 1
arr
copiedArr
这时候 arr
和 copiedArr
将指向不同的内存地址,对原来的数组重新赋值的时候,就不会再影响新的数组了。另一种等效的做法是通过 Array
的初始化方法建立一个新的 Array
:
var arr = [0,0,0]
var newArr = Array(arr)
arr[0] = 1
arr
newArr
值得一提的是,对于 Array
这个 struct
的这种特殊行为,Apple 还准备了另一个函数unshare()
给我们使用。unshare()
的作用是如果对象数组不是唯一参照,则复制一份,并将作用的参照指向新的地址(这样它就变成唯一参照,不会意外改变原来的别的同样的参照了);而如果这个参照已经是唯一参照了的话,就什么都不做。
var arr = [0,0,0]
var newArr = arr
arr.unshare()
arr[0] = 1
arr
newArr
这个设计的意图是为了更安全地使用这个优化过的行为奇怪的数组结构体。关于 unshare()
的行为,我们也可以通过使用 LLDB 断点来观察内存地址的变化。参见下图:
另外一个要加以注意的是,Array
在 copy 时执行的不是深拷贝,所以 Array
中的参照类型在拷贝之后仍然会是参照。Array 中嵌套 Array 的情况亦是如此:对一个 Array 进行的 copy 只会将被拷贝的 Array
指向新的地址,而保持其中所有其他 Array
的引用。当然你可以为 Array
(或者准确说是 Array)写一个递归的深拷贝扩展,但这是另外一个故事了。
真 参照和值,Array和Dictionary背后的一些故事
2014 年 7 月 13 日更新
Apple 在 beta 3 里重写了 Array
,它的行为简化了许多。首先 copy
和 unshare
两个方法被删掉了,而类似的行为现在以更合理的方式在幕后帮我们完成了。还是举上面的那个例子:
var dic = [0:0, 1:0, 2:0]
var newDic = dic
dic[0] = 1
dic
newDic
var arr = [0,0,0]
var newArr = arr
arr[0] = 1
arr
newArr
Dictionary
当然还是 OK,但是对于 Array
中元素的改变,在 beta 3 中发生了变化。现在不再存在作为一个值类型但是却在赋值和改变时表现为参照类型的 Array
的特例,而是彻头彻尾表现出了值类型的特点。这个改变避免了原来需要小心翼翼地对 Array
进行 copy
或者 unshare
这样的操作,而 Apple 也承诺在性能上没有问题。文档中提到其实现在的行为和之前是一贯的,只不过对于数组的复制工作现在是在背后由 Apple 只在必要的时候才去做。所以可以猜测其实在背后Array
和 Dictionary
的行为并不是像其他 struct 那样简单的在栈上分配,而是类似参照那样,通过栈上指向堆上位置的指针来实现的。而对于它的复制操作,也是在相对空间较为宽裕的堆上来完成的。当然,现在还无法(或者说很难)拿到最后的汇编码,所以这只是一个猜测而已。最后如果能够证实对错的话,我会再进行更新。
总之,beta 3 之后,原来飘忽不定难以捉摸(其实真正理解之后还是很稳定的,也很适合出笔试题)的 Array
现在彻底简单化了。基本只需要记住它的行为在表面上和其他的值类型完全无异,而性能方面的考量可以交给 Apple 来做。
Array vs Slice
因为 Array
类型实在太重要了,因此不得不再多说两句。查看 Array
在 Swift 中的定义,我们可以发现其实
请发表评论