Rust Trait 学习

概述

特征(trait)是rust中的概念,类似于其他语言中的接口(interface)。特征定义了一个可以被共享的行为,只要实现了特征,你就能使用该行为。
如果不同的类型具有相同的行为,那么我们就可以定义一个特征,然后为这些类型实现该特征。定义特征是把一些方法组合在一起,目的是定义一个实现某些目标所必需的行为的集合。例如,我们现在有圆形和长方形两个结构体,它们都可以拥有周长,面积。因此我们可以定义被共享的行为,只要实现了特征就可以使用。

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;签名的定义完全一致的方法。

Rust语言中的trait是非常重要的概念。在Rust中,trait这一个概念承 担了多种职责。很类似Go中的interface,但trait职责远比interface更多。trait中可以包含:函数、常量、类型等。

1,成员方法

们在特质中定义了一个成员方法,代码如下:

trait Shape {
fn area(&self) -> f64;
}

所有的trait中都有一个隐藏的类型Self(大写S),代表当前这个实 现了此trait的具体类型。

trait中定义的函数,也可以称作关联函数 (associated function)。

函数的第一个参数如果是Self相关的类型,且 命名为self(小写s),这个参数可以被称为“receiver”(接收者)。

具有 receiver参数的函数,我们称为“方法”(method),可以通过变量实例使 用小数点来调用。

没有receiver参数的函数,我们称为“静态函 数”(static function),可以通过类型加双冒号::的方式来调用。在 Rust中,函数和方法没有本质区别。

Rust中Self(大写S)和self(小写s)都是关键字,大写S的是类型名,小写s的是变量名。请大家一定注意区分。

self参数同样也可以指定类型,当然这个类型是有限制的,必须是包装在Self类型之上的类型。

对于第一个self参数,常见的类型有self:Self、self:&Self、self:&mut Self等类型。

对于以上这些类型,Rust提供了一种简化的写法,我们可 以将参数简写为self、&self、&mut self。self参数只能用在第一个参数的 位置。

请注意“变量self”和“类型Self”的大小写不同。比如:

trait T {
fn method1(self: Self);
fn method2(self: &Self);
fn method3(self: &mut Self);
}trait T {
fn method1(self);
fn method2(&self);
fn method3(&mut self);
}

我们可以为某些具体类型实现(impl)这个Shape trait。假如我们有一个结构体类型Circle,它实现了这个trait,代码如下:

trait Shape {
fn area(&self) -> f64;
}struct Circle {radius: f64,
}
impl Shape for Circle {// Self 类型就是 Circle// self 的类型是 &Self,即 &Circlefn area(&self) -> f64 {// 访问成员变量,需要用 self.radiusstd::f64::consts::PI * self.radius * self.radius}
}
fn main() {let c = Circle { radius : 2f64};// 第一个参数名字是 self,可以使用小数点语法调用println!("The area is {}", c.area());
}

另外,针对一个类型,我们可以直接对它impl来增加成员方法,无 须trait名字。比如:

impl Circle {fn get_radius(&self) -> f64 { self.radius }
}

我们可以把这段代码看作是为Circle类型impl了一个匿名的trait。用这种方式定义的方法叫作这个类型的“内在方法”(inherent methods)。

trait中可以包含方法的默认实现。如果这个方法在trait中已经有了 方法体,那么在针对具体类型实现的时候,就可以选择不用重写。

当然,如果需要针对特殊类型作特殊处理,也可以选择重新实现 来“override”默认的实现方式。比如,在标准库中,迭代器Iterator这个 trait中就包含了十多个方法,但是,其中只有fn next(&mut self)- >OptionSelf::Item是没有默认实现的。

其他的方法均有其默认实 现,在实现迭代器的时候只需挑选需要重写的方法来实现即可。

self参数甚至可以是Box指针类型self:Box。另外,目前Rust 设计组也在考虑让self变量的类型放得更宽,允许更多的自定义类型作为receiver,比如MyType。看下面的代码:

