清理特型Drop
当一个值的拥有者消失时,Rust 会丢弃(drop)该值。丢弃一个值就必须释放 该值拥有的任何其他值、堆存储和系统资源。
丢弃可能发生在多种情况下:
- 当变量超出作用域时;
- 在表达式语句的末尾;
- 当截断一个向量时,会从其末尾移除元素
struct Appellation {
name: String,
nicknames: Vec<String>
}
每当 Appellation 被丢弃时,Rust 都会自动清理所有这些内容,无须你进行任何进一步的编码。也可以通过实现 std::ops::Drop
特型来自定义 Rust 该如何丢弃此类型的值:
trait Drop {
fn drop(&mut self);
}
Drop 的实现类似于 C++ 中的析构函数或其他语言中的终结器。
当一个值被丢弃时,如果它实现了 std::ops::Drop
,那么 Rust 就会调用它的 drop 方 法,然后像往常一样继续丢弃它的字段或元素拥有的任何值。这种对 drop 的隐式调用是调用该方法的唯一途径。如果你试图显式调用该方法,那么 Rust 会将 其标记为错误。
Rust 在丢弃某个值的字段或元素之前会先对值本身调用 Drop::drop,该方法收到的值仍然是已完全初始化的。因此,在 Appellation 类型的 Drop 实现 中可以随意使用其字段:
impl Drop for Appellation {
fn drop(&mut self) {
print!("Dropping {}", self.name);
if !self.nicknames.is_empty() {
print!(" (AKA {})", self.nicknames.join(", "));
}
println!("");
}
}
{
let mut a = Appellation {
name: "Zeus".to_string(),
nicknames: vec!["cloud collector".to_string(),
"king of the gods".to_string()]
};
println!("before assignment");
a = Appellation { name: "Hera".to_string(), nicknames: vec![] };
println!("at end of block");
}
将第二个 Appellation 赋值给 a 时,就会丢弃第一个 Appellation,而当我们离开 a 的作用域时,就会丢弃第二个 Appellation。
String 在内部使用 Vec 来保存它 的文本,因此 String 不需要自己实现 Drop,它会让 Vec 负责释放这些字符。
一个值可能会从一个地 方移动到另一个地方,但 Rust 只会丢弃它一次。
如果一个类型实现了 Drop,就不能再实现 Copy 特型了。如果类型是 Copy 类 型,就表示简单的逐字节复制足以生成该值的独立副本。
限界特型Sized
固定大小类型是指其每个值在内存中都有相同大小的类型。
Sized
是 Rust 中的一个特殊 trait,它表示一个类型在编译时具有已知的大小。这个 trait 由编译器自动实现,并且对于所有具有固定大小的类型都是默认实现的。理解 Sized
特性非常重要,因为它影响了如何使用泛型、trait 对象和其他高级特性。
Sized
的作用
-
确保类型有已知大小:当一个类型实现了
Sized
trait,意味着它的大小可以在编译时确定。这对于许多操作来说是必要的,比如将值放在栈上或作为函数参数传递。 -
默认约束:在 Rust 中,泛型参数默认要求实现
Sized
trait。这意味着如果你定义了一个泛型函数或结构体,默认情况下它只能接受那些在编译时大小已知的类型。
?Sized
和 Sized
的区别
有时你可能希望放宽对类型的大小限制,允许它们可以是动态大小(DST, Dynamically Sized Type)。为此,Rust 提供了 ?Sized
语法,这表明类型不一定需要实现 Sized
。例如:
fn print_type_size<T: ?Sized>(t: &T) {
println!("Type T might not be sized");
}
在这个例子中,T
可以是任何类型,包括那些没有固定大小的类型,如切片 [T]
或者特质对象 dyn Trait
。
动态大小类型 (DST)
Rust 支持几种动态大小类型,这些类型的大小只有在运行时才能确定。常见的 DST 包括:
- 切片
[T]
- 特质对象
dyn Trait
- 裸指针到 DST,如
*const [T]
或*mut dyn Trait
由于这些类型的大小无法在编译时确定,因此它们不能直接存储在栈上或作为普通变量使用。相反,它们通常通过指针(如 &[T]
、Box<dyn Trait>
)来间接引用。
使用场景
-
泛型编程:当你编写泛型代码时,默认情况下你的泛型参数是
Sized
的。如果你想让泛型参数接受动态大小类型,你需要显式地指定?Sized
。 -
特质对象:当你创建特质对象(如
Box<dyn Trait>
或&dyn Trait
)时,特质本身并不实现Sized
,因为特质对象的实际大小是在运行时决定的。 -
结构体和枚举:如果结构体或枚举包含字段或变体,这些字段或变体必须是
Sized
的,除非你使用了特殊的语法(如裸指针)来处理动态大小类型。
示例代码
下面是一个简单的例子,展示了 Sized
和 ?Sized
的使用:
// 这个函数只接受具有已知大小的类型
fn only_sized<T: Sized>(t: T) {
// 函数体...
}
// 这个函数可以接受任何类型,包括动态大小类型
fn also_unsized<T: ?Sized>(t: &T) {
// 函数体...
}
struct Wrapper<T: ?Sized> {
value: Box<T>,
}
fn main() {
only_sized(42); // OK, i32 实现了 Sized
// only_sized([1, 2, 3]); // 错误,数组的大小未知
also_unsized(&42); // OK, &i32 实现了 ?Sized
also_unsized(&[1, 2, 3]); // OK, &[i32] 实现了 ?Sized
let boxed_slice = Wrapper { value: Box::new([1, 2, 3]) };
}
only_sized
函数只能接受实现了 Sized
的类型,而 also_unsized
函数则更加灵活,它可以接受任何类型的引用,包括那些动态大小的类型。此外,Wrapper
结构体能够包含动态大小类型,因为它使用了 Box<T>
来间接持有数据。
Rust 中的几乎所有类型都是固定大小的,比如每个 u64 占用 8 字节,每个 (f32, f32, f32) 元组占用 12 字节。甚至枚举也是有大小的,也就是说,无论实际存在的是哪个变体,枚举总会占据足够的空间来容纳其最大的变体。尽管 Vec 拥有一个 大小可变的堆分配缓冲区,但 Vec 值本身是指向“缓冲区、容量和长度”的指针
,因此 Vec 也是一个固定大小类型。
所有固定大小类型都实现了 std::marker::Sized
特型,该特型没有方法或 关联类型。Rust 自动为所有适用的类型实现了 std::marker::Sized 特型, 你不能自己实现它。Sized 的唯一用途是作为类型变量的限界:像 T: Sized 这样的限界要求 T 必须是在编译期已知的类型。由于 Rust 语言本身会使用这种类型的特型为具有某些特征的类型打上标记,因此我们将其称为标记特型
。
然而,Rust 也有一些无固定大小类型,它们的值大小不尽相同。例如,字符串 切片类型 str(注意没有 &)就是无固定大小的。字符串字面量 “diminutive” 和 “big” 是对占用了 10 字节和 3 字节的 str 切片的引用。像 [T](同样没有 &)这样的数组切片类型也是无固 定大小的,即像 &[u8] 这样的共享引用可以指向任意大小的 [u8] 切片。因为 str 类型和 [T] 类型都表示不定大小的值集,所以它们是无固定大小类型。
Rust 中另一种常见的无固定大小类型是 dyn
类型,它是特型对象的引用目标
,是指向实现了给定特型的某个值的指针
。例如,类型 &dyn std::io::Write 和 Box
是指向实现了 Write 特型的某个值的指针。引用目标可能是文件、网络套接字,或某种实现了 Write 的自定义类型。由于实现了 Write 的类型集是开放式的,因此dyn Write
作为一个类型也是无固定大小的,也就 是说它的值可以有各种大小。
Rust 不能将无固定大小的值存储在变量中或将它们作为参数传递。你只能通过 像 &str 或 Box 这样的本身是固定大小的指针来处理它们。
指向无固定大小值的指针始终是一个胖指针,宽度为两个机器字: 指向切片的指针带有切片的长度,特型对象带有指向方法实现的虚表的指针。 特型对象和指向切片的指针在结构上很像。这两种类型,胖指针都会补齐类型中缺少的信息——它携 带着长度或虚表指针。既然欠缺静态信息,那就用动态信息来弥补。 由于无固定大小类型处处受限,因此大多数泛型类型变量应当被限制为固定大小 的 Sized 类型。
事实上,鉴于这种情况的普遍性,Sized 已经成为 Rust 中的 隐式默认值:如果你写 struct S<T> { ... }
,那么 Rust 会将其理解为 struct S<T:Sized> { ... }
。如果你不想以这种方式约束 T,就必须将 其明确地排除,写成 struct S<T:?Sized> { ... }
。?Sized
语法专用 于这种情况,意思是“不要求固定大小的”。如果你写 struct S <T: ?Sized>{ b: Box }
,那么 Rust 将允许写成 S<str> 和 S<dyn Write>
,这样这 两个 Box 就变成了胖指针,而不像 S<i32> 和 S<String>
的 Box 那样只是普通指针。
尽管存在一些限制,但无固定大小类型能让 Rust 的类型系统工作得更顺畅。偶尔会遇到类型变量上的 ?Sized
限界,这几乎总是表明 “给定的类型只能通过指针访问”,并能让其关联的代码与切片对象和特型对象以及普通值一起使用。当类型变量具有?Sized
限界时,它的大小不确定,既可能是固定大小,也可能不是。
【柔性数组】除了切片对象和特型对象,还有另一种无固定大小类型。结构体类型的最后一个字段(而且只能是最后一个)可以是无固定大小的,并且这样的结构体本身也是 无固定大小的。例如,Rc 引用计数指针的内部实现是指向私有类型 RcBox 的指针,后者把引用计数和 T 保存在一起。下面是 RcBox 的简化定义:
struct RcBox<T: ?Sized> {
ref_count: usize, value: T,
}
Rc <T>
是引用计数指针,其中的 value 字段是 Rc <T>
对其进行引用计数的 T 类型。Rc<T>
会解引用成指向 value 字段的指针。ref_count 字段会保存其 引用计数。 真正的 RcBox 只是标准库的一个实现细节,无法在外部使用。但假设我们正在使用前面这种定义,那么就可以将此 RcBox 与固定大小类型一起使用,比如 RcBox <String>
的结果是一个固定大小的结构体类型。
或者也可以将它与无固 定大小类型一起使用,比如 RcBox<dyn Display>
就成了无固定大小的结构体类型。 不能直接构建 RcBox<dyn Display>
值,而应该先创建一个普通的固定大小 的 RcBox,并让其 value 类型实现 Display,比如 RcBox<String>
。然后 Rust 就会允许你将引用 &RcBox <String>
转换为胖引用 &RcBox<dyn Display>
:
let boxed_lunch: RcBox<String> = RcBox {
ref_count: 1,
value: "lunch".to_string()
};
use std::fmt::Display;
let boxed_displayable: &RcBox<dyn Display> = &boxed_lunch;
将值传给函数时会发生隐式转换,可以将 &RcBox 传给需 要 &RcBox 的函数:
fn display(boxed: &RcBox<dyn Display>) {
println!("For your enjoyment: {}", &boxed.value);
}
display(&boxed_lunch);
深拷贝特型Clone
std::clone::Clone 特型适用于可复制自身的类型。
trait Clone: Sized {
fn clone(&self) -> Self;
fn clone_from(&mut self, source: &Self) { // Self类型标记
*self = source.clone()
}
}
clone
方法应该为 self 构造一个独立的副本并返回它。由于此方法的返回类型是 Self,并且函数本来也不可能返回无固定大小的值,因此 Clone 特型也是扩展自 Sized 特型的,进而导致其实现代码中的 Self 类型被限界成了 Sized。
克隆一个值需要为它拥有的任何值分配副本,因此 clone 无论在时间消 耗还是内存占用方面都是相当昂贵的。例如,克隆 Vec 不仅会复制 此向量,还会复制它的每个 String 元素。这就是 Rust 不会自动克隆值,而是 要求你进行显式方法调用的原因。
像 Rc 和 Arc 这样的引用计数指针类型
属于例外,即克隆其中任何一个都只会增加引用计数并为你返回一个新指针。
clone_from
方法会把 self 修改成 source 的副本。clone_from 的默认定义只是克隆 source,然后将其转移给 *self。对于某些类 型,有一种更快的方法可以获得同样的效果。假设 s 和 t 都是 String。s = t.clone(); 语句必然会克隆 t,丢弃 s 的旧值,然后将克隆后的值转移给 s,这会进行一次堆分配和一次堆释放。但是如果属于原始 s 的堆缓冲区有足够的容量来保存 t 的内容,则不需要分配或释放:可以简单地将 t 的文本复制到 s 的缓冲区并调整长度。在泛型代码中,应该优先使用 clone_from,以便充分利用这种优化。
如果 Clone 实现只需要简单地对类型中的每个字段或元素进行 clone,然后从这些克隆结果中构造一个新值,并且认为 clone_from 的默认定义已经足够好了,那么 Rust 也可以帮你实现:只要在类型定义上方写 # [derive(Clone)]
就可以了。
标准库中几乎所有能合理复制的类型都实现了 Clone。不仅 bool、i32 等原始 类型实现了 Clone,String、Vec 和 HashMap 等容器类型也实现了 Clone。
而那些无法合理复制的类型(如 std::sync::Mutex
)则没有实现 Clone。像 std::fs::File
这样的类型虽然可以复制,但如果操作系统无法 提供必要的资源,则复制可能会失败。这些类型也没有实现 Clone,因为 clone 必须是不会失败的。作为替代,std::fs::File
提供了一个try_clone
方法,该方法会返回一个 std::io::Result
值,用以报告失败信息。
浅拷贝特型Copy
对于大多数类型,赋值时会移动值,而不是复制它们。移动值可以更简单地跟踪它们所拥有的资源。但是也有需要复制值的场景。
如果一个类型实现了 std::marker::Copy
标记特型,那么它就是 Copy 类型,允许通过逐位复制的方式产生副本。其定义 如下所示:
trait Copy: Clone { }
Copy 是一种对语言有着特殊意义的标记特型,因此只有当类型需要一个浅层的逐字节复制时,Rust 才允许它实现 Copy。拥有任何其他资源(比如堆缓冲区或操作系统句柄)的类型都无法实现 Copy。
任何实现了 Drop 特型的类型都不能是 Copy 类型。Rust 认为如果一个类型需要特殊的清理代码,那么就必然需要特殊的复制代码,因此不能是 Copy 类型。
可以使用 #[derive(Copy)]
让 Rust 为你派生出 Copy 实现。你会经常看到同时使用 #[derive(Copy, Clone)]
进行派生的代码。 在允许一个类型成为 Copy 类型之前务必慎重考虑。尽管这样做能让该类型更易于使用,但也对其实现施加了严格的限制。如果复制的开销很高,那么就不适合进行隐式复制。
Clone和Copy区别
Copy
特性
-
含义:当一个类型实现了
Copy
特性时,它意味着该类型的值可以被简单地通过位拷贝来复制。这意味着当你将一个值赋给另一个变量或将其作为参数传递给函数时,Rust 会自动创建该值的一个副本,而不是移动所有权。 -
语义:对于实现了
Copy
的类型来说,复制操作是浅拷贝,并且不会影响原始数据。例如,整数、浮点数、字符以及小的固定大小的数据类型通常都是Copy
的。 -
性能:因为只是简单地复制内存中的位,所以
Copy
操作是非常快速和高效的。 -
实现限制:并不是所有的类型都可以实现
Copy
。特别是那些包含动态分配资源(如堆上分配的内存)或者具有非复制语义(如文件句柄或网络连接)的类型,不能实现Copy
,因为简单的位复制无法正确处理这些资源的所有权和生命周期问题。 -
默认实现:如果一个类型只包含其他
Copy
类型,并且没有自定义析构函数(即没有实现Drop
trait),那么它可以自动派生Copy
。
#[derive(Copy, Clone)]
struct Point {
x: i32,
y: i32,
}
Clone
特性
-
含义:
Clone
特性允许你显式地调用.clone()
方法来创建一个对象的深拷贝。这意味着对于复杂类型,Clone
可能会执行更深层次的复制逻辑,以确保新旧对象独立于彼此。 -
语义:
Clone
可以用于任何需要深拷贝的场景,包括但不限于那些拥有堆分配数据的类型。开发者可以根据需要自定义Clone
的行为,以确保正确的复制语义。 -
性能:由于
Clone
可能涉及更深一层的数据结构复制,因此它的性能可能不如Copy
那么高效,具体取决于实现方式。 -
实现灵活性:几乎所有类型都可以实现
Clone
,包括那些管理动态资源的类型。但是,实现者必须确保正确处理所有相关资源,避免双重释放等问题。
struct ComplexType {
data: String, // String 不是 Copy 的,因为它管理了堆上的资源
}
impl Clone for ComplexType {
fn clone(&self) -> Self {
ComplexType {
data: self.data.clone(), // 手动处理深拷贝
}
}
}
-
Copy
提供了一种轻量级的复制机制,适用于不需要关心所有权转移的小而简单的类型; -
Clone
则提供了一种更加通用和可控的方式来进行深拷贝,适用于各种类型,尤其是那些需要管理复杂资源的类型。