Rust 语法梳理与总结(中)
楔子
我们继续来梳理 Rust 语法,本次让我们把目光聚焦于函数、闭包、泛型和 trait 上面,开始接下来的内容。并且在梳理的时候,同样会对之前没有说的内容做一个补充。
函数
函数(function)使用 fn 关键字来声明,函数的参数需要标注类型,就和变量一样。如果函数返回一个值,返回值的类型必须在箭头 -> 之后指定。
函数的最后一个表达式将作为返回值,当然也可以在函数内使用 return 语句来提前返回一个值。
// 一个返回布尔值的函数,判断 b 能够被 a 整除
fn is_divisible_by(a: u32, b: u32) -> bool {
if b == 0 {
false
} else {
a % b == 0
}
}fn main() {
println!("{}", is_divisible_by(5, 2));
println!("{}", is_divisible_by(5, 0));
println!("{}", is_divisible_by(6, 3));
/*
false
false
true
*/
}
然后函数参数默认都是不可变的,如果希望它可变,那么在声明的时候也要使用 mut 关键字。
fn is_divisible_by(a: u32, mut b: u32) -> bool {
// 此时 b 就是一个可变参数
// 如果 b 等于 0,那么就将它改成 1
if b == 0 {
b = 1
}
a % b == 0
}fn main() {
println!("{}", is_divisible_by(5, 2));
println!("{}", is_divisible_by(5, 0));
println!("{}", is_divisible_by(6, 3));
/*
false
true
true
*/
}
既然提到了函数,那么就不得不提方法,方法是依附于某个对象的函数,在 impl 代码块内定义。
#[derive(Debug)]
struct Point {
x: f64,
y: f64,
}// 实现的代码块
impl Point {
// 这是一个静态方法(static method)
// 静态方法不需要被实例调用
// 这类方法一般用作构造器(constructor)
fn origin() -> Point {
Point { x: 0.0, y: 0.0 }
}
// 另一个静态方法,需要两个参数:
fn new(x: f64, y: f64) -> Point {
Point { x: x, y: y }
}
// 计算两个点之间的曼哈顿距离
fn distance(&self, other: &Point) -> f64 {
(self.x - other.x).abs() + (self.y - other.y).abs()
}
// 修改某个点的坐标
fn modify_x(&mut self, x: f64) {
self.x = x;
}
fn modify_y(&mut self, y: f64) {
self.y = y;
}
}
fn main() {
let mut p1 = Point::new(3.2, 1.4);
let mut p2 = Point::new(4.3, 1.5);
println!("{:?}", p1);
println!("{:?}", p2);
/*
Point { x: 3.2, y: 1.4 }
Point { x: 4.3, y: 1.5 }
*/
println!("{}", p1.distance(&p2));
println!("{}", p2.distance(&p1));
/*
1.1999999999999997
1.1999999999999997
*/
p1.modify_x(10.0);
p2.modify_y(10.0);
println!("{:?}", p1);
println!("{:?}", p2);
/*
Point { x: 10.0, y: 1.4 }
Point { x: 4.3, y: 10.0 }
*/
}
函数和方法本身还是很简单的,我们重点来看一下闭包。
闭包
闭包包含两部分:
函数,在 Rust 里面必须是匿名函数;
环境,由函数捕获的外层作用域中的变量组成;
我们先来看看普通函数和匿名函数在语法上的区别。
fn main() {
fn add1(a: i32, b: i32) -> i32 {
a + b
}
let add2 = |a: i32, b: i32| -> i32 {
a + b
};
}使用 fn 关键字定义的是普通函数,而匿名函数没有函数名,并且要把参数所在的小括号换成 |...|。然后这里我们将匿名函数赋值给了变量 add2,因此在使用上,add1 和 add2 没有区别,普通函数和匿名函数都是函数。
再来看看闭包,闭包本质上也是一个匿名函数,但它可以捕获外层作用域的变量,而这是 fn 定义的普通函数所做不到的。
fn main() {
let start = 1;
let add = |a: i32, b: i32| -> i32 {
a + b + start
}; println!("{}", add(2, 1)); // 4
}
因为变量 start 是定义在 main 函数里面的,所以闭包 add 捕获了外层作用域(这里是 main 函数的作用域)的变量。而我们说普通函数做不到这一点,来验证一下。
我们都不用执行,IDE 就已经给出提示了。普通函数无法捕获外层作用域的变量,我们应该使用 || {...} 定义的闭包。
捕获变量会产生额外开销
然后闭包还可以省略参数的类型,以及返回值类型。
fn main() {
// 不指定参数的类型和返回值类型
// Rust 会根据我们后续的使用推断出来
let add = |i, j| {
(i, j)
};
// 这里调用了两次 add,显然 Rust 会进行推断
// 会将参数 i 推断为 f32,将 j 推断为 u16
// 然后返回的是 (i, j),那么返回值类型就是 (f32, u16)
println!("{:?}", add(3.14, 666u16));
println!("{:?}", add(2.71f32, 777));
/*
(3.14, 666)
(2.71, 777)
*/ // 一旦推断出来,那么参数和返回值类型就确定了
// 所以下面的调用是不合法的,因为参数类型错了
// add(12, 13); // 报错,将 12 改成 12.0,那么就没问题了
}
总的来说,Rust 的类型推断还是很智能的,但即便如此,我们也最好明确指定类型。
另外,如果代码块只有一行代码的话,那么大括号也可以省略。
fn main() {
let add = |i, j| (i, j);
println!("{:?}", add(3.14, 666u16));
println!("{:?}", add(2.71f32, 777));
/*
(3.14, 666)
(2.71, 777)
*/
}注意:只有闭包才可以这么干,普通函数是不可以的。不过为了代码的可读性,还是不建议将大括号去掉,另外参数和返回值的类型最好也不要省略。
将闭包作为参数
先来回顾一下函数是怎么作为参数的。
type F = fn(i32, i32) -> i32;
// calc 接收三个参数
// 第一个参数:接收两个 i32 返回一个 i32 的函数
// 第二个参数和第三个参数均是 i32
fn calc(op: F, a: i32, b: i32) -> i32 {
op(a, b)
}fn main() {
let add = |a: i32, b: i32| -> i32 {
a + b
};
println!("{}", calc(add, 11, 22)); // 33
}
咦,参数 op 接收的是普通函数,而我们传递的 add 是一个闭包,为啥没报错呢?很简单,因为在 Rust 里面闭包本质上就是一个匿名函数,只不过它可以捕获外层作用域的变量罢了。但我们这里没有捕获,因此它和 fn 定义的普通函数是完全等价的,都是函数。如果我们将代码修改一下:
我们看到报错了,告诉我们类型不匹配。由于使用了外部的变量 start,所以闭包 add 就不能作为参数传递了,此时它和 fn 定义的普通函数不是完全等价的,因为后者无法捕获外层作用域的变量。
所以我们要修改函数 calc 的第一个参数的类型,将它改成闭包类型,而这需要使用一个叫 Fn 的 trait 来实现(一会儿介绍 trait)。
fn calc<T>(op: T, a: i32, b: i32) -> i32
where
T: Fn(i32, i32) -> i32,
{
op(a, b)
}
// 或者你还可以这么定义:
/*
fn calc(op: impl Fn(i32, i32) -> i32,
a: i32, b: i32) -> i32 {}
*/fn main() {
let start = 1;
let add = |a: i32, b: i32| -> i32 {
a + b + start
};
println!("{}", calc(add, 11, 22)); // 34
println!("{}", add(11, 22)); // 34
}
以上我们就将闭包作为参数传递了,另外传递一个 fn 关键字定义的普通函数也是可以的。虽然它不是闭包,但它实现了 Fn 这个 trait。
fn calc<T>(op: T, a: i32, b: i32) -> i32
where
T: Fn(i32, i32) -> i32,
{
op(a, b)
}fn main() {
let add = |a: i32, b: i32| -> i32 {
a + b
};
println!("{}", calc(add, 11, 22)); // 33
fn add(a: i32, b: i32) -> i32 {
a + b
}
println!("{}", calc(add, 11, 22)); // 33
}
在 Rust 里面,不管有没有捕获外层作用域的变量,匿名函数都是一个闭包。只是在没有捕获的情况下,它和 fn 定义的普通函数也是完全等价的。
闭包作为返回值
再来看看如何将闭包作为返回值,这中间会有一些和其它语言不一样的地方。
fn login(username: String, passwd: String) -> impl Fn() -> String {
move || {
if username.eq(&String::from("satori")) &&
passwd.eq(&String::from("123456")){
return String::from("欢迎来到编程教室")
} else {
return String::from("用户名密码错误")
}
}
}fn main() {
let username = String::from("satori");
let passwd = String::from("123456");
let closure = login(username, passwd);
println!("{}", closure());
/*
欢迎来到编程教室
*/
let username = String::from("satori");
let passwd = String::from("654321");
let closure = login(username, passwd);
println!("{}", closure());
/*
用户名密码错误
*/
}
代码非常简单,调用 login 函数之后返回一个闭包,然后调用闭包的时候比较用户名和密码,返回不同的字符串。需要注意的是,字符串的 eq 方法的第一个参数接收的是引用,所以闭包中调用 username.eq 和 passwd.eq 时,拿到的是 username 和 passwd 两个字符串的引用,而不是字符串本身。
但问题是相比之前,这段闭包代码多了一个 move 关键字,这是干啥用的。
我们仔细想一下,当闭包返回之后,login 函数是不是调用结束了,它的相关参数是不是也就销毁了呢。那么后续调用闭包的时候,再使用 username 和 passwd 就会报错,因为引用的对象已经被销毁了。
于是 Rust 提供了 move 关键字,会将捕获的外部变量移动到闭包内部,确保闭包在调用的时候能够找得到。如果变量是可 Copy 的,那么移动的时候会拷贝一份;如果不是可 Copy 的,那么移动的时候就转移所有权。
我们看这个例子,定义闭包的时候使用了 move 关键字,那么它会将外部变量,移动到闭包内部。所以在闭包的下面再使用 name 变量就会报错,因为所有权已经被转移了。
至于 age 则没有关系,因为它是可 Copy 的,这种变量的数据都完全分配在栈上,不涉及到堆。而栈上数据在传递的时候只会拷贝一份,所以传递之后你的是你的,我的是我的,两者没有关系,自然也就不会出现所有权的转移,因为数据都是各自独立的。
但对于涉及到堆内存的变量来说就不一样了,由于 Rust 默认不会拷贝堆上数据,那么变量在传递之后就会出现两个栈上指针,指向同一份堆内存。于是 Rust 就会转移所有权,只让一个变量拥有操作堆内存的权利。所以 name 在移动之后就不能再用了,因为它已经失去了操作堆内存的权利。
当然啦,对于当前这个例子,即使不使用 move 也是可以的,因为这是在 main 函数里面,闭包的存活时间没有超过它所捕获的变量。但之前那个例子不行,之前那个例子的闭包使用了 login 函数的两个参数,而在参数伴随 login 函数调用结束而销毁之后,闭包却依旧存活,因此报错。
Rust 闭包的实现和所有权的实现,在语义上是统一的。
Fn、FnOnce、FnMut
Rust 一切皆类型,并由 trait 掌握类型的行为逻辑。至于闭包类型则需要通过 Fn trait 来指定,但除了 Fn 之外,还有两个 trait,也就是 FnOnce 和 FnMut。那么它们之间有什么区别呢?
FnOnce
实现此 trait 的闭包在使用外部变量的时候,可以夺走所有权。
我们看到两个函数的函数体都是一样的,但使用 Fn trait 就有问题,而使用 FnOnce trait 则一切正常。原因就是对于 FnOnce 来说,它可以夺走外部变量的所有权,而 Fn 不能。
fn test() -> impl FnOnce() {
let name = String::from("古明地觉");
|| {
let new_name = name;
}
}fn main() {
// 拿到闭包,test 里变量 name 的所有权,
// 会转移给闭包内部的 new_name 变量
let closure = test();
// 调用闭包,结束之后 name 被回收
closure();
// 如果再次调用,那么就会报错
// closure() // 报错,name 已被释放
}
所以这也是它叫 FnOnce 的原因,就是因为它在剥夺外部变量所有权的时候只能调用一次。但如果没有转移所有权,那么需要使用 move 关键字将变量移动到闭包内部,举个例子:
fn test1() -> impl FnOnce() {
let name = String::from("我是闭包");
// 在闭包中将 name 赋值给了 new_name
// 显然所有权会发生转移,这是很合理的
|| {
let new_name = name;
}
}fn test2() -> impl FnOnce() {
let name = String::from("我是闭包");
// 由于闭包内部用的都是 name 的引用
// 所以这时候不会发生所有权的转移
// 但很明显这种情况是会出问题的
// 因为后续使用闭包的时候,name 已经被销毁了
// 所以这里要通过 move,手动地将变量转移至闭包内部
move || {
let new_name = &name; // name 的引用
// 这里拿到的也是 name 的引用
println!("{}", name);
}
}
所以实现 FnOnce trait 的闭包,能够转移捕获的外部变量的所有权。而如果不涉及所有权的转移,也就是通过引用的方式使用了外部变量,那么我们需要通过 move 关键字将变量移动到闭包内部,否则后续使用闭包的时候就会出问题。
注意:我们上面捕获的变量是 String 类型,它没有实现 Copy trait,所以一直反复提到所有权。但如果捕获的变量实现了 Copy trait,那么一定要加 move,将变量移动到闭包内部。
Fn
实现此 trait 的闭包在使用外部变量的时候,需要满足一个条件:不可以在闭包内部夺走外部变量的所有权,必须以不可变引用的方式进行操作。
IDE 提示的很明显,我们不能在闭包内部转移外部变量的所有权,即便使用 move 关键字将变量移动到了闭包内部。不过对于实现了 Copy trait 的变量来说是可以的,因为我们说这种变量的数据都完全分配在栈上,传递之后数据都是各自独立的,不存在所有权的转移。
但对于涉及到堆内存的变量来说就不一样了,由于 Rust 默认不会拷贝堆上数据,于是会转移所有权。但对于实现 Fn trait 的闭包,Rust 不允许转移外部变量的所有权,因此只能以引用的方式操作。
fn test() -> impl Fn() {
let name = "satori".to_string();
let age = 17;
let c = move || {
// 虽然在定义闭包的时候,将name移动到了闭包内部
// 但在使用 name 的时候,仍必须以引用的方式
// 否则它的所有权就会转移给 new_name
// 而 Rust 不允许这种情况出现
let new_name = &name;
// age 是可 Copy 的,不涉及所有权的转移
// 因此使用 age 和 &age 均可
let new_age = age;
};
// age 依然有效,它是可 Copy 的
// 在移动的时候会拷贝一份,所以不影响
println!("{}", age);
// 但 name 就不行了,因为它被移动到了闭包内部
c
}所以 Fn、FnOnce、FnMut 描述的都是闭包将以何种方式去捕获外部变量。对于当前的 Fn 来说,只能以引用的方式去捕获,而不能夺走所有权,即使我们使用 move 关键字将变量移动到了闭包内部。
FnMut
它和 Fn 一样都只能获取外部变量的引用,但 Fn 在使用的时候只能拿到不可变引用,而 FnMut 还可以获取可变引用。
test1 函数是不合法的,实现 Fn trait 的闭包不可以获取外部变量的可变引用。
fn test() -> impl FnMut() -> String {
let mut name = "komeiji".to_string();
move || {
let new_name = &mut name;
new_name.push_str(" satori");
name.clone()
}
}fn main() {
// 这里闭包也要声明为可变的
let mut c = test();
println!("{}", c());
println!("{}", c());
println!("{}", c());
/*
komeiji satori
komeiji satori satori
komeiji satori satori satori
*/
// name 的所有权自始至终在闭包内部
// 每一次调用都会对其进行修改
}
注意这段代码,里面有几个需要注意的地方。我们在闭包内部创建变量 new_name 的时候,只能将 name 的引用赋值给它,而不能是 name 本身,那样的话所有权就发生转移了。然后要修改字符串,那么 name 必须可变,new_name 也要是可变引用。
最后我们返回了 name.clone(),问题来了,直接返回 name 不行吗?答案是不行,对于 Fn 和 FnMut 来说,Rust 不允许在闭包内部转移外部变量的所有权,纵使这个外部变量被移动到了闭包内部。所以返回 name 是不合法的,因为那样所有权就发生转移了。
再来写一个计数器的例子,我们先看用 Go 如何去写:
package mainimport (
"fmt"
)
// ticker 函数返回了一个闭包
// 闭包在调用时会返回一个 uint32
// 而闭包返回的 uint32 来自 ticker 里的 start 变量
func ticker() func() uint32 {
var start uint32 = 0
return func() uint32 {
start++
return start
}
}
func main() {
var tk = ticker()
fmt.Println(tk()) // 1
fmt.Println(tk()) // 2
fmt.Println(tk()) // 3
}
还是很简单的,Go 的变量没啥可变不可变,所以读起来就非常简单,然后是 Rust。
fn ticker() -> impl FnMut() -> u32 {
// 后续要修改 start,start 必须可变
let mut start = 0;
// 闭包也要是可变的。然后还要使用 move 关键字
// 将 start 移动到闭包内部,这是必须的
// 否则后续使用闭包的时候,会发现 start 已被销毁
let mut c = move || {
start += 1;
start
};
c
}fn main() {
// 想在闭包内修改外部变量,则需要使用 FnMut
// 拿到闭包之后,也要声明为可变的
let mut c = ticker();
println!("{}", c());
println!("{}", c());
println!("{}", c());
/*
1
2
3
*/
}
以上我们就用 Rust 实现了一个定时器。
关于 Fn、FnOnce、FnMut 我们再做一下总结:
如果你需要转移外部变量的所有权,使用 FnOnce,并且闭包只能调用一次;
如果你不需要转移所有权,并且也不需要修改外部变量,那么使用 Fn;
如果你不需要转移所有权,但是需要修改外部变量,那么使用 FnMut;
另外为避免出现悬空引用,记得使用 move 关键字将变量移动到闭包内部。一般来说,如果你将闭包作为了返回值,那么基本上都会使用 move。
泛型
再来看一看泛型,泛型可以出现在结构体、枚举、函数、方法等结构中,用于增强类型的表达能力,以及减少重复代码。
// 结构体使用泛型
struct Point<T, W> {
x: T,
y: W,
z: i32
}
// 枚举使用泛型
enum Cell<T> {
Foo(T),
Bar(String)
}// 函数使用泛型
fn func<T>(a: T, b: T) {}
用到了哪些泛型,必须提前用尖括号声明好。
struct T;// S1 的第一个元素的类型为单元结构体 T
struct S1(T);
// S2 的第一个元素的类型为泛型 T
// 这个 T 可以代表任意类型,至于具体代表哪一种
// 则看在实例化的时候,传递的值是什么
struct S2<T>(T);
我们来实例化测试一下:
struct T;
struct S1(T);
struct S2<T>(T);fn main() {
let s1 = S1(T{});
// 对单元结构体来说,写成 T 也是可以的
// 这里我们将类型也指定一下
let s1: S1 = S1(T);
// 然后是 S2 的实例化
let s2 = S2(123);
let s2 = S2("hello");
let s2 = S2(String::from("hello"));
// 由于 S2 接收的是泛型 T,所以它可以代表任意类型
// 这里的 T 是单元结构体 T
// 如果没有定义这个单元结构体,那么这里就会报错
let s2 = S2(T);
// 然后是指定类型,在指定类型的同时
// 还要指定泛型所代表的类型
let s2: S2<u8> = S2(33);
let s2: S2<&str> = S2("hello");
let s2: S2<Vec<f32>> = S2(vec![3.14, 2.71, 1.414]);
}
因此这便是泛型,它可以代表任意类型,这样极大地增强了数据的表达能力。接下来,我们将带泛型的结构体放到函数参数里面。
struct S<T>(T);// 因为结构体 S 带了一个泛型 T,
// 所以在指定类型的时候,光有一个 S 是不够的
// 我们还要将泛型指定好,比如 S<u32>,
// 那么就只有当 S 实例的第一个元素为 u32 类型时,
// 才能传给这里的 s 参数
fn f1(s: S<u32>) {
println!("f1 被调用")
}
// 针对的是 S(f64)
fn f2(s: S<f64>) {
println!("f2 被调用")
}
fn main() {
f1(S(123)); // f1 被调用
f2(S(3.14)); // f2 被调用
}
然后函数自身也是可以有泛型的。
struct S<T>(T);// 泛型只是一个符号而已,它可以代表任意类型
// 但具体是哪一种,则取决于我们传递的值
// 另外这里为了区分,刻意写成了 W,但写成 T 仍是可以的
fn f<W>(s: S<W>) {
println!("函数被调用")
}
fn main() {
f(S(123));
f(S(3.14));
f(S("hello"));
/*
函数被调用
函数被调用
函数被调用
*/
}
函数 f 带了一个泛型 W,而参数 s 的类型是 S<W>。这就说明在调用函数的时候,只要传一个结构体 S 的实例即可,而不必管元素是什么类型,因为泛型可以代表任意类型。
以上在确定泛型的时候,都是根据值推断出来的,我们也可以显式地告诉 Rust。
fn test<T>(s: T) {
println!("函数被调用")
}fn main() {
// 调用时 T 会代表 i32
test(123);
// 调用时 T 会代表 u8
test(123u8);
test::<u8>(123);
}
通过 test::<u8> 则可以告诉编译器,在调用的时候,泛型 T 代表的是 u8 类型。当然啦,此时传递的也必须是一个合法的 u8 类型的值。
泛型不影响性能,Rust 在编译期间会将泛型 T 替换成具体的类型,这个过程叫做单态化。
方法中的泛型
再来看看方法中的泛型。
struct Point<T, U> {
x: T,
y: U
}// 针对 i32、f64 实现的方法
// 只有传递的 T、U 对应 i32、f64 才可以调用
impl Point<i32, f64> {
fn m1(&self) {
println!("我是 m1 方法")
}
}
fn main() {
let p1 = Point{x: 123, y: 3.14};
p1.m1(); // 我是 m1 方法
let p2 = Point{x: 3.14, y: 123};
//p2.m1();
//调用失败,因为 T 和 U 不是 i32、f64,而是 f64、i32
//所以 p2 无法调用 m1 方法
}
可能有人好奇了,声明方法的时候不考虑泛型可不可以,也就是 impl Point {}。答案是不可以,如果结构体中有泛型,那么声明方法的时候必须指定。但这就产生了一个问题,那就是只有指定类型的结构体才能调用方法。
比如上述代码,只有当 x 和 y 分别为 i32、f64 时,才可以调用方法,如果我希望所有的结构体实例都可以调用呢?
struct Point<T, U> {
x: T,
y: U
}// 针对 K、f64 实现的方法,由于 K 是一个泛型
// 所以它可以代表任何类型(泛型只是一个符号)
// 因此不管 T 最终是什么类型,i32 也好、&str 也罢
// K 都可以接收,只要 U 是 f64 即可
// 然后注意:如果声明方法时结构体后面指定了泛型
// 那么必须将使用的泛型在 impl 后面声明
impl <K> Point<K, f64> {
fn m1(&self) {
println!("我是 m1 方法")
}
}
// 此时 K 和 S 都是泛型,那么此时对结构体就没有要求了
// 因为不管 T 和 W 代表什么,K 和 S 都能表示,因为它们都是泛型
impl <K, S> Point<K, S> {
fn m2(&self) {
println!("我是 m2 方法")
}
}
// 这里我们没有使用泛型,所以也就无需在 impl 后面声明
// 但很明显,此时结构体实例如果想调用 m3 方法
// 那么必须满足 T 是 u8,W 是 f64
impl Point<u8, f64> {
fn m3(&self) {
println!("我是 m3 方法")
}
}
fn main() {
// 显然 p1 可以同时调用 m1 和 m2 方法,但 m3 不行
// 因为 m3 要求 T 是一个 u8,而当前是 &str
let p1 = Point{x: "hello", y: 3.14};
p1.m1(); // 我是 m1 方法
p1.m2(); // 我是 m2 方法
// 显然 p2 可以同时调用 m1、m2、m3
// 另外这里的 x 可以直接写成 123,无需在结尾加上 u8
// 因为 Rust 看到我们调用了 m3 方法,会自动推断为 u8
let p2 = Point{x: 123u8, y: 3.14};
p2.m1(); // 我是 m1 方法
p2.m2(); // 我是 m2 方法
p2.m3(); // 我是 m3 方法
// 显然 p3 只能调用 m2 方法,因为 m2 对 T 和 W 没有要求
// 但是像 m3 就不能调用,因为它是为 <u8, f64> 实现的方法
// 只有当 T、W 为 u8、f64 时才可以调用
// 显然此时是不满足的,因为都是 &str,至于 m1 方法也是同理
// 所以 p3 只能调用 m2,这个方法是为 <K, S> 实现的
// 而 K 和 S 也是泛型,可以代表任意类型,因此没问题
let p3 = Point{x: "3.14", y: "123"};
p3.m2(); // 我是 m2 方法
}
然后注意:我们上面的泛型本质上针对的还是结构体,而我们定义方法的时候也可以指定泛型,其语法和在函数中定义泛型是一样的。
#[derive(Debug)]
struct Point<T, U> {
x: T,
y: U,
}// 使用 impl 时 Point 后面的泛型名称可以任意
// 比如我们之前起名为 K 和 S,但这样容易乱,因为字母太多了
// 所以建议:使用 impl 时的泛型和定义结构体时的泛型保持一致即可
impl<T, U> Point<T, U> {
// 方法类似于函数,它是一个独立的个体,可以有自己独立的泛型
// 然后返回值,因为 Point 里面是泛型,可以代表任意类型
// 那么自然也可以是其它的泛型
fn m1<V, W>(self, a: V, b: W) -> Point<U, W> {
// 所以返回值的成员 x 的类型是 U,那么它应该来自于 self.y
// 成员 y 的类型是 W,它来自于参数 b
Point { x: self.y, y: b }
}
}
fn main() {
// T 是 i32,U 是 f64
let p1 = Point { x: 123, y: 3.14 };
// V 是 &str,W 是 (i32, i32, i32)
println!("{:?}", p1.m1("xx", (1, 2, 3)))
// Point { x: 3.14, y: (1, 2, 3) }
}
以上就是 Rust 的泛型,当然在工作中我们不会声明的这么复杂,这里只是为了更好地掌握泛型的语法。
然后注意一下方法里面的 self,不是说方法的第一个参数应该是引用吗?理论上是这样的,但此处不行,而且如果写成 &self 是会报错的,会告诉我们没有实现 Copy 这个 trait。
之所以会有这个现象,是因为我们在返回值当中将 self.y 赋值给了成员 x。那么问题来了,如果方法的第一个参数是引用,就意味着结构体在调用完方法之后还能继续用,那么结构体内部所有成员的值都必须有效,否则结构体就没法用了。这和动态数组相似,如果动态数组是有效的,那么内部的所有元素必须都是有效的,否则就可能访问非法的内存。
因此在构建返回值、将 self.y 赋值给成员 x 的时候,就必须将 self.y 拷贝一份,并且还要满足拷贝完之后数据是各自独立的,互不影响。如果 self.y 的数据全部在栈上(可 Copy 的),那么这是没问题的;如果涉及到堆,那么只能转移 self.y 的所有权,因为 Rust 默认不会拷贝堆数据,但如果转移所有权,那么方法调用完之后结构体就不能用了,这与我们将第一个参数声明为引用的目的相矛盾。
所以 Rust 要求 self.y 必须是可 Copy 的,也就是数据必须都在栈上,这样才能满足在不拷贝堆数据的前提下,让 self.y 赋值之后依旧保持有效。但问题是 self.y 的类型是 U,而 U 代表啥类型 Rust 又不知道,所以 Rust 认为 U 不是可 Copy 的,或者说没有实现 Copy 这个 trait,于是报错。
因此第一个参数必须声明为 self,此时泛型是否实现 Copy 就不重要了,没实现的话会直接转移所有权。因为该结构体实例在调用完方法之后会被销毁,不再被使用,那么此时可以转移内部成员的所有权。正所谓人都没了,还要这所有权有啥用,不如在销毁之前将成员值的所有权交给别人。
使用泛型和使用具体类型的速度是一样的,因此这就要求 Rust 在编译的时候能够推断出泛型的具体类型,所以类型要明确。
枚举中的泛型
枚举也是支持泛型的,比如之前使用的 Option<T> 就是一种泛型,它的结构如下:
enum Option<T> {
Some(T),
None
}里面的 T 可以代表任意类型,然后我们再来自定义一个枚举。
// 签名中的泛型参数必须都要使用
// 比如函数签名的泛型,要全部体现在参数中
// 枚举和结构体签名的泛型,要全部体现在成员中
enum MyOption<A, B, C> {
// 这里 A、B、C 都是我们随便定义的,可以代指任意类型
// 具体是哪种类型,则看我们传递了什么
Some1(A),
Some2(B),
Some3(C),
}fn main() {
// 泛型不影响效率,是因为 Rust 要进行单态化
// 所以泛型究竟代表哪一种类型要提前确定好
// 这里必须要显式指定 x 的类型。枚举和结构体不同
// 结构体每个成员都要赋值,所以 Rust 能够基于赋的值推断出所有的泛型
// 但枚举的话,每次只会用到里面的一个成员
// 如果还有其它泛型,那么 Rust 就无法推断了
// 比如这里只能推断出泛型 C 代表的类型,而 A 和 B 就无法推断了
// 因此每个泛型代表什么类型,需要我们手动指定好
let x: MyOption<i32, f64, u8> = MyOption::Some3(123);
match x {
MyOption::Some1(v) => println!("我是 i32"),
MyOption::Some2(v) => println!("我是 f64"),
MyOption::Some3(v) => println!("我是 u8"),
}
// 泛型可以代表任意类型,指定啥都是可以的
let y: MyOption<u8, i32, String> =
MyOption::Some3(String::from("xxx"));
match y {
MyOption::Some1(v) => println!("我是 u8"),
MyOption::Some2(v) => println!("我是 i32"),
MyOption::Some3(v) => println!("我是 String"),
}
/*
我是 u8
我是 String
*/
}
如果觉得上面的例子不好理解的话,那么再举个简单的例子:
enum MyOption<T> {
MySome1(T),
MySome2(i32),
MySome3(T),
MyNone
}fn main() {
// 这里我们没有指定 x 的类型
// 这是因为 MyOption 只有一个泛型
// 通过给 MySome1 传递的值,可以推断出 T 的类型
let x = MyOption::MySome1(123);
// 同样的道理,Rust 可以自动推断,得出 T 是 &str
let x = MyOption::MySome3("123");
// 但此处就无法自动推断了,因为赋值的是 MySome2 成员
// 此时 Rust 获取不到任何有关 T 的信息,无法执行推断
// 因此我们需要手动指定类型,但仔细观察一下声明
// 首先,如果没有泛型的话,那么直接 let x: MyOption = ... 即可
// 但里面有泛型,所以此时除了类型之外,还要连同泛型一起指定
// 也就是 MyOption<f64>
let x: MyOption<f64> = MyOption::MySome2(123);
// 当然泛型可以代表任意类型,此时的 T 则是一个 Vec<i32> 类型
let x: MyOption<Vec<i32>> = MyOption::MySome2(123);
}
所以一定要注意:在声明变量的时候,如果 Rust 不能根据我们赋的值推断出泛型代表的类型,那么我们必须要手动声明类型,来告诉 Rust 泛型的相关信息,这样才可以执行单态化。
trait
Rust 一切皆类型,并由 trait 掌握类型的行为逻辑,我们看个例子。
#[derive(Debug)]
struct Point<T> {
x: T,
}impl<T> Point<T> {
fn m(&self) {
let var = self.x;
}
}
fn main() {
let p = Point { x: 123 };
}
你觉得这段代码有问题吗?不用想,肯定是有问题的。因为方法 m 的第一个参数是引用,这就意味着方法调用完毕之后,结构体实例依旧保持有效,也意味着实例的所有成员值都保持有效。
但在方法 m 里面,我们将成员 x 的值赋给了变量 var。如果成员 x 的类型不是可 Copy 的,也就是数据不全在栈上,还涉及到堆,那么就会转移所有权,因为 Rust 默认不会拷贝堆数据。所以调用完方法 m 之后,成员 x 的值不再有效,进而使得结构体不再有效。
所以 Rust 为了避免这一点,在赋值的时候强制要求 self.x 的类型必须是可 Copy 的,但泛型 T 可以代表任意类型,它不满足这一特性。或者说 T 最终代表的类型是不是可 Copy 的,Rust 是不知道的,所以 Rust 干脆认为它不是可 Copy 的。
那么问题来了,虽然 T 可以代表任意类型,但如果我们赋的值决定了 T 代表的类型一定是可 Copy 的,那么可不可以告诉 Rust,让编译器按照可 Copy 的类型来处理呢?答案是可以的,而实现这一功能的机制就叫做 trait。
给泛型施加约束
trait 类似于 Go 里面的接口,相当于告诉编译器,某种类型具有哪些可以与其它类型共享的功能。
#[derive(Debug)]
struct Point<T> {
x: T,
}
// 这里便给泛型 T 施加了一个 Copy 的约束
// 此时的 T 代表的是实现了 Copy trait 的任意类型
impl<T: Copy> Point<T> {
fn m(&self) {
let var = self.x;
}
}fn main() {
let p = Point { x: 123 };
}
此时这段代码就没问题了,因为 T 是可 Copy 的。并且当调用 p.m 的时候,编译器会检查成员 x 的值是不是可 Copy 的,如果不是,则不允许调用 p.m 方法。
以上是在定义方法时,在 impl 块里面指定了 trait,但结构体里面的泛型也是可以指定 trait 的。
use std::fmt::Display;
use std::fmt::Debug;// 成员 x 的类型要实现 Display
// 成员 y 的类型要实现 Debug
struct Point<T: Display, W: Debug> {
x: T,
y: W
}
fn main() {
// Display 要求变量能够以 "{}" 的方式打印
// Debug 要求变量能够以 "{:?}" 的方式打印
let s1 = Point{x: 123, y: 345};
let s2 = Point{x: 123, y: (1, 2, 3)};
// 下面的 s3 不合法,因为元组没有实现 Display
// let s3 = Point{x: (1, 2, 3), y: 123};
}
所以这就是 trait,它描述了类型的行为。如果我们希望某个类型的变量不受限制,但必须满足某个特性,那么便可以给泛型施加一些制约。比如定义一个函数,接收两个相同类型的变量,变量具体是什么类型不重要,只要能够比较大小即可。
fn compare<T: PartialOrd>(a: T, b: T) -> String {
if a > b {
String::from("大于")
} else if a < b {
String::from("小于")
} else {
String::from("等于")
}
}
fn main() {
println!("{}", compare(1, 2));
println!("{}", compare(3.14, 2.71));
println!("{}", compare("a", "b"));
println!("{}", compare("你好", "你好"));
/*
小于
大于
小于
等于
*/
}如果 compare 里面的 T 不使用 trait,那么它可以代表任意类型,显然会报错,因为不是所有类型的变量都能够比较。于是便可以使用 trait 给 T 施加一些约束,PartialOrd 表示 T 代表的类型必须能够比较。
如果在调用 compare 时,传递的值不能比较,那么编译器就能够检查出来,从而报错。
自定义 trait
标准库内置了很多 trait,但我们也可以自定义 trait。
#[derive(Debug)]
struct Girl {
name: String,
age: i32
}// trait 类似 Go 里面的接口
// 然后里面可以定义一系列的方法
// 这里我们创建了一个名为 Summary 的 trait
// 并在内部定义了一个 summary 方法
trait Summary {
// trait 里面的方法只需要写声明即可
fn summary(&self) -> String;
}
// Go 里面只要实现了接口里的所有方法,便实现了该接口
// 但是在 Rust 里面必须显式地指明实现了哪一个 trait
// impl Summary for Girl 表示为类型 Girl 实现 Summary 这个 trait
impl Summary for Girl {
fn summary(&self) -> String {
// format! 宏用于拼接字符串,它的语法和 println! 一样
// 并且这两个宏都不会获取参数的所有权
// 比如这里的 self.name,format! 拿到的只是引用
format!("name: {}, age: {}", self.name, self.age)
}
}
fn main() {
let g = Girl{name: String::from("satori"), age: 16};
println!("{}", g.summary()); // name: satori, age: 16
}
所以 trait 里面的方法只需要写上声明即可,实现交给具体的结构体来做。当然啦,trait 里面的方法也是可以有默认实现的。
#[derive(Debug)]
struct Girl {
name: String,
age: i32
}trait Summary {
// 我们给方法指定了具体实现
fn summary(&self) -> String {
String::from("hello")
}
}
impl Summary for Girl {
// 如果要为类型实现 trait,那么要实现 trait 里面所有的方法
// 这一点和 Go 的接口是相似的,但 Go 里面实现接口是隐式的
// 只要你实现了某个接口所有的方法,那么默认就实现了该接口
// 但在 Rust 里面,必须要显式地指定实现了哪个 trait
// 同时还要实现该 trait 里的所有方法
// 但 Rust 的 trait 有一点特殊,Go 接口里面的方法只能是定义
// 而 trait 里面除了定义之外,也可以有具体的实现
// 如果 trait 内部已经实现了,那么这里就可以不用实现
// 不实现的话则用 trait 的默认实现,实现了则调用我们实现的
// 因此这里不需要定义任何的方法,它依旧实现了 Summary 这个 trait
// 只是我们仍然要通过 impl Summary for Girl 显式地告诉 Rust
// 如果只写 impl Girl,那么 Rust 则不认为我们实现了该 trait
}
fn main() {
let g = Girl{name: String::from("satori"), age: 16};
// 虽然没有 summary 方法,但因为实现了 Summary 这个 trait
// 而 trait 内部有 summary 的具体实现,所以不会报错
// 但如果 trait 里面的方法只有声明没有实现,那么就必须要我们手动实现了
println!("{}", g.summary()); // hello
}
总结一下就是 trait 里面可以有很多的方法,这个方法可以只有声明,也可以同时包含实现。如果要为类型实现某个 trait,那么要通过 impl xxx for 进行指定,并且实现该 trait 内部定义的所有方法。但如果 trait 的某个方法已经包含了具体实现,那么我们也可以不实现,会使用 trait 的默认实现。
trait 作为参数
假设有一个函数,只要是实现了 info 方法,都可以作为参数传递进去,这时候应该怎么做呢?
struct Girl {
name: String,
age: i32,
}struct Boy {
name: String,
age: i32,
salary: u32,
}
trait People {
fn info(&self) -> String;
}
// 为 Girl 和 Boy 实现 People 这个 trait
impl People for Girl {
fn info(&self) -> String {
format!("{} {}", &self.name, self.age)
}
}
impl People for Boy {
fn info(&self) -> String {
format!("{} {} {}", &self.name, self.age, self.salary)
}
}
// 定义一个函数,注意参数 p 的类型
// 如果是 p: xxx,则表示参数 p 的类型为 xxx
// 如果是 p: impl xxx,则表示参数 p 的类型任意,只要实现了xxx这个trait即可
fn get_info(p: impl People) -> String {
p.info()
}
fn main() {
let g = Girl {
name: String::from("satori"),
age: 16,
};
let b = Boy {
name: String::from("可怜的我"),
age: 26,
salary: 3000,
};
// 只要实现了 People 这个 trait
// 那么实例都可以作为参数传递给 get_info
println!("{}", get_info(g)); // satori 16
println!("{}", get_info(b)); // 可怜的我 26 3000
}
然后以 trait 作为参数的时候,还有另外一种写法,我们之前一直在用:
// 如果是 <T> 的话,那么 T 表示泛型,可以代表任意类型
// 但这里是 <T: People>,那么就不能表示任意类型了
// 它表示的应该是实现了 People 这个 trait 的任意类型
fn get_info<T: People>(p: T) -> String {
p.info()
}以上两种写法是等价的,但是第二种写法在参数比较多的时候,可以简化长度。
fn get_info<T: People>(p1: T, p2: T) -> String {}
// 否则话要这么写
fn get_info(p1: impl People, p2: impl People) -> String {
}
当然啦,一个类型并不仅仅可以实现一个 trait,而是可以实现任意多个 trait。
struct Girl {
name: String,
age: i32,
gender: String
}trait People {
fn info(&self) -> String;
}
trait Female {
fn info(&self) -> String;
}
// 不同的 trait 内部可以有相同的方法
impl People for Girl {
fn info(&self) -> String {
format!("{} {}", &self.name, self.age)
}
}
impl Female for Girl {
fn info(&self) -> String {
format!("{} {} {}", &self.name, self.age, self.gender)
}
}
// 这里在 impl People 前面加上了一个 &
// 表示调用的时候传递的是引用
fn get_info1(p: &impl People) {
println!("{}", p.info())
}
fn get_info2<T: Female>(f: &T) {
println!("{}", f.info())
}
fn main() {
let g = Girl {
name: String::from("satori"),
age: 16,
gender: String::from("female")
};
get_info1(&g); // satori 16
get_info2(&g); // satori 16 female
}
不同 trait 内部的方法可以相同也可以不同,而 Girl 同时实现了People 和 Female 两个 trait,所以它可以传递给 get_info1,也可以传递给 get_info2。然后为 trait 实现了哪个方法,就调用哪个方法,所以两者的打印结果不一样。
那么问题来了,如果我在定义函数的时候,要求某个参数同时实现以上两个 trait,该怎么做呢?
// 我们只需要使用 + 即可
// 表示参数 p 的类型必须同时实现 People 和 Female 两个 trait
fn get_info1(p: impl People + Female) {
// 但由于 Poeple 和 Female 里面都有 info 方法
// 此时就不能使用 p.info() 了,这样 Rust 不知道该使用哪一个
// 应该采用下面这种做法,此时需要手动将引用传过去
People::info(&p);
Female::info(&p);
}// 如果想接收引用的话,那么需要这么声明
// 因为优先级的原因,需要将 impl People + Female 整体括起来
fn get_info2(p: &(impl People + Female)) {}
// 或者使用泛型的写法
fn get_info3<T: People + Female>(p: T) {}
最后还有一个更加优雅的写法,在介绍闭包的时候用过:
// 显然这种声明方式要更加优雅,如果没有 where 的话
// 那么这个 T 就是可以代表任意类型的泛型
// 但这里出现了 where
// 因此 T 就表示实现了 People 和 Female 两个 trait 的任意类型
fn get_info<T>(p: T)
where
T: People + Female
{
}如果要声明多个实现 trait 的泛型,那么使用逗号分隔。
fn get_info<T, W>(p1: T, p2: W)
where
T: People + Female,
W: People + Female
{
}可以看出,Rust 的语法表达能力还是挺丰富的。
trait 作为返回值
trait 也是可以作为返回值的。
struct Girl {
name: String,
age: i32,
gender: String,
}trait People {
fn info(&self) -> String;
}
impl People for Girl {
fn info(&self) -> String {
format!("{} {}", &self.name, self.age)
}
}
fn init() -> impl People {
Girl {
name: String::from("satori"),
age: 16,
gender: String::from("female"),
}
}
fn main() {
let g = init();
println!("{}", g.info()); // satori 16
}
注意代码中的变量 g,它具体是什么类型 Rust 其实是不知道的。init 返回的不是一个具体的类型,因为一个 trait 可以有很多种类型实现,返回任意一个都是可以的。这个时候我们应该显式地指定变量 g 的类型,即 let g: Girl = init()。
最后 trait 也是可以带泛型的。
struct Number(i32);trait SomeTrait<T: PartialOrd> {
fn compare(&self, n: T) -> bool;
}
// 为 Number 实现 T 为 i32 的 SomeTrait
impl SomeTrait<i32> for Number {
fn compare(&self, n: i32) -> bool {
self.0 >= n
}
}
fn main() {
let num = Number(66);
println!("{:?}", num.compare(67)); // false
println!("{:?}", num.compare(65)); // true
}
以上就是 trait 相关的内容,如果一个 trait 里面没有定义任何方法,那么相当于不起任何约束作用,也就是空约束。
小结
本篇文章我们复习了函数、闭包、泛型和 trait,内容还是蛮多的,建议私底下也可以多动手敲一敲。
总而言之,Rust 的学习门槛确实不是一般的高,但努力总有收获。