Rust 学习笔记 - 高级特性

不安全 Rust

Rust 中隐藏着第二个语言,它没有强制内存安全保证:Unsafe Rust(不安全的 Rust)。和普通的 Rust 一样,但提供了额外的“超能力”。

Unsafe Rust 存在的原因:

  • 静态分析是保守的,使用 Unsafe Rust 就是告诉 Rust 我知道自己在做什么,并承担相应风险
  • 计算机硬件本身就是不安全的,Rust 需要能够进行底层系统编程,如果不使用 Unsafe Rust,这些工作就无法完成

Unsafe 超能力

使用 unsafe 关键字来切换到 unsafe Rust,开启一个块,里面放着 unsafe 代码。

Unsafe Rust 里可以执行四个动作(这就是 unsafe 超能力):

  1. 解引用原始指针
  2. 调用 unsafe 函数或方法
  3. 访问或修改可变的静态变量
  4. 实现 unsafe trait

注意:

  • unsafe 并没有关闭借用检查或停用其它安全检查,仅仅是让你可以访问上述 4 种不会被编译器进行内存安全检查的特性,所以即便是在 unsafe 代码块中你仍然可以获得一定程度的安全性
  • 任何内存安全相关的错误必须留在 unsafe 块里
  • 尽可能隔离 unsafe 代码,最好将其封装在安全的抽象里,提供安全的 API,很多标准库就是这么做的

解引用原始指针

原始指针(Raw Pointer):

  • 可变的:*mut T(注意 * 并非解引用符号,而是类型名的一部分)
  • 不可变的:*const T,意味着指针在解引用之后不难直接对其进行赋值

原始指针与引用的不同之处:

  • 允许通过同时具有不可变和可变指针或多个指向同一位置的可变指针来忽略借用规则
  • 无法保证能指向合理的内存
  • 原始指针允许为 null
  • 不实现任何自动清理

放弃保证的安全,换取更好的性能以及与其它语言或硬件接口的能力。

fn main() {
    let mut num = 5;

    let r1 = &num as *const i32; // 不可变原始指针
    let r2 = &mut num as *mut i32; // 可变原始指针

    unsafe {
        println!("r1: {}", *r1);
        println!("r2: {}", *r2);
    }

    let address = 0x012345usize; // 这个地址可能有数据也可能没数据,但是仍然可以创建原始指针
    let r = address as *const i32;

    unsafe {
        println!("r: {}", *r); // 不会报错,但是程序会异常
    }
}
复制代码

为什么要使用原始指针?

  • 与 C 语言进行接口
  • 构建借用检查器无法理解的安全抽象

调用 unsafe 函数或方法

unsafe 函数或方法在定义前加上了 unsafe 关键字。调用前需手动满足一些条件(主要靠看文档),因为 Rust 无法对这些条件进行验证,需要在 unsafe 块里进行调用。

unsafe fn dangerous() {}

fn main() {
    unsafe {
        dangerous();
    }
}
复制代码

创建 unsafe 代码的安全抽象:函数包含 unsafe 代码并不意味着需要将整个函数标记为 unsafe。将 unsafe 代码包裹在安全函数中是一个常见的抽象。

// 这原本是标准库的一个方法
fn split_at_mut(slice: &mut [i32], mid: usize) -> (&mut [i32], &mut [i32]) {
    let len = slice.len();

    assert!(mid <= len);

    (&mut slice[..mid], &mut slice[mid..]) // 报错,不能两次可变借用 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);
    assert_eq!(a, &mut [1, 2, 3]);
    assert_eq!(b, &mut [4, 5, 6]);
}
复制代码

使用 unsafe 代码修改

扫描二维码关注公众号,回复: 13619765 查看本文章
use std::slice;

fn split_at_mut(slice: &mut [i32], mid: usize) -> (&mut [i32], &mut [i32]) {
    let len = slice.len();
    let ptr = slice.as_mut_ptr();

    assert!(mid <= len);

    unsafe {
        (
            slice::from_raw_parts_mut(ptr, mid),
            slice::from_raw_parts_mut(ptr.add(mid), len - mid),
        )
    }
}
复制代码

