模式匹配
- 本章涉及:
1、不安全Rust,舍弃Rust 的某些安全保障并负责手动维护相关规则。
2、高级trait,关联类型、默认类型参数、完全限定语法(full qualified syntax)超 trait(supertrait),以及与trait 相关的 newtype 模式。
3、高级类型:更多关于newtype模式的内容、类型别名、never 类型和动态大小类型。
4、搞基函数和闭包:函数指针与返回闭包。
5、宏,在编译初期生成更多代码的方法。
知识汇总
不安全Rust
- 可以在代码块前使用关键字 unsafe 来切换到不安全模式,并在被标记后的代码块中使用不安全的代码。
- Rust 允许执行4中在安全Rust中不被允许的操作,也就是(unsafe superpower)
1、解引用裸指针。
2、调用不安全的函数或方法。
3、访问或修改可变的静态变量。
4、实现不安全trait。
解引用裸指针
- 裸指针要么是可变的要么是不可变的,分别被写作
*const T
和*mut T
- 举例:
fn main() {
let mut num =5 ;
// 创建裸指针并不需要unsafe 标记,使用时才需要,如下代码是可以编译通过的。
// 因为创建他们并不危险,使用它们时才会发生危险。
let r1 = &num as *const i32;
let r2 = &mut num as * mut i32;
}
读取裸指针中的变量
- 这需要一个 unsafe {} 块。
fn main() {
let mut num =5 ;
let r1 = &num as *const i32;
let r2 = &mut num as *mut i32;
// 裸指针的使用必须在 unsafe 块中进行
unsafe {
// 注意使用时不要忘记用*进行解引用。
println!("r1 value : {}", *r1);
println!("r2 value : {}", *r2);
}
}
调用不安全函数或方法
- unsafe 关键字可以加在方法 fn 前面,用来签名该函数是不安全的。
- 例子:(需要注意使用的时候,也一定是在unsafe块中否则无法使用。)
unsafe fn dangerous() {}
fn main() {
// 使用unsafe 方法也必须在 unsafe 块中
unsafe {
dangerous();
}
}
- 需要特别注意的是,代码中包含不安全的代码,但是并不意味着我们需要将整个函数标记为不安全。
自己实现一个 split_at_mut
- 这可以让你体验一下,为什么要使用不安全的裸指针
- 函数中不能简单的用 (&mut slice[..mid],&mut slice[mid..]) 作为返回值,因为,同一个 slice 不能被两次可变引用切割,Rust编译器无法知道这个是否安全。
- 为了解决这个需要裸指针的帮忙,当然还需要
slice::from_raw_parts_mut()
函数的帮忙: - 实例代码如下,建议敲一遍:
use std::slice;
fn main() {
let mut v = vec![1,2,3,4,5,6];
let r = &mut v[..];
// let (a,b) = r.split_at_mut(3);
let (a,b) = split_at_mut(r, 4);
println!("a = {:?}", a);
println!("b = {:?}", b);
}
// 自制切割函数
fn split_at_mut(slice: &mut [i32], mid: usize) -> (&mut [i32], &mut [i32]) {
let len = slice.len();
assert!( mid <= len) ;
// 下面三行代码无法工作,注释掉了,Rust 无法是被同一个数组被切了两次。
// let left_list = &mut slice[..mid];
// let rigth_list = &mut slice[mid..];
// (left_list,rigth_list)
// 返回这个Vec[i32]的裸指针
let ptr = slice.as_mut_ptr();
unsafe {
// 这里注意,ptr 是一个指针 from_raw_parts_mut 会返回 ptr 位置开始向后移动mid 个位置的数组
let left_arr = slice::from_raw_parts_mut(ptr, mid);
// 这里面要注意 ptr.offset(mid as isize) 的意思是把指针的位置,偏移 mid 个位置,第二个参数 = (总长度 - 中值)其实就是剩余长度。
let rigth_arr = slice::from_raw_parts_mut(ptr.offset(mid as isize), len - mid);
(left_arr, rigth_arr)
}
}
使用extern函数调用外部代码
- 某些场景下Rust代码可能需要与另外一种语言进行交互,Rust为此提供了extern关键字来简化创建和使用外部接口。
- FFI(Foreign Function Interface)是编程语言定义函数的一种方式,它允许其他编程语言来调用这些函数。
- 任何在 extern 块中声明的函数都是不安全的。
- 举例来说:
use std::slice;
extern "C" {
// 定义一个调用abs 函数的 extern
fn abs(input: i32) -> i32;
}
fn main() {
unsafe {
println!("Absolute value of -3 according to C: {}", abs(-3));
}
}
- 我们可以同样使用 extern 来创建一个允许其他语言调用Rust 函数的接口,这需要将 extern 关键字及对应的ABI添加到函数签名的fn关键字前。
访问或修改一个可变静态变量
- 常亮需要用 static 关键字进行定义,且约定俗成的用 全大写字母。
- 访问一个不可变的静态变量是安全的。
- 静态变量和常亮的区别
1、静态变量的值在内存中的位置是固定的地址,使用他们的值总会访问到相同的数据。
2、常亮则允许在任何被使用到的时候复制其数据?不太明白。
3、常亮和静态变量之间的另一个区别在于静态变量是可变的。
4、访问和修改可变的静态变量是不安全的。
- 例子:任何读写常亮都是不安全的,所以需要放到 unsafe{} 中,单线程下面的代码没问题,但是如果是多线程就会发生数据竞争。
static mut COUNTER : u32 = 0;
fn add_to_count(inc:u32) {
unsafe {
COUNTER += inc;
}
}
fn main() {
add_to_count(3);
unsafe {
println!("COUNTER: {}", COUNTER);
}
}
高级trait
实现不安全trait
- 当某个trait 中存在至少一个方法拥有编译器无法校验的不安全因素时,我们就成这个trait是不安全的。
- 需要在这个trait前面加上unsafe关键字来声明一个不安全的trait
- 对应的实现也需要加上 unsafe 关键字。
在trait的定义中使用关联类型制定占位类型。
- 关联类型(associated type)是trait中的类型占位符,它可以被用于trait的方法签名中。
- trait的实现者需要根据特定的场景来为关联类型制定具体的类型。
- 通过这一技术我们可以定义出包含某些类型的trait,而无需在实现前确定他们的具体类型是什么。
- 最典型的一个例子就是 Iterator trait 的定义:
pub trait Iterator {
// 这里的 type Item 就是一个占位符
type Item;
fn next(&mut self) -> Option;
}
impl Iterator for Counter {
type Item = u32;
fn next(&mut self) -> Option { ... }
}
- 关联类型看起来很像泛型,那么二者有什么区别呢?
为了解决这个问题我们先看一下如果用泛型定义接口会是什么样子:
// 泛型定义 Iterator 接口
pub trait Iterator {
fn next(&mut sel) -> Option;
}
// 此时实现这个泛型版本的 Iterator 大致如此
impl Iterator for Counter {
fn next(_: &mut _) -> _ {
todo!()
}
}
- 所以用关联类型的好处就是,不需要在每次调用Counter的next方法时来显示的生命这是一个u32类型的迭代器。
默认泛型参数和运算符重载
- 可以在定义泛型时通过语法
来为泛型指定默认类型。 - 这个技术常常被用于运算符重载上面。
- 参考下面的例子:
#[derive(Debug, 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}, "重载 + 运算符测试。")
}
- 上面的测试成功了,然后让我们欣赏一下 trait Add 的接口是怎么写的:
// Rhs = Self 表示默认类型参数(default type parameter),加入在实现Add trait 的过程中
// 没有为Rhs制定一个具体的类型,那么Rhs 的类型就默认为 Self,也就是我们正在为其实现的那个类型
// Rsh = (right-handle side),
pub trait Add {
type Output;
fn add(self, rhs: Rhs) -> Self::Output;
}
- 上面的接扣看明白了实际上我们还可以让 Point 和一个 i32 进行相加,比如:
use std::ops::Add;
#[derive(Debug, PartialEq)]
struct Point {
x: i32,
y: i32,
}
// 给u32 实现一个,让他可以和一个u32字符相加。
impl Add for Point {
type Output = Point;
fn add(self, rhs: i32) -> Self::Output {
Point {
x: self.x + rhs,
y: self.y + rhs,
}
}
}
fn main() {
assert_eq!(Point {x:1, y:0} + 6i32, Point {x:7,y:6})
}
- 再举一个例子,有两个存放数据的结构体 Millimeters 与 Meters
- 我们希望将毫米与米进行相加,这是后就可以通过实现Add接口来实现重载 + 运算符。
- 例子:(如下例子有了上面的基础并不难理解,这是书上的例子非常好。)
use std::ops::Add;
#[derive(Debug, PartialEq)]
struct Millimeters(u32);
#[derive(Debug, PartialEq)]
struct Meters(u32);
impl Add for Millimeters {
type Output = Millimeters;
fn add(self, rhs: Meters) -> Self::Output {
Millimeters(rhs.0 * 1000 + self.0)
}
}
fn main() {
let mer_num = Meters(3) ; //3米
let millmer_num = Millimeters(5000) ; //5000毫米
assert_eq!(millmer_num + mer_num , Millimeters(8000) , "一共等于8000毫米");
}
用于消除歧义的完全限定语法:(调用相同名称的方法)
- Rust 既不会组织两个 trait 拥有相同名称的方法,也不会阻止你为同一个类型实现这样的两个 trait。
- 甚至可以已在这个类型上直接实现与trait 方法同名的方法。
- 只要你明确的告诉Rust 你期望调用的具体对象,Rust就可以自动推导出应该执行的具体方法。
- 举例:
use std::ops::Add;
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 man = Human {};
Pilot::fly(&man);
Wizard::fly(&man);
Human::fly(&man); // 如此调用和下面是一样的,只不过稍长。
man.fly();
}
// 屏幕输出如下:
// This is your captain speaking.
// Up!
// * waving arms furiously*
// * waving arms furiously*
- 然而因为trait 中关联函数没有self参数,所以当在同一作用域下有两个实现了此种trait的类型时,Rust无法推到导出你究竟想要调用哪一个具体的类型,除非使用完全限定语法(fully qualified syntax)。
- 完全限定语法的格式是:
例子:::function()
use std::ops::Add;
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!("Dog: {}", Dog::baby_name());
// 如下调用会出错,因为Rust 无法推断到底想要调用哪个Anamil的实现方法。
// println!("Anamil: {}", Animal::baby_name());
// 为了可以调用到 Anamil for Dog 我们需要完全限定语法 ::function()
println!("Animal: {}", ::baby_name());
}
// 如上代码输出:
// Dog: Spot
// Animal: Puppy
用于在trait中附带另外一个trait功能的超trait
- 这种写法实际上类似一种约束,请看下面的例子 trait OutlinePrint: fmt::Display{}
- 如果想实现 OutlinePrint 则原对象必须先实现 fmt::Display 否则无法实现 OutlinePrint
use std::fmt;
use std::fmt::Formatter;
trait OutlinePrint: fmt::Display {
fn outline_print(&self) {
// 因为所有实现了 fmt::Display 接口的具体类都实现了 to_string()
// 所以这里才可以放心的调用 .to_string() ,如果没有上面的 :fmt::Display 那么直接调用 self.to_string 就会报错。
let output = self.to_string() ;
let len = output.len();
println!("{}", "*".repeat(len + 4));
println!("*{}*", " ".repeat(len + 2 ));
println!("* {} *", output);
println!("*{}*", " ".repeat(len + 2));
println!("{}", "*".repeat(len + 4));
}
}
// 定义一个结构
struct Point {
x: i32,
y: i32,
}
/// 如果Point 没有实现:Display 那么就不能实现 OutlinePrint trail
/// 因为 OutlinePrint 依赖 fmt::Display 的实现。
impl fmt::Display for Point {
// 这里不要忘了加上 fmt::Result 否则它会定义到标准库下的 Result 枚举中
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(f, "({}, {})", self.x, self.y)
}
}
/// 如果Point 没有实现:Display 就直接实现 OutlinePrint trail就会得到如下的一个错误:
/// the trait bound `Point: Display` is not satisfied [E0277]
/// the trait `Display` is not implemented for `Point`
impl OutlinePrint for Point {}
fn main() {
let p = Point { x: 10, y:200};
p.outline_print()
}
// 上面的程序输出:
// *************
// * *
// * (10, 200) *
// * *
// *************
使用newtype模式在外部类型上实现外部trait
- 孤儿原则提到,只有当类型和对应trait中的人一个定义在本地包内,我们才能够为该类型实现这一trait。
- 例如这会阻止我们直接为Vec
实现Display。 - 但是我们可以为先创建一个持有Vec
实例的Wrapper结构体,然后便可以为Wrapper实现Display并使用Vec 值了。
use std::fmt;
use std::fmt::Formatter;
// 创建一个包装结构
struct Wrapper(Vec);
// 说白了就是虽然无法直接给Vec实现fmt::Display 接口
// 但是我们可以给包装类实现嘛,这样一来就绕过去了。
impl fmt::Display for Wrapper {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
write!(f, "[{}]",self.0.join(", "))
}
}
fn main() {
let w = Wrapper(vec![String::from("Hello"), String::from("world")]);
println!("w = {}", w);
}
高级类型
- 首先讨论更为通用的newtype 模式,该模式作为类型在某些场景下十分有用。
- 接着,我们会把目光移至类型别名,它与newtype类似但拥有不同语义。
使用newtype模式实现类型安全与抽象
- newtype 模式用来静态的保证各种值之间不会被混淆及表明值使用的单位。
- newtype 模式的另外一个用途是为类型的某些细节提供抽象能力。
- newtype 模式还可以被用来隐藏内部实现。
// 如下两种都是创建了 newtype 从而实现更好的安全保障。
struct Millimeters(u32);
struct Meters(u32);
使用类型别名创建同义类型
- 除了newtype 模式,Rust 还提供了创建类型别名(type alias) 的功能。
- 他可以为现有的类型生成另外的别名。
// type alias 模式
type Kilometers = i32;
// 现在别名 Kilometers 被视作了 i32 的同义词。
let x: i32 = 5;
let y: Kilometers = 5;
// 直接相加也是没问题的。
println!("x+y={}", x+y);
- 类型别名的最主要用途是减少代码字符重复,例如:
type Thunk = Box;
// 这样就变得方便多了。
- 另外一个例子就是,Result
经常使用别名来减少代码量:
type Result = Result;
// 因为std::io::Error是重复且不变的,所以这里直接用 Result 替代它。
pub trait Write {
fn write(&mut self, buf: &[u8]) -> Result;
fn flush(&mut self) -> Result;
}
永不返回的Never类型
- Rust 有一个名为!的特殊类型,他在类型系统中的术语为空类型(empty type),因为它没有任何值。
// 读作,函数bar永远不会返回值。
fn bar() -> ! {
}
- 不会返回值的函数也被称作发散函数(diverging function)
- 为了深入了解先看一下段错误的代码,并仔细看里面的注释:
/// 这段代码Rust 是无法编译的。
/// 因为Rust 无法推断出 guress 到底是什么类型。
/// 即便标注了,Rust 不能确保 guress 就一定是i32。
/// 所以看看用于不会返回值的类型,也就是 Never 类型吧。
fn main() {
let v:Vec> = vec![Ok(5), Ok(6), Err("PassMe"), Ok(8)];
for x in v.iter() {
let guess:i32 = match *x {
Ok(num) => num,
Err(_) => "What", // 类型和OK的不一样就不能成功
};
println!("Guess num : {}", guess);
}
}
- 但是 continue 就可以。
// 这就是 Never 类型的作用了,记住这个概念就可以了,这也就是为什么虽然 Err(_) 返回的不是 i32 但是仍然可以通过编译的原因,因为 Never 真的不一样。
fn main() {
let v:Vec> = vec![Ok(5), Ok(6), Err("PassMe"), Ok(8)];
for x in v.iter() {
let guess:i32 = match *x {
Ok(num) => num,
Err(_) => continue,
};
println!("Guess num : {}", guess);
}
}
// 程序运行结果:
// Guess num : 5
// Guess num : 6
// Guess num : 8
动态大小类型和Sized trait
- str 正好就是一个动态大小类型(注意说的不是&str)
- 这也意味着我们无法创建一个str类型的变量,或者使用str类型作为函数参数。
- 思考一下吧《这里要西西体会什么事需要指针,因为指针的大小是确定的》。
- 如下代码无法正常工作:
fn main() {
let s1: str = "Hello there!";
let s2: str = "How's it going?";
}
// 如上会报错:
// let s2: str = "How's it going?";
// | ^^ doesn't have a size known at compile-time
- Rust 需要在编译的时候确定某个特定类型的值究竟占据多少内存,而同一类型的所有值都必须使用等量的内存。
- 为了使用trait对象来存储不同类型的值(trait实现的对象大小是不固定的)我们必须将它放置某种指针之后。
// 比如:
&dyn Trait
Box
Rc
- 为了处理动态大小类型,Rust还提供了一个特殊的Sized trait来确定一个类型的大小在编译时是否可知。
- 另外,Rust还会为每一个泛型函数隐式的添加Sized约束。
- 例子:
fn generic(t: T) {}
会被隐式的转换为:
fn generic(t: T) {}
- 默认情况下,泛型函数只能被用于在编译时已经知道大小的类型,但是可以通过如下所示的特殊语法来解除这一限制:
// ?Sized trait 约束表达了与Sized 相反的含义。
fn generic (t: &T) {
}
- ?Sized trait 约束表达了与Sized 相反的含义,我们可以读作"T"可能是也可能不是Sized的。另外这个语法只能用在Sized上,而不能用在于其他trait。
- 因为类型可能不是Sized的,所以我们需要将它放置在某种指针的后面。
- 也就是说如果对应的类型大小是不可知的那么就需要放到指针后面。
高级函数与闭包
- 除了传递闭包,普通函数也可以当做参数传入到另一个函数中。
- 函数在传递的过程中被强制转换成fn 类型。
- 例子:
fn add_one (x: i32) -> i32 {
x + 1
}
fn do_twice(f: fn(i32)->i32 , argnum: i32 ) -> i32 {
f(argnum) + f(argnum)
}
fn main() {
let result = do_twice(add_one, 30);
println!("Result is {}", result);
}
- 与闭包不同 fn 是一个类型而不是一个trait。因此,可以直接指定fn 为参数类型,而不是声明一个以Fn trait 为约束的泛型参数。
- 由于函数指针实现了全部3种闭包trait(Fn、FnMut、以及FnOnce)所以总是可以把函数指针用作参数,因此更倾向于这样做。
- 来看一个例子:
fn main() {
let list_of_numbers = vec![1,2,3];
let list_of_strings : Vec = list_of_numbers
.iter()
// 使用闭包,处理map函数
.map(|i| {i.to_string()})
.collect();
println!("使用闭包的结果:{:?}", list_of_strings);
let list_of_numbers = vec![1,2,3];
let list_of_strings : Vec = list_of_numbers
.iter()
// 使用函数处理map函数,注意这里,会如此展开:ToString::to_string(&str)
.map(ToString::to_string)
.collect();
println!("使用函数的结果:{:?}", list_of_strings);
}
// 返回:
// 使用闭包的结果:["1", "2", "3"]
// 使用函数的结果:["1", "2", "3"]
- 还有一种十分有用的模式,它利用了元祖结构体和元祖结构枚举变体实现细节。
- 看一段代码就大概懂了:
fn main() {
enum Status {
Value(u32),
Stop,
}
let list_of_statuses:Vec = (0u32..20)
// 用函数参数初始化枚举列表。
.map(Status::Value)
.collect();
}
返回闭包
- 因为闭包没有一个可供返回的具体类型,所以你无法直接返回闭包。
- 事实上可以通过封装一个trait 对象来解决此问题。
- 无法通过编译的例子:
// 这个根本就编译不过去
fn return_closure() -> Fn(i32) ->i32 {
|x|x+1
}
- 封装到trait 对象上试试,比如Box
fn return_closure() -> Boxi32>{
Box::new(|x| x + 1 )
}
fn main() {
let f = return_closure();
let num = f(6);
println!("num is : {:?}", num);
}
// 返回结果:
// num is : 7
宏
宏与函数的区别
- 简单的说宏就是可以生成代码的代码。
- 另外由于编译器会在解释代码前展开宏,所以宏可以被用来执行某些较为特殊的任务,比如为类型实现trait。
- 宏的缺点是,宏的定义要比函数复杂的多,宏定义通常要比函数定义更加难以阅读、理解和维护。
用于通用元编程的 macro_rules! 声明宏
- 举例子:
// #[macro_export] 意味着这个宏会在它所处的包被引入作用域后可用,少了它则宏不能被引入作用域。
#[macro_export]
macro_rules! vec2 {
// ($($x:expr),*) 很类似模式匹配
($($x:expr),*) => {
{
let mut temp_vec = Vec::new();
// 这里面的 $() 生成上面匹配的 $()*
$(
// $x 会被上面的$x 所替代。
temp_vec.push($x);
)*
temp_vec
}
};
}
fn main() {
let num = vec2!(1,2,3);
println!("{:?}", num);
}
如何编写一个自定义derive宏
- 构成红需要被单独放到它们自己的包中,需要了解下的点:
$ 当前的Rust版本(未来也许不需要这样)对于一个名为 foo 的包。
$ 必须要生成一个用于放置自定义派生过程宏的包 foo_derive。
$ 由于这两个包紧密相关,所以将他们放到了同一目录中,如果改变了 foo 中的 trait 定义,那么也需要痛要修改 foo_derive 中的相关定义
$ 使用他们应当分别添加这两个依赖并将他们导入作用域中。
$ 当然也可以让 foo 包依赖于 foo_derive 并重新导入过程宏代码。
- 另外一些需要注意的是,foo_derive/Cargo.toml 需要明确 [lib] 是一个过程宏的库。
[lib]
proc-macr = true
[dependencies]
syn = "0.14.4"
quote = "0.6.3"
- 这部分稍后补齐。
基于属性创建代码的过程宏
- 第二种形式的宏更像是函数,所以它们被称作过程宏。
结束
- 感谢阅读,See you at work.