接之前一篇 Pattern Matching 的文章,Type System 是另一项编程语言,或者说编译器所提供的便利。Pattern Matching 可以让我们少写代码,而 Type System 可以让我们少犯错误,减少 Type 相关的各种 bug。
一般来说,我们写代码时为了降低 bug 率,一是依赖于程序员自身的经验积累,二是靠编译器做各种静态检查,type system 则是属于静态检查这一类。Swift 较之 Objective C 的 type system 有了很大的改进,下面文章中主要是介绍 Swift 相关的一些特性。在开始之前,先聊下如何靠程序员经验来降低 bug。
Bug 第六感
从我自身的体验推断,我相信大部分程序员在写代码的时候,对于代码是否存在 bug 是有一定感知的。只不过有些新入行的朋友,在写代码的时候操之过急,或者由于和产品经理讨论吃了败仗心情不佳,coding 时目标变成了写能 work 的代码,而不是写高质量的代码。
facebook 面试有一个环节叫 whiteboard coding,要求程序员能在白板上写出几乎是 「bug free」的代码,这听起来有点耸人听闻,写代码没有 Xcode 提示就罢了,bug free 更是难上加难了。写一段几乎没有 bug 的代码到底有多难呢?说难不难,说易不易。
写代码时,慢一点,再慢一点。好好的想下代码有可能出错的地方在哪,想清楚了 bug 就少。一般来说,要减少 bug 量,一是靠程序员自身修养,二是靠编译器提供的静态检查。Type System 属于第二类,在深入之前,先简单聊下如何靠自身修养降低 bug 率,提升 bug 感知的第六感。
少写 Bug 的简易准则
要提升程序员的自身修养来降低 bug 率,是个大话题,而且多和自身的知识积累有关,需要长年累月的学习和养成。本文的目的不在于此,所以只介绍一个小技巧来养成感知 bug 的好习惯。
我们可以粗略的将我们所写代码分为 data 和 behavior,behavior 围绕 data 执行各种逻辑。一个函数可以看做是一个 behavior,而函数本身又由若干 data 和 behavior 所构成。很多时候,代码有 bug,是因为 data 出现了预料之外的变化。有一个简易准则可以减少这类 bug:只要遇到 data,就做 aggressive check。
具体到一个自定义的函数,函数会包含哪些 data 呢?细心理一理没几个。
函数入参
内部临时变量
依赖的外部变量
返回的最终结果
这几类 data 是我们在一个函数中最经常遇到的,只要我们对他们做好检查就可保平安。做哪些检查呢?最常见的也就那么几样,比如是否为 0,为 nil,数组元素 count 为 0,如果期待正数则是否为负数,数组是否越界,多线程是否安全等。做下总结就可以完成大部分的可靠性检查。简而言之,只要是使用 data 的时候,就围绕 data 做好应该的检查,做到这点,写一个几乎没有 bug 的函数就不怎么难了。
这个原则更精准的表达是:在任何场景下,无论是定义变量还是使用变量,都对变量的各种可能性做检查和保护。
Type System
回到我们的正题 Type System,Type System 是由编程语言和 type 相关的各种规则所构成。它的用处也简单,可以帮助我们减少和 type 相关的 bug。
编程语言大多都有自己的 Type System,Objective C 和 Swift 都有。在开始讨论 Type System 之前,要明确 Type 的定义。
Type 就像自然语言里的名词,动词,介词等等,可以规范我们的表达。在编程语言中,type 则是一种避免代码表达错误的约束。Type 不仅仅包括诸如 int,float,bool 这类 primitive type,对象的 class type,还包括 function,block 等不那么明显的 type。变量,常量,函数等等都(且一定)具备 type 信息,有些一眼能看出,有些要靠推断。
Static vs Dynamic
有些 type 信息是交由程序员去推断和维护的,有些则是留给编译器去管理的。前者的 type 约束是在 runtime 检查的,偏向「dynamic」,后者则在 compile 的时候就做了 check,偏向「static」。
很多技术文章都会讨论编程语言的 dynamic 和 static 属性,我们要分清楚 dynamic 和 static 其实是个宽泛的说法,他们可能包含不同的语义和场景。dynamic 和 static 既可以用来讨论 type system,又可以用来形容函数调用机制。比如我们认为 Swift 是 statically typed,但 Objective C 的 runtime 和 message 机制又显然是 dynamic 的,这两种场景下 static 和 dynamic 说的其实不是一回事。
回到 type system 的场景,讨论下语言是 statically typed 还是 dynamically typed。还是要进一步看场景,在 Objective C 中,type 信息既可以是 static 的,也可以是 dynamic 的,看我们如何使用了,比如下面的代码中 type 信息是 static 的:
因为 type 的上下文信息是完整的,编译器可以做类型判断。而如下代码中 type 信息则是 dynamic 的:
由于 id 可以指向任意对象类型,id 可以在不同的时间点里指向不同的类型,编译器此时无法根据类型信息作出判断,是否存在类型使用错误的。所以我们会说像 Objective C 这类编程语言在 type system 上,是同时具备 static 和 dynamic 属性的,关键还是看具体的使用场景。
但 Swift 却是货真价实的,纯粹的 statically typed 编程语言,不具备任何 dynamically typed 的属性。比如在 Swift 中,如下代码是无法通过编译的:
编译器会提示:Type annotation missing in pattern,也就是缺少 type 信息。要声明一个变量,我们可以通过如下两种方式来提供 type 信息:
方式一是通过赋值来做 type inference,方式二是通过显式的提供 type 信息。Swift 在 type 的使用上非常苛刻,当之无愧为 statically typed。
显然,static type 比 dynamic type 更安全,编译器可以帮我们做类型检查,这也是为什么 Swift 比 Objective C 在 type safety 上更优秀的原因。当然,dynamic type 并非全无好处,初期开发起来速度会快于 static type,而且省去了编译时的 type 检查,每次编译速度更快。缺点是一旦出现 runtime 中的类型错误,要花更多的时间去调试,要写更多的 test case,准备更多的文档。这种缺陷在较大规模的项目上会更明显,Swift 选择 static type 策略应该也有这方面的考虑。
Type Inference
类型推断(type inference)也是 type system 当中的一个常见概念。不少编程语言比如 Swift 都有 type inference 的功能。type inference 有什么用处呢?statically typed 的编程语言决定了变量都必须具备类型信息,意味着我们每次使用变量的时候都需要显式的声明 type 信息,比如在 Objective C中,这样会显得有些繁琐和啰嗦,一旦有了 type inference,我们可以在代码中省略掉很多关于 type 累赘的表述。我们看如下代码:
var i = 0
这行代码中,有两个实体有 type 信息,变量 i 和常量 0,0 默认的 type 信息是 int,i 的 type 信息没有显示的声明出来,但在 Swift 中,由于 0 被赋值给了 i,所以可以通过 type inference 推断出 i 的 type 信息也是 int。这种类型推断会发生在很多程序员意识不到的角落,这种具备传染特性的 type 信息可以层层叠叠,一级一级的输送到更多的其他变量实体。编译器就是通过这种传染的特性来做 type inference 的。
在 Swift 中,type inference 配合 static type 让代码既精炼又安全。
Optional Type
前面提到 type 信息本质上是一种约束,可以避免 type 的使用错误。我们在编写代码时,经常遇到的一种 bug 是对于空对象或者说对象为 nil 情况,漏写了为空的判断。Swift 通过引入 optional type 来强制开发者考虑 nil 的场景,更妙的是,当「是否为 nil 」成为 type 信息之后,编译器也可以一起来帮助检查 nil 的使用场景。看下 optional type 的定义就一清二楚了:
通过 enum type 来定义 optional type,以表达是否为 nil 的含义。这也是 Swift 为什么要引入 optional type 的根本原因,让编译器以类型检查的方式,来帮助开发者分析是否存在漏判 nil 的场景。
Generic Type
再次强调下,type 本质上是一种约束。当我们定义 int i 时,int 就成为了变量 i 的一种约束。我们可以把这种约束进一步强化,比如引入 generic type(泛型)。generic type 有两个主要特性,其一是允许开发者在后期再指定 type 的值,其二是可以把 type 约束施加到指定的代码范围里。理解这两个特性,是我们掌握 generic type 各种表现形式的基础。
generic type 可以让我们写出更加符合 type safety 的代码,Objective C 和 Swift 都支持定义 generic type,只不过 Swift 中 generic 的概念更加广泛,应用面也大很多,在一些显式的和隐式的地方都存在 generic 的身影。比如前面提到的 optional,其实也是个 generic type。
generic 可以作用于很多其他的复杂 type,比如 optional 就是 generic 作用于 enum 的结果,除了 enum 之外,还有 struct,class,function 等都可以和 generic 搭配使用。我们再看一个 Swift 自带的例子,Array:
只需要在 struct 名字后面以 的形式,就可以在 struct 的作用域内部声明一个新的 xxx type(xxx 在 Array 的 extension 中也是可见的),xxx 可以在使用时再确定具体指代什么 type。使用 Array 的时候,我们也不必显示的指明 xxx 代表什么,可以依赖前面提到的 type inference:
上面第二行会报错,这是 Swift 和 Objective C 的差异之处,在 Objective C 中,我们可以在 Array 中放入不同类型的对象,而在 Swift 中,一旦 Array 中的元素类型被 type inference 确定,就不能放入其他类型的对象了。generic type 和 type inference 配合的场景在 Swift 当中经常出现。
Named Type vs Compound Type
named type 指的是我们传统意义上所理解的 data type,例如 int,float,string,自定义的 class 等等。在 Swift 中,设计者引入了 compound type 的概念,可以把 compound type 理解成 named type 的某种集合,比如 function 和 tuple,他们往往都包含多个 named type。
我们知道在 Swift 中,function 是一等公民,可以作为变量声明,参数,返回值等等,要理解并运用这一点,需要在思维上做转换,把 function 也看做一种 data type(compound type),在原先使用 named type 的位置,我们几乎都可以使用 compound type。
compound type 增强了语言的表达力,但其灵活性在一些场景下,也会一定程度的降低代码的可阅读性。compound type 可以包其他 compound type,可以一层层的套嵌,这种 nested compound type 有时候会让代码看上去没那么直观,比如下面一段 Swift 代码:
上面的函数里,function 和 tuple 作为 compound type 存在套嵌,代码本身虽然不长,要一眼把其中包含的 type 都识别出来不那么容易。compound type 的使用可能是不少从 Objective C 转向 Swift 的同学初期感觉难以适应的原因之一。
总结
上述所提到的概念都是和 type system 相关的基础知识,虽然基础,却十分重要。对 type system 建立完整全面的认识,多利用语言本身的 type 制约来避免 bug,可以让我们对自己代码的安全性有更好的把握,对于代码质量的提升也有极大的帮助。