Rust之泛型、特性和生命期(二):通用数据类型

news2024/12/26 22:59:56

开发环境

  • Windows 10
  • Rust 1.69.0

 

  • VS Code 1.78.2

  项目工程

这里继续沿用上次工程rust-demo

通用数据类型

 我们使用泛型来为函数签名或结构等项目创建定义,然后我们可以将其用于许多不同的具体数据类型。让我们首先看看如何使用泛型来定义函数、结构、枚举和方法。然后我们将讨论泛型如何影响代码性能。 

函数的泛型定义

当定义一个使用泛型的函数时,我们将泛型放在函数的签名中,在这里我们通常会指定参数和返回值的数据类型。这样做使我们的代码更加灵活,为我们的函数的调用者提供更多的功能,同时防止代码重复。

继续我们的最大函数,示例4显示了两个函数,它们都能找到一个片断中的最大值。然后我们将把它们合并成一个使用泛型的函数。

文件名:src/main.rs

fn largest_i32(list: &[i32]) -> &i32 {         // largest_i32 函数
    let mut largest = &list[0];

    for item in list {
        if item > largest {
            largest = item;
        }
    }

    largest
}

fn largest_char(list: &[char]) -> &char {      // largest_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);
}

示例4:两个函数只在名称和签名中的类型上有所不同

largest_i32函数就是我们在上一节示例3中提取的那个函数,它可以找到一个片断中最大的i32biggest_char函数找到一个片断中最大的char。这些函数体有相同的代码,所以让我们通过在一个函数中引入一个通用类型参数来消除重复。 

为了在一个新的单一函数中对类型进行参数化,我们需要对类型参数进行命名,就像我们对函数的值参数所做的那样。你可以使用任何标识符作为类型参数名。但是我们将使用T,因为按照惯例,Rust中的类型参数名很短,通常只有一个字母,而且Rust的类型命名惯例是UpperCamelCase(驼峰命名法)。T是 "类型 "的简称,是大多数Rust程序员的默认选择。

当我们在函数主体中使用参数时,我们必须在签名中声明参数名称,以便编译器知道这个名称的含义。同样地,当我们在函数签名中使用一个类型参数名时,我们必须在使用它之前声明类型参数名。为了定义通用的largest,将类型名称声明放在角括号内,<>,在函数名称和参数列表之间,像这样:

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

我们把这个定义理解为:函数largest在某种类型T上是通用的。这个函数有一个名为list的参数,它是一个类型为T的值的切片。largest将返回一个对同一类型T的值的引用。

示例5显示了在其签名中使用通用数据类型的组合largest定义。该清单还显示了我们如何用i32值的切片或char值调用该函数。注意,这段代码还不能编译,但我们将在本章后面修复它。 

文件名: src/main.rs

fn largest<T>(list: &[T]) -> &T {       // largest函数的泛型定义
    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);                // 调用largest函数
    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);
}

 示例5:使用通用类型参数的largest,这还不能编译

编译

cargo run

 帮助信息提到了std::cmp::PartialOrd,它是一个trait,我们将在下一节讨论trait。现在,我们要知道,这个错误说明,largest对所有可能的T的类型都不起作用。因为我们想在主体中比较类型T的值,我们只能使用值可以被排序的类型。为了实现比较,标准库有std::cmp::PartialOrd特质,你可以在类型上实现它(关于这个特质的更多信息,见附录C)。按照帮助信息的建议,我们把对T有效的类型限制为只实现PartialOrd的类型,这个例子就可以编译了,因为标准库对i32char都实现了PartialOrd

结构体的泛型定义

我们还可以使用<>语法定义结构,在一个或多个字段中使用通用类型参数。示例6 定义了一个 Point<T> 结构来保存任何类型的 x y 坐标值。

文件名: src/main.rs

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

示例6:一个带有T类型的xy值的Point<T>结构

在结构体定义中使用泛型的语法与函数定义中使用的语法类似。首先,我们在结构名称之后的角括号内声明类型参数的名称。然后,我们在结构定义中使用泛型,否则我们会指定具体的数据类型。 

注意,因为我们只用了一个泛型来定义Point<T>,这个定义说Point<T>结构是某个类型T的泛型,字段xy都是同一类型,不管这个类型是什么。如果我们创建一个具有不同类型值的 Point<T> 实例,如示例7 所示,我们的代码将无法编译。

文件名:src/main.rs

struct Point<T> {
    x: T,
    y: T,
}

fn main() {
    let wont_work = Point { x: 5, y: 4.0 };
}

示例7: 字段xy必须是相同的类型,因为两者有相同的通用数据类型T

编译

cargo run

 在这个例子中,当我们把整数值5分配给x时,我们让编译器知道,对于Point<T>的这个实例,通用类型T将是一个整数。然后,当我们为y指定4.0时,我们已经定义了与x相同的类型,我们会得到一个类型不匹配的错误,像这样: 

