十八、Rust 进阶特性 (二)
1. 高级类型
1.1 类型安全和抽象
-
通过使用 newtype 模式来实现类型安全和抽象
-
使用 newtype 模式的优点:
- 可以抽象掉一些类型的实现细节,只暴露出期望的公有 API,以限制其功能
- 可以隐藏内部的泛型类型,只提供特定的类型
1.2 类型别名
-
类似于
Typescript里的类型别名type EpochTimeStamp = number;-
关键字:
type -
示例:
type Kilometers = i32; -
一个重要作用:减少重复
// 来自官方教程的一个示例 type Thunk = Box<dyn Fn() + Send + 'static>; let f: Thunk = Box::new(|| println!("hi")); fn takes_long_type(f: Thunk) { // --snip-- } fn returns_long_type() -> Thunk { // --snip-- Box::new(|| ()) }
-
1.3 特殊类型 !
- empty type
- 作用:在函数从不返回的时候充当返回值
- 类似于
Typescript里的never
- 类似于
- 示例:
fn bar() -> ! {} - 从不返回的函数被称为 发散函数 (diverging functions)
match语句的continue返回的就是这个类型的值,即!
1.4 动态大小类型
-
动态大小类型:dynamic sized type, DST
-
Rust 需要知道应该为某一类型的值分配多少内存,且同一类型的值必须使用相同大小的内存
-
不可能创建一个存放动态大小类型的变量:不同的值,其大小不相同,这违背了上述规则
-
常规用法:通过引入额外的 meta info 来存储动态信息的大小,如
&str类型 -
Sizedtrait:决定一个类型的大小是否在编译时可知- Rust 隐式地为每一个泛型函数增加了
Sizedbound
- Rust 隐式地为每一个泛型函数增加了
-
放宽泛型的 Sized 限制
fn generic<T: ?Sized>(t: &T) {}:将Sized变成可选的
2. 高级函数与闭包
2.1 函数指针
-
类似于
Javascript里的回调函数,直接传递一个函数引用进入 -
函数指针实现了三个闭包 trait(
Fn、FnMut和FnOnce) ,因此期望闭包作为参数的函数,也可以在对应参数位置传入一个函数指针- 注意:反之则不成立,在只接受函数指针作为参数的地方,有可能不接受传入闭包作为参数。比如与不存在闭包的外部代码交互时
- C 语言的函数可以接受函数作为参数,但 C 语言没有闭包
- 注意:反之则不成立,在只接受函数指针作为参数的地方,有可能不接受传入闭包作为参数。比如与不存在闭包的外部代码交互时
-
使用示例
fn add_one(x: i32) -> i32 { x + 1 } // 注意这个函数签名 fn do_twice<T = i32>(f: fn(T) -> T, arg: T) -> T { f(arg) + f(arg) } fn main() { let answer = do_twice(add_one, 5); println!("The answer is: {}", answer); }
2.2 闭包作为返回值
-
闭包表现为 trait,这意味着不能 直接 返回闭包
-
trait 作为返回值
- 使用实现了期望返回的 trait 的具体类型来作为函数的返回值
-
闭包作为返回值
-
使用
Box<T>来包裹期望返回的闭包// 来自官方教程的一个示例 // 注意这里的返回值类型定义,是 Box<dyn Fn> fn returns_closure() -> Box<dyn Fn(i32) -> i32> { // 注意这里的返回值,是被 Box 包裹的 Box::new(|x| x + 1) }
-
3. 宏
-
作用:提供元编程能力,提高封装度,减少代码量
-
宏与函数的区别
- 宏提供了函数的能力,且提供了一些额外的能力:
- 宏可以在编译器翻译代码前展开
- 宏可以在一个给定类型上实现 trait
- 宏的缺点:宏定义比函数定义更复杂、更难阅读、更难理解、更难维护
- 在调用宏 之前 就必须定义并引入作用域,而函数则可以在任何地方定义和调用
- 宏提供了函数的能力,且提供了一些额外的能力:
-
在 Rust 中,宏 (macros) 分为两种:
-
使用
macro_rules!声明的自定义宏-
允许 developer 编写一些类似
match表达式的代码 -
一个示例
#[macro_export] // 如果没有这个注解,那么这个宏就不能被引入作用域 // 宏的名称后面是没有 `!` 的,要注意哦 macro_rules! vec { // 大括号里面的就是宏定义体 // ↓ 下面这个是一个单边模式。一个宏可以有大于零的任意个单边模式 // 单边模式匹配的是 Rust 代码结构,而不是值 ( $( $x:expr ),* ) => { { let mut temp_vec = Vec::new(); $( temp_vec.push($x); )* temp_vec } }; }
-
-
过程 (Procedural) 宏
-
作用:从属性生成代码
-
原理:接收 Rust 代码作为输入,然后在这些代码上进行操作,最终产生新的代码作为输出
-
特点:
- 过程宏包含一个函数,这也是其得名的原因
- 这个函数接受一个
TokenStream作为输入,最终产生一个TokenStream作为输出- 宏需要处理的代码即为输入
TokenStream - 宏生成的代码即为输出
TokenStream
- 宏需要处理的代码即为输入
- 这个函数上有一个属性,这个属性表明了这个过程宏的类型
-
-
-
分类:
-
自定义
#[derive]宏:在结构体和枚举上,通过指定derive来添加的代码-
derive只能用于结构体和枚举 -
截至目前 (v1.60.0),过程式宏必须在其自己的 crate 内,该限制最终可能被取消
-
示例代码
// 来自官方教程的一个示例 // src/main.rs use hello_macro::HelloMacro; use hello_macro_derive::HelloMacro; // 在这个结构体上使用自定义的宏 #[derive(HelloMacro)] struct Pancakes; fn main() { // 所以这里就可以使用宏里定义的函数了 Pancakes::hello_macro(); }
-
-
类属性宏:定义了壳用于任意元素的自定义属性
-
与自定义派生宏相似,不同于为
derive属性生成代码,它允许你创建新的属性 -
除了可用于枚举和结构体外,还能用于其他项,比如函数
-
实现方式:与自定义派生宏工作方式一致。先创建
proc-macrocrate 类型的 crate,再实现希望生成代码的函数 -
一个使用示例
// 使用类属性宏 #[route(GET, "/")] fn index() {} // 定义类属性宏 #[proc_macro_attribute] pub fn route(attr: TokenStream, item: TokenStream) -> TokenStream {}
-
-
类函数宏:看起来像函数,但作用于作为参数传递的 token
-
看起来像函数调用的宏,但比函数更灵活
- 比如:可以接受未知数的参数
-
一个使用示例
// 使用类函数宏 let sql = sql!(SELECT * FROM posts WHERE id=1); // 定义类函数宏 #[proc_macro] pub fn sql(input: TokenStream) -> TokenStream {}
-
-
-
-
-
-