第8条:熟悉引用和指针类型在一般的编程中,引用(reference)是一种间接访问数据结构的方式,它与拥有该数据结构的变量是分开的。在实践中,引用通常由指针(pointer)来实现。指针是一个数字,它的值是数据结构的变量在内存中的地址。现代CPU通常会对指针施加一些限制:内存地
在一般的编程中,引用(reference) 是一种间接访问数据结构的方式,它与拥有该数据结构的变量是分开的。在实践中,引用 通常由 指针(pointer) 来实现。指针 是一个数字,它的值是数据结构的变量在内存中的地址。
现代 CPU 通常会对指针施加一些限制:内存地址应该处于有效的内存范围内(虚拟内存或物理内存),并且可能需要对齐(例如,一个4字节的整数值可能只有在其地址是4的倍数时才能访问)。
然而,高级编程语言通常会在其类型系统中编码更多关于指针的信息。在 C 衍生的语言(包括 Rust )中,指针中有一个类型,该类型表示期望在所指向的内存地址存储哪种类型数据结构。这允许通过代码解释在该地址以及随后内存中的内容。
这种基本的指针信息-假定的内存位置和预期的数据结构布局-在 Rust 中被表示为一个裸指针(raw point)。然而,安全的 Rust 代码不使用裸指针,因为 Rust 提供了更丰富的引用和指针类型,这些类型提供了额外的安全保证和约束。这些引用和指针类型是本节的主题;裸指针则留待[第16条]讨论(该节讨论 unsafe 代码)。
在 Rust 中,最常见的指针类型是 引用,用 &T
表示,其中 T
是任意类型。尽管在底层这是一个指针值,但编译器会确保在使用时遵循一些规则:
T
的实例。这些额外的约束总是隐含在 Rust 中的 引用 中,因此 裸指针 通常很少出现。
Rust 引用必须指向有效、正确对齐的项的约束,与 C++ 的引用类型相同。然而,C++ 没有生命周期的概念,因此允许使用悬空引用而导致错误[^1]:
// C++
const int& dangle() {
int x = 32; // on the stack, overwritten later
return x; // return reference to stack variable!
}
Rust 的借用和生命周期检查会让等价的代码甚至不能编译:
fn dangle() -> &'static i64 {
let x: i64 = 32; // 在栈上
&x
}
error[E0515]: cannot return reference to local variable `x`
--> src/main.rs:477:5
|
477 | &x
| ^^ returns a reference to data owned by the current function
Rust 的引用 &T
允许只读访问底层元素(大致相当于 C++ 的const T&
)。一个允许修改底层元素的可变引用写为 &mut T
,同样也遵循第 15 项讨论的借用检查规则。这种命名方式反映了 Rust 和 C++ 之间略微不同的思维方式:
mut
)。const
)。
编译器会将使用引用的 Rust 代码转换为使用简单指针的机器码,在 64 位平台上这些指针的长度为 8 个字节(本节假设一直如此)。例如,一对局部变量以及对它们的引用:
pub struct Point {
pub x: u32,
pub y: u32,
}
let pt = Point { x: 1, y: 2 };
let x = 0u64;
let ref_x = &x;
let ref_pt = &pt;
可能最终在栈上布局如图1-2所示。
<img src="https://rustx-labs.github.io/effective-rust-cn/images/item8/stack.svg" alt="img"> 图1-2.带有指向局部变量的指针的栈布局
Rust 引用可以指向位于栈或堆上的元素。Rust 默认情况下会在栈上分配内存,但是 Box<T>
指针类型(大致相当于 C++ 的 std::unique_ptr<T>
) 会强制分配到堆上,这意味着分配的元素可以比当前代码块的作用域更长寿。本质上,Box<T>
也是一个简单的8字节指针值(64 位平台):
注意:
let box_pt = Box::new(Point { x: 10, y: 20 });
这在图1-3中被描述。
<img src="https://rustx-labs.github.io/effective-rust-cn/images/item8/heap.svg" alt="img">
图1-3.栈上的 Box
指针指向堆上的 struct
期望一个引用参数,如 &Point
的方法也可以接受一个 &Box<Point>
:
fn show(pt: &Point) {
println!("({}, {})", pt.x, pt.y);
}
show(ref_pt);
show(&box_pt);
(1, 2)
(10, 20)
这之所以可能,因为 Box<T>
实现了 [Deref
] 特征(Trait
),Target = T
。某个类型实现这个特征意味着该特征的 [deref()
] 方法可以用于创建对 Target
类型的引用。还有一个等效的 [DerefMut
] 特征,它会生成对 Target
类型的可变引用。
Deref
/DerefMut
特征有点特别,因为Rust编译器在处理实现它们的类型时有特定的行为。当编译器遇到解引用表达式(例如,[*x
]),它会根据解引用是否需要可变访问来查找并使用这些特征的实现。这种 Deref
转换允许各种智能指针类型像普通引用一样工作,它是 Rust 中少数允许隐式类型转换的机制之一(如[第5条]所述)。
作为一个技术细节,理解为什么 Deref
特征不能对目标类型是泛型的(Deref<Target>
)是很值得的。如果它们是,那么某些类型 ConfusedPtr
就可以同时实现 Deref<TypeA>
和 Deref<TypeB>
,这将使编译器无法为 *x
这样的表达式推导出唯一的类型。因此,目标类型被编码为一个名为 Target
的关联类型。
这种技术细节与另两个标准指针特征 [AsRef
] 和 [AsMut
] 形成对比。这些特征不会在编译器中引起特殊行为,但允许通过对其特征函数([as_ref()
] 和 [as_mut()
])的显式调用进行引用或可变引用的转换。转换的目标类型被编码为类型参数(例如,AsRef<Point>
),这意味着一个容器类型可以支持多个目标类型。
例如,标准 [String
] 类型实现了 Deref
特征,Target = str
,这意味着像 &my_string
这样的表达式可以强制转换为类型 &str
。但它也实现了以下特征:
AsRef<[u8]>
,允许转换为字节切片 &[u8]
AsRef<OsStr>
,允许转换为OS字符串AsRef<Path>
,允许转换为文件系统路径AsRef<str>
,允许转换为字符串切片 &str
(与 Deref
相同)Rust有两个内置的胖指针类型:切片(Slice
)和特征(Trait
)对象。这些类型的行为像指针,但它们持有关于指向对象的额外信息。
第一种胖指针类型是切片:它引用某个连续值集合的子集。切片由一个(没有所有权的)简单指针和一个长度字段组成,因此大小是简单指针的两倍(在 64 位平台上为 16 字节)。切片的类型写为 &[T]
- 它表示对 [T]
的引用,[T]
是类型 T 的连续值集合的概念类型。
概念类型 [T]
不能被实例化,但是有两种常见的容器实现了它。第一种是数组:一个连续的值集合,其大小在编译时是已知的。
一个有5个值的数组将始终有5个值。因此,切片可以引用数组的一个子集(如图1-4所示):
let array: [u64; 5] = [0, 1, 2, 3, 4];
let slice = &array[1..3];
图1-4.指向栈数组的栈切片
连续值的另一种常见容器是 Vec<T>
。这像数组一样持有连续的值集合,但与数组不同,Vec
中的值的数量可以增长(例如,用 [push(value)
] )或缩小(例如,用 [pop()
] )。
Vec
的内容保存在堆上(这允许其大小发生变化),并且总是连续的,因此切片可以引用 Vec
的子集,如图 1-5 所示:
let mut vector = Vec::<u64>::with_capacity(8);
for i in 0..5 {
vector.push(i);
}
let vslice = &vector[1..3];
图1-5.指向堆上的Vec内容的栈切片
表达式 &vector[1..3]
的底层有很多细节,所以值得将其拆解成d多个部分:
1..3
部分是一个[范围表达式(range expression)];编译器会将其转换为 [Range<usize>
] 类型的实例,该类型包含下限(1)但不包含上限(3)。Range
类型[实现了][SliceIndex<T>
]特征,该特征描述了对任意类型 T
的切片的索引操作(因此Output
类型为[T]
)。vector[]
部分是一个[索引表达式(indexing expression)];编译器将其转换为在 vector
上调用 [Index
] 特征的 [index
] 方法,并附加一次解引用(即 *vector.index()
)。[^2]vector[1..3]
会调用 Vec<T>
的 Index<I>
[实现],它要求 I
是 SliceIndex<[u64]>
的一个实例。这是因为 `Range<us...如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!