使用 extern 函数调用外部代码

extern 关键字可以简化创建和使用外部函数接口(FFI)的过程。

外部函数接口(FFI,Foreign Function Interface):它允许一种编程语言定义函数,并让其它编程语言能调用这些函数。

extern "C" {
    fn abs(input: i32) -> i32;
}

fn main() {
    unsafe {
        println!("Abbsolute value of -3 according to C: {}", abs(-3));
    }
}
复制代码

应用二进制接口(ABI,Application Binary Interface):定义函数在汇编层的调用方式。

"C" ABI 是最常见的 ABI,它遵循 C 语言的 ABI。

从其它语言调用 Rust 函数

可以使用 extern 创建接口,其它语言通过它们可以调用 Rust 的函数。在 fn 前添加 extern 关键字,并指定 ABI。还需要添加 #[no_mangle] 注解:避免 Rust 在编译时改变它的名称。

#[no_mangle]
pub extern "C" fn call_from_c() {
    println!("Just called a Rust function from C!");
}

fn main() {}
复制代码

访问或修改一个可变静态变量

Rust 支持全局变量,但因为所有权机制可能产生某些问题,例如数据竞争。在 Rust 里面,全局变量叫做静态(static)变量。

静态变量与常量类似,命名规范是大写的 snake case(SCREAMING_SNAKE_CASE)。声明的时候必须标注类型。静态变量只能存储 'static 生命周期的引用,无需显式标注。访问不可变的静态变量是安全的。

常量和不可变静态变量的区别:

  • 静态变量:有固定的内存地址,使用它的值总会访问同样的数据
  • 常量:允许使用它们的时候对数据进行复制
  • 静态变量:可以是可变的,访问和修改静态可变变量是不安全(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);
    }
}
复制代码

实现不安全(unsafe)trait

当某个 trait 中存在至少一个方法拥有编译器无法校验的不安全因素时,就称这个 trait 是不安全的。

声明 unsafe trait:在定义前加 unsafe 关键字,该 trait 只能在 unsafe 代码块中实现。

unsafe trait Foo {
    // methods go here
}

unsafe impl Foo for i32 {
    // methods implementations go here
}

fn main() {}
复制代码

何时使用 unsafe 代码

  • 编译器无法保证内存安全,保证 unsafe 代码正确并不简单
  • 有充足理由使用 unsafe 代码时,就可以这样做
  • 通过显式标记 unsafe,可以在出现问题时轻松的定位

高级 Trait

在 Trait 定义中使用关联类型来指定占位类型

关联类型(associated type)是 Trait 中的类型占位符,它可以用于 Trait 的方法签名中:可以定义出包含某些类型的 Trait,而在实现前无需知道这些类型是什么。

// 标准库中的 Iterator 就是带有关联类型的 Trait
pub trait Iterator {
    type Item; // 关联类型,和泛型有点儿像

    fn next(&mut self) -> Option<Self::Item>;
}

fn main() {}
复制代码

关联类型与泛型的区别

泛型 关联类型
每次实现 Trait 时需要标注类型 无需标注类型
可以为一个类型多次实现某个 Trait(不同的泛型参数) 无法为单个类型多次实现某个 Trait
// 泛型的例子
pub trait Iterator2<T> {
    fn next(&mut self) -> Option<T>;
}

struct Counter {}

impl Iterator2<String> for Counter {
    fn next(&mut self) -> Option<String> {
        None
    }
}

impl Iterator2<u32> for Counter { // 可以实现多次
    fn next(&mut self) -> Option<u32> {
        None
    }
}
复制代码
// 关联类型例子
pub trait Iterator {
    type Item;

    fn next(&mut self) -> Option<Self::Item>;
}

impl Iterator for Counter { // 只能实现一次,否则会报错
    type Item = u32;

