泛型、Trait 和生命周期(上)

news2024/11/15 19:29:14

目录

1、提取函数来减少重复

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

3、结构体定义中的泛型 

4、枚举定义中的泛型

5、方法定义中的泛型

6、泛型代码的性能


每一门编程语言都有高效处理重复概念的工具。在 Rust 中其工具之一就是 泛型generics)。泛型是具体类型或其他属性的抽象替代。我们可以表达泛型的属性,比如它们的行为或如何与其他泛型相关联,而不需要在编写和编译代码时知道它们在这里实际上代表什么。

首先,我们将回顾一下提取函数以减少代码重复的机制。接下来,我们将使用相同的技术,从两个仅参数类型不同的函数中创建一个泛型函数。我们也会讲到结构体和枚举定义中的泛型。

之后,我们讨论 trait,这是一个定义泛型行为的方法。trait 可以与泛型结合来将泛型限制为只接受拥有特定行为的类型,而不是任意类型。

最后介绍 生命周期lifetimes),它是一类允许我们向编译器提供引用如何相互关联的泛型。Rust 的生命周期功能允许在很多场景下借用值的同时仍然使编译器能够检查这些引用的有效性。

1、提取函数来减少重复

让我们从下面这个这个寻找列表中最大值的小程序开始,如下所示:

fn main() {
    let number_list = vec![34, 50, 25, 100, 65];  // 定义一个列表

    let mut largest = &number_list[0];  // 定义一个可变变量largest,默认取列表的第一个值

    for number in &number_list {
        if number > largest {          // 遍历如果值大于上面定义第一个值,则对应的值覆盖原来的值
            largest = number;
        }
    }

    println!("The largest number is {}", largest);  // 100
}

为了消除重复,我们要创建一层抽象,定义一个处理任意整型列表作为参数的函数。这个方案使得代码更简洁,并且表现了寻找任意列表中最大值这一概念。

fn largest(list: &[i32]) -> &i32 {
    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 number_list = vec![102, 34, 6000, 89, 54, 2, 43, 8];

    let result = largest(&number_list);
    println!("The largest number is {}", result);
}

largest 函数有一个参数 list,它代表会传递给函数的任何具体的 i32值的 slice。函数定义中的 list 代表任何 &[i32]。当调用 largest 函数时,其代码实际上运行于我们传递的特定值上。

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

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

回到 largest 函数,以下示例中展示了两个函数,它们的功能都是寻找 slice 中最大值。接着我们使用泛型将其合并为一个函数。

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 相同类型的引用。

在以下示例中,largest 函数在它的签名中使用了泛型,统一了两个实现。该示例也展示了如何调用 largest 函数,把 i32 值的 slice 或 char 值的 slice 传给它。请注意这些代码还不能编译!

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

运行以上代码,会报以下错误。 

帮助说明中提到了 std::cmp::PartialOrd,这是一个 trait。下一部分会讲到 trait。不过简单来说,这个错误表明 largest 的函数体不能适用于 T 的所有可能的类型。因为在函数体需要比较 T 类型的值,不过它只能用于我们知道如何排序的类型。为了开启比较功能,标准库中定义的 std::cmp::PartialOrd trait 可以实现类型的比较功能(查看附录 C 获取该 trait 的更多信息)。依照帮助说明中的建议,我们限制 T 只对实现了 PartialOrd 的类型有效后代码就可以编译了,因为标准库为 i32 和 char 实现了 PartialOrd

3、结构体定义中的泛型 

同样也可以用 <> 语法来定义结构体,它包含一个或多个泛型参数类型字段。下列示例定义了一个可以存放任何类型的 x 和 y 坐标值的结构体 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 是泛型的,而且字段 x 和 y 都是 相同类型的,无论它具体是何类型。如果尝试创建一个有不同类型值的 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 相同类型,所以将会得到一个像这样的类型不匹配错误:

如果想要定义一个 x 和 y 可以有不同类型且仍然是泛型的 Point 结构体,我们可以使用多个泛型类型参数。在以下示例,我们修改 Point 的定义为拥有两个泛型类型 T 和 U。其中字段 x 是 T 类型的,而字段 y 是 U 类型的:

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

4、枚举定义中的泛型

和结构体类似,枚举也可以在成员中存放泛型数据类型。第六章我们曾用过标准库提供的 Option<T> 枚举,这里再回顾一下:

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 枚举有两个泛型类型,T 和 EResult 有两个成员: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 实例。示例 10-10 展示了一个没有在 impl 之后(的尖括号)声明泛型的例子,这里使用了一个具体类型,f32

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) 之间的距离,并使用了只能用于浮点型的数学运算符。

