rust 具有很多高级的特性,比如高级的 trait 、高级的类型和高级的函数和闭包
关联类型(associated types)是一个将类型占位符与 trait 相关联的方式,这样 trait 的方法签名中就可以使用这些占位符类型。我们使用 type 来定义占位符类型
一个带有关联类型的 trait 的例子是标准库提供的 Iterator
trait,它有一个叫做 Item
的关联类型来替代遍历的值的类型,这个 trait 的实现者会指定 Item
的具体类型,然而不管实现者指定何种类型,next
方法都会返回一个包含了此具体类型值的 Option
。
pub trait Iterator {
type Item;
fn next(&mut self) -> Option<Self::Item>;
}
关联类型和泛型的区别是,使用泛型时,则不得不在每一个实现中标注类型;关联类型,则无需标注类型,因为不能多次实现这个 trait
当使用泛型类型参数时,可以为泛型指定一个默认的具体类型。比如下面的例子:如果实现 Add
trait 时不指定 Rhs
的具体类型,Rhs
的类型将是默认的 Self
类型,也就是在其上实现 Add
的类型。
trait Add<Rhs=Self> {
type Output;
fn add(self, rhs: Rhs) -> Self::Output;
}
Rust 并不允许创建自定义运算符或重载任意运算符,不过 std::ops
中所列出的运算符和相应的 trait 可以通过实现运算符相关 trait 来重载,例如下面的例子,重载了 + 运算使得可以对结构体进行运算
use std::ops::Add;
#[derive(Debug, Copy, Clone, PartialEq)]
struct Point {
x: i32,
y: i32,
}
impl Add for Point {
type Output = Point;
fn add(self, other: Point) -> Point {
Point {
x: self.x + other.x,
y: self.y + other.y,
}
}
}
fn main() {
assert_eq!(
Point { x: 1, y: 0 } + Point { x: 2, y: 3 },
Point { x: 3, y: 3 }
);
}
Rust 既不能避免一个 trait 与另一个 trait 拥有相同名称的方法,也不能阻止为同一类型同时实现这两个 trait,在方法名前指定 trait 名向 Rust 澄清了我们希望调用哪个 fly
实现:
trait Pilot {
fn fly(&self);
}
trait Wizard {
fn fly(&self);
}
struct Human;
impl Pilot for Human {
fn fly(&self) {
println!("This is your captain speaking.");
}
}
impl Wizard for Human {
fn fly(&self) {
println!("Up!");
}
}
impl Human {
fn fly(&self) {
println!("*waving arms furiously*");
}
}
fn main() {
let person = Human;
Pilot::fly(&person);
Wizard::fly(&person);
person.fly();
}
但是关联函数是 trait 的一部分,但没有 self 参数。当同一作用域的两个类型实现了同一 trait,Rust 就不能计算出我们期望的是哪一个类型,为了消歧义并告诉 Rust 我们希望我们使用 完全限定语法,这是调用函数时最为明确的方式:
trait Animal {
fn baby_name() -> String;
}
struct Dog;
impl Dog {
fn baby_name() -> String {
String::from("Spot")
}
}
impl Animal for Dog {
fn baby_name() -> String {
String::from("puppy")
}
}
fn main() {
println!("A baby dog is called a {}", <Dog as Animal>::baby_name());
//A baby dog is called a puppy
println!("A baby dog is called a {}", Dog::baby_name());
//A baby dog is called a Spot
}
有时我们可能会需要某个 trait 使用另一个 trait 的功能。在这种情况下,需要能够依赖相关的 trait 也被实现。这个所需的 trait 是我们实现的 trait 的 父 trait 。
例如:我们希望创建一个带有 outline_print
方法的 trait OutlinePrint
,但是在 outline_print
的实现中,因为希望能够使用 Display
trait 的功能,则需要说明 OutlinePrint
只能用于同时也实现了 Display
并提供了 OutlinePrint
需要的功能的类型。可以通过在 trait 定义中指定 OutlinePrint: Display
来做到这一点。这类似于为 trait 增加 trait bound。
use std::fmt;
trait OutlinePrint: fmt::Display {
fn outline_print(&self) {
//....
}
}
我们曾经说过孤儿规则,例如:如果想要在 Vec
上实现 Display
,而孤儿规则阻止我们直接这么做,因为 Display
trait 和 Vec
都定义于我们的 crate 之外。
一个绕开这个限制的方法是使用 newtype 模式(newtype pattern),你可以在元组结构体中创建一个新类型,这个元组结构体带有一个字段作为希望实现 trait 的类型的简单封装。接着这个封装类型对于 crate 是本地的,这样就可以在这个封装上实现 trait,例如:可以创建一个包含 Vec
实例的 Wrapper
结构体,接着可以在 Wrapper
上实现 Display
并使用 Vec
的值:
use std::fmt;
struct Wrapper(Vec<String>);
impl fmt::Display for Wrapper {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "[{}]", self.0.join(", "))
}
}
fn main() {
let w = Wrapper(vec![String::from("hello"), String::from("world")]);
println!("w = {}", w);
}
Rust 还提供了声明 类型别名(type alias)的能力,使用 type
关键字来给予现有类型另一个名字。例如,可以像这样创建 i32
的别名 Kilometers
,Kilometers
类型的值将被完全当作 i32
类型值来对待
type Kilometers = i32;
let x: i32 = 5;
let y: Kilometers = 5;
Rust 有一个叫做 !
的特殊类型。在类型理论术语中,它被称为 empty type,因为它没有值。我们更倾向于称之为 never type,它在函数从不返回的时候充当返回值:
fn bar() -> ! {
// --snip--
}
never type 可以强转为任何其他类型,我们可以看到很多返回 ! 的函数,比如说输出函数 print!
,panic!
都是 !
类型,你可以在模式匹配中使用他们,因为他们返回never type,可以强制转型成任何类型:
match self {
Some(val) => val,
None => panic!("called `Option::unwrap()` on a `None` value"),
}
Rust 有一个特定的 trait 来决定一个类型的大小是否在编译时可知:这就是 Sized
trait。这个 trait 自动为编译器在编译时就知道大小的类型实现,另外,Rust 隐式的为每一个泛型函数增加了 Sized
bound。也就是说,对于如下泛型函数定义:
fn generic<T>(t: T) {
// --snip--
}
//相等
fn generic<T: Sized>(t: T) {
// --snip--
}
泛型函数默认只能用于在编译时已知大小的类型。然而可以使用如下特殊语法来放宽这个限制,?Sized
上的 trait bound 意味着 “T
可能是也可能不是 Sized
” 同时这个注解会覆盖泛型类型必须在编译时拥有固定大小的默认规则。这种意义的 ?Trait
语法只能用于 Sized
,而不能用于任何其他 trait。
fn generic<T: ?Sized>(t: &T) {
// --snip--
}
通过函数指针允许我们使用函数作为另一个函数的参数。函数的类型是 fn
(使用小写的 “f” )以免与 Fn
闭包 trait 相混淆。fn
被称为 函数指针(function pointer)。指定参数为函数指针的语法类似于闭包,不同于闭包,fn
是一个类型而不是一个 trait,所以直接指定 fn
作为参数而不是声明一个带有 Fn
作为 trait bound 的泛型参数。
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);
}
你可以用如下的方式返回一个闭包。注意,第二段代码尝试直接返回闭包并不能编译,Rust 并不知道需要多少空间来储存闭包,所以需要使用 trait 对象:
fn returns_closure() -> Box<dyn Fn(i32) -> i32> {
Box::new(|x| x + 1)
}
//不能运行
fn returns_closure() -> dyn Fn(i32) -> i32 {
|x| x + 1
}
宏(Macro)指的是 Rust 中一系列的功能,从根本上来说,宏是一种为写其他代码而写代码的方式,对于减少大量编写和维护的代码是非常有用的,它也扮演了函数扮演的角色,但宏有一些函数所没有的附加能力。
rust 最常用的宏形式是 声明宏(declarative macros)。其核心概念是,声明宏允许我们编写一些类似 Rust match 表达式的代码,宏也将一个值和包含相关代码的模式进行比较;此种情况下,该值是传递给宏的 Rust 源代码字面值,模式用于和前面提到的源代码字面值进行比较,每个模式的相关代码会替换传递给宏的代码。所有这一切都发生于编译时。
可以使用 macro_rules!
来定义宏:#[macro_export]
注解表明只要导入了定义这个宏的 crate,该宏就应该是可用的。所定义的宏并不带感叹号。名字后跟大括号表示宏定义体,在该例中宏名称是 vec 。
此后有一个分支模式 ( $( $x:expr ),* )
,后跟 =>
以及和模式相关的代码块,一对括号包含了整个模式。接下来是美元符号( $
),后跟一对括号,其捕获了符合括号内模式的值用以在替代代码中使用。$()
内则是 $x:expr
,其匹配 Rust 的任意表达式,并将该表达式命名为 $x
。$()
之后的逗号说明一个可有可无的逗号分隔符可以出现在 $()
所匹配的代码之后。紧随逗号之后的 *
说明该模式匹配零个或更多个 *
之前的任何模式。匹配到模式中的$()
的每一部分,都会在(=>
右侧)$()*
里生成temp_vec.push($x)
,生成零次还是多次取决于模式匹配到多少次。$x
由每个与之相匹配的表达式所替换。
#[macro_export]
macro_rules! vec {
( $( $x:expr ),* ) => {
{
let mut temp_vec = Vec::new();
$(
temp_vec.push($x);
)*
temp_vec
}
};
}
当以 vec![1, 2, 3];
调用该宏时,替换该宏调用所生成的代码会是下面这样:
{
let mut temp_vec = Vec::new();
temp_vec.push(1);
temp_vec.push(2);
temp_vec.push(3);
temp_vec
}
对于全部的宏模式语法,请查阅参考。
第二种形式的宏被称为 过程宏(procedural macros),因为它们更像函数(一种过程类型),过程宏接收 Rust 代码作为输入,在这些代码上进行操作,然后产生另一些代码作为输出,而非像声明式宏那样匹配对应模式然后以另一部分代码替换当前代码。
创建过程宏时,其定义必须驻留在它们自己的具有特殊 crate 类型的 crate 中。有三种类型的过程宏(自定义派生(derive),类属性和类函数),不过它们的工作方式都类似:
我们可以这样定义一个derive 宏 ,是三个自带的模块:proc_macro
crate 是编译器用来读取和操作我们 Rust 代码的 API。syn
crate 将字符串中的 Rust 代码解析成为一个可以操作的数据结构。quote
则将 syn
解析的数据结构转换回 Rust 代码。这些 crate 让解析任何我们所要处理的 Rust 代码变得更简单:为 Rust 编写整个的解析器并不是一件简单的工作。
use proc_macro::TokenStream;
use quote::quote;
use syn;
#[proc_macro_derive(HelloMacro)]
pub fn hello_macro_derive(input: TokenStream) -> TokenStream {
// Construct a representation of Rust code as a syntax tree
// that we can manipulate
let ast = syn::parse(input).unwrap();
// Build the trait implementation
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()
}
我们将代码分成了hello_macro_derive
和 impl_macro_derive
两个函数,前者负责解析 TokenStream
,后者负责转换语法树:这使得编写过程宏更方便。几乎你看到或者创建的每一个过程宏的外部函数(这里是hello_macro_derive
)中的代码都跟这里是一样的。你放入内部函数(这里是impl_macro_derive
)中的代码根据你的过程宏的设计目的会有所不同。
在impl_macro_derive
,我们得到一个包含以 ast.ident
作为注解类型名字(标识符)的 Ident
结构体实例。示例 中的结构体表明当 impl_hello_macro
函数运行于示例的代码上时 ident
字段的值是 "Pancakes"
。因此,示例中 name
变量会包含一个 Ident
结构体的实例,当打印时,会是字符串 "Pancakes"
quote!
宏能让我们编写希望返回的 Rust 代码。quote!
宏执行的直接结果并不是编译器所期望的所以需要转换为 TokenStream
。为此需要调用 into
方法,它会消费这个中间表示(intermediate representation,IR)并返回所需的 TokenStream
类型值。
//src/main.rs
use hello_macro::HelloMacro;
use hello_macro_derive::HelloMacro;
#[derive(HelloMacro)]
struct Pancakes;
fn main() {
Pancakes::hello_macro();
}
//src/lib.rs
use hello_macro::HelloMacro;
struct Pancakes;
impl HelloMacro for Pancakes {
fn hello_macro() {
println!("Hello, Macro! My name is Pancakes!");
}
}
fn main() {
Pancakes::hello_macro();
}
当用户在一个类型上指定 #[derive(HelloMacro)]
时,hello_macro_derive
函数将会被调用。因为我们已经使用 proc_macro_derive
及其指定名称HelloMacro
对 hello_macro_derive
函数进行了注解,指定名称HelloMacro
就是 trait 名,这是大多数过程宏遵循的习惯。
类属性宏与自定义派生宏相似,不同的是 derive
属性生成代码,它们(类属性宏)能让你创建新的属性。它们也更为灵活;derive
只能用于结构体和枚举;属性还可以用于其它的项,比如函数。作为一个使用类属性宏的例子,可以创建一个名为 route
的属性用于注解 web 应用程序框架(web application framework)的函数:
#[route(GET, "/")]
fn index() {
类函数(Function-like)宏的定义看起来像函数调用的宏。类似于 macro_rules!
,它们比函数更灵活;例如,可以接受未知数量的参数。一个类函数宏例子是可以像这样被调用的 sql!
宏,这个宏会解析其中的 SQL 语句并检查其是否是句法正确的:
let sql = sql!(SELECT * FROM posts WHERE id=1);