26.高级特性(上)

发布于:2024-06-29 ⋅ 阅读:(5) ⋅ 点赞:(0)

一、不安全的Rust

  • Rust隐藏有第二种语言,它没有强制内存安全保证,这被称为不安全Rust(unsafe Rust);
  • 不安全Rust存在的原因:
    • 静态分析是保守的,使用unsafe Rust则是告诉编译器自己知道在做啥;
    • 计算机硬件本身是不安全的, Rust需要能够进行底层系统编程;

二、不安全的超能力

2.1 概念

  • 可以通过unsafe关键字将Rust切换到不安全Rust,开启一个块,存放不安全代码;
  • Unsafe Rust里执行的五种操作
    1. 解引用裸指针;
    2. 调用不安全的函数或方法;
    3. 访问或修改可变静态变量;
    4. 实现不安全trait;
    5. 访问union的字段;
  • unsafe 并不会关闭借用检查器或禁用任何其他安全检查;
  • unsafe 关键字只是提供了那五个不会被编译器检查内存安全的功能;
  • 隔离unsafe代码,最好将它们封装进一个安全的抽象并提供安全API;

2.2 解引用裸指针

  • 可变的原始指针: *mut T;
  • 不可变的原始指针: *const T;
  • 不可变意味着指针解引用之后不能直接赋值;
  • *不是解引用运算符,它是类型名称的一部分;
  • 裸指针与引用和智能指针的区别:
    • 允许忽略借用规则,可以同时拥有不可变和可变的指针,或多个指向相同位置的可变指针;
    • 不保证指向有效的内存;
    • 允许为空;
    • 不能实现任何自动清理功能;
fn main() {
    let mut num = 5;

    let r1 = &num as *const i32;
    let r2 = &mut num as *mut i32;
    let address = 0x012345usize;
    let r = address as *const i32;
//    println!("r1 is: {}", *r1);
//    println!("r2 is: {}", *r2);
}
  • 代码同时创建不可变和可变裸指针;
  • 可以在安全代码中创建裸指针,但是解引用必须在unsafe代码块里;
  • 使用as将不可变和可变引用强制转换为对应的裸指针类型;
  • address是一个不能确定其有效性的裸指针;
  • 放开最后两行注释而直接进行解引用会产生错误;

在这里插入图片描述

  • 使用unsafe {}把他们包裹起来就能正确运行了;
  • 为什么要用原始指针?
    • 与C语言进行交互;
    • 构建借用检查器无法理解的安全抽象;

2.3 调用不安全的函数或方法

  • unsafe函数或方法:在定义前加上unsafe关键字;
  • 调用unsafe函数要先满足条件(看文档),Rust无法对这些条件进行验证;
  • 需要在unsafe块里调用;
unsafe fn dangerous() {}

fn main() {
   dangerous();
}
  • 编译错误
  • 在main函数里,用unsafe{} 将dangerous函数调用包裹起来就能通过了;

在这里插入图片描述

2.3 创建不安全代码的安全抽象

  • 函数包含不安全代码并不意味着整个函数都需要标记为不安全;
  • 将不安全代码封装进安全函数是一个常见的抽象;
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..])
}

fn main() {
    let mut v = vec![1, 2, 3, 4, 5, 6];
    let (a, b) = split_at_mut(&mut v[..], 3);
    
    println!("a = {:?}", a);
    println!("b = {:?}", b);
}
  • 上述代码中split_at_mut函数将传入的可变切片进行分割,返回两个可变切片;
  • 编译报错

在这里插入图片描述

  • Rust的借用检查器任何代码将同一个变量借用了两次;
  • 代码里明确了两个切片不会重叠,这就要触及不安全代码了;
  • 这就需要用unsafe块,裸指针和一些不安全函数调用来修改split_at_mut
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))
    }
}
  • 使用len方法获取切片的长度,使用as_mut_ptr获取访问切片的裸指针;
    • slice变量是一个i32类型的可变切片,因此as_mut_ptr返回一个*mut i32类型的裸指针,储存在 ptr 变量中;
  • slice::from_raw_parts_mut函数获取一个裸指针和一个长度来创建一个切片;
  • 无需将split_at_mut函数标记为unsafe,且该函数可以在安全的Rust中被调用;