结构体定义中的泛型类型参数并不总是与结构体方法签名中使用的泛型是同一类型。示例 10-11 中为 Point 结构体使用了泛型类型 X1 和 Y1,为 mixup 方法签名使用了 X2 和 Y2 来使得示例更加清楚。这个方法用 self 的 Point 类型的 x 值(类型 X1)和参数的 Point 类型的 y 值(类型 Y2)来创建一个新 Point 类型的实例:

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

在 main 函数中,定义了一个有 i32 类型的 x(其值为 5)和 f64 的 y(其值为 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

6、泛型代码的性能

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

在这个过程中,编译器所做的工作正好与示例 10-5 中我们创建泛型函数的步骤相反。编译器寻找所有泛型代码被调用的位置并使用泛型代码针对具体类型生成代码。

让我们看看这如何用于标准库中的 Option 枚举:

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

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

点燃设计之魂,展现艺术力量:NCDA邀你参赛!

未来设计师全国高校数字艺术设计大赛&#xff08;NCDA&#xff09;教育部中国高等教育协会认定&#xff0c;由工业和信息化部人才交流中心主办&#xff0c;教育部发文支持“学习强国”学习平台&#xff0c;与联合国国际大赛对接。比赛始于2012年&#xff0c;每年举行一次&#…

LLM Agents 的细分技术

2.1 RAG&#xff1a;知识&#xff0c;记忆与技能库 2.1.1 RAG简介 RAG的核心思想就是将传统的语言生成模型&#xff08;如GPT系列&#xff09;与一个检索系统相结合&#xff0c;在处理一个输入时&#xff0c;RAG首先使用检索系统从一个大规模的文档集合中检索出相关的文本片段…

算法练习-四数之和(思路+流程图+代码)

难度参考 难度&#xff1a;中等 分类&#xff1a;数组 难度与分类由我所参与的培训课程提供&#xff0c;但需要注意的是&#xff0c;难度与分类仅供参考。且所在课程未提供测试平台&#xff0c;故实现代码主要为自行测试的那种&#xff0c;以下内容均为个人笔记&#xff0c;旨在…

【算法与数据结构】718、1143、1035、392、115、LeetCode最长重复子数组+最长公共子序列+不相交的线+判断子序列+不同的子序列

文章目录 一、718、最长重复子数组二、1143、最长公共子序列三、1035、不相交的线四、392、判断子序列五、115、不同的子序列六、完整代码 所有的LeetCode题解索引&#xff0c;可以看这篇文章——【算法和数据结构】LeetCode题解。 一、718、最长重复子数组 思路分析&#xff1…

vue3-内置组件-TransitionGroup

<TransitionGroup> 是一个内置组件&#xff0c;用于对 v-for 列表中的元素或组件的插入、移除和顺序改变添加动画效果。 与 <Transition> 的区别 <TransitionGroup> 支持和 <Transition> 基本相同的 props、CSS 过渡 class 和 JavaScript 钩子监听器&…

Linux联网安装MySQL Server

yum安装 以下代码复制粘贴到控制台即可 yum list | grep mysql-server #查看可以下载的MySQLyum install -y mysql-server #安装MySQLmysql_secure_installation #引导安装 引导安装实例如下 systemctl enable mysqld 设置开机自动启动 systemctl sta…

重生奇迹MU冰风谷雪人王

奇迹雪人王刷怪点在哪里 冰风谷&#xff1a;就一个刷怪点&#xff0c;这里还是比较适合走动打怪&#xff0c;这里刷新4只雪人王&#xff0c;这里从100来级挂到200级问题不大。一般来说冰风谷的雪人王以及雪人王以上的怪都有一定几率会掉宝石,去血色、广场还有寺院掉的要多,不固…

XxlJob深度性能优化实践

一、背景 天画项目的数据工厂目前在与xxl-job对接自动化数据生成任务,另外我司也在使用该组件做业务,所以想深入了解下XxlJob。在跟进了社区的github等仓库issue发现开发迭代停滞了一段时间,思来想去准备开个下游分支做一些性能优化和特性开发等,于是fork了下源码,将其作…

环境配置:ROS melodic安装turtlebot

文章目录 1.已有的环境2.安装2.1.首先cd进入到catkin_ws文件夹&#xff0c;然后输入以下命令行&#xff1a;2.2.将turtlebot加入环境2.3.选择性安装其它依赖包2.4.运行turtlebot in stage [perfect] 1.已有的环境 在之前已经配置好了小乌龟的前提下&#xff0c;我们已经拥有了…

2024/2/4 备战蓝桥杯 5-1 前缀和

目录 求和 0求和 - 蓝桥云课 (lanqiao.cn) 可获得的最小取值 0可获得的最小取值 - 蓝桥云课 (lanqiao.cn) 领地选择 P2004 领地选择 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn) 求和 0求和 - 蓝桥云课 (lanqiao.cn) 思路&#xff1a;先对公式进行合并同类相&#x…

