RUST 中常用部分学习结束之后,我们来接触一些 RUST 中的其他高级用法。
- 不安全 Rust:用于当需要舍弃 Rust 的某些保证并负责手动维持这些保证
- 高级 trait:与 trait 相关的关联类型,默认类型参数,完全限定语法(fully qualified syntax),超(父)trait(supertraits)和
newtype
模式 - 高级类型:关于
newtype
模式的更多内容,类型别名,never
类型和动态大小类型 - 高级函数和闭包:函数指针和返回闭包
- 宏:定义在编译时定义更多代码的方式
一、不安全 Rust
不安全 Rust 之所以存在,是因为静态分析本质上是保守的。当编译器尝试确定一段代码是否支持某个保证时,拒绝一些合法的程序比接受无效的程序要好一些。这必然意味着有时代码可能是合法的,但如果 Rust 编译器没有足够的信息来确定,它将拒绝该代码。使用不安全 Rust 风险自担:如果不安全代码出错了,比如解引用空指针,可能会导致不安全的内存使用。
另一个 Rust 存在不安全一面的原因是:底层计算机硬件固有的不安全性。如果 Rust 不允许进行不安全操作,那么有些任务则根本完成不了。Rust 需要能够进行像直接与操作系统交互,甚至于编写你自己的操作系统这样的底层系统编程!这也是 Rust 语言的目标之一。
可以通过 unsafe
关键字来切换到不安全 Rust,接着可以开启一个新的存放不安全代码的块。这里有五类可以在不安全 Rust 中进行而不能用于安全 Rust 的操作,它们称之为 “不安全的超能力(unsafe superpowers)” 。这些超能力是:
- 解引用裸指针
- 调用不安全的函数或方法
- 访问或修改可变静态变量
- 实现不安全 trait
- 访问
union
的字段
有一点很重要,unsafe
并不会关闭借用检查器或禁用任何其他 Rust 安全检查:如果在不安全代码中使用引用,它仍会被检查。unsafe
关键字只是提供了那五个不会被编译器检查内存安全的功能。你仍然能在不安全块中获得某种程度的安全。
再者,unsafe
不意味着块中的代码就一定是危险的或者必然导致内存安全问题:其意图在于作为程序员你将会确保 unsafe
块中的代码以有效的方式访问内存。
人是会犯错误的,错误总会发生,不过通过要求这五类操作必须位于标记为 unsafe
的块中,就能够知道任何与内存安全相关的错误必定位于 unsafe
块内。保持 unsafe
块尽可能小,如此当之后调查内存 bug 时就会感谢你自己了。
为了尽可能隔离不安全代码,将不安全代码封装进一个安全的抽象并提供安全 API 是一个好主意。标准库的一部分被实现为在被评审过的不安全代码之上的安全抽象。这个技术防止了 unsafe
泄露到所有你或者用户希望使用由 unsafe
代码实现的功能的地方,因为使用其安全抽象是安全的。
1.1 解引用裸指针
不安全 Rust 有两个被称为裸指针(raw pointers)的类似于引用的新类型。和引用一样,裸指针是不可变或可变的,分别写作 *const T
和 *mut T
。在裸指针的上下文中,不可变意味着指针解引用之后不能直接赋值。
裸指针与引用和智能指针的区别在于:
- 允许忽略借用规则,可以同时拥有不可变和可变的指针,或多个指向相同位置的可变指针
- 不保证指向有效的内存
- 允许为空
- 不能实现任何自动清理功能
通过去掉 Rust 强加的保证,你可以放弃安全保证以换取性能或使用另一个语言或硬件接口的能力,此时 Rust 的保证并不适用。
以下是使用裸指针解引用的示例代码:
fn main() {
let mut num = 42;
let ptr: *mut i32 = &mut num as *mut i32;
unsafe {
// 解引用裸指针
*ptr = 100;
}
println!("num: {}", num);
}
-
定义变量:
let mut num = 42;
这里定义了一个可变整数变量 num
并初始化为 42。
-
创建裸指针:
let ptr: *mut i32 = &mut num as *mut i32;
这里将 num
的可变引用转换为一个可变裸指针 *mut i32
。as
关键字用于类型转换。
-
使用
unsafe
块:unsafe { // 解引用裸指针 *ptr = 100; }
- Rust 强制要求在解引用裸指针时必须使用
unsafe
块。这是因为裸指针绕过了 Rust 的内存安全保证,可能会引发未定义行为。 - 在
unsafe
块中,可以使用解引用操作符*
来解引用裸指针。这里将裸指针ptr
解引用并赋值为 100。
-
输出结果:
println!("num: {}", num);
输出变量 num
的值,此时它已经被修改为 100。
注意事项
- 类型安全:Rust 的裸指针不提供类型安全保证。如果错误地使用裸指针,可能会访问到错误的内存地址,导致程序崩溃或数据损坏。
- 内存安全:使用裸指针时,必须确保指针是有效的,并且不会超出其指向的内存范围。否则,可能会访问到未初始化或已释放的内存。
- 生命周期:在使用裸指针时,还需要考虑指针的生命周期。确保指针在解引用时指向的内存是有效的,并且不会被其他部分的代码释放或修改。
总之,尽管裸指针提供了底层的内存操作能力,但它们也带来了更高的风险。在实际开发中,除非有充分的理由,否则应尽量避免使用裸指针。
1.2 调用不安全的函数或方法
不安全函数和方法与常规函数方法十分类似,除了其开头有一个额外的 unsafe
。通过在 unsafe
块中调用不安全函数,表明我们已经阅读过此函数的文档并对其是否满足函数自身的契约负责。
使用不安全的 Rust 内建函数
Rust标准库中有一些标记为 unsafe
的函数,因为它们可能会违反Rust的安全原则。例如,std::slice::from_raw_parts
函数可以创建一个 slice
,但是调用者必须保证这个 slice
不违反内存安全规则。
use std::slice;
fn main() {
let data = [1, 2, 3, 4, 5];
let data_ptr = data.as_ptr();
let length = data.len();
unsafe {
let slice = slice::from_raw_parts(data_ptr, length);
println!("{:?}", slice); // 输出:[1, 2, 3, 4, 5]
}
}
在 main
函数中,我们创建了一个数组 data
,并获取了它的指针 data_ptr
和长度 length
。在 unsafe
代码块中,我们使用 slice::from_raw_parts
来创建一个 slice
,这个 slice
指向原始数组的数据。由于我们完全控制了这个 slice
的生命周期和它所指向的数据,所以我们可以安全地使用它。
调用 C 语言函数
Rust 允许与 C 语言进行互操作,但是调用C语言函数需要使用 extern "C"
声明,并且在调用时需要使用 unsafe
。
extern "C" {
fn c_add(a: i32, b: i32) -> i32;
}
fn main() {
let result = unsafe { c_add(5, 7) };
println!("Result of C function: {}", result);
}
在这个例子中,我们声明了一个 C 语言函数 c_add
,它接受两个 i32
参数并返回它们的和。在 main
函数中,我们使用 unsafe
块来调用这个C函数,并打印结果。由于 C 语言不保证 Rust 的内存安全原则,所以调用 C 函数需要使用 unsafe
。
1.3 访问或修改可变静态变量
在 Rust 中,静态变量是全局的,并且它们在程序的生命周期内一直存在。静态变量默认是不可变的,因为它们可以被程序的任何部分访问,而全局可变状态可能导致数据竞争和其他并发问题。然而,有时候出于性能或其他原因,我们可能需要一个可变的静态变量。在这种情况下,我们可以使用 unsafe
代码来实现。
以下是一个使用 unsafe
来创建和修改可变静态变量的例子:
use std::sync::atomic::{AtomicI32, Ordering};
// 创建一个可变的静态变量,使用Atomic类型来保证线程安全
static GLOBAL_COUNTER: AtomicI32 = AtomicI32::new(0);
// 一个不安全的函数,用于增加全局计数器的值
unsafe fn increment_global_counter() {
GLOBAL_COUNTER.fetch_add(1, Ordering::SeqCst);
}
// 安全的函数,用于获取全局计数器的当前值
fn get_global_counter() -> i32 {
unsafe { GLOBAL_COUNTER.load(Ordering::SeqCst) }
}
fn main() {
// 使用不安全的代码块来调用increment_global_counter函数
unsafe {
increment_global_counter();
increment_global_counter();
}
// 获取并打印全局计数器的值
let counter_value = get_global_counter();
println!("Global counter value: {}", counter_value);
}
-
GLOBAL_COUNTER
是一个使用AtomicI32
类型的可变静态变量。AtomicI32
提供了原子操作,可以安全地在多线程环境中使用,从而避免了数据竞争。 -
increment_global_counter
函数是一个unsafe
函数,因为它修改了全局状态。它使用fetch_add
方法来原子地增加GLOBAL_COUNTER
的值。 -
get_global_counter
是一个安全的函数,它使用load
方法来获取GLOBAL_COUNTER
的当前值。尽管读取操作本身是安全的,但由于它依赖于GLOBAL_COUNTER
,我们仍然在unsafe
代码块中调用它。 -
在
main
函数中,我们使用unsafe
代码块来调用increment_global_counter
函数,以增加全局计数器的值。然后,我们调用get_global_counter
函数来获取并打印计数器的值。
1.4 实现不安全 trait
unsafe
的另一个操作用例是实现不安全 trait。
以下是一个更复杂的示例,展示如何实现一个涉及原始指针操作的不安全的 trait:
trait MyTrait {
unsafe fn process(&self, data: *const i32);
}
struct MyStruct { }
impl MyTrait for MyStruct {
unsafe fn process(&self, data: *const i32) {
let value = *data;
println!("Processed value: {}", value);
}
}
fn main() {
let my_struct = MyStruct { };
let data = 10;
unsafe {
my_struct.process(&data as *const i32);
}
}
-
定义了一个 trait
MyTrait
,它有一个方法process
,这个方法接受一个*const i32
类型的参数。 -
定义了一个结构体
MyStruct
。 -
为
MyStruct
实现了MyTrait
trait。在实现中,process
方法被标记为unsafe
,因为它涉及到对原始指针的解引用操作。 -
在
main
函数中,我们创建了一个MyStruct
实例my_struct
和一个i32
类型的整数data
。然后,我们使用unsafe
代码块来调用process
方法,并传入data
的地址。
1.5 访问 union
的字段
联合体(Union)是一种特殊的数据结构,它允许在同一内存位置存储不同的数据类型。由于联合体的字段共享相同的内存位置,访问其中一个字段可能会覆盖其他字段,因此访问联合体的字段通常是不安全的。为了访问联合体中的字段,你需要使用 unsafe
代码块。
以下是一个访问联合体字段的示例:
union MyUnion {
i: i32,
f: f32,
}
fn main() {
// 创建一个MyUnion实例,并初始化为i32类型
let mut my_union = MyUnion { i: 10 };
// 使用unsafe代码块访问联合体中的i32字段
unsafe {
println!("i32 value: {}", my_union.i);
}
// 改变联合体的类型为f32
my_union.f = 3.14;
// 使用unsafe代码块访问联合体中的f32字段
unsafe {
println!("f32 value: {}", my_union.f);
}
}
运行结果
i32 value: 10
f32 value: 3.14
-
定义了一个名为
MyUnion
的联合体,它有两个字段:i
(类型为i32
)和f
(类型为f32
)。 -
在
main
函数中,我们创建了一个MyUnion
实例my_union
,并将其初始化为一个i32
类型的值(10)。 -
使用
unsafe
代码块来访问my_union
中的i
字段。由于访问联合体的字段可能会覆盖其他字段,因此我们需要使用unsafe
。 -
改变
my_union
的类型为f32
,并将f
字段的值设置为 3.14。注意,这一步实际上是不合法的,因为 Rust 不允许在不使用unsafe
的情况下改变联合体的类型。 -
再次使用
unsafe
代码块来访问my_union
中的f
字段。
二、高级 trait
2.1 关联类型在 trait 定义中指定占位符类型
关联类型(associated types)是一个将类型占位符与 trait 相关联的方式,这样 trait 的方法签名中就可以使用这些占位符类型。trait 的实现者会针对特定的实现在这个占位符类型指定相应的具体类型。如此可以定义一个使用多种类型的 trait,直到实现此 trait 时都无需知道这些类型具体是什么。
trait Iterator {
// 定义一个关联类型,用于迭代器返回的元素类型
type Item;
// 定义一个方法,返回迭代器的下一个元素
fn next(&mut self) -> Option<Self::Item>;
}
// 实现 Iterator trait 的具体类型
struct Counter {
count: i32,
}
// 为 Counter 实现 Iterator trait
impl Iterator for Counter {
// 指定 Item 的具体类型为 i32
type Item = i32;
fn next(&mut self) -> Option<Self::Item> {
if self.count < 5 {
let result = Some(self.count);
self.count += 1;
result
} else {
None
}
}
}
fn main() {
let mut counter = Counter { count: 0 };
while let Some(x) = counter.next() {
println!("{}", x);
}
}
运行结果
0
1
2
3
4
在这个例子中:
Iterator
trait 定义了一个关联类型Item
,这个类型用于指定迭代器返回的元素类型。Counter
结构体实现了Iterator
trait,指定Item
类型为i32
。next
方法返回一个Option<Self::Item>
,其中Self::Item
就是Counter
实现中指定的i32
类型。
这样,Iterator
trait 就可以被不同的类型以不同的元素类型实现,增加了代码的复用性和灵活性。
2.2 默认泛型类型参数和运算符重载
当使用泛型类型参数时,可以为泛型指定一个默认的具体类型。如果默认类型就足够的话,这消除了为具体类型实现 trait 的需要。为泛型类型指定默认类型的语法是在声明泛型类型时使用 <PlaceholderType=ConcreteType>
。
这种情况的一个非常好的例子是使用运算符重载(Operator overloading),这是指在特定情况下自定义运算符(比如 +)行为的操作。
Rust 并不允许创建自定义运算符或重载任意运算符,不过 std::ops
中所列出的运算符和相应的 trait 可以通过实现运算符相关 trait 来重载。
默认泛型类型参数
在 Rust 中,你可以为泛型类型参数设置默认类型,这样当调用者没有指定类型参数时,编译器会自动使用默认类型。
struct MyContainer<T = i32> {
value: T,
}
fn main() {
let int_container = MyContainer { value: 10 };
let string_container = MyContainer { value: "Hello" };
println!("int_container value: {}", int_container.value);
println!("string_container value: {}", string_container.value);
}
在这个例子中,MyContainer
结构体有一个泛型类型参数 T
,默认类型为 i32
。因此,当你创建 int_container
时,不需要显式指定类型参数,它会自动使用默认的 i32
类型。
运算符重载
Rust 允许你为自定义类型重载运算符,但需要显式定义运算符函数。以下是一些示例:
- 重载
==
运算符:
#[derive(Debug)]
struct Point {
x: i32,
y: i32,
}
impl PartialEq for Point {
fn eq(&self, other: &Self) -> bool {
self.x == other.x && self.y == other.y
}
}
fn main() {
let p1 = Point { x: 1, y: 2 };
let p2 = Point { x: 1, y: 2 };
let p3 = Point { x: 3, y: 4 };
println!("p1 == p2: {}", p1 == p2); // 输出 true
println!("p1 == p3: {}", p1 == p3); // 输出 false
}
- 重载
+
运算符:
#[derive(Debug)]
struct Point {
x: i32,
y: i32,
}
impl std::ops::Add for Point {
type Output = Self;
fn add(self, other: Self) -> Self::Output {
Point {
x: self.x + other.x,
y: self.y + other.y,
}
}
}
fn main() {
let p1 = Point { x: 1, y: 2 };
let p2 = Point { x: 3, y: 4 };
let p3 = p1 + p2;
println!("p3: {:?}", p3); // 输出 Point { x: 4, y: 6 }
}
- 重载
*
运算符:
#[derive(Debug)]
struct Point {
x: i32,
y: i32,
}
impl std::ops::Mul<i32> for Point {
type Output = Self;
fn mul(self, scalar: i32) -> Self::Output {
Point {
x: self.x * scalar,
y: self.y * scalar,
}
}
}
fn main() {
let p1 = Point { x: 1, y: 2 };
let p2 = p1 * 3;
println!("p2: {:?}", p2); // 输出 Point { x: 3, y: 6 }
}
2.3 完全限定语法与消歧义:调用相同名称的方法
在 Rust 中,当你需要调用实现了某个特质(trait)的类型的关联函数时,可以使用完全限定语法 <Type as Trait>::function(receiver_if_method, next_arg, ...)
。这种语法特别有用在处理多态和特质实现时。
假设我们有一个特质 Print
和两个实现了这个特质的类型 Person
和 Message
。
trait Print {
fn print(&self);
}
struct Person {
name: String,
}
impl Print for Person {
fn print(&self) {
println!("Person: {}", self.name);
}
}
struct Message {
content: String,
}
impl Print for Message {
fn print(&self) {
println!("Message: {}", self.content);
}
}
fn main() {
let person = Person { name: "Alice".to_string() };
let message = Message { content: "Hello".to_string() };
// 使用完全限定语法调用 Person 的 print 方法
<Person as Print>::print(&person);
// 使用完全限定语法调用 Message 的 print 方法
<Message as Print>::print(&message);
}
在这个例子中,我们通过 <Type as Trait>::function(receiver_if_method, ...)
的方式调用了 Person
和 Message
的 print
方法。
2.4 父 trait 用于在另一个 trait 中使用某 trait 的功能
有时我们可能会需要编写一个依赖另一个 trait 的 trait 定义:对于一个实现了第一个 trait 的类型,你希望要求这个类型也实现了第二个 trait。如此就可使 trait 定义使用第二个 trait 的关联项。这个所需的 trait 是我们实现的 trait 的父(超)trait(supertrait)。
在 Rust 中,可以使用父特质(supertrait)来确保一个特质(trait)具有另一个特质的功能。这通常用于定义一些基础行为,然后让其他特质扩展或实现这些行为。
使用父特质
假设我们有一个基础特质 Print
,它定义了一个打印方法。然后我们定义另一个特质 AdvancedPrint
,它扩展了 Print
特质,添加了一些额外的功能。
trait Print {
fn print(&self);
}
trait AdvancedPrint: Print {
fn print_with_newline(&self) {
self.print();
println!();
}
fn print_twice(&self) {
self.print();
self.print();
}
}
struct Person {
name: String,
}
impl Print for Person {
fn print(&self) {
println!("Person: {}", self.name);
}
}
impl AdvancedPrint for Person {
// 这里可以添加额外的实现,但也可以不实现,因为 AdvancedPrint 已经提供了默认实现
}
struct Message {
content: String,
}
impl Print for Message {
fn print(&self) {
println!("Message: {}", self.content);
}
}
impl AdvancedPrint for Message {
// 这里可以添加额外的实现,但也可以不实现,因为 AdvancedPrint 已经提供了默认实现
}
fn main() {
let person = Person { name: "Alice".to_string() };
let message = Message { content: "Hello".to_string() };
// 使用 AdvancedPrint 的方法
person.print_with_newline();
message.print_twice();
}
在这个例子中:
Print
是一个基础特质,定义了一个print
方法。AdvancedPrint
是一个扩展了Print
的特质,它提供了print_with_newline
和print_twice
方法的默认实现。Person
和Message
都实现了Print
特质。Person
和Message
也实现了AdvancedPrint
特质,从而获得了print_with_newline
和print_twice
方法的功能。
使用父特质确保类型安全
另一个例子是使用父特质来确保类型安全,例如,确保一个类型在实现某个特质之前必须先实现另一个特质。
trait Display {
fn display(&self);
}
trait Debug: Display {
fn debug(&self);
}
struct MyStruct {
value: i32,
}
impl Display for MyStruct {
fn display(&self) {
println!("Value: {}", self.value);
}
}
impl Debug for MyStruct {
fn debug(&self) {
println!("Debug value: {:?}", self.value);
}
}
fn main() {
let my_struct = MyStruct { value: 42 };
// 使用 Debug 的方法,隐式地也使用了 Display 的方法
my_struct.debug();
}
在这个例子中:
Display
是一个基础特质,定义了一个display
方法。Debug
是一个扩展了Display
的特质,它要求实现者必须先实现Display
特质。MyStruct
实现了Display
特质,然后才能实现Debug
特质。
2.5 newtype 模式用以在外部类型上实现外部 trait
在 Rust 中,newtype
模式是一种常见的技巧,用于创建一个新类型,这个新类型是某个现有类型的封装。这种模式特别有用在需要为一个类型实现特定的 trait,但这个类型本身并不是你控制的,或者你不想修改它的情况下。
基本示例
假设我们有一个外部类型 i32
,我们想要为它实现一个自定义的 trait Print
。我们可以通过定义一个新类型 Int
并为这个新类型实现 Print
trait 来实现这一点。
use std::fmt;
// 定义一个新类型 Int
struct Int(i32);
// 为 Int 实现 Print trait
impl fmt::Display for Int {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Int({})", self.0)
}
}
fn main() {
let num = Int(42);
println!("{}", num);
}
为什么使用 newtype
模式?
- 封装:通过创建一个新类型,可以封装原始类型并添加额外的逻辑或行为。
- 类型安全:新类型可以提供更清晰的类型安全,避免类型混淆。
- 实现 trait:可以为新类型实现特定的 trait,而不需要修改原始类型。
更复杂的示例
假设我们有一个外部库提供的类型 ExternalType
,我们想要为它实现一些额外的逻辑。
use std::fmt;
// 假设 ExternalType 是一个外部库提供的类型
struct ExternalType {
value: i32,
}
// 定义一个新类型 MyType
struct MyType(ExternalType);
// 为 MyType 实现一些额外的逻辑
impl MyType {
fn new(value: i32) -> Self {
MyType(ExternalType { value })
}
fn get_value(&self) -> i32 {
self.0.value
}
}
// 为 MyType 实现 Display trait
impl fmt::Display for MyType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "MyType with value: {}", self.get_value())
}
}
fn main() {
let my_type = MyType::new(42);
println!("{}", my_type);
}
在这个例子中,MyType
是 ExternalType
的封装,我们通过 MyType
可以访问 ExternalType
的值,并且可以为 MyType
实现额外的逻辑和 trait。
三、高级类型
3.1 类型别名用来创建类型同义词
Rust 提供了声明类型别名(type alias)的能力,使用 type
关键字来给予现有类型另一个名字。
基本类型别名
type Kilometers = i32;
fn main() {
let x: Kilometers = 100;
println!("100 kilometers");
}
复杂类型别名
type Point = (i32, i32);
fn main() {
let origin: Point = (0, 0);
println!("Origin: {}, {}", origin.0, origin.1);
}
为函数定义别名
type Increment = fn(i32) -> i32;
fn add_one(x: i32) -> i32 {
x + 1
}
fn main() {
let inc: Increment = add_one;
println!("Increment: {}", inc(5));
}
为智能指针定义别名
type BoxedInt = Box<i32>;
fn main() {
let boxed_value: BoxedInt = Box::new(42);
println!("Boxed value: {}", boxed_value);
}
3.2 从不返回的 never type
Rust 有一个叫做 !
的特殊类型。在类型理论术语中,它被称为 empty type,因为它没有值。我们更倾向于称之为 never type。这个名字描述了它的作用:在函数从不返回的时候充当返回值。而从不返回的函数被称为发散函数(diverging functions)。
never type 通常用于那些会无限循环或者总是引发 panic
的函数。以下是一些使用 !
类型的示例代码:
示例 1:无限循环
fn loop_forever() -> ! {
loop {
println!("Hello, world!");
}
}
fn main() {
loop_forever();
}
在这个例子中,loop_forever
函数会无限循环,因此它返回 !
类型。
示例 2:总是引发 panic
fn always_panic() -> ! {
panic!("This function always panics!");
}
fn main() {
always_panic();
}
在这个例子中,always_panic
函数总是引发 panic,因此它也返回 !
类型。
3.3 动态大小类型和 Sized trait
Rust 需要知道有关类型的某些细节,例如为特定类型的值需要分配多少空间。这便是起初留下的一个类型系统中令人迷惑的角落:即动态大小类型(dynamically sized types)。这有时被称为 “DST” 或 “unsized types”,这些类型允许我们处理只有在运行时才知道大小的类型。
str
是一个 DST;直到运行时我们都不知道字符串有多长。因为直到运行时都不能知道其大小,也就意味着不能创建 str
类型的变量,也不能获取 str
类型的参数。考虑一下这些代码,它们不能工作:
let s1: str = "Hello there!";
let s2: str = "How's it going?";
Rust 需要知道应该为特定类型的值分配多少内存,同时所有同一类型的值必须使用相同数量的内存。如果允许编写这样的代码,也就意味着这两个 str
需要占用完全相同大小的空间,不过它们有着不同的长度。这也就是为什么不可能创建一个存放动态大小类型的变量的原因。
那么该怎么办呢?你已经知道了这种问题的答案:s1
和 s2
的类型是 &str
而不是 str
。slice 数据结构仅仅储存了开始位置和 slice 的长度。所以虽然 &T
是一个储存了 T
所在的内存位置的单个值,&str
则是 两个值:str
的地址和其长度。这样,&str
就有了一个在编译时可以知道的大小:它是 usize
长度的两倍。也就是说,我们总是知道 &str
的大小,而无论其引用的字符串是多长。这里是 Rust 中动态大小类型的常规用法:它们有一些额外的元信息来储存动态信息的大小。这引出了动态大小类型的黄金规则:必须将动态大小类型的值置于某种指针之后。
可以将 str
与所有类型的指针结合:比如 Box<str>
或 Rc<str>
。
为了处理 DST,Rust 提供了 Sized
trait 来决定一个类型的大小是否在编译时可知。这个 trait 自动为编译器在编译时就知道大小的类型实现。另外,Rust 隐式的为每一个泛型函数增加了 Sized
bound。
fn print_size<T: Sized>(x: &T) {
println!("Size of T: {}", std::mem::size_of::<T>());
}
struct Point {
x: i32,
y: i32,
}
fn main() {
let p = Point { x: 1, y: 2 };
print_size(&p);
}
在这个例子中,print_size
函数需要一个实现了 Sized
trait 的类型参数 T
,这意味着它不能接受动态大小类型。
四、高级函数和闭包
4.1 函数指针
这个技术在我们希望传递已经定义的函数而不是重新定义闭包作为参数时很有用。函数满足类型 fn
(小写的 f),不要与闭包 trait 的 Fn
相混淆。fn
被称为函数指针(function pointer)。通过函数指针允许我们使用函数作为另一个函数的参数。
基本函数指针
fn add(x: i32, y: i32) -> i32 {
x + y
}
fn multiply(x: i32, y: i32) -> i32 {
x * y
}
fn apply_operation(op: fn(i32, i32) -> i32, a: i32, b: i32) -> i32 {
op(a, b)
}
fn main() {
let result = apply_operation(add, 2, 3);
println!("Result of add: {}", result);
let result = apply_operation(multiply, 2, 3);
println!("Result of multiply: {}", result);
}
运行结果
Result of add: 5
Result of multiply: 6
在这个例子中,apply_operation
函数接受一个函数指针 op
并调用它。
使用函数指针作为返回值
fn get_operation() -> fn(i32, i32) -> i32 {
add
}
fn add(x: i32, y: i32) -> i32 {
x + y
}
fn main() {
let op = get_operation();
let result = op(2, 3);
println!("Result of operation: {}", result);
}
运行结果
Result of operation: 5
在这个例子中,get_operation
函数返回一个函数指针。
4.2 返回闭包
闭包表现为 trait,这意味着不能直接返回闭包。对于大部分需要返回 trait 的情况,可以使用实现了期望返回的 trait 的具体类型来替代函数的返回值。
fn create_adder() -> Box<dyn Fn(i32) -> i32> {
let adder = |x| x + 10;
Box::new(adder)
}
fn main() {
let adder = create_adder();
println!("Result: {}", adder(5)); // 输出 15
}
在这个例子中,create_adder
函数返回一个闭包的 Box
,这个闭包接受一个 i32
参数并返回一个 i32
结果。
五、宏
宏(Macro)指的是 Rust 中一系列的功能:使用 macro_rules!
的声明(Declarative)宏,和三种 过程(Procedural)宏:
- 自定义
#[derive]
宏在结构体和枚举上指定通过derive
属性添加的代码 - 类属性(Attribute-like)宏定义可用于任意项的自定义属性
- 类函数宏看起来像函数不过作用于作为参数传递的
token
5.1 宏和函数的区别
从根本上来说,宏是一种为写其他代码而写代码的方式,即所谓的元编程(metaprogramming)。所有的这些宏以展开的方式来生成比你所手写出的更多的代码。
元编程对于减少大量编写和维护的代码是非常有用的,它也扮演了函数扮演的角色。但宏有一些函数所没有的附加能力。
一个函数签名必须声明函数参数个数和类型。相比之下,宏能够接收不同数量的参数:用一个参数调用 println!("hello")
或用两个参数调用 println!("hello {}", name)
。而且,宏可以在编译器翻译代码前展开,例如,宏可以在一个给定类型上实现 trait。而函数则不行,因为函数是在运行时被调用,同时 trait 需要在编译时实现。
实现宏不如实现函数的一面是宏定义要比函数定义更复杂,因为你正在编写生成 Rust 代码的 Rust 代码。由于这样的间接性,宏定义通常要比函数定义更难阅读、理解以及维护。
宏和函数的最后一个重要的区别是:在一个文件里调用宏之前必须定义它,或将其引入作用域,而函数则可以在任何地方定义和调用。
5.2 使用 macro_rules! 的声明宏用于通用元编程
Rust 最常用的宏形式是声明宏(declarative macros)。它们有时也被称为 “macros by example”、“macro_rules! 宏” 或者就是 “macros”。其核心概念是,声明宏允许我们编写一些类似 Rust match 表达式的代码。宏也将一个值和包含相关代码的模式进行比较;此种情况下,该值是传递给宏的 Rust 源代码字面值,模式用于和前面提到的源代码字面值进行比较,每个模式的相关代码会替换传递给宏的代码。所有这一切都发生于编译时。
以下是一些使用 macro_rules!
的示例:
示例 1:简单的打印宏
macro_rules! print {
($val:expr) => {
println!("{}", $val);
};
}
fn main() {
print!("Hello, world!");
}
macro_rules!
定义了一个名为print
的宏。($val:expr)
是宏的模式匹配部分,$val
是一个模式变量,expr
表示它匹配任何表达式。=> { ... }
是宏的替换部分,定义了当宏被调用时生成的代码。- 在
main
函数中,调用print!("Hello, world!")
会展开为println!("Hello, world!");
。
示例 2:条件编译宏
macro_rules! my_debug {
($val:expr) => {
#[cfg(debug_assertions)]
println!("Debug: {}", $val);
};
}
fn main() {
my_debug!("Hello, debug mode!");
}
- 这个宏在调试模式下(即编译时带有
debug_assertions
标志)才会展开。 #[cfg(debug_assertions)]
是一个属性宏,仅在调试模式下包含该代码。
示例 3:泛型宏
macro_rules! my_print {
($val:expr) => {
println!("{}", $val);
};
($($val:expr),*) => {
$(
println!("{}", $val);
)*
};
}
fn main() {
my_print!("Single value");
my_print!(1, 2, 3, 4);
}
- 宏定义了两种模式:单一表达式和多个表达式。
- 第一个模式匹配单个表达式,并打印它。
- 第二个模式使用
$(...),*
匹配零个或多个表达式,并打印它们。
示例 4:使用宏生成函数
macro_rules! make_adder {
($n:expr) => {
fn adder(x: i32, y: i32) -> i32 {
x + y + $n
}
};
}
make_adder!(10);
fn main() {
let result = adder(5, 3);
println!("Result: {}", result); // 输出 18
}
make_adder!
宏接受一个表达式$n
,并生成一个名为adder
的函数。- 函数
adder
接受两个i32
参数,并返回它们的和加上$n
。 - 调用
make_adder!(10);
生成了函数adder
。
示例 5:使用宏生成多个函数
macro_rules! make_functions {
($($name:ident => $n:expr),*) => {
$(
fn $name(x: i32, y: i32) -> i32 {
x + y + $n
}
)*
};
}
make_functions! {
add5 => 5,
add10 => 10,
add15 => 15
}
fn main() {
let result = add5(2, 3);
println!("Result of add5: {}", result); // 输出 10
let result = add10(2, 3);
println!("Result of add10: {}", result); // 输出 15
}
make_functions!
宏接受一个或多个函数名和数值的组合。- 每个函数名和数值的组合生成一个函数,函数名由
$name
指定,数值由$n
指定。 - 调用
make_functions!
生成了三个函数add5
,add10
,add15
。
5.3 用于从属性生成代码的过程宏
第二种形式的宏被称为过程宏(procedural macros),因为它们更像函数(一种过程类型)。过程宏接收 Rust 代码作为输入,在这些代码上进行操作,然后产生另一些代码作为输出,而非像声明式宏那样匹配对应模式然后以另一部分代码替换当前代码。有三种类型的过程宏(自定义派生(derive),类属性和类函数),不过它们的工作方式都类似。
5.3.1 自定义 derive 宏
在 Rust 中,自定义 derive
宏是一种强大的功能,它允许你扩展 Rust 的派生特性,从而为类型自动生成额外的代码。下面是一个简单的自定义 derive
宏的例子。
创建一个新的库项目
首先,使用 Cargo 创建一个新的库项目:
cargo new my_derive --lib
cd my_derive
修改 Cargo.toml
在 Cargo.toml
文件中添加依赖项:
my_derive/Cargo.toml
[package]
name = "my_derive"
version = "0.1.0"
edition = "2021"
[dependencies]
syn = "1.0"
quote = "1.0"
proc-macro2 = "1.0"
[lib]
proc-macro = true
这里添加了 syn
、quote
和 proc-macro2
这三个库,它们是编写 Rust 宏所必需的。
编写自定义 derive
宏
在 my_derive/src/lib.rs
文件中,编写自定义 derive
宏:
extern crate proc_macro;
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, DeriveInput};
#[proc_macro_derive(MyTrait)]
pub fn my_trait_derive(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let name = input.ident;
let expanded = quote! {
impl MyTrait for #name {
fn my_method(&self) -> String {
format!("Hello from {}", stringify!(#name))
}
}
};
TokenStream::from(expanded)
}
这段代码定义了一个名为 MyTrait
的派生宏。当一个类型派生 MyTrait
时,会自动为其生成一个 my_method
方法。
使用自定义 derive
宏
返回到和 my_derive
工程同一层目录,创建一个新项目 my_project
。
cargo new my_project
cd my_project
在 Cargo.toml
文件中添加依赖项(my_derive):
my_project/Cargo.toml
[package]
name = "my_project"
version = "0.1.0"
edition = "2021"
[dependencies]
my_derive = { path = "../my_derive" }
在 my_project/src/main.rs
中,添加以下代码来使用自定义的 derive
宏:
use my_derive::MyTrait;
pub trait MyTrait {
fn my_method(&self) -> String;
}
#[derive(MyTrait)]
struct MyStruct;
fn main() {
let my_struct = MyStruct;
println!("{}", my_struct.my_method());
}
这里定义了一个 MyTrait
trait 和一个 MyStruct
结构体。MyStruct
使用了 #[derive(MyTrait)]
来自动派生 MyTrait
。
如此,就完成了一个简单的自定义 derive
宏的编写和使用。
编译并运行
使用以下命令编译并运行项目:
PS E:\Test\my_project> cargo run
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.22s
Running `target\debug\my_project.exe`
Hello from MyStruct
5.3.2 类属性宏
类属性宏与自定义派生宏相似,不同的是 derive
属性生成代码,它们(类属性宏)能让你创建新的属性。它们也更为灵活;derive
只能用于结构体和枚举;属性还可以用于其它的项,比如函数。
proc_macro_attribute
是一种特殊的宏,它允许你在编译时处理属性(Attribute)。下面是一个使用 proc_macro_attribute
的例子:
my_attr/Cargo.toml
[package]
name = "my_attr"
version = "0.1.0"
edition = "2021"
[dependencies]
syn = { version = "1.0", features = ["full"] }
quote = "1.0"
proc-macro2 = "1.0"
[lib]
proc-macro = true
my_attr/src/lib.rs
extern crate proc_macro;
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, ItemFn};
#[proc_macro_attribute]
pub fn my_attribute(_attr: TokenStream, item: TokenStream) -> TokenStream {
let input = parse_macro_input!(item as ItemFn);
let fn_name = &input.sig.ident;
let block = &input.block;
let expanded = quote! {
fn #fn_name() {
println!("Before the function call");
#block
println!("After the function call");
}
};
TokenStream::from(expanded)
}
这个例子中,我们定义了一个名为 my_attribute
的宏,它接受一个属性和一个项(在这个例子中是一个函数)。我们首先解析输入项为一个 ItemFn
类型,然后提取函数名和函数体。接下来,我们使用 quote!
宏来生成一个新的函数,这个新函数在调用原始函数之前和之后分别打印一条消息。最后,我们将生成的代码转换为 TokenStream
并返回。
然后,你可以在你的项目中像这样使用这个宏:
my_project/Cargo.toml
[package]
name = "my_project"
version = "0.1.0"
edition = "2021"
[dependencies]
my_attr = { path = "../my_attr" }
my_project/src/main.rs
use my_attr::my_attribute;
#[my_attribute]
fn my_function() {
println!("Hello, world!");
}
fn main() {
my_function();
}
运行结果
Before the function call
Hello, world!
After the function call
这说明 my_attribute
宏成功地在 my_function
的前后添加了额外的代码。
5.3.3 类函数宏
类函数(Function-like)宏的定义看起来像函数调用的宏。类似于 macro_rules!,它们比函数更灵活;例如,可以接受未知数量的参数。然而 macro_rules! 宏只能使用之前 “使用 macro_rules!
的声明宏用于通用元编程” 介绍的类匹配的语法定义。类函数宏获取 TokenStream
参数,其定义使用 Rust 代码操纵 TokenStream
,就像另两种过程宏一样。
下面我们来看一个例子:
my_macro/Cargo.toml
[package]
name = "my_macro"
version = "0.1.0"
edition = "2021"
[dependencies]
syn = { version = "1.0", features = ["full"] }
quote = "1.0"
proc-macro2 = "1.0"
[lib]
proc-macro = true
my_macro/src/lib.rs
extern crate proc_macro;
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, ItemFn};
#[proc_macro]
pub fn my_macro(input: TokenStream) -> TokenStream {
let input_fn = parse_macro_input!(input as ItemFn);
// 获取函数名
let name = &input_fn.sig.ident;
// 获取函数参数
let args = &input_fn.sig.inputs;
// 获取函数返回类型
let output = &input_fn.sig.output;
// 创建新的函数体
let expanded = quote! {
fn #name(#args) #output {
println!("Hello from macro!");
}
};
TokenStream::from(expanded)
}
这个例子中
引入依赖:
extern crate proc_macro;
:引入过程宏相关的库。
use proc_macro::TokenStream;
:使用 TokenStream
类型,用于处理 Rust 代码的抽象语法树(AST)。
use quote::quote;
:使用 quote 宏,用于生成代码。
use syn::{parse_macro_input, ItemFn};
:使用 syn 库解析宏输入。
定义过程宏:
#[proc_macro]
:标记这是一个过程宏。
pub fn my_macro(input: TokenStream) -> TokenStream
:定义一个公开的函数宏,输入和输出都是 TokenStream
。
解析宏输入:
let input_fn = parse_macro_input!(input as ItemFn);
:将输入的 TokenStream
解析为一个函数的 AST。
生成新代码:
let name = &input_fn.sig.ident;
:获取函数名。
let args = &input_fn.sig.inputs;
:获取函数参数。
let output = &input_fn.sig.output;
:获取函数返回类型。
let expanded = quote! { ... };
:使用 quote!
宏生成新的代码。
返回生成的代码:
TokenStream::from(expanded)
:将生成的代码转换为 TokenStream
并返回。
然后,你可以在你的项目中像这样使用这个宏:
my_project/Cargo.toml
[package]
name = "my_project"
version = "0.1.0"
edition = "2021"
[dependencies]
my_macro = { path = "../my_macro" }
my_project/src/main.rs
use my_macro::my_macro;
fn main() {
my_macro!(fn my_function() {
println!("Function body");
});
my_function();
}
运行结果
Hello from macro!
my_function()
中的 Function body
打印已被 Hello from macro!
取代,说明过程宏已生效。
参考链接
- Rust 官方网站:https://www.rust-lang.org/zh-CN
- Rust 官方文档:https://doc.rust-lang.org/
- Rust Play:https://play.rust-lang.org/
- 《Rust 程序设计语言》