理解rust中的deref运算符*与移动语义

  • Po
  • 更新于 2023-01-09 08:56
  • 阅读 2520

理解rust中的deref运算符*与移动语义

引子

先从一个例子说起,看如下代码:


struct Thing {
    field: String
}

fn f1(sth: &Thing)  {
    let tmp = *sth;
//            ┗━ Move data out of `thing`.

}

我刚开始学习Rust的时候是这么理解的: sth是对Thing的不可变引用,因为*对sth解引用,因此产生了Move,由于所有权规则不可变引用sth不能Move,所以导致上述代码不能编译。那么事实是这样吗?

实际上并非不能对不可变引用进行解引用操作,真正导致sth产生Move的是=运算符,可以按照如下的方式理解:


//doesn't compile.
fn f1(thing: &Thing) {
    let tmp = *thing;
//          ┃ ┗━ Point directly to the referenced data.
//          ┗━━━ Try to copy RHS's value, otherwise move it into `tmp`.
}

//Compiles.
fn f2(sth: &Thing) -> &String {
    &(*sth).field
}

fn main() {
    let x = Thing {field: String::from("hello")};
    f1(&x);
}

由于Thing是结构体类型,且其成员包括引用语义的字段field,因此Thing具有Move而非Copy语义,在进行赋值操作时会强制进行Move,将thing引用指向的变量x绑定的数据所有权转移给tmp

如代码f2,可以对*sth解引用后再取其成员field,最后再取引用,改代码可以正常编译,这也验证了解引用并不会导致Move

结论: 可以对不可变引用进行解引用(*),但是不能Move该不可变引用。

那么在Rust中什么时候会产生Move?

回答这个问题,首先需要搞清楚,所有权转移(Move)的定义:一个值的所有权被转移给另一个变量绑定的过程就叫做所有权转移[引自:张汉东《Rust编程之道》],这意味只要同时满足这两点的场景就会产生Move:

  • 该值必须是引用语义,即分配到Heap上的类型(如String,Vec,包含String等引用语义成员的Struct,Box<T>中的T等);否则,值语义的类型对变量值重新绑定的时候执行的是复制,而非所有权转移.
  • 需要有新的变量,以重新绑定

注:

  • 值语义:按位复制以后,与原始对象无关。
  • 引用语义:也叫指针语义。一般是指将数据存储于堆内存中,通过栈内存的指针 来管理堆内存的数据,并且引用语义禁止按位复制。按位复制就是指栈复制,也叫浅复制,它只复制栈上的数据。相对而言,深复制就是对栈上和堆上的数据一起复制。

列举Rust中常见的语句,可知对String等引用语义进行如下操作会产生Move:

  1. 赋值语句=

    let s = String::from("hello");
    let x = s;  //Move
  1. 作为函数参数或函数返回值

可以理解为将s转移到新的绑定变量x中。


// Cannot Compile
fn test(x:String){}

fn main() {
    let s = "hello".to_string();
    test(s);   //Move
    println!("{}",s);
}
  1. for,while和loop循环语句

    因为for循环实际上是一个语法糖,rust编译器会将for val in v替换成for val in IntoIterator::into_iter(v),此时转换为第2种作为函数参数的场景


    let v = vec![1,2,3];
    for val in v{   //Move
        println!("{}",val);
    }
    println!("{:?}", v);

形如for val in &v不会拿走v的所有权,只会获取它的不可变引用,因为rust会将其替换成for val in v.iter()

  1. {}词法作用域 在新的词法作用域中,Rust会重新声明变量,导致发生所有权转移

    let outer_sp = "hello".to_string();
    {
        outer_sp;  //Move
    }
    println!("{}", outer_sp);  
  1. if let和 while let语句,重新绑定

    let a = Some("hello".to_string());
    if let Some(s) = a {  //Move
        println!("{:?}",a);
    }
  1. match的词法作用域

    let a = Some("hello".to_string());
    match a {
        _ => println!("test")
    }
    println!("{:?}", a);
    match a {//-------------------------------------------
        Some(s) => println!("{}", s),  //Move    |
        _ => println!("test")                          //|match scope
    } //--------------------------------------------------
    println!("{:?}", a);

注意:

  • match a本身不会转移a的所有权,而是在作用域中对a内部的值进行绑定为s时,才会Move。
  • println!语句只需要获取a的不可变引用& a即可,因此也不会转移a的所有权。

如果想避免在match中产生Move,可以采用ref关键字。比如对于如下的二叉树节点,希望计算所有节点的权重,但是不希望在计算中把节点的所有权转移,可以在match模式匹配中使用ref获取节点的引用:


enum BTree {
    Leaf(i32),
    Node(Box&lt;BTree>, i32, Box&lt;BTree>)
}

fn sampleTree() -> BTree{
    let l1 = Box::new(BTree::Leaf(1));
    let l2 = Box::new(BTree::Leaf(3));
    let n1 = Box::new(BTree::Node(l1,2,l2));
    let r2 = Box::new(BTree::Leaf(5));
    BTree::Node(n1, 4, r2)
}

fn tree_weight(t: &BTree) -> i32 {
    match *t{
       BTree::Leaf(payload) => payload,
       BTree::Node(ref l, payload,ref r )=> {
        tree_weight(l) + payload + tree_weight(r)
       } 
    }
}

fn main() {
    let t = sampleTree();
    assert_eq!(tree_weight(&t), 1+2+3+4+5);
}
  1. 闭包

闭包与{}类似,也会创建新的词法作用域,并将作用域中的变量a与外层的变量a的值绑定。


    let a = "hello".to_string();
    let c = || {a;};  //Move
    // print!("{}", a);
    c();
    c();   //由于a被Move到闭包中,因此c为FnOnce,不能再次执行

注: let c = || {println!("{}", a)};则不会转移所有权,因此此时获取的是a的不可变绑定& a

小Quiz:

如下的代码中match *self是否会发生Move,导致代码不能编译?


#[derive(Debug)]
enum Color {
    Red,
    Blue
}

impl Color {
    fn to_str(&self) -> &str {
        match *self {
            Color::Red => "red",
            Color::Blue => "blue"
        }
    }
}

fn main() {
    let c = Color::Red;
    c.to_str();
    println!("{:?}",c);
}

理解引用的其他常见误区

  1. 对于实现了Deref Trait的类型x,x.deref()的类型与*x一致吗?

实际上Deref返回的是引用类型, *x*(x.deref())是一致的,这是由于如果不返回引用类型,会导致deref函数返回值产生所有权转移,而这不是我们期望的,我们希望在*操作符之后再决定是否发生所有权转移。如标准库中实现了String向str的解引用转换:


impl ops::Deref for String {
    type Target = str;

    #[inline]
    fn deref(&self) -> &str {
        unsafe { str::from_utf8_unchecked(&self.vec) }
    }
}

如果sString类型,那么:


s: String
&s: &String
x.deref(): &str
*x: str
&*s: &str
  1. 方法调用的时候只进行Deref吗? 实际上除了Deref外,还会进行事先进行一次构建引用列表的操作,整个过程如下:

方法接收者类型.png

  • 1.方法接收者类型为T
  • 2.构建方法接收者候选类型列表: T, &T,&mut T
  • 3.对上述三种类型分别进行解引用得到:*T, T, mut T
  • 4.循环上述过程直至到unsized coercion转换

然后对上述列表中的每种类型U的本身impl的方法和从trait中集成的方法中找到一个可用的方法。

  • 原创
  • 学分: 11
  • 分类: Rust
  • 标签: Rust 
点赞 1
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
Po
Po
0xB332...C3ba
Blockchain & AI change the world!