Rust 类型进阶:trait、泛型、生命周期与闭包
先完成所有权与类型。此前批次记录使用 edition 2024、Rust 1.99.1 验证,示例最低语法版本见 Cargo.toml;本轮仅静态审读,未重新编译或执行示例,以下断言和诊断是未来自跑预期。目标是解释接口允许什么、拒绝什么,再把接口交给并发项目。
trait 约束与关联类型
泛型函数只可使用约束提供的行为,不能因为当前调用者“恰好有某个方法”就调用它。T: Render 表示调用方选择一种实现;type Output 表示该实现选择输出类型,调用者不能随意改成别的输出。适合迭代器 Item 这类“对一个实现有一个关联结果”的关系;允许一个类型实现多个输入类型时可选 trait 的泛型参数。
where 与参数处的 bound 表达同一类约束,复杂时用 where 提高可读性。默认方法可以调用同一 trait 的其他方法;supertrait 要求实现者同时满足父 trait,并非结构体继承。实现外部 trait 与外部类型要遵守一致性/孤儿规则;给外部类型增加行为可用本地 newtype。type Alias = u32 不会创建新类型,无法隔离单位。
示例 Render::LABEL 展示关联常量;<Score as Render>::render 在同名方法有歧义时明确选择实现。派生 Debug/Clone/Eq 等只产生定义好的实现,不会证明业务不变量。下面是完整程序,所有断言须通过并输出 types ok。
use std::fmt::Display;
trait Render {
type Output: Display;
const LABEL: &'static str;
fn render(&self) -> Self::Output;
}
struct Score(u32);
impl Render for Score {
type Output = u32;
const LABEL: &'static str = "score";
fn render(&self) -> u32 { self.0 }
}
fn show<T: Render>(value: &T) -> String { format!("{}={}", T::LABEL, value.render()) }
fn describe(value: &dyn Display) -> String { format!("{value}") }
fn longest<'a>(a: &'a str, b: &'a str) -> &'a str {
if a.len() >= b.len() { a } else { b }
}
fn apply<F: FnOnce() -> usize>(f: F) -> usize { f() }
fn add<T: std::ops::Add<Output = T>>(left: T, right: T) -> T { left + right }
fn main() {
assert_eq!(show(&Score(4)), "score=4");
assert_eq!(describe(&Score(4).render()), "4");
assert_eq!(add(2u8, 3u8), 5);
let owned = String::from("rust");
assert_eq!(longest(&owned, "rs"), "rust");
let take = move || { let moved = owned; moved.len() };
assert_eq!(apply(take), 4);
let names = ["Ada", "Lin"];
let lengths: Vec<_> = names.iter().map(|name| name.len()).collect();
assert_eq!(lengths, vec![3, 3]);
// Fully qualified syntax resolves trait method names without a runtime cast.
assert_eq!(<Score as Render>::render(&Score(9)), 9);
println!("types ok");
}静态分发、dyn 与 Sized
show<T: Render> 在编译期按实际类型生成代码,利于内联,也可能增加二进制体积;不能从“零成本抽象”推出每个泛型都更快。示例 add<T: Add<Output=T>> 表示运算符也通过trait约束选择行为,关联Output约束返回同一类型;一般Add可选择别的输出类型。加法通常消费操作数,整数溢出规则受构建配置影响,业务范围验证用checked_add等显式处理,不能以release不报错视为正确。describe(&dyn Display) 通过 trait 对象在运行时选择方法,引用同时携带对象与行为信息。适合运行时才知道实现、需要异构集合的场景;不同实现必须有可统一调用的接口。
Render 带有关联常量,不适合作为本例的 dyn Render;示例用标准库的 dyn Display。可动态兼容的方法通常不能有未确定的泛型参数,也不能依赖调用方不知道大小的 Self 值返回;具体规则以 Reference dyn compatibility为准。str、[T]、dyn Trait 的大小不是编译时固定值,要经由引用或 Box 使用。泛型默认 Sized,接受动态大小的借用可用 T: ?Sized。&String 到 &str 的解引用强制转换便于借用,不会复制字符串,也不是任意类型隐式转换。
生命周期是借用关系
longest<'a>(a: &'a str, b: &'a str) -> &'a str 让返回借用不能比两个输入共同可用的区域更长。标注不会延长对象寿命,不是运行时引用计数;编译器按调用点约束检查。只有一个输入借用的 first(&str) -> Option<&str> 可省略标注。结构体持有引用时也需要表达关系,或者选择拥有 String,代价是分配但接口更独立。
以下是预期编译失败的完整文件。执行 rustc --edition=2024 negative/lifetime.rs 必须非零退出并报告 E0515;如果“修复”为 unsafe 延长生命则破坏接口保证。正确做法是返回 String,或从调用方提供的借用返回。
fn dangling<'a>() -> &'a str {
let owned = String::from("short");
&owned
}
fn main() { println!("{}", dangling()); }'static 作为类型约束表示不含短命借用,不表示值永远不会释放。拥有的 String 可以满足 'static 并在函数退出后正常 Drop。跨线程接口常要求 'static,因为线程可能活得比当前栈帧久;短期借用线程可研究 thread::scope,不能用泄漏内存代替所有权设计。
闭包、迭代与释放
闭包的捕获方式取决于对捕获值的使用。Fn 能经共享借用反复调用;FnMut 需要可变借用;FnOnce 至少能消费一次。move 规定捕获所有权,不直接决定只能调用一次。例子将 owned 移出闭包环境,因此只能满足 FnOnce。迭代器适配器是惰性的;map 不消费就不会执行映射,collect 或 count 才驱动遍历。iter/iter_mut/into_iter 分别共享借用、独占借用或取得集合元素所有权;Rust 2024 数组 into_iter 是按值。
RAII 把资源释放绑定到值的 Drop,移动后由新所有者负责释放。Box 独占堆对象,Rc 单线程共享,Arc 跨线程共享;共享所有权不自动允许修改。循环强引用会泄漏,可用 Weak 打破反向边。标准容器优先于手写裸指针;堆和栈是理解模型,优化器可能消除分配,不能从变量声明位置推导真实机器分配。
运行与自检
在仓库 examples/rust/concurrency-lab 中,PowerShell/Bash 均可运行 cargo run --offline --example types;完整项目无第三方依赖。cargo test --offline 同时核对第一行借用和 unsafe 包装边界。准备正常工具链时无需本仓库验证环境使用的专用链接器。
练习:① 让 Score 输出字符串,需要修改 trait 调用方吗?② 为什么 longest 的返回值不能逃出较短输入的作用域?③ 让闭包只读取 owned.len(),它仍然必须只能调用一次吗?④ 需要保存米和秒,别名是否足够?
参考解法
① 将 Score 的关联 Output 改为 String,render 返回格式化文本;show 的 Display 约束已经允许它。② 返回值可能指向任一输入,签名必须限制最短可用区域;即使某次分支返回长寿命输入,也不能违反声明的统一契约。③ 不再移出 owned,move 捕获的闭包仍可实现 Fn 并重复读取。④ 用不同 newtype 并显式提供转换,别名与原类型完全相同。
资料与版本核对
2026-10-17 核对 官方中文学习入口、英文 Book trait、生命周期、闭包。中文入口用于比较术语,接口保证回到英文规范与标准库;本站机制解释和项目独立编写。
继续阅读
- 排错
- 排错
- 排错