AVR 328pb触摸功能基本介绍和使用

AVR 328pb触摸功能基本介绍和使用 &#x1f4dd;ATMEGA328PB-AU外设中带外围触摸控制器&#xff08;PTC&#xff09;电容式触摸按钮、滑块和轮子24个自帽通道和144个互帽通道。&#xff08;ATMEGA328P没有的&#xff09; ✅PTC-外围触摸控制器 &#x1f343;低功耗、高灵敏度、…

图形界面基础

X Window System 什么是 X Window System&#xff1f; 最初是1984年麻省理工学院的研究&#xff0c;之后变成UNIX、类UNIX、以及OpenVMS等操作系统所一致适用的标准化软体工具套件及显示架构的运作协定。X Window系统透过软体工具及架构协定来建立操作系统所用的图形用户界面&a…

Flink SQL Client 安装各类 Connector、Format 组件的方法汇总(持续更新中....)

博主历时三年精心创作的《大数据平台架构与原型实现&#xff1a;数据中台建设实战》一书现已由知名IT图书品牌电子工业出版社博文视点出版发行&#xff0c;点击《重磅推荐&#xff1a;建大数据平台太难了&#xff01;给我发个工程原型吧&#xff01;》了解图书详情&#xff0c;…

Redis-布隆过滤器解决穿透详解

本文已收录于专栏 《中间件合集》 目录 背景介绍概念说明原理说明解决穿透安装使用安装过程Redis为普通安装的配置方式Redis为Docker镜像安装的配置方式 具体使用控制台操作命令说明Spring Boot集成布隆过滤器 总结提升 背景介绍 布隆过滤器可以帮助我们解决Redis缓存雪崩的问题…

算法笔记刷题日记——3.简单入门模拟 3.2 查找元素

刷题日记 3.2 查找元素 B1041 B1004 B1028 B1032 A1011 A1006 A1036 错题记录 B1028 人口普查 某城镇进行人口普查&#xff0c;得到了全体居民的生日。现请你写个程序&#xff0c;找出镇上最年长和最年轻的人。 这里确保每个输入的日期都是合法的&#xff0c;但不一定是合理的…

Javascript入门学(基础)

软件篇 JS基础语法第一天 1.javascript介绍 1.1 js是什么 是什么 是一种运行在客户端&#xff08;浏览器&#xff09;的编程语言&#xff0c;实现人机交互效果&#xff0c;而html和css是标记性语言&#xff0c;并非编程语言有什么用 js的组成 htmlcssjs实现按钮点击功能 …

使用mmrotate对自定义数据集进行检测

这里写自定义目录标题 安装虚拟环境创建与准备安装mmrotate 自定义数据集标注数据与格式转换数据集划分与大图像切片 训练与测试修改配置文件执行训练进行测试鸣谢 安装 mmrotate是一个自带工作目录的python工具箱&#xff0c;个人觉得&#xff0c;在不熟悉的情况下&#xff0…

node.js后端+小程序前端+mongoDB(增删改查)

前言 今天我对比了以下node.js的express与python的fastAPI&#xff0c;我决定我还是出一期关于node.jsmangoDB小程序的小案例吧。 不是python的fastAPI不好用&#xff0c;因为fastAPI是python较新的技术&#xff0c;我不敢果断发出教学文章&#xff08;这件事情还是留着给pyt…

Leetcode—59. 螺旋矩阵 II【中等】

2024每日刷题&#xff08;113&#xff09; Leetcode—59. 螺旋矩阵 II 实现代码 class Solution { public:vector<vector<int>> generateMatrix(int n) {vector<vector<int>> ans(n, vector<int>(n));int num 0;int c1 0, c2 n - 1;int r1 …

探索C语言结构体:编程中的利器与艺术

✨✨ 欢迎大家来到贝蒂大讲堂✨✨ &#x1f388;&#x1f388;养成好习惯&#xff0c;先赞后看哦~&#x1f388;&#x1f388; 所属专栏&#xff1a;C语言学习 贝蒂的主页&#xff1a;Betty‘s blog 1. 常量与变量 1. 什么是结构体 在C语言中本身就自带了一些数据类型&#x…