类型系统简介
为什么存在类型系统
在底层的硬件和机器代码级别,程序逻辑及其操作的数据是用位来表示的。例如在汇编程序中数据的定义如下:
; 内存中的数据只是字节序列
var1 db 65 ; 一个字节: 0x41
var2 dw 0x4241 ; 两个字: 'A'(0x41), 'B'(0x42)
var3 dd 123 ; 四个字节: 0x7B 00 00 00
从上述代码中很难看出实际定义的类型是什么,例如 65 可以表示整数 65,或者表示字符 ‘A’。当对数据进行处理时,需要将 65 实际表示的含义记住。稍不留意,可能导致严重的 Bug。
而且在汇编语言编译后,代码块和数据块对于硬件来说没有区别,所以当系统将代码当成数据,或将数据当成代码时,就会出现异常,甚至导致系统崩溃。
所以我们需要在更高的层次上对数据进行抽象,赋予数据实际的意义。类型可以告诉软件在给定上下文中如何解释给定的位序列,这样对于 65,在字符类型时便可按照字符解析。除了对数据进行有效准确的解析外,类型还起到了约束有效值的范围,可以通过集合进行表达。
关于程序语言的类型,实际包含两个主要概念:
- 类型:对数据进行分类,定义数据的有效范围。
- 类型系统:对数据进行类型分配和实现类型解析(约束)。类型系统包括两种类型分配方式:1. 程序员显式指定类型,2. 类型系统根据上下文隐式推断出某个元素的类型。类型解析提供边界校验、类型转换、类型兼容性校验、泛型实例化、重载解析等功能。
类型系统的优点
类型的主要优点有:正确性、不可变性、封装、可组合性、可读性。如何理解类型带来的这些优势呢?
正确性是指代码的行为符合预期,能够产生期望的结果,不会导致运行时错误或崩溃。通过类型对代码进行严格的限制,从而确保代码具有正确的行为。
function scriptAt(s: string):number {
return s.indexOf("Script")
}
scriptAt('typeScript')
scriptAt(36)
类型系统通过 const、final、readonly 等修饰符,在编译时强制执行不可变性约束,防止对不可变数据的意外修改,这增强了程序的线程安全性和可预测性。
类型系统通过访问控制修饰符(public/private/protected/internal)实现封装,将数据和行为捆绑在类型内部,只暴露安全的接口,隐藏实现细节,从而减少模块间的耦合。
类型系统通过泛型实现了类型的组合关系,泛型的本质是将类型作为一等公民,允许编写与类型无关的通用代码、通过参数化创建复杂的类型表达式、一次编写,类型可以多处使用,在编译时计算和验证类型关系。
类型系统通过类型签名可以实现代码文档化,不必单独撰写相关文档,例如 JSDoc 就可以根据类型注释生成代码文档。
类型系统可以消除魔法数字,在 TypeScript 中 const Normal = 1,Normal 的类型是字面量类型 1,而不是普通的 number 类型。
类型让我们更容易看出复杂的数据结构和控制流:
type authenticate = (user: User, password: string) => Result<Session, AuthError>
类型驱动的思维过程
在代码编写阶段,可以先想类型:这个函数应该接受什么?返回什么?再想实现:如何实现这个类型签名?类型约束会提示可能的实现方式。
类型系统的类型
类型系统分为两种:1. 静态类型系统,在编译期间进行类型校验,能更早地发现错误。2. 动态类型系统,将类型校验推迟到运行时,当类型不匹配时会出现运行时错误。
类型系统有强弱之分,JavaScript 是弱类型系统,提供了大量的隐式类型转换,而强类型系统则要求显式类型转换。
function add(a, b) {
return a + b
}
add(1, '1') // JavaScript 会自动将数字 1 转换为字符串 '1',结果为 '11'
function add(a:number, b:number): number {
return a + b
}
add(1, Number('1'))
静态类型和动态类型都有类型推断,静态类型中编译器可以推断出某个变量或函数的类型。例如当给一个变量赋值 36,TypeScript 可以推断出变量类型为 number。
在动态类型系统中,add(1, '1') JavaScript 解析器会在运行时自动进行类型推断和转换。
总结
类型系统是对低层次数据的分类和约束,它从底层硬件抽象而来,为数据赋予了明确的语义。类型系统包含两个核心概念:类型负责对数据进行分类并定义有效范围,类型系统则负责类型分配和类型解析,既支持程序员显式指定类型,也支持根据上下文隐式推断类型。
类型系统带来了诸多优势。通过类型约束可以在编译时发现错误,避免运行时崩溃,提高代码的正确性。通过 const、final、readonly 等修饰符可以保证数据不被意外修改,增强程序安全性。通过访问控制修饰符可以实现封装,减少模块间的耦合。通过泛型可以实现类型的组合和复用,编写通用代码。类型签名本身就可以作为文档,提高代码的可读性和维护性。
类型系统有静态和动态之分,静态类型在编译时校验,能更早地发现错误,而动态类型将校验推迟到运行时。类型系统还有强弱之分,强类型要求显式转换,弱类型允许隐式转换。无论是静态还是动态类型系统,都具备类型推断能力,只是工作时机和方式不同。
在编写代码时,可以先思考类型签名,想清楚函数应该接受什么、返回什么,再思考具体实现。这种类型驱动的思维方式能够引导我们写出更健壮的代码。而且类型系统通过类型推导机制,在保证类型安全的同时,减少了显式类型注解的负担,让程序员能够专注于业务逻辑的实现。