    fn next(&mut self) -> Option<Self::Item> {
        None
    }
}
复制代码

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

  • 可以在使用泛型参数时为泛型指定一个默认的具体类型。
  • 语法:<PlaceholderType=ConcreteType>
  • 这种技术常用于运算符重载(operator overloading
  • Rust 不允许创建自己的运算符及重载任意的运算符
  • 但可以通过实现 std::ops 中列出的那些 trait 来重载一部分相应的运算符
use std::ops::Add;

#[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});
}
复制代码
use std::ops::Add;
#[derive(Debug, PartialEq)]
struct Millimeters(u32);
struct Meters(u32);

impl Add<Meters> for Millimeters {
    type Output = Millimeters;

    fn add(self, other: Meters) -> Millimeters {
        Millimeters(self.0 + (other.0 * 1000))
    }
}
fn main() {
    assert_eq!(Millimeters(500) + Meters(1), Millimeters(1500));
    assert_eq!(Meters(1) + Millimeters(500), Millimeters(1500)); // 报错
}
复制代码

默认泛型参数的主要应用场景

  • 扩展一个类型而不破坏现有代码
  • 允许在大部分用户都不需要的特定场景下进行自定义(上面 Millimeters + Meters 案例)

完全限定语法(Fully Qualified Syntax)

调用同名方法。

trait Pilot {
    fn fly(&self);
}

trait Wizard {
    fn fly(&self);
}

struct Human;

impl Pilot for Human { // 又实现了 trait 的 fly
    fn fly(&self) {
        println!("This is your captain speaking.");
    }
}

impl Wizard for Human { // 又实现了 trait 的 fly
    fn fly(&self) {
        println!("Up!");
    }
}

impl Human { // 自己实现了 fly
    fn fly(&self) {
        println!("*waving arms furiously*");
    }
}

fn main() {
    let person = Human;
    person.fly(); // 调用了本身的方法
    Pilot::fly(&person);
    Wizard::fly(&person);
}
复制代码
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::baby_name());
    println!("A baby dog is called a {}", Animal::baby_name()); // 报错了
}
复制代码

上面这个错误可以通过完全限定语法解决:<Type as Trait>::function(receiver_if_method, netx_arg, ...);。这种语法可以在任何调用函数或方法的地方使用。它允许忽略那些从其它上下文能推导出来的部分。不过只有当 Rust 无法区分你期望调用哪个具体实现的时候,才需要使用这种语法。

println!("A baby dog is called a {}", Animal::baby_name());
// 修改为
println!("A baby dog is called a {}", <Dog as Animal>::baby_name());
复制代码

使用 supertrait 来要求 trait 附带其它 trait 的功能

有时候我们需要在一个 trait 中使用其它 trait 的功能,也就是需要被依赖的 trait 也被实现,那个被间接依赖的 trait 就是当前 trait 的 supertrait

use std::fmt;

trait OutlinePrint: fmt::Display { // supertrait
    fn outline_print(&self) {
        // 要求其实现类型有 to_string 方法,也就相对于要求要实现 Display 这个 trait
        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,
}

impl OutlinePrint for Point {}

impl fmt::Display for Point { // 这个 trait 不实现就会报错
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "({}, {})", self.x, self.y)
    }
}

fn main() {
    let p = Point { x: 1, y: 3 };
    p.outline_print();
}
复制代码

使用 newtype 模式在外部类型上实现外部 trait

孤儿规则:只有当 trait 或类型定义在本地包时,才能为该类型实现这个 trait。然而我们可以使用 newtype 模式来绕过这一规则。就是利用 tuple struct(元祖结构体)创建一个新的类型。

use std::fmt;
// 为 Vec 实现 Display 这个 trait
// 但是 Vec 和 Display 都定义在外部的包中,所以无法直接实现
// 将 Vec 包裹在 Wrapper 中就可以为 Wrapper 实现 Display trait 了
struct Wrapper(Vec<String>);

