首页
/ Rustlings 23_conversions 实战指南:Rust 标准库中的类型转换全景——as、From/Into、TryFrom/TryInto、FromStr 与 AsRef/AsMut

Rustlings 23_conversions 实战指南:Rust 标准库中的类型转换全景——as、From/Into、TryFrom/TryInto、FromStr 与 AsRef/AsMut

2026-09-04 18:23:39作者:魏侃纯Zoe

本文以 Rustlings 仓库中 23_conversions 练习集的官方导读文档 exercises/23_conversions/README.md 为核心,系统梳理 Rust 标准库提供的五类类型转换机制:as 强制转换、From/IntoTryFrom/TryIntoFromStr 字符串解析、AsRef/AsMut 引用转换。每个机制都结合该练习集中对应练习文件与参考实现的完整代码、单元测试展开,帮助读者掌握“何时用哪种转换”的判断标准,并能独立完成从温度单位换算、字符串解析到 RGB 颜色校验等典型场景的转换代码。

Rust 官方文档指出:as 是最简单的类型转换形式;而 From/IntoTryFrom/TryIntoAsRef/AsMut 定义在标准库的 std::convert 模块中,FromStr 则定义在 std::str 模块中。导读文档特别强调,这几种方式就是标准库范围内把数据转换成目标类型的主要途径。

五种转换机制总览

23_conversions 练习集由 5 个练习文件组成,与导读文档中列出的转换方式一一对应:

转换机制 所属模块 能否失败 返回值 对应练习
as 强制转换 语言内建语法 不报错(可能静默截断/取整) 目标类型值 conversions1.rs
From / Into std::convert 不能(必然成功) 目标类型值 conversions2.rs
FromStr / parse std::str Result<Self, Err> conversions3.rs
TryFrom / TryInto std::convert 能(受控失败) Result<Self, Error> conversions4.rs
AsRef / AsMut std::convert 不能 引用(&T / &mut T conversions5.rs

下面按导读文档的顺序逐一展开。

as 强制转换:类型转换表达式

导读文档中的原话是:

The simplest form of type conversion is a type cast expression. It is denoted with the binary operator as.

as 是最直接的转换方式。例如 println!("{}", 1 + 1.0); 无法编译,因为 1 是整数而 1.0 是浮点数;而 println!("{}", 1 as f32 + 1.0) 可以正常编译——1 as f32 先把整数 1 强制转换为 f32,再参与浮点加法。

练习文件 conversions1.rs 给出了一个典型的真实需求:计算 f64 数组的平均值。文件开头的注释还提醒了一点容易被忽略的事实——as 除了类型转换之外,也用于给导入项重命名(如 use std::io::Read as ReadFromStdin)。

题目代码:

fn average(values: &[f64]) -> f64 {
    let total = values.iter().sum::<f64>();
    // TODO: Make a conversion before dividing.
    total / values.len()
}

fn main() {
    let values = [3.5, 0.3, 13.0, 11.7];
    println!("{}", average(&values));
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn returns_proper_type_and_value() {
        assert_eq!(average(&[3.5, 0.3, 13.0, 11.7]), 7.125);
    }
}

问题出在 total / values.len()totalf64,而 values.len() 返回 usize(整数),两者直接相除无法通过编译。

参考实现 solutions/23_conversions/conversions1.rs 的解法非常简洁——对 len() 的返回值做 as f64

fn average(values: &[f64]) -> f64 {
    let total = values.iter().sum::<f64>();
    total / values.len() as f64
    //                   ^^^^^^
}

需要注意 as 的行为边界:它只做位级或算术级的直接转换(如 i64 as f64u8 as i16),对浮点取整、整数向更小位宽的整数转换时会静默截断而不报错。因此在标准库层面,凡是“可能失败”的整数转换,更推荐使用下一节的 TryFrom/TryInto,这也正是 conversions4 练习验证的场景。

From 与 Into:必然成功的值转换

导读文档指出 FromIntostd::convert 模块下,用于“便于类型转换”。conversions2.rs 的注释给出了两个关键点:

  1. From 用于值到值的转换;
  2. 只要实现了 From,编译器会自动提供对应的 Into 实现(反之亦然,但惯例只手写 From)。

该练习用一个业界常见模式开场:用不同的结构体表示不同的度量单位,避免把摄氏度、华氏度数值混用。题目要求实现双向转换:

struct Celsius(f64);

struct Fahrenheit(f64);

impl From<Celsius> for Fahrenheit {
    // TODO: Convert Celsius to Fahrenheit. Don't worry about floating-point
    // precision. The formula is: F = C * 1.8 + 32
}

impl From<Fahrenheit> for Celsius {
    // TODO: Convert Fahrenheit to Celsius.
}

参考实现 solutions/23_conversions/conversions2.rs

impl From<Celsius> for Fahrenheit {
    fn from(Celsius(celsius): Celsius) -> Self {
        Fahrenheit(celsius * 1.8 + 32.0)
    }
}

impl From<Fahrenheit> for Celsius {
    fn from(Fahrenheit(fahrenheit): Fahrenheit) -> Self {
        Celsius((fahrenheit - 32.0) / 1.8)
    }
}

实现中有一个值得注意的 Rust 惯用法:fn from(Celsius(celsius): Celsius) 直接在参数位置做模式解构,把 Celsius 内部封装的 f64 取出,函数体里就不用写 .0

转换公式为:

  • 摄氏 → 华氏:F = C * 1.8 + 32
  • 华氏 → 摄氏:C = (F - 32) / 1.8

该练习的测试用 6 组固定用例验证双向转换,并通过 .round() 屏蔽浮点精度误差:

const CASES: [(f64, f64); 6] = [
    (-50.0, -58.0),
    (0.0, 32.0),
    (20.0, 68.0),
    (100.0, 212.0),
    (400.0, 752.0),
    (1000.0, 1832.0),
];

#[test]
fn celsius_to_fahrenheit() {
    for (celsius, fahrenheit) in CASES {
        let Fahrenheit(actual) = Celsius(celsius).into();
        assert_eq!(actual.round(), fahrenheit);
    }
}

#[test]
fn fahrenheit_to_celsius() {
    for (celsius, fahrenheit) in CASES {
        let Celsius(actual) = Fahrenheit(fahrenheit).into();
        assert_eq!(actual.round(), celsius);
    }
}

注意测试里没有调用 Fahrenheit::from(...),而是调用 .into()——这就是“实现 From 后自动获得 Into”的直接体现:Into 提供了目标类型无关的方法调用形式,调用端无需关心具体 From 实现。

FromStr:把字符串解析为结构化类型

导读文档提到:std::str 模块提供了一个 FromStr trait,通过字符串的 parse 方法把字符串转换成目标类型。文档给出的示例是:若为 Person 类型正确实现了该 trait,那么 let p: Person = "Mark,20".parse().unwrap() 应当既编译通过又运行不 panic。

conversions3.rs 的注释进一步说明:与 From 不同,FromStr 表达的转换可能失败,因此返回 Result。该练习要求把形如 "Mark,20" 的字符串解析为 Person 结构体:

use std::num::ParseIntError;
use std::str::FromStr;

#[derive(Debug, PartialEq)]
struct Person {
    name: String,
    age: u8,
}

// We will use this error type for the `FromStr` implementation.
#[derive(Debug, PartialEq)]
enum ParsePersonError {
    // Incorrect number of fields
    BadLen,
    // Empty name field
    NoName,
    // Wrapped error from parse::<u8>()
    ParseInt(ParseIntError),
}

impl FromStr for Person {
    type Err = ParsePersonError;

    fn from_str(s: &str) -> Result<Self, Self::Err> {}
}

fn main() {
    let p = "Mark,20".parse::<Person>();
    println!("{p:?}");
}

题目注释把实现步骤明确拆成了 6 步:

  1. 按逗号分割字符串;
  2. 分割结果不足或多于 2 个元素时返回 ParsePersonError::BadLen
  3. 第一个元素作为姓名;
  4. 姓名为空时返回 ParsePersonError::NoName
  5. 把第二个元素解析为 u8 作为年龄(如 "4".parse::<u8>());
  6. 年龄解析失败时返回 ParsePersonError::ParseInt

参考实现 solutions/23_conversions/conversions3.rs 展示了现代 Rust 的 let-else 惯用法:

fn from_str(s: &str) -> Result<Self, Self::Err> {
    let mut split = s.split(',');
    let (Some(name), Some(age), None) = (split.next(), split.next(), split.next()) else {
        //                      ^^^^ there should be no third element
        return Err(ParsePersonError::BadLen);
    };

    if name.is_empty() {
        return Err(ParsePersonError::NoName);
    }

    let age = age.parse().map_err(ParsePersonError::ParseInt)?;

    Ok(Self {
        name: name.into(),
        age,
    })
}

这里有三个细节值得留意:

  • 长度校验技巧(split.next(), split.next(), split.next()) 取三次 next,用 let-else 要求“恰好两个 Some 加一个 None”,一次性覆盖了空字符串、缺逗号、尾部多余逗号等情况——"John,32," 会因第三个字段是 Some("") 而落入 BadLen 分支;
  • 错误包装age.parse().map_err(ParsePersonError::ParseInt)? 把内层的 ParseIntError 包进自己的错误枚举,而不是让它直接穿透,这样错误类型对外是封闭的;
  • name.into()name&str,而 Person.nameString,这里又用到了 From<&str> for String 的自动转换——同一个 From/Into 机制在不同层级的复用。

配套测试对边界输入覆盖得很全,是学习“解析类 API 应如何写测试”的好样本(节选自 conversions3.rs):

#[test]
fn empty_input() {
    assert_eq!("".parse::<Person>(), Err(BadLen));
}

#[test]
fn missing_age() {
    assert!(matches!("John,".parse::<Person>(), Err(ParseInt(_))));
}

#[test]
fn missing_name() {
    assert_eq!((",1").parse::<Person>(), Err(NoName));
}

#[test]
fn trailing_comma() {
    assert_eq!("John,32,".parse::<Person>(), Err(BadLen));
}

一个易错点:missing_age 测试("John,")断言的是 Err(ParseInt(_)) 而非 NoName——因为逗号后空字符串解析为 u8 失败优先被捕获。另外注意测试里 ",1".parse::<Person>() 之类的输入,空姓名会先触发 NoName 分支,说明实现中“姓名为空”检查先于年龄解析。

TryFrom 与 TryInto:可能失败的受控转换

conversions4.rs 的注释对 TryFrom 的定位是:“a simple and safe type conversion that may fail in a controlled way under some circumstances. Basically, this is the same as From. The main difference is that this should return a Result type instead of the target type itself.”——本质上和 From 相同,唯一区别是返回 Result

该练习把 Color(三个 u8 分量,各分量取值范围 0..=255)分别实现了三个 TryFrom(i16, i16, i16) 元组、[i16; 3] 数组、&[i16] 切片,并定义了共享错误类型:

use std::convert::{TryFrom, TryInto};

#[derive(Debug, PartialEq)]
struct Color {
    red: u8,
    green: u8,
    blue: u8,
}

#[derive(Debug, PartialEq)]
enum IntoColorError {
    // Incorrect length of slice
    BadLen,
    // Integer conversion error
    IntConversion,
}

impl TryFrom<(i16, i16, i16)> for Color {
    type Error = IntoColorError;
    fn try_from(tuple: (i16, i16, i16)) -> Result<Self, Self::Error> {}
}

impl TryFrom<[i16; 3]> for Color {
    type Error = IntoColorError;
    fn try_from(arr: [i16; 3]) -> Result<Self, Self::Error> {}
}

// This implementation needs to check the slice length.
impl TryFrom<&[i16]> for Color {
    type Error = IntoColorError;
    fn try_from(slice: &[i16]) -> Result<Self, Self::Error> {}
}

参考实现 solutions/23_conversions/conversions4.rs 体现了**“以元组实现为基准,其余实现全部复用”**的结构:

impl TryFrom<(i16, i16, i16)> for Color {
    type Error = IntoColorError;

    fn try_from(tuple: (i16, i16, i16)) -> Result<Self, Self::Error> {
        let (Ok(red), Ok(green), Ok(blue)) = (
            u8::try_from(tuple.0),
            u8::try_from(tuple.1),
            u8::try_from(tuple.2),
        ) else {
            return Err(IntoColorError::IntConversion);
        };

        Ok(Self { red, green, blue })
    }
}

impl TryFrom<[i16; 3]> for Color {
    type Error = IntoColorError;

    fn try_from(arr: [i16; 3]) -> Result<Self, Self::Error> {
        // Reuse the implementation for a tuple.
        Self::try_from((arr[0], arr[1], arr[2]))
    }
}

impl TryFrom<&[i16]> for Color {
    type Error = IntoColorError;

    fn try_from(slice: &[i16]) -> Result<Self, Self::Error> {
        if let &[red, green, blue] = slice {
            // Reuse the implementation for a tuple.
            Self::try_from((red, green, blue))
        } else {
            Err(IntoColorError::BadLen)
        }
    }
}

几个值得学习的点:

  • u8::try_from(i16) 是标准库自带的范围校验i16u8 的窄化转换只有在值落在 0..=255 内才返回 Ok,否则返回 Err。这正是标准库为整数窄化转换提供的受控版本,与静默截断的 as u8 形成对照。实现中再次用 let-else 对三个 Result 做整体匹配,任何一个越界就整体返回 IntConversion
  • 切片实现负责长度校验if let &[red, green, blue] = slice 用固定长度的解构模式天然要求切片恰好 3 个元素,长度不符(2 个或 4 个)落入 BadLen
  • TryFromTryInto 也互为自动派生:练习的 main 函数展示了两种调用形式:
// Using the `try_from` function.
let c1 = Color::try_from((183, 65, 14));
println!("{c1:?}");

// Since `TryFrom` is implemented for `Color`, we can use `TryInto`.
let c2: Result<Color, _> = [183, 65, 14].try_into();
println!("{c2:?}");

let v = vec![183, 65, 14];
// With slice we should use the `try_from` function
let c3 = Color::try_from(&v[..]);
println!("{c3:?}");
// or put the slice within round brackets and use `try_into`.
let c4: Result<Color, _> = (&v[..]).try_into();
println!("{c4:?}");

配套测试覆盖了“正向越界、负向越界、合法值、长度不符”四类输入(完整 15 个测试见 conversions4.rs 测试模块),例如:

#[test]
fn test_tuple_out_of_range_positive() {
    assert_eq!(Color::try_from((256, 1000, 10000)), Err(IntConversion));
}

#[test]
fn test_tuple_correct() {
    let c: Result<Color, _> = (183, 65, 14).try_into();
    assert!(c.is_ok());
    assert_eq!(
        c.unwrap(),
        Color { red: 183, green: 65, blue: 14 }
    );
}

#[test]
fn test_slice_excess_length() {
    let v = vec![0, 0, 0, 0];
    assert_eq!(Color::try_from(&v[..]), Err(BadLen));
}

#[test]
fn test_slice_insufficient_length() {
    let v = vec![0, 0];
    assert_eq!(Color::try_from(&v[..]), Err(BadLen));
}

test_slice_correct 中还特意验证了 &v[..]Vec 转切片引用)这条路径,说明 Vec<i16> 本身不是转换源,只有切片是。

