类型与 let
在前五章中,你一直把 Auto 当作脚本语言来使用——用 var 声明变量,依赖类型推断,用匿名对象来组织数据。这种方式让你快速上手,也是 Auto 的优势之一。但 Auto 还有更深层的一面。
本章是第二阶段(Phase 2)的第一章。我们将"成长"起来。你将学习为什么不可变性很重要,如何为变量添加显式类型注解,如何用 type 定义自定义数据类型,如何用 ext 附加方法,以及如何使用元组和类型别名。这些工具能让你的程序不仅正确,而且清晰、安全、可维护。
在本章中,我们将讨论:
let—— 不可变绑定及其重要性- 类型注解 —— 为变量添加显式类型
- 自定义类型 —— 用
type定义结构化数据 - 构造函数与字段访问 —— 创建实例并读取字段
- 用
ext定义方法 —— 为类型附加行为,&self - 元组 —— 固定大小的异构集合
- 类型别名 —— 为类型起有意义的名称
let —— 默认不可变
在第一阶段,所有变量都用 var 声明。var 关键字创建的是_可变_绑定——你可以多次重新赋值。这对计数器和累加器来说没问题,但大多数时候你并不真的需要改变一个值。
Auto 提供了 let 用于创建不可变绑定。一旦你给 let 绑定赋了值,就不能再重新赋值。这不是限制——而是安全特性。当你读到 let name = "Alice" 时,你可以确定 name 在其作用域内始终指向 "Alice"。没有任何函数能在你不知情的情况下改变它。
输出:
6
10注意被注释掉的 // y = 11 这一行。如果你取消注释,Auto 编译器会拒绝编译你的程序。这是编译时错误,不是运行时错误。编译器在你的程序运行之前就捕获了这个错误。
看看每种语言如何表达不可变性:
- Rust 用
let表示不可变绑定,用let mut表示可变绑定。Auto 的let对应 Rust 的let,Auto 的var对应 Rust 的let mut。 - Python 没有内置的变量不可变机制。转译器将
let转换为普通赋值,并添加注释说明语义差异。 - C 用
const表示不可变变量。Auto 的let对应const,Auto 的var对应普通声明。 - TypeScript 用
const表示不可变绑定,用let表示可变绑定。这与 Rust 的命名相反,但语义相同。
对照表如下:
| Auto | Rust | Python | C | TypeScript |
|---|---|---|---|---|
var x = 5 | let mut x = 5 | x = 5 | long long x = 5 | let x = 5 |
let y = 10 | let y = 10 | y = 10 | const long long y = 10 | const y = 10 |
今后,只要值不需要改变,就使用 let。只在确实需要重新赋值时才使用 var。这不仅仅是风格偏好——它让代码更容易推理,也帮助编译器生成更好的代码。
类型注解
Auto 可以推断大多数类型。当你写 var x = 5 时,编译器知道 x 是 int。当你写 let name = "Alice" 时,它知道 name 是 String。但有时你想要显式标注——为了记录意图,为了尽早发现错误,或者为了满足目标语言的要求。
Auto 允许在变量名之后添加类型注解:
var count int = 0
let name String = "Alice"语法是 名称 类型——变量名在前,类型在后,用空格分隔。这与 type 定义和函数参数中的空格分隔语法一致。
输出:
0
Alice
3.14159
trueAuto 的基本类型到各目标语言的映射如下:
| Auto | Rust | Python | C | TypeScript |
|---|---|---|---|---|
int | i64 | int | long long | number |
float | f64 | float | double | number |
String | String | str | const char* | string |
bool | bool | bool | bool | boolean |
几点说明:
- Auto 使用大写的
String表示堆分配的文本字符串,与 Rust 的约定一致。这与str不同——在 Rust 中str指的是借用的字符串切片。 int和float在 TypeScript 中都映射为number,因为 JavaScript 在语言层面不区分整数和浮点数。- C 字符串是
const char*——指向以 null 结尾的字符的指针。C 转译器使用printf的%s格式化字符串输出。
什么时候应该加类型注解?一个经验法则:当类型从值中看不出来时。var count int = 0 很有帮助,因为单独的 0 可能是 int 也可能是 float。但 let name = "Alice" 已经足够清晰——类型显然是字符串。Auto 从不_要求_注解,但始终_接受_注解。
自定义类型
在第一阶段,你用匿名对象来组织相关数据:{ name: "Alice", age: 30 }。这在快速脚本中很好用,但有局限性。你无法复用这个形状——每个 { name, age } 对象都是独立的。你无法为它附加方法。编译器也无法在编译时帮你发现字段名拼写错误。
type 关键字解决了所有这些问题。它让你定义一个_命名_类型——一个描述值有哪些字段以及字段类型的蓝图:
type User {
name String
age int
active bool
}这定义了一个名为 User 的类型,有三个字段:name(String 类型)、age(int 类型)和 active(bool 类型)。注意语法:每个字段是 名称 类型,用空格分隔。没有逗号,没有冒号——只有字段名和它的类型。
输出:
Alice
30每个目标语言将 type 映射到其最接近的等价物:
- Rust 使用
struct。字段语法从name Type(Auto)变为name: Type(Rust)。字符串字面量需要用String::from()包装。 - Python 使用
@dataclass类。字段成为构造函数的参数,使用关键字参数语法。 - C 使用
typedef struct,字符串用固定大小的字符数组。字段赋值在声明后逐个进行,字符串使用strcpy。 - TypeScript 使用带构造函数的
class。Auto 的命名字段构造函数语法映射到构造函数参数。
命名字段构造函数语法 User { name: "Alice", age: 30 } 在 Auto、Rust 和 TypeScript 中通用。Python 使用关键字参数(User(name="Alice", age=30)),C 则在声明后逐个赋值。
构造函数与字段访问
每个 type 定义都自动生成一个_构造函数_——创建实例的方式。你已经见过这个了:User { name: "Alice", age: 30, active: true }。构造函数由类型名后跟花括号和 字段: 值 对组成。
有了实例后,你用点号访问字段:alice.name、alice.age。这在所有四个目标语言中的工作方式相同。
输出:
50area 函数接受一个 Rectangle 参数,返回宽度与高度的乘积。注意几个重要细节:
area的返回类型从表达式r.width * r.height推断为float。你不需要写fn area(r Rectangle) float { ... },虽然你可以这么做。r参数是_按值传递_的——函数接收到的是矩形的副本。这是 Auto(以及 Rust 和 C)的默认行为。main中的原始rect不会被改变。- C 转译器也是按值传递结构体。
area函数在栈上接收整个结构体的副本。
与第一阶段的匿名对象对比。{ name: "Alice", age: 30 } 给你的是一个动态的字段包。而 type User { name String, age int } 给你的是一个_类型_——一个编译器理解、检查并可以用来捕获错误的命名形状。
用 ext 定义方法
没有行为的数据只是一条记录。真正的程序需要为数据附加操作——计算点的距离、平移坐标、格式化用户全名。Auto 提供了 ext 关键字来实现这个目的。它让你在不修改类型定义本身的情况下为类型添加方法。
输出:
5
4点 (3.0, 4.0) 到原点的距离是 5.0(经典的 3-4-5 直角三角形)。平移 (1.0, 2.0) 后得到新点 (4.0, 6.0),我们打印其 x 坐标:4。
让我们拆解一下 ext 块:
ext Point { ... }为Point类型附加方法。方法定义在块内部,与函数类似,但可以访问self。&self是每个方法的第一个参数。它指向调用方法的实例。当你写p.distance()时,方法体中的self指的就是p。distance(&self) float不需要额外参数,返回一个float。它用勾股定理计算到原点的欧几里得距离。translate(&self, dx float, dy float) Point接受两个额外参数,返回一个_新的_Point。原始点不变——这正是"默认不可变"理念的体现。
&self 参数在各目标语言中有特殊含义:
| 语言 | 机制 | Auto 的 &self 映射为 |
|---|---|---|
| Rust | 带 &self 的 impl 块 | &self(完全相同) |
| Python | 带 self 的实例方法 | self(第一个参数) |
| C | 带指针参数的函数 | Point* self(指针) |
| TypeScript | 使用 this 的类方法 | this(隐式引用) |
在 Rust 中,ext 直接映射为 impl——在结构体上定义方法的标准方式。在 Python 中,方法成为 @dataclass 主体的一部分。在 C 中,方法成为接受结构体指针的独立函数。在 TypeScript 中,方法直接定义在类内部。
&self 中的 & 前缀借鉴自 Rust。它表示方法接收实例的_引用_而非消费它。这暗示了底层的内存模型:方法可以读取数据但不拥有它。目前,你只需要记住 &self 的意思是"这个方法操作该类型的一个实例"。
元组
有时你需要将几个不同类型的值组合在一起,而不想定义完整的 type。_元组_是一个固定大小的有序集合,每个位置可以有不同类型。你用圆括号和逗号分隔的值来创建它:
var pair = (1, "hello")这个元组有两个元素:位置 0 是 int,位置 1 是 String。类型被推断为 (int, String)。
输出:
1
hello
1
hello有两种方式访问元组元素:
- 位置访问:
pair.0和pair.1通过从零开始的位置编号访问第一个和第二个元素。这在 Auto 和 Rust 中有效。 - 解构:
var (a, b) = pair将元组拆包为两个独立变量。这是一种一次性提取所有元素的简洁方式。
每种语言处理元组的方式不同:
- Rust 原生支持元组。位置访问
.0、.1直接有效。用let (a, b) = pair解构也是原生的。 - Python 使用索引
pair[0]、pair[1]。用a, b = pair解构是 Python 的元组拆包特性。 - C 没有元组,所以转译器生成一个
typedef struct,字段命名为_0、_1。解构变成逐个字段赋值。 - TypeScript 将元组映射为带固定类型签名的数组
[number, string]。索引和解构都能自然工作。
元组在需要从函数返回多个值或临时组织少量相关值时很有用。它很轻量——不需要类型定义。但如果你发现自己反复使用相同的元组形状,考虑定义一个 type。命名的类型总是比未命名的类型更清晰。
类型别名
有时类型名称本身不够表达含义。int 可能是计数、ID、索引或年龄。float 可能是坐标、重量或温度。_类型别名_为现有类型赋予有意义的名称,而不创建新类型:
type Point = (float, float)
type Id = inttype 关键字身兼两职:带花括号时定义自定义类型,带 = 时创建别名。别名只是同一底层类型的另一个名称——Id 与 int 完全相同,但它告诉读者(和编译器)这个值是作为标识符使用的。
输出:
(0.0, 0.0)
42类型别名在大型程序中特别有用。当你看到函数签名 fn lookup(id Id) User 时,你立刻知道 id 是用户标识符,而不是某个随意的整数。相比之下,fn lookup(id int) User——类型是对的,但含义丢失了。
每个目标语言有自己的类型别名机制:
- Rust 使用
type关键字(与 Auto 语法完全相同):type Point = (f64, f64);。 - Python 使用简单赋值:
Point = tuple[float, float]。这是运行时别名——Python 不区分Point和底层元组类型。 - C 使用
typedef处理别名和结构体定义。typedef long long Id;创建别名,typedef struct { ... } Point;创建结构体类型。 - TypeScript 使用
type关键字(与 Auto 完全相同):type Point = [number, number];。
总结
本章标志着从第一阶段到第二阶段的过渡。你已经从把 Auto 当作简单脚本语言使用,成长到了理解其类型系统。以下是你的收获:
let创建不可变绑定。默认使用let,只在需要重新赋值时才用var。编译器在编译时强制不可变性。- 类型注解——
var count int = 0——让你显式声明变量的类型。可选但有助于文档和错误检测。 - 用
type定义自定义类型取代了第一阶段的匿名对象。type定义创建一个命名的、可复用的形状。 - 构造函数与字段访问——
User { name: "Alice" }创建实例,alice.name读取字段。默认按值传递。 - 用
ext定义方法让你为类型附加函数。&self参数指向调用方法的实例。 - 元组
(1, "hello")将固定数量的不同类型的值组合在一起。用位置编号(.0、.1)访问,或用var (a, b) = pair解构。 - 类型别名——
type Id = int——为现有类型赋予有意义的名称以提高可读性。
type 关键字是本章最通用的工具。带花括号时,它定义类似结构体的类型。带 = 时,它创建别名。结合 ext,你可以为任何已定义的类型添加方法。这些功能共同构成了 Auto 类型系统的基础——一个足以应对复杂程序但又足够简单、不需要样板代码的系统。
在下一章中,我们将探索模式匹配——一种基于值的形状来检查和解构值的强大方式。