bigywater.tech
Rust · 25

Struct 中保存引用

把引用存进结构体是 Rust 里「零拷贝」的主要手段,也是生命周期标注最常出现的地方。这篇验证它的语法要求、它对数据寿命和修改的限制,并用一个自定义的计数分配器量出它省下了多少堆分配。

E0106
结构体字段是引用却没声明生命周期
13 vs 10013
借用版与拥有版分词器的堆分配次数(1 万个词)
<'a, 'b>
一个结构体可以有多个独立的生命周期参数

语法要求

实测 · 本机执行输出
  源码:
      1 | struct Excerpt { part: &str }
      2 | fn main() {}
  编译:失败
    error[E0106]: missing lifetime specifier
     --> <实验目录>/src/u25a.rs:1:24
      |
    1 | struct Excerpt { part: &str }
      |                        ^ expected named lifetime parameter
      |
    help: consider introducing a named lifetime parameter
      |
    1 | struct Excerpt<'a> { part: &'a str }
      |               ++++          ++
    
    error: aborting due to 1 previous error
    
    For more information about this error, try `rustc --explain E0106`.
  源码:
      1 | struct Excerpt<'a> { part: &'a str }
      2 | fn main() {
      3 |     let novel = String::from("Call me Ishmael. Some years ago...");
      4 |     let first = novel.split('.').next().unwrap();
      5 |     let e = Excerpt { part: first };
      6 |     println!("摘录:{}", e.part);
      7 | }
  编译:成功
  运行(退出码 0):
    摘录:Call me Ishmael

寿命与修改限制

实测 · 本机执行输出
  源码:
      1 | struct Excerpt<'a> { part: &'a str }
      2 | fn main() {
      3 |     let e;
      4 |     {
      5 |         let novel = String::from("Call me Ishmael.");
      6 |         e = Excerpt { part: &novel[..4] };
      7 |     }
      8 |     println!("{}", e.part);
      9 | }
  编译:失败
    error[E0597]: `novel` does not live long enough
     --> <实验目录>/src/u25c.rs:6:30
      |
    5 |         let novel = String::from("Call me Ishmael.");
      |             ----- binding `novel` declared here
    6 |         e = Excerpt { part: &novel[..4] };
      |                              ^^^^^ borrowed value does not live long enough
    7 |     }
      |     - `novel` dropped here while still borrowed
    8 |     println!("{}", e.part);
      |                    ------ borrow later used here
    
    error: aborting due to 1 previous error
    
    For more information about this error, try `rustc --explain E0597`.
  源码:
      1 | struct Excerpt<'a> { part: &'a str }
      2 | fn main() {
      3 |     let mut novel = String::from("Call me Ishmael.");
      4 |     let e = Excerpt { part: &novel[..4] };
      5 |     novel.push_str(" Some years ago");
      6 |     println!("{}", e.part);
      7 | }
  编译:失败
    error[E0502]: cannot borrow `novel` as mutable because it is also borrowed as immutable
     --> <实验目录>/src/u25d.rs:5:5
      |
    4 |     let e = Excerpt { part: &novel[..4] };
      |                              ----- immutable borrow occurs here
    5 |     novel.push_str(" Some years ago");
      |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ mutable borrow occurs here
    6 |     println!("{}", e.part);
      |                    ------ immutable borrow later used here
    
    error: aborting due to 1 previous error
    
    For more information about this error, try `rustc --explain E0502`.

零拷贝的收益

实测 · 本机执行输出
  源码:
      1 | use std::alloc::{GlobalAlloc, Layout, System};
      2 | use std::sync::atomic::{AtomicUsize, Ordering};
      3 | struct Counting;
      4 | static ALLOCS: AtomicUsize = AtomicUsize::new(0);
      5 | unsafe impl GlobalAlloc for Counting {
      6 |     unsafe fn alloc(&self, l: Layout) -> *mut u8 { ALLOCS.fetch_add(1, Ordering::Relaxed); unsafe { System.alloc(l) } }
      7 |     unsafe fn dealloc(&self, p: *mut u8, l: Layout) { unsafe { System.dealloc(p, l) } }
      8 | }
      9 | #[global_allocator] static A: Counting = Counting;
     10 | 
     11 | struct Tokens<'a> { words: Vec<&'a str> }                 // 借用:切片指回原文
     12 | struct OwnedTokens { words: Vec<String> }                 // 拥有:每个词一个 String
     13 | fn main() {
     14 |     let text: String = (0..10_000).map(|i| format!("word{i} ")).collect();
     15 |     let before = ALLOCS.load(Ordering::Relaxed);
     16 |     let t = Tokens { words: text.split_whitespace().collect() };
     17 |     let a1 = ALLOCS.load(Ordering::Relaxed) - before;
     18 |     let before = ALLOCS.load(Ordering::Relaxed);
     19 |     let o = OwnedTokens { words: text.split_whitespace().map(String::from).collect() };
     20 |     let a2 = ALLOCS.load(Ordering::Relaxed) - before;
     21 |     println!("文本里有 {} 个词", t.words.len());
     22 |     println!("借用版(Vec<&str>)  堆分配 {} 次", a1);
     23 |     println!("拥有版(Vec<String>)堆分配 {} 次", a2);
     24 |     println!("第 5000 个词:{} / {};两者内容相同:{}", t.words[5000], o.words[5000], t.words[5000] == o.words[5000]);
     25 | }
  编译:成功
  运行(退出码 0):
    文本里有 10000 个词
    借用版(Vec<&str>)  堆分配 13 次
    拥有版(Vec<String>)堆分配 10013 次
    第 5000 个词:word5000 / word5000;两者内容相同:true

分配次数通过自定义 #[global_allocator] 计数得到(只数 alloc 次数,不含字节数);release 构建。自定义分配器相关的更多内容这里没有展开。

多个生命周期参数

实测 · 本机执行输出
  源码:
      1 | struct Pair<'a, 'b> { left: &'a str, right: &'b str }
      2 | fn main() {
      3 |     let a = String::from("长寿的");
      4 |     let p;
      5 |     {
      6 |         let b = String::from("短命的");
      7 |         let tmp = Pair { left: &a, right: &b };
      8 |         println!("内层:{} {}", tmp.left, tmp.right);
      9 |         p = tmp.left;                 // 只取出生命周期 'a 的那部分
     10 |     }
     11 |     println!("外层:{}", p);
     12 | }
  编译:成功
  运行(退出码 0):
    内层:长寿的 短命的
    外层:长寿的

分步演示

结构体中的引用
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / rustc 1.98.1(brew)/ cargo 1.98.1;单个文件用 rustc 直接编译,需要依赖时用 cargo(经代理从 crates.io 下载);计时用 std::time::Instant,数字每次运行会有小幅抖动。分配次数为 release 构建下单次运行的结果
    • 没有验证:分配的字节数与耗时对比、带泛型与 trait bound 的 Struct<'a, T>、自引用结构体(Pin 相关)、cow 版本(见 Cow 一篇)
    京ICP备2026067502号-1京公网安备11010502063487号