Rust 泛型与特征(trait)

112 阅读13分钟

Rust 泛型与特征(trait)

1.泛型

1. 泛型的基本概念

在 Rust 中,泛型(Generics)是一种强大的特性,允许你编写通用的代码,而不需要指定具体的类型。泛型可以用于函数、结构体、枚举和方法,从而提高代码的复用性和灵活性,泛型允许你定义函数、结构体或枚举时,不指定具体的类型,而是使用占位符类型(类型参数)。这些类型参数在使用时会被具体的类型替换。

2. 为什么需要引入泛型

如果我们有这样一个需求,写一个计算(本例用加法演示)函数,同时支持各种数值类型的计算,如果我们没有泛型,那么代码是这样的

fn main() {
    println!("add i8: {}", add_i8(2i8, 3i8));
    println!("add i32: {}", add_i32(20, 30));
    println!("add f64: {}", add_f64(1.23, 1.23));
    // ...
}

fn add_i8(a:i8, b:i8) -> i8 {
    a + b
}
fn add_i32(a:i32, b:i32) -> i32 {
    a + b
}
fn add_f64(a:f64, b:f64) -> f64 {
    a + b
}

// ...

这样虽然也能实现相应的需求,但是代码未免太复杂了,我们需要把这个函数变得更加通用,把类型交给调用它的人决定,下面这个引入泛型的代码就可以实现一个函数计算不同的数值类型,代码也变得简洁易读。

fn main() {
    println!("add i8: {}", add(2i8, 3i8));
    println!("add i32: {}", add(20, 30));
    println!("add f64: {}", add(1.23, 1.23));
    //...
}


fn add<T:std::ops::Add<Output = T>>(a:T, b:T) -> T {
    a + b
}

3. 泛型语法(常见用法)

在 Rust 中,泛型是一种非常强大的工具,用于编写可复用且类型安全的代码。泛型可以用于函数、结构体、枚举、trait 等多种场景。以下是对 Rust 中泛型的全面总结,涵盖各种常见用法:

3.1 泛型函数

泛型函数允许你定义一个函数,使其可以接受多种类型的参数,而无需为每种类型单独实现。

示例 1:简单的泛型函数

use std::fmt::{Display, Formatter};
fn main() {
    let fruit = Fruit::new("apple".to_string(), 12);
    print_value(fruit);
}

#[derive(Debug)]
struct Fruit {
    name:String,
    age:u32,
}

impl Fruit {
    fn new(name: String, age: u32) -> Self{
        Fruit {
            name,
            age
        }
    }
}

impl Display for Fruit{
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        write!(f, "Fruit {{ name: {}, age: {} }}", self.name, self.age)
    }
}

fn print_value<T:Display>(value: T) {
    println!("{}", value);
}
  • T: std::fmt::Display:约束 T 必须实现 Display trait,确保可以打印。

示例 2:带有多个泛型参数的函数

use std::cmp::Ordering;
use std::fmt::{Display, Formatter};
fn main() {
    let fruit1 = Fruit::new("apple".to_string(), 12,23);
    let fruit2 = Fruit::new("banana".to_string(), 12, 22);
    compare_and_print(fruit1, fruit2);
}
#[derive(Debug)]
struct Fruit {
    name:String,
    age:u32,
    price:u32,
}

impl Fruit {
    fn new(name: String, age: u32,price:u32) -> Self{
        Fruit {
            name,
            age,
            price
        }
    }
}

impl Display for Fruit{
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        write!(f, "Fruit {{ name: {}, age: {}, price: {} }}", self.name, self.age, self.price)
    }
}

//PartialEq是PartialOrd的前提,因此你需要实现PartialEq。比较age字段以及price字段是否相等
impl PartialEq for Fruit{
    fn eq(&self, other: &Self) -> bool {
        self.age == other.age && self.price == other.price
    }
}

//在PartialOrd的实现中,使用age字段进行比较,如果age相等的话,再使用price进行比较
impl PartialOrd for Fruit {
    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
        match self.age.partial_cmp(&other.age) {
            Some(Ordering::Equal) => self.price.partial_cmp(&other.price),
            other => other,
        }
    }
}

