Appearance
一些基础概念
Turbo-Fish
Turbo-Fish 是 Rust 语言中一种用于在调用泛型函数或方法时显式指定泛型类型参数的语法。它使用双冒号(::
)后跟尖括号(<>
)来包裹类型参数,例如 ::<Type>
。
这种写法因其形状 ::<>
被戏称为 "Turbo-Fish"。它主要用于以下情况:
- 帮助编译器推断类型:当编译器无法从上下文中明确推断出泛型参数的具体类型时。
- 消除歧义:当一个方法或函数有多个泛型实现,或者可能与同名单非泛型方法冲突时。
下面是一些示例:
rust
// 示例1: Vec<T> 的类型推断
// 虽然在这里编译器通常可以推断出来,但显式指定更清晰
let numbers: Vec<i32> = Vec::new(); // 通过类型注解
// 使用 Turbo-Fish 明确指定 Vec 的元素类型为 i32
let mut numbers_tf = Vec::<i32>::new();
numbers_tf.push(1);
// 示例2: collect() 方法的歧义消除
// collect() 可以将迭代器转换成多种集合类型
let strings = vec!["hello", "world"];
// 如果不指定类型,编译器可能不知道要收集成什么
// let collected = strings.iter().map(|s| s.to_uppercase()).collect(); // 编译错误:类型不明确
// 使用 Turbo-Fish 指定收集为 Vec<String>
let collected_vec = strings.iter().map(|s| s.to_uppercase()).collect::<Vec<String>>();
// 也可以通过类型注解指定,但 Turbo-Fish 在这种场景下更常见于 collect 这类方法
let collected_vec_annotated: Vec<String> = strings.iter().map(|s| s.to_uppercase()).collect();
注意,let x = "hello".to_string();
这种写法不是 Turbo-Fish,to_string()
方法在这里并不需要显式泛型参数。Turbo-Fish 特指 ::<>
这种形式的泛型参数指定。
元组无法迭代
元组(Tuple)是 Rust 中一种将多个不同类型的值组合成一个复合类型的方式。元组的大小是固定的,一旦声明,其元素的数量和类型就不能改变。由于其元素可以是不同类型的,所以元组是异构(Heterogeneous)的。
Rust 的标准迭代器 (std::iter::Iterator
trait) 设计上是同构(Homogeneous)的,即 next()
方法返回 Option<Self::Item>
,其中 Self::Item
是一个确定的类型。这意味着迭代器一次只处理一种类型的元素。
由于元组中的元素类型可能各不相同(例如 (i32, f64, &str)
),它们在内存中的大小和布局也不同。这就导致了无法用一个统一的、类型安全的方式来实现标准的 Iterator
trait 来遍历元组中的所有元素。编译器无法在编译时确定一个通用的 Item
类型来代表元组中所有可能的元素类型。
主要原因总结:
- 类型异构性:元组可以包含不同类型的元素。
- 迭代器同构性:
Iterator
trait 的next()
方法期望返回固定类型的Item
。 - 编译时类型安全:Rust 强大的类型系统需要在编译时知道迭代元素的具体类型。
什么场景下会用到元组这种数据结构呢?
元组非常适合用于函数需要返回多个不同类型的值的场景。调用者可以方便地通过模式匹配或索引来解构并使用这些值。
rustfn get_coordinates() -> (i32, i32, String) { (10, 20, String::from("Location A")) } fn main() { let (x, y, location) = get_coordinates(); println!("Coordinates: ({}, {}), Location: {}", x, y, location); let point = (100, 200); println!("Accessing by index: {}, {}", point.0, point.1); }
常见的还有返回结果和错误,例如
Result<T, E>
类型的Ok(value)
和Err(error)
就可以看作是特殊形式的元组(枚举变体)。元组为什么不通过
[]
访问而是通过.
加上索引号呢?- 区分数组/切片:使用
[]
进行索引是数组(Array)和切片(Slice)的访问方式。数组和切片是同构的,所有元素类型相同,通过[]
访问符合其线性、同质的内存布局。 - 类型安全和编译时检查:元组的元素类型在编译时是固定的,通过
tuple.0
,tuple.1
这样的语法,编译器可以静态地检查索引是否越界,以及每个索引对应的元素类型是什么。如果允许[]
访问,由于索引是运行时变量,类型检查会更复杂,且可能引入运行时错误。 - 异构性:元组的元素类型不同,意味着它们在内存中的大小可能也不同。通过点号和数字索引(如
.0
)访问,编译器可以直接计算出每个元素在内存中的偏移量,而不需要像数组那样依赖统一的元素大小进行指针运算。
- 区分数组/切片:使用
虽然元组本身不能直接用 for
循环迭代,但你可以通过模式匹配来分别处理元组中的每个元素,或者如果需要对固定结构的元组进行类似迭代的操作,可以手动实现。
Rust 中数据存储位置:堆与栈
在 Rust 中,数据是存储在栈(Stack)上还是堆(Heap)上,主要取决于数据的类型、大小以及生命周期。理解这一点对于编写高效且内存安全的代码至关重要。
栈(Stack)分配:
- 特点:
- 分配速度快:栈是一块连续的内存区域,分配和释放内存仅涉及移动栈指针。
- LIFO(后进先出):最后压入栈的数据最先被弹出。
- 大小固定:在编译时,存储在栈上的数据类型其大小必须是已知的(实现了
Sized
trait)。 - 生命周期明确:栈上数据的生命周期与函数调用或作用域绑定,离开作用域时自动清理。
- 适合存储的数据:
- 基本类型:如整数 (
i32
,u64
)、浮点数 (f32
,f64
)、布尔值 (bool
)、字符 (char
)。 - 编译时已知大小的复合类型:如元组(如果其所有元素大小都已知)、固定大小的数组 (
[T; N]
)、以及不包含堆分配字段的小型结构体和枚举。
- 基本类型:如整数 (
rust
fn stack_example() {
let x = 5; // i32,存储在栈上
let arr = [1, 2, 3]; // [i32; 3],存储在栈上
let point = (10, 20); // (i32, i32),存储在栈上
// 当 stack_example 函数结束时,x, arr, point 会自动从栈上移除
}
堆(Heap)分配:
- 特点:
- 分配速度相对较慢:需要在堆上寻找合适的内存块,并记录分配信息。
- 大小可变或编译时未知:适合存储动态大小的数据。
- 生命周期灵活:堆上数据的生命周期不直接与特定作用域绑定,可以通过智能指针(如
Box<T>
,Rc<T>
,Arc<T>
)来管理。 - 需要显式或隐式管理:虽然 Rust 的所有权系统会自动处理内存释放,但分配本身是程序员通过特定类型(如
String
,Vec<T>
,Box<T>
)发起的。
- 适合存储的数据:
- 动态大小的集合:如
String
(可增长的字符串)、Vec<T>
(动态数组)。 - 需要在函数调用结束后依然存在的数据:通过
Box<T>
将数据移到堆上,并转移其所有权。 - 大型数据结构:即使大小已知,如果数据量过大,放在栈上可能导致栈溢出,此时也适合用
Box<T>
存储在堆上。 - Trait 对象 (
dyn Trait
):其具体类型和大小在编译时未知,通常通过Box<dyn Trait>
存储在堆上。
- 动态大小的集合:如
rust
fn heap_example() {
let s1 = String::from("hello"); // String 类型,其数据存储在堆上,s1 本身(包含指针、长度、容量)在栈上
let mut numbers = Vec::new(); // Vec<T> 类型,其元素存储在堆上
numbers.push(1);
numbers.push(2);
let b = Box::new(10); // 将 i32 类型的值 10 显式分配在堆上
// 当 s1, numbers, b 离开作用域时,它们指向的堆内存会被自动释放
}
结构体和枚举的存储:
结构体和枚举本身的存储位置取决于它们是否是 Sized
的,以及它们在何处被实例化:
如果结构体或枚举的所有字段都是栈上分配的类型,并且整个结构体/枚举实例在函数内部声明,那么它通常会存储在栈上。
ruststruct Point { x: i32, y: i32, } let p = Point { x: 1, y: 2 }; // p 存储在栈上
如果结构体或枚举包含堆分配的字段(如
String
,Vec<T>
,Box<T>
),那么结构体/枚举本身(包含那些堆指针和其它栈上字段)会存储在栈上(如果它作为局部变量),而它所拥有的数据则部分或全部存储在堆上。ruststruct Person { name: String, // String 的数据在堆上 age: u32, // u32 在栈上 (作为 Person 的一部分) } let person = Person { name: String::from("Alice"), age: 30 }; // person 实例在栈上,但其 name 字段指向堆上的字符串数据
如果整个结构体或枚举实例被
Box::new()
包裹,那么这个实例(包括其所有字段)都会被移动到堆上。rustlet boxed_point = Box::new(Point { x: 1, y: 2 }); // Point 实例现在存储在堆上
总结:
并非所有复杂类型都存储在堆上。Rust 倾向于尽可能在栈上分配内存以提高性能。只有当数据大小在编译时未知、数据需要动态增长、或者数据需要在当前作用域之外继续存在时,才会使用堆分配。所有权系统和智能指针使得 Rust 能够安全有效地管理堆内存,避免了常见的内存错误如悬垂指针和内存泄漏。
Rust 中的属性:#![...]
与 #[...]
在 Rust 中,属性(Attributes)是一种元数据,用于向编译器提供额外的信息、指令或进行代码生成。它们以 #[...]
或 #![...]
的形式出现,主要区别在于其作用域。
1. #![...]
(Crate-level Attributes / Inner Attributes - 内部属性)
- 作用域:应用于整个 Crate 或当前模块(如果用在模块文件中)。
- 语法:以
#![
开头,通常放置在 Crate 根文件(src/main.rs
或src/lib.rs
)的顶部,或者模块文件(如mod.rs
或其他*.rs
文件)的顶部。 - 用途:
Crate 配置:设置整个 Crate 的编译选项、特性开关、lint 等级等。
rust// 在 src/lib.rs 或 src/main.rs 的开头 #![allow(unused_variables)] // 允许未使用的变量,作用于整个 crate #![deny(unsafe_code)] // 禁止整个 crate 使用 unsafe 代码 #![feature(async_closure)] // 启用实验性的 async_closure 特性 #![crate_type = "lib"] // 指定 crate 类型为库 #![recursion_limit = "256"] // 设置递归限制
模块级配置:当用在模块文件中时,其作用域是该模块。
rust// 在某个模块 my_module.rs 的开头 // #![allow(dead_code)] // 这个 lint 设置将应用于 my_module 模块内的代码
文档注释:
#![doc = "..."]
可以为整个 Crate 或模块提供文档。
2. #[...]
(Item-level Attributes / Outer Attributes - 外部属性)
作用域:应用于紧随其后的单个“项”(Item),如函数、结构体、枚举、模块声明、
use
语句、常量、静态变量、trait 定义、impl 块等。语法:以
#[
开头,放置在它要修饰的项的正上方。用途:
派生宏 (Derive Macros):自动为类型实现某些 trait。
rust#[derive(Debug, Clone, PartialEq)] struct Point { x: i32, y: i32, }
条件编译 (Conditional Compilation):根据配置或特性决定是否编译某段代码。
rust#[cfg(target_os = "linux")] fn linux_specific_function() { // ... } #[cfg(feature = "my_feature")] mod advanced_functionality;
测试函数标记:
rust#[test] fn my_test_function() { assert_eq!(2 + 2, 4); }
Lints 控制:针对特定项控制 lint 检查。
rust#[allow(non_snake_case)] fn MyFunctionWithWeirdName() {}
宏属性 (Attribute-like Macros):自定义宏,可以作用于函数、结构体等。
rust// 假设有一个名为 `route` 的属性宏,用于 web 框架 // #[route(GET, "/users/:id")] // async fn get_user(id: u32) -> impl Responder { ... }
文档注释:
#[doc = "..."]
或更常见的///
(等同于#[doc = "..."]
) 和/** ... */
(块文档注释) 用于为项生成文档。rust/// 这是一个公开的函数,它做一些重要的事情。 #[doc = "这个函数非常重要,请仔细阅读文档。"] pub fn important_function() {}
其他:如标记
#[inline]
建议内联,#[must_use]
提示函数结果必须被使用等。
总结:
#![...]
(内部属性) 通常用于影响整个 Crate 或模块的设置和行为,写在文件或模块的开头。#[...]
(外部属性) 用于修饰紧随其后的单个代码项,提供特定的功能或元数据。
理解这两种属性的区别对于阅读和编写 Rust 代码,以及利用 Rust 强大的元编程能力都非常重要。
Orphan Rule(孤儿规则)
Rust 的孤儿规则(Orphan Rule)是其 Trait 系统中一条重要的相干性规则(Coherence Rule)。这条规则旨在确保在整个生态系统中,对于一个特定的类型和一个特定的 Trait,它们的实现是唯一的、无冲突的,从而保证了代码的可靠性和可组合性。
该规则的核心内容可以概括为:
在当前 Crate 中为一个类型 T
实现一个 Trait Tr
时,必须至少满足以下条件之一:
- Trait
Tr
是在当前 Crate 中定义的(即Tr
是本地 Trait)。 - 类型
T
是在当前 Crate 中定义的(即T
是本地类型)。
(当然,如果 Trait Tr
和类型 T
都是在当前 Crate 中定义的,那也是允许的。)
简单来说,孤儿规则禁止你为“别人家的孩子”同时穿上“别人家的衣服”。即,你不能在你的 Crate 中为一个外部定义的 Trait(Foreign Trait)实现一个外部定义的类型(Foreign Type)。 这样的实现被称为“孤儿实现”(Orphan Implementation),因为这个实现既不属于 Trait 的定义 Crate,也不属于类型的定义 Crate。
为什么要有孤儿规则?
- 防止冲突和歧义:如果没有孤儿规则,多个不同的第三方 Crate 可能会为同一对外部 Trait 和外部类型提供不同的、相互冲突的实现。当一个项目同时依赖这些 Crate 时,编译器将无法确定使用哪个实现,导致编译错误或不可预测的行为。
- 保证全局一致性:孤儿规则确保了 Trait 实现的来源是可追溯的,要么来自 Trait 的定义者,要么来自类型的定义者。
- 增强模块化和封装:Crate 的维护者可以清晰地知道他们需要对哪些 Trait 实现负责。
示例说明:
假设我们有以下场景:
std::fmt::Display
是一个在标准库std
中定义的 Trait (外部 Trait)。Vec<T>
是一个在标准库std
中定义的类型 (外部类型)。http::Request
(假设来自http
crate) 是一个外部定义的类型 (外部类型)。
在你的 Crate (my_crate
) 中:
rust
// my_crate/src/lib.rs
// 假设我们定义了一个本地类型
pub struct MyLocalType {
pub data: String,
}
// 假设我们定义了一个本地 Trait
pub trait MyLocalTrait {
fn describe(&self) -> String;
}
// 1. 实现本地 Trait (MyLocalTrait) 为本地类型 (MyLocalType) - ✅ 允许
impl MyLocalTrait for MyLocalType {
fn describe(&self) -> String {
format!('''MyLocalType with data: {}''', self.data)
}
}
// 2. 实现本地 Trait (MyLocalTrait) 为外部类型 (Vec<i32>) - ✅ 允许
// 因为 MyLocalTrait 是本地定义的
impl MyLocalTrait for Vec<i32> {
fn describe(&self) -> String {
format!('''A vector with {} elements''', self.len())
}
}
// 3. 实现外部 Trait (std::fmt::Display) 为本地类型 (MyLocalType) - ✅ 允许
// 因为 MyLocalType 是本地定义的
impl std::fmt::Display for MyLocalType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, '''Displaying MyLocalType: {}''', self.data)
}
}
// 4. 实现外部 Trait (std::fmt::Display) 为外部类型 (Vec<i32>) - ❌ 不允许 (孤儿规则冲突)
// impl std::fmt::Display for Vec<i32> {
// // fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
// // write!(f, "Displaying Vec<i32>")
// // }
// }
// 编译错误: only traits defined in the current crate can be implemented for types defined outside of the crate
// 原因:`Display` 和 `Vec<i32>` 都不是在 `my_crate` 中定义的。
// (注意:标准库本身已经为 Vec<T> 实现了 Display (如果T实现了Display),这里只是为了举例说明规则)
// 5. 实现外部 Trait (std::fmt::Display) 为另一个外部类型 (如 http::Request<()>) - ❌ 不允许 (孤儿规则冲突)
// // 假设 `http` crate 和 `Request` 类型存在
// // use http::Request;
// // impl std::fmt::Display for Request<()> {
// // // ...
// // }
// 原因:`Display` (来自 std) 和 `Request` (来自 http crate) 都不是在 `my_crate` 中定义的。
如何处理孤儿规则的限制?
当你确实需要为一个外部类型实现一个外部 Trait 时(例如,你想让一个来自第三方库的类型能够使用另一个第三方库的 Trait 功能),标准的做法是使用 Newtype 模式:
- 在你的 Crate 中定义一个新的结构体(Newtype),这个结构体包装了你想要操作的外部类型。
- 因为这个 Newtype 是你本地定义的类型,所以你可以为它实现任何外部 Trait。
rust
// my_crate/src/lib.rs
// // 为了使示例可独立运行,我们在此处定义它们
// // 在真实场景中,它们会从外部 crates导入
// // 模拟外部 crate 和类型/trait
pub mod some_external_crate {
#[derive(Debug)] // 添加 Debug 以便打印
pub struct ExternalType(pub String);
impl ExternalType {
pub fn new(s: &str) -> Self { ExternalType(s.to_string()) }
}
}
pub mod another_external_crate {
pub trait ExternalTrait {
fn perform_action(&self) -> String;
}
}
// --- 结束模拟 ---
use some_external_crate::ExternalType;
use another_external_crate::ExternalTrait;
// 定义一个本地的 Newtype,包装外部类型 ExternalType
pub struct MyWrapper(pub ExternalType);
// 现在我们可以为本地的 MyWrapper 实现外部 Trait ExternalTrait
// 这是允许的,因为 MyWrapper 是本地类型
impl ExternalTrait for MyWrapper {
fn perform_action(&self) -> String {
// 我们可以访问包装的类型 self.0
format!('''MyWrapper performing action on: {:?}''', self.0)
}
}
// 也可以为 MyWrapper 实现标准库的 Trait,如 Display
impl std::fmt::Display for MyWrapper {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "MyWrapper({})", self.0.0) // 假设 ExternalType.0 是可访问的
}
}
/* // 示例用法,如果这是一个 main.rs 或者测试
fn main() {
let external_val = ExternalType::new("hello from external");
let wrapped_val = MyWrapper(external_val);
println!("{}", wrapped_val.perform_action()); // 使用 ExternalTrait 的方法
println!("Display: {}", wrapped_val); // 使用 Display trait
}
*/
通过这种方式,你可以在不违反孤儿规则的前提下,扩展外部类型的行为。
孤儿规则是 Rust 实现其“无畏并发”和强大生态系统组合性的基石之一。理解并遵守它对于编写健壮、可维护的 Rust 代码至关重要。
参考链接:
- The Rust Programming Language Book: Traits - Defining Shared Behavior (Implementing a Trait on a Type) (其中提到了相干性规则)
- Rust RFC 0115 - Orphan Impls (虽然是早期的 RFC,但有助于理解背景)
- Rust API Guidelines: Trait Implementation Coherence (C-COHERENCE)
Sized
与 ?Sized
:理解 Rust 中的大小类型
在 Rust 中,类型的大小是一个核心概念,它影响着数据如何在内存中存储和传递。Sized
trait 和 ?Sized
标记是 Rust 类型系统中用于处理类型大小的重要工具。
Sized
Trait
- 定义:
Sized
是一个特殊的编译器内置 trait。如果一个类型T
实现了Sized
trait (写作T: Sized
),意味着该类型在编译时其大小是已知的。 - 普遍性:绝大多数 Rust 类型都是
Sized
的。例如,i32
(4 字节)、bool
(1 字节)、[u8; 10]
(10 字节)、自定义的struct Point { x: f64, y: f64 }
(通常 16 字节) 等。 - 默认约束:在泛型编程中,Rust 默认会对所有泛型参数
T
添加Sized
约束。也就是说,当你写fn foo<T>(arg: T)
时,它实际上被编译器理解为fn foo<T: Sized>(arg: T)
。这是因为 Rust 通常需要知道类型的大小才能在栈上分配空间、进行值传递等操作。
动态大小类型 (Dynamically Sized Types - DSTs)
与 Sized
类型相对的是动态大小类型(DSTs)。这些类型的大小在编译时是未知的,只有在运行时才能确定。
- 常见的 DSTs:
- 切片 (Slices):如
&[T]
(数组切片) 或&str
(字符串切片)。切片本身是一个“胖指针”,包含一个指向数据的指针和一个长度信息。数据部分的长度是动态的。 - Trait 对象 (Trait Objects):如
&dyn MyTrait
或Box<dyn MyTrait>
。Trait 对象允许你在运行时使用不同具体类型的实例,只要它们都实现了指定的 trait。Trait 对象也是一个“胖指针”,包含一个指向实例数据的指针和一个指向虚函数表(vtable)的指针。
- 切片 (Slices):如
- DSTs 的限制:
- 不能直接在栈上创建 DST 实例,因为编译器不知道要分配多少空间。
- 不能直接按值传递或返回 DSTs。
- 结构体中最后一个字段可以是 DST,但这样的结构体本身也会变成 DST。
?Sized
标记 (Maybe Sized / Potentially Unsized)
?Sized
(读作 "T may be Sized" 或 "T is potentially unsized") 用于放宽泛型参数默认的 Sized
约束。
- 语法:
T: ?Sized
表示类型T
可能是Sized
的,也可能不是Sized
的(即可能是 DST)。 - 用途:当你希望泛型代码能够处理 DSTs 时,就需要使用
?Sized
。这通常涉及到指针类型,因为 DSTs 必须通过某种形式的指针(如引用&
、Box<T>
、Rc<T>
等)来间接操作。
示例与解释
rust
// 1. Sized 类型 (默认)
fn process_sized<T>(data: T) { // 隐式约束 T: Sized
// 可以在栈上操作 data,因为大小已知
let _x = data;
}
// 2. 使用 ?Sized 处理 DSTs
// 这个函数可以接受 &[i32] (DST) 或 [i32; 3] (Sized) 的引用
fn print_slice_info<T: ?Sized + std::fmt::Debug>(slice_ref: &T) {
// 注意:我们操作的是引用 &T,而不是 T 本身
// 因为 T 可能是 DST,不能直接在栈上创建或按值传递
println!("Slice info: {:?}", slice_ref);
}
// 3. 结构体与 ?Sized
// 结构体 MyStruct<T> 可以持有 Sized 或 DST 类型 T
// 但如果 T 是 DST,MyStruct<T> 本身也变成 DST
// 并且 DST 字段必须是最后一个字段
struct MyStruct<T: ?Sized> {
info: String, // Sized 字段
data: T, // 可能是 DST
}
fn main() {
// Sized 类型
process_sized(10i32);
process_sized([1, 2, 3]);
// DST 示例: &[i32] 和 &str
let numbers = [1, 2, 3, 4, 5];
let slice_numbers: &[i32] = &numbers[1..4]; // slice_numbers 是一个 DST 的引用
let string_slice: &str = "hello"; // string_slice 也是一个 DST 的引用
print_slice_info(slice_numbers); // 传递 &[i32]
print_slice_info(string_slice); // 传递 &str
print_slice_info(&[7,8,9]); // 也可以传递 Sized 类型的引用
// 使用包含 DST 的结构体 (必须通过指针,如 Box)
let sized_struct = MyStruct { info: "Sized data".to_string(), data: 100u32 };
// let unsized_struct_direct = MyStruct { info: "Unsized".to_string(), data: *slice_numbers }; // 编译错误:不能直接创建包含 DST 的结构体实例
// 要创建包含 DST 的结构体实例,通常需要 Box<T>
// 首先,创建一个 Box<[i32]>
let boxed_slice: Box<[i32]> = vec![10, 20, 30].into_boxed_slice();
let unsized_struct_boxed = MyStruct {
info: "Boxed DST data".to_string(),
data: *boxed_slice, // 注意:这里实际上是将 Box<[i32]> 的内容(一个 [i32] DST)“放”到结构体中
// 这使得 MyStruct { data: [i32] } 本身成为 DST
};
// 因此,我们需要将整个 MyStruct<[i32]> 实例也放到 Box 中
let boxed_unsized_struct: Box<MyStruct<[i32]>> = Box::new(MyStruct {
info: "Boxed MyStruct with DST".to_string(),
data: *vec![1,2,3].into_boxed_slice(), // data: [i32]
});
println!("Boxed struct info: {}", boxed_unsized_struct.info);
// println!("{:?}", boxed_unsized_struct.data); // 直接访问 MyStruct<[i32]> 中的 data 字段比较复杂
// Trait 对象也是 DST
let my_displayable_int: Box<dyn std::fmt::Display> = Box::new(123);
let my_displayable_str: Box<dyn std::fmt::Display> = Box::new("Rust");
// print_slice_info(&*my_displayable_int); // 编译错误,dyn Display 没有实现 Debug
// 需要 T: ?Sized + std::fmt::Debug
}
关键点总结:
Sized
是默认:大多数类型大小已知,泛型默认要求Sized
。- DSTs 大小未知:
[T]
和str
(作为切片类型,而不是String
) 以及dyn Trait
是主要的 DSTs。 - DSTs 通过指针操作:由于大小未知,DSTs 总是通过指针(
&T
,Box<T>
,Rc<T>
等)来使用。 ?Sized
放宽约束:允许泛型代码接受Sized
或 DST 类型,通常用于处理指针后的数据。- 结构体与 DSTs:一个结构体可以包含一个 DST 作为其最后一个字段,但这会使该结构体本身也成为 DST。这样的结构体实例也必须通过指针(如
Box
)来创建和操作。
理解 Sized
和 ?Sized
对于编写灵活且能与 Rust 核心抽象(如切片和 trait 对象)交互的泛型代码至关重要。它也揭示了 Rust 如何在保证内存安全和性能的同时,提供处理动态大小数据的能力。
何时避免 ?Sized
?
正如原始文本中提到的,如果你的代码逻辑依赖于知道类型在编译时的确切大小(例如,在数组中存储多个该类型的实例,或在栈上直接创建实例),那么就不应该使用 ?Sized
,而应坚持使用默认的 Sized
约束。