Rust 基础知识24 - 高级特性

模式匹配

  • 本章涉及:

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<Self::Item>;
}
impl Iterator for Counter {
    type Item = u32;
    fn next(&mut self) -> Option<Self::Item> { ... }
}
  • 关联类型看起来很像泛型,那么二者有什么区别呢?

为了解决这个问题我们先看一下如果用泛型定义接口会是什么样子:

// 泛型定义 Iterator 接口
pub trait Iterator<T> {
    fn next(&mut sel) -> Option<T>;
}

// 此时实现这个泛型版本的 Iterator 大致如此
impl Iterator<u32> for Counter {
    fn next(_: &mut _) -> _ {
        todo!()
    }
}
  • 所以用关联类型的好处就是,不需要在每次调用Counter的next方法时来显示的生命这是一个u32类型的迭代器。

默认泛型参数和运算符重载

  • 可以在定义泛型时通过语法<PlaceholderType=ConcreteType>来为泛型指定默认类型。
  • 这个技术常常被用于运算符重载上面。
  • 参考下面的例子:
#[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<Rhs = Self> {
    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<i32> 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<Meters> 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)。
  • 完全限定语法的格式是:<Type as Trait>::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 我们需要完全限定语法 <Type as Trait>::function()
    println!("Animal: {}", <Dog as 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<T>实现Display。
  • 但是我们可以为先创建一个持有Vec<T>实例的Wrapper结构体,然后便可以为Wrapper实现Display并使用Vec<T>值了。
use std::fmt;
use std::fmt::Formatter;

// 创建一个包装结构
struct Wrapper(Vec<String>);

// 说白了就是虽然无法直接给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<dyn Fn()+Send+'static>;
// 这样就变得方便多了。

  • 另外一个例子就是,Result<T,E> 经常使用别名来减少代码量:
type Result<T> = Result<T, std::io::Error>;
// 因为std::io::Error是重复且不变的,所以这里直接用 Result<T> 替代它。
pub trait Write {
    fn write(&mut self, buf: &[u8]) -> Result<unsize>;
    fn flush(&mut self) -> Result<unsize>;
}

永不返回的Never类型

  • Rust 有一个名为!的特殊类型,他在类型系统中的术语为空类型(empty type),因为它没有任何值。
// 读作,函数bar永远不会返回值。
fn bar() -> ! {
    
}
  • 不会返回值的函数也被称作发散函数(diverging function)
  • 为了深入了解先看一下段错误的代码,并仔细看里面的注释:
/// 这段代码Rust 是无法编译的。
/// 因为Rust 无法推断出 guress 到底是什么类型。
/// 即便标注了,Rust 不能确保 guress 就一定是i32。
/// 所以看看用于不会返回值的类型,也就是 Never 类型吧。
fn main() {
    let v:Vec<Result<i32,&str>> = 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<Result<i32,&str>> = 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<dyn Trait>
Rc<dny Trait>
  • 为了处理动态大小类型,Rust还提供了一个特殊的Sized trait来确定一个类型的大小在编译时是否可知。
  • 另外,Rust还会为每一个泛型函数隐式的添加Sized约束。
  • 例子:
fn generic<T>(t: T) {} 
会被隐式的转换为:
fn generic<T: Sized>(t: T) {}
  • 默认情况下,泛型函数只能被用于在编译时已经知道大小的类型,但是可以通过如下所示的特殊语法来解除这一限制:
// ?Sized trait 约束表达了与Sized 相反的含义。
fn generic<T: ?Sized> (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<String> = 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<String> = 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<Status> = (0u32..20)
        // 用函数参数初始化枚举列表。
        .map(Status::Value)
        .collect();
}

返回闭包

  • 因为闭包没有一个可供返回的具体类型,所以你无法直接返回闭包。
  • 事实上可以通过封装一个trait 对象来解决此问题。
  • 无法通过编译的例子:
// 这个根本就编译不过去
fn return_closure() -> Fn(i32) ->i32 {
    |x|x+1
}
  • 封装到trait 对象上试试,比如Box<T>
fn return_closure() -> Box<dyn Fn(i32)->i32>{
    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.

推荐阅读更多精彩内容