在上一篇文章中,我们讲到了 Rust 中的方法与特征,今天,我们进入另一个类型话题——泛型。
泛型(Generics),是编程语言中一种参数化类型机制,旨在通过抽象化实现代码复用与类型安全。
哦,对了,我们先来看上一文的答案:
trait Logger {
fn log(&self, verbosity: u8, message: &str);
}
struct StderrLogger;
impl Logger for StderrLogger {
fn log(&self, verbosity: u8, message: &str) {
eprintln!("verbosity={verbosity}: {message}");
}
}
struct VerbosityFilter {
max_verbosity: u8,
inner: StderrLogger,
}
impl Logger for VerbosityFilter {
fn log(&self, verbosity: u8, message: &str) {
if verbosity <= self.max_verbosity {
self.inner.log(verbosity, message);
}
}
}
fn main() {
let logger = VerbosityFilter { max_verbosity: 3, inner: StderrLogger };
logger.log(5, "FYI");
logger.log(2, "Uhoh");
}
// Output
// verbosity=2: Uhoh
泛型函数
Rust 支持泛型,这使你能够对算法或数据结构(例如排序或二叉树)中的类型进行抽象,从而大大的简化后续扩展流程。
先举一个简单的例子:
fn pick<T>(cond: bool, left: T, right: T) -> T {
if cond {
left
} else {
right
}
}
fn main() {
println!("picked a number: {:?}", pick(true, 222, 333));
println!("picked a string: {:?}", pick(false, 'L', 'R'));
}
// Output
// picked a number: 222
// picked a string: 'R'
如果我们不使用泛型,那么我们应该会这样实现代码:
fn pick_i32(cond: bool, left: i32, right: i32) -> i32 {
if cond {
left
} else {
right
}
}
fn pick_char(cond: bool, left: char, right: char) -> char {
if cond {
left
} else {
right
}
}
我们会针对 i32 和 char 类型实现两个函数,如果后续还需要使用 i64,f32等其他类型,我们就需要为每一个类型编写一个 pick_xxx 函数。这,正是泛型的用武之地,避免了无意义的重复流程。
Rust 会根据参数和返回值的类型为 T 推断出一个类型。
上面的例子中,我们只展示了 i32 和 char 类型,我们当然可以在此处使用任何类型,包括自定义类型:
struct Foo {
val: u8,
}
pick(false, Foo { val: 7 }, Foo { val: 99 });
这与 C++ 模板类似,但 Rust 会立即对泛型函数进行部分编译,因此该函数对于所有符合约束的类型都必须有效。例如,尝试将 pick 修改为在 cond 为 false 时返回 left + right。即使仅使用针对整数的 pick 实例化,Rust 仍会认为这是无效的。而 C++ 则允许你这样做:
fn pick<T>(cond: bool, left: T, right: T) -> T {
if cond {
left
} else {
left + right
}
}
pick(true, 222, 333) // compile error: cannot add `T` to `T`
你并没有表明 T 之间可以相加,所以在函数中进行相加是错误的。
在 Rust,泛型代码会根据调用点转换为非泛型代码。这是一种零开销抽象:你得到的结果与手动编写没有使用该抽象的数据结构时完全相同。也就是说,当我们编写泛型代码的时候,Rust 会为我们生成非泛型的版本,在所有调用的地方调用的是非泛型版本(这和 Java 这样的语言有很大的不同,Java 的泛型是会有运行时开销)。
Trait 边界
在使用泛型时,你通常希望要求类型实现某些特征,以便可以调用该特征的方法。 你可以使用 T: Trait 来做到这一点:
fn duplicate<T: Clone>(a: T) -> (T, T) {
(a.clone(), a.clone())
}
struct NotCloneable;
fn main() {
let foo = String::from("foo");
let pair = duplicate(foo);
println!("{pair:?}");
}
Rust 还支持另一种泛型边界的写法:
fn duplicate<T>(a: T) -> (T, T)
where
T: Clone,
{
(a.clone(), a.clone())
}
如果你有很多的泛型边界,那么这种 where 写法确实会很有用。欣赏一下 std::collections::HashMap 中 get 的写法:
pub fn get<Q: ?Sized>(&self, k: &Q) -> Option<&V>
where
K: Borrow<Q>,
Q: Hash + Eq,
{
self.base.get(k)
}
Rust 不直接支持传统意义上的函数重载,即允许同一个函数名有多个不同参数列表的版本。
fn duplicate(a: i32) -> (i32, i32) {
(a.clone(), a.clone())
}
// 无法通过编译
fn duplicate(a: f64) -> (f64, f64) {
(a.clone(), a.clone())
}
这样是行不通的。
所以对于原有的 duplicate,添加一个专门针对 a: u32 的 duplicate 是无效的:
fn duplicate<T>(a: T) -> (T, T)
where
T: Clone,
{
(a.clone(), a.clone())
}
// 无法通过编译
fn duplicate(a: i32) -> (i32, i32) {
(a.clone(), a.clone())
}
这样当然也不行。
泛型数据类型
你可以使用泛型来对具体的字段类型进行抽象。回到上一部分的练习:
pub trait Logger {
fn log(&self, verbosity: u8, message: &str);
}
struct StderrLogger;
impl Logger for StderrLogger {
fn log(&self, verbosity: u8, message: &str) {
eprintln!("verbosity={verbosity}: {message}");
}
}
struct VerbosityFilter<L> {
max_verbosity: u8,
inner: L,
}
impl<L: Logger> Logger for VerbosityFilter<L> {
fn log(&self, verbosity: u8, message: &str) {
if verbosity <= self.max_verbosity {
self.inner.log(verbosity, message);
}
}
}
fn main() {
let logger = VerbosityFilter { max_verbosity: 3, inner: StderrLogger };
logger.log(5, "FYI");
logger.log(2, "Uhoh");
}
我们可以专门针对 L: Logger 类型编写实现,即,当 L 是 Logger 的子类型的之后,才能有 Logger 的能力。
我们不会在 VerbosityFilter 类型本身设置 trait 约束。你也可以在那里设置约束,但在 Rust 中,通常我们只在 impl 块上设置 trait 约束。
泛型 Trait
特性(Traits)也可以像类型和函数一样是泛型的。特性的参数在使用时会被赋予具体的类型。例如,From<T> 特性用于定义类型转换:
// 该 Trait 是标准库的声明
pub trait From<T>: Sized {
fn from(value: T) -> Self;
}
#[derive(Debug)]
struct Foo(String);
impl From<u32> for Foo {
fn from(from: u32) -> Foo {
Foo(format!("Converted from integer: {from}"))
}
}
impl From<bool> for Foo {
fn from(from: bool) -> Foo {
Foo(format!("Converted from bool: {from}"))
}
}
fn main() {
let from_int = Foo::from(123);
let from_bool = Foo::from(true);
dbg!(from_int);
dbg!(from_bool);
}
// Output
//
// [src/main.rs:19:5] from_int = Foo(
// "Converted from integer: 123",
// )
// [src/main.rs:20:5] from_bool = Foo(
// "Converted from bool: true",
// )
该特性的实现并不需要涵盖所有可能的类型参数。在这里,Foo::from("hello") 将无法编译,因为 Foo 并没有针对 From<&str> 的实现。
泛型特性将类型作为 “输入”,而关联类型是一种 “输出” 类型。一个特性可以针对不同的输入类型有多个实现。
实际上,Rust 要求对于任何类型 T,一个特性最多只有一个匹配的实现。与其他一些语言不同,Rust 没有用于选择 “最具体” 匹配的启发式方法。目前有关于添加此支持的工作,称为特化(specialization)。
impl Trait
与特性界限类似,impl trait 语法可用于函数参数和返回值中:
// 下面写法的语法糖:
// fn add_42_millions<T: Into<i32>>(x: T) -> i32 {
fn add_42_millions(x: impl Into<i32>) -> i32 {
x.into() + 42_000_000
}
fn pair_of(x: u32) -> impl std::fmt::Debug {
(x + 1, x - 1)
}
fn main() {
let many = add_42_millions(42_i8);
dbg!(many);
let many_more = add_42_millions(10_000_000);
dbg!(many_more);
let debuggable = pair_of(27);
dbg!(debuggable);
}
// Output
//
// [src/main.rs:13:5] many = 42000042
// [src/main.rs:15:5] many_more = 52000000
// [src/main.rs:17:5] debuggable = (
// 28,
// 26,
// )
impl trait 允许你使用无法命名的类型。
impl trait 在不同位置的含义略有不同:
- 对于参数,
impl trait就像是带有特性界限的匿名泛型参数。 - 对于返回类型,它意味着返回类型是实现了该特性的某个具体类型,但无需指定该类型的名称。当你不想在公共 API 中暴露具体类型时,这会很有用。
在返回位置进行类型推导比较困难。返回 impl Foo 的函数会选择它返回的具体类型,但无需在源代码中写出。返回像 collect<B>() -> B 这样泛型类型的函数可以返回满足 B 的任何类型,调用者可能需要选择一种类型,比如通过 let x: Vec<i32> = foo.collect() 或使用 turbofish 语法 foo.collect::<Vec<i32>>()。
dyn Trait
除了通过泛型使用特性进行静态分派之外,Rust 还支持通过特性对象将其用于类型擦除的动态分派:
struct Dog {
name: String,
age: i8,
}
struct Cat {
lives: i8,
}
trait Pet {
fn talk(&self) -> String;
}
impl Pet for Dog {
fn talk(&self) -> String {
format!("Woof, my name is {}!", self.name)
}
}
impl Pet for Cat {
fn talk(&self) -> String {
String::from("Miau!")
}
}
// 使用泛型——静态分派。
fn generic(pet: &impl Pet) {
println!("Hello, who are you? {}", pet.talk());
}
// 使用类型擦除——动态分派。
fn dynamic(pet: &dyn Pet) {
println!("Hello, who are you? {}", pet.talk());
}
fn main() {
let cat = Cat { lives: 9 };
let dog = Dog { name: String::from("Fido"), age: 5 };
generic(&cat);
generic(&dog);
dynamic(&cat);
dynamic(&dog);
}
// Output
// Hello, who are you? Miau!
// Hello, who are you? Woof, my name is Fido!
// Hello, who are you? Miau!
// Hello, who are you? Woof, my name is Fido!
泛型(包括 impl trait)使用单态化(monomorphization)为泛型实例化的每种不同类型创建函数的专用实例。这意味着在泛型函数中调用特性方法仍然使用静态分派,因为编译器拥有完整的类型信息,并且可以解析要使用哪种类型的特性实现。
而使用 dyn trait 时,它会通过虚方法表(vtable)使用动态分派。这意味着,无论传入何种类型的 Pet,都使用 fn dynamic 的单一版本。
在运行时,&dyn Pet 被表示为一个 “胖指针”,即一对指针:一个指针指向实现 Pet 的具体对象,另一个指针指向该类型的 trait 实现的虚函数表(vtable)。当在 &dyn Pet 上调用 talk 方法时,编译器会在虚函数表中查找 talk 的函数指针,然后调用该函数,并将指向 Dog 或 Cat 的指针传递给该函数。编译器在执行此操作时无需知道 Pet 的具体类型。
dyn trait 被视为 “类型擦除”,因为我们在编译时不再知道具体类型是什么。
练习
这是一个很简短的练习,你将使用 Ordering trait 实现一个通用的 min 函数,用于确定两个值中的最小值:
// TODO: 实现 `min` 函数
#[test]
fn integers() {
assert_eq!(min(0, 10), 0);
assert_eq!(min(500, 123), 123);
}
#[test]
fn chars() {
assert_eq!(min('a', 'z'), 'a');
assert_eq!(min('7', '1'), '1');
}
#[test]
fn strings() {
assert_eq!(min("hello", "goodbye"), "goodbye");
assert_eq!(min("bat", "armadillo"), "armadillo");
}