首页
/ Rustlings 14_generics 练习详解:用 Vec 类型推断与泛型 Wrapper 掌握 Rust 泛型基础

Rustlings 14_generics 练习详解:用 Vec 类型推断与泛型 Wrapper 掌握 Rust 泛型基础

2026-09-04 12:42:24作者:裘旻烁

本文基于 Rustlings 仓库中 exercises/14_generics/README.md 所定义的泛型(Generics)章节展开,完整继承该章节的练习题、测试与参考答案,并结合 dev/Cargo.toml 的构建配置与 src/exercise.rs 的检查流程,说明每一道泛型练习的解题思路、语法要点以及 Rustlings 如何自动验证你的答案。读完后,你将能够独立完成 Rust 中最常见的泛型使用方式——类型参数(type parameters)的声明与推断,并理解为何泛型要求“谨慎地指定一个泛型类型在哪些类型上有效”。

一、Rustlings 如何定义 Generics 这一主题

exercises/14_generics/README.md 对泛型主题给出了四点核心论断,这也是本章节两道练习的编写依据:

  1. Generics 的本质是“泛化”:把类型和功能推广到更一般的情形(generalizing types and functionalities to broader cases);
  2. 价值在于减少代码重复:同一个结构、同一个方法无需为每种具体类型各写一份;
  3. 代价是语法更复杂:泛型会引入一些相对繁琐的语法;
  4. 关键约束:使用泛型时必须仔细指明一个泛型类型究竟在哪些类型上是合法的(specify over which types a generic type is actually considered valid);
  5. 最简且最常用的形式是类型参数(type parameters)。

此外,从 exercises/README.md 的“练习与《The Rust Book》章节映射表”可以看到,generics 对应书籍的 §10 章(Generics, Traits, and Lifetimes),而紧随其后的 traits(§10.2)与 lifetimes(§10.3)正是本章节“进一步信息”中 Bounds 概念的后续展开。因此本章练习的定位是:先建立“类型参数”这一基础心智模型,为后续 traits 约束与生命周期参数打地基。

二、环境准备:练习如何被注册与校验

Rustlings 中每道练习都是独立的二进制目标。generics 章节的两道题及其参考解在 dev/Cargo.toml 中成对注册:

{ name = "generics1",    path = "../exercises/14_generics/generics1.rs" },
{ name = "generics1_sol", path = "../solutions/14_generics/generics1.rs" },
{ name = "generics2",    path = "../exercises/14_generics/generics2.rs" },
{ name = "generics2_sol", path = "../solutions/14_generics/generics2.rs" },

_sol 后缀的目标指向 solutions/14_generics 目录下的参考答案,rustlings 校验失败时会打印该文件路径供你对比(相关输出逻辑见 src/exercise.rs 中的 solution_link_line)。

src/exercise.rsRunnableExercise::run 实现可以看出,每道练习通过以下流水线判定“完成”:

  1. cargo build 编译练习二进制,编译不过直接判失败;
  2. 若练习含测试(generics2#[cfg(test)] 模块会被识别),执行 cargo test,测试全部通过才算数;
  3. 执行 cargo clippy --profile test 做静态检查;
  4. 运行二进制,非零退出码视为失败。

其中 --profile test 参数是关键细节:它让 Clippy 也检查 #[cfg(test)] 下的代码,因此你在 generics2.rs 测试模块里写的断言同样受 lint 约束。

另外,dev/Cargo.toml[lints] 段对练习代码施加了额外纪律,这些直接影响泛型代码的写法:

[lints.rust]
unsafe_code = "forbid"        # 练习中禁止 unsafe
unstable_features = "forbid"  # 禁止 nightly 特性
dead_code = "allow"           # 允许死代码,避免学习期干扰

[lints.clippy]
todo = "forbid"      # 别忘了删掉 todo!()
empty_loop = "forbid"
infinite_loop = "deny"
mem_forget = "deny"

也就是说,即使泛型代码逻辑正确,若残留 todo!() 或使用了 unsafe,rustlings 也会判你不通过。

三、练习一 generics1:为 Vec<T> 补上类型标注

3.1 题目原文

exercises/14_generics/generics1.rs 的完整代码如下(注释是题目的一部分):

// `Vec<T>` is generic over the type `T`. In most cases, the compiler is able to
// infer `T`, for example after pushing a value with a concrete type to the vector.
// But in this exercise, the compiler needs some help through a type annotation.

fn main() {
    // TODO: Fix the compiler error by annotating the type of the vector
    // `Vec<T>`. Choose `T` as some integer type that can be created from
    // `u8` and `i8`.
    let mut numbers = Vec::new();

    // Don't change the lines below.
    let n1: u8 = 42;
    numbers.push(n1.into());
    let n2: i8 = -1;
    numbers.push(n2.into());

    println!("{numbers:?}");
}

3.2 为什么编译器在这里“推不出来”

Vec<T>T 是泛型的。大多数情况下,编译器能通过你对向量的使用(比如 push 一个具体类型值)反推出 T。但本题中 push 接收的是 n1.into()n2.into()——Into 是单参泛型方法,n1: u8 可以转换成许多目标类型(i16i32i64f64……),n2: i8 同样可以。两条语句各自能成立的目标类型集合虽然都很大,但编译器在没有任何显式约束时,无法从 u8i8 两个来源唯一确定 Vec 的元素类型,于是给出类似 “type annotations needed for Vec<_>” 的编译错误。这正是 README 中“being generic requires taking great care to specify over which types a generic type is actually considered valid” 的最小示例:泛型类型合法,但具体是哪个实例化,必须由你明确

题目要求 T 是“能由 u8i8 构造出来的某个整数类型”。u8(0..=255)与 i8(-128..=127)的并集必须完整容纳,所以 T 的范围必须同时覆盖两者。

3.3 参考解法

solutions/14_generics/generics1.rs 只改动了一行:

fn main() {
    // `u8` and `i8` can both be converted to `i16`.
    let mut numbers: Vec<i16> = Vec::new();
    //             ^^^^^^^^^^ added

    // Don't change the lines below.
    let n1: u8 = 42;
    numbers.push(n1.into());
    let n2: i8 = -1;
    numbers.push(n2.into());

    println!("{numbers:?}");
}

要点解析:

  • 选择 i16 是最“紧凑”的合法解:它是最小的能同时容纳 u8i8 全部取值范围的整数类型,From<u8> for i16From<i8> for i16 都在标准库中实现,因此 into() 编译通过;
  • 标注 Vec<i16> 后,n1.into()n2.into() 的目标类型被唯一钉死为 i16,推断链闭合;
  • 运行输出为 [42, -1]

你也可以把标注写成 Vec<i32>Vec<u16>u8i8 均可无损转入),只要“由 u8 和 i8 均可构造”这一条件成立即可——这道题考察的是理解推断失败的机制,而不是背出唯一答案。

四、练习二 generics2:把 Wrapper 改写成泛型 Wrapper<T>

4.1 题目原文

exercises/14_generics/generics2.rs 的完整代码如下:

// This powerful wrapper provides the ability to store a positive integer value.
// TODO: Rewrite it using a generic so that it supports wrapping ANY type.
struct Wrapper {
    value: u32,
}

// TODO: Adapt the struct's implementation to be generic over the wrapped value.
impl Wrapper {
    fn new(value: u32) -> Self {
        Wrapper { value }
    }
}

fn main() {
    // You can optionally experiment here.
}

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

    #[test]
    fn store_u32_in_wrapper() {
        assert_eq!(Wrapper::new(42).value, 42);
    }

    #[test]
    fn store_str_in_wrapper() {
        assert_eq!(Wrapper::new("Foo").value, "Foo");
    }
}

注意题目结构:structimpl 块都写着 TODO,要求“支持包裹任意类型”;而两个单元测试 store_u32_in_wrapperstore_str_in_wrapper 已经写死——它们分别要求 Wrapper 能装 u32 字面量和 &'static str,这就是 README 所说“指明泛型类型在哪些类型上有效”的具体验收标准:至少对 u32&str 同时成立

4.2 参考解法

solutions/14_generics/generics2.rs 的完整改动只有两处:

struct Wrapper<T> {
    value: T,
}

impl<T> Wrapper<T> {
    fn new(value: T) -> Self {
        Wrapper { value }
    }
}

语法要点逐条拆解:

  1. struct Wrapper<T>:在结构体名后声明类型参数 T,字段 value 从具体类型 u32 升级为占位类型 T。此时 Wrapper<u32>Wrapper<&str>Wrapper<Vec<i32>> 都是合法的具体化类型;
  2. impl<T> Wrapper<T>impl 块头部必须重复声明 T,否则编译器不知道 T 从哪来。注意这里没有加任何 trait 约束(如 T: CopyT: PartialEq),因为 new 只是把值存进结构体,不对其做任何操作——这正是“越泛越好”的体现:不需要约束时就不要加约束;
  3. fn new(value: T) -> Self:参数类型改为 T,返回 Self(即当前实例化的 Wrapper<T>),函数体 Wrapper { value } 一行未变。方法体中唯一用到的操作是构造,因此它对任意 T 都成立;
  4. 测试侧零改动即可通过:Wrapper::new(42) 推断为 Wrapper<u32>Wrapper::new("Foo") 推断为 Wrapper<&str>assert_eq! 比较的都是同型值。

对照 Rustlings 的校验流水线(见第二节),这道题的通过条件实际是:cargo build 通过 + cargo test 两个断言通过 + cargo clippy --profile test 无告警 + 运行退出码为 0。由于测试代码位于 #[cfg(test)] 模块,--profile test 保证了 Clippy 也会对断言语句进行检查。

五、小结:本章建立的两个泛型心智模型

结合 exercises/14_generics/README.md 的主题定位与两道练习,本章需要带走的能力可以归纳为两条:

  1. 类型标注是泛型推断的“补全键”:当编译器从多条可能的 Into/From 转换路径中无法唯一确定类型参数时(generics1.rs),显式写出 Vec<i16> 这类标注就是正确的解法,而不是修改 push 语句;
  2. 泛型 = 声明类型参数 + 在无约束需求时不加约束:把 structimpl、函数签名中的具体类型替换为 T 即可让代码作用于任意类型(generics2.rs),而何时需要 trait bound 则是下一章(15_traits,对应书 §10.2)的主题。

完成本章后,仓库中的后续练习 15_traits/16_lifetimes/ 会继续深化 Bounds 与生命周期参数;若想知道某道练习与官方教程的对应关系,可随时查阅 exercises/README.md 中的映射表。

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

项目优选

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