为了定义一个xy都是泛型但可能有不同类型的Point结构,我们可以使用多个泛型参数。例如,在示例8中,我们将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 };
    
    println!("both_integer.x = {}, both_integer.y= {}", both_integer.x, both_integer.y);
    println!("both_float.x = {}, both_float.x = {}", both_float.x, both_float.y);
    println!("integer_and_float.x = {}, integer_and_float.y = {}", integer_and_float.x, integer_and_float.y);
}

示例8:一个Point<T, U>在两种类型上的泛型,以便xy可以是不同类型的值

编译

 

 现在,所有显示的Point的实例都被允许了! 你可以在一个定义中使用尽可能多的泛型参数,但使用多了会使你的代码难以阅读。如果你发现你的代码中需要大量的泛型,这可能表明你的代码需要重组成小块。

枚举的泛型定义

就像我们对结构体所做的那样,我们可以定义枚举来保存其变体中的通用数据类型。让我们再看看标准库提供的Option<T>枚举,我们在之前的章节中使用过它:

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

这个定义现在对你来说应该更有意义了。正如你所看到的,Option<T>枚举对T类型是通用的,有两个变体: Some,它持有一个T类型的值,和一个None变体,它不持有任何值。通过使用Option<T>枚举,我们可以表达一个可选值的抽象概念,由于Option<T>是通用的,所以无论可选值的类型是什么,我们都可以使用这个抽象概念。

枚举也可以使用多个通用类型。我们在之前章节种使用的Result枚举的定义就是一个例子: 

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

 Result枚举在两种类型上是通用的,即TE,并且有两个变量:Ok, 持有一个T类型的值,Err,它持有一个E类型的值。这个定义使得我们可以在任何有可能成功(返回某种类型的T的值)或失败(返回某种类型的E的错误)的操作中使用Result枚举。事实上,这就是我们在之前章节的示例中用来打开一个文件的方法,当文件被成功打开时,T被填写为std::fs::File类型,当打开文件有问题时,E被填写为std::io::Error类型。

当你发现你的代码中有多个结构或枚举的定义,而这些定义只在它们持有的值的类型上有区别时,你可以通过使用通用类型来避免重复的情况。

方法的泛型定义

我们可以在结构体和枚举上实现方法(就像我们在之前章中做的那样),也可以在它们的定义中使用通用类型。示例9显示了我们在清示例6中定义的Point<T>结构,并在其上实现了一个名为x的方法。

文件名: src/main.rs

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

示例9:在Point<T>结构上实现一个名为x的方法,该方法将返回一个对T类型的x字段的引用

编译

这里,我们在Point<T>上定义了一个名为x的方法,返回对字段x中数据的引用。 

请注意,我们必须在impl之后声明T,这样我们就可以用T来指定我们在Point<T><T>这个类型上实现方法。通过在 impl之后将T声明为泛型,Rust 可以确定 Point 中角括号中的类型是一个泛型,而不是一个具体类型。我们可以为这个泛型参数选择一个与结构定义中声明的泛型参数不同的名字,但使用相同的名字是常规的。在声明了泛型的impl函数中编写的方法将被定义在该类型的任何实例上,无论最终用什么具体类型来替代泛型。 

在定义类型上的方法时,我们也可以指定对泛型的约束。例如,我们可以只在 Point<f32> 实例上实现方法,而不是在具有任何泛型的 Point<T> 实例上。在示例10中,我们使用了具体的类型f32,这意味着我们没有在impl后面声明任何类型。

文件名: src/main.rs

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

示例10:一个impl块,它只适用于具有特定具体类型的通用类型参数T的结构

这段代码意味着Point<f32>类型将有一个distance_from_origin方法;其他Point<T>的实例,如果T不属于f32类型,则不会定义这个方法。该方法测量我们的点离坐标(0.0, 0.0)处的点有多远,并使用仅对浮点类型可用的数学运算。 

结构定义中的通用类型参数并不总是与你在同一结构的方法签名中使用的参数相同。示例11在Point结构中使用了通用类型X1Y1,在mixup方法签名中使用了X2 Y2,以使例子更加清晰。该方法创建了一个新的Point实例,其X值来自于自身的Point(类型为X1),Y值来自于传入的Point(类型为Y2)。

文件名: src/main.rs

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

示例11:一个使用不同于其结构定义的通用类型的方法

编译

 

 在main中,我们定义了一个Point,其中i32x(值为5),f64y(值为10.4)。p2变量是一个Point结构,其中x是一个字符串片(值为 "Hello"),y是一个char(值为c)。在p1上调用mixup,参数为p2,得到p3,它有一个i32x,因为x来自p1p3变量将有一个char作为y,因为y来自p2println!宏调用将打印p3.x = 5, p3.y = c。 

