Rust 类型转换:From、Into、TryFrom、FromStr 与 AsRef
程序时时刻刻都在做转换:表单里的文字变成数字,三个数字组成的元组变成一个 Color,一种错误类型变成另一种。Rust 为此准备了一小组标准 trait。认识它们之后,你会发现真实代码里到处都是它们的身影。
可以把这些 trait 想象成出国旅行时带的转换插头,每个都只负责一件事。有的永远插得上(From)。有的可能插不上,而且会老实告诉你(TryFrom)。有的看一眼标签就能把东西造出来(FromStr)。还有的让你手上有什么就插什么(AsRef)。这一页会慢慢地一个个讲。你已经在类型转换那一页见过 as,也在常用 trait 那一页初步认识了 From,我们就在这些基础上继续。
为什么不直接用 as?
Section titled “为什么不直接用 as?”as 很方便,但有两个局限。它只能在数字、char、bool 这类简单的内置类型之间转换。而且它从不失败:值放不下的时候,它会悄悄给你一个别的数字。
fn main() { let items_in_cart: i32 = 300;
// `as` 把 300 硬塞进 u8,悄无声息地算错了 let squeezed = items_in_cart as u8; println!("用 as:{squeezed}");
// `try_from` 会发现问题并告诉你 let checked = u8::try_from(items_in_cart); println!("用 try_from:{:?}", checked);
// 只有转换永远不会丢失数据时,才有 `from` 可用 let widened = i64::from(items_in_cart); println!("用 from:{widened}");}输出:
用 as:44用 try_from:Err(TryFromIntError(PosOverflow))用 from:300这个 44 就是那种能藏上好几个月的 bug。转换 trait 从三个方面解决了这个问题:
- 它们很诚实:只有在不可能出错时才会实现
From。可能出错时,就用TryFrom,它返回一个Result。 - 它们能用在你自己的类型上:你没法写
(255, 0, 0) as Color,但可以实现From<(u8, u8, u8)> for Color。 - 其他代码都认识它们:
?运算符、.parse()以及很多库函数都是建立在这些 trait 之上的。
所以有一条好用的经验:as 只留给快速又安全的数字小把戏,其他场合都用这些 trait。
From 与 Into:总能成功的转换
Section titled “From 与 Into:总能成功的转换”From 的意思是:“我知道怎样用那个类型把自己造出来。”你其实已经用过它了,很多标准类型都实现了它:
let greeting = String::from("hello"); // &str -> Stringlet big = i64::from(42_i32); // i32 -> i64,永远放得下let precise = f64::from(7_u8); // u8 -> f64,永远精确let scores = Vec::from([90, 85, 77]); // 数组 -> Vec<i32>注意少了什么:没有 i32::from(some_i64)。i64 的值不一定放得进 i32,所以 Rust 不会假装这个转换总能成功。我们会在讲 TryFrom 的那一节回到这个问题。
给自己的类型实现 From
Section titled “给自己的类型实现 From”下面的 Color 可以由红、绿、蓝三个值组成的元组构造出来:
#[derive(Debug)]struct Color { red: u8, green: u8, blue: u8,}
impl From<(u8, u8, u8)> for Color { fn from((red, green, blue): (u8, u8, u8)) -> Self { Color { red, green, blue } }}
fn main() { let orange = Color::from((255, 136, 0)); println!("{:?}", orange);
// 我们从没写过 `Into`,却免费得到了它: let sky: Color = (135, 206, 235).into(); println!("天空的颜色是 r={} g={} b={}", sky.red, sky.green, sky.blue);}输出:
Color { red: 255, green: 136, blue: 0 }天空的颜色是 r=135 g=206 b=235妙就妙在这里。标准库规定:“如果 B 实现了 From<A>,那么 A 就自动实现 Into<B>。”所以写一个 impl From,你就得到了两种转换方式:
Color::from(tuple)读作“用这个造一个Color”。tuple.into()读作“把它变成这里需要的类型”。
为什么要写 let sky: Color?因为 .into() 本身没说要变成什么。理论上,一个元组可以转换成很多种类型,类型注解告诉 Rust 你指的是哪一种。如果 Rust 已经能看出目标类型,比如从函数参数推断出来,就不需要注解。
你还会经常在函数参数里看到 .into()。像 fn new(title: impl Into<String>) 这样的函数,既能接收 &str,也能接收 String,然后在函数里调用 title.into()。这里 Rust 知道目标类型,所以不需要注解。在练习 2 里你会亲手试一试这种写法。
错误处理中的 From:? 怎样转换错误
Section titled “错误处理中的 From:? 怎样转换错误”From 在这里默默替你做了很多事。? 遇到 Err 时,并不只是把它原样返回,而是先对这个错误调用 From::from,把它变成你的函数返回的错误类型。
所以,如果你的函数返回自己的 AppError,而 AppError 实现了 From<ParseIntError>,那么 ? 就能自己把解析错误变成 AppError:
use std::num::ParseIntError;
#[derive(Debug)]enum AppError { MissingEquals, BadNumber(ParseIntError),}
impl From<ParseIntError> for AppError { fn from(e: ParseIntError) -> Self { AppError::BadNumber(e) }}
// 读取像 "max_users = 50" 这样的一行配置fn read_setting(line: &str) -> Result<(String, u32), AppError> { let (key, value) = line.split_once('=').ok_or(AppError::MissingEquals)?; let number = value.trim().parse::<u32>()?; // ParseIntError -> AppError Ok((key.trim().to_string(), number))}
fn main() { println!("{:?}", read_setting("max_users = 50")); println!("{:?}", read_setting("max_users = lots")); println!("{:?}", read_setting("max_users 50"));}输出:
Ok(("max_users", 50))Err(BadNumber(ParseIntError { kind: InvalidDigit }))Err(MissingEquals)如果没有那个 impl From,第二个 ? 就无法编译,因为 Rust 不知道怎样把 ParseIntError 变成 AppError。有了它,函数体就能保持简洁。自定义错误那一页会更深入地讲怎样设计错误类型,anyhow 与 thiserror 那一页则介绍能替你写好这些 From 实现的 crate。
TryFrom 与 TryInto:可能失败的转换
Section titled “TryFrom 与 TryInto:可能失败的转换”有些转换只是有时候能成功。300 放不进 u8,-5 也不是有效的百分比。对这类情况,Rust 提供了 TryFrom。它和 From 用法相似,但返回一个 Result,所以你必须处理“不行”的情况。这是好事:失败不会从你眼皮底下溜过去。
你在这一页开头已经见过内置的版本了:u8::try_from(300_i32) 返回一个 Err,而不是一个错误的数字。就像 From 自带 Into 一样,TryFrom 也自带 TryInto,所以 let small: Result<u8, _> = big.try_into(); 也能用。从 2021 版次(edition)开始,TryInto 就在 prelude 里了,不需要导入任何东西。
给自己的类型实现 TryFrom
Section titled “给自己的类型实现 TryFrom”我们来做一个只能存放 0 到 100 的 Percentage 类型。如果每个 Percentage 都必须通过 try_from 创建,那么程序里的每个 Percentage 都是有效的。只在门口检查一次,以后再也不用操心。
#[derive(Debug)]struct Percentage(u8);
#[derive(Debug)]struct OutOfRange(i32);
impl TryFrom<i32> for Percentage { type Error = OutOfRange;
fn try_from(value: i32) -> Result<Self, Self::Error> { if (0..=100).contains(&value) { Ok(Percentage(value as u8)) // 安全:刚刚检查过范围 } else { Err(OutOfRange(value)) } }}
fn main() { println!("{:?}", Percentage::try_from(85)); println!("{:?}", Percentage::try_from(150));
let battery: Result<Percentage, _> = (-5).try_into(); println!("{:?}", battery);}输出:
Ok(Percentage(85))Err(OutOfRange(150))Err(OutOfRange(-5))type Error = OutOfRange; 这一行告诉 Rust 这个转换会返回哪种错误。错误类型由你来选。这里它带着那个不合格的值,拿到错误的人就能显示一条有用的提示。
FromStr 与 .parse():从文字构造值
Section titled “FromStr 与 .parse():从文字构造值”文字是你最常需要转换的东西。文件、命令行参数和网页表单给你的都是字符串。FromStr trait 描述“怎样用 &str 构造出这个类型”,而 .parse() 是调用它的友好方式。
当你写 "42".parse::<i32>() 时,Rust 会找到 i32 的 FromStr 实现并运行它。::<i32> 这部分(大家管它叫“turbofish”)告诉 Rust 你想要哪个类型。给变量加类型注解也一样可以。
let age = "42".parse::<i32>(); // Ok(42)let price: Result<f64, _> = "4.99".parse(); // Ok(4.99)let broken = "forty-two".parse::<i32>(); // Err(ParseIntError { kind: InvalidDigit })每次调用都返回 Result,因为文字永远可能是错的。
给自己的类型实现 FromStr
Section titled “给自己的类型实现 FromStr”想象一家相框店,顾客输入 "30x40" 这样的尺寸。我们来教 Rust 读懂它:
use std::str::FromStr;
#[derive(Debug)]struct Size { width: u32, height: u32,}
#[derive(Debug)]enum SizeError { MissingX, BadNumber,}
impl FromStr for Size { type Err = SizeError;
fn from_str(text: &str) -> Result<Self, Self::Err> { let (w, h) = text.split_once('x').ok_or(SizeError::MissingX)?; let width = w.trim().parse().map_err(|_| SizeError::BadNumber)?; let height = h.trim().parse().map_err(|_| SizeError::BadNumber)?; Ok(Size { width, height }) }}
fn main() { println!("{:?}", "30x40".parse::<Size>());
println!("{:?}", "30 by 40".parse::<Size>()); println!("{:?}", "30xbig".parse::<Size>());}输出:
Ok(Size { width: 30, height: 40 })Err(MissingX)Err(BadNumber)有两个小细节。第一,FromStr 把它的错误类型叫做 Err,而不是像 TryFrom 那样叫 Error。这是历史遗留的命名习惯,刚开始搞混很正常,编译器会提醒你。第二,实现这个 trait 需要 use std::str::FromStr;,但调用 .parse() 不需要。
.map_err(|_| SizeError::BadNumber) 把数字解析错误换成我们自己的错误。只在一个地方需要时,用它来代替写一个 From 实现很方便。
AsRef:接收任何能借给你“视图”的东西
Section titled “AsRef:接收任何能借给你“视图”的东西”前面的 trait 都会创建一个新值。AsRef 不一样,它什么也不构造,只是说:“我能以那个类型的身份,把自己廉价地借给你看一眼。”
为什么需要这个?假设你写一个统计单词数的函数。如果参数是 &str,手里拿着 String 的调用者就得写 &name。如果参数是 String,手里拿着 &str 的调用者就得分配一个新的 String。用 impl AsRef<str>,这些写法统统都能用:
fn word_count(text: impl AsRef<str>) -> usize { text.as_ref().split_whitespace().count()}
fn main() { let note = String::from("buy milk and eggs");
println!("{}", word_count("hello rust world")); // &str println!("{}", word_count(¬e)); // &String println!("{}", word_count(note)); // String}输出:
344这里的句子保留了英文,因为 split_whitespace() 按空格切分单词,而中文句子里通常没有空格。在函数内部,不管调用者传进来的是什么,text.as_ref() 都会给你一个普通的 &str。
标准库里的 AsRef:路径
Section titled “标准库里的 AsRef:路径”你很可能早就在不知不觉中用过 AsRef 了。std::fs 里的文件函数长这样:
pub fn read_to_string<P: AsRef<Path>>(path: P) -> io::Result<String>所以 fs::read_to_string("notes.txt")、fs::read_to_string(my_string) 和 fs::read_to_string(my_path_buf) 都能用。你在自己的代码里也可以用同样的技巧。下面的例子只看路径的名字,不会碰任何真实文件:
use std::path::{Path, PathBuf};
fn extension_of(path: impl AsRef<Path>) -> String { match path.as_ref().extension() { Some(ext) => ext.to_string_lossy().to_string(), None => String::from("(无扩展名)"), }}
fn main() { println!("{}", extension_of("notes.txt")); println!("{}", extension_of(String::from("holiday.jpeg"))); println!("{}", extension_of(PathBuf::from("backups/2024/site.tar.gz"))); println!("{}", extension_of("README"));}输出:
txtjpeggz(无扩展名)AsMut 是同样的思路,只不过它借出的是可变视图。例如,Vec 和数组都能把自己借出为可变 slice:
fn reset_all(scores: &mut impl AsMut<[u32]>) { for score in scores.as_mut() { *score = 0; }}
// 这两种写法都可以:// reset_all(&mut vec![12, 7, 30]);// reset_all(&mut [3, 1, 4]);AsMut 的使用频率比 AsRef 低得多,见到时认得出来就够了。
该用哪一个?
Section titled “该用哪一个?”拿不准的时候,可以从这张表开始:
| 你想要…… | 用 | 例子 |
|---|---|---|
| 在数字类型之间快速转换,接受截断 | as |
x as f64 |
| 转换永远不会失败 | From |
i64::from(n)、String::from("hi") |
| 同上,而且目标类型已经确定 | Into |
let s: String = "hi".into(); |
| 转换可能失败 | TryFrom / TryInto |
u8::try_from(300) |
| 从文字构造一个值 | FromStr / .parse() |
"42".parse::<i32>() |
| 廉价地接收各种类似字符串或路径的输入 | AsRef |
fn f(p: impl AsRef<Path>) |
| 把值变成给人读的文字 | Display → .to_string() |
42.to_string() |
最后一行是白送的:实现 Display(见常用 trait),你的类型的每个值就都有了 .to_string() 方法。
在 Rust 看不出目标类型时调用 .into()
fn main() { let name = "Ferris".into(); println!("{name}");}error[E0283]: type annotations needed --> src/main.rs:2:9 |2 | let name = "Ferris".into(); | ^^^^ ---- type must be known at this point | = note: the type must implement `From<&str>` = note: required for `&str` to implement `Into<_>`help: consider giving `name` an explicit type |2 | let name: /* Type */ = "Ferris".into(); | ++++++++++++编译器说:需要类型注解。&str 可以变成 String、Box<str>、Vec<u8> 等等,Rust 不会去猜。加上类型:let name: String = "Ferris".into();,或者干脆写 String::from("Ferris")。
实现 Into 而不是 From:看起来两者应该是一回事,所以这是个很容易踩的坑:
use std::num::ParseIntError;
#[derive(Debug)]struct AppError(String);
impl Into<AppError> for ParseIntError { fn into(self) -> AppError { AppError(self.to_string()) }}
fn read_port(text: &str) -> Result<u16, AppError> { let port = text.parse::<u16>()?; Ok(port)}error[E0277]: `?` couldn't convert the error to `AppError` --> src/main.rs:13:35 |12 | fn read_port(text: &str) -> Result<u16, AppError> { | --------------------- expected `AppError` because of this13 | let port = text.parse::<u16>()?; | --------------^ the trait `From<ParseIntError>` is not implemented for `AppError` | = note: the question mark operation (`?`) implicitly performs a conversion on the error value using the `From` trait编译器说:? 没法把错误转换成 AppError,因为 AppError 没有实现 From<ParseIntError>。这份“赠品”只有一个方向:实现 From 会送你 Into,但实现 Into 不会送你 From。而 ? 和大量库代码要的都是 From。所以永远实现 From,让 Into 自己跟过来。(Rust 的代码检查工具 Clippy 甚至会对这种写法发出警告。)
1. 读取时钟时间
Section titled “1. 读取时钟时间”写一个 Time 结构体,包含 hour: u8 和 minute: u8。实现 FromStr,让 "09:45".parse::<Time>() 能用。如果没有 :、某一部分不是数字,或者小时大于 23、分钟大于 59,就返回错误。用 "09:45"、"25:00" 和 "午饭" 试一试。提示:split_once(':') 能给你前后两半。错误类型用一个简单的 String 就行。
查看答案
use std::str::FromStr;
#[derive(Debug)]struct Time { hour: u8, minute: u8,}
impl FromStr for Time { type Err = String;
fn from_str(text: &str) -> Result<Self, Self::Err> { let (h, m) = text.split_once(':').ok_or("缺少 ':'")?; let hour: u8 = h.parse().map_err(|_| "小时不是数字")?; let minute: u8 = m.parse().map_err(|_| "分钟不是数字")?;
if hour > 23 || minute > 59 { return Err(format!("{text} 不是有效的时间")); } Ok(Time { hour, minute }) }}
fn main() { for input in ["09:45", "25:00", "午饭"] { match input.parse::<Time>() { Ok(t) => println!("{input} -> {} 时 {} 分", t.hour, t.minute), Err(e) => println!("{input} -> 错误:{e}"), } }}输出:
09:45 -> 9 时 45 分25:00 -> 错误:25:00 不是有效的时间午饭 -> 错误:缺少 ':'.ok_or("缺少 ':'") 后面的 ? 之所以能用,是因为 String 实现了 From<&str>。这正是错误处理那一节里的 From 技巧,只不过这次用的是标准类型。
2. 厨房计时器
Section titled “2. 厨房计时器”写一个 Timer 结构体,包含 minutes: u32 和 seconds: u32。实现 From<u32>,把总秒数变成一个 Timer(150 变成 2 分 30 秒)。然后写一个打印时间的 fn start(timer: impl Into<Timer>),分别用 150 和 Timer { minutes: 1, seconds: 5 } 调用它。提示:分钟数是 total / 60,剩下的是 total % 60。每个类型本来就能转换成它自己,所以直接传一个 Timer 也行。
查看答案
struct Timer { minutes: u32, seconds: u32,}
impl From<u32> for Timer { fn from(total: u32) -> Self { Timer { minutes: total / 60, seconds: total % 60 } }}
fn start(timer: impl Into<Timer>) { let t = timer.into(); println!("计时器已设为 {}:{:02}", t.minutes, t.seconds);}
fn main() { start(150_u32); start(Timer { minutes: 1, seconds: 5 });}输出:
计时器已设为 2:30计时器已设为 1:05我们只写了 From<u32>,start 却既能接收数字,也能接收 Timer。数字靠的是我们写的 From;Timer 能用,是因为标准库规定每个类型都能转换成它自己。
as用于快速的数字转换,可能会悄悄丢失数据。转换 trait 明确、安全,还能用在你自己的类型上。- 对总能成功的转换实现
From,就免费得到Into。.into()需要知道目标类型。 ?运算符会调用From来转换错误类型,所以impl From<OtherError> for MyError能让错误处理保持简短。- 转换可能失败时,用
TryFrom/TryInto,它们返回Result。 - 实现
FromStr,就能对文字使用.parse::<YourType>()。 - 用
impl AsRef<str>或impl AsRef<Path>廉价地接收多种输入类型,不需要复制。
小测验
1. 你实现了 From<Celsius> for Fahrenheit。还会自动得到什么?
2. 一个 i32 的值可能是 300,要把它安全地变成 u8,该用哪种写法?
3. 要让 "30x40".parse::<Size>() 能用,需要实现什么?
4. 为什么 let name = "Ferris".into(); 无法编译?
得分: 0 / 4
std::convert— 标准库文档(英文):From、Into、TryFrom、TryInto、AsRef和AsMut的大本营,附有一份简短的指南,说明什么时候该实现哪一个。FromStr— 标准库文档(英文):.parse()背后的 trait,附完整的实现示例。AsRef— 标准库文档(英文):AsRef和Borrow有什么区别,以及为什么用它来做廉价的引用转换。- 类型转换 —《通过例子学 Rust》:关于
From/Into、TryFrom/TryInto以及字符串互转的几个简短可运行章节。 - Conversions — Rust API Guidelines(英文):社区关于自定义类型应该实现哪些转换 trait 的建议。