Rust特征(Trait)
特征(Trait)
特征(trait)是rust中的概念,类似于其他语言中的接口(interface)。在之前的代码中,我们也多次见过特征的使用,例如 #[derive(Debug)]
,它在我们定义的类型(struct)上自动派生 Debug 特征,接着可以使用 println!("{:?}", x)
打印这个类型。
特征定义了一个可以被共享的行为,只要实现了特征,你就能使用该行为。
定义特征
如果不同的类型具有相同的行为,那么我们就可以定义一个特征,然后为这些类型实现该特征。定义特征是把一些方法组合在一起,目的是定义一个实现某些目标所必需的行为的集合。
例如,我们现在有圆形和长方形两个结构体,它们都可以拥有周长,面积。因此我们可以定义被共享的行为,只要实现了特征就可以使用。
pub trait Figure { // 为几何图形定义名为Figure的特征fn girth(&self) -> u64; // 计算周长fn area(&self) -> u64; // 计算面积
}
这里使用 trait 关键字来声明一个特征,Figure 是特征名。在大括号中定义了该特征的所有方法,在这个例子中有两个方法,分别是fn girth(&self) -> u64;
和fn area(&self) -> u64;
,特征只定义行为看起来是什么样的,而不定义行为具体是怎么样的。因此,我们只定义特征方法的签名,而不进行实现,此时方法签名结尾是 ;,而不是一个 {}。
接下来,每一个实现这个特征的类型都需要具体实现该特征的相应方法,编译器也会确保任何实现 Figure 特征的类型都拥有与fn girth(&self) -> u64;
和fn area(&self) -> u64;
签名的定义完全一致的方法。
熟悉C++的同学看到这里,会觉得trait和C++的纯虚函数非常类似;而熟悉go语言的同学看到这里会觉得和go语言的interface非常类似。
实现特征
上面声明了特征,但是它只包含了一个函数声明,而没有实现。接下来要为具体的类型实现特征。
use std::f64::consts::PI;trait Figure { // 为几何图形定义名为Figure的特征fn girth(&self) -> f64; // 计算周长fn area(&self) -> f64; // 计算面积
}
struct Rectangle{x:f64,y:f64,
}struct Circle{r:f64,
}impl Rectangle {// 为Rectangle实现构造方法fn new(x:f64, y:f64) -> Self{ Rectangle { x, y }}
}impl Circle {// 为Circle实现构造方法fn new(r:f64) -> Self{Circle { r }}
}impl Figure for Rectangle {fn area(&self) -> f64 {self.x * self.y}fn girth(&self) -> f64 {2.0 * (self.x + self.y)}
}impl Figure for Circle{fn area(&self) -> f64 {2.0 * PI * self.r}fn girth(&self) -> f64 {PI * self.r.powi(2i32)}
}fn main() {let rec = Rectangle::new(1.0, 2.0);let cir = Circle::new(3.0);println!("长方形的周长是{},面积是{}",rec.girth(), rec.area());println!("圆形的周长是{},面积是{}", cir.girth(), cir.area());
}
impl Figure for Circle
,意为“为 Circle 类型实现 Figure 特征”,然后在 impl 中实现该特征的具体方法。这种将接口分离出来的做法有别于传统面向对象的语言(例如C++)。这种做法是组合优于继承的一种体现。
这段代码执行结果如下所示:
长方形的周长是6,面积是2
圆形的周长是28.274333882308138,面积是18.84955592153876
特征定义与实现的位置(孤儿规则)
如果想让别人使用我们的特征,那么需要将特征和对应的类型定义为pub,例如:
pub trait Figure { // 为几何图形定义名为Figure的特征fn girth(&self) -> f64; // 计算周长fn area(&self) -> f64; // 计算面积
}
pub struct Rectangle{pub x:f64,pub y:f64,
}pub struct Circle{pub r:f64,
}
关于特征实现与定义的位置,有一条非常重要的原则:如果你想要为类型 A 实现特征 T,那么 A 或者 T 至少有一个是在当前作用域中定义的!
但是你无法在当前作用域中,为 String 类型实现 Display 特征,因为它们俩都定义在标准库中,其定义所在的位置都不在当前作用域,跟你半毛钱关系都没有,看看就行了。该规则被称为孤儿规则,可以确保其它人编写的代码不会破坏你的代码,也确保了你不会莫名其妙就破坏了风马牛不相及的代码。
特征的默认实现
你可以在特征中定义具有默认实现的方法,这样其它类型无需再实现该方法,或者也可以选择重载该方法。这和C++的虚函数类似,允许方法有实现,而不仅仅是声明。例如:
pub struct Dog;
pub struct Cat;pub trait Animal {fn run(&self) {println!("跑跑跑");}
}impl Animal for Dog { // 为Dog实现特征Animal}impl Animal for Cat { // 为Cat实现特征Animalfn run(&self) {println!("猫在跑");}
}fn main() {let cat = Cat;let dog = Dog;dog.run();cat.run();
}
程序执行结果:
跑跑跑
猫在跑
在特征中定义具有默认实现的方法,这样其它类型无需再实现该方法,或者也可以选择重载该方法。Dog类型使用默认的run方法,而Cat类型选择重载了run方法。
默认实现允许调用相同特征中的其他方法,哪怕这些方法没有默认实现。如此,特征可以提供很多有用的功能而只需要实现指定的一小部分内容。例如:
pub struct Dog;
pub struct Cat;pub trait Animal {fn run(&self) {println!("跑跑跑");}fn eat(&self);fn sleep(&self);fn every_daya(&self) {self.eat();self.sleep();}
}impl Animal for Dog { // 为Dog实现特征Animalfn eat(&self) {println!("狗需吃狗粮");}fn sleep(&self) {println!("狗在睡觉");}
}impl Animal for Cat { // 为Cat实现特征Animalfn run(&self) {println!("猫在跑");}fn eat(&self) {println!("猫咪需要吃猫粮");}fn sleep(&self) {println!("猫咪白天在睡觉");}
}fn main() {let cat = Cat;let dog = Dog;dog.run();cat.run();dog.every_daya();cat.every_daya();
}
我们给Animal特征加上了sleep,eat以及every_day这三个方法。其中every_day方法,我们做了实现。而sleep方法和eat方法没有在特征中做默认实现,但是这不影响我们在every_day方法中调用它们。我们分别为Cat和Dog类型实现了eat方法和sleep方法。
这和C++的虚基类非常类似,在C++中我们需要做的是在派生类中实现纯虚函数或者重载虚函数,这里用的手段是继承。而在rust里可以由特征来实现。
使用特征作为函数参数
特征如果仅仅是用来实现方法,那真的有些大材小用,现在我们来讲下,真正可以让特征大放光彩的地方。
现在,先定义一个函数,使用特征作为函数参数:
pub fn notify(item: &impl Animal) {println!("{}", item.run());
}
impl Animal,它的意思是 实现了Animal特征 的 item 参数。你可以使用任何实现了 Summary 特征的类型作为该函数的参数,同时在函数体内,还可以调用该特征的方法。例如,可以传递 Cat 或 Dog 的实例来作为参数,而其它类型,如 String 或者 i32 的类型则不能用做该函数的参数,因为它们没有实现 Animal 特征。
特征约束(trait bound)
虽然 impl Trait 这种语法非常好理解,但是实际上它只是一个语法糖,在泛型中如下所示:
pub fn ast<T: Summary>(item: &T) {println!("{}", item.a());
}
T: Summary 被称为特征约束,它能够做的对类型进行约束。例如上面的notify函数的参数只要实现了Animal特征即可,那么意味着我们可以传递Dog或者Cat的实例对象。如果我们有下面这样的函数。
fn func(a:&impl Animal, b:&impl Animal){// todo
}
想要函数的两个参数是同一种类型,而不仅仅是实现了Animal特征即可。此时就需要使用特征约束。
fn func<T: Animal>(a:&T, b:&T){// todo
}
像这样的方式就说明了泛型类型T必须实现Animal特征,且a和b必须是同一种类型。
多重约束
除了单个约束条件,我们还可以指定多个约束条件,例如除了让参数实现 Animal 特征外,还可以让参数实现 Display 特征以控制它的格式化输出:
pub fn func(item: &(impl Animal + Display)) {}
当然了,在泛型中使用如下的形式:
pub fn func<T: Animal + Display>(item: &T) {}
Where约束
当特征约束变得很多时,函数的签名将变得很复杂:
fn some_function<T: Display + Clone, U: Clone + Debug>(t: &T, u: &U) -> i32 {}
通过 where约束,可以将其变成下面的形式。
fn some_function<T, U>(t: &T, u: &U) -> i32where T: Display + Clone,U: Clone + Debug
{}
使用特征约束有条件地实现方法或特征
特征约束,可以让我们在指定类型 + 指定特征的条件下去实现方法,例如:
#![allow(unused)]
fn main() {
use std::fmt::Display;struct Pair<T> {x: T,y: T,
}impl<T> Pair<T> {fn new(x: T, y: T) -> Self {Self {x,y,}}
}impl<T: Display + PartialOrd> Pair<T> {fn cmp_display(&self) {if self.x >= self.y {println!("The largest member is x = {}", self.x);} else {println!("The largest member is y = {}", self.y);}}
}
}
cmp_display 方法,并不是所有的 Pair 结构体对象都可以拥有,只有 T 同时实现了 Display + PartialOrd 的 Pair 才可以拥有此方法。 该函数可读性会更好,因为泛型参数、参数、返回值都在一起,可以快速的阅读,同时每个泛型参数的特征也在新的代码行中通过特征约束进行了约束。
也可以有条件地实现特征, 例如,标准库为任何实现了 Display 特征的类型实现了 ToString 特征:
impl<T: Display> ToString for T {// --snip--
}
函数返回impl Trait
fn return_run() -> impl Animal {Cat
}
Cat实现了Animal特征,因此可以用Cat对象的实例作为返回值。要注意的是,虽然我们知道这里是一个 Cat 类型,但是对于 return_run 的调用者而言,他只知道返回了一个实现了 Animal 特征的对象,但是并不知道返回了一个 Cat 类型。
这种 impl Trait 形式的返回值,在一种场景下非常非常有用,那就是返回的真实类型非常复杂,你不知道该怎么声明时(毕竟 Rust 要求你必须标出所有的类型),此时就可以用 impl Trait 的方式简单返回。
通过 derive 派生特征
形如 #[derive(Debug)]
的代码已经出现了很多次,这种是一种特征派生语法,被 derive 标记的对象会自动实现对应的默认特征代码,继承相应的功能。
例如 Debug 特征,它有一套自动实现的默认代码,当你给一个结构体标记后,就可以使用 println!(“{:?}”, s) 的形式打印该结构体的对象。
再如 Copy 特征,它也有一套自动实现的默认代码,当标记到一个类型上时,可以让这个类型自动实现 Copy 特征,进而可以调用 copy 方法,进行自我复制。
总之,derive 派生出来的是 Rust 默认给我们提供的特征,在开发过程中极大的简化了自己手动实现相应特征的需求,当然,如果你有特殊的需求,还可以自己手动重载该实现。
调用方法需要引入特征
如果你要使用一个特征的方法,那么你需要将该特征引入当前的作用域中。后续在包和模块中,我们来演示该部分。
参考资料
Rust语言圣经
相关文章:

Rust特征(Trait)
特征(Trait) 特征(trait)是rust中的概念,类似于其他语言中的接口(interface)。在之前的代码中,我们也多次见过特征的使用,例如 #[derive(Debug)],它在我们定义的类型(struct)上自动…...

详解七大排序算法
对于排序算法,是我们在数据结构阶段,必须要牢牢掌握的一门知识体系,但是,对于排序算法,里面涉及到的思路,代码……各种时间复杂度等,都需要我们,记在脑袋瓜里面!…...

Vue+ECharts实现可视化大屏
由于项目需要一个数据大屏页面,所以今天学习了vue结合echarts的图标绘制 首先需要安装ECharts npm install echarts --save因为只是在数据大屏页面绘制图表,所以我们无需把它设置为全局变量。 可以直接在该页面引入echarts,就可以在数据大…...

百度Apollo规划算法——轨迹拼接
百度Apollo规划算法——轨迹拼接引言轨迹拼接1、什么是轨迹拼接?2、为什么要进行轨迹拼接?3、结合Apollo代码为例理解轨迹拼接的细节。参考引言 在apollo的规划算法中,在每一帧规划开始时会调用一个轨迹拼接函数,返回一段拼接轨迹…...

6. unity之脚本
1. 说明 当整个游戏运行起来之后,我们无法再借助鼠标来控制物体,此时可以使用脚本来更改物体的各种姿态,驱动游戏的整体运动逻辑。 2. 脚本添加 首先在Assets目录中,新创建一个Scripts文件夹,在该文件内右键鼠标选择…...