下面这段代码使用slice时会崩溃

use std::slice;

fn main() {
    let address = 0x01234usize;
    let r = address as *mut i32;
    
    let slice: &[i32]  = unsafe {
        slice::from_raw_parts_mut(r, 10000)
    };

    println!("slice = {:#?}", slice);
}

编译正确,运行崩溃
在这里插入图片描述

2.4 使用extern函数调用外部代码

  • extern函数可以创建和使用外部函数接口;
  • extern块中声明的函数在 Rust 代码中总是不安全的;
  • 外部函数接口是一个编程语言用以定义函数的方式,其允许不同(外部)编程语言调用这些函数。
extern "C" {
    fn abs(input: i32) -> i32;
}

fn main() {
    unsafe {
        println!("Absolute value of -3 according to C: {}", abs(-3));
    }
}
  • 上述代码展示了如何集成C标准库中的abs函数;
  • extern "C"块中,列出了要调用的C语言中的外部函数;

从其它语言调用Rust函数

  • 使用extern创建一个允许其他语言调用 Rust 函数的接口;
  • 还需增加#[no_mangle]标注;
    • Mangling是指当编译器将代码中指定的函数名进改时会增加一些额外的信息;
    • 每一个编程语言的编译器都会以稍微不同的方式mangle函数名;
#[no_mangle]
pub extern "C" fn call_from_c() {
    println!("Just called a Rust function from C!");
}
  • 将上述代码编译为动态库并从C语言中链接,call_from_c函数就能够在 C 代码中访问;
  • extern 的使用无需使用unsafe标注;

2.5 访问或修改可变静态变量

  • 如果有两个线程访问相同的可变全局变量,则可能会造成数据竞争;
  • 全局变量在 Rust 中被称为 静态(static)变量
  • 通常静态变量的名称采用SCREAMING_SNAKE_CASE写法;
  • 静态变量只能储存拥有static生命周期的引用,因此静态变量的生命周期可以被Rust自动计算;
  • 访问不可变静态变量是安全的,访问和修改可变静态变量都是不安全的;
static mut COUNTER: u32 = 0;
static HELLO_WORLD: &str = "Hello, world!";

fn add_to_count(inc: u32) {
    unsafe {
        COUNTER += inc;
    }
}

fn main() {
    add_to_count(3);

    unsafe {
        println!("COUNTER: {}", COUNTER);
    }

    println!("name is: {}", HELLO_WORLD);
}
  • 代码展示了不可变静态变量HELLO_WORLD的声明和使用;
  • 任何访问或修改可变静态变量COUNTER的代码都必须位于unsafe中;

2.6 实现不安全trait

  • 当 trait 中至少有一个方法中包含编译器无法验证的不变式(invariant)时 trait 是不安全的;
  • 在 trait 之前增加 unsafe 关键字将 trait 声明为 unsafe,同时 trait 的实现也应该标记为unsafe;

2.7 访问联合体中的字段

参考文档:https://rustwiki.org/zh-CN/reference/items/unions.html

三、高级trait

3.1 关联类型在trait定义中指定占位符类型

  • 关联类型(associated types) 是一个将类型占位符与 trait 相关联的方式,这样 trait 的方法参数中就可以使用这些占位符类型;
  • trait的实现者会针对特定的实现在这个类型的位置指定相应的具体类型;
  • 标准库提供的Iterator trait就是一个带有关联类型的trait,内部的关联类型Item替代遍历的值的类型;
pub trait Iterator {
    type Item;

    fn next(&mut self) -> Option<Self::Item>;
}
  • Item是占位符,next的返回值说明它返回Option<Self::Item>类型的值;
  • trait的实现者指定Item的具体类型;
  • 用起来像泛型;

3.2 关联类型与泛型的区别

泛型 关联类型
每次实现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
    }
}
  • 上述代码为 Counter实现了Iterator2的trait,返回值为String和u32;
pub trait Iterator{
    type Item;

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

struct Counter{}

impl Iterator for Counter{
    
