理解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该不可变引用。
回答这个问题,首先需要搞清楚,所有权转移(Move)的定义:一个值的所有权被转移给另一个变量绑定的过程就叫做所有权转移[引自:张汉东《Rust编程之道》],这意味只要同时满足这两点的场景就会产生Move:
注:
- 值语义:按位复制以后,与原始对象无关。
- 引用语义:也叫指针语义。一般是指将数据存储于堆内存中,通过栈内存的指针 来管理堆内存的数据,并且引用语义禁止按位复制。按位复制就是指栈复制,也叫浅复制,它只复制栈上的数据。相对而言,深复制就是对栈上和堆上的数据一起复制。
列举Rust中常见的语句,可知对String等引用语义进行如下操作会产生Move:
=
let s = String::from("hello");
let x = s; //Move
可以理解为将s转移到新的绑定变量x中。
// Cannot Compile
fn test(x:String){}
fn main() {
let s = "hello".to_string();
test(s); //Move
println!("{}",s);
}
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()
let outer_sp = "hello".to_string();
{
outer_sp; //Move
}
println!("{}", outer_sp);
let a = Some("hello".to_string());
if let Some(s) = a { //Move
println!("{:?}",a);
}
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<BTree>, i32, Box<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);
}
闭包与{}类似,也会创建新的词法作用域,并将作用域中的变量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
。
如下的代码中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);
}
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) }
}
}
如果s
为String
类型,那么:
s: String
&s: &String
x.deref(): &str
*x: str
&*s: &str
然后对上述列表中的每种类型U的本身impl的方法和从trait中集成的方法中找到一个可用的方法。
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!