Rust语言从入门到精通系列 - Rust泛型
泛型是一种非常强大的特性,它是现代编程语言中必不可少的特性之一。它允许我们最大限度的编写通用和可重用的代码。在本文中,我们将深入探讨Rust中Struct和泛型,并学习如何使用它们来创建更加灵活和可扩展的代码。
Rust中的泛型
泛型是一种在编程语言中广泛使用的概念,它允许我们编写可以适用于多种不同类型的代码。在Rust中,我们可以使用泛型来编写更加通用和可重用的代码。 在Rust中,我们使用尖括号<>来指定泛型类型。下面是一个使用泛型的简单示例:
1
2
3
4
5
6
7
8
fn print<T>(x: T) {
println!("{}", x);
}
fn main() {
print(1);
print("hello");
}
在上面的示例中,我们定义了一个名为print的函数,并使用泛型类型T来表示函数参数的类型。在函数体中,我们使用println!宏来打印出函数参数的值。在main函数中,我们分别调用了print函数,并传入了一个整数和一个字符串作为参数。由于print函数使用了泛型类型,因此它可以适用于任何类型的参数。 在Rust中,我们还可以使用泛型来定义struct、enum和trait。 下面回顾一下,我们上一篇中提到的泛型Pair结构体:
1
2
3
4
5
6
7
8
9
struct Pair<T> {
first: T,
second: T,
}
fn main() {
let pair_of_ints = Pair { first: 1, second: 2 };
let pair_of_strings = Pair { first: "hello".to_string(), second: "world".to_string() };
}
在上面的示例中,我们定义了一个名为Pair的struct,并使用泛型类型T来表示数据项的类型。在main函数中,我们分别创建了一个存储两个整数的Pair对象和一个存储两个字符串的Pair对象。
泛型约束
在Rust中,我们可以使用泛型约束来限制泛型类型的范围。泛型约束允许我们指定泛型类型必须满足的条件,例如实现了某个trait或者是某个特定类型。 下面是一个使用泛型约束的示例:
1
2
3
4
5
6
7
8
9
10
use std::fmt::Display;
fn print<T: Display>(x: T) {
println!("{}", x);
}
fn main() {
print(1);
print("hello");
}
在上面的示例中,我们使用use语句导入了std::fmt::Display trait,然后在print函数中使用了泛型约束<T: Display>来表示函数参数必须实现Display trait。在函数体中,我们使用println!宏来打印出函数参数的值。在main函数中,我们分别调用了print函数,并传入了一个整数和一个字符串作为参数。由于整数和字符串都实现了Display trait,因此它们都可以作为参数传递给print函数。 在Rust中,我们还可以使用where关键字来指定泛型约束。 下面是一个使用where关键字的示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
use std::fmt::Display;
fn print<T>(x: T)
where
T: Display,
{
println!("{}", x);
}
fn main() {
print(1);
print("hello");
}
在上面的示例中,我们使用where关键字来指定泛型约束,与使用<T: Display>的方式相同。使用where关键字的方式通常更加清晰和易读,特别是当我们需要指定多个泛型约束时。
常见的泛型结构体
在Rust中,我们可以使用泛型来编写更加通用和可重用的代码。下面是一些常见的泛型应用:
Vec
1
2
3
4
5
6
7
8
9
let mut v: Vec<i32> = Vec::new();
v.push(1);
v.push(2);
v.push(3);
for i in &v {
println!("{}", i);
}
在上面的示例中,我们首先创建了一个空的Vec
Option
1
2
3
4
5
6
7
8
9
10
11
let x: Option<i32> = Some(5);
let y: Option<i32> = None;
match x {
Some(i) => println!("x is {}", i),
None => println!("x is None"),
}
match y {
Some(i) => println!("y is {}", i),
None => println!("y is None"),
}
在上面的示例中,我们首先创建了一个存储整数5
的Some
对象,并将它赋值给变量x
。然后,我们创建了一个None
对象,并将它赋值给变量y
。最后,我们使用match
表达式来匹配x
和y
的值,并打印出相应的信息。
Result<T, E>
是Rust标准库中的一个枚举类型,它可以表示一个操作成功或失败的情况,并可以携带一个成功的结果或一个错误信息。下面是一个使用Result<T, E>
的示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
use std::fs::File;
use std::io::ErrorKind;
fn main() {
let f = File::open("hello.txt");
let f = match f {
Ok(file) => file,
Err(error) => match error.kind() {
ErrorKind::NotFound => match File::create("hello.txt") {
Ok(fc) => fc,
Err(e) => panic!("Failed to create file: {:?}", e),
},
other_error => panic!("Failed to open file: {:?}", other_error),
},
};
}
在上面的示例中,我们使用File::open函数来打开一个名为hello.txt的文件。如果文件存在,则返回一个Ok对象,其中包含文件的句柄;否则,返回一个Err对象,其中包含一个io::Error对象,表示文件不存在的错误。在match表达式中,我们使用ErrorKind::NotFound模式匹配来检查错误类型,并使用File::create函数来创建一个新文件。如果创建成功,则返回一个新的文件句柄;否则,使用panic!宏来抛出一个错误。如果文件存在或创建成功,则返回一个文件句柄。
const 泛型
Const泛型,也称“常量泛型”,参数允许程序项在常量值上泛型化。const标识符为常量参数引入了一个名称,并且该程序项的所有实例必须用给定类型的值去实例化该参数。
常量参数类型值允许为:u8, u16, u32, u64, u128, usize, i8, i16, i32, i64, i128, isize, char 和 bool 这些类型。
Animal泛型示例
现在,结合上一篇定义的Animal类。我们扩展它并将Animal扩展成一个通用抽象的结构体,并让这个类可以适用于任何类型的动物,它可以存储动物的名称、年龄、性别、物种和体重等信息。
首先,我们定义一个泛型类型T,用于表示动物的类型。然后,我们定义一个名为Animal的struct,并使用泛型类型T来表示动物的类型。在Animal struct中,我们定义了五个数据项,分别是名称、年龄、性别、物种和体重。我们还为Animal struct实现了一个new关联函数,用于创建一个新的Animal对象。在new函数中,我们传入五个参数,分别是名称、年龄、性别、物种和体重,并使用它们来创建一个新的Animal对象。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
struct Animal<T> {
name: String,
age: u8,
is_male: bool,
species: String,
weight: f32,
animal_type: std::marker::PhantomData<T>,
}
impl<T> Animal<T> {
fn new(name: String, age: u8, is_male: bool, species: String, weight: f32) -> Animal<T> {
Animal {
name,
age,
is_male,
species,
weight,
animal_type: std::marker::PhantomData,
}
}
}
在上面的示例中,我们还使用了一个名为PhantomData的标记类型,用于表示泛型类型T的存在。由于Rust中的泛型是在编译时实现的,因此编译器需要知道泛型类型的存在,才能正确地生成代码。使用PhantomData标记类型可以告诉编译器,泛型类型T确实存在,从而使编译器能够正确地生成代码。 现在,我们可以使用Animal类来创建不同类型的动物对象。下面是一个使用Animal类创建猫和狗对象的示例:
1
2
3
4
5
6
7
struct Cat;
struct Dog;
fn main() {
let cat = Animal::<Cat>::new("Tommy".to_string(), 3, true, "cat".to_string(), 4.5);
let dog = Animal::<Dog>::new("Buddy".to_string(), 5, true, "dog".to_string(), 12.3);
}
在上面的示例中,我们首先定义了Cat和Dog两个类型,用于表示猫和狗。然后,我们使用Animal::
泛型约束和trait结合
泛型约束通常都是和trait一起使用的,用来限制泛型类型的范围。例如,我们可以使用std::fmt::Display trait来限制泛型类型必须实现Display trait。下面是一个使用泛型约束和trait的示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
use std::fmt::Display;
struct Animal<T: Display> {
name: String,
age: u8,
is_male: bool,
species: String,
weight: f32,
animal_type: std::marker::PhantomData<T>,
}
impl<T: Display> Animal<T> {
fn new(name: String, age: u8, is_male: bool, species: String, weight: f32) -> Animal<T> {
Animal {
name,
age,
is_male,
species,
weight,
animal_type: std::marker::PhantomData,
}
}
fn print_info(&self) {
println!("Name: {}", self.name);
println!("Age: {}", self.age);
println!("Is male: {}", self.is_male);
println!("Species: {}", self.species);
println!("Weight: {}", self.weight);
}
}
struct Cat;
struct Dog;
impl Display for Cat {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "cat")
}
}
impl Display for Dog {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "dog")
}
}
fn main() {
let cat = Animal::<Cat>::new("Tommy".to_string(), 3, true, Cat.to_string(), 4.5);
let dog = Animal::<Dog>::new("Buddy".to_string(), 5, true, Dog.to_string(), 12.3);
cat.print_info();
dog.print_info();
}
在上面的示例中,我们使用<T: Display>泛型约束来限制泛型类型必须实现Display trait。然后,我们为Animal类实现了一个print_info方法,用于打印出动物的信息。在main函数中,我们首先定义了Cat和Dog两个类型,并为它们实现了Display trait。然后,我们使用Animal::
接下来,我们可以使用泛型特性来重新定义Zoo类和print_animals函数。首先,我们可以使用一个泛型类型T来表示动物的类型,如下所示:
1
2
3
struct Zoo<T> {
animals: Vec<Animal<T>>,
}
这个Zoo类使用了一个泛型类型T,它表示动物的类型。这个类可以用来容纳不同种类的动物,例如:
1
2
let zoo1 = Zoo { animals: vec![cat, dog, elephant] };
let zoo2 = Zoo { animals: vec![elephant, dog, cat] };
接下来,我们定义支持使用泛型特性的print_animals函数,如下所示:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
fn print_animals<T>(zoo: &Zoo<T>) {
for animal in &zoo.animals {
println!("{} ({}) is {} years old.", animal.name, animal.animal_type, animal.age);
}
}
fn main() {
let cat = Animal { name: "Tommy".to_string(), age: 3, animal_type: "Cat" };
let dog = Animal { name: "Buddy".to_string(), age: 5, animal_type: "Dog" };
let elephant = Animal { name: "Dumbo".to_string(), age: 10, animal_type: "Elephant" };
let zoo1 = Zoo { animals: vec![cat, dog, elephant] };
let zoo2 = Zoo { animals: vec![elephant, dog, cat] };
print_animals(zoo1);
print_animals(zoo2);
}
这个函数使用了一个泛型类型T,它表示动物的类型。这个函数可以遍历Zoo中的所有动物,并打印它们的名字、类型和年龄。
现在我们可以使用Animal和Zoo类的泛型特性来实现更加灵活和可复用的代码。例如,我们可以使用Animal
通过这两个泛型示例,相信大家都已经掌握了泛型的使用。
Rust泛型最佳实践
Rust 中泛型是零成本的抽象,意味着你在使用泛型时,完全不用担心性能上的问题。 因为是在编译阶段为泛型对应的多个类型,生成各自的代码,所以增加了编译耗时,增大了最终生成二进制文件的大小。
当我们需要在内存资源比较紧张的机器上允许rust时,可以尝试使用const泛型进行优化。
结论
在本文中,我们深入探讨了Rust中的泛型和struct,并学习了如何使用它们来创建更加灵活和可扩展的代码。我们还讨论了泛型约束和trait,以及如何使用它们来限制泛型类型的范围。最后,我们使用Animal类作为示例,演示了如何使用泛型和trait来创建通用的类,并限制类的类型参数必须满足某些条件。
本作品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。欢迎转载、使用、重新发布,但务必保留文章署名 TinyZ Zzh (包含链接: https://tinyzzh.github.io ),不得用于商业目的,基于本文修改后的作品务必以相同的许可发布。 如有任何疑问,请 与我联系 (tinyzzh815@gmail.com) 。
评论