trait Shape {fn area(self: Box<Self>) -> f64;
}struct Circle {radius: f64,
}impl Shape for Circle {// Self 类型就是 Circle// self 的类型是 Box<Self>,即 Box<Circle>fn area(self : Box<Self>) -> f64 {// 访问成员变量,需要用 self.radiusstd::f64::consts::PI * self.radius * self.radius}
}fn main() {let c = Circle { radius : 2f64};// 编译错误// c.area();let b = Box::new(Circle {radius : 4f64});// 编译正确b.area();
}
//impl的对象甚至可以是trait。示例如下:trait Shape {fn area(&self) -> f64;
}trait Round {fn get_radius(&self) -> f64;
}struct Circle {radius: f64,
}impl Round for Circle {fn get_radius(&self) -> f64 { self.radius }
}// 注意这里是
impl Trait for Trait impl Shape for Round { //为满足T:Round的具体类型增加一个成员方法fn area(&self) -> f64 {std::f64::consts::PI * self.get_radius() * self.get_radius()}
}fn main() {let c = Circle { radius : 2f64};// 编译错误// c.area();let b = Box::new(Circle {radius : 4f64}) as Box<Round>;// 编译正确b.area();
}

impl Shape for Round和impl<T:Round>Shape for T是不一样的。

在前一种写法中,self是&Round类型,它是一个trait object,是胖指针。

而在后一种写法中,self是&T类型,是具体类型。

前一种写法是为trait object增加一个成员方法,而后一种写法是为所有的满足T:Round的具体类型增加一个成员方法。

所以上面的示例中, 我们只能构造一个trait object之后才能调用area()成员方法。
impl Shape for Round这种写法确实是很让初学者纠结的, Round既是trait又是type。在将来,trait object的语法会被要求加上dyn关键字。

2,静态方法

没有receiver参数的方法(第一个参数不是self参数的方法)称作“静态方法”。

静态方法可以通过Type::FunctionName()的方式调用。

需要注意的是,即便我们的第一个参数是Self相关类型,只要变量名字不是self,就不能使用小数点的语法调用函数。

struct T(i32);
impl T {
// 这是一个静态方法fn func(this: &Self) {println!{"value {}", this.0};}
}
fn main() {
let x = T(42);
// x.func(); 小数点方式调用是不合法的
T::func(&x);
}

在标准库中就有一些这样的例子。Box的一系列方法Box:: into_raw(b:Self)   Box::leak(b:Self),

以及Rc的一系列方法 Rc::try_unwrap(this:Self)Rc::downgrade(this:&Self),都是这种情况。
它们的receiver不是self关键字,这样设计的目的是强制用户 用Rc::downgrade(&obj)的形式调用,而禁止obj.downgrade()形 式的调用。

这样源码表达出来的意思更清晰,不会因为Rc里面的成员方法和T里面的成员方法重名而造成误解问题。

trait中也可以定义静态函数。下面以标准库中的std::default:: Default trait为例,介绍静态函数的相关用法:

pub trait Default {
fn default() -> Self;
}

上面这个trait中包含了一个default()函数,它是一个无参数的函 数,返回的类型是实现该trait的具体类型。Rust中没有“构造函数”的念。Default trait实际上可以看作一个针对无参数构造函数的统一抽象.比如在标准库中,Vec::default()就是一个普通的静态函数。

impl<T> Default for Vec<T> {
fn default() -> Vec<T> {
Vec::new()
}
}

跟C++相比,在Rust中,定义静态函数没必要使用static关键字,因 为它把self参数显式在参数列表中列出来了。

作为对比,C++里面成员 方法默认可以访问this指针,因此它需要用static关键字来标记静态方 法。

Rust不采取这个设计,主要原因是self参数的类型变化太多,不同写法语义差别很大,选择显式声明self参数更方便指定它的类型。

3,扩展方法

我们还可以利用trait给其他的类型添加成员方法,哪怕这个类型不 是我们自己写的。比如,我们可以为内置类型i32添加一个方法:

trait Double {
fn double(&self) -> Self;
}
impl Double for i32 {
fn double(&self) -> i32 { *self * 2 }
}
fn main() {
// 可以像成员方法一样调用
let x : i32 = 10.double();
println!("{}", x);
}

哪怕这个类型不是在当前 的项目中声明的,我们依然可以为它增加一些成员方法。

但我们也不是随随便便就可以这么做的,Rust对此有一个规定:

在声明trait和 impltraitl的时候,Rust规定CoherenceRule(一致性规则)或称为OrphanRule(孤儿规则):

