bigywater.tech
Rust · 16

Move 语义真实验证

所有权是 Rust 最核心也最让人困惑的概念,而它的入口是「移动」。这篇用编译器的完整报错和运行时的地址、drop 顺序,把移动讲清楚:它拷贝了什么、没拷贝什么,编译器是怎么追踪「已被移走」的,以及值最终在哪里被释放。

E0382
使用已移动的值
24 字节
移动一个 String 实际拷贝的大小(指针、长度、容量)
每个值 drop 一次
由当前所有者在作用域结束时执行

赋值即移动

实测 · 本机执行输出
  源码:
      1 | fn main() {
      2 |     let s1 = String::from("hello");
      3 |     let s2 = s1;
      4 |     println!("{s2}");
      5 |     println!("{s1}");
      6 | }
  编译:失败
    error[E0382]: borrow of moved value: `s1`
     --> <实验目录>/src/u16a.rs:5:16
      |
    2 |     let s1 = String::from("hello");
      |         -- move occurs because `s1` has type `String`, which does not implement the `Copy` trait
    3 |     let s2 = s1;
      |              -- value moved here
    4 |     println!("{s2}");
    5 |     println!("{s1}");
      |                ^^ value borrowed here after move
      |
    help: consider cloning the value if the performance cost is acceptable
      |
    3 |     let s2 = s1.clone();
      |                ++++++++
    
    error: aborting due to 1 previous error
    
    For more information about this error, try `rustc --explain E0382`.

移动了什么

实测 · 本机执行输出
  源码:
      1 | fn main() {
      2 |     let s1 = String::from("hello");
      3 |     let p1 = s1.as_ptr();
      4 |     let s2 = s1;                      // 移动
      5 |     println!("移动后堆数据地址不变:{}", p1 == s2.as_ptr());
      6 |     let s3 = s2.clone();              // 克隆
      7 |     println!("克隆得到新的堆缓冲区:{}(地址不同)", s2.as_ptr() != s3.as_ptr());
      8 |     println!("size_of::<String>() = {} 字节", std::mem::size_of::<String>());
      9 | }
  编译:成功
  运行(退出码 0):
    移动后堆数据地址不变:true
    克隆得到新的堆缓冲区:true(地址不同)
    size_of::<String>() = 24 字节

String 的内存布局见Box、Rc、Arc 的内存布局。

传参与返回

实测 · 本机执行输出
  源码:
      1 | fn takes(s: String) { println!("函数里:{s}"); }       // s 在函数结束时被 drop
      2 | fn gives_back(s: String) -> String { s }
      3 | fn main() {
      4 |     let a = String::from("a");
      5 |     takes(a);
      6 |     // println!("{a}");       // 取消注释会报 E0382
      7 |     let b = String::from("b");
      8 |     let b = gives_back(b);
      9 |     println!("拿回来了:{b}");
     10 | }
  编译:成功
  运行(退出码 0):
    函数里:a
    拿回来了:b
  编译:失败
    error[E0382]: borrow of moved value: `a`
     --> <实验目录>/src/u16d.rs:5:16
      |
    3 |     let a = String::from("a");
      |         - move occurs because `a` has type `String`, which does not implement the `Copy` trait
    4 |     takes(a);
      |           - value moved here
    5 |     println!("{a}");
      |                ^ value borrowed here after move
      |
    note: consider changing this parameter type in function `takes` to borrow instead if owning the value isn't necessary
     --> <实验目录>/src/u16d.rs:1:13
      |
    1 | fn takes(s: String) { println!("{s}"); }
      |    -----    ^^^^^^ this parameter takes ownership of the value
      |    |
      |    in this function
    help: consider cloning the value if the performance cost is acceptable
      |
    4 |     takes(a.clone());
      |            ++++++++
    
    error: aborting due to 1 previous error
    
    For more information about this error, try `rustc --explain E0382`.

循环里的移动

实测 · 本机执行输出
  源码:
      1 | fn consume(_s: String) {}
      2 | fn main() {
      3 |     let s = String::from("x");
      4 |     for _ in 0..2 { consume(s); }
      5 | }
  编译:失败
    error[E0382]: use of moved value: `s`
     --> <实验目录>/src/u16e.rs:4:29
      |
    3 |     let s = String::from("x");
      |         - move occurs because `s` has type `String`, which does not implement the `Copy` trait
    4 |     for _ in 0..2 { consume(s); }
      |     -------------           ^ value moved here, in previous iteration of loop
      |     |
      |     inside of this loop
      |
    note: consider changing this parameter type in function `consume` to borrow instead if owning the value isn't necessary
     --> <实验目录>/src/u16e.rs:1:16
      |
    1 | fn consume(_s: String) {}
      |    -------     ^^^^^^ this parameter takes ownership of the value
      |    |
      |    in this function
    help: consider cloning the value if the performance cost is acceptable
      |
    4 |     for _ in 0..2 { consume(s.clone()); }
      |                              ++++++++
    
    error: aborting due to 1 previous error
    
    For more information about this error, try `rustc --explain E0382`.

部分移动与 drop

实测 · 本机执行输出
  源码:
      1 | struct D(&'static str);
      2 | impl Drop for D { fn drop(&mut self) { println!("  drop {}", self.0); } }
      3 | struct Pair { a: D, b: D }
      4 | fn main() {
      5 |     let p = Pair { a: D("a"), b: D("b") };
      6 |     let moved = p.a;                         // 只移走字段 a
      7 |     println!("移走 a 之后,p.b 还能用:{}", p.b.0);
      8 |     {
      9 |         let _inner = moved;                  // 新所有者在这个块结束时 drop
     10 |         println!("进入内层块");
     11 |     }
     12 |     println!("内层块已结束,main 马上结束");
     13 | }
  编译:成功
  运行(退出码 0):
    移走 a 之后,p.b 还能用:b
    进入内层块
      drop a
    内层块已结束,main 马上结束
      drop b

Drop 的规则见Drop Trait 与资源释放;不想转移所有权时用借用,见不可变借用与可变借用。

分步演示

移动语义
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / rustc 1.98.1(brew)/ cargo 1.98.1;单个文件用 rustc 直接编译,需要依赖时用 cargo(经代理从 crates.io 下载);计时用 std::time::Instant,数字每次运行会有小幅抖动。地址比较只用「是否相等」,不依赖具体数值
    • 没有验证:移动在优化后是否被消除(memcpy)、Pin 与不可移动类型、闭包捕获时的移动(move 关键字)、条件移动后的 drop flag 机制
    京ICP备2026067502号-1京公网安备11010502063487号