Rust之泛型、trait与生命周期

泛型是具体类型或其他属性的抽象替代。在编写代码时,可以直接描述泛型的行为,或者它与其他泛型产生的联系,而无须知晓它在编译和运行代码时采用的具体类型。

1、泛型数据类型:

们可以在声明函数签名或结构体等元素时使用泛型,并在随后搭配不同的具体类型来使用这些元素。

(1)、在函数定义中:

当使用泛型来定义一个函数时,需要将泛型放置在函数签名中通常用于指定参数和返回值类型的地方。以这种方式编写的代码更加灵活,并可以在不引入重复代码的同时向函数调用者提供更多的功能。
当需要在函数签名中使用类型参数时,也需要在使用前声明这个类型参数的名称。为了定义泛型版本的largest函数,类型名称的声明必须被放置在函数名与参数列表之间的一对尖括号<>中,如下所示:

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

即函数largest拥有泛型参数T,它接收一个名为list的T值切片作为参数,并返回一个同样拥有类型T的值作为结果。

(2)、在结构体定义中:

可以使用<>语法来定义在一个或多个字段中使用泛型的结构体。示例:

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时仅使用了一个泛型,这个定义表明Point结构体对某个类型T是通用的。而无论具体的类型是什么,字段x与y都同时属于这个类型。但是使用不同的值类型来创建Point实例,那么代码是无法通过编译的。示例:

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

这段程序无法编译通过。字段x和y必须是相同的类型,因为它们拥有相同的泛型T。
为了在保持泛型状态的前提下,让Point结构体中的x和y能够被实例化为不同的类型,可以使用多个泛型参数。示例:

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

(3)、在枚举定义中:

枚举定义也可以在它们的变体中存放泛型数据。例如标准库中提供的Option枚举:

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

Option是一个拥有泛型T的枚举。它拥有两个变体:持有T类型值的Some变体,以及一个不持有任何值的None变体。Option被用来表示一个值可能存在的抽象概念。也正是因为Option使用了泛型,所以无论这个可能存在的值是什么类型,都可以通过Option来表达这一抽象。
枚举同样也可以使用多个泛型参数。例如的Result枚举:

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

Result枚举拥有两个泛型:T和E。它也同样拥有两个变体:持有T类型值的Ok,以及一个持有E类型值的Err。这个定义使得Result枚举可以很方便地被用在操作可能成功(返回某个T类型的值),也可能失败(返回某个E类型的错误)的场景。

(4)、在方法定义中:

方法也可以在自己的定义中使用泛型。例如结构体Point实现了一个名为x的方法:

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定义了一个名为x的方法,它会返回一个指向字段x中数据的引用。
注意,必须紧跟着impl关键字声明T,以便能够在实现方法时指定类型Point。通过在impl之后将T声明为泛型,Rust能够识别出Point尖括号内的类型是泛型而不是具体类型。

(5)、泛型代码的性能问题:

Rust实现泛型的方式决定了使用泛型的代码与使用具体类型的代码相比不会有任何速度上的差异。
Rust会在编译时执行泛型代码的单态化(monomorphization)。单态化是一个在编译期将泛型代码转换为特定代码的过程,它会将所有使用过的具体类型填入泛型参数从而得到有具体类型的代码。
在这个过程中,编译器会寻找所有泛型代码被调用过的地方,并基于该泛型代码所使用的具体类型生成代码。

2、trait:定义共享行为:

trait(特征)被用来向Rust编译器描述某些特定类型拥有且能够被其他类型共享的功能,它使我们可以以一种抽象的方式来定义共享特征。还可以使用trait约束泛型参数指定为实现了某些特定行为的类型。

(1)、定义trait:

类型的行为由该类型本身可供调用的方法组成。当在不同的类型上调用了相同的方法时,就称这些类型共享了相同的行为。trait提供了一种将特定方法签名组合起来的途径,它定义了达成某种目的所必需的行为集合。示例:

pub trait Summary {fn summarize(&self) -> String;
}

这里,我们使用了trait关键字来声明tait,紧随关键字的是该trait的名字。在其后的花括号中,声明了用于定义类型行为的方法签名。在方法签名后,省略了花括号及具体的实现,直接使用分号终结了当前的语句。任何想要实现这个trait的类型都需要为上述方法提供自定义行为。编译器会确保每一个实现了Summary trait的类型都定义了与这个签名完全一致的summarize方法。
一个trait可以包含多个方法:每个方法签名占据单独一行并以分号结尾。