imp块要么与trait的声明在同一个crate中,要么与类型的声明在同一个crate中。

这是有意的设计。如果我们在使用其他的crate的时候, 强行把它们“拉郎配”,是会制造出bug的。比如说,我们写了一个程 序,引用了外部库lib1和lib2,lib1中声明了一个trait T,lib2中声明了一 个struct S,我们不能在自己的程序中针对S实现T。这也意味着,上游开 发者在给别人写库的时候,尤其要注意,一些比较常见的标准库中的 trait,如Display Debug ToString Default等,应该尽可能地提供好。否 则,使用这个库的下游开发者是没办法帮我们把这些trait实现的。同理,如果是匿名impl,那么这个impl块必须与类型本身存在于同一个crate中。

Rust是一种用户可以对内存有精确控制能力的强类型语言。我们可 以自由指定一个变量是在栈里面,还是在堆里面,变量和指针也是不同 的类型。类型是有大小(Size)的。有些类型的大小是在编译阶段可以 确定的,有些类型的大小是编译阶段无法确定的。目前版本的Rust规 定,在函数参数传递、返回值传递等地方,都要求这个类型在编译阶段 有确定的大小。否则,编译器就不知道该如何生成代码了。 而trait本身既不是具体类型,也不是指针类型,它只是定义了针对 类型的、抽象的“约束”。不同的类型可以实现同一个trait,满足同一个 trait的类型可能具有不同的大小。因此,trait在编译阶段没有固定大小,目前我们不能直接使用trait作为实例变量、参数、返回值。比如:

let x: Shape = Circle::new(); // Shape 不能做局部变量的类型
fn use_shape(arg : Shape) {} // Shape 不能直接做参数的类型
fn ret_shape() -> Shape {} // Shape 不能直接做返回值的类型

这样的写法是错误的,请一定要记住。trait的大小在编译阶段是不固定的,需要写成dynShape形式,即编译的时候把不确定大小的东西通过胖指针来代替,而指针在编译期是确定的。

4,完整函数调用方法

Fully Qualified Syntax提供一种无歧义的函数调用语法,允许程序员精确地指定想调用的是那个函数。以前也叫UFCS(universal function call syntax),也就是所谓的“通用函数调用语法”。这个语法可以允许使用类似的写法精确调用任何方法,包括成员方法和静态方法。其他一切 函数调用语法都是它的某种简略形式。它的具体写法为::item。示例如下:

trait Cook {
fn start(&self);
}
trait Wash {
fn start(&self);
}
struct Chef;
impl Cook for Chef {
fn start(&self) { println!("Cook::start");}
}
impl Wash for Chef {
fn start(&self) { println!("Wash::start");}
}
fn main() {
let me = Chef;
me.start(); //error,出现歧义,编译其器不知道调用哪个方法
}//有必要使用完整的函数调用语法来进行方法调用
fn main() {
let me = Chef;
// 函数名字使用更完整的path来指定,同时,self参数需要显式传递 <Cook>::start(&me);
<Chef as Wash>::start(&me);
}

由此我们也可以看到,所谓的“成员方法”也没什么特殊之处,它跟 普通的静态方法的唯一区别是,第一个参数是self,而这个self只是一个 普通的函数参数而已。只不过这种成员方法也可以通过变量加小数点的 方式调用。变量加小数点的调用方式在大部分情况下看起来更简单更美 观,完全可以视为一种语法糖。
需要注意的是,通过小数点语法调用方法调用,有一个“隐藏 着”的“取引用”步骤。虽然我们看起来源代码长的是这个样子 me.start(),但是大家心里要清楚,真正传递给start()方法的参数是 &me而不是me,这一步是编译器自动帮我们做的。\color{red}不论这个方法接受 的self参数究竟是Self、&Self还是&mut Self,最终在源码上,我们都是 统一的写法:variable.method()。而如果用UFCS语法来调用这个方 法,我们就不能让编译器帮我们自动取引用了,必须手动写清楚。下面用一个示例演示一下成员方法和普通函数其实没什么本质区别。

struct T(usize);
impl T {
fn get1(&self) -> usize {self.0}
fn get2(&self) -> usize {self.0}
}
fn get3(t: &T) -> usize { t.0 }
fn check_type( _ : fn(&T)->usize ) {}
fn main() {
check_type(T::get1);
check_type(T::get2);
check_type(get3);
}

