用统一功能的函数处理不同类型的数据,在不支持泛型的语言当中,通常需要为每一种类型编写一个函数:
fn add_18(a:i8,b:i8)->i8{
a+b
}
fn add_12(a:i32,b:i32)->i32{
a+b
}
fn add_f64(a:f64,b:f64)->f64{
a+b
}
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<T>(a:T,b:T)->T{
a+b
}
fn main(){
println!("add i8: {}", add(2i8, 3i8));
println!("add i32: {}", add(20, 30));
println!("add f64: {}", add(1.23, 1.23));
}
泛型讲解
上面代码中的<T>就是泛型参数,rust中,泛型参数的名称可以随便起,但是我们一般用T作为首选(T是type的首字母)
使用泛型参数有一个先决条件:必须在使用前进行声明:
fn largest<T>(list:&[T])->T
从列表中找到最大的值,其中列表中的元素类型是T,largest<T>对T进行了声明,然后踩在函数参数中使用该泛型参数list:&[T]
largest有泛型类型T,有参数list,其元素是类型为T的数组切片,最后返回的类型也是T
1个错误示例:
fn largest<T>(list:&[T])->T{
let mut largest = list[0];
for &item in list.iter(){
if item > largest{
largest = item;
}
}
}
fn main(){
let number_list = vec![34,50,25,100,65];
let result = largest(&number_list);
println!("The latgest number is {}",result);
let char_list = vec!['y','m','a','f'];
let result = largest(&char_list);
println!("The largest char is {}",result);
}
Error
对于上面的错误,编译器建议我们对T添加一个类型限制,使用std::cmp::PartialOrd特征对T进行限制,该特征就是为了让类型实现可比较的功能
同样的,最开始的add也会error,如果运行会得到error:
不是所有的T都可以相加,需要用std::ops::Add<Output=T>对T进行限制:
fn add<T:std::ops::Add<Output=T>>(a:T,b:T){
a+b
}
显式的指定泛型的类型参数
有时候,编译器无法推断你想要的泛型参数
use std::fmt::Display
fn create_and_print<T>() where T:From<i32>+Display{
let a:T=100.into(); //创建了类型为T的变量,初始值由100转换而来
println!("a is{}",a);
}
fn main()
{
create_and_print();
}
Error:
好心的编译器让我们:显式指定类型create_and_print::<T>()
于是我们修改代码
use std::fmt::Diplay
fn create_and_point<T>() where T:From<i32>+Display{
let a:T = 100.into();
println!("a is {}",a);
}
fn main(){
create_and_point::<i64>();
}
结构体中使用泛型
struct Point<T>{
x:T,
y:T,
}
fn main(){
let integer = Point{x:5,y:10};
let float = Pont{x:1.0,y:4.0};
}
Notice:
- 提前声明,和泛型函数定义类似,首先我们在使用泛型参数之前必须进行声明
Pony<T> - x和y是相同的类型
struct Point<T>
{
x:T,
y:T,
}
fn main(){
let p = Point{x:1,y:1.1};
}
Error:
如过想让x``y既能够类型相同,又能够类型不同,就需要使用不同的泛型参数:
struct Point<T,U>{
x:T,
y:U,
}
fn main(){
let p = Point{x:1,y:1.1};
}
枚举中使用泛型
Option应该是枚举中第一杯想起来的
enum Option<T>{
Some<T>,
None,
}
Option<T>是1个具有泛型T的枚举类型,第一个成员是Some<T>,存放了一个类型为T的值;我们可以在任何一个需要返回值的函数中国,取使用Option<T>取作为返回值,用于返回一个任意类型的Some<T>,或者没有值None
Result->卧龙凤雏,得1可安天下
enum Result<T,E>{
Ok(T),
Error(E),
}
这个枚举也主要用于函数返回值
如果函数正常运行,则最后返回一个
Ok(T),T是函数具体的返回值类型,如果函数异常运行,则返回一个Err(E),E是错误类型。例如打开一个文件:如果成功打开文件,则返回Ok(std::fs::File),因此T对应的是std::fs::File类型;而当打开文件时出现问题时,返回Err(std::io::Error),E对应的就是std::io::Error类型。
方法中使用泛型
方法中使用泛型:
struct Point<T>{
x:T,
y:T
}
impl<T> Point<T>{
fn x(&self)->&T{
&self.x
}
}
fn main(){
let p = Point{x:5,y:10};
println!("p.x={}",p.x());
}
依旧提前声明impl<T>,这里的Point<T>不再是声明,而是一个完整的结构体类型
我们还能在该结构体的方法中定义额外的泛型参数:
struct Poiunt<T,U>{
x:T,
y:U,
}
impl <T,U> Point<T,U>{
fn mixip<V,W>(self,other:Point<V,W>)->Point<T,W>{
Point{
x:self.x,
y:self.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);
}
其中,<V,W>是Point上的泛型参数,V,W是单独定义在mixup上的泛型参数
为具体的泛型类型实现方法
对于Point<T>类型,不仅能定义基于T的方法,还能针对特定的具体类型,进行方法定义:
impl Point<f32>{
fn distance_from_origin(&self)->f32{
(self.x.powi(2)+self.y.powi(2)).sqrt()
}
}
这里意味着Point<f32>类型会有1个方法distance_from_origin,但是其他T不是f32类型的Point<T>实例则没有定义此方法,使用了只能用于浮点型的数学运算符号
const泛型
有没有针对值的泛型?
之前我们有提到,[i32;2]和[i32;3]是不同的数组类型:
之前我们有提到[i32;3]和[i32;2]是不同的数组类型
fn display_array(arr:[i32;3]){
println!("{:?}",arr);
}
fn main(){
let arr:[i32;3]=[1,2,3];
display_array(arr);
let arr:[i32;2]=[1,2];
display_arr(arr);
}
Error:
我们来修改代码:
fn display_array(arr:&[i32]){
println!("{:?}",arr);
}
fn main(){
let arr:[i32;3]=[1,2,3];
display_array(&arr);
let arr:[i32;2]=[1,2];
display_arr(&arr);
}
很简单,只需要使用数组切片,然后传入arr的不可变引用即可;
接着,我们将i32改为所有类型的数组
fn display_array<T:std::fmt::Debug>(arr:&[T]){
println!("{:?}",arr);
}
fn main(){
let arr:[i32;3]=[1,2,3];
display_array(&arr);
let arr:[i32;2]=[1,2];
display_arr(&arr);
}
我们只需要给T加一个限制std::fmt::Debug,表明T可以用在println!("{:?}",arr)中,因为{:?}形式的格式化输出需要arr实现改特征
之前rust的一些数组库,在使用的时候限定长度不超过32->因为会为每个长度单独实现一个函数
现在我们有了const泛型,也就是针对值的泛型,可以用于处理数组长度的问题:
fn display_array<T:std::fmt::Debug,const N:usize>(arr:[T;N]){
println!("{:?}",arr);
}
fn main(){
let arr:[i32;3]=[1,2,3];
display_array(&arr);
let arr:[i32;2]=[1,2];
display_arr(&arr);
}
如上,我们定义了一个类型为[T;N]的数组,其中T是一个基于类型的泛型参数,N是一个基于值的泛型参数。
const泛型表达式
假设我们需要在一个内存很小的平台上工作,因此需要限制函数参数占用的内存大小,使用const泛型表达式来实现
#![allow(incomplete_features)]
#![feature(generic_const_exprs)]
fn something<T>(val:T){
where
Assert<{core::mem::size_of::<T>()<768}>:isTrue,
// ^-----------------------------^ 这里是一个 const 表达式,换成其它的 const 表达式也可以
{
//
}
}
fn main(){
something([0u8;0]);
something([0u8;512]);
something([0u8;1024]);
}
pub enum Assert<const CHECK:bool>{
//
}
pub trait IsTrue{
//
}
impl IsTrue for Arrert<true>{
//
}
const fn
我们要提及1个和它密切相关的强大的特性:const fn
为什么需要const fn
通常情况下,函数是在运行时被调用和执行的。然而,在某些场景下,我们希望在编译器就计算出一些值
有了const fn,我们可以在编译器执行这些函数,,从而将计算结果直接嵌入到生成的代码中,不仅提高了运行的性能,还使得代码更加简介安全
定义一个常量函数,只需要在函数声明上加上const关键字:
基本用法:
const fn add(a:usize,b:usize)->usize{
a+b
}
const RESULT:usize = add(5,10);
fn main(){
println!("The result is:{}",RESULT);
}
限制:
由于在编译期间执行,为了确保函数能够在编译期间被安全的求值,因此有一些限制,不可以将随机数生成器携程const fn
如果我们在极端的情况下进行复杂的浮点运算,可能会得到不同结果,因此不建议使数组长度(arr.len)和Enum判别式依赖于浮点运算
const fn和const 泛型结合,实现更加灵活和高效的代码设计
struct Buffer<const N:usize>{
data:[u8;N],
}
const fn compute_buffer_size(factor:usize)->usize{
factor*1024
}
fn main(){
const SIZE:usize = compute_buffer_size(4);
let buffer = Buffer::<SIZE>{
data:[0;SIZE];
};
println!("Buffer size:{} bytes",buffer.data.len());
}
在这个例子中compute_buffer_size是一个常量函数,我们用compute_buffer_size来计算缓冲区大小为4096bytes,并假期作为泛型参数传给Buffer->这样缓冲区的大小在编译器就被确定下来了,避免了运行的计算开销
泛型的性能
我们在rust上收获了性能;但是鱼与熊掌不可得兼,rust在编译期间为泛型对应的多个类型,生成格子的代码,因此损失了编译速度增大了最终生成文件的大小。 rust在编译期进行泛型代码的单态化来保证效率,是一个通过填充编译时使用的具体类型,将通用diamagnetic祝你华为特定代码的过程 编译器所做的工作和我们创建泛型函数的步骤相反,寻找所有泛型代码被调用的位置并针对具体的类型生成代码 看看1个标准库中的例子:
let integer = Some(5);
let float = Some(5.0);
rust编译这些代码时候,会进行单态化,编译器会读取传递给Option<T>的值并发现有2种Option<T>:一种对应i32一种对应f64,将Option<T>展开为Option_i32和Option_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);
}
我们可以在使用泛型来编写不重复的代码,rust会为每一个实例编译其特性类型的代码,意味着使用泛型时没有运行时开销;代码运行,执行效率好像手写每个具体定义的重复代码一样,这正是rust的泛型运行时及其高效的原因