AsRef 与 AsMut:廉价的引用到引用转换

导读文档将 AsRefAsMut 归入 std::convert 模块的转换 trait 之一。conversions5.rs 的注释给出其定位:“AsRef and AsMut allow for cheap reference-to-reference conversions.”——它们做的是引用到引用的转换,不移动值、不拷贝数据。

题目要求给三个通用函数补上 trait bound:

// Obtain the number of bytes (not characters) in the given argument
// (`.len()` returns the number of bytes in a string).
// TODO: Add the `AsRef` trait appropriately as a trait bound.
fn byte_counter<T>(arg: T) -> usize {
    arg.as_ref().len()
}

// Obtain the number of characters (not bytes) in the given argument.
// TODO: Add the `AsRef` trait appropriately as a trait bound.
fn char_counter<T>(arg: T) -> usize {
    arg.as_ref().chars().count()
}

// Squares a number using `as_mut()`.
// TODO: Add the appropriate trait bound.
fn num_sq<T>(arg: &mut T) {
    // TODO: Implement the function body.
}

参考实现 solutions/23_conversions/conversions5.rs 的补全:

fn byte_counter<T: AsRef<str>>(arg: T) -> usize {
    arg.as_ref().len()
}

fn char_counter<T: AsRef<str>>(arg: T) -> usize {
    arg.as_ref().chars().count()
}

