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。PartialEq
和PartialOrd
的实现需要保持一定的逻辑一致性。如果两个值在 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
,而不需要为每种类型(如 i32
、String
)单独实现。具体的返回类型(&'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)
}
}
Dog
和Cat
都实现了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]