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

引用与指针 ​

在大多数编程语言中,你有两种使用数据的方式:你可以_拥有_它(变量的副本存储在你的变量中),或者你可以_借用_它(你的变量持有一个指向他人数据的地址)。Auto 将这种区别通过一个分层引用系统显式地表达出来,该系统借鉴了 Rust 的借用规则和 C 的指针模型,但以更简洁、更直观的语法呈现。

在本章中,我们将讨论:

  • 什么是引用? —— 借用与拥有
  • view T —— 只读引用(类似 Rust 的 &T)
  • mut T —— 可变引用(类似 Rust 的 &mut T)
  • 引用规则 —— 一个可变或多个不可变
  • *T —— 用于系统级访问的裸指针
  • &self 在方法中 —— 方法扩展中的自引用
  • 生命周期 —— 引用存活多久

什么是引用? ​

Auto 中的每个变量都拥有其值。当你写 var x = 5 时,值 5 属于 x。当 x 离开作用域时,该值会被清理。这是你从第一章开始一直在使用的_所有权_ 模型。

但有时你不想转移或复制一个值——你只是想_查看_它或_原地修改_它。这就是 引用的用途。_引用_是一个句柄,让你无需取得所有权就能访问数据。

Auto 提供三个级别的引用访问,每个级别拥有不同的能力并附带相应的限制:

引用类型AutoRustC++作用
只读view T&Tconst T&只读不能修改
可变mut T&mut TT&可读可改
裸指针*T*T / *mut TT*无限制的系统级访问

这种分层设计意味着你可以为不同的任务选择合适的工具。大多数代码使用 view T 和 mut T,它们是安全的并在编译时检查。只有底层系统代码需要 *T,而且需要显式选择使用。

view T —— 只读引用 ​

view T 引用允许函数_读取_数据而不取得所有权。原始变量在函数调用后仍然 有效且可用。这是 Auto 中等价于 Rust 的 &T 或 C++ 的 const T& 的概念。

使用 `view T` 读取字符串而不消费它

输出:

13
Hello, Auto!

函数 print_length 接收 s view String,意味着它接收字符串的一个只读视图。 它可以调用 .len() 读取字符串长度,但不能修改字符串内容。调用返回后, name 在 main 中仍然有效——view 不会消费值。

与不使用 view 的情况对比:

auto
fn consume(s String) {
    // s 现在被此函数拥有
}

fn main() {
    var name = "Hello"
    consume(name)
    // print(name)  // ERROR: name 已被移动
}

没有 view 时,Auto 的隐式移动语义会将 name 的所有权转移到 consume 中,之后 name 不再可用。view 关键字显式地选择不使用这种移动行为。

view T 在各语言中的映射:

  • Rust 生成 &T 引用参数。调用者传递 &name 来借用值。
  • Python 直接传递字符串——Python 字符串是不可变的,所以 const 语义 是自动的。
  • C 生成 const char* 参数,在类型级别强制只读访问。
  • TypeScript 直接传递字符串——TypeScript 字符串默认不可变。

mut T —— 可变引用 ​

mut T 引用允许函数_原地读取和修改_数据。通过引用所做的修改对原始所有者 可见。这是 Auto 中等价于 Rust 的 &mut T 或 C 的指针参数的概念。

使用 `mut T` 原地修改值

输出:

6

函数 increment 接收 n mut int,意味着它接收一个整数的可变引用。在函数 内部,n = n + 1 修改了原始值。回到 main 中,x 现在是 6,因为修改 是原地进行的。

关键观察:

  • main 中的变量 x 必须用 var(可变)声明。你不能将不可变的 let 变量作为 mut T 引用传递。
  • 编译器确保 x 在 increment 持有可变引用时不会被使用。这在编译时防 止了数据竞争和悬垂引用。

mut T 在各语言中的映射:

  • Rust 生成 &mut T 参数。调用者传递 &mut x。函数体使用 *n 解引 用并赋值。
  • Python 对基本类型没有原生的可变引用。转译器将值包装在列表中( [5])来模拟传引用语义。
  • C 生成指针参数(long long* n)。调用者传递 &x。这是 C 中最接近 的类比。
  • TypeScript 将值包装在对象中({ value: 5 })来模拟传引用,因为 TypeScript 基本类型按值传递。

引用规则 ​

Auto 在编译时强制执行与 Rust 相同的借用规则。这些规则以零运行时开销防止 数据竞争和未定义行为:

  1. 你可以对同一个值拥有任意数量的 view T(不可变)引用 —— 多个读 者总是安全的。

  2. 你对同一个值最多只能拥有一个 mut T(可变)引用 —— 同一时间只 能有一个写者。

  3. 不可变引用和可变引用不能同时存在 —— 你不能同时读写同一个值。

引用规则:同一时间只能有一个可变引用

输出:

5
5

第二行被注释掉了,因为在 Auto 中这会导致编译错误。编译器拒绝 var r2 mut x = x,因为 r1 已经持有对 x 的可变引用。对同一数据拥有 两个可变引用会导致未定义行为——两者可能同时写入,或者其中一个可能使另一 个失效。

注意 print(x) 与 r1 同时存在是被允许的。在 Auto 中,在简单上下文中, 当可变引用存在时通过非引用路径读取原始值是允许的,因为编译器可以验证在 读取期间 x 不会通过其他路径被修改。

这些规则由 Auto 编译器严格执行。相比之下:

  • Python 不强制执行借用规则。多个变量可以自由地引用和修改同一个对象。 这很灵活但可能导致隐蔽的 bug。
  • C 不强制执行借用规则。指向同一数据的多个指针可以存在,没有任何东 西阻止同时读写。这是 C 程序中内存安全 bug 的常见来源。
  • TypeScript 因与 Python 相同的原因不强制执行借用规则。

为什么这些规则很重要 ​

"一个可变或多个不可变"规则是 Auto 内存安全保证的关键。它在编译时消除数 据竞争(没有两个线程可以通过 Auto 的引用同时修改同一数据),并防止迭代 器失效(你不能在迭代集合时修改它)。编译器完成所有检查——零运行时开销。

*T —— 裸指针 ​

对于系统编程——编写操作系统、设备驱动程序或与 C 库的 FFI 绑定——Auto 提 供了使用 *T 语法的裸指针。裸指针完全绕过借用规则。它们可以为空、可以 悬垂、可以别名。能力越大,责任越大。

使用 `*T` 裸指针进行直接内存访问

输出:

42
100

var p *int = &x 创建一个裸指针 p,它持有 x 的地址。*p 语法解引用 指针以读取或写入它指向的值。在 *p = 100 之后,原始的 x 也变成了 100, 因为 p 直接指向 x 的内存位置。

Auto 中的裸指针与 C 指针工作方式相同:

  • &x 取 x 的地址
  • *p 解引用指针
  • *p = value 通过指针写入

在 Rust 中,裸指针操作需要 unsafe 块。Auto 编译器会生成适当的安全包装。 裸指针用于高级用例:

  • 通过 FFI 与 C 库交互
  • 实现需要指针算术的自定义数据结构
  • 编写操作系统内核或嵌入式固件

对于应用级代码,优先使用 view T 和 mut T——它们提供相同的性能,同时 具有编译时安全保证。

&self 在方法中 ​

当你使用 ext 为类型定义方法时,通常需要方法操作在调用它的实例上。Auto 在方法签名中使用 &self 来表示方法借用了实例。对于需要修改实例的方法, 可以使用 &mut self。

在方法扩展中使用 `&self`

输出:

3

Counter 类型在其 ext 块中定义了两个方法:

  • increment(&self) 修改计数器。&self 参数借用实例,允许更新 self.count。在 Rust 中,这映射为 &mut self,因为方法修改了数据。
  • get(&self) int 读取计数器的值。&self 参数不可变地借用实例。在 Rust 中,这映射为 &self。

Auto 方法签名中的 &self 语法映射为你从面向对象语言中期望的调用约定:

  • Rust 根据方法是否修改实例生成 &self 或 &mut self。
  • Python 使用标准的 self 参数——Python 的类系统默认基于引用。
  • C 生成带有显式指针参数的函数(Counter* self 或 const Counter* self)。使用 -> 运算符通过指针访问字段。
  • TypeScript 生成带有 this 的标准类方法——TypeScript 对象默认是引 用类型。

生命周期 ​

Auto 中的引用不会永远存在。每个引用都有一个_生命周期_——引用有效的代码 跨度。编译器使用生命周期来确保没有引用比它指向的数据活得更久。

在大多数情况下,Auto 可以自动推断生命周期。对于简单情况,你不需要编写 生命周期注解。编译器会分析代码并算出每个引用需要有效多久。

带生命周期的引用——编译器推断有效性

输出:

world!

函数 longest 接收两个 view String 引用并返回一个 view String 引用, 指向较长的那个字符串。返回值是对输入之一的引用——它不创建新字符串。

编译器需要验证返回的引用是有效的。在这个例子中,返回的引用指向 s1 或 s2,两者在 result 被使用时都还活着。所以代码是有效的。

在 Rust 中,这个函数需要显式的生命周期注解('a)。该注解告诉 Rust 编 译器返回的引用与两个输入生命周期中较短的那个一样长。Auto 的编译器执行相 同的分析,但在像这样的简单情况下不需要你编写注解。

编译器会拒绝试图返回指向即将被销毁数据的引用的代码:

auto
fn bad() view String {
    var s = "temporary"
    s  // ERROR: s 即将离开作用域
}

这会失败,因为 s 是一个局部变量。当 bad 返回时,s 被销毁,返回的引 用将指向无效内存。编译器在编译时捕获这一点。

生命周期在各语言中的映射:

  • Rust 在编译器无法推断时要求在函数签名中显式标注生命周期('a)。 这是最显式的系统。
  • Python 使用引用计数。当对象的引用计数降为零时释放对象,因此在正常 代码中不可能出现悬垂引用。
  • C 没有生命周期检查。返回指向局部变量的指针是_未定义行为_——这是崩 溃和安全漏洞的常见来源。
  • TypeScript 依赖垃圾回收。与 Python 类似,当对象不再可达时会被回收, 因此不需要担心悬垂引用。

总结 ​

Auto 的引用系统提供三个级别的访问,从安全高效到强大底层:

概念AutoRustC安全性
只读引用view T&Tconst T*编译时检查
可变引用mut T&mut TT*编译时检查
裸指针*T*mut TT*程序员负责
方法自借用&self&self / &mut selfType* self编译时检查
生命周期自动推断'a 注解无编译时检查

关键设计原则:

  1. 默认安全:view T 和 mut T 在编译时检查。编译器以零运行时开销 防止数据竞争、悬垂引用和使用后释放 bug。

  2. 能力显式选择:裸指针(*T)在 FFI 或系统编程需要时可用,但必须 显式选择使用。编译器不会让你意外绕过安全规则。

  3. 最小的注解负担:在大多数情况下,Auto 自动推断生命周期。你只需要 在从函数返回引用时考虑生命周期——即使如此,编译器也会用清晰的错误消息 引导你。

  4. 零成本抽象:引用没有运行时开销。view T 与指针大小相同,所有检查 在编译时完成。生成的代码与手写 C 一样高效。

在下一章中,我们将探讨 Auto 的集合——List、Map 和 Set——它们结合所 有权和引用,提供安全、高效的动态数据结构。

基于 MIT 许可发布。