在Rust编程中使用泛型

1.摘要

Rust中的泛型可以让我们为像函数签名或结构体这样的项创建定义, 这样它们就可以用于多种不同的具体数据类型。下面的内容将涉及泛型定义函数、结构体、枚举和方法, 还将讨论泛型如何影响代码性能。

2.在函数定义中使用泛型

当使用泛型定义函数时,本来在函数签名中指定参数和返回值的类型的地方,会改用泛型来表示。采用这种技术,使得代码适应性更强,从而为函数的调用者提供更多的功能,同时也避免了代码的重复。

看下面的代码例子, 定义了两个函数, 功能都差不多,作用是分别寻找slice中最大的i32和slice中最大的char, 只是数据类型不同。

fn largest_i32(list: &[i32]) -> &i32 {let mut largest = &list[0];
​for item in list {if item > largest {largest = item;}}
​largest
}
​
fn largest_char(list: &[char]) -> &char {let mut largest = &list[0];
​for item in list {if item > largest {largest = item;}}
​largest
}
​
fn main() {let number_list = vec![34, 50, 25, 100, 65];
​let result = largest_i32(&number_list);println!("The largest number is {}", result);
​let char_list = vec!['y', 'm', 'a', 'q'];
​let result = largest_char(&char_list);println!("The largest char is {}", result);
}

编译一下代码, 输出如下:

我们现在需要定义一个新函数, 引进泛型参数来消除这种因数据类型不同而导致的函数重复定义。为了参数化这个新函数中的这些类型,我们需要为类型参数命名,道理和给函数的形参起名一样。任何标识符都可以作为类型参数的名字。这里选用 T,因为传统上来说,Rust 的类型参数名字都比较短,通常仅为一个字母,同时,Rust 类型名的命名规范是首字母大写驼峰式命名法(UpperCamelCase)。T 作为 “type” 的缩写是大部分 Rust 程序员的首选。

如果要在函数体中使用参数,就必须在函数签名中声明它的名字,好让编译器知道这个名字指代的是什么。同理,当在函数签名中使用一个类型参数时,必须在使用它之前就声明它。为了定义泛型版本的 largest 函数,类型参数声明位于函数名称与参数列表中间的尖括号 <> 中,像这样:

fn largest<T>(list: &[T]) -> &T {

可以这样理解这个定义:函数 largest 有泛型类型 T。它有个参数 list,其类型是元素为 T 的 slice。largest 函数会返回一个与 T 相同类型的引用。

按照这个思想, 我们将代码改造如下:

fn largest<T>(list: &[T]) -> &T {let mut largest = &list[0];
​for item in list {if item > largest {largest = item;}}
​largest
}
​
fn main() {let number_list = vec![34, 50, 25, 100, 65];
​let result = largest(&number_list);println!("The largest number is {}", result);
​let char_list = vec!['y', 'm', 'a', 'q'];
​let result = largest(&char_list);println!("The largest char is {}", result);
}

一切似乎很顺利, 尝试编译这段代码, 编译器结果如下:

这次编译没有通过的原因Rust编译器用绿色标识出来了, 缺少一个: std:cmp::PartialOrd, 先暂且认为这个是Rust标准库要求的东西, 加上重新编译一下试试:

fn largest<T: std::cmp::PartialOrd>(list: &[T]) -> &T {let mut largest = &list[0];
​for item in list {if item > largest {largest = item;}}
​largest
}

重新编译结果如下:

我们在代码中下了一个断点, 能够执行到此处说明代码已经没有问题。实际上上面这个错误表明 largest 的函数体不能适用于 T 的所有可能的类型。因为在函数体需要比较 T 类型的值,不过它只能用于我们知道如何排序的类型。为了开启比较功能,标准库中定义的 std::cmp::PartialOrd trait 可以实现类型的比较功能, 我们限制 T 只对实现了 PartialOrd 的类型有效后代码就可以编译了,因为标准库为 i32char 实现了 PartialOrd

3.在结构体中使用泛型

同样也可以用 <> 语法来定义结构体,它包含一个或多个泛型参数类型字段。下面的代码片段定义了一个可以存放任何类型的 xy 坐标值的结构体 Point

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 };
}