(2)、为类型实现trait:

基于Summary trait定义了所期望的行为,现在就可以在多媒体聚合中依次为每个类型实现这个trait了。示例:

pub struct NewsArticle {pub headline: String,pub location: String,pub author: String,pub content: String,
}
impl Summary for NewsArticle {fn summarize(&self) -> String {format!("{}, by {} ({})", self.headline, self.author, self.location)}
}
pub struct Tweet {pub username: String,pub content: String,pub reply: bool,pub retweet: bool,
}
impl Summary for Tweet {fn summarize(&self) -> String {format!("{}: {}", self.username, self.content)}
}

为类型实现trait与实现普通方法的步骤十分类似。它们的区别在于我们必须在impl关键字后提供我们想要实现的trait名,并紧接for关键字及当前的类型名。在impl代块中,我们同样需要填入trait中
的方法签名。但在每个签名的结尾不再使用分号,而是使用花括号并在其中编写函数体来为这个特定类型实现该trait的方法所应具有的行为。
一旦实现了trait,我们便可以基于NewsArticle和Tweet的实例调用该trait的方法了,正如我们调用普通方法一样。示例:

let tweet = Tweet {username: String::from("horse_ebooks"),content: String::from("of course, as you probably already know, people"),reply: false,retweet: false,
};
println!("1 new tweet: {}", tweet.summarize());

注意,实现trait有一个限制:只有当trait或类型定义于我们的库中时,我们才能为该类型实现对应的trait。
我们不能为外部类型实现外部trait。例如,我们不能在aggregator库内为Vec实现Display trait,因为Display与Vec都被定义在标准库中,而没有定义在aggregator库中。这个限制被称为孤儿规则 (orphan rule),之所以这么命名是因为它的父类型没有定义在当前库中。这一规则也是程序一致性 (coherence)的组成部分,它确保了其他人所编写的内容不会破坏到你的代码,反之亦
然。如果没有这条规则,那么两个库可以分别对相同的类型实现相同的trait,Rust将无法确定应该使用哪一个版本。

(3)、默认实现:

有些时候,为trait中的某些或所有方法都提供默认行为非常有用,它使我们无须为每一个类型的实现都提供自定义行为。当我们在为某个特定类型实现trait时,可以选择保留或重载每个方法的默认行为。示例:

pub trait Summary {fn summarize(&self) -> String {String::from("(Read more...)")}
}

假如我们决定在NewsArticle的实例中使用这种默认实现而不是自定义实现,那么我们可以指定一个空的impl代码块:impl Summaryfor NewsArticle {}
为summarize提供一个默认实现并不会影响为Tweet实现Summary时所编写的代码。这是因为重载默认实现与实现trait方法的语法完全一致。
还可以在默认实现中调用相同trait中的其他方法,哪怕这些方法没有默认实现。基于这一规则,trait可以在只需要实现一小部分方法的前提下,提供许多有用的功能。示例:

pub trait Summary {fn summarize_author(&self) -> String;fn summarize(&self) -> String {format!("(Read more from {}...)", self.summarize_author())}
}
impl Summary for Tweet {fn summarize_author(&self) -> String {format!("@{}", self.username)}
}
let tweet = Tweet {username: String::from("horse_ebooks"),content: String::from("of course, as you probably already know, people"),reply: false,retweet: false,
};
println!("1 new tweet: {}", tweet.summarize());

这 段 代 码 会 打 印 出 1 new tweet: (Read@horse_ebooks...)

(4)、使用trait作为参数:

前面我们为NewsArticle 与 Tweet 类 型 实 现 了Summary trait。我们可以定义一个notify函数来调用其item参数的summarize方法,这里的参数item可以是任何实现了Summary trait的类型。

pub fn notify(item: impl Summary) {println!("Breaking news! {}", item.summarize());
}

我们没有为item参数指定具体的类型,而是使用了impl关键字及对应的trait名称。这一参数可以接收任何实现了指定trait的类型。在notify的函数体内,我们可以调用来自Summary trait的任何方法,
当然也包括summarize。我们可以在调用notify时向其中传入任意一个NewsArticle或Tweet实例。假设我们需要接收两个都实现了Summary的参数,那么使用impl Trait的写法如下所示:

pub fn notify(item1: impl Summary, item2: impl Summary) {}

假如notify函数需要在调用summarize方法的同时显示格式化后的item,那么item就必须实现两个不同的trait:Summary和Display。我们可以使用+语法做到这一点:

pub fn notify(item: impl Summary + Display) {}

(5)、返回实现了trait的类型:

同样可以在返回值中使用impl Trait语法,用于返回某种实现了trait的类型:

fn returns_summarizable() -> impl Summary {Tweet {username: String::from("horse_ebooks"),content: String::from("of course, as you probably already know, people"),reply: false,retweet: false,}
}

3、使用生命周期保证引用的有效性:

Rust的每个引用都有自己的生命周期(lifetime),它对应着引用保持有效性的作用域。在大多数时候,生命周期都是隐式且可以被推导出来的,就如同大部分时候类型也是可以被推导的一样。当出现了多个可能的类型时,就必须手动声明类型。

(1)、使用生命周期来避免悬垂引用:

生命周期最主要的目标在于避免悬垂引用,进而避免程序引用到非预期的数据。

(2)、借用检查器:

Rust编译器拥有一个借用检查器 (borrow checker),它被用于比较不同的作用域并确定所有借用的合法性。

fn main() {let r;                // ---------+-- 'a//          |{                     //          |let x = 5;        // -+-- 'b  |r = &x;           //  |       |}                     // -+       |//          |println!("r: {}", r); //          |
}                         // ---------+

在编译过程中,Rust会比较两段生命周期的大小,并发现r拥有生命周期a,但却指向了拥有生命周期b的内存。这段程序会由于ba短而被拒绝通过编译:被引用对象的存在范围短于引用者。

(3)、生命周期标注语法:

生命周期的标注并不会改变任何引用的生命周期长度。如同使用了泛型参数的函数可以接收任何类型一样,使用了泛型生命周期的函数也可以接收带有任何生命周期的引用。在不影响生命周期的前提下,标注本身会被用于描述多个引用生命周期之间的关系。
生命周期的标注使用了一种明显不同的语法:它们的参数名称必须以撇号(')开头,且通常使用全小写字符。与泛型一样,它们的名称通常也会非常简短。'a被大部分开发者选择作为默认使用的名称。我们会将生命周期参数的标注填写在&引用运算符之后,并通过一个空格符来将标注与引用类型区分开来。
单个生命周期的标注本身并没有太多意义,标注之所以存在是为了向Rust描述多个泛型生命周期参数之间的关系。

(4)、函数签名中的生命周期标注:

如同泛型参数一样,我们同样需要在函数名与参数列表之间的尖括号内声明泛型生命周期参数。在这个签名中我们所表达的意思是:参数与返回值中的所有引用都必须拥有相同的生命周期。

fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {if x.len() > y.len() {x} else {y}
}

这段代码的函数签名向Rust表明,函数所获取的两个字符串切片参数的存活时间,必须不短于给定的生命周期'a。这个函数签名同时也意味着,从这个函数返回的字符串切片也可以获得不短于'a的生命周期。
当我们在函数签名中指定生命周期参数时,我们并没有改变任何传入值或返回值的生命周期。我们只是向借用检查器指出了一些可以用于检查非法调用的约束。

(5)、深入理解生命周期:

当函数返回一个引用时,返回类型的生命周期参数必须要与其中一个参数的生命周期参数相匹配。当返回的引用没有 指向任何参数时,那么它只可能是指向了一个创建于函数内部的值,由于这个值会因为函数的结束而离开作用域,所以返回的内容也就变成了悬垂引用。

(6)、结构体定义中的生命周期标注:

struct ImportantExcerpt<'a> {part: &'a str,
}
fn main() {let novel = String::from("Call me Ishmael. Some years ago...");let first_sentence = novel.split('.').next().expect("Could not find a '.'");let i = ImportantExcerpt { part: first_sentence };
}

(7)、方法定义中的生命周期标注:

结构体字段中的生命周期名字总是需要被声明在impl关键字之后,并被用于结构体名称之后,因为这些生命周期是结构体类型的一部分。
在impl代码块的方法签名中,引用可能是独立的,也可能会与结构体字段中的引用的生命周期相关联。另外,生命周期省略规则在大部分情况下都可以帮我们免去方法签名中的生命周期标注。
我们定义一个名为level的方法,它仅有一个指向self的参数,并返回i32类型的值作为结果,这个结果并不会引用任何东西:

impl<'a> ImportantExcerpt<'a> {fn level(&self) -> i32 {3}
}

(8)、静态生命周期:

Rust中还存在一种特殊的生命周期’static,它表示整个程序的执行期。所有的字符串字面量都拥有’static生命周期,示例:

let s: &'static str = "I have a static lifetime.";

字符串的文本被直接存储在二进制程序中,并总是可用的。因此,所有字符串字面量的生命周期都是’static。

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

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

相关文章

TDD(测试驱动开发)?

01、前言 很早之前&#xff0c;曾在网络上见到过 TDD 这 3 个大写的英文字母&#xff0c;它是 Test Driven Development 这三个单词的缩写&#xff0c;也就是“测试驱动开发”的意思——听起来很不错的一种理念。 其理念主要是确保两件事&#xff1a; 确保所有的需求都能被照…

macOS Ventura 13.5.1(22G90)发布(附黑/白苹果系统镜像地址)

系统镜像下载&#xff1a;百度&#xff1a;黑果魏叔 系统介绍 黑果魏叔 8 月 18 日消息&#xff0c;苹果今日向 Mac 电脑用户推送了 macOS 13.5.1 更新&#xff08;内部版本号&#xff1a;22G90&#xff09;&#xff0c;本次更新距离上次发布隔了 24 天。 本次更新重点修复了…

Redis 缓存过期及删除

一、Redis缓存过期策略 物理内存达到上限后&#xff0c;像磁盘空间申请虚拟内存(硬盘与内存的swap),甚至崩溃。 内存与硬盘交换 (swap) 虚拟内存&#xff0c;频繁I0 性能急剧下降&#xff0c;会造成redis内存急剧下降&#xff1b; 一般设置物理内存的3/4&#xff0c;在redis…

内存不足V4L2 申请DMC缓存报错问题

当内存不足时,V4L2可能存在申请DMA缓存报错,如下日志: 13:36:54:125 [15070.640862] rkcifhw fdfe0000.rkcif: swiotlb buffer is full (sz: 1843200 bytes) 13:36:54:125 [15070.640891] rkcifhw fdfe0000.rkcif: swiotlb: coherent allocation failed, size=1843200 13:3…

超分辨率地震速度模型

文献分享 1. Multitask Learning for Super-Resolution 原题目&#xff1a;Multitask Learning for Super-Resolution of Seismic Velocity Model 全波形反演&#xff08;FWI&#xff09;是估算地下速度模型的强大工具。与传统反演策略相比&#xff0c;FWI充分利用了地震波的…

typedef

t y p e d e f typedef typedef 声明&#xff0c;简称typedef&#xff0c;是创建现有类型的新名字。 比如&#xff1a; #include <bits/stdc.h> using namespace std; typedef long long ll; int main() {ll n;scanf("%lld",&n);printf("%lld"…

C++ 面向对象三大特性——多态

✅<1>主页&#xff1a;我的代码爱吃辣 &#x1f4c3;<2>知识讲解&#xff1a;C 继承 ☂️<3>开发环境&#xff1a;Visual Studio 2022 &#x1f4ac;<4>前言&#xff1a;面向对象三大特性的&#xff0c;封装&#xff0c;继承&#xff0c;多态&#xff…

30W IP网络有源音箱 校园广播音箱

SV-7042XT是深圳锐科达电子有限公司的一款2.0声道壁挂式网络有源音箱&#xff0c;具有10/100M以太网接口&#xff0c;可将网络音源通过自带的功放和喇叭输出播放&#xff0c;可达到功率30W。同时它可以外接一个30W的无源副音箱&#xff0c;用在面积较大的场所。5寸进口全频低音…

RNN模型简单理解和CNN区别

目录 神经网络&#xff1a;水平方向延伸&#xff0c;数据不具有关联性 ​ RNN&#xff1a;在神经网络的基础上加上了时间顺序&#xff0c;语义理解 ​RNN: 训练中采用梯度下降&#xff0c;反向传播 ​ 长短期记忆模型 ​输出关系&#xff1a;1 toN&#xff0c;N to N 单入…

Spring三级缓存