可以看到,get1、get2和get3都可以自动转成fn(&T)→usize类型。

5,trait 约束和继承

Rust的trait的另外一个大用处是,作为泛型约束使用。

未完待完善。。。

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

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

相关文章

JavaScript性能优化实战(9):图像与媒体资源优化

引言 在当今视觉驱动的网络环境中,图像和媒体资源往往占据了网页总下载量的60%-80%,因此对图像和媒体资源进行有效优化已成为前端性能提升的关键领域。尽管网络带宽持续提升,但用户对加载速度的期望也在不断提高,特别是在移动设备和网络条件不稳定的场景下。 本文作为Jav…

NHANES指标推荐:LC9

文章题目&#xff1a;Association between lifes crucial 9 and kidney stones: a population-based study DOI&#xff1a;10.3389/fmed.2025.1558628 中文标题&#xff1a;生命的关键 9 与肾结石之间的关联&#xff1a;一项基于人群的研究 发表杂志&#xff1a;Front Med 影响…

谷歌 NotebookLM 支持生成中文播客

谷歌 NotebookLM 支持生成中文播客。 2025 年 4 月 29 日&#xff0c;NotebookLM 宣布其 “音频概览”&#xff08;Audio Overviews&#xff09;功能新增 76 种语言支持&#xff0c;其中包括中文。用户只需将文档、笔记、研究材料等上传至 NotebookLM&#xff0c;然后在设置中选…

ElasticSearch深入解析(十):字段膨胀(Mapping 爆炸)问题的解决思路

文章目录 一、核心原理&#xff1a;动态映射的双刃剑1. 动态映射的工作机制2. 映射爆炸的触发条件3. 底层性能损耗 二、典型场景与案例分析1. 日志系统&#xff1a;动态标签引发的灾难2. 物联网数据&#xff1a;设备属性的无序扩展 三、系统性解决方案1. 架构层优化2. 配置层控…

交互式智能体面临长周期决策和随机环境反馈交互等挑战 以及解决办法

交互式智能体面临长周期决策和随机环境反馈交互等挑战 以及解决办法 目录 交互式智能体面临长周期决策和随机环境反馈交互等挑战 以及解决办法随机初始化参数,lora但是训练需要更加细粒度的评价指数(对思考过程评价,对得出结果的证明评价,对结果评价)用户进看到结果《RAGE…

4:机器人目标识别无序抓取程序二次开发

判断文件是否存在 //判断文件在不在 int HandEyeCalib::AnsysFileExists(QString FileAddr) {QFile File1(FileAddr);if(!File1.exists()){QMessageBox::warning(this,QString::fromLocal8Bit("提示"),FileAddrQString::fromLocal8Bit("文件不存在"));retu…

【Touching China】2007-2011

文章目录 1、20072、20083、20094、20105、2011 1、2007 钱学森 身份&#xff1a;中国航天事业奠基人&#xff0c;中国科学院、中国工程院资深院士获奖事迹&#xff1a;钱学森1955年冲破重重阻力回到祖国&#xff0c;长期担任火箭导弹和航天器研制的技术领导职务。他以总体、动…

linux常用基础命令_最新版

常用命令 查看当前目录下个各个文件大小查看当前系统储存使用情况查看当前路径删除当前目录下所有包含".log"的文件linux开机启动jar更改自动配置文件后操作关闭自启动linux静默启动java服务查询端口被占用查看软件版本重启关机开机启动取别名清空当前行创建文件touc…

Mamba+Attention+CNN 预测模型:破局长程依赖的计算机视觉新范式

目录 一、引言:从 CNN 到 Mamba 的视觉建模进化之路 二、模型关键组成部分解析 (一)CNN 基干:局部特征提取器 (二)Mamba 块:长程依赖建模核心 (三)注意力机制:特征交互增强器 三、模型创新点 四、模型原理与作用 五、优缺点对比 六、应用领域 一、引言:从 C…

LangChain4j +DeepSeek大模型应用开发——8 Function Calling 函数调用