flink-note笔记:flink-state模块中broadcast state(广播状态)解析
github开源项目flink-note的笔记。本博客的实现代码都写在项目的flink-state/src/main/java/state/operator/BroadcastStateDemo.java文件中。 项目github地址: github 1. 广播状态是什么 网上关于flink广播变量、广播状态的讲解很杂。我翻了flink官网发现,实际上在1.15里面…...

vue——预览PDF
下载插件 npm install --save vue-pdf创建组件 <template><div class"ins-submit-docs-content ins-submit-docs-pdf"><div v-if"loading" style"position: absolute; top: 40%; width: 100%;text-align: center;"><el-l…...

数据库复习
什么是数据库系统 数据库系统是指在计算机系统中引入数据库后构成的系统,一般由数据库、数据库管理系统(及其开发工具)、应用系统、数据库管理员和用户构成 数据库系统的特点是什么? 数据结构化数据的共享性高,冗余度低且易扩充数据独立性高数…...

vscode插件推荐
文章目录前言一、vscode插件推荐?1、 Chinese (Simplified) (简体中文) Language Pack for Visual Studio Code2、Auto Close Tag3、Auto Import3、Error Lens4、vscode-icons5、ES7 React/Redux/React-Native snippets6、GitLens — Git supercharged7、JavaScript…...

