阶段二 · 类型转换与函数式

类型转换体系

一句话总结

Rust 的类型转换分两条路:as 是面向原始数值的"截断式"强制转换,可能悄悄丢数据.
From/IntoTryFrom/TryInto 则是基于特征的,语义清晰的安全转换--能否失败由类型本身表达出来.

前置回顾

前三篇围绕字符串展开,其中 to_string(),parse(),解引用强制转换都涉及类型之间的转化.本篇把这些零散的点串成一个体系:Rust 的类型转换到底有几种路径,什么时候该用哪条.

两个世界:as 与特征转换

新手常把所有转换都用 as 一把梭,结果在数值溢出时被坑.理解 Rust 转换体系的第一步,是分清两个截然不同的世界.

维度 as 强转 From/Into 特征转换
适用对象 原始数值,指针等 任意实现了特征的类型
失败处理 静默截断/环绕,不报错 From 不会失败;TryFrom 返回 Result
可扩展性 语言内置,不可为自定义类型扩展 可为自己的类型实现
可读性 简短但语义弱 显式表达转换意图

as:锋利但危险的数值强转

as 直接作用于内存表示,不做任何范围检查.当目标类型容纳不下源值时,它会静默截断.

let big = 300_i32;
let small = big as u8; // 300 超出 u8 (0-255),结果是 44(300 % 256)

let neg = -1_i32;
let unsigned = neg as u32; // 4294967295,按位重新解释

let pi = 3.99_f64;
let truncated = pi as i32; // 3,向零截断,不是四舍五入

as 不会报错,只会给出错误的数

300 as u8 得到 44,编译器既不报错也不警告.这类静默数据损坏是难以排查的 bug 源头.

仅在明确知道值域安全,或确实需要按位重新解释(如底层位运算)时才用 as.涉及外部输入或不确定范围的数值收窄,一律改用 TryFrom.

From 与 Into:一体两面

From 描述"如何从另一个类型构造出我",Into 描述"如何把我变成另一个类型".关键在于,只要实现 From,编译器会自动免费送一个对应的 Into.

struct Celsius(f64);
struct Fahrenheit(f64);

// 只需实现 From
impl From<Celsius> for Fahrenheit {
fn from(c: Celsius) -> Self {
Fahrenheit(c.0 * 9.0 / 5.0 + 32.0)
}
}

let c = Celsius(100.0);

let f1 = Fahrenheit::from(c); // 用 From,显式写出目标类型
// let f2: Fahrenheit = c.into(); // 用 Into,自动获得,靠类型标注推断

实现 From,别实现 Into

标准库有一条覆盖实现:impl<T, U: From<T>> Into<U> for T.意思是只要 U: From<T> 成立,T: Into<U> 就自动成立.

所以惯例是永远只实现 From,Into 白送.两者的选择只体现在调用风格上:From 适合显式写目标类型,Into 适合作为泛型约束 fn f<T: Into<U>>(x: T),让函数接受多种可转换的输入.

FromInto 像同一笔交易的买卖双方视角:卖家说"我可以把货卖出去"(From),买家说"我可以买下这件货"(Into).
只要登记了卖家一方,买家一方的合同自动成立,不必重复签字.

TryFrom 与 TryInto:可能失败的转换

当转换可能失败时(如大整数收窄到小整数),用 From 就不合适了--它的签名不允许返回错误.这时改用 TryFrom,它返回 Result.

use std::convert::TryFrom;

let big = 300_i32;

// 安全收窄:超范围会得到 Err,而不是静默截断
match u8::try_from(big) {
Ok(v) => println!("转换成功: {v}"),
Err(e) => println!("转换失败: {e}"), // 走这里,300 放不进 u8
}

let ok = u8::try_from(200_i32).unwrap(); // 200 在范围内,成功

同样地,实现 TryFrom 后会自动获得 TryInto.这就是 parse 背后的同类思想:把"可能失败"显式编码进类型签名,逼调用方处理错误.

收窄转换的正确姿势

记住这条对照:放大永远安全(u8 → u32From,标准库已实现);收窄可能丢值(u32 → u8TryFrom,强制面对失败).把这两者区分开,能消除一大类隐蔽的数值 bug.

AsRef:廉价的引用转换

还有一类转换不涉及所有权转移,只是"把我当作某种引用来看待",这就是 AsRef.它常用于让函数接受多种引用类型.

// 同时接受 &str,String,&String 等任何能看作 str 的类型
fn print_len<S: AsRef<str>>(s: S) {
println!("{}", s.as_ref().len());
}

print_len("literal"); // &str
print_len(String::from("owned")); // String
print_len(&String::from("borrowed")); // &String

From/Into 与 AsRef 的分工

From/Into 表示"消耗并转换成一个新值",通常涉及所有权转移甚至堆分配.
AsRef 表示"临时借用为另一种引用",零成本,不转移所有权.

设计 API 时:要拿走数据的所有权用 Into,只读访问用 AsRef.文件路径相关函数大量用 AsRef<Path> 正是这个道理.

孤儿规则:为什么有时实现不了 From

当试图为转换实现 From 时,可能撞上这个编译错误:

error[E0117]: only traits defined in the current crate can be
implemented for types defined outside of the crate

这是孤儿规则(Orphan Rule) 在起作用:实现 impl Trait for Type 时,TraitType 至少有一个必须定义在自己的 crate 里.不能同时拿别人的特征和别人的类型来组合,否则两个不同的库可能给同一组合写出冲突的实现,编译器无从裁决.

绕过孤儿规则:newtype 模式

想为外部类型(如 Vec<T>)实现外部特征时,标准做法是 newtype:用一个自己定义的元组结构体把它包一层,例如 struct MyVec(Vec<T>).现在 MyVec 是本地类型,就能自由地为它实现任何特征了.这是 Rust 生态里极为常见的模式.

快速回顾

  • as 强转:仅用于数值/指针,静默截断,危险,慎用
  • From/Into:不会失败的安全转换,只实现 From,Into 自动获得
  • TryFrom/TryInto:可能失败的转换,返回 Result,用于收窄等场景
  • AsRef:零成本引用转换,让函数接受多种引用类型
  • 孤儿规则:特征或类型至少一方在本 crate;用 newtype 绕过

动手练习

  1. 验证 as 截断:分别打印 256_i32 as u8,-1_i32 as u8,2.9_f64 as i32,解释每个结果.
  2. 实现 From:定义 struct Meters(f64)struct Feet(f64),实现 From<Meters> for Feet,再用 .into() 完成一次转换,体会两种调用风格.
  3. 安全收窄:用 TryFrom 写一个把 i64 安全转成 u16 的逻辑,对越界输入返回 Err 并打印错误信息.