其语法类似于函数定义中使用泛型。首先,必须在结构体名称后面的尖括号中声明泛型参数的名称。接着在结构体定义中可以指定具体数据类型的位置使用泛型类型。

注意 Point<T> 的定义中只使用了一个泛型类型,这个定义表明结构体 Point<T> 对于一些类型 T 是泛型的,而且字段 xy 都是 相同类型的,无论它具体是何类型。

如果尝试创建一个有不同类型值的 Point<T> 的实例, 看下面的代码:

struct Point<T> {x: T,y: T,
}
​
fn main() {let wont_work = Point { x: 5, y: 4.0 };
}

在这个例子中,当把整型值 5 赋值给 x 时,就告诉了编译器这个 Point<T> 实例中的泛型 T 全是整型。接着指定 y 为浮点值 4.0,因为它y被定义为与 x 相同类型,所以将会得到一个像这样的类型不匹配错误:

如果想要定义一个 xy 可以有不同类型且仍然是泛型的 Point 结构体,我们可以使用多个泛型类型参数。修改 Point 的定义为拥有两个泛型类型 TU。其中字段 xT 类型的,而字段 yU 类型的:

struct Point<T, U> {x: T,y: U,
}
​
fn main() {let both_integer = Point { x: 5, y: 10 };let both_float = Point { x: 1.0, y: 4.0 };let integer_and_float = Point { x: 5, y: 4.0 };
}

现在所有这些 Point 实例都合法了!我们可以在定义中使用任意多的泛型类型参数,不过太多的话,代码将难以阅读和理解。当你发现代码中需要很多泛型时,这可能表明你的代码需要重构分解成更小的结构。

4.枚举中使用泛型

和结构体类似,枚举也可以在成员中存放泛型数据类型。例如:

enum Option<T> {Some(T),None,
}

Option<T> 是一个拥有泛型 T 的枚举,它有两个成员:Some,它存放了一个类型 T 的值,和不存在任何值的None。通过 Option<T> 枚举可以表达有一个可能的值的抽象概念,同时因为 Option<T> 是泛型的,无论这个可能的值是什么类型都可以使用这个抽象。

枚举也可以拥有多个泛型类型, 例如:

enum Result<T, E> {Ok(T),Err(E),
}

Result 枚举有两个泛型类型,TEResult 有两个成员:Ok,它存放一个类型 T 的值,而 Err 则存放一个类型 E 的值。这个定义使得 Result 枚举能很方便的表达任何可能成功(返回 T 类型的值)也可能失败(返回 E 类型的值)的操作。

总结:当意识到代码中定义了多个结构体或枚举,它们不一样的地方只是其中的值的类型的时候,不妨通过泛型类型来避免重复。

5.方法定义中的泛型

在为结构体和枚举实现方法时, 一样也可以用泛型。看下面的代码:

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());
}

这里在 Point<T> 上定义了一个叫做 x 的方法来返回字段 x 中数据的引用。注意必须在 impl 后面声明 T,这样就可以在 Point<T> 上实现的方法中使用 T 了。通过在 impl 之后声明泛型 T,Rust 就知道 Point 的尖括号中的类型是泛型而不是具体类型。我们可以为泛型参数选择一个与结构体定义中声明的泛型参数所不同的名称,不过依照惯例使用了相同的名称。impl 中编写的方法声明了泛型类型可以定位为任何类型的实例,不管最终替换泛型类型的是何具体类型。

定义方法时也可以为泛型指定限制(constraint)。例如,可以选择为 Point<f32> 实例实现方法,而不是为泛型 Point 实例。代码如下:

impl Point<f32> {fn distance_from_origin(&self) -> f32 {(self.x.powi(2) + self.y.powi(2)).sqrt()}
}

这段代码意味着 Point<f32> 类型会有一个方法 distance_from_origin,而其他 T 不是 f32 类型的 Point<T> 实例则没有定义此方法。这个方法计算点实例与坐标 (0.0, 0.0) 之间的距离,并使用了只能用于浮点型的数学运算符。

结构体定义中的泛型类型参数并不总是与结构体方法签名中使用的泛型是同一类型。看下面的代码:

struct Point<X1, Y1> {x: X1,y: Y1,
}
​
impl<X1, Y1> Point<X1, Y1> {fn mixup<X2, Y2>(self, other: Point<X2, Y2>) -> Point<X1, Y2> {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 结构体使用了泛型类型 X1Y1,为 mixup 方法签名使用了 X2Y2 来使得示例更加清楚。这个方法用 selfPoint 类型的 x 值(类型 X1)和参数的 Point 类型的 y 值(类型 Y2)来创建一个新 Point 类型的实例

main 函数中,定义了一个有 i32 类型的 x(其值为 5)和 f64y(其值为 10.4)的 Pointp2 则是一个有着字符串 slice 类型的 x(其值为 "Hello")和 char 类型的 y(其值为c)的 Point。在 p1 上以 p2 作为参数调用 mixup 会返回一个 p3,它会有一个 i32 类型的 x,因为 x 来自 p1,并拥有一个 char 类型的 y,因为 y 来自 p2println! 会打印出 p3.x = 5, p3.y = c

这个例子的目的是展示一些泛型通过 impl 声明而另一些通过方法定义声明的情况。这里泛型参数 X1Y1 声明于 impl 之后,因为它们与结构体定义相对应。而泛型参数 X2Y2 声明于 fn mixup 之后,因为它们只是相对于方法本身的。

6.泛型代码性能

不用担心使用泛型会比使用具体类型的代码性能低。

Rust 通过在编译时进行泛型代码的 单态化monomorphization)来保证效率。单态化是一个通过填充编译时使用的具体类型,将通用代码转换为特定代码的过程。

在这个过程中,编译器寻找所有泛型代码被调用的位置并使用泛型代码针对具体类型生成代码。

下面看看这个怎样用于标准库中的 Option 枚举:

let integer = Some(5);
let float = Some(5.0);

当 Rust 编译这些代码的时候,它会进行单态化。编译器会读取传递给 Option<T> 的值并发现有两种 Option<T>:一个对应 i32 另一个对应 f64。为此,它会将泛型定义 Option<T> 展开为两个针对 i32f64 的定义,接着将泛型定义替换为这两个具体的定义。

编译器生成的单态化版本的代码看起来像这样(编译器会使用不同于如下假想的名字):

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 泛型在运行时极其高效的原因。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/192329.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

竞赛选题 疲劳驾驶检测系统 python

文章目录 0 前言1 课题背景2 Dlib人脸识别2.1 简介2.2 Dlib优点2.3 相关代码2.4 人脸数据库2.5 人脸录入加识别效果 3 疲劳检测算法3.1 眼睛检测算法3.2 打哈欠检测算法3.3 点头检测算法 4 PyQt54.1 简介4.2相关界面代码 5 最后 0 前言 &#x1f525; 优质竞赛项目系列&#x…

基于Adapter用CLIP进行Few-shot Image Classification

文章目录 【ECCV 2022】《Tip-Adapter: Training-free Adaption of CLIP for Few-shot Classification》【NeuIPS 2023】《Meta-Adapter: An Online Few-shot Learner for Vision-Language Model》 【ECCV 2022】《Tip-Adapter: Training-free Adaption of CLIP for Few-shot C…

使用Redis实现分布式锁

Hi, I’m Shendi 使用Redis实现分布式锁 需求场景 需要使用到分布式锁的场景非常多&#xff0c;例如抢单等并发场景&#xff0c;这里举一个例子。 有一个商品&#xff0c;限量出售100个&#xff0c;一个用户下单&#xff0c;数量就减少一个&#xff0c;当剩下最后一个时&…

Unity在Windows选项下没有Auto Streaming

Unity在Windows选项下没有Auto Streaming Unity Auto Streaming插件按网上说的不太好使最终解决方案 Unity Auto Streaming插件 我用的版本是个人版免费版&#xff0c;版本号是&#xff1a;2021.2.5f1c1&#xff0c;我的里边Windows下看不到Auto Streaming选项,就像下边这张图…

基于猕猴感觉运动皮层Spike信号的运动解码分析不同运动参数对解码的影响

公开数据集中文版详细描述参考前文&#xff1a;https://editor.csdn.net/md/?not_checkout1&spm1011.2124.3001.6192神经元Spike信号分析参考前文&#xff1a;https://blog.csdn.net/qq_43811536/article/details/134359566?spm1001.2014.3001.5501神经元运动调制分析参考…

CUDA编程一、基本概念和cuda向量加法

目录 一、cuda编程的基本概念入门 1、GPU架构和存储结构 2、cuda编程模型 3、cuda编程流程 二、cuda向量加法实践 1、代码实现 2、代码运行和结果 有一段时间对模型加速比较感兴趣&#xff0c;其中的一块儿内容就是使用C和cuda算子优化之类一起给模型推理提速。之前一直…

HAL库STM32串口开启DMA接收数据

STM32CubeMx的配置 此博客仅仅作为记录&#xff0c;这个像是有bug一样&#xff0c;有时候好使&#xff0c;有时候不好&#xff0c;所以趁现在好使赶紧记录一下&#xff0c;很多地方用到串口接收数据&#xff0c;DMA又是一种非常好的接收方式&#xff0c;可以节约CPU的时间&…

Unity - Cinemachine

动态获取Cinemachine的内部组件 vCam.GetCinemachineComponent<T>() 动态修改Cinemachine的Transposer属性 var vCamComp transfrom.GetComponent<CinemachineVirtualCamera>(); var transposerComp vCamComp.GetCinemachineComponent<CinemachineTransposer&…

2023最新最全【OpenMV】 入门教程

1. 什么是OpenMV OpenMV 是一个开源&#xff0c;低成本&#xff0c;功能强大的 机器视觉模块。 OpenMV上的机器视觉算法包括 寻找色块、人脸检测、眼球跟踪、边缘检测、标志跟踪 等。 以STM32F427CPU为核心&#xff0c;集成了OV7725摄像头芯片&#xff0c;在小巧的硬件模块上&a…

我们应该如何理解Java集合框架的关键知识点?

我们应该如何理解Java集合框架的关键知识点&#xff1f; java集合是教存取数据的一个容器&#xff0c;涵盖了各种存和取的方式&#xff0c;应用在不同的工作场景中&#xff0c;要想了解java集合的相关知识&#xff0c;建议先好好学习一下数据结构这本书。最近很多小伙伴找我&am…

使用Lychee搭建个人图片存储系统并进行远程访问设置实现公网访问本地私人图床

文章目录 1.前言2. Lychee网站搭建2.1. Lychee下载和安装2.2 Lychee网页测试2.3 cpolar的安装和注册 3.本地网页发布3.1 Cpolar云端设置3.2 Cpolar本地设置 4.公网访问测试5.结语 1.前言 图床作为图片集中存放的服务网站&#xff0c;可以看做是云存储的一部分&#xff0c;既可…

qt library创建和使用

1、创建library 2、修改library中的代码 3、把library进行编译&#xff0c;编译后会生成相关文件 4、把编译后的文件拷贝到主程序目录下面。 5、并把library中的testlib头文件拷贝到主程序&#xff0c;并在pro文件加入&#xff08;这里在后面使用library中函数有关&#xff0…