fn compare_and_print<T: Display + PartialOrd>(a: T, b: T) {
    if a > b {
        println!("{} is greater than {}", a, b);
    } else {
        println!("{} is less than or equal to {}", a, b);
    }
}

  • T: std::fmt::Display + PartialOrd:T 必须同时实现 Display 和 PartialOrd trait。
  • PartialEqPartialOrd 的实现需要保持一定的逻辑一致性。如果两个值在 PartialOrd 中被认为是相等的,PartialEq 中也必须被认为是相等的
  • Option<Ordering> 的变体有 Some(T) 或者 None , Ordering的枚举有三个值: Ordering::Less Ordering::Equal Ordering::Greater

示例 3:泛型函数的返回值

fn main() {
    let tuple1 = create_tuple("str", 12);
    let tuple2 = create_tuple(12, 15_i64);
    println!("tuple1 is {:?} and tuple2 is {:?}", tuple1, tuple2);
}

fn create_tuple<T, U>(a: T, b: U) -> (T, U) {
    (a, b)
}
  • 返回值是泛型类型 (T, U),表示可以返回任意类型的元组。
3.2 泛型结构体

泛型结构体允许你定义一个结构体,使其可以存储多种类型的字段。

示例 1:单个泛型字段

fn main() {
    let box1 = Box::new(12);
    let box2 = Box::new("str");
    println!("box1 is {} and box2 is {}", box1.value, box2.value);
}

struct Box<T> {
    value:T,
}

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

  • Box<T>:一个泛型结构体,可以存储任意类型 T

示例 2:多个泛型字段

use std::fmt::{write, Display, Formatter};

fn main() {
    let pair = Pair::new("123", 234);
    let pair_swap = pair.swap();
    println!("pair is {:?} and pairSwap is {:?}", pair, pair_swap);
}

#[derive(Debug,Copy, Clone)]
struct Pair<T, U> {
    first: T,
    second: U,
}

impl<T, U> Pair<T, U> {
    fn new(first:T,second:U)->Self{
        Pair{first,second}
    }

    fn swap(self) -> Pair<U,T>{
        Pair {
            first: self.second,
            second: self.first,
        }
    }
}

impl<T:Display, U:Display> Display for Pair<T, U> {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        write!(f, "Pair(first: {}, second: {})", self.first, self.second)
    }
}

  • Pair<T, U>:一个泛型结构体,可以存储两个不同类型的字段。

示例 3:带有生命周期的泛型结构体

struct RefPair<'a, T> {
    first: &'a T,
    second: &'a T,
}

impl<'a, T> RefPair<'a, T> {
    fn new(first: &'a T, second: &'a T) -> Self {
        RefPair { first, second }
    }
}
  • 'a:生命周期参数,确保引用的生命周期一致。
3.3 泛型枚举

泛型枚举允许你定义一个枚举,使其可以包含多种类型的值。

示例 1:标准库中的 Result 枚举

enum Result<T, E> {
    Ok(T),
    Err(E),
}
  • Result<T, E>:一个泛型枚举,T 是成功时的值类型,E 是错误时的值类型。

示例 2:自定义泛型枚举

enum Option<T> {
    Some(T),
    None,
}
  • Option<T>:一个泛型枚举,表示可能有值(Some(T))或没有值(None)。
3.4 泛型 trait

泛型 trait 允许你定义一个 trait,使其可以适用于多种类型。

示例 1:自定义泛型 trait

fn main() {
    let number = 42;
    let text = "Hello, world!";

    number.print(); // 打印数字
    text.print();   // 打印字符串
}

trait Print {
    fn print(&self);
}

impl<T: std::fmt::Display> Print for T {
    fn print(&self) {
        println!("{}", self);
    }
}

示例:泛型关联类型

fn main() {
    let array = [1, 2, 3, 4, 5];
    let mut iter = ArrayIterator {
        array: &array,
        current_index: 0,
    };
    while let Some(value) = iter.next() {
        println!("{}", value);
    };

}

trait Iterator{
    //在 trait 定义中,type Item; 是一个占位符,表示迭代器返回的元素类型
    //在实现 Iterator 时,必须指定 Item 的具体类型(在这个例子中是 &T)
    type Item;

    fn next(&mut self) -> Option<Self::Item>;
}

