Rust 智能指针

2022-09-19 rust language

Rust 中有所有权的概念,常见的赋值、函数传参、函数返回值等场景,都可能会发生所有权的转移,而某些场景下,只是想使用一下,所以就有了引用。

简介

引用类似于指针,可以用来传递地址,进而访问保存在该地址里的变量,不过指针可能是空指针,但引用可确保这个值是有效的。

当涉及到所有权时,就引入了借用的概念,将创建一个引用的行为称为借用,如名所示,只是借用,并没有这个变量的所有权,使用完之后需要再还回去。

如果要想修改引用所指向的变量,那么就有了可变引用。

fn length(s: &String) -> usize {
    s.len()
}

fn append(s: &mut String) {
    s.push_str(" World!!!")
}

fn main() {
    let hi = String::from("Hello");
    println!("Length of '{hi}' is {}", length(&hi));

    let mut hi = String::from("Hello");
    append(&mut hi);
    println!("{}", hi);
}

混搭

这样就涉及到了类似于 C/CPP 中与指针类似的问题,也就是 const * * const const *const 的区别,其对应到 Rust 的引用就是 &mut mut & mut &mut 的区别是什么。

  • &mut 可变变量的引用,变量可以修改,引用无法修改,当确定引用那个变量之后,就不能再引用其它变量了。
  • mut & 引用可变,可以引用 A 也可以引用 B,但是 A B 本身不可变。
  • mut &mut 上面两者的结合,引用和变量都可以修改。

示例如下。

fn main() {
    let data = String::from("Data0");
    let info = String::from("Hello");

    let refr = &data;
    println!("{}", refr); // Data0
    // _refr = &info; error: cannot assign twice to immutable variable

    let mut refw = &data;
    println!("{}", refw); // Data0
    refw = &info;
    println!("{}", refw); // Hello
    //refw.push_str(" World!!!"); error: the data it refers to cannot be borrowed

    let mut data = String::from("Hey");
    let mut info = String::from("Hello");

    let refw = &mut info;
    refw.push_str(" World!!!");
    println!("{}", refw); // Hello World!!!

    let mut refrw = &mut data;
    refrw.push_str(" World!!!");
    println!("{}", refrw); // Hey World!!!
    refrw = &mut info;
    refrw.push_str(" World!!!");
    println!("{}", refrw); // Hello World!!!
}

ref 关键字

除了上述的 & 操作之外,还支持 ref 来定义指针,区别在于声明变量时两者的位置不同,例如上述的定义 let refw = &mut info 等价于 let ref mut refw = info ,两者只在定义时有区别,使用时并无差异。

fn foobar1(x: &mut i32) {
    *x = 91;
}

fn foobar2(ref mut x: i32) {
    *x = 92;
}

fn foobar3(ref mut x: &mut i32) {
    **x = 93;
}

fn main() {
    let mut a: i32 = 0;
    foobar1(&mut a); // 可以修改变量
    println!("result = {}", a); // 91
    foobar2(a);      // 无法修改变量
    println!("result = {}", a); // 91
    foobar3(&mut a); // 可以修改,但使用比较复杂
    println!("result = {}", a); // 93
}

其中的 foobar2() 函数语义是将变量复制到栈空间,而 x 变量是栈所在数据的可变引用,所以,即使能够修改,修改的也是栈上的数据,对源变量也没有影响。

另外,还有一种场景的区别。

fn main() {
    let s = Some(String::from("hello"));

    // 如下这种方式意味着发生了所有权转移,后面的打印会报错
    // match s {
    //     Some(v) => println!("got value {}", v),
    //     None => {}
    // }

    // 意味着 match 只是借用了 s 变量,没有发生所有权转移
    match &s {
        Some(v) => println!("got value {}", v),
        None => {}
    }

    // 通过 ref 声明 v 是个指针,同样可以避免所有权转移
    match s {
        Some(ref v) => println!("got value {}", v),
        None => {}
    }
    println!("value is {}", s.unwrap());
}

解引用

与其它语言类似,通过 & 表示引用/借用,而 * 解引用,常规的引用是一个指针类型,包含了目标数据的内存地址。而且读取变量的时候,即使有多层引用,Rust 会自动进行解引用,智能指针也支持。

fn main() {
    let v = 5;
    let x = &v;
    let y = &&v;
    let z = &&&v;

    println!("v={} x={} y={} z={}", v, *x, **y, ***z);
    println!("v={} x={} y={} z={}", v, x, y, z);
}

最终都会输出 v=5 x=5 y=5 z=5 数据,其中的 y 就是一个常规引用,包含的是 5 所在的地址,那么通过解引用 *y 获取到了对应的值 5,如果执行 assert_eq!(5, y) 就会报错,因为引用和数值不是一个类型。

Deref 特征

上述的解引用根很多成熟语言比较类似,不过 Rust 有自己的扩展,如果结构他实现了 Deref 特征,那么也可以通过 * 方式解引用,这也是很多智能指针实现的基础,例如 Box<T> 类型。

fn main() {
    let x = Box::new(1);
    println!("{}", *x + 1);
}

对应的特征在 std::ops::Deref 中定义,内容如下。

pub trait Deref {
    type Target: ?Sized;
    fn deref(&self) -> &Self::Target;
}

对智能指针解引用时,实际调用了 *(x.deref()) 代码,如上,通过 deref() 返回变量的常规引用,然后再通过 * 对其进行解引用,最终获取到目标值。

隐式转换

用于将某种类型的引用转换为另一种类型,例如标准库中的 String 实现了如下的 Deref 特征。

impl ops::Deref for String {
    type Target = str;
    fn deref(&self) -> &str {
        unsafe { str::from_utf8_unchecked(&self.vec) }
    }
}

也就是可以将 &String 转换为 &str 类型,那么在调用的时候,会根据函数签名、是否实现目标类型的解引用接口,来判断是否进行隐式转换。

fn hello(name: &str) {
    println!("Hello {}!!!", name)
}

fn main() {
    let name = String::from("World");
    hello(&name);
}

也就是说:

  • String 实现了 Deref 特征,可以在需要的时候自动转换为 &str 类型。
  • 上述的 &name&String 类型,传给 hello() 时会自动转换为 &str 类型。
  • 必须使用 &name 方式来触发自动解引用,注意,仅引用类型实参才会触发自动。

而且,还可以实现连续的自动解引用,将如上代码修改为。

fn hello(name: &str) {
    println!("Hello {}!!!", name)
}

fn main() {
    let name = Box::new(String::from("World"));
    hello(&name);
    hello(&(*name)[..]);

    let simple: &str = &name;
    println!("{simple}");
}

这种隐式转换的好处就是代码比较简洁,但是因为不确定某个类型是否实现了 Deref 特征,导致阅读起来比较复杂。

其它

Rust 仅对 &v 的引用进行解引用操作,常见的智能指针 (例如 Box Rc Arc Cow 等) 会脱壳为内部引用;而多重 &&&v 则会归一为 &v 调用。第一种比较好容易理解,而第二种对应如下代码。

impl<T: ?Sized> Deref for &T {
    type Target = T;
    fn deref(&self) -> &T {
        *self
    }
}

注意,上述的实现有点难理解,其中 &selfself: &Self 的简写,而这里的 Self 又是 &T 类型,所以会自动将上述的 &&&v 会依次转换为 &&v &v 类型。

从解引用的角度看,相比 impl Deref for &T 来说 impl Deref for T 是完全没用的,因为解引用针对的是引用,而不是对象,当调用 &T 的解引用时,Rust 会看 &T 有没有实现 Deref 而不是 T