泛型,英文是generic。
泛型是一种参数化多态。就是把类型作为参数,使用时才指定具体类型。
这样一套代码可以应用于多种类型。比如Vec<T>
,可以是整型向量Vec<i32>
,也可以是浮点型向量Vec<f64>
。
Rust中的泛型属于静多态,它是一种编译期多态。
在编译期,会根据指定的具体类型,生成一套特化代码,这叫单态化。比如将一个泛型函数生成具体类型对应的函数。
单态化是编译器进行静态分发的一种策略。
单态化静态分发的好处就是性能好,没有运行时开销;缺点就是造成编译后生成的二进制文件膨胀。
一、定义泛型
语法格式
<T>
T就是泛型参数。
(一)在函数中使用泛型
使用了泛型的函数就叫泛型函数
1.语法格式
泛型函数的定义语法如下
fn function_name<T[:trait_name]>(param1:T, [other_params]) {
// 函数实现代码
}
实例
fn max<T>(array: &[T]) -> T {
let mut max_index = 0;
let mut i = 1;
while i < array.len() {
if array[i] > array[max_index] {
max_index = i;
}
i += 1;
}
array[max_index]
}
(二)在结构体中使用泛型
使用了泛型的结构体叫做泛型结构体
1.语法格式
泛型结构体的定义语法如下
struct struct_name<T> {
field:T
}
例子
struct Point<T> {
x: T,
y: T
}
let p1 = Point {x: 1, y: 2};
let p2 = Point {x: 1.0, y: 2.0};
使用时并没有声明类型,这里使用的是自动推断机制,但不允许出现类型不匹配的情况如下:
let p = Point {x: 1, y: 2.0};
2.在结构体的实现块中使用泛型
语法格式
impl<T> Point<T> {
}
注意,impl关键字的后方必须有 <T>
,因为Point<T>
要以它为实参。
实例
struct Point<T> {
x: T,
y: T,
}
impl<T> Point<T> {
fn x(&self) -> &T {
&self.x
}
}
fn main() {
let p = Point { x: 1, y: 2 };
println!("p.x = {}", p.x());
}
运行结果:
p.x = 1
也可以直接特化
impl Point<f64> {
fn x(&self) -> f64 {
self.x
}
}
在成员方法中使用泛型
impl块本身的泛型不影响成员方法的泛型:
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,
}
}
}
方法mixup将一个Point<T, U>点的x与Point<V, W>点的y融合成一个类型为Point<T, W>的新点。
(三)在枚举中使用泛型
使用了泛型的枚举叫泛型枚举
诸如Option和Result
enum Option<T> {
Some(T),
None
}
enum Result<T, E> {
Ok(T),
Err(E)
}
(四)在trait中使用泛型
struct AS {}
trait AT<T> {
fn foo(&self, para: T);
}
impl AT<String> for AS{
fn foo(&self, para: String){}
}
(五)在别名中使用泛型
比如
type IoResult<T>=Result<T, IoError>;
二、泛型参数约束
1.trait约束
限制参数为必须实现了某个特性
范例
传递的参数必须是实现了Display特性的类型。
use std::fmt::Display;
fn main(){
print_pro(10 as u8);
print_pro(20 as u16);
print_pro("Hello TutorialsPoint");
}
fn print_pro<T:Display>(para:T){
println!("Inside print_pro generic function:");
println!("{}",para);
}
编译运行结果如下
Inside print_pro generic function:
10
Inside print_pro generic function:
20
Inside print_pro generic function:
Hello TutorialsPoint
2.多重约束
使用+
比如
fn notify<T: Summary + Display>(item: T){}
实例
struct StructA<T> {}
impl<T: TraitB + TraitC> StructA<T> {
fn d(&self) {}
}
StructA<T>
类型必须在T已经实现B和C特性的前提下才能实现此impl块。
3.使用where关键字简写约束
约束也可以使用where分句来表达,它放在 { 的前面。
例如:
fn some_function<T: Display + Clone, U: Clone + Debug>(t: T, u: U){
}
可以简化成:
fn some_function<T, U>(t: &T, u: &U) -> i32
where
T: Display + Clone,
U: Clone + Debug,{
}
下面的impl如果不用where从句,就无法直接表达。
use std::fmt::Debug;
trait PrintInOption {
fn print_in_option(self);
}
// 这里需要一个 `where` 从句,否则就要表达成 `T: Debug`(这样意思就变了),
// 或者改用另一种间接的方法。
impl<T> PrintInOption for T
where
Option<T>: Debug {
// 我们要将 `Option<T>: Debug` 作为约束,因为那是要打印的内容。
// 否则我们会给出错误的约束。
fn print_in_option(self) {
println!("{:?}", Some(self));
}
}
fn main() {
let vec = vec![1, 2, 3];
vec.print_in_option();
}
三、泛型参数默认值
1.当使用泛型时,可以为泛型参数指定一个默认的具体类型。
语法
<PlaceholderType=ConcreteType>
这种情况的一个非常好的例子是运算符重载。运算符重载是指自定义运算符(比如 +)的行为。
Rust并不允许创建自定义运算符或重载任意运算符,不过std::ops中所列出的运算符和相应的trait可以通过实现运算符相关trait来重载。
例如,下例展示了如何在Point结构体上实现Add trait来重载 + 运算符,这样就可以将两个Point实例相加了
use std::ops::Add;
#[derive(Debug, PartialEq)]
struct Point {
x: i32,
y: i32,
}
impl Add for Point {
type Output = Point;
fn add(self, other: Point) -> Point {
Point {
x: self.x + other.x,
y: self.y + other.y,
}
}
}
fn main() {
assert_eq!(Point { x: 1, y: 0 } + Point { x: 2, y: 3 },
Point { x: 3, y: 3 });
}
add方法将两个Point实例的x值和y值分别相加来创建一个新的Point。
Add定义如下
trait Add<RHS=Self> {
type Output;
fn add(self, rhs: RHS) -> Self::Output;
}
RHS=Self,Self就是泛型参数默认值,它表示实现Add的类型,在这里就是Point类型。如果实现Add时不指定RHS的具体类型,RHS的类型将是Self类型。
2.不使用默认类型的例子。
我们希望能够将毫米值与米值相加,并让Add的实现正确处理转换。可以为Millimeters实现Add并以Meters作为RHS
use std::ops::Add;
struct Millimeters(u32);
struct Meters(u32);
impl Add<Meters> for Millimeters {
type Output = Millimeters;
fn add(self, other: Meters) -> Millimeters {
Millimeters(self.0 + (other.0 * 1000))
}
}
为了使Millimeters和Meters能够相加,我们使用Add<Meters>
而不是使用默认的Add。
四、泛型代码的性能
Rust通过在编译时单态化来保证效率。单态化是一个通过填充编译时使用的具体类型,将通用代码转换为特定代码的过程。
编译器寻找所有泛型代码被调用的位置并针对具体类型生成特化代码。
例子
let integer = Some(5);
let float = Some(5.0);
当Rust编译这些代码的时候,它会单态化。编译器会读取传递给Option<T>
的值并发现有两种Option<T>
:一个对应i32,另一个对应f64。为此,它会将泛型定义Option<T>
展开为两个针对i32和f64的定义,接着将泛型定义替换为这两个具体的定义。
编译器生成的单态化代码看起来像这样:
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);
}
Option<T>
被编译器替换为了具体的定义。因为Rust会将每种情况下的泛型代码编译为具体类型,使用泛型没有运行时开销。当代码运行时,它的执行效率就跟好像手写每个具体定义的重复代码一样。这个单态化过程正是Rust泛型在运行时极其高效的原因。