Appearance
一些基础概念
Turbo-Fish
Turbo-Fish 是 Rust 语言的一个自定义语法扩展,在泛型函数或方法调用时,显式指定泛型类型参数的一种语法规则,使用双冒号(::
)和尖括号(<>
)组合在一起,例如:
rust
let x = "hello".to_string();
// 使用泛型类型
let y = Vec::<i32>::new();
这种写法被称为Turbo-Fish,因为它看起来像一条鱼的轮廓(::<>)。这种语法通常用于帮助编译器确定泛型类型或避免二义性。
元组无法迭代
元组(Tuple)是不同类型的值的固定大小集合,也称为异构(Heterogeneous)。而 Rust 的迭代器实现通常被认为是同态的(Homogeneous),即它们针对同一种类型的元素进行迭代。
根据 Rust 标准库文档,标准库中的 std::iter::Iterator
trait 可以用于定义一种迭代器,该迭代器可以按顺序遍历某个数据结构中的所有元素,并且具有类似指针的特性。因此,在处理一个含有多种类型元素的元组时,由于不同类型元素所占字节、排列方式等不同,导致统一的迭代器遍历起来具有很大的难度和不确定性。
- 什么场景下会用到元组这种数据结构呢?
函数要返回多个变量时,调用返回时直接解构,很方便 比如 返回(res, err)
这类。
- 元组为什么不通过
[]
访问而是通过.
呢?
区别于数组。并且本身跟数组有区别,数组元素类型一致,意味着存储空间大小一致,移动指针容易计算。元组各元素类型不一致,访问时指针位置要计算。
rust 中复杂类型都是存储在堆上吗?
Rust 中的复杂类型可以存储在堆上或栈上,具体取决于该类型的大小和其生命周期。
当变量的大小在编译时是已知并且不超过一个固定大小(通常为 16 或 32 字节)时,它们会被分配在栈上。例如,整数、浮点数和较小的结构体都可以在栈上进行分配,这通常比在堆上分配要更快并且不需要执行显式内存管理操作(如清理)。
然而,如果变量的大小在编译时未知或可能超过固定大小,则需要使用堆来分配内存。例如,字符串、向量和其他动态类型都使用堆来存储数据。这些类型的大小不能在编译时确定,并且它们的生命周期可能超出当前函数作用域。
此外,Rust 还支持将某些复杂类型存储在栈上或堆上。例如,在 Rust 中,结构体可以包含堆分配和栈分配的字段,这意味着结构体可能会很大,并且只有一部分内存被存储在堆上。
综上所述,Rust 中的复杂类型可以存储在栈上或堆上。根据类型的大小和生命周期,Rust 编译器在编译时决定将其分配到哪个区域。
#![...]
和#[...]
区别
在 Rust 中,#![...]
和 #[...]
都是属性宏的使用方式,但它们的作用范围不同。
#![...]
是一个 crate 属性,它用于设置整个 crate (包含多个模块的 Rust 项目)的元数据信息、编译选项和其他一些指令。这些属性会被编译器读取并对整个 crate 进行配置,而不仅仅是某个单独的模块或函数。
rust
#![allow(dead_code)]
#![allow(unused_variables)]
#![feature(proc_macro_hygiene)]
举个例子,以下代码片段展示了如何使用 #![feature(some_feature)]
设置一个编译时特性,以启用某些实验性的 Rust 特性:
rust
#![feature(some_feature)]
fn main() {
// 使用实验性特性...
}
在上述代码中,#![feature(some_feature)]
声明了一个特性,让编译器知道这个 crate 中使用了某些实验性特性,并在编译时将其启用。这个特性会影响整个 crate的编译过程,而不仅仅是 main
函数。 通常将这些属性写在项目的根模块(src/lib.rs
或 src/main.rs
)的开头。
#[...]
是一个 item 级别的属性,它只影响当前定义的函数、结构体、枚举、trait 等实体。这些属性同样可以用于控制编译器的行为或者告诉编译器一些信息,例如:
rust
#[derive(Debug)]
pub struct Person {
name: String,
age: u8,
}
fn main() {
let student = Person {
name: "ChandlerVer5".to_string(),
age: 31,
};
println!("{:?}", student); // 输出 Person { name: "ChandlerVer5", age: 31 }
}
这里的 #[derive(Debug)]
属性告诉编译器为 Person
结构体实现 Debug trait,允许我们使用 println!
宏输出结构体的内容。
总之,#![...]
用于给整个 crate 设置属性,#[...]
用于给一个 Rust 项设置属性。
Orphan Rule(孤儿规则)
Rust 规定了一个 Coherence Rule(一致性规则),也被称为 Orphan Rule(孤儿规则)。这个规则是 Rust 编译器用于检查和保证类型安全的重要机制。
在 Implementing a Trait on a Type 中有提及。 这个规则避免了两个库对同一个类型实现相同的 trait,从而导致冲突和不可预测的行为。
具体来说,Coherence Rule 有以下几个方面的限制和约束:
- 在同一个 crate 内部,所有实现同一个 Trait 的类型必须唯一。
- 如果一个 Trait 是在外部的 crate 中定义的,则不能在当前 crate 中为该 Trait 实现某个类型。否则,会导致两个库同时实现了同一个 Trait,编译器无法判断使用哪一个实现。
- 如果两个 crate 都为同一个类型实现了相同的 Trait,那么这两个 crate 必须显式地指定使用哪个实现。否则,编译器也无法判断使用哪一个实现。
总之,Coherence Rule 确保了 Trait 和其实现之间的关系是唯一的、确定的,从而让 Rust 编译器能够在编译时进行类型检查和类型推导,并在运行时保证代码的安全性和正确性。
下面是一个简单的示例:
假设我们有两个 crate:crate A 和 crate B。crate A 定义了一个名为 MyTrait
的 Trait,crate B 中定义了一个名为 MyType
的类型,并为其实现了 MyTrait
。现在,我们希望在 crate A 中使用 MyType
类型和 MyTrait
Trait。
首先,我们需要在 crate A 的 Cargo.toml
文件中添加以下依赖项:
toml
[dependencies]
crate_b = { version = "0.1", path = "../path/to/crate_b" }
然后,在 crate A 的代码中,我们可以这样调用 MyType
类型和 MyTrait
Trait:
rust
use crate_b::MyType;
pub trait MyTrait {
fn my_function(&self);
}
impl MyTrait for MyType {
fn my_function(&self) {
println!("Hello, world!");
}
}
在这里,我们首先通过 use
语句引入了 crate B 中定义的 MyType
类型。然后,在 MyTrait
Trait 的实现中,我们为 MyType
类型实现了该 Trait。
需要注意的是,如果我们在 crate A 中为 MyType
类型实现了 MyTrait
Trait,而在 crate B 中也为其实现了同一个 Trait,那么就会触发 Coherence Rule 的限制,导致编译错误。为了解决这个问题,我们需要在代码中显式地指定使用哪一个实现,如下所示:
rust
use crate_b::MyType;
pub trait MyTrait {
fn my_function(&self);
}
impl MyTrait for MyType {
fn my_function(&self) {
println!("Hello from crate B!");
}
}
impl MyTrait for crate_a::MyType {
fn my_function(&self) {
println!("Hello from crate A!");
}
}
在这里,我们为 MyType
类型分别实现了两个不同的 MyTrait
Trait 实现,分别输出不同的字符串。然后,在使用时,我们需要显式地指定使用哪个实现:
rust
use crate_a::MyType;
use crate_a::MyTrait;
fn main() {
let x = MyType;
crate_b::MyTrait::my_function(&x); // 输出:Hello from crate B!
MyTrait::my_function(&x); // 输出:Hello from crate A!
}
在这里,我们在调用 my_function
方法时,分别使用了 crate_b::MyTrait::my_function
和 MyTrait::my_function
两个不同的 Trait 实现。
如果您遇到需要绕过孤儿规则限制的情况,则可以使用 newtype 模式。
Sized
与 ?Sized
Sized
是一个 Rust trait,用于表示编译时已知类型的大小。所有的 Rust 类型都必须实现此 trait,因为在编译时 Rust 需要知道每个类型占用的内存大小以便合理地分配内存。
你可以使用 Sized
trait 来指定函数参数或者某些泛型的限制条件。例如,fn foo<T: Sized>(arg: T)
表示 arg
参数必须是一个大小已知的类型。
因为大多数类型都是大小已知的,所以在 Rust 中 Sized
trait 很常见,并且通常不需要显式地指明。
在 Rust 中,?Sized
是一种泛型类型限制语法,表示一个类型可以是 Sized
的也可以是动态大小的(即不确定大小的)。它通常用于那些需要支持动态大小类型的代码中,例如定义 trait 或者实现类型参数。
在 Rust 中,如果将一个类型的大小称为静态大小,那么动态大小类型就指那些只能在运行时才能确定其大小的类型。通常来说,这类类型都是通过动态内存分配而来的,例如 Vec<T>
,Box<T>
等等。
带有 ?Sized
后缀的泛型类型参数则表示该类型可以是静态大小的,也可以是动态大小的。例如:
rust
trait MyTrait<T: ?Sized> {
fn my_method(&self, arg: &T);
}
struct MyStruct<T: ?Sized> {
data: T,
}
impl<T: ?Sized> MyTrait<T> for MyStruct<T> {
fn my_method(&self, arg: &T) {
// ...
}
}
fn main() {
let x = MyStruct { data: "hello" };
let y = MyStruct { data: vec![1, 2, 3] };
x.my_method(&"world");
y.my_method(&vec![4, 5, 6]);
}
在这个示例程序中,我们定义了一个泛型 trait MyTrait
,它的泛型类型参数 T
带有 ?Sized
后缀表示可以是动态大小类型。 然后我们定义了一个结构体 MyStruct
,它的泛型类型参数 T
同样带有 ?Sized
后缀,表示可以是静态大小类型或动态大小类型。 最后我们通过实现 MyTrait
trait 来给 MyStruct
类型添加一个方法,这个方法接受一个实现了泛型类型 T
的引用参数,并对其进行处理。
在 main
函数中,我们分别创建了两个 MyStruct
对象,一个包含字符串数据,另一个包含整数数组数据。我们对这两个对象调用了 my_method
方法,传递了不同类型的参数。由于 MyTrait
的泛型类型参数 T
带有 ?Sized
后缀,因此可以接受动态大小类型的参数。
注意,在使用 ?Sized
语法时需谨慎,因为一些操作是只能在静态大小类型上执行的,比如直接访问数组元素、对类型进行栈分配等等。
如果你的代码需要明确地知道类型的大小,请避免使用 ?Sized
语法。