//生命周期 'a 确保迭代器引用的数组在迭代器的生命周期内有效
struct ArrayIterator<'a,T>{
    array:&'a[T],
    current_index:usize,
}

impl <'a,T> Iterator for ArrayIterator<'a,T>{
    type Item = &'a T;
    fn next(&mut self) -> Option<Self::Item> {
        if self.current_index < self.array.len() {
            let item = &self.array[self.current_index];
            self.current_index+=1;
            Some(item)
        } else {
            None
        }
    }
}

  • trait Iterator
    • 定义了一个名为 Iterator 的 trait,用于表示迭代器的抽象行为。
    • 这是 Rust 标准库中非常重要的 trait,用于处理可迭代的集合。
  • type Item;
    • 这是一个关联类型(Associated Type),用于在 trait 中定义一个类型占位符。
    • 关联类型允许你在 trait 中声明一个类型,但具体实现时再指定这个类型是什么。
    • 在这个例子中,Item 表示迭代器返回的元素类型。
  • fn next(&mut self) -> Option<Self::Item>
    • 这是迭代器的核心方法,表示从迭代器中获取下一个元素。
    • 返回值是一个 Option<Self::Item>,其中:
      • Some(value) 表示成功获取了一个值。
      • None 表示迭代器已经耗尽(没有更多元素)。

可能会有一个疑惑,就是实现trait的时候,需要用 type Item = &'a T; 来指定类型,如果我们不用关联类型要怎么实现呢?

fn main() {
    let array = [1, 2, 3, 4, 5];
    let array = ["1".to_string(), "2".to_string(), "3".to_string()];
    let mut iter = ArrayIterator {
        array: &array,
        current_index: 0,
    };
    while let Some(value) = iter.next() {
        println!("{}", value);
    };
}

trait Iterator<T>{
    fn next(&mut self) -> Option<T>;
}

struct ArrayIterator<'a,T> {
    array: &'a [T],
    current_index: usize,
}

//当你为某个类型实现这个trait时,你需要为T指定一个具体的类型
impl <'a>  Iterator<i32> for ArrayIterator<'a,i32>{
    fn next(&mut self) -> Option<i32> {
        if self.current_index<self.array.len() {
            let value = &self.array[self.current_index];
            self.current_index += 1;
            Some(*value)
        }else {
            None
        }
    }
}
//String版本
impl<'a> Iterator<String> for ArrayIterator<'a, String> {
    fn next(&mut self) -> Option<String> {
        if self.current_index < self.array.len() {
            let value = &self.array[self.current_index];
            self.current_index += 1;
            Some(value.clone())  // String 需要克隆
        } else {
            None
        }
    }
}

