{"content":{"title":"理解rust中的deref运算符*与移动语义","body":"# 引子\r\n先从一个例子说起,看如下代码:\r\n\r\n```\r\n\r\nstruct Thing {\r\n    field: String\r\n}\r\n\r\nfn f1(sth: &Thing)  {\r\n    let tmp = *sth;\r\n//            ┗━ Move data out of `thing`.\r\n\r\n}\r\n```\r\n\r\n我刚开始学习Rust的时候是这么理解的: sth是对Thing的不可变引用，因为*对sth解引用，因此产生了Move，由于所有权规则不可变引用sth不能Move，所以导致上述代码不能编译。那么事实是这样吗?\r\n\r\n实际上并非不能对不可变引用进行解引用操作，真正导致sth产生Move的是`=`运算符，可以按照如下的方式理解:\r\n\r\n```\r\n\r\n//doesn't compile.\r\nfn f1(thing: &Thing) {\r\n    let tmp = *thing;\r\n//          ┃ ┗━ Point directly to the referenced data.\r\n//          ┗━━━ Try to copy RHS's value, otherwise move it into `tmp`.\r\n}\r\n\r\n//Compiles.\r\nfn f2(sth: &Thing) -> &String {\r\n    &(*sth).field\r\n}\r\n\r\nfn main() {\r\n    let x = Thing {field: String::from(\"hello\")};\r\n    f1(&x);\r\n}\r\n```\r\n\r\n由于Thing是结构体类型，且其成员包括引用语义的字段field，因此Thing具有Move而非Copy语义，在进行赋值操作时会强制进行Move，将thing引用指向的变量`x`绑定的数据所有权转移给`tmp`。\r\n\r\n如代码`f2`，可以对`*sth`解引用后再取其成员`field`，最后再取引用，改代码可以正常编译，这也验证了解引用并不会导致`Move`。\r\n\r\n> 结论: 可以对不可变引用进行解引用(*)，但是不能Move该不可变引用。\r\n\r\n- [参考资料](https://www.reddit.com/r/rust/comments/thvc2e/comment/i1a8zpw)\r\n\r\n# 那么在Rust中什么时候会产生Move?\r\n回答这个问题，首先需要搞清楚，所有权转移(Move)的定义：**一个值的所有权被转移给另一个变量绑定的过程就叫做所有权转移**[引自:张汉东《Rust编程之道》]，这意味只要同时满足这两点的场景就会产生Move:\r\n- 该值必须是引用语义，即分配到Heap上的类型(如String,Vec,包含String等引用语义成员的Struct，Box<T>中的T等)；否则，值语义的类型对变量值重新绑定的时候执行的是复制，而非所有权转移.\r\n- 需要有新的变量，以重新绑定\r\n\r\n> 注:\r\n>  - 值语义:按位复制以后，与原始对象无关。\r\n>  - 引用语义:也叫指针语义。一般是指将数据存储于堆内存中，通过栈内存的指针\r\n来管理堆内存的数据，并且引用语义禁止按位复制。按位复制就是指栈复制，也叫浅复制，它只复制栈上的数据。相对而言，深复制就是对栈上和堆上的数据一起复制。\r\n\r\n列举Rust中常见的语句，可知对String等引用语义进行如下操作会产生Move:\r\n\r\n1. 赋值语句`=`\r\n\r\n```\r\n\r\n    let s = String::from(\"hello\");\r\n    let x = s;  //Move\r\n```\r\n\r\n2. 作为函数参数或函数返回值\r\n\r\n可以理解为将s转移到新的绑定变量x中。\r\n```\r\n\r\n// Cannot Compile\r\nfn test(x:String){}\r\n\r\nfn main() {\r\n    let s = \"hello\".to_string();\r\n    test(s);   //Move\r\n    println!(\"{}\",s);\r\n}\r\n```\r\n\r\n3. for,while和loop循环语句\r\n\r\n   因为for循环实际上是一个语法糖，rust编译器会将`for val in v`替换成`for val in  IntoIterator::into_iter(v)`,此时转换为第2种作为函数参数的场景\r\n\r\n```\r\n\r\n    let v = vec![1,2,3];\r\n    for val in v{   //Move\r\n        println!(\"{}\",val);\r\n    }\r\n    println!(\"{:?}\", v);\r\n```\r\n> 形如`for val in &v`不会拿走v的所有权，只会获取它的不可变引用,因为rust会将其替换成`for val in v.iter()`\r\n\r\n4. {}词法作用域\r\n在新的词法作用域中，Rust会重新声明变量，导致发生所有权转移\r\n\r\n```\r\n\r\n    let outer_sp = \"hello\".to_string();\r\n    {\r\n        outer_sp;  //Move\r\n    }\r\n    println!(\"{}\", outer_sp);  \r\n```\r\n\r\n4. if let和 while let语句，重新绑定\r\n\r\n```\r\n\r\n    let a = Some(\"hello\".to_string());\r\n    if let Some(s) = a {  //Move\r\n        println!(\"{:?}\",a);\r\n    }\r\n```\r\n\r\n5. match的词法作用域\r\n\r\n```\r\n\r\n    let a = Some(\"hello\".to_string());\r\n    match a {\r\n        _ => println!(\"test\")\r\n    }\r\n    println!(\"{:?}\", a);\r\n    match a {//-------------------------------------------\r\n        Some(s) => println!(\"{}\", s),  //Move    |\r\n        _ => println!(\"test\")                          //|match scope\r\n    } //--------------------------------------------------\r\n    println!(\"{:?}\", a);\r\n```\r\n\r\n> 注意:\r\n> - `match a`本身不会转移a的所有权，而是在作用域中对a内部的值进行绑定为s时，才会Move。\r\n> - println!语句只需要获取a的不可变引用`& a`即可，因此也不会转移a的所有权。\r\n\r\n如果想避免在match中产生Move，可以采用[ref](https://doc.rust-lang.org/stable/rust-by-example/scope/borrow/ref.html)关键字。比如对于如下的二叉树节点，希望计算所有节点的权重，但是不希望在计算中把节点的所有权转移，可以在match模式匹配中使用ref获取节点的引用:\r\n\r\n```\r\n\r\nenum BTree {\r\n    Leaf(i32),\r\n    Node(Box<BTree>, i32, Box<BTree>)\r\n}\r\n\r\nfn sampleTree() -> BTree{\r\n    let l1 = Box::new(BTree::Leaf(1));\r\n    let l2 = Box::new(BTree::Leaf(3));\r\n    let n1 = Box::new(BTree::Node(l1,2,l2));\r\n    let r2 = Box::new(BTree::Leaf(5));\r\n    BTree::Node(n1, 4, r2)\r\n}\r\n\r\nfn tree_weight(t: &BTree) -> i32 {\r\n    match *t{\r\n       BTree::Leaf(payload) => payload,\r\n       BTree::Node(ref l, payload,ref r )=> {\r\n        tree_weight(l) + payload + tree_weight(r)\r\n       } \r\n    }\r\n}\r\n\r\nfn main() {\r\n    let t = sampleTree();\r\n    assert_eq!(tree_weight(&t), 1+2+3+4+5);\r\n}\r\n```\r\n\r\n6. 闭包\r\n\r\n闭包与{}类似，也会创建新的词法作用域，并将作用域中的变量`a`与外层的变量`a`的值绑定。\r\n\r\n```\r\n\r\n    let a = \"hello\".to_string();\r\n    let c = || {a;};  //Move\r\n    // print!(\"{}\", a);\r\n    c();\r\n    c();   //由于a被Move到闭包中，因此c为FnOnce，不能再次执行\r\n```\r\n\r\n> 注: `let c = || {println!(\"{}\", a)};`则不会转移所有权，因此此时获取的是a的不可变绑定`& a`。\r\n\r\n\r\n## 小Quiz:\r\n\r\n如下的代码中`match *self`是否会发生Move，导致代码不能编译?\r\n\r\n```\r\n\r\n#[derive(Debug)]\r\nenum Color {\r\n    Red,\r\n    Blue\r\n}\r\n\r\nimpl Color {\r\n    fn to_str(&self) -> &str {\r\n        match *self {\r\n            Color::Red => \"red\",\r\n            Color::Blue => \"blue\"\r\n        }\r\n    }\r\n}\r\n\r\nfn main() {\r\n    let c = Color::Red;\r\n    c.to_str();\r\n    println!(\"{:?}\",c);\r\n}\r\n```\r\n\r\n# 理解引用的其他常见误区\r\n1. 对于实现了`Deref Trait`的类型x，x.deref()的类型与*x一致吗?\r\n\r\n实际上Deref返回的是引用类型, `*x`与`*(x.deref())`是一致的，这是由于如果不返回引用类型，会导致deref函数返回值产生所有权转移，而这不是我们期望的，我们希望在`*`操作符之后再决定是否发生所有权转移。如标准库中实现了String向str的解引用转换:\r\n\r\n```\r\n\r\nimpl ops::Deref for String {\r\n    type Target = str;\r\n\r\n    #[inline]\r\n    fn deref(&self) -> &str {\r\n        unsafe { str::from_utf8_unchecked(&self.vec) }\r\n    }\r\n}\r\n```\r\n如果`s`为`String`类型，那么:\r\n\r\n```\r\n\r\ns: String\r\n&s: &String\r\nx.deref(): &str\r\n*x: str\r\n&*s: &str\r\n```\r\n\r\n2. 方法调用的时候只进行Deref吗？\r\n实际上除了Deref外，还会进行事先进行一次构建引用列表的操作，[整个过程](https://doc.rust-lang.org/reference/expressions/method-call-expr.html)如下:\r\n\r\n![方法接收者类型.png](https://img.learnblockchain.cn/attachments/2023/01/6SoRXsjL63b82d7477f3b.png)\r\n- 1.方法接收者类型为T\r\n- 2.构建方法接收者候选类型列表: T, &T，&mut T\r\n- 3.对上述三种类型分别进行解引用得到:*T, T, mut T\r\n- 4.循环上述过程直至到[unsized coercion](https://doc.rust-lang.org/reference/type-coercions.html#unsized-coercions)转换\r\n\r\n然后对上述列表中的每种类型U的本身impl的方法和从trait中集成的方法中找到一个可用的方法。"},"author":{"user":"https://learnblockchain.cn/people/720","address":null},"history":"QmZmbTmMXf1o2abXetXQ46uMDRUQvz1J9rAzcigmaaEvfV","timestamp":1673023161,"version":1}