THUPC2023初赛总结
今天参加了THUPC2023初赛,感觉还行。 比赛本来是11:00-16:00,但出了点问题,比赛延迟了十分钟。 刚开始,我从第一题往后看,寻找简单的题。 过了一会儿,一看排行榜,怎么最后一题全是绿的&#…...

unity知识点小结02
虚拟轴 虚拟轴就是一个数值在-11内的轴,这个数轴上重要的数值就是-1,0和1。当使用按键模拟一个完整的虚拟轴时需要用到两个按键,即将按键1设置为负轴按键,按键2设置为正轴按键。在没有按下任何按键的时候,虚拟轴的数值为0…...

总线(四)Modbus总线 协议
文章目录Modbus技术背景Modbus OSI分布Moudbus分类通讯过程Moudbus协议通信过程以及报文解析RTU 与 ASCII 收发数据区别Modbus技术背景 Modbus是一种串行通信协议。 1971年,Modicon公司首次退出Modbus协议,ModbusRTU和Modbus ASCII诞生于此。 后来施耐德…...

Cadence Allegro 导出Component Report详解
⏪《上一篇》 🏡《总目录》 ⏩《下一篇》 目录 1,概述2,Component Report作用3,Component Report示例4,Component Report导出方法4.1,方法14,2,方法2B站关注“硬小二”浏览更多演示视频 1,...

程序猿成长之路之密码学篇-DES算法详解
DES的算法实现原理详情请见 https://blog.csdn.net/qq_31236027/article/details/128209185 DES算法密钥获取详情请见 https://blog.csdn.net/qq_31236027/article/details/129224730 编码工具类获取详见 https://blog.csdn.net/qq_31236027/article/details/128579451 DES算法…...

maven生命周期、阶段与默认绑定插件梳理
maven生命周期、阶段与默认绑定插件梳理 CSDN博客 码云源码 1.maven生命周期、阶段与默认绑定插件 序号生命周期lifecycle阶段phase默认绑定插件(链接官网)默认绑定插件(链接maven库)说明1cleancleanmaven-clean-pluginmaven-clean-plugin清理2.1buildvalidate——验证2.2b…...

【数学基础】
文章目录『 第1讲 高等数学预备知识 』1.1 函数的概念与特性函数的四种特性【 重要结论 】1.2 函数的图像直角坐标系下的图像极坐标系下的图像参数方程1.3 常用基础知识【 情报#1 】『 第2讲 数列极限 』2.1 引言2.2 求数列极限【 情报#2 】『 第1讲 高等数学预备知识 』 1.1 …...

网上电子商城的设计与实现
技术:Java、JSP等摘要:21 世纪以来,人类经济高速发展,人们的生活发生了日新月异的变化,特别是计算机的应用及普及到经济和社会生活的各个领域。在消费领域,网上购物已经成为大众所接受的一种新型的消费方式…...

2023thupc总结
A 大富翁 很有意思的题 ∑x∈A∑y∈B[x支配y]−∑x∈A∑y∈B[y支配x]−∑x∈Awx\sum_{x\in A}\sum_{y\in B}[x支配y]-\sum_{x\in A}\sum_{y\in B}[y支配x]-\sum_{x\in A}w_x∑x∈A∑y∈B[x支配y]−∑x∈A∑y∈B[y支配x]−∑x∈Awx ∑x∈A∑y[x支配y]−∑x∈A∑y[y支…...

【数据库】MySQL数据库基础
目录 1.数据库: 2.数据库基本操作 2.1 MySQL的运行原理 2.2显示数据库: 2.3创建数据库 2.4使用数据库 2.5删除数据库 3.常见的数据类型 3.1数值类型: 3.2字符型类型 3.3日期类型 4.表的操作 4.1创建表 4.2查看表 4.3删除表 5.汇总…...

grid了解
结构 <div class"grid"><div>1</div><div>2</div><div>3</div><div>4</div><div>5</div><div>6</div><div>7</div><div>8</div><div>9</div>&l…...

2023年全国最新工会考试精选真题及答案13
百分百题库提供工会考试试题、工会考试预测题、工会考试真题、工会证考试题库等,提供在线做题刷题,在线模拟考试,助你考试轻松过关。 81.女职工委员会在()下开展工作。 A.企业工会委员会领导 B.企业工会委员会指导 …...

初识HTML技术
文章目录一、为什么学习前端?二、第一个HTML文件VSCode三. HTML元素四. HTML页面一、为什么学习前端? 我们作为一个后端程序员,为什么还要学习前端,因为我们的终极目的是实现web开发,搭建网站,网站 前端 后端 比如我们随便…...

我们为什么要用消息队列?
消息队列是系统设计中存在时间最长的中间件之一,从系统有通信需求开始,就产生了消息队列。 消息队列的使用场景 在日常系统设计与实现的过程中,下面3种场景会涉及到消息队列: 异步处理流量控制服务解耦 异步处理 典型的应用场…...

Linux进程控制
进程控制fork函数进程终止退出码常见的退出方式进程等待什么是进程等待,为什么要进程等待阻塞与非阻塞进程替换替换原理替换函数执行系统命令执行自己写的程序模拟实现简易的shellfork函数 fork函数是创建一个子进程,之前用过。 #include <unistd.h…...

PMP项目管理引论介绍
目录1. 指南概述和目的1.1 项目管理标准1.2 道德与专业行为规范2 基本要素2.1 项目2.2 项目管理的重要性2.3 项目、项目集、项目组合以及运营管理之间的关系2.3.1 概述2.3.2. 项目组合与项目集管理2.3.3. 运营管理2.3.4. 组织级项目管理和战略2.3.5. 项目管理2.3.6. 运营管理与…...

计算机视觉废钢堆提取问题
计算机视觉废钢堆提取问题 背景介绍 在钢铁炼制中,废钢是非常重要的原料,不同等级废钢对于钢成品影响很大,因此需要对废钢进行正确分类。某废钢料场中,卸料区域布置了多个摄像头,用于拍摄卸料场中废钢堆,…...

判断水仙花数-课后程序(Python程序开发案例教程-黑马程序员编著-第二章-课后作业)
实例5:判断水仙花数 水仙花数是一个3位数,它的每位数字的3次幂之和等于它本身,例如13 53 33 153,153就是一个水仙花数。 本实例要求编写程序,实现判断用户输入的3位数是否为水仙花数的功能。 实例目标 掌握Pytho…...

目标检测: 数据增强代码详解
1. 常见的数据增强 1.1 翻转图像 左右水平翻转 假设图片的宽高为w,h,bdbox左上角A坐标为(x1,y1), 右下角B为(x2,y2)。经过左右水平翻转后,bdbox的左上角A1坐标(w-x2,y1) ,右下角B1坐标为(w-x1,y2)左右水平翻转的代码实现如下:from PIL import Image image = Image.open(i…...

第二讲:ambari编译复盘,如何实现一次性成功编译ambari
上节课我们已经讲解了如何成功编译ambari源码,安装ambari-server rpm包以及成功部署ambari。本节课我们来复盘一下上节课的编译过程,以及思考如何实现一次性成功编译ambari。 要想一次性成功编译ambari,那么就需要将预置工作做好,比如: maven镜像源配置,node_moudle模块…...

Windows下jdk安装与卸载-超详细的图文教程
jdk安装 下载jdk 由于现在主流就是jdk1.8,所以这里就下载jdk1.8进行演示。官方下载地址:https://www.oracle.com/java/technologies/downloads/#java8-windows。 官方下载需要注册oracle账号,国内下载有可能速度慢,若不想注册账…...