impl fmt::Display for Wrapper { // 这个 trait 不实现就会报错
    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);
}
复制代码

高级类型

使用 newtype 模式实现类型安全和抽象

newtype 模式可以:

  • 用来静态的保证各种值之间不会混淆并表明值的单位
  • 为类型的某些细节提供抽象能力
  • 通过轻量级的封装来隐藏内部实现细节

使用类型别名创建类型同义词

Rust 提供了类型别名的功能:为现有类型生成另外的名称(同义词),其并不是一个独立的类型,使用 type 关键字实现。

主要用途:减少代码字符重复。

type Kilometers = i32;
fn main() {
    let x: i32 = 5;
    let y: Kilometers = 5;
    println!("x + y = {}", x + y);
}
复制代码

下面这种情况,类型很复杂,而且到处都在用,很容易出错,就可以使用类型别名了

fn takes_long_type(f: Box<dyn Fn() + Send + 'static>) {
    // --snip--
}

fn returns_long_type() -> Box<dyn Fn() + Send + 'static> {
    Box::new(|| println!("hi"))
}
fn main() {
    let f: Box<dyn Fn() + Send + 'static> = Box::new(|| println!("hi"));
}
复制代码

修改如下:

type Thunk = Box<dyn Fn() + Send + 'static>;
fn takes_long_type(f: Thunk) {
    // --snip--
}

fn returns_long_type() -> Thunk {
    Box::new(|| println!("hi"))
}
fn main() {
    let f: Thunk = Box::new(|| println!("hi"));
}
复制代码

Never 类型

有一个名为 ! 的特殊类型:它没有任何值,行话称为空类型(empty type)。我们倾向于叫它 never 类型,因为它在不返回的函数中充当返回类型。不返回值的函数也被称作发散函数(diverging function)。

impl<T> Option<T> {
    pub fn unwrap(self) -> T {
        match self {
            Some(val) => val,
            None => panic!(""), // never 类型
        }
    }
}
复制代码
fn main() {
    println!("forever");

    loop {
        println!("and ever");
    }
}
复制代码

动态大小和 Sized Trait

动态大小

Rust 需要在编译时确定为一个特定类型的值分配多少空间。动态大小的类型(Dynamically Sized Types,DST)的概念:编写代码时使用只有在运行时才能确定大小的值。

str 是动态大小的类型(注意不是 &str):只有运行时才能确定字符串的长度。

下列代码无法正常工作:

let s1: str = "Hello";
复制代码

使用 &str 来解决:

let s1: &str = "Hello";
复制代码

使用动态大小类型时总会把它的值放在某种指针后边,附带一些额外的元数据来存储动态信息的大小。

另一种动态太小的类型就是 trait,每个 trait 都是一个动态大小的类型,可以通过名称对其进行引用,为了将 trait 用作 trait 对象,必须将它放置在某种指针之后。例如:&dyn TraitBox<dyn Trait> (Rc<dyn Trait>) 之后。

Sized trait

为了处理动态大小的类型,Rust 提供了一个 Sized trait 来确定一个类型的大小在编译时是否已知。编译时可计算出大小的类型会自动实现这一 trait。Rust 还会为每一个泛型函数隐式的添加 Sized 约束。

fn generic<T>(t: T) {}
// 上面会隐式转换为下面
fn generic<T: Sized>(t: T) {}
复制代码

默认情况下,泛型函数只能被用于编译时已经知道大小的类型,可以通过特殊语法解除这一限制:?Sized trait 约束。

// T 可能是也可能不是 Sized,这个语法只能用在 Sized 上面,不能被用于其它 trait
fn generic<T: ?Sized>(t: &T) {}
复制代码

高级函数和闭包

函数指针

可以将函数传递给其它函数,函数在传递过程中会被强制转换成 fn 类型。fn 类型就是“函数指针(function pointer)”。

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);
}
复制代码