这个例子的目的是演示这样一种情况:一些泛型参数与 impl 一起声明,一些则与方法定义一起声明。在这里,泛型参数X1Y1是在impl之后声明的,因为它们与结构定义一起。泛型参数X2Y2是在fn mixup之后声明的,因为它们只与方法相关。 

使用泛型的代码的性能

你可能想知道在使用通用类型参数时是否有运行时间成本。好消息是,使用泛型不会使你的程序比使用具体类型的程序运行得更慢。

Rust通过在编译时对使用泛型的代码进行单态化来实现这一点。单态化是指通过填写编译时使用的具体类型,将泛型代码变成具体代码的过程。在这个过程中,编译器所做的与我们在示例5中创建泛型函数的步骤相反:编译器会查看所有调用泛型代码的地方,并为泛型代码所调用的具体类型生成代码。

 让我们通过使用标准库的通用Option<T>枚举来看看这是如何工作的:

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

当Rust编译这段代码时,它进行了单态化。在这个过程中,编译器读取了Option<T>实例中使用的值,并确定了两种Option<T>:一种是i32,另一种是f64。因此,它将Option<T>的通用定义扩展为两个专门针对i32f64的定义,从而用特定的定义代替了通用定义。

代码的单态化版本看起来与下面类似(编译器使用的名称与我们在这里使用的不同,以示说明):

文件名: src/main.rs 

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/544091.html

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

相关文章

27K 入职字节的那天,我哭了····

先说一下自己的个人情况&#xff0c;计算机专业&#xff0c;18年本科毕业&#xff0c;一毕业就进入了“阿里”测试岗(进去才知道是接了个阿里外包项目&#xff0c;可是刚毕业谁知道什么外包不外包的)。 更悲催的是&#xff1a;刚入职因为家里出现一些变故&#xff0c;没有精力…

【软件测试】Python自动化软件测试算是程序员吗?

今天早上一觉醒来&#xff0c;突然萌生一个念头&#xff0c;【软件测试】软件测试算是程序员吗&#xff1f;左思右想&#xff0c;总感觉哪里不对。做了这么久的软件测试&#xff0c;还真没深究过这个问题。 基于&#xff0c;内事问百度的准则&#xff1a; 结果…… 我刚发出软…

刚测完Bug,就被开除了····

我曾在一家软件公司担任功能测试工程师&#xff0c;经历了三年的工作。在这段时间里&#xff0c;我积累了丰富的测试经验和技能&#xff0c;在团队中也有着不错的表现。然而&#xff0c;最终我却被公司辞退了。 在我入职时&#xff0c;公司还没有建立完善的测试流程和标准。我的…

JVM系列-第10章-垃圾回收概述和相关算法(JVisualVM)

垃圾回收概述 Java 和 C语言的区别&#xff0c;就在于垃圾收集技术和内存动态分配上&#xff0c;C语言没有垃圾收集技术&#xff0c;需要程序员手动的收集。 垃圾收集&#xff0c;不是Java语言的伴生产物。早在1960年&#xff0c;第一门开始使用内存动态分配和垃圾收集技术的L…

【分享】阿里版ChatGPT—通义千问(初体验)

哈喽&#xff0c;大家好&#xff0c;我是木易巷~ 在上个月4月7号&#xff0c;木易巷开始申请阿里云大模型开始邀请测试「通义千问」&#xff0c;到今天早上&#xff0c;木易巷收到了申请通过的短信。 官网地址&#xff1a;tongyi.aliyun.com 迫不及待去测试了一下&#xff0c;效…

「直播精选问答」释放数据潜力,助力零售数智升级!

​5月10日的《释放数据潜力&#xff0c;助力零售数智升级》虹科零售专场BI直播课程活动&#xff0c;在各位观众的积极互动与热情反馈中落下帷幕&#xff01; 为激发观众和读者朋友对于零售行业数智化趋势的思考与讨论&#xff0c;真正让BI知识在数字时代得以共享和碰撞&#xf…

MaaS来临,SaaS进入「奇点」时刻|产业深度

大模型热度持续发酵。MaaS的到来&#xff0c;不仅改变了云厂商的竞争格局&#xff0c;SaaS行业也将迎来「奇点」时刻。未来十年&#xff0c;基于MaaS底座&#xff0c;国内SaaS甚至可能会出现Salesforce一样的巨头。 作者|思杭 编辑|皮爷 出品|产业家 大模型热度正在持续发酵。…

阿里云镜像服务下载并安装Go环境

