//转载:http://onevcat.com/2014/06/walk-in-swift/
从周一 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
//Check dic and newDic
dic[0] = 1
dic //[0: 1, 1: 0, 2: 0]
newDic //[0: 0, 1: 0, 2: 0]
var arr = [0,0,0]
var newArr = arr
arr[0] = 1
//Check arr and newArr
arr //[1, 0, 0]
newArr //[1, 0, 0]
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 //[1, 0, 0]
copiedArr //[0, 0, 0]
这时候 arr
和 copiedArr
将指向不同的内存地址,对原来的数组重新赋值的时候,就不会再影响新的数组了。另一种等效的做法是通过 Array
的初始化方法建立一个新的 Array
:
var arr = [0,0,0]
var newArr = Array(arr)
arr[0] = 1
arr //[1, 0, 0]
newArr //[0, 0, 0]
值得一提的是,对于 Array
这个 struct
的这种特殊行为,Apple 还准备了另一个函数 unshare()
给我们使用。unshare()
的作用是如果对象数组不是唯一参照,则复制一份,并将作用的参照指向新的地址(这样它就变成唯一参照,不会意外改变原来的别的同样的参照了);而如果这个参照已经是唯一参照了的话,就什么都不做。
var arr = [0,0,0]
var newArr = arr
//Breakpoint 1
arr.unshare()
//Breakpoint 2
arr[0] = 1
arr //[1, 0, 0]
newArr //[0, 0, 0]
这个设计的意图是为了更安全地使用这个优化过的行为奇怪的数组结构体。关于 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
//Check dic and newDic
dic[0] = 1
dic //[0: 1, 1: 0, 2: 0]
newDic //[0: 0, 1: 0, 2: 0]
var arr = [0,0,0]
var newArr = arr
arr[0] = 1
//Check arr and newArr
arr //[1, 0, 0]
newArr //before beta3:[1, 0, 0], after beta3:[0, 0, 0]
Dictionary
当然还是 OK,但是对于 Array
中元素的改变,在 beta 3 中发生了变化。现在不再存在作为一个值类型但是却在赋值和改变时表现为参照类型的 Array
的特例,而是彻头彻尾表现出了值类型的特点。这个改变避免了原来需要小心翼翼地对 Array
进行 copy
或者 unshare
这样的操作,而 Apple 也承诺在性能上没有问题。文档中提到其实现在的行为和之前是一贯的,只不过对于数组的复制工作现在是在背后由 Apple 只在必要的时候才去做。所以可以猜测其实在背后 Array
和 Dictionary
的行为并不是像其他 struct 那样简单的在栈上分配,而是类似参照那样,通过栈上指向堆上位置的指针来实现的。而对于它的复制操作,也是在相对空间较为宽裕的堆上来完成的。当然,现在还无法(或者说很难)拿到最后的汇编码,所以这只是一个猜测而已。最后如果能够证实对错的话,我会再进行更新。
总之,beta 3 之后,原来飘忽不定难以捉摸(其实真正理解之后还是很稳定的,也很适合出笔试题)的Array
现在彻底简单化了。基本只需要记住它的行为在表面上和其他的值类型完全无异,而性能方面的考量可以交给 Apple 来做。
Array vs Slice
因为 Array
类型实在太重要了,因此不得不再多说两句。查看 Array
在 Swift 中的定义,我们可以发现其实 Array
实现了两个很重要的接口 MutableCollection
和 Sliceable
。第一个接口比较简单,为Array
实现了下标等特性,通过 Collection
通用的一些概念,可以从数据结构中获取元素,比较简单。而第二个接口 Sliceable
实现了通过 Range 来取出部分数组,这里稍微有点特殊。
Swift 引入了在其他很多语言中很流行的用 ..
和 ...
(beta3 中 ..
被改成了 ..<
,虽说是为了更明确的意义,但是看起来会比较奇怪)来表示 Range 的概念。从一个数组里面取出一个子数组其实是蛮普遍的一个需求,但是如果你足够细心的话,可能会发现我们无法写这样的代码:
var arr = [0,0,0]
var partOfArr: Array = arr[0...1]
//Could not find an overload for 'subscript' that accepts the supplied arguments
你会得到一个编译错误,告诉你没有重载下标。在我们去掉我们强制加上的 : Array
类型设置之后,编译能通过了。这就告诉我们,我们使用 Rang 从 Array 中取出来的东西,并不是 Array
类型。那它到底是个什么东西?使用 REPL 可以很容易看到,在使用 Range 从 Array 里取出来的其实是一个 Slice
,而不是一个 Array
。
1> var arr = [0,0,0]
arr: Int[] = size=3 {
[0] = 0
[1] = 0
[2] = 0
}
2> var slice = arr[0...1]
slice: Slice<Int> = size=2 {
[0] = 0
[1] = 0
}
So, what is a slice?查看 Slice
的定义,可以看到它几乎和 Array
一模一样,实现了同样的接口,拥有同样的成员,那么为什么不直接干脆给个爽快,而要新弄一个 Slice
呢?Apple gets crazy?当然不是..Slice的存在当然有其自己的价值和含义,而这和我们刚才提到的值和引用有一些关系。
So, why is a slice?让我们先尝试 play with it。接着上面的情况,运行下面的代码试试看:
var arr : Array = [0,0,0]
var slice = arr[0...1]
arr[0] = 1
arr //[1, 0, 0]
slice //[1, 0]
slice[1] = 2
arr //[1, 2, 0]
slice //[1, 2]
我想你已经明白一些什么了吧?这里的 slice
和 arr
当然不可能是同一个引用(它们的类型都不一样),但是很有趣的是,通过 Range 拿到的 Slice
中的元素,是指向原来的 Array
的。这个特性就非常有趣了,我们可以对感兴趣的数组片段进行观察或者操作,并且它们的值和原来的数组是对应的同步的。
理所当然的,在对应着的 Array
或者 Slice
其中任意一个的内存指向发生变化时(比如添加或移除了元素,重新赋值等等),这种关系就会被打破。
对于 Slice
和 Array
,其实是可以比较简单地转换的。因为 Collection
接口是实现了 +
重载的,于是我们可以简单地通过相加来生成一个 Array
(如果我们愿意的话)。不过,要是真的有需要的话,使用Array
的初始化方法会是比较好的选择:
var arr : Array = [0,0,0]
var slice = arr[0...1]
var result1 : Array = [] + slice
var result2 : Array = Array(slice)
使用 Range 下标的方式,不仅可以取到这个 Range 内的 Slice
,还可以对原来的数组进行批量"赋值":
var arr : Array = [0,0,0]
arr[0...1] = [1,1]
arr //[1, 1, 0]
细心的同学可能注意到了,这里我把“赋值”打上了双引号。实际上这里做的是替换,数组的内存已经发生了变化。因为 Swift 没有强制要求替换的时候 Range 的范围要和用来替换的 Collection
的元素个数一致,所以其实这里一定会涉及内存的分配和新的数组生成。我们可以看看下面的例子:
var arr : Array = [0,0,0]
var otherArr = arr
arr[0...1] = [1,1]
arr //[1, 1, 0]
otherArr //[0, 0, 0]
arr[0..1] = [1,1]
arr //[1, 1, 1, 0]
给一个数组进行 Range 赋值,背后其实调用了数组的 replaceRange
方法,将取到的 Slice
,替换成了赋给它的 Array
或者 Slice
。而只要 Range 有效,我们就可以很灵活地写出类似这样的所谓的插入方法:
var arr : Array = [0,0,0]
arr[1..1] = [1, 1]
arr //[0, 1, 1, 0, 0]
这里的 1..1
是一个起点为 1,长度为 0 的Range,于是它取到的是原来 [0, 0, 0]
中 index 为 1 的位置的一个空 Slice
,将其替换为 [1, 1]
。清楚明白。
既然都提到了这么多次 Range
,还是需要说明一下这个 Swift 里很重要的概念(其实在 objc 里 NSRange
也很重要,只不过没有像 Swift 里这么普遍)。Range
结构体中有两个非常重要的值,startIndex
和endIndex
,它表示了这个 Range 的范围。而这个值永远是右开的,也就是说,它们会和 x..y
这样的表示中 x
和 y
分别相等。对于 x < y
的情况下的 Range,是存在数学上的表达意义的,比如 2..1
这样的 Range 表示从 2 开始往前数 1。但是在实际从 Array
或者 Slice
中取值时这种表达是没有意义,并且会抛出一个运行时的 EXCBADINSTRUCTION 的,在使用的时候还要加以注意。
颜文字很好,但是...
有了上面的一些基础,我们可以来谈谈 String
了。当说到我们可以在原生的 String
中使用 UniCode 字符时,全场一片欢呼。没错,以后我们可以把代码写成这样了!
let π = 3.14159
let 你好 = "你好世界"
let