    type Item = u32;
    fn next(&mut self) -> Option<Self::Item> {
        None    
    }
}
  • 上述代码实现了关联类型,只能写一个,如果再写一遍impl Iterator for Counter{,里面的Item写成String,就会报错;

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

  • 可以在使用泛型参数的时候为泛型指定一个默认的具体类型;
  • 语法为:<PlaceholderType=ConcreteType>
  • 这种技术通常用于运算符重载
  • 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 });
}
  • 上述代码在Point结构体上实现Add trait来重载+运算符;
  • Add trait定义如下
#[doc(alias = "+")]
#[const_trait]
pub trait Add<Rhs = Self> {
    /// The resulting type after applying the `+` operator.
    #[stable(feature = "rust1", since = "1.0.0")]
    type Output;

    #[must_use = "this returns the result of the operation, without modifying the original"]
    #[rustc_diagnostic_item = "add"]
    #[stable(feature = "rust1", since = "1.0.0")]
    fn add(self, rhs: Rhs) -> Self::Output;
}
  • Rhs = Self默认类型参数
  • RHS是一个泛型类型参数,它用于定义 add 方法中的 rhs 参数;
  • 如果实现 Add trait 时不指定 RHS 的具体类型,RHS 的类型将是默认的 Self 类型,也就是Point;
  • 下面是一个实现Add trait时使用自定义类型而不是默认类型的例子;
use std::ops::Add;

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))
    }
}
  • 这是将毫米和米相加的例子;
  • 所以用Add<Meters>指明是米,相加的时候传进来的是米,所以乘以1000再相加;
  • 返回值是毫米;

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

  • 扩展一个类型而不破坏现有代码;
  • 允许在大部分用户都不需要的特定场景下进行自定义;

3.4 完全限定语言与消歧义:如何调用相同名称的方法

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;
    person.fly();
}
  • PilotWizard都有fly方法;
  • Human又定义了一个fly
  • 几个方法的参数是完全相同的,那么调用哪个?
    在这里插入图片描述
  • 所以调用的是Human本身的fly方法;
  • 如下代码演示了调用Pilot和Wizard的fly方法;
fn main() {
    let person = Human;
    Pilot::fly(&person);
    Wizard::fly(&person);
    person.fly();
}
  • 当同一作用域的两个类型都实现了同一trait,Rust就不能明确的知道调用哪个函数;
  • 使用完全限定语法(fully qualified syntax) 可以解决这个问题;
  • 语法为:<Type as Trait>::function(receiver_if_method, next_arg, ...);
    • 可以在任何调用函数或方法的地方使用;
    • 允许忽略那些从其它上下文能推导出来的部分;
    • 当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::baby_name()); //A baby dog is called a Spot
    //println!("A baby dog is called a {}", Animal::baby_name());
}
  • Animal trait有关联函数baby_name,结构体 Dog 实现了 Animal,都是一些关联方法(没有self);
  • baby_name直接在Dog之上,因此使用Dog::baby_name直接调用就可以;
  • 放开最后一个println!,则无法编译通过;
    在这里插入图片描述
  • 使用完全限制语法解决它:println!("A baby dog is called a {}", <Dog as Animal>::baby_name());

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

  • 需要在一个trait中使用其它trait的功能;
    • 需要被依赖的trait也被实现
    • 那个被间接依赖的trait就是当前trait的supertrait;
use std::fmt;

trait OutlinePrint: fmt::Display {
    fn outline_print(&self) {
        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 {}
  • OutlinePrint trait里的outline_print函数要被使用,则必须实现fmt::Displaytrait;
  • 结构体Point实现了OutlinePrint,但它没有实现Displaytrait,所以会报错;
    在这里插入图片描述
  • 在Point上实现了Display后就会通过编译;
impl fmt::Display for Point {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "({}, {})", self.x, self.y)
    }
}

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

  • 孤独规则: 只有当trait或类型定义在本地包时,才能为该类型实现这个trait;
  • 可以通过newtype模式绕过这个规则;
    • 利用tuple struct元组结构体创建一个新的类型;
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);
}
  • 想在Vec<T>上实现Display确被孤独规则阻止(Display trait和Vec<T>都定义于外面的包中);
  • 可以创建一个包含Vec<T>实例的 Wrapper 结构体, 在它之上实现 Display 并使用Vec<T>的值;

网站公告

今日签到

点亮在社区的每一天
去签到