【阿里云镜像】下载并安装Go环境 一、参考链接 阿里巴巴开源镜像站-OPSX镜像站-阿里云开发者社区 (aliyun.com) golang镜像-golang下载地址-golang安装教程-阿里巴巴开源镜像站 (aliyun.com) GO语言安装以及国内镜像 - DbWong_0918 - 博客园 (cnblogs.com) 二、Go介绍 Gol…

vscode使用git对代码进行管理

2、暂存自己更改&#xff1b;3、拉取别人的代码&#xff0c;防止别人更改代码合并时发生冲突&#xff1b;4、上传自己的更改并合并代码

深度学习在自然语言处理方面的应用

前言 自然语言处理是一种将自然语言转换为计算机可处理的形式的技术。深度学习是一种非常强大的机器学习技术&#xff0c;它在自然语言处理方面也有广泛的应用。本文将详细介绍深度学习在自然语言处理方面的应用。 自然语言处理的基本步骤 自然语言处理的基本步骤包括分词、词…

智能驾驶汽车的系统开发与验证软件PreScan2022版

Simcenter Prescan是基于物理学的最佳仿真平台&#xff0c;在桌面、集群和云端&#xff0c;用于智能驾驶车辆开发&#xff0c;于2022年三月宣布发布2022.1版本。Simcenter Prescan 2022.1引入了与SUMO的协同仿真&#xff0c;用于自动生成交通&#xff0c;并配有程序化的脚本&am…

RestTemplate使用详解

文章目录 1.1 RestTemplate环境准备1&#xff09;背景说明2&#xff09;工程配置RestTemplate 1.2 RestTemplate API入门-11&#xff09;get请求携带参数访问外部url2&#xff09;get请求响应数据自动封装vo实体对象3&#xff09;请求头携带参数访问外部接口 1.3 RestTemplate …

Spring:AOP 的详细内容

文章目录 Spring&#xff1a;Day 03AOP一、概述二、搭建环境三、实现 AOP1. 方式一&#xff1a;使用原生 Spring 的 API 接口2. 方式二&#xff1a;自定义类&#xff08;切面&#xff09;3. 方式三&#xff1a;注解 四、总结 Spring&#xff1a;Day 03 AOP 一、概述 AOP&…

相见恨晚的5款良心软件,每款都是经过时间检验的精品

今天来给大家推荐5款良心软件,每款都是经过时间检验的精品,用起来让你的工作效率提升飞快&#xff0c;各个都让你觉得相见恨晚&#xff01; 1.颜色选择器——ColorPicker ColorPicker是一款用于在屏幕上选择颜色的工具。它可以让你快速地获取任意像素的颜色值,并复制到剪贴板…

信息收集-子域名

&#xff08;一&#xff09;、子域名 子域名是父域名的下一级&#xff0c;比如“huiyuan.xxx.com”和“bbs.xxx.com”这两个域名是“xxx.com”的子域名&#xff0c; 企业可能有多个、几十个甚至更多的子域名应用&#xff0c;因为子域名数量多&#xff0c;企业子域名应用的防护…

设计模式之【迭代器模式】,对集合访问的统一

文章目录 一、什么是迭代器模式1、迭代器模式使用场景2、迭代器模式的优势3、迭代器模式的四种角色4、迭代器模式的优缺点 二、手写一个迭代器1、迭代器模式的一般写法2、课程迭代器 三、源码中的迭代器1、ArrayList 一、什么是迭代器模式 迭代器模式&#xff08;Iterator Pat…

(5.12-5.18)【大数据新闻速递】

关 注gzh“大数据食铁兽”&#xff0c;了解更多大数据快讯 【打造全国首个数据要素产业集聚区&#xff01;浦东数据要素产业规模2025年将达1000亿元】 5月16日&#xff0c;“数启浦东”2023浦东新区数据要素产业主题系列活动启动。记者获悉&#xff0c;《张江数据要素产业集聚…

【实用工具】Guava EventBus(事件总线)快速入门

介绍 EventBus是Guava的事件处理机制&#xff0c;是设计模式中的观察者模式&#xff08;生产/消费者编程模型&#xff09;的优雅实现。对于事件监听和发布订阅模式&#xff0c;EventBus是一个非常优雅和简单解决方案&#xff0c;我们不用创建复杂的类和接口层次结构。 Java案…

树莓派报错Oops - unable to determine board type . . .model:17

报错原因 改变方法 cd /tmp wget https://project-downloads.drogon.net/wiringpi-latest.deb sudo dpkg -i wiringpi-latest.deb成功

技术的力量:如何用数据驱动实现设备健康管理

在当今数字化时代&#xff0c;设备管理部门面临着日益复杂的挑战。传统的设备管理方法已经无法满足快速发展的需求&#xff0c;而数字化转型成为了提升效率、降低成本、增强竞争力的关键路径。 本文将介绍设备管理部门数字化转型的痛点、可参考的蓝图规划以及PreMaint平台在实现…