本节内容想为你建立一个完善的类型认知,不知道能不能达到效果。如果有不能理解的地方可以跳过,等学完全部内容后再看这一节。看大家的反馈吧,实在难以读懂,我就调整一下顺序。
一门编程语言在设计之初,需要考虑众多因素,其中设计类型系统
就是非常重要的一环。
类型系统发展
早期的编程语言,例如 C 语言,使用简单的数据类型,例如整数、浮点数和字符。
随着编程语言的发展,出现了面向对象的编程语言,例如 C++ 和 Java。面向对象的编程语言引入了类和接口等抽象类型,可以描述更复杂的概念。
近年来,出现了函数式编程语言,例如 Haskell 和 Scala。函数式编程语言强调类型系统的表达力,并引入了类型推断等技术。
如何设计类型系统
类型系统主要解决下面两个问题:
语言的类型有哪些?
类似一次能源、二次能源,我们把类型分为一次类型和二次类型。一次类型就是语言内置类型,二次类型是基于一次类型创建的,包括标准库或用户自定义的类型。此外还有一种是抽象类型,仅用于描述类型的行为,如特征和泛型。如何检查类型错误?
Rust 类型系统通过编译时检查和运行时检查来确保类型安全。
Rust 编译器会在编译时进行类型检查,以确保以下内容:
- 变量的类型声明与赋值表达式匹配。
- 函数参数的类型与调用表达式匹配。
- 返回值的类型与函数声明匹配。
- 所有权和借用规则得到遵守。
例如,以下代码会导致编译错误:
let x: i32 = 1.0; // 无法将浮点数赋值给 i32 变量
Rust 运行时也会进行一些类型检查,以确保以下内容:
- 空指针访问不会发生。
- 数组越界访问不会发生。
- 数据竞争不会发生。
例如,以下代码会导致运行时错误:
let mut x = [1, 2, 3];
let y = x[4]; // 数组越界访问
Rust 中类型错误的常见原因:
- 变量类型声明错误。
- 函数参数类型声明错误。
- 返回值类型声明错误。
- 所有权和借用规则使用错误。
- 类型转换错误。
类型系统的本质
类型系统是编程语言中用于定义和控制类型的一组规则。包括定义类型、类型之间的交互、类型检查和类型推断这四部分。
1. 类型定义
类型是用来描述值的性质和行为的抽象概念。
- 一次类型:整数、浮点数、字符、bool、指针
- 二次类型:数组、切片、元组、结构体、枚举、函数、闭包
- 抽象类型:trait、泛型
2. 类型规则
类型规则定义了类型之间如何相互作用。例如,以下类型规则定义了赋值操作:
赋值操作的左值和右值的类型必须一致。
3. 类型检查
类型检查是用来确保程序遵循类型规则的过程。类型检查可以在编译时或运行时进行。
所有权、借用、生命周期就是编译器进行类型检查的规则。我们在上节内存释放时见到过,这些重点我们会在学完类型系统以后介绍它们。
4. 类型推断
类型推断是一种自动推断变量类型的技术。类型推断可以使代码更加简洁易读。
我们来看一些类型推断的例子:
/// 1. **变量声明**
let x = 1; // 编译器推断 x 的类型为 i32
/// 2. **函数参数**
fn add_one(x: i32) -> i32 {
x + 1
}
let result = add_one(1); // 编译器推断 result 的类型为 i32
/// 3. **函数返回值**
fn get_string() -> String {
"hello".to_string()
}
let s = get_string(); // 编译器推断 s 的类型为 String
/// 4. **运算符**
let x = 1 + 2; // 编译器推断 x 的类型为 i32
/// 5. **模式匹配**
let x = Some(1);
match x {
Some(n) => println!("{}", n),
None => println!("None"),
}
总结
一块内存就像陶泥,没有经过塑形它可以是任何形状。如果把它做成一个杯子,用它装水,装油,甚至还可以装泥巴,装大便,但是你不能用杯子切菜。
对于一块内存在没有类型约束时,你可以对他做任何操作,但通常没有任何意义。为内存赋予一个类型,例如i32类型,一旦赋予了类型那么能够对这块内存施加的操作也就定下来了,i32类型允许加减等操作,但你要对这块内存做字符串拆分操作是不允许的,编译器会报错。经过编译步骤,对内存类型检查后,程序程序运行时对这块内存的操作也就固定了,CPU才不管这四个字节是什么类型,只是按照指令操作这块内存。
学完这一章,你一定会直呼高级语言是管理内存的艺术,那我们赶快进入下一节看看具体的类型。