大家好,
欢迎来到学习 Rust 的第16天。今天,我们来看看 Rust 中的泛型类型,泛型类型帮助我们以类型安全的方式减少代码重复。
让我们快速了解一下。
引言
Rust 中的泛型类型允许你编写可以操作多种数据类型的函数、结构体和枚举,而不牺牲类型安全,从而实现代码复用和灵活性。来看一个用例。
fn find_max(x: i32, y: i32) -> i32{
if x > y {
x
}else{
y
}
}
fn main(){
let result: i32 = find_max(9,10);
println!("最大值是:{}", result);
}
输出:
最大值是:10
如果我想使用相同的函数处理字符或浮点数,我必须为特定的类型创建一个新函数,逻辑基本相同。泛型类型有助于减少这种代码重复。
我们可以为函数/结构体声明多个泛型类型。单个泛型类型通常用 T
表示,代表 Type
。
函数中的泛型类型用 <>
声明。
示例:
fn find_max<T: PartialOrd>(x: T, y: T) -> T {
if x > y {
x
} else {
y
}
}
fn main() {
let result = find_max(10, 5);
println!("最大值是:{}", result);
let result_float = find_max(4.5, 3.2);
println!("最大值是:{}", result_float);
let result_char = find_max('x','a');
println!("最大值是:{}", result_char);
}
输出:
最大值是:10
最大值是:4.5
最大值是:x
解释:
find_max
函数用泛型类型参数T
定义。<T: PartialOrd>
语法表明T
必须实现PartialOrd
特性,这允许对T
类型的值进行比较。PartialOrd
是 Rust 中定义值之间部分排序关系(小于、小于等于、大于、大于等于)的特性。- 实现
PartialOrd
的类型可以使用比较运算符(<
、<=
、>
、>=
)进行比较,并可用于依赖排序的函数,比如排序算法或在此例中,寻找最大值。 - 这使得该函数能够处理各种类型,只要它们支持使用
PartialOrd
特性的比较。 - 通过使用泛型,函数在保持类型安全的同时获得了灵活性和可重用性。
- 该函数返回一个
T
类型的值,确保最大值与输入值类型相同。
结构体中的泛型类型
假设我想存储一个点的坐标。这是我通常使用的结构体
struct Point{
x: i32,
y: i32,
}
fn main(){
let point = Point{x: 3, y: 10};
//这行代码不会工作,因为我们的结构体只能存储有符号的32位整数
let point2 = Point{x:3.4, y: 6.9};
}
让我们解决这个问题:
struct Point<T>{
x: T,
y: T,
}
fn main(){
let point = Point{x: 3, y: 10};
let point2 = Point{x:3.4, y: 6.9};
//这行代码不会工作,因为我们的结构体不能存储两种不同的数据类型
let point3 = Point{x:3.4, y: 6};
}
为了解决这个问题,我们可以在结构体中添加另一个泛型类型。
struct Point<T, U>{
x: T,
y: U,
}
fn main(){
let point = Point{x: 3, y: 10};
let point2 = Point{x:3.4, y: 6.9};
let point3 = Point{x:3.4, y: 6};
}
现在这一切都能工作了,这就是我们如何在结构体中使用泛型类型…
枚举中的泛型类型
我们可以对枚举做同样的事情,我将给你展示两个最流行的带有泛型类型的枚举示例:
enum Option<T>{
Some(T),
None,
}
enum Result<T,E>{
Ok(T),
Err(E),
}
方法中的泛型类型
如果你还记得,我们可以使用 impl
为结构体声明实现块,以声明结构体的方法。
回到我们的 Point 结构体,让我们使用泛型类型为结构体声明一个实现块。
struct Point<T>{
x: T,
y: T,
}
impl<U> Point<U>{
fn x(&self) -> &U {
&self.x
}
}
fn main(){
let point = Point{x: 3, y: 10};
let point2 = Point{x:3.4, y: 6.9};
}
X 方法返回点的 X 坐标,取自 self
的引用。
注意,声明结构体时使用了 T
作为泛型类型,在 impl
块中我使用了 U
,这表明这两个泛型类型并不相连。
如果我声明另一个具体类型的 impl
块会发生什么?
struct Point<T>{
x: T,
y: T,
}
impl<U> Point<U>{
fn x(&self) -> &U {
&self.x
}
}
impl Point<i32>{
fn y(&self) -> i32{
self.y
}
}
fn main(){
let point1 = Point{x: 3, y: 10};
let point2 = Point{x:3.4, y: 6.9};
println!("X: {}, Y: {}",point1.x(),point1.y());
println!("X: {}, Y: {}",point2.x(),point2.y);
}
y
方法只对实例可用,这些实例的 x
和 y
值为有符号的 32 位整数,而 x
方法可以用于任何类型的实例。
混合使用泛型
让我们看一个更复杂的例子,我们的 Point 结构体中有两个泛型,并理解混合它们的需要…
struct Point<T, U>{
x: T,
y: U,
}
impl<T,U> Point<T,U>{
fn mixup<V, W>(self,other: Point<V, W>) -> Point<V, W>{
Point{
x: self.x,
y: other.y,
}
}
}
fn main(){
let point = Point{x: 3, y: 10};
let point2 = Point{x:3.4, y: 6.9};
let point3 = Point{x:3.4, y: 6};
let point4 = point1.mixup(point2);
println!("X: {}, Y: {}",point4.x, point4.y);
}
为什么要混合使用泛型?
V
和 W
像是类型的占位符。我们在 mixup
方法中使用它们,因为它允许我们混合匹配两个 Point
实例的 x
和 y
值的不同类型。这种灵活性使我们的代码更加多样化,因为它允许我们处理不同的类型而无需改变结构体本身。所以,使用 V
和 W
而不是 T
和 U
,在方法中保持事物清晰且灵活。
结论
总之,泛型类型是减少代码重复的绝佳方式,因此它们对性能有巨大的影响,通过在 Rust 中利用泛型类型,开发者可以编写更多样化和可重用的代码,同时保持类型安全和性能。这种方法不仅减少了冗余,还增强了代码的可维护性和可扩展性,使 Rust 成为广泛应用程序的强大语言选择。此外,拥抱泛型促进了更清晰、更灵活的设计,使开发者能够轻松适应不断变化的需求。