iOS 编程中的 Type System

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