跳转到内容

Rust 类型转换:From、Into、TryFrom、FromStr 与 AsRef

程序时时刻刻都在做转换:表单里的文字变成数字,三个数字组成的元组变成一个 Color,一种错误类型变成另一种。Rust 为此准备了一小组标准 trait。认识它们之后,你会发现真实代码里到处都是它们的身影。

可以把这些 trait 想象成出国旅行时带的转换插头,每个都只负责一件事。有的永远插得上(From)。有的可能插不上,而且会老实告诉你(TryFrom)。有的看一眼标签就能把东西造出来(FromStr)。还有的让你手上有什么就插什么(AsRef)。这一页会慢慢地一个个讲。你已经在类型转换那一页见过 as,也在常用 trait 那一页初步认识了 From,我们就在这些基础上继续。

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 的意思是:“我知道怎样用那个类型把自己造出来。”你其实已经用过它了,很多标准类型都实现了它:

let greeting = String::from("hello"); // &str -> String
let 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 的那一节回到这个问题。

下面的 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 里了,不需要导入任何东西。

我们来做一个只能存放 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 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,因为文字永远可能是错的。

想象一家相框店,顾客输入 "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(&note)); // &String
println!("{}", word_count(note)); // String
}

输出:

3
4
4
▶ 动手试一试

这里的句子保留了英文,因为 split_whitespace() 按空格切分单词,而中文句子里通常没有空格。在函数内部,不管调用者传进来的是什么,text.as_ref() 都会给你一个普通的 &str。

你很可能早就在不知不觉中用过 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"));
}

输出:

txt
jpeg
gz
(无扩展名)
▶ 动手试一试

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 低得多,见到时认得出来就够了。

拿不准的时候,可以从这张表开始:

你想要…… 用 例子
在数字类型之间快速转换,接受截断 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 this
13 | 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 甚至会对这种写法发出警告。)

写一个 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 技巧,只不过这次用的是标准类型。

写一个 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