目录
- 一、为了类型安全和抽象而使用 newtype 模式
- 二、使用类型别名创建类型同义词
- 2.1 使用type关键赋予现有类型一个别名
- 2.2 减少重复
- 2.3 与Result<T, E>结合使用
- 2.4 从不返回的 never type
- 三、高级函数和闭包
- 3.1 函数指针
- 3.2 返回闭包
- 四、宏
- 4.1 宏和函数的区别
- 4.2 macro_rules! 的声明宏
- 4.3 基于属性生成代码的过程宏
- 4.4 编写自定义 derive 宏
一、为了类型安全和抽象而使用 newtype 模式
newtype
模式的应用可以用于确保静态值不被混淆以及表示一个值的单元;newtype
模式的应用可以抽象掉一些类型的实现细节;- 例如封装类型可以暴露出与直接使用其内部私有类型时所不同的公有 API,以便限制其功能;
newtype
模式也可以隐藏其内部的泛型类型;
二、使用类型别名创建类型同义词
2.1 使用type关键赋予现有类型一个别名
fn main() {
type Kilometers = i32;
let x: i32 = 5;
let y: Kilometers = 5;
println!("x + y = {}", x + y);
}
- 代码输出:
x + y = 10
;
2.2 减少重复
- 类型别名的主要用途是减少重复,例如类型Box<dyn Fn() + Send + 'static>;
let f: Box<dyn Fn() + Send + 'static> = Box::new(|| println!("hi"));
fn takes_long_type(f: Box<dyn Fn() + Send + 'static>) {}
fn returns_long_type() -> Box<dyn Fn() + Send + 'static> {}
- 通过
type
关键字引入类型别名,则可以修改为
type Thunk = Box<dyn Fn() + Send + 'static>;
let f: Thunk = Box::new(|| println!("hi"));
fn takes_long_type(f: Thunk) {}
fn returns_long_type() -> Thunk {}
2.3 与Result<T, E>结合使用
- 标准库中的
std::io
模块; - I/O 操作通常会返回一个
Result<T, E>
; - 标准库中的
std::io::Error
结构体代表了所有可能的 I/O 错误; std::io
中大部分函数会返回Result<T, E>
;
use std::io::Error;
use std::fmt;
pub trait Write {
fn write(&mut self, buf: &[u8]) -> Result<usize, Error>;
fn flush(&mut self) -> Result<(), Error>;
fn write_all(&mut self, buf: &[u8]) -> Result<(), Error>;
fn write_fmt(&mut self, fmt: fmt::Arguments) -> Result<(), Error>;
}
- 上述代码出现了很多
Result<..., Error>
,因此,std::io
有别名声明;
type Result<T> = std::result::Result<T, std::io::Error>;
- 由于位于
std::io
,可用的完全限定的别名是std::io::Result<T>
,即Result<T, E>
中 E 放入了std::io::Error
- 最后的效果如下
pub trait Write {
fn write(&mut self, buf: &[u8]) -> Result<usize>;
fn flush(&mut self) -> Result<()>;
fn write_all(&mut self, buf: &[u8]) -> Result<()>;
fn write_fmt(&mut self, fmt: fmt::Arguments) -> Result<()>;
}
2.4 从不返回的 never type
- Rust有一个
!
的特殊类型,它被称为empty type
,更倾向于称之为never type
; - 主要用于在函数从不返回的时候充当返回值;
- 从不返回的函数被称为发散函数;
fn bar() -> ! {}
用途
- 有如下代码
let guess: u32 = match guess.trim().parse() {
Ok(num) => num,
Err(_) => continue,
};
- 我们知道
match
分支必须返回相同的类型; - 如下代码必无法通过编译
let guess = match guess.trim().parse() {
Ok(_) => 5,
Err(_) => "hello",
}
- 上述代码里的
guess
必须既是整型也是字符串,而 Rust 要求guess 只能是一个类型; - 所以continue 返回的值是
!
;
never type 的另一个用途是 panic!
三、高级函数和闭包
3.1 函数指针
- 可以向函数传递闭包,也可以向函数传递常规函数;
- 函数的类型是
fn
,它被称为函数指针 ;
fn add_one(x: i32) -> i32 {
x + 1
}
fn do_twice(f: fn(i32) -> i32, arg: i32) -> i32 {
f(arg) + f(arg)
}
fn main() {
let answer = do_twice(add_one, 5);
println!("The answer is: {}", answer); //The answer is: 12
}
do_twice
函数中的f
被指定为一个接受一个i32 参数并返回 i32 的函数指针;- 就可以在
do_twice
函数体中调用该函数; fn
是一个类型而不是一个trait;- 直接指定 fn 作为参数;
- 而非声明一个带有 Fn 作为 trait bound 的泛型参数;
- 函数指针实现了所有三个闭包 trait:Fn、FnMut 和 FnOnce;
- 总是可以在调用期望闭包的函数时传递函数指针作为参数;
- 当与不存在闭包的外部代码交互时,可以只期望接受 fn 而不接受闭包;
let list_of_numbers = vec![1, 2, 3];
let list_of_strings: Vec<String> = list_of_numbers
.iter()
.map(|i| i.to_string())
.collect();
- 上述代码使用map函数将一个数字vector 转换为一个字符串 vector;
- 也可以将函数作为 map 的参数来代替闭包;
let list_of_numbers = vec![1, 2, 3];
let list_of_strings: Vec<String> = list_of_numbers
.iter()
.map(ToString::to_string)
.collect();
- 另一个实用的模式暴露了元组结构体和元组结构体枚举成员的实现细节;
- 这些项使用 () 作为初始化语法(看起来就像函数调用);
- 同时它们确实被实现为返回由参数构造的实例的函数;
- 它们也被称为实现了闭包 trait 的函数指针,并可以采用类似如下的方式调用;
enum Status {
Value(u32),
Stop,
}
let list_of_statuses: Vec<Status> =
(0u32..20)
.map(Status::Value)
.collect();
- 创建了
Status::Value
实例,它通过map用范围的每一个u32 值调用 Status::Value 的初始化函数;
3.2 返回闭包
- 如下的代码不能通过编译
fn returns_closure() -> Fn(i32) -> i32 {
|x| x + 1
}
- 使用 trait 对象解决
fn returns_closure() -> Box<dyn Fn(i32) -> i32> {
Box::new(|x| x + 1)
}
四、宏
- 宏(Macro)指的是 Rust 中一组相关特性的集合:
- 使用
macro_rules!
声明的(Declarative)宏,和三种过程(Procedural)宏:- 自定义
#[derive]
宏,用于结构体和枚举上指定通过derive属性添加的代码; - 类似属性宏,可用于任意项的自定义属性;
- 类函数宏,看起来像函数调用,作用于作为参数传递的 token;
- 自定义
- 使用
4.1 宏和函数的区别
- 宏是一种为写其他代码而编写的代码,即所谓的元编程(metaprogramming);
- 一个函数标签必须声明函数参数个数和类型,宏能够接受不同数量的参数;
- 在一个文件里调用宏之前必须定义,或将其引入作用域,函数则可以在任何地方定义和调用;
4.2 macro_rules! 的声明宏
- 最常用的宏形式是 声明宏(declarative macros),它允许我们编写一些类似 Rust match 表达式的代码 ;
- 使用
macro_rules!
定义宏; vec![1, 2, 3];
调用下面的宏 (简化);
#[macro_export]
macro_rules! vec {
( $( $x:expr ),* ) => {
{
let mut temp_vec = Vec::new();
$(
temp_vec.push($x);
)*
temp_vec
}
};
}
#[macro_export]
标注说明,只要将定义了宏的crate
引入作用域,宏就应当是可用的;- 没有该标注的宏不能被引入作用域;
- 使用
macro_rules!
和宏名称开始宏定义,且所定义的宏并不带感叹号,名字后跟大括号表示宏定义体; $x:expr
指匹配任何的Rust表达式并命名为$x
,后面的逗号表示传入的逗号分隔符,后面的*
表示能匹配0个或多个;- 全部宏语法,参参阅:https://rustwiki.org/zh-CN/reference/macros.html
4.3 基于属性生成代码的过程宏
- 过程宏(procedural macros),更像函数(一种过程类型);
- 过程宏接收Rust代码作为输入,然后产生另一些代码作为输出;
- 还有一种类型的过程宏:
- 自定义派生;
- 属性宏;
- 函数宏;
- 创建过程宏时
- 宏定义必须单独放在它们自己的包中,并使用特殊的包类型;
use proc_macro;
#[some_attribute]
pub fn some_name(input: TokenStream) -> TokenStream {
}
- some_attribute是过程宏的占位符;
- 定义过程宏的函数以一个
TokenStream
作为输入并产生一个TokenStream
作为输出; - 该
TokenStream
类型由包含在 Rust 中的proc_macro
crate定义,并表示令牌序列;
4.4 编写自定义 derive 宏
- 创建
hello_macro
crate,定义一个拥有关联函数HelloMacro的 trait 和关联函数hello_macro; - 提供一个能自动实现trait的过程宏;
- 使用户在它们的类型上标注
#[derive(HelloMacro)]
,进而得到hello_macro的默认实现;
实现
- 在一个全新目录下(称为工作空间)创建
Cargo.toml
文件,写上[workspace]
就行了; - 再相同的目录下输入下面两条指令,创建两个crate;
cargo new hello_macro --lib
cargo new hello_macro_derive --lib
cargo new pancakes
hello_macro_derive\Cargo.toml
文件的内容如下
[package]
name = "hello_macro_derive"
version = "0.1.0"
edition = "2021"
[lib]
proc-macro = true
[dependencies]
syn = "2.0.68"
quote = "1.0"
Cargo.toml
文件内容如下
[workspace]
members = ["hello_macro", "hello_macro_derive", "pancakes"]
pancakes/Cargo.toml
文件内容如下
[package]
name = "pancakes"
version = "0.1.0"
edition = "2021"
[dependencies]
hello_macro = {path = "../hello_macro"}
hello_macro_derive = {path = "../hello_macro_derive"}
- 将过程宏放到
hello_macro_derive
里,其lib.rs内容如下
extern crate proc_macro;
use crate::proc_macro::TokenStream;
use quote::quote;
use syn;
#[proc_macro_derive(HelloMacro)]
pub fn hello_macro_derive(input: TokenStream) -> TokenStream {
// 将 Rust 代码解析为语法树以便进行操作
let ast = syn::parse(input).unwrap();
// 构建 trait 实现
impl_hello_macro(&ast)
}
fn impl_hello_macro(ast: &syn::DeriveInput) -> TokenStream {
let name = &ast.ident;
let gen = quote! {
impl HelloMacro for #name {
fn hello_macro() {
println!("Hello, Macro! My name is {}", stringify!(#name));
}
}
};
gen.into()
}
proc_macro
包提供了编译器接口,从而可以读取可操作的Rust代码;syn
是把Rust代码从字符串转换为可供我们进一步操作的数据结构;quote
包能够将syn产生的数据结构重新转换为Rust代码;- 函数
hello_macro_derive
负责解析TokenStream,函数内部的impl_hello_macro
负责转换语法库; - 效果是:用户标注
#[derive(HelloMacro)]
后,hello_macro_derive 会被自动调用; - 详细的看相关的文档;
- hello_macro/src/lib.rs中的代码为
pub trait HelloMacro{
fn hello_macro();
}
- pancakes/src/main.rs的代码为
use hello_macro::HelloMacro;
use hello_macro_derive::HelloMacro;
#[derive(HelloMacro)]
struct Pancakes;
fn main() {
Pancakes::hello_macro();
}
- 运行的结果如下