喜欢的话别忘了点赞、收藏加关注哦,对接下来的教程有兴趣的可以关注专栏。谢谢喵!(=・ω・=)
题外话:泛型的概念非常非常非常重要!!!整个第10章全都是Rust的重难点!!!
10.2.1. 什么是泛型
泛型的主要功能是提高代码的复用能力,适用于处理重复代码的问题,也可以说是实现数据与算法的分离。
泛型是具体类型或其它属性的抽象代替。 它的意思是你写代码时写的泛型代码并不是最终的代码,而是一种模版,里面有一些“占位符”。
编译器在编译时会把“占位符”替换为具体的类型。 还是看个例子:
fn largest<T>(list:&[T]) -> T {
//......
}
这个函数的定义就使用了泛型类型参数,这个T
就是所谓的“占位符”,写代码时这个T
可以是任意的数据类型,但是在编译时编译器会根据具体的使用把T
替换为具体的类型,这个过程叫单态化。
这个T
叫做泛型的类型参数。其实可以使用任意合法的标识符来作为它的类型参数的名,但是按惯例通常是使用大写的T
(代表Type)。其实在选择泛型类型参数名的时候,它的名称是很短的,通常一个字母就够了。如果你实在要写长一点,使用驼峰命名规范即可。
10.2.2. 函数定义中的泛型
当使用泛型来定义一个函数的时候,需要将泛型的类型参数放在函数的签名里。而泛型的类型参数通常是用于指定参数和返回的类型。
以上一篇文章的代码为例,使用泛型稍作修改:
fn largest<T>(list: &[T]) -> T{ let mut largest = list[0]; for &item in list{ if item > largest{ largest = item; } } largest
}
整个函数的定义可以这么理解:函数largest
拥有泛型的类型参数T
,它接收切片作为参数,切片内的元素为T
,而这个元素返回值的类型也是T
。
尝试编译一下,输出:
error[E0369]: binary operation `>` cannot be applied to type `T`--> src/main.rs:4:17|
4 | if item > largest{| ---- ^ ------- T| || T|
help: consider restricting type parameter `T`|
1 | fn largest<T: std::cmp::PartialOrd>(list: &[T]) -> T{| ++++++++++++++++++++++
这里先不讲原因和修改的方法,只需要知道使用泛型参数大概是这么个写法就对了。后面的文章会讲如何指定特定的trait。
10.2.3. struct定义中的泛型
结构体里定义的泛型类型参数主要是用在它的字段里,看个例子:
struct Point<T> { x: T, y: T,
} fn main() { let integer = Point { x: 5, y: 10 }; let float = Point { x: 1.0, y: 4.0 };
}
在结构体的名字后面呢加上<>
,在里面写泛型参数的名称,而这个泛型类型就可以应用于这个结构体下的每个字段。
在main
函数里实现了这个结构体的实例化,integer
里的两个字段是两个i32
,float
里的两个字段是两个f64
,因为结构体在声明时x
和y
的类型都是T
,所以实例化的x
和y
的类型也得是一个类型,两者的类型得保持一致。
那如果我想要x
和y
是两种不同的类型呢?很简单,声明两个泛型类型就可以:
struct Point<T, U> { x: T, y: U,
} fn main() { let integer = Point { x: 5, y: 1.0 }; let float = Point { x: 1.0, y: 40 };
}
这个时候实例化的x
和y
就可以是不同的类型,当然也可以是一样的类型。
需要注意的是,虽然可以使用多个泛型类型参数,但是,太多的泛型会使得可读性下降,通常这意味着代码需要重组为更多的小单元。
10.2.4. enum定义中的泛型
和结构体差不多,枚举中使用泛型类型参数主要是用在变体中华,可以让枚举的变体持有泛型数据类型,比如说最常见的Option<T>
和Result<T, E>
。
看个例子:
enum Option<T> { Some(T), None,
} enum Result<T, E> { Ok(T), Err(E),
}
Option
枚举中Some(T)
也就是Some
这个变体持有T
类型的值,而None
这个变体表示不持有任何值。而正是因为Option
枚举使用了泛型,所以无论这个可能存在的值是什么类型的,都可以使用Option<T>
来表示- 同样的,枚举的类型参数也可以使用多个泛型类型参数,比如说
Result
这个枚举就使用了T
和E
,在变体Ok
里存储的是T
,Err
存储的是E
10.2.5. 在方法定义中的泛型
方法可以附在枚举或是结构体上,既然枚举和结构体都可以使用泛型参数,那方法自然也可以,如下例:
struct Point<T> { x: T, y: T,
} impl<T> Point<T> { fn x(&self) -> &T { &self.x }
}
方法x
相当于一个getter,而针对Poinnt<T>
这个结构来实现方法的时候需要在impl
关键字的后面加上<T>
。这样写就表示它是针对泛型T
而不是针对某个具体的类型来实现的。
当然,如果是根据具体的类型来实现方法就不需要了:
impl Point<i32> { fn x1(&self) -> &i32 { &self.x }
}
而x1
这个方法就只有在Point<i32>
这个具体的类型上才有,而其他Point<T>
的类型就没有这个方法,类比C++的特化和偏特化。
还有一点需要注意,结构体里的泛型类型参数可以和方法的泛型类型参数不同。看个例子:
struct Point<T, U> { x: T, y: U,
} impl<T, U> Point<T, U> { fn mixup<V, W>(self, other: Point<V, W>) -> Point<T, W> { Point { x: self.x, y: other.y, } }
} fn main() { let p1 = Point { x: 5, y: 10.4 }; let p2 = Point { x: "Hello", y: 'c' }; let p3 = p1.mixup(p2); println!("p3.x = {}, p3.y = {}", p3.x, p3.y);
}
针对Point<T, U>
实现了方法mixup
,mixup
有两个泛型类型参数,一个叫V
,一个叫W
,方法的两个类型参数和Point
的两个类型参数是不一样的,当然具类型也有可能是一样的。mixup
的第二个参数是other
,它的类型也是Point
,但这个Point
不一定和self
所指向的Point
的数据类型是一样的,所以需要另起2个新的泛型类型参数。再看看返回类型,是Point<T, W>
,T
来自Point<T, U>
,W
来自Point<V, W>
。
看下main
函数,首先声明了p1
,它的两个字段都是i32
;然后又声明了p2
,它的两个字段分别是&str
(字符串切片)和char
(用''
代表是单个字符)。接着使用了mixup
这个函数,p1
对应的是Point<T, U>
,p2
对应的是Point<V, W>
,又根据各自的字段的类型可以推断出T
是i32
,U
是i32
,V
是String
,W
是char
。mixup
返回类型是Point<T, W>
,具体到这个例子中就是Point<i32, char>
。
输出:
p3.x = 5, p3.y = c
10.2.6. 泛型代码的性能
使用泛型的代码和使用具体类型的代码的运行速度是一样的。Rust在编译时会执行单态化,将泛型类型替换为具体的类型,这样在执行的时候就省去了类型替换的过程。
举个例子:
fn main() {let integer = Some(5);let float = Some(5.0)
}
这里integer
是Option<i32>
,float
是Option<f64>
,在编译的时候编译器会把Option<T>
展开为Option_i32
和Option_f64
:
enum Option_i32 {Some(i32),None,
}enum Option_f64 {Some(f64),None,
}
也就是把Option<T>
这个泛型定义替换为了两个具体类型的定义。
单态后的main
函数也变成了这样:
enum Option_i32 {Some(i32),None,
}enum Option_f64 {Some(f64),None,
}fn main(){let integer = Option_i32::Some(5);let float = Option_f64::Some(5.0);
}