行走于 Swift 的世界中

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 世界中的所有基本型IntStringArrayDictionay 等等,其实它们都是结构体。而这些基本类型通过定义本身,以及众多的 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 背后的一些故事

如果你坚持看到了这里,那么恭喜你…本文最无趣和枯燥的部分已经结束了(同时也应该吓走了不少抱着玩玩看的心态来看待 Swift 的读者吧..笑),那么开始说一些细节的东西吧。

首先要明白的概念是,参照和值。在 C 系语言里摸爬滚打过的同学都知道,我们在调用一个函数的时候,往里传的参数有两种可能。一种是传递类似一个数字或者结构体这样的基本元素,这时候这个整数的值会被在内存中复制一份然后传到函数内部;另一种情况是传递一个对象,为了性能和内存上的考虑,这时候一般不会去将对象的内容复制一遍,而是会传递的一个指向同一块内存的指针。

在 Swift 中一个与其他语言都不太一样的地方是,它的 Collection 类型,也就是 Array 和 Dictionary,并不是 class 类型,而是 struct 结构体。那么按照我们以往的经验,在传值或者赋值的时候应该是会复制一份。我们来试试看是不是这样的~

Dictionary 的值没有问题,我们改变了 dic 中的值,但是 newDic 保持了原来的值,说明 newDic 确实被复制了一份。而当我们检查到Array 的时候,发生了一点神奇的事情。虽然 Array 是 struct,但是当我们改变 arr 时,新的 newArr 也发生了改变,也就是说,arr 和newArr 其实是同一个参照。这里的原因其实在 Apple 的官方文档中有一些说明。Swift 考虑到实际使用的情景,对 Array 做了特殊的处理。除非需要(比如 Array 的大小发生改变,或者显式地要求进行复制),否则 Array 在传递的时候会使用参照。

在这里如果你想要只改变 arr 的值,而保持新赋予的 newArr 不变的话,你需要显式地对 arr 进行 copy(),像下面这样。

这时候 arr 和 copiedArr 将指向不同的内存地址,对原来的数组重新赋值的时候,就不会再影响新的数组了。另一种等效的做法是通过 Array的初始化方法建立一个新的 Array

值得一提的是,对于 Array 这个 struct 的这种特殊行为,Apple 还准备了另一个函数 unshare() 给我们使用。unshare() 的作用是如果对象数组不是唯一参照,则复制一份,并将作用的参照指向新的地址(这样它就变成唯一参照,不会意外改变原来的别的同样的参照了);而如果这个参照已经是唯一参照了的话,就什么都不做。

这个设计的意图是为了更安全地使用这个优化过的行为奇怪的数组结构体。关于 unshare() 的行为,我们也可以通过使用 LLDB 断点来观察内存地址的变化。参见下图:swift_unshare_array

另外一个要加以注意的是,Array 在 copy 时执行的不是深拷贝,所以 Array 中的参照类型在拷贝之后仍然会是参照。Array 中嵌套 Array 的情况亦是如此:对一个 Array 进行的 copy 只会将被拷贝的 Array 指向新的地址,而保持其中所有其他 Array 的引用。当然你可以为 Array (或者准确说是 Array)写一个递归的深拷贝扩展,但这是另外一个故事了。

Array vs Slice

因为 Array 类型实在太重要了,因此不得不再多说两句。查看 Array 在 Swift 中的定义,我们可以发现其实 Array 实现了两个很重要的接口MutableCollection 和 Sliceable。第一个接口比较简单,为 Array 实现了下标等特性,通过 Collection 通用的一些概念,可以从数据结构中获取元素,比较简单。而第二个接口 Sliceable 实现了通过 Range 来取出部分数组,这里稍微有点特殊。

Swift 引入了在其他很多语言中很流行的用 .. 和 ... 来表示 Range 的概念。从一个数组里面取出一个子数组其实是蛮普遍的一个需求,但是如果你足够细心的话,可能会发现我们无法写这样的代码:

你会得到一个编译错误,告诉你没有重载下标。在我们去掉我们强制加上的 : Array 类型设置之后,编译能通过了。这就告诉我们,我们使用 Rang 从 Array 中取出来的东西,并不是 Array 类型。那它到底是个什么东西?使用 REPL 可以很容易看到,在使用 Range 从 Array 里取出来的其实是一个 Slice,而不是一个 Array

So, what is a slice?查看 Slice 的定义,可以看到它几乎和 Array 一模一样,实现了同样的接口,拥有同样的成员,那么为什么不直接干脆给个爽快,而要新弄一个 Slice 呢?Apple gets crazy?当然不是..Slice的存在当然有其自己的价值和含义,而这和我们刚才提到的值和引用有一些关系。

So, why is a slice?让我们先尝试 play with it。接着上面的情况,运行下面的代码试试看:

我想你已经明白一些什么了吧?这里的 slice 和 arr 当然不可能是同一个引用(它们的类型都不一样),但是很有趣的是,通过 Range 拿到的Slice 中的元素,是指向原来的 Array 的。这个特性就非常有趣了,我们可以对感兴趣的数组片段进行观察或者操作,并且它们的值和原来的数组是对应的同步的。

理所当然的,在对应着的 Array 或者 Slice 其中任意一个的内存指向发生变化时(比如添加或移除了元素,重新赋值等等),这种关系就会被打破。

对于 Slice 和 Array,其实是可以比较简单地转换的。因为 Collection 接口是实现了 + 重载的,于是我们可以简单地通过相加来生成一个Array (如果我们愿意的话)。不过,要是真的有需要的话,使用 Array 的初始化方法会是比较好的选择:

使用 Range 下标的方式,不仅可以取到这个 Range 内的 Slice,还可以对原来的数组进行批量”赋值”:

细心的同学可能注意到了,这里我把“赋值”打上了双引号。实际上这里做的是替换,数组的内存已经发生了变化。因为 Swift 没有强制要求替换的时候 Range 的范围要和用来替换的 Collection 的元素个数一致,所以其实这里一定会涉及内存的分配和新的数组生成。我们可以看看下面的例子:

给一个数组进行 Range 赋值,背后其实调用了数组的 replaceRange 方法,将取到的 Slice,替换成了赋给它的 Array 或者 Slice。而只要 Range 有效,我们就可以很灵活地写出类似这样的所谓的插入方法:

这里的 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 中取值时这种表达是没有意义,并且会抛出一个运行时的 EXC_BAD_INSTRUCTION 的,在使用的时候还要加以注意。

颜文字很好,但是…

有了上面的一些基础,我们可以来谈谈 String 了。当说到我们可以在原生的 String 中使用 UniCode 字符时,全场一片欢呼。没错,以后我们可以把代码写成这样了!

收藏 评论

相关文章

可能感兴趣的话题



直接登录
跳到底部
返回顶部