函数指针与闭包的不同

  • fn 是一个类型,不是一个 trait,可以直接指定 fn 为参数类型,不用声明一个以 Fn trait 为约束的泛型参数。
  • 函数指针实现了全部3种闭包 trait(FnFnMutFnOnce),总是可以把函数指针用作参数传递给一个接收闭包的函数,所以,倾向于搭配闭包 trait 的泛型来编写函数:可以同时接收闭包和普通函数。

某些情景,只想接收 fn 而不接收闭包,与外部不支持闭包的代码交互,比如:C 函数。

fn main() {
    let list_of_numbers = vec![1, 2, 3];
    let list_of_strings: Vec<String> = list_of_numbers
    .iter()
    .map(|i| i.to_string()) // 闭包
    .collect();

    let list_of_numbers = vec![1, 2, 3];
    let list_of_string: Vec<String> = list_of_numbers
    .iter()
    .map(ToString::to_string) // 传函数
    .collect();
}
复制代码
fn main() {
    enum Status {
        Value(u32),
        Stop,
    }

    let v = Status::Value(3);

    let list_of_statuses: Vec<Status> = 
        (0u32..20)
        .map(Status::Value)
        .collect();
}
复制代码

返回闭包

闭包使用 trait 进行表达,无法在函数中直接返回一个闭包,可以将一个实现了该 trait 的具体类型作为返回值。

// 报错,Rust 无法推断出需要多少空间来存储这个闭包
fn returns_closure() -> Fn(i32) -> i32 {
    |x| x + 1
}

// 正确
fn returns_closure() -> Box<dyn Fn(i32) -> i32> {
    Box::new(|x| x + 1)
}
复制代码

宏(macro)

宏在 Rust 里指的是一种相关特性的集合称谓:使用 macro_rules! 构建的声明宏(declarative macro)以及 3 种过程宏。

3 种过程宏:

  • 自定义 #[derive] 宏,用于 structenum,可以为其指定随 derive 属性添加的代码。
  • 类似属性的宏,在任何条目上添加自定义属性
  • 类似函数的宏,看起来像函数调用,对其指定为参数的 token 进行操作

函数与宏的差别

本质上,宏是用来编写可以生成其它代码的代码(元编程,metaprogramming)。函数在定义签名时,必须声明参数的个数和类型,宏可处理可变的参数。编译器会在解释代码前展开宏。宏的定义比函数复杂得多,难以阅读、理解、维护。在某个文件调用宏时,必须提前定义宏或将宏引入当前作用域,而函数可以在任何位置定义并在任何位置使用。

macro_rules! 声明宏(弃用)

Rust 中最常见的宏形式:声明宏。类似 match 的模式匹配,定义声明宏的时候需要使用 macro_rules!

// let v: Vec<u32> = vec![1, 2, 3]
// 简化版的 vec! 宏
#[macro_export]
macro_rules! vec {
    ( $( $x:expr ),* ) => {
        {
            let mut temp_vec = Vec::new();
            ${
                temp_vec.push($x);
            }*
            temp_vec
        }
    };
}

// 实际生成并执行了下面的代码
let mut temp_vec = Vec::new();
temp_vec.push(1);
temp_vec.push(2);
temp_vec.push(3);
temp_vec
复制代码

声明宏其实一般用不到,不必研究太深入。

过程宏

基于属性来生成代码的过程宏,这种形式更像函数(某种形式的过程)一些,它会接收并操作输入的 Rust 代码,然后生成另外一些 Rust 代码作为结果。

三种过程宏:

  • 自定义派生
  • 属性宏
  • 函数宏

创建过程宏时,宏定义必须单独放在它们自己的包中,并使用特殊的包类型。

use proc_macro;

#[some_attribute]
pub fn some_name(input: TokenStream) -> TokenStream {
    
}
复制代码

就这样吧,后面的,以后再学,再写吧,初学者也用不着,关键我也不会了...

猜你喜欢

转载自juejin.im/post/7042199818369761294