目录 循环依赖问题 三级缓存 三级缓存创建Bean的流程&#xff08;解决循环依赖问题&#xff09; 三级缓存的局限性 Spring的三级缓存是为了解决单例Bean的循环依赖问题而存在的。 循环依赖问题 简单来说就是A依赖B&#xff0c;而B又依赖A。即创建A的时候&#xff0c;需要先…

【HarmonyOS】【DevEco Studio】ohpm安装失败该如何解决?

【关键词】 HarmonyOS、DevEco Studio、ohpm安装失败 【问题背景及解决方案】 最近遇到很多DevEco Studio安装ohpm失败的问题&#xff0c;下面给大家介绍几种出现的问题以及解决方案&#xff1a; 1、ohpm not set up&#xff0c;报错截图如下&#xff1a; ​ 解决方案&…

一百六十、Kettle——Linux上安装的Kettle9.2.0连接Hive3.1.2

一、目标 Kettle9.2.0在Linux上安装好后&#xff0c;需要与Hive3.1.2数据库建立连接 之前已经在本地上用kettle9.2.0连上Hive3.1.2 二、各工具版本 &#xff08;一&#xff09;kettle9.2.0 kettle9.2.0安装包网盘链接 链接&#xff1a;https://pan.baidu.com/s/15Zq9w…

C++中class嵌套时构造函数,析构函数调用的顺序

#include<iostream> using namespace std; class Phone { public:Phone(string pname){m_pnamepname;cout<<"phone的构造函数调用"<<endl;}~Phone(){cout<<"Phone的析构函数调用"<<endl;}string m_pname; }; class Person {…

网安周报|Monti Ransomware团伙推出了一个新的Linux加密器

Monti Ransomware团伙推出了一个新的Linux加密器 经过两个月的休息&#xff0c;Monti 勒索软件运营商带着新的 Linux 版本的加密器返回。该变体被用于针对政府和法律部门组织的攻击。研究人员注意到两个团伙的TTP之间有多个相似之处&#xff0c;Monti运营商还基于Conti泄露的源…

2023 Robocom 游记+题解

Robocom赛前一天熬夜打了一场edu,全程眯眯眼&#xff0c;三题滚粗了&#xff0c;前三题花了一小时才写完&#xff0c;第四题写了一小时也没写明白&#xff0c;好像预示着Robocom的结局&#xff1f; 早上七点醒了&#xff0c;感觉自己浑身无力&#xff0c;想睡觉但是又睡不着的…

AutoSAR配置与实践(基础篇)3.3 BSW的通信功能

传送门 -> AUTOSAR配置与实践总目录 AutoSAR配置与实践&#xff08;基础篇&#xff09;3.3 BSW的通信功能 一、收发过程概览1.1 发送过程概览1.2 接收过程概览 二、BSW的通信功能模块组成三、收发过程解析3.1 发送过程3.2 发送后的结果确认3.3 接收过程 一、收发过程概览 1…

Airbnb开源数据可视化工具Visx

一、什么是visx visx 是用于 React 的富有表现力的底层可视化组件集合,结合了 d3 的强大功能来生成可视化,以及 React 更新 DOM 的诸多优势。 在 Airbnb 内部,visx 的目标是统一整个公司的可视化堆栈,在此过程中,创建了 visx 项目,从而有效的将 D3 的强大功能与 React …

内核调试之devmem直接读写寄存器

今天分享一个内核调试实用工具——devmem。 相信很多做底层驱动的人都会经常用到。 什么是devmem&#xff1f; 在Linux系统&#xff0c;如果我们想要访问某个寄存器&#xff0c;就需要写一个驱动程序&#xff0c;在驱动中映射寄存器地址&#xff0c;转为虚拟地址后就可以访问…

windows电脑系统自带的画图工具如何实现自由拼图

1.首先选中你要拼接的第一张图片&#xff0c;右键选着编辑&#xff0c;会自动打开自带的画图工具 然后就是打开第一张图片&#xff0c;如下图所示 接着就是将画布托大&#xff0c;如下图所示。 然后点击选择&#xff0c;选择下面的空白区域&#xff0c;选着区域的范围要比准备拼…

05-微信小程序常用组件-表单组件

05-微信小程序常用组件-表单组件 文章目录 表单组件button 按钮案例代码 form 表单案例代码 image 图片支持长按识别的码案例代码 微信小程序包含了六大组件&#xff1a; 视图容器、 基础内容、 导航、 表单、 互动和 导航。这些组件可以通过WXML和WXSS进行布局和样式设…