Function Calling 函数调用也叫 Tools 工具 入门案例 例如&#xff0c;大语言模型本身并不擅长数学运算。如果应用场景中偶尔会涉及到数学计算&#xff0c;我们可以**为他提供一个 “数学工具”。**当我们提出问题时&#xff0c;大语言模型会判断是否使用某个工具。 创建工具…

【Prometheus-Mongodb Exporter安装配置指南,开机自启】

目录 内容概述 一、创建MongoDB监控专用用户二、安装MongoDB Exporter三、启动Exporter服务四、配置Systemd服务五、服务管理命令六、Prometheus集成配置七、Grafana看板 内容概述 本教程详细演示了如何在Linux系统中部署MongoDB Exporter以监控MongoDB数据库&#xff0c;并将…

在 Ubuntu 上安装 cPanel

开始之前&#xff0c;请确保拥有一台 Ubuntu 服务器&#xff0c;推荐使用 Ubuntu 22.04 LTS。如果没有&#xff0c;可以查看免费服务器&#xff1a; 11个免费 VPS&#xff0c;够用一辈子了&#xff01;&#xff08;2025最新&#xff09;Top 11 免费VPS推荐平台对比&#xff08…

【算法基础】插入排序算法 - JAVA

一、算法基础 1.1 什么是插入排序 插入排序是一种简单直观的排序算法&#xff0c;它的工作原理类似于我们打牌时整理手牌的过程。插入排序的核心思想是将数组分为已排序和未排序两部分&#xff0c;每次从未排序部分取出一个元素&#xff0c;插入到已排序部分的适当位置。 1.…

WEB前端小练习——记事本

一、登陆页面 <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width, initial-scale1.0"><title>记事本登录注册</title><link…

[ACTF2020 新生赛]Include [ACTF2020 新生赛]Exec

[ACTF2020 新生赛]Include 因为前端过滤的太多了 所以直接使用 日志包含 搞 包含这个 /var/log/nginx/access.log [ACTF2020 新生赛]Include蚁剑连接 翻看 flag{1ce7a81e-0339-44ef-a398-a7784d3efe37} [ACTF2020 新生赛]Exec [ACTF2020 新生赛]Exec 127.0.0.1 |echo <?…

VFS Global 携手 SAP 推动数字化转型

2025年5月2日&#xff0c;SAP 公司宣布&#xff0c;全球领先的签证、领事和技术服务提供商 VFS Global 将采用 SAP 的多项核心软件解决方案&#xff0c;推动其全球政务服务和跨境流动解决方案迈向全面数字化和智能化。此次合作标志着 VFS Global 在 AI 赋能的政府科技&#xff…

GTC2025全球流量大会:领驭科技以AI云端之力,助力中国企业出海破浪前行

在全球化与数字化浪潮下&#xff0c;AI技术正成为中国企业出海的重要驱动力。一方面&#xff0c;AI通过语言处理、数据分析等能力显著提升出海企业的运营效率与市场适应性&#xff0c;尤其在东南亚等新兴市场展现出"高性价比场景适配"的竞争优势&#xff1b;另一方面…

安全漏洞扫描费用受哪些因素影响?市场价格区间是多少?

安全漏洞扫描费用是个复杂且关键的话题。它涉及多种影响因素。合理的费用可让企业有效防范安全风险。下面我们深入探讨一番。 市场价格区间 安全漏洞扫描的费用在市场上差别很大。小型企业进行简单扫描&#xff0c;可能只要几千元。大型企业做全面的深度扫描&#xff0c;费用…

n8n工作流自动化平台的实操:解决中文乱码

解决问题&#xff1a; 通过ftp读取中文内容的文件&#xff0c;会存在乱码&#xff0c;如下图&#xff1a; 解决方案 1.详见《安装 iconv-lite》 2.在code节点&#xff0c;写如下代码&#xff1a; const iconv require(iconv-lite);const items $input.all(); items.forEa…

豪越科技消防立库方案:实现应急物资高效管理

在消防救援工作中&#xff0c;应急物资管理是至关重要的一环。然而&#xff0c;当前应急物资管理的现状却令人担忧。传统的应急物资管理方式存在诸多弊端&#xff0c;严重影响了消防救援的效率和效果。 走进一些传统的消防仓库&#xff0c;映入眼帘的往往是杂乱无章的存储场景。…