fn num_sq<T: AsMut<u32>>(arg: &mut T) {
    let arg = arg.as_mut();
    *arg *= *arg;
}

这组代码同时说明了 AsRef/AsMut 的价值所在:

  • 一个函数同时接受 &strString:测试里 byte_counter(s) 传入字符串字面量(&'static str),byte_counter(s) 又传入 String,两者都能编译。因为 str: AsRef<str>String: AsRef<str> 都成立,函数体里通过 as_ref() 统一拿到 &str 再调用 .len()(字节数)或 .chars().count()(字符数);
  • 字节数与字符数不等价:测试用例 "Café au lait" 中含一个多字节字符 é,所以 char_counter(s) != byte_counter(s);而纯 ASCII 的 "Cafe au lait" 两者相等。这对理解 Rust 字符串的 UTF-8 底层表示是个很好的锚点;
  • AsMut 提供“透过包装拿到内部可变引用”的通用接口num_sq 要求 T: AsMut<u32>,内部 arg.as_mut() 得到 &mut u32,再执行 *arg *= *arg 完成平方。标准库中 Box<T> where T: ?SizedRefCellRc 等都实现了 AsMut,因此 num_sq(&mut Box::new(3)) 这种调用成立——测试 mut_box 正是验证这一点:
#[test]
fn mut_box() {
    let mut num: Box<u32> = Box::new(3);
    num_sq(&mut num);
    assert_eq!(*num, 9);
}

AsRef/AsMutFrom 的区别在于:前者操作的是引用(便宜、无所有权的“视图”转换),后者是的转换。当你的 API 只想“把参数当字符串看”而不关心它是 String&str 还是 OsString 时,AsRef<str>/AsRef<Path> 这类 bound 是标准库风格的首选。

机制选型与总结

把导读文档与五个练习串起来,可以得到一张实用的选型表(结论均来自 exercises/23_conversions/README.md 及各练习注释的表述):

场景 首选机制 依据
数值间直接位级/算术转换(如 usize as f64 as 编译期表达式,最简单;但不做范围检查,可能静默截断
必然成功的值转换(如温度单位换算、&str → String From/Into 返回目标值本身;实现 From 自动获得 Into
可能失败的受控值转换(如 i16 → u8、RGB 校验) TryFrom/TryInto 返回 Result;标准库已为整数窄化转换提供
从字符串文本解析结构化数据 FromStr(配合 .parse() 返回 Result,可用 .parse::<T>() 统一入口
泛型函数接收“看起来像某类型”的参数、透过包装取内部引用 AsRef<T> / AsMut<T> 引用到引用的廉价转换,无所有权转移

如何在 Rustlings 中运行这些练习

本仓库是一个通过 rustlings CLI 驱动练习的项目(入口见 src/main.rs),每个练习文件的 main 函数可独立运行,测试通过 Cargo 测试框架验证。典型的运行流程(以 conversions2 为例):

# 列出当前状态
rustlings

# 运行并验证 conversions2 练习
rustlings run conversions2

运行成功的前提是练习代码能通过编译且其内部 #[cfg(test)] 模块的测试全部通过(如 celsius_to_fahrenheitfahrenheit_to_celsius)。如果练习卡住,可以直接对照 solutions/23_conversions/ 下的参考实现(conversions1.rsconversions5.rs)与 exercises/23_conversions/ 下的题目文件逐行对比,两者结构完全一致,差异即为本练习要补全的代码。

需要说明的适用前提:以上机制描述均以标准库(std::convertstd::str)为准,导读文档也明确限定范围为“within the standard library”;as 的行为、TryFrom 对整数窄化转换的内置实现等细节,依赖当前工具链的标准库版本,以 rustlings 仓库随附练习可编译运行成功为准。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.12 K
2.72 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
528
588
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
906
1.82 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
854
1.34 K
docsdocs
暂无描述
Markdown
891
5.78 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.53 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.34 K
1.45 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
987
504
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384