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
的内存。这段程序会由于b
比a
短而被拒绝通过编译:被引用对象的存在范围短于引用者。
(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。
相关文章:
Rust之泛型、trait与生命周期
泛型是具体类型或其他属性的抽象替代。在编写代码时,可以直接描述泛型的行为,或者它与其他泛型产生的联系,而无须知晓它在编译和运行代码时采用的具体类型。 1、泛型数据类型: 们可以在声明函数签名或结构体等元素时使用泛型&am…...

GPU Microarch 学习笔记 [1]
WARP GPU的线程从thread grid 到thread block,一个thread block在CUDA Core上执行时,会分成warp执行,warp的颗粒度是32个线程。比如一个thread block可能有1024个线程,分成32个warp执行。 上图的CTA(cooperative thre…...

Transformer(一)简述(注意力机制,NLP,CV通用模型)
目录 1.Encoder 1.1简单理解Attention 1.2.什么是self-attention 1.3.怎么计算self-attention 1.4.multi-headed(q,k,v不区分大小写) 1.5.位置信息表达 2.Decoder(待补充) 3.BERT 参考文献 1.Encode…...

回归预测 | MATLAB实现BiLSTM双向长短期记忆神经网络多输入多输出预测
回归预测 | MATLAB实现BiLSTM双向长短期记忆神经网络多输入多输出预测 目录 回归预测 | MATLAB实现BiLSTM双向长短期记忆神经网络多输入多输出预测预测效果基本介绍程序设计往期精彩参考资料 预测效果 基本介绍 MATLAB实现BiLSTM双向长短期记忆神经网络多输入多输出预测&#x…...

使用Dockker创建vwas容器时报错的解决方法
执行命令 docker run -it -d -p 13443:3443 --cap-add LINUX_IMMUTABLE secfa/docker-awvs没有详细看报错之前找了各种各样的解决办法,都无法解决。因此以后在看报错提示的时候耐心一点看关键词Error 后来才发现启动vwas时docker报了这个错: OSError: …...

【数据结构OJ题】链表分割
原题链接:https://www.nowcoder.com/practice/0e27e0b064de4eacac178676ef9c9d70?tpId8&&tqId11004&rp2&ru/activity/oj&qru/ta/cracking-the-coding-interview/question-ranking 目录 1. 题目描述 2. 思路分析 3. 代码实现 1. 题目描述 2…...
无感知发布
什么是无感知发布 "无感知发布"是指在软件系统或应用程序进行更新或升级时,尽可能地避免对用户或系统的正常运行产生影响或中断。这种发布方式通常采用一系列技术和策略,以确保新版本的软件可以平滑地替代旧版本,而不会造成用户的…...

C++ 虚继承
C棱形继承 在 C 中,在使用 多继承 时,如果发生了如果类 A 派生出类 B 和类 C,类 D 继承自类 B 和类 C,这时候就发生了菱形继承。 如果发生了菱形继承,这个时候类 A 中的 成员变量 和 成员函数 继承到类 D 中变成了两…...
git commit用法
git commit 是 Git 版本控制系统中的一个命令,用于将更改提交到本地存储库。以下是 git commit 的一些常见用法和选项: 基本用法: git commit -m "提交信息"使用 -m 选项可以直接在命令行中添加提交信息。 提交所有更改: git commit -a -m &q…...

【LeetCode】543.二叉树的直径
题目 给你一棵二叉树的根节点,返回该树的 直径 。 二叉树的 直径 是指树中任意两个节点之间最长路径的 长度 。这条路径可能经过也可能不经过根节点 root 。 两节点之间路径的 长度 由它们之间边数表示。 示例 1: 输入:root [1,2,3,4,5]…...
TypeScript教程(五)条件语句,循环,函数
一、条件语句 条件语句基于不同的条件来执行不同的动作 1.if语句:只有当指定条件为true时,使用该语句来执行代码 2.if...else语句:当条件为true时执行代码,当条件为else时执行其他代码 3.if...else if...else语句:…...

vue使用jsplumb 流程图
安装jsPlumb库:在Vue项目中使用npm或yarn安装jsPlumb库。 npm install jsplumb 创建一个Vue组件:创建一个Vue组件来容纳jsPlumb的功能和呈现。 <template><div style"margin: 20px"><div style"margin: 20px">&l…...
【BASH】回顾与知识点梳理(二十八)
【BASH】回顾与知识点梳理 二十八 二十八. 例行性工作排程(crontab)28.1 什么是例行性工作排程Linux 工作排程的种类: at, cronCentOS Linux 系统上常见的例行性工作 28.2 仅执行一次的工作排程atd 的启动at 的运作方式实际运作单一工作排程at 工作的管理batch&…...

LangChain源码逐行解密之系统(二)
LangChain源码逐行解密之系统 20.2 serapi.py源码逐行剖析 我们可以看一下Google查询的例子,在LangChain中有多种实现的方式。 如图20-5所示,在utilities的serpapi.py代码文件中实现了SerpAPIWrapper。 图20- 5 utilities的serpapi.py的SerpAPIWrapper 在langchain目录的se…...

QT的设计器介绍
设计器介绍 Qt制作 UI 界面,一般可以通过UI制作工具QtDesigner和纯代码编写两种方式来实现。纯代码实现暂时在这里不阐述了在后续布局章节详细说明,QtDesigner已经继承到开发环境中,在工程中直接双击ui文件就可以直接在QtDesigner设计器中打…...

[LitCTF 2023]Ping
因为直接ping会有弹窗。这里在火狐f12,然后f1选禁用javascript,然后ping 然后输入127.0.0.1;cat /flag 得到flag, 查看其他大佬的wp ,这里还可以抓包。但是不知道为什么我这里的burp 用不了...

Spring Cloud面试突击班1
Spring Cloud面试突击班1 1.Spring Cloud 中有哪些组件,整个项目架构中我们的重点又有哪些? Spring Cloud 是一套基于Spring Boot的微服务解决方案。 Spring Cloud生态在国内主流的分为两套,一套是以奈飞开源的Spring Cloud Netfilx 20%&a…...

线上售楼vr全景看房成为企业数字化营销工具
在房地产业中,VR全景拍摄为买家提供了虚拟看房的全新体验。买家可以通过相关设备,远程参观各个楼盘的样板间和实景,感受房屋的空间布局和环境氛围,极大地提高了购房决策的准确性。对于房地产开发商和中介机构来说,VR全…...
“深入探索JVM内部机制:解密Java虚拟机原理“
标题:深入探索JVM内部机制:解密Java虚拟机原理 摘要:本文将深入探索Java虚拟机(JVM)的内部机制,揭示其工作原理和关键组成部分,包括类加载、内存管理、垃圾回收、即时编译和运行时数据区域等。…...
最长 上升子序列
大家好 我是寸铁 希望这篇题解对你有用,麻烦动动手指点个赞或关注,感谢您的关注 不清楚蓝桥杯考什么的点点下方👇 考点秘籍 想背纯享模版的伙伴们点点下方👇 蓝桥杯省一你一定不能错过的模板大全(第一期) 蓝桥杯省一你一定不…...

网络编程(Modbus进阶)
思维导图 Modbus RTU(先学一点理论) 概念 Modbus RTU 是工业自动化领域 最广泛应用的串行通信协议,由 Modicon 公司(现施耐德电气)于 1979 年推出。它以 高效率、强健性、易实现的特点成为工业控制系统的通信标准。 包…...

未来机器人的大脑:如何用神经网络模拟器实现更智能的决策?
编辑:陈萍萍的公主一点人工一点智能 未来机器人的大脑:如何用神经网络模拟器实现更智能的决策?RWM通过双自回归机制有效解决了复合误差、部分可观测性和随机动力学等关键挑战,在不依赖领域特定归纳偏见的条件下实现了卓越的预测准…...

地震勘探——干扰波识别、井中地震时距曲线特点
目录 干扰波识别反射波地震勘探的干扰波 井中地震时距曲线特点 干扰波识别 有效波:可以用来解决所提出的地质任务的波;干扰波:所有妨碍辨认、追踪有效波的其他波。 地震勘探中,有效波和干扰波是相对的。例如,在反射波…...
论文解读:交大港大上海AI Lab开源论文 | 宇树机器人多姿态起立控制强化学习框架(二)
HoST框架核心实现方法详解 - 论文深度解读(第二部分) 《Learning Humanoid Standing-up Control across Diverse Postures》 系列文章: 论文深度解读 + 算法与代码分析(二) 作者机构: 上海AI Lab, 上海交通大学, 香港大学, 浙江大学, 香港中文大学 论文主题: 人形机器人…...
java_网络服务相关_gateway_nacos_feign区别联系
1. spring-cloud-starter-gateway 作用:作为微服务架构的网关,统一入口,处理所有外部请求。 核心能力: 路由转发(基于路径、服务名等)过滤器(鉴权、限流、日志、Header 处理)支持负…...

简易版抽奖活动的设计技术方案
1.前言 本技术方案旨在设计一套完整且可靠的抽奖活动逻辑,确保抽奖活动能够公平、公正、公开地进行,同时满足高并发访问、数据安全存储与高效处理等需求,为用户提供流畅的抽奖体验,助力业务顺利开展。本方案将涵盖抽奖活动的整体架构设计、核心流程逻辑、关键功能实现以及…...
React Native 导航系统实战(React Navigation)
导航系统实战(React Navigation) React Navigation 是 React Native 应用中最常用的导航库之一,它提供了多种导航模式,如堆栈导航(Stack Navigator)、标签导航(Tab Navigator)和抽屉…...
日语学习-日语知识点小记-构建基础-JLPT-N4阶段(33):にする
日语学习-日语知识点小记-构建基础-JLPT-N4阶段(33):にする 1、前言(1)情况说明(2)工程师的信仰2、知识点(1) にする1,接续:名词+にする2,接续:疑问词+にする3,(A)は(B)にする。(2)復習:(1)复习句子(2)ために & ように(3)そう(4)にする3、…...
【Linux】C语言执行shell指令
在C语言中执行Shell指令 在C语言中,有几种方法可以执行Shell指令: 1. 使用system()函数 这是最简单的方法,包含在stdlib.h头文件中: #include <stdlib.h>int main() {system("ls -l"); // 执行ls -l命令retu…...

【SQL学习笔记1】增删改查+多表连接全解析(内附SQL免费在线练习工具)
可以使用Sqliteviz这个网站免费编写sql语句,它能够让用户直接在浏览器内练习SQL的语法,不需要安装任何软件。 链接如下: sqliteviz 注意: 在转写SQL语法时,关键字之间有一个特定的顺序,这个顺序会影响到…...