所有权转移 Move ownership
大多数类型, 在赋值, 函数参数及函数返回值时, 都是 move 的方式, 而不是拷贝的方式. 原变量的值的所有权转移给目标变量, 同时原变量成为未初始化, 也不再能被使用.
let x = y; foo(x)
当进行赋值及作为函数参数时, 变量的所有权就进行了传递.
传递所有权时, 可以更改值的可变性:
#![allow(unused)] fn main() { let x = Box::new(42i32); let mut x2 = x; }
转移所有权的实现
作为与上文中转移字符串的 C++ 代码的对应, 我们实现了相应的 Rust 代码, 看它是如何转移所有权的:
#[allow(unused_variables)] fn main() { let s1 = "Rust".to_owned(); let s2 = s1; }
使用命令 rustc --emit asm move-string.rs
可以生成相应的汇编文件,
我们摘取 main()
函数的代码并加上了一些注释, 可以对比上面的 rust 代码片段:
.section .text._ZN11move_string4main17h2b54fa27eb759b1dE,"ax",@progbits
.p2align 4, 0x90
.type _ZN11move_string4main17h2b54fa27eb759b1dE,@function
_ZN11move_string4main17h2b54fa27eb759b1dE:
.cfi_startproc
subq $56, %rsp
.cfi_def_cfa_offset 64
;let s1 = "Hello, Rust".to_owned();
leaq 8(%rsp), %rdi
leaq .L__unnamed_10(%rip), %rsi
movl $4, %edx
callq _ZN5alloc3str56_$LT$impl$u20$alloc..borrow..ToOwned$u20$for$u20$str$GT$8to_owned17hd870369ca9fdf413E
; s2.ptr = s1.ptr;
movq 8(%rsp), %rax
movq %rax, 32(%rsp)
; s2.cap = s1.cap;
movq 16(%rsp), %rax
movq %rax, 40(%rsp)
; s2.len = s1.len;
movq 24(%rsp), %rax
movq %rax, 48(%rsp)
; drop(s2);
leaq 32(%rsp), %rdi
callq _ZN4core3ptr42drop_in_place$LT$alloc..string..String$GT$17hcecf74a2a26a41e6E
addq $56, %rsp
.cfi_def_cfa_offset 8
retq
.Lfunc_end31:
.size _ZN11move_string4main17h2b54fa27eb759b1dE, .Lfunc_end31-_ZN11move_string4main17h2b54fa27eb759b1dE
.cfi_endproc
根据上面的汇编代码指令, 我们画一下对应的图:
可以看到, s2 的栈内存数据都是从 s1 拷贝的, 但并没有拷贝它的堆内存, 这个属于浅拷贝 (shallow copy).
是函数结束前, 只调用了 drop(s2)
来释放 s2 的堆内存, 并没有调用 drop(s1)
来释放 s1 的堆内存, 为什么呢?
因为在哪里调用 drop()
函数, 以及是否调用 drop()
函数, 都是编译器在编译期间确定的.
例如上面的汇编代码就是编译器自动填充的:
; drop(s2);
leaq 32(%rsp), %rdi
callq _ZN4core3ptr42drop_in_place$LT$alloc..string..String$GT$17hcecf74a2a26a41e6E
因为 s1 已经被编译器标记为 未初始化 uninit
, 所以它不再能被使用, 也不必要调用 drop(s1)
释放它的内存.
与 drop 动作相关的更多细节, 我们在后面的 Drop 一节中有更全面和深入的介绍.
与 C++ 比较
可以看到, 在 Rust 中实现所有权的转移, 非常自然, 相对应的, 在C++中就需要:
- move constructor
- move assignment operator
std::move()
与C++中的转移语言的不同点是:
- 在 C++ 中,
s1
和s2
字符串变量都是可以访问的, 在超出作用域后, 它们的析构函数都会被调用 - 在 Rust 中, 只有
s2
字符串可以被访问; 而s1
已经被编译器标记为未初始化
, 所以它不能再被使用, 并且它也不会被 drop - 在 C++ 中, 默认是深拷贝 (deep copy) 为主, 如果想要实现移动语义实现浅拷贝, 就需要用前文提到的方法修改代码
- 在 Rust 中, 默认实现的是移动语义, 如果想深拷贝, 就要显式地调用
clone()
方法
另外, 作为对比, 我们也实现一个 Person 类, 可以参看一下前文的 C++ 代码:
#[derive(Debug, Clone)] pub struct Person { name: String, } impl Person { #[must_use] #[inline] pub const fn new(name: String) -> Self { Self { name } } #[must_use] #[inline] pub fn name(&self) -> &str { &self.name } } #[allow(unused_variables)] fn main() { let name = "Julia".to_owned(); let p2 = Person::new(name); println!("creating p3"); let p3 = Person::new("Julia".to_owned()); // 使用 clone println!("creating p4:"); let p4 = p3.clone(); // 使用 move println!("creating p5:"); let p5 = p4; }