由此我们可以看出,如果我们不使用关联类型,我们需要为trait进行每种类型的实现,使用关联类型后,我们只需要为 ArrayIterator 实现一次 Iterator,而不需要为每种类型(如 i32String)单独实现。具体的返回类型(&'a T)在实现时通过 type Item 指定。

3.5 泛型方法

泛型方法允许你在结构体或枚举的实现块中定义泛型方法。

示例 1:结构体中的泛型方法

fn main() {
    MyStruct::process(10);
    MyStruct::process("Hello");
}

struct MyStruct;

impl MyStruct{
    fn process<T:std::fmt::Debug>(value:T){
        println!("{:?}", value);
    }
}
  • fn process<T: std::fmt::Debug>:泛型方法,接受任意实现了 Debug trait 的类型。

示例 2:枚举中的泛型方法

fn main() {
    let value = MyEnum::Value(42);
    //要加类型,让编译器推断出来
    let none: MyEnum<i32> = MyEnum::None;
    if let Some(v) = value.get() {
        println!("Got a value: {}", v);
    } else {
        println!("No value");
    }
    if let Some(v) = none.get() {
        println!("Got a value: {}", v);
    } else {
        println!("No value");
    }
}


enum MyEnum<T> {
    Value(T),
    None,
}

impl <T> MyEnum<T> {
    fn get(&self) -> Option<&T>{
        match self {
            MyEnum::Value(v) => {Some(v)}
            MyEnum::None => {None}
        }
    }

}

  • impl<T>:为泛型枚举实现方法。
3.6 泛型与生命周期

泛型和生命周期结合使用,可以处理涉及引用的复杂场景。

示例 1:带有生命周期的泛型函数

fn main() {
    let x = String::from("hello");
    let y = String::from("world");
    let result = longest(&x, &y);
    println!("The longest string is: {}", result);

    let x = 123;
    let y = 456;
    let result = longest(&x, &y);
    println!("The largest number is: {}", result);

}

fn longest<'a,T>(x:&'a T,y:&'a T) -> &'a T 
where T: PartialOrd + std::fmt::Display,
{
    if x>y {
        x
    }else {
        y
    }
}

  • <'a, T>:同时使用生命周期和泛型。
  • where:用于进一步约束类型或生命周期。

示例 2:带有生命周期的泛型结构体

同泛型结构体

3.7 泛型与默认类型

你可以为泛型参数提供默认类型,使其在某些情况下更灵活。

示例:带有默认类型的泛型结构体

fn main() {
    let container = Container::new(10);// 默认类型为 i32
    let container_f64 = Container::<f64>::new(3.14);// 显式指定类型为 f64
}

struct Container<T = i32>{
    value:T,
}

impl <T> Container<T>{
    fn new(value:T) -> Self{
        Container{ value}
    }
}
  • T = i32:为泛型参数 T 提供默认类型。
3.8 泛型与常量泛型

Rust 也支持常量泛型,常量泛型允许你在结构体或函数中使用常量值作为泛型参数,从而实现更灵活的类型定义。

示例:常量泛型结构体

fn main() {
    let array = Array::new([1, 2, 3]);// Array<i32, 3>
    println!("array is: {:?}", array.data);
}

//const N:usize : 常量泛型参数,表示数组的长度
struct Array<T,const N:usize> {
    //一个固定长度的数组,类型为T,长度为N。
    data: [T; N],
}

impl<T ,const N:usize> Array<T,N>{
    fn new(data:[T;N]) -> Self{
        Array{data}
    }
}

常量泛型的用途常量

泛型允许你在结构体或函数中使用常量值作为泛型参数,从而实现更灵活的类型定义。例如:

• 固定长度的数组:可以定义一个固定长度的数组,而不需要使用动态数组(如Vec)。

• 优化性能:固定长度的数组在某些情况下可以提供更好的性能,因为它们的大小是已知的,编译器可以进行更多的优化。

2.特征(trait)

1. trait 的定义

trait 是通过 trait 关键字定义的,它声明了一组方法签名,但不提供具体实现。这些方法的具体实现需要在具体的类型上完成。简单的来说,特征就是一组共享的行为,需要使用这些行为的结构进行特征实现即可,就像上面看到的 T: PartialOrd + std::fmt::Display 就是指泛型必须实现了部分排序trait(PartialOrd)和自定义格式化输出trait(Display),这样实现他们的泛型就共同了拥有了一组行为,接下来的逻辑也可以通用化

示例:

trait Animal {
    fn make_sound(&self) -> String;
}
  • Animal 是一个 trait,定义了一个方法 make_sound,动物会有叫声(可能有的没有),需要发声的动物则可以实现这个 trait。
  • 这个方法接受一个不可变引用 &self,并返回一个 String

2. 为类型实现 trait

你可以为任何类型实现一个 trait,只要这个类型满足 trait 的约束。这包括自定义类型、标准库类型或其他库中的类型。

示例:


trait Animal{
    fn make_sound(&self) -> String;
}

struct Dog {
    name: String,
}

impl Animal for Dog{
    fn make_sound(&self) -> String {
        format!("{} says: Woof!", self.name)
    }
}

struct Cat{
    name:String,
}
impl Animal for Cat{
    fn make_sound(&self) -> String {
        format!("{} says: Meow!", self.name)
    }
}

  • DogCat 都实现了 Animal trait。
  • 每个类型提供了 make_sound 方法的具体实现。

3. 使用 trait

trait 的主要作用是提供一种通用的接口,使得不同的类型可以共享相同的行为。你可以通过 trait 来编写通用的代码,而不需要关心具体的类型。

示例:

fn print_sound(animal: &impl Animal) {
    println!("{}", animal.make_sound());
}

fn main() {
    let dog = Dog { name: "Buddy".to_string() };
    let cat = Cat { name: "Whiskers".to_string() };

    print_sound(&dog);  // 输出: Buddy says: Woof!
    print_sound(&cat);  // 输出: Whiskers says: Meow!
}
  • print_sound 函数接受一个实现了 Animal trait 的引用。
  • 这种方式允许你编写通用的代码,而不需要为每种类型单独实现。
  • &impl Animal 表示实现了Animal trait 的引用。只要实现了Animal trait的类型都可以作为参数

print_sound方法 也可以用泛型的形式实现,效果是一样的,但是&impl方式显得更加简洁

fn print_sound<T:Animal>(animal: &T){
    println!("The animal says: {}", animal.make_sound());
}

4. trait 的高级特性

4.1 默认方法实现

trait 可以为方法提供默认实现。如果某个类型实现了该 trait,但没有为某个方法提供具体实现,那么默认实现将被使用。

fn main() {
    let rock = Rock {};
    print_sound(&rock);
}

trait Animal{
    fn make_sound(&self) -> String{
        format!("This animal makes no sound.")
    }
}
struct Rock;

impl Animal for Rock{}

fn print_sound<T:Animal>(animal: &T){
    println!("The animal says: {}", animal.make_sound());
}

  • Rock 类型没有为 make_sound 提供实现,因此会使用默认实现。
4.2 关联类型

trait 可以定义关联类型,允许你在 trait 中声明类型占位符,并在具体的实现中指定这些类型。

示例:

trait Iterator {
    type Item;

    fn next(&mut self) -> Option<Self::Item>;
}
  • type Item 是一个关联类型,表示迭代器返回的元素类型。
  • 具体实现时,你需要指定 Item 的类型:

见上面 泛型 trait 中的 示例:泛型关联类型

4.3 泛型与 trait Bound

你可以使用 trait bound 来约束泛型参数,确保泛型参数实现了某个 trait。

示例:

fn print_sound<T: Animal>(animal: &T) {
    println!("{}", animal.make_sound());
}
  • T: Animal 表示泛型参数 T 必须实现了 Animal trait。
4.4 trait 对象

trait 对象允许你将不同类型的值存储在同一个变量中,只要这些类型实现了相同的 trait。

示例:

fn main() {
    let dog = Dog { name: "Buddy".to_string() };
    print_sound(&dog);  // 输出: Buddy says: Woof!
}

trait Animal{
    fn make_sound(&self) -> String{
        format!("This animal makes no sound.")
    }
}
struct Dog {
    name: String,
}

impl Animal for Dog{
    fn make_sound(&self) -> String {
        format!("{} says: Woof!", self.name)
    }
}
fn print_sound(animal: &dyn Animal){
    println!("{}", animal.make_sound());
}
  • &dyn Animal 是一个 trait 对象,表示它可以接受任何实现了 Animal trait 的引用。

  • dyn 关键字用于表示动态分发(Dynamic Dispatch)。它允许你使用 trait 的方法,而不需要知道具体的实现类型。动态分发在运行时解析方法调用,而不是在编译时。

4.5 条件实现

你可以为某个类型有条件地实现 trait,这通常用于特性检测或依赖注入。

示例:

fn main() {
    let dog = Dog { name: "Buddy".to_string() };
    println!("{}", dog.make_sound());
}

struct Dog{
    name:String
}

trait Animal{
    fn make_sound(&self) -> String;
}

impl Animal for Dog{
    #[cfg(not(feature = "logging"))]
    fn make_sound(&self) -> String {
        format!("{} says:woof!",self.name)
    }

    #[cfg(feature = "logging")]
    fn make_sound(&self) -> String {
        println!("Logging: Dog says Woof!");
        format!("{} says: Woof",self.name)
    }
}

//使用 cargo run 会打印
//Buddy says:woof!
//使用 cargo run --features "logging" 会打印
//Logging: Dog says Woof!
//Buddy says: Woof

在 Rust 中,特性(Feature)是通过 Cargo 的特性管理功能实现的,主要用于控制代码的条件编译。要自己实现一个特性,你需要在项目的 Cargo.toml 文件中定义特性,并在代码中使用 #[cfg(feature = "your_feature")] 来控制代码的编译。

[package]
name = "untitled"
version = "0.1.0"
edition = "2021"

[features]
# 定义一个名为"logging"的特性
logging = []

# 定义一个名为"network"的特性
network = []

[dependencies]