Skip to content
📖 Auto 编程语言(主书)本书目录

类型与 let ​

在前五章中,你一直把 Auto 当作脚本语言来使用——用 var 声明变量,依赖类型推断,用匿名对象来组织数据。这种方式让你快速上手,也是 Auto 的优势之一。但 Auto 还有更深层的一面。

本章是第二阶段(Phase 2)的第一章。我们将"成长"起来。你将学习为什么不可变性很重要,如何为变量添加显式类型注解,如何用 type 定义自定义数据类型,如何用 ext 附加方法,以及如何使用元组和类型别名。这些工具能让你的程序不仅正确,而且清晰、安全、可维护。

在本章中,我们将讨论:

  • let —— 不可变绑定及其重要性
  • 类型注解 —— 为变量添加显式类型
  • 自定义类型 —— 用 type 定义结构化数据
  • 构造函数与字段访问 —— 创建实例并读取字段
  • 用 ext 定义方法 —— 为类型附加行为,&self
  • 元组 —— 固定大小的异构集合
  • 类型别名 —— 为类型起有意义的名称

let —— 默认不可变 ​

在第一阶段,所有变量都用 var 声明。var 关键字创建的是_可变_绑定——你可以多次重新赋值。这对计数器和累加器来说没问题,但大多数时候你并不真的需要改变一个值。

Auto 提供了 let 用于创建不可变绑定。一旦你给 let 绑定赋了值,就不能再重新赋值。这不是限制——而是安全特性。当你读到 let name = "Alice" 时,你可以确定 name 在其作用域内始终指向 "Alice"。没有任何函数能在你不知情的情况下改变它。

`let` 用于不可变绑定,`var` 用于可变绑定

输出:

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 的命名相反,但语义相同。

对照表如下:

AutoRustPythonCTypeScript
var x = 5let mut x = 5x = 5long long x = 5let x = 5
let y = 10let y = 10y = 10const long long y = 10const y = 10

今后,只要值不需要改变,就使用 let。只在确实需要重新赋值时才使用 var。这不仅仅是风格偏好——它让代码更容易推理,也帮助编译器生成更好的代码。

类型注解 ​

Auto 可以推断大多数类型。当你写 var x = 5 时,编译器知道 x 是 int。当你写 let name = "Alice" 时,它知道 name 是 String。但有时你想要显式标注——为了记录意图,为了尽早发现错误,或者为了满足目标语言的要求。

Auto 允许在变量名之后添加类型注解:

auto
var count int = 0
let name String = "Alice"

语法是 名称 类型——变量名在前,类型在后,用空格分隔。这与 type 定义和函数参数中的空格分隔语法一致。

为变量添加类型注解

输出:

0
Alice
3.14159
true

Auto 的基本类型到各目标语言的映射如下:

AutoRustPythonCTypeScript
inti64intlong longnumber
floatf64floatdoublenumber
StringStringstrconst char*string
boolboolboolboolboolean

几点说明:

  • 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 关键字解决了所有这些问题。它让你定义一个_命名_类型——一个描述值有哪些字段以及字段类型的蓝图:

auto
type User {
    name String
    age int
    active bool
}

这定义了一个名为 User 的类型,有三个字段:name(String 类型)、age(int 类型)和 active(bool 类型)。注意语法:每个字段是 名称 类型,用空格分隔。没有逗号,没有冒号——只有字段名和它的类型。

用 `type` 创建自定义类型

输出:

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。这在所有四个目标语言中的工作方式相同。

创建实例和访问字段

输出:

50

area 函数接受一个 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 关键字来实现这个目的。它让你在不修改类型定义本身的情况下为类型添加方法。

用 `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。_元组_是一个固定大小的有序集合,每个位置可以有不同类型。你用圆括号和逗号分隔的值来创建它:

auto
var pair = (1, "hello")

这个元组有两个元素:位置 0 是 int,位置 1 是 String。类型被推断为 (int, String)。

元组——不同类型的固定大小集合

输出:

1
hello
1
hello

有两种方式访问元组元素:

  1. 位置访问:pair.0 和 pair.1 通过从零开始的位置编号访问第一个和第二个元素。这在 Auto 和 Rust 中有效。
  2. 解构: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 可能是坐标、重量或温度。_类型别名_为现有类型赋予有意义的名称,而不创建新类型:

auto
type Point = (float, float)
type Id = int

type 关键字身兼两职:带花括号时定义自定义类型,带 = 时创建别名。别名只是同一底层类型的另一个名称——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 类型系统的基础——一个足以应对复杂程序但又足够简单、不需要样板代码的系统。

在下一章中,我们将探索模式匹配——一种基于值的形状来检查和解构值的强大方式。

基于 MIT 许可发布。