第1章 - 泛型
嗨,朋友!我是长安。
欢迎来到进阶教程的第一章!今天我们要聊一个非常强大的特性——泛型(Generics)。
泛型可以让你写出适用于多种类型的代码,避免重复。听起来有点抽象?别担心,我会用最简单的方式给你讲清楚。
🤔 为什么需要泛型?
想象一下,你要写两个函数,一个找出整数列表中的最大值,一个找出浮点数列表中的最大值:
fn largest_i32(list: &[i32]) -> i32 {
let mut largest = list[0];
for &item in list {
if item > largest {
largest = item;
}
}
largest
}
fn largest_f64(list: &[f64]) -> f64 {
let mut largest = list[0];
for &item in list {
if item > largest {
largest = item;
}
}
largest
}
你会发现,这两个函数的逻辑完全一样,只是类型不同。如果再来一个 char 类型,还要再写一遍?太麻烦了!
泛型的解决方案:写一个函数,适用于所有类型!
长安说
泛型就像是一个模板,你定义一次,可以用于多种类型。就像做蛋糕的模具,同一个模具可以做巧克力蛋糕、草莓蛋糕、抹茶蛋糕!
📖 泛型函数
基本语法
使用 <T> 来定义泛型参数:
fn largest<T>(list: &[T]) -> T {
let mut largest = list[0];
for &item in list {
if item > largest { // ❌ 这样还不能编译
largest = item;
}
}
largest
}
这里 T 是一个类型参数,代表"任意类型"。
注意
上面的代码还不能编译!因为不是所有类型都能用 > 比较。我们需要添加 trait 约束,下一章会详细讲。现在先看一个能编译的例子。
实际例子:交换两个值
fn swap<T>(a: T, b: T) -> (T, T) {
(b, a)
}
fn main() {
// 用于整数
let (x, y) = swap(1, 2);
println!("x = {}, y = {}", x, y); // x = 2, y = 1
// 用于字符串
let (s1, s2) = swap("hello", "world");
println!("s1 = {}, s2 = {}", s1, s2); // s1 = world, s2 = hello
// 用于浮点数
let (f1, f2) = swap(3.14, 2.71);
println!("f1 = {}, f2 = {}", f1, f2); // f1 = 2.71, f2 = 3.14
}
同一个 swap 函数,可以用于不同类型!Rust 编译器会根据使用情况自动推断 T 的具体类型。
多个泛型参数
你可以使用多个泛型参数:
fn print_pair<T, U>(a: T, b: U) {
println!("a = {:?}, b = {:?}", a, b);
}
fn main() {
print_pair(5, "hello"); // T=i32, U=&str
print_pair(3.14, true); // T=f64, U=bool
print_pair("rust", 2024); // T=&str, U=i32
}
🏗️ 泛型结构体
结构体也可以使用泛型:
struct Point<T> {
x: T,
y: T,
}
fn main() {
let integer_point = Point { x: 5, y: 10 };
let float_point = Point { x: 1.0, y: 4.0 };
println!("整数点: ({}, {})", integer_point.x, integer_point.y);
println!("浮点点: ({}, {})", float_point.x, float_point.y);
}
注意
在上面的例子中,x 和 y 必须是相同类型,因为它们都是 T。
如果你想让 x 和 y 可以是不同类型,需要两个泛型参数:
struct Point<T, U> {
x: T,
y: U,
}
fn main() {
let mixed_point = Point { x: 5, y: 4.0 }; // x是i32,y是f64
println!("混合点: ({}, {})", mixed_point.x, mixed_point.y);
}
🔧 泛型方法
结构体的方法也可以使用泛型:
struct Point<T> {
x: T,
y: T,
}
impl<T> Point<T> {
fn new(x: T, y: T) -> Self {
Point { x, y }
}
fn x(&self) -> &T {
&self.x
}
}
fn main() {
let p = Point::new(5, 10);
println!("x 坐标: {}", p.x());
}
注意 impl<T> 中的 <T>,这表示我们为泛型类型 T 实现方法。
为特定类型实现方法
你也可以只为某个具体类型实现方法:
impl Point<f64> {
fn distance_from_origin(&self) -> f64 {
(self.x.powi(2) + self.y.powi(2)).sqrt()
}
}
fn main() {
let p = Point { x: 3.0, y: 4.0 };
println!("距离原点: {}", p.distance_from_origin()); // 5.0
// let p2 = Point { x: 3, y: 4 };
// p2.distance_from_origin(); // ❌ 错误!只有 Point<f64> 有这个方法
}
📦 泛型枚举
你已经见过泛型枚举了!Option<T> 和 Result<T, E> 都是泛型枚举:
enum Option<T> {
Some(T),
None,
}
enum Result<T, E> {
Ok(T),
Err(E),
}
自己定义一个泛型枚举:
enum Container<T> {
Single(T),
Double(T, T),
Triple(T, T, T),
}
fn main() {
let c1 = Container::Single(5);
let c2 = Container::Double("hello", "world");
let c3 = Container::Triple(1.1, 2.2, 3.3);
match c2 {
Container::Double(a, b) => println!("{} {}", a, b),
_ => {}
}
}
⚡ 泛型的性能
你可能会担心:泛型会不会影响性能?
好消息:在 Rust 中,泛型是零成本抽象!
编译器会为每个具体类型生成专门的代码,这个过程叫做单态化(Monomorphization)。
例如这段代码:
fn swap<T>(a: T, b: T) -> (T, T) {
(b, a)
}
fn main() {
swap(1, 2);
swap("a", "b");
}
编译器会生成两个版本的 swap 函数:
// 编译器自动生成
fn swap_i32(a: i32, b: i32) -> (i32, i32) {
(b, a)
}
fn swap_str(a: &str, b: &str) -> (&str, &str) {
(b, a)
}
所以运行时性能和手写的代码完全一样!
长安说
这就是 Rust 的魔法:你写的是泛型代码(简洁、不重复),但运行的是具体类型的代码(快速、高效)!
🎯 实战例子:通用容器
让我们写一个简单的容器,可以存储任意类型的值:
struct Box<T> {
value: T,
}
impl<T> Box<T> {
fn new(value: T) -> Self {
Box { value }
}
fn get(&self) -> &T {
&self.value
}
fn set(&mut self, value: T) {
self.value = value;
}
}
fn main() {
let mut int_box = Box::new(42);
println!("整数盒子: {}", int_box.get());
int_box.set(100);
println!("修改后: {}", int_box.get());
let string_box = Box::new(String::from("Rust"));
println!("字符串盒子: {}", string_box.get());
}
💡 小结
在这一章,我们学会了:
- 泛型让你写出适用于多种类型的代码
- 使用
<T>定义泛型参数 - 泛型函数:一个函数,多种类型
- 泛型结构体:灵活的数据结构
- 泛型枚举:
Option<T>和Result<T, E> - 零成本抽象:泛型不会影响性能
核心要点:
- 泛型参数用
<T>表示 - 可以有多个泛型参数:
<T, U, V> - 编译器会自动推断类型
- 单态化保证性能
长安的建议
刚开始用泛型可能会觉得语法有点复杂,但多写几次就习惯了。泛型是 Rust 中非常强大的特性,能让你的代码更加灵活和可复用!
🚀 下一步
你可能注意到,我们还没讲如何对泛型进行约束(比如要求 T 必须能比较大小)。
下一章,我们会学习 Trait,它和泛型配合使用,威力无穷!
💪 练习题
- 写一个泛型函数
first<T>,返回切片的第一个元素(返回Option<&T>) - 定义一个泛型结构体
Pair<T>,包含两个相同类型的值,并实现方法打印这对值 - 定义一个泛型枚举
Choice<T, U>,可以是First(T)或Second(U) - 为
Point<T>实现一个方法swap,交换 x 和 y 的值
答案示例
// 练习1
fn first<T>(list: &[T]) -> Option<&T> {
if list.is_empty() {
None
} else {
Some(&list[0])
}
}
// 练习2
struct Pair<T> {
first: T,
second: T,
}
impl<T: std::fmt::Display> Pair<T> {
fn print(&self) {
println!("({}, {})", self.first, self.second);
}
}
// 练习3
enum Choice<T, U> {
First(T),
Second(U),
}
// 练习4
impl<T> Point<T> {
fn swap(&mut self) {
std::mem::swap(&mut self.x, &mut self.y);
}
}
fn main() {
let numbers = [1, 2, 3, 4, 5];
if let Some(f) = first(&numbers) {
println!("第一个元素: {}", f);
}
let pair = Pair { first: 10, second: 20 };
pair.print();
let choice1: Choice<i32, &str> = Choice::First(42);
let choice2: Choice<i32, &str> = Choice::Second("hello");
}
