引用与指针
在大多数编程语言中,你有两种使用数据的方式:你可以_拥有_它(变量的副本存储在你的变量中),或者你可以_借用_它(你的变量持有一个指向他人数据的地址)。Auto 将这种区别通过一个分层引用系统显式地表达出来,该系统借鉴了 Rust 的借用规则和 C 的指针模型,但以更简洁、更直观的语法呈现。
在本章中,我们将讨论:
- 什么是引用? —— 借用与拥有
view T—— 只读引用(类似 Rust 的&T)mut T—— 可变引用(类似 Rust 的&mut T)- 引用规则 —— 一个可变或多个不可变
*T—— 用于系统级访问的裸指针&self在方法中 —— 方法扩展中的自引用- 生命周期 —— 引用存活多久
什么是引用?
Auto 中的每个变量都拥有其值。当你写 var x = 5 时,值 5 属于 x。当 x 离开作用域时,该值会被清理。这是你从第一章开始一直在使用的_所有权_ 模型。
但有时你不想转移或复制一个值——你只是想_查看_它或_原地修改_它。这就是 引用的用途。_引用_是一个句柄,让你无需取得所有权就能访问数据。
Auto 提供三个级别的引用访问,每个级别拥有不同的能力并附带相应的限制:
| 引用类型 | Auto | Rust | C++ | 作用 |
|---|---|---|---|---|
| 只读 | view T | &T | const T& | 只读不能修改 |
| 可变 | mut T | &mut T | T& | 可读可改 |
| 裸指针 | *T | *T / *mut T | T* | 无限制的系统级访问 |
这种分层设计意味着你可以为不同的任务选择合适的工具。大多数代码使用 view T 和 mut T,它们是安全的并在编译时检查。只有底层系统代码需要 *T,而且需要显式选择使用。
view T —— 只读引用
view T 引用允许函数_读取_数据而不取得所有权。原始变量在函数调用后仍然 有效且可用。这是 Auto 中等价于 Rust 的 &T 或 C++ 的 const T& 的概念。
输出:
13
Hello, Auto!函数 print_length 接收 s view String,意味着它接收字符串的一个只读视图。 它可以调用 .len() 读取字符串长度,但不能修改字符串内容。调用返回后, name 在 main 中仍然有效——view 不会消费值。
与不使用 view 的情况对比:
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 的指针参数的概念。
输出:
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 相同的借用规则。这些规则以零运行时开销防止 数据竞争和未定义行为:
你可以对同一个值拥有任意数量的
view T(不可变)引用 —— 多个读 者总是安全的。你对同一个值最多只能拥有一个
mut T(可变)引用 —— 同一时间只 能有一个写者。不可变引用和可变引用不能同时存在 —— 你不能同时读写同一个值。
输出:
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 语法的裸指针。裸指针完全绕过借用规则。它们可以为空、可以 悬垂、可以别名。能力越大,责任越大。
输出:
42
100var 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。
输出:
3Counter 类型在其 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 的编译器执行相 同的分析,但在像这样的简单情况下不需要你编写注解。
编译器会拒绝试图返回指向即将被销毁数据的引用的代码:
fn bad() view String {
var s = "temporary"
s // ERROR: s 即将离开作用域
}这会失败,因为 s 是一个局部变量。当 bad 返回时,s 被销毁,返回的引 用将指向无效内存。编译器在编译时捕获这一点。
生命周期在各语言中的映射:
- Rust 在编译器无法推断时要求在函数签名中显式标注生命周期(
'a)。 这是最显式的系统。 - Python 使用引用计数。当对象的引用计数降为零时释放对象,因此在正常 代码中不可能出现悬垂引用。
- C 没有生命周期检查。返回指向局部变量的指针是_未定义行为_——这是崩 溃和安全漏洞的常见来源。
- TypeScript 依赖垃圾回收。与 Python 类似,当对象不再可达时会被回收, 因此不需要担心悬垂引用。
总结
Auto 的引用系统提供三个级别的访问,从安全高效到强大底层:
| 概念 | Auto | Rust | C | 安全性 |
|---|---|---|---|---|
| 只读引用 | view T | &T | const T* | 编译时检查 |
| 可变引用 | mut T | &mut T | T* | 编译时检查 |
| 裸指针 | *T | *mut T | T* | 程序员负责 |
| 方法自借用 | &self | &self / &mut self | Type* self | 编译时检查 |
| 生命周期 | 自动推断 | 'a 注解 | 无 | 编译时检查 |
关键设计原则:
默认安全:
view T和mut T在编译时检查。编译器以零运行时开销 防止数据竞争、悬垂引用和使用后释放 bug。能力显式选择:裸指针(
*T)在 FFI 或系统编程需要时可用,但必须 显式选择使用。编译器不会让你意外绕过安全规则。最小的注解负担:在大多数情况下,Auto 自动推断生命周期。你只需要 在从函数返回引用时考虑生命周期——即使如此,编译器也会用清晰的错误消息 引导你。
零成本抽象:引用没有运行时开销。
view T与指针大小相同,所有检查 在编译时完成。生成的代码与手写 C 一样高效。
在下一章中,我们将探讨 Auto 的集合——List、Map 和 Set——它们结合所 有权和引用,提供安全、高效的动态数据结构。