Rust入门之高级Trait
Rust入门之高级Trait
- 本文源码
引言
前面学习了迭代器(Iterators),Iterator源码中就用到了关联类型的功能。关联类型就属于高级trait的内容,这次我们学习一下高级trait,了解关联类型等知识。关联类型看似和泛型相似,与此同时再分析一下关联类型和泛型的区别和作用。
在Trait中定义使用关联类型来指定占位类型
关联类型(Associated Types)是 trait 中的类型占位符,它可以用于Trait 的方法签名中:
- 可以定义出包含哪些关联类型的 trait ,而在实现前无需知道这些类型是什么。
可以看一下标准库当中的 Iterator
源码,如下:
pub trait Iterator {type Item;#[lang = "next"]#[stable(feature = "rust1", since = "1.0.0")]fn next(&mut self) -> Option<Self::Item>;//...
}
这个Item
就是迭代器中所迭代元素的类型。next方法中返回的Option中就是这个Item。
看起来和泛型的功能有些相似,这里列一下关联类型和泛型参数的区别:
特性 | 关联类型 | 泛型参数 |
---|---|---|
实现数量 | 每个类型对同一 trait 只能有一个实现 | 可为不同泛型参数多次实现同一 trait |
类型关系 | 表达类型与 trait 的固定关联关系 | 表达 trait 对不同类型的通用处理能力 |
典型用途 | Iterator::Item , Deref::Target | From<T> , Add<Rhs> |
代码简洁性 | 减少方法签名中的类型参数 | 需在调用时或定义中携带泛型参数 |
pub trait Iterator1 {type Item;fn next(&mut self) -> Option<Self::Item>;}
pub trait Iterator2<T> {fn next(&mut self) -> Option<T>;}struct Counter {}impl Iterator1 for Counter {type Item = u32;fn next(&mut self) -> Option<Self::Item> {None}}
/// 会报错,不允许为同一个类型实现多个 trait
// impl Iterator1 for Counter {
// type Item = String;// fn next(&mut self) -> Option<Self::Item> {
// None
// }// }impl Iterator2<u32> for Counter {fn next(&mut self) -> Option<u32> {None}}impl Iterator2<String> for Counter {fn next(&mut self) -> Option<String> {None}}
默认泛型参数和运算符重载
- 可以在使用泛型参数时为泛型类指定一个默认的具体类型。
- 语法:
<PlaceholderType=ConcreteType>
。 - 这种技术常用于运算符重载。
- Rust 并不允许创建自定义运算符或重载任意运算符。
std::ops
中所列出的运算符和相应的 trait 可以通过实现运算符相关 trait 来重载。
use std::ops::Add;#[derive(Debug, PartialEq)]
struct Point {x: i32,y: i32,
}impl Add for Point {type Output = Point;fn add(self, other: Point) -> Point {Point {x: self.x + other.x,y: self.y + other.y,}}
}#[cfg(test)]
mod tests {use super::*;#[test]fn test_add() {let p1 = Point { x: 1, y: 2 };let p2 = Point { x: 3, y: 4 };let p3 = p1 + p2;assert_eq!(p3, Point { x: 4, y: 6 });}
}
标准库中的 Add
trait,这是一个带有一个方法和一个关联类型的 trait。<Rhs = Self>
这个语法叫做 默认类型参数(default type parameters)。RHS
是一个泛型类型参数(“right hand side” 的缩写),它用于定义 add
方法中的 rhs
参数。如果实现 Add
trait 时不指定 RHS
的具体类型,RHS
的类型将是默认的 Self
类型,也就是在其上实现 Add
的类型。
在上边的代码示例中,Rhs
就是Point
。
Add trait 源码:
#[doc(alias = "+")]
pub trait Add<Rhs = Self> {/// The resulting type after applying the `+` operator.#[stable(feature = "rust1", since = "1.0.0")]type Output;/// Performs the `+` operation.////// # Example////// ```/// assert_eq!(12 + 1, 13);/// ```#[must_use = "this returns the result of the operation, without modifying the original"]#[rustc_diagnostic_item = "add"]#[stable(feature = "rust1", since = "1.0.0")]fn add(self, rhs: Rhs) -> Self::Output;
}
再看一个不使用默认类型参数的例子:
#[derive(Debug, PartialEq)]
struct Millimeters(u32);
#[derive(Debug, PartialEq)]
struct Meters(u32);impl Add<Meters> for Millimeters {type Output = Millimeters;fn add(self, other: Meters) -> Millimeters {Millimeters(self.0 + (other.0 * 1000))}
}
单元测试:
#[cfg(test)]
mod tests {use super::*;#[test]fn test_add_millimeters() {let m1 = Millimeters(100);let m2 = Meters(1);let m3 = m1 + m2;assert_eq!(m3, Millimeters(1100));}
}
上面的代码中厘米,米两个struct,有我们希望能够将毫米值与米值相加,并让 Add
的实现正确处理转换。可以为 Millimeters
实现 Add
并以 Meters
作为泛型参数而不使用默认的Self
,以 Millimeters
作为关联类型。
默认参数类型主要用于如下两个方面:
- 扩展类型而不破坏现有代码。
- 在大部分用户都不需要的特定情况进行自定义。
标准库的 Add
trait 就是一个第二个目的例子:大部分时候你会将两个相似的类型相加,不过它提供了自定义额外行为的能力。在 Add
trait 定义中使用默认类型参数意味着大部分时候无需指定额外的参数。换句话说,一小部分实现的样板代码是不必要的,这样使用 trait 就更容易了。
第一个目的是相似的,但过程是反过来的:如果需要为现有 trait 增加类型参数,为其提供一个默认类型将允许我们在不破坏现有实现代码的基础上扩展 trait 的功能。
完全限定语法与消歧义:调用相同名称的方法
在Rust中两个trait是可以有相同名称的方法声明的,甚至在结构体上定义的方法也可能同名。
代码示例如下,定义Pilot
,Wizard
两个trait,都定义了相同方法名的fly
方法,fly
方法都有参数&self
。同时也为结构体Human
定义了关联方法fly
方法。那么结构体调用fly
方法时,分别如何调用各自的fly
方法实现呢?
trait Pilot {fn fly(&self);
}trait Wizard {fn fly(&self);}struct Human;impl Pilot for Human {fn fly(&self) {println!("Pilot flying");}
}impl Wizard for Human {fn fly(&self) {println!("Wizard flying");}
}impl Human {fn fly(&self) {println!("Human flying");}
}trait Animal {fn baby_name() -> String;
}struct Dog;impl Dog {fn baby_name() -> String {String::from("Spot")}
}impl Animal for Dog {fn baby_name() -> String {String::from("Puppy")}
}
#[test]fn test_fly() {let human = Human;human.fly(); // 调用 Human 的 fly 方法Pilot::fly(&human); // 调用 Pilot 的 fly 方法Wizard::fly(&human); // 调用 Wizard 的 fly 方法}#[test]fn test_baby_name() {// 不相等assert_ne!(Dog::baby_name(), String::from("Puppy"));// 相等assert_eq!(Dog::baby_name(), String::from("Spot"));}
对于有&self
的方法可以使用如下调用方式:
human.fly(); // 调用 Human 的 fly 方法
Pilot::fly(&human); // 调用 Pilot 的 fly 方法
Wizard::fly(&human); // 调用 Wizard 的 fly 方法
对于 Animal trait
中定义的baby_name
方法是没有参数的。
调用Dog::baby_name()
,打印的是Spot
,可以知道调用的是Dog
自己的baby_name
方法。那么这时候如何调用为Dog
实现Animal
中的baby_name
方法呢?这里就用到了完全限定语法。
完全限定语法定义:
<Type as Trait>::function(receiver_if_method, next_arg, ...);
上面的代码中如何想要调用Animal
的实现,就要这么写:
// 相等
assert_eq!(<Dog as Animal>::baby_name(), String::from("Puppy"));
对于不是方法的关联函数,其没有一个 receiver
,故只会有其他参数的列表。可以选择在任何函数或方法调用处使用完全限定语法。然而,允许省略任何 Rust 能够从程序中的其他信息中计算出的部分。只有当存在多个同名实现而 Rust 需要帮助以便知道我们希望调用哪个实现时,才需要使用这个较为冗长的语法。
父 trait 用于在另一个 trait 中使用某 trait 的功能
有时需要在一个trait中使用其他trait的功能
- 需要被依赖的trait也被实现
- 那个被间接依赖的trait就是当前trait的super trait
trait OutlinePrint: fmt::Display {fn outline_print(&self) {let output = self.to_string();let len = output.len();println!("{}", "*".repeat(len + 4));println!("*{}*", " ".repeat(len + 2));println!(" {} ", output);println!("*{}*", " ".repeat(len + 2));println!("{}", "*".repeat(len + 4));}
}impl OutlinePrint for Point { // 此时会报错}
只为Point
实现OutlinePrint
时,会编译错误,提示我们必须为Point
实现std::fmt::Display
这个trait
`Point` doesn't implement `std::fmt::Display`
the trait `std::fmt::Display` is not implemented for `Point`
in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) insteadrustcClick for full compiler diagnostic
lib.rs(81, 21): required by a bound in `OutlinePrint`
这样代码就不会报错了:
trait OutlinePrint: fmt::Display {fn outline_print(&self) {let output = self.to_string();let len = output.len();println!("{}", "*".repeat(len + 4));println!("*{}*", " ".repeat(len + 2));println!(" {} ", output);println!("*{}*", " ".repeat(len + 2));println!("{}", "*".repeat(len + 4));}
}impl OutlinePrint for Point { // 此时会报错}impl fmt::Display for Point {fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {write!(f, "({}, {})", self.x, self.y)}
}
OutlinePrint
trait 在定义时指定了依赖的 trait是 fmt::Display
,所以在为 Point
实现OutlinePrint
,也必须同时为 Point
实现fmt::Display
。
使用 newtype 模式在外部类型上实现外部 trait
这里需要先引入一个 “孤儿规则”,孤儿规则(Orphan Rule) 是一种 trait 实现(trait implementation)的限制规则,其核心目的是为了保证类型系统的安全性和一致性。具体规则如下:
当你为某个类型实现某个 trait 时,必须满足以下条件之一:
- 类型(Type) 是在当前 crate 中定义的
- Trait 是在当前 crate 中定义的
如果类型和 trait 都来自外部 crate,则你无法为该类型实现该 trait。这种情况下,编译器会报错,并提示你违反了孤儿规则。
这条规则确保了其他人编写的代码不会破坏你代码,反之亦然。没有这条规则的话,两个 crate 可以分别对相同类型实现相同的 trait,而 Rust 将无从得知应该使用哪一个实现。
现在,想要绕开这个限制方法是使用 newtype 模式,使用一个元组结构体对我们想要实现trait的类型封装起来。由于这个封装类型对于 crate 是本地的,这样就可以在这个封装上实现 trait。
简单来说,newtype 模式是指创建一个包含另一个类型作为其单个字段的新的元组结构体。
示例代码:
/// newtype pattern
struct Wrapper(Vec<String>);impl fmt::Display for Wrapper {fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {write!(f, "[{}]", self.0.join(", "))}
}
单元测试:
#[test]fn test_newtype() {let w = Wrapper(vec![String::from("hello"),String::from("world"),]);println!("{}", w);}
执行结果:
running 1 test
test tests::test_newtype ... oksuccesses:---- tests::test_newtype stdout ----
[hello, world]successes:tests::test_newtypetest result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 5 filtered out; finished in 0.00s
newtype有一个缺点,因为Wrapper
是一个新类型,它并不具备所封装值的方法。如果要使用封装值的某个方法,必须在Wrapper
中同样实现该方法并使用self.0
来调用。如果希望新类型拥有其内部类型的每一个方法,可以为封装类型实现Deref trait
,并返回其内部类型是一种解决方案。 这里查阅参考:Deref Trait 允许自定义解引用运算符*
的行为
代码示例:
- 当想调用被封装值的len方法,就同样在
Wrapper
中实现len
方法。
impl Wrapper {pub fn len(&self) -> usize {self.0.len()}
}
- 如果希望新类型拥有内部类型的每一个方法,可以为封装类型实现
Deref trait
impl Deref for Wrapper {type Target = Vec<String>;fn deref(&self) -> &Vec<String> {&self.0}
}impl DerefMut for Wrapper {fn deref_mut(&mut self) -> &mut Vec<String> {&mut self.0}
}
同时我们也实现DerefMut
trait 来保证可变,这样就可以调用被封装类型的其他方法了。
#[test]fn test_newtype() {let mut w = Wrapper(vec![String::from("hello"),String::from("world"),]);println!("{}", w);assert_eq!(w.len(), 2);w.push(String::from("rust"));println!("{}", w);}
相关文章:
Rust入门之高级Trait
Rust入门之高级Trait - 本文源码 引言 前面学习了迭代器(Iterators),Iterator源码中就用到了关联类型的功能。关联类型就属于高级trait的内容,这次我们学习一下高级trait,了解关联类型等知识。关联类型看似和泛型相…...
从 Set、Map 到 WeakSet、WeakMap 的进阶之旅
在 ES5 时代,JavaScript 的数据结构主要依赖于两种类型:数组和对象。然而,随着应用规模的增长和复杂性上升,传统的数据结构越来越难以满足开发需求。比如,需要一个能自动去重的集合、一个支持任意类型键名的字典、一个…...
TTL (Time-To-Live) 解析
文章目录 TTL (Time-To-Live) 解析:网络与Java中的应用一、TTL的定义二、TTL在网络中的应用1. **路由和数据包的生命周期**2. **DNS中的TTL**3. **防止环路** 三、TTL在Java中的应用1. **缓存管理**2. **Java中的ThreadLocal**3. **网络通信中的TTL** 四、TTL的注意…...

Qt/C++开发监控GB28181系统/录像文件查询/录像回放/倍速播放/录像文件下载
一、前言 搞定了实时预览后,另一个功能就是录像回放,录像回放和视频点播功能完全一致,唯一的区别就是发送点播的sdp信息中携带了开始时间和结束时间,因为是录像文件,所以有这个时间,而实时视频预览这个对应…...

季报中的FPGA行业:U型反转,春江水暖
上周Lattice,AMD两大厂商相继发布2025 Q1季报,尽管恢复速度各异,但同时传递出FPGA行业整体回暖的复苏信号。 5月5日,Lattice交出了“勉强及格”的答卷,报告季度营收1亿2000万,与华尔街的预期基本相符。 对于这家聚焦在中小规模器件的领先厂商而言,按照其CEO的预期,长…...

嵌入式机器学习平台Edge Impulse图像分类 – 快速入门
陈拓 2025/05/08-2025/05/11 1. 简介 官方网址 https://edgeimpulse.com/ 适用于任何边缘设备的人工智能: Gateways - 网关 Sensors & Cameras - 传感器和摄像头 Docker Containers - Docker容器 MCUs, NPUs, CPUs, GPUs 构建数据集、训练模型并优化库以…...
web 自动化之 yaml 数据/日志/截图
文章目录 一、yaml 数据获取二、日志获取三、截图 一、yaml 数据获取 需要安装 PyYAML 库 import yaml import os from TestPOM.common import dir_config as Dir import jsonpathclass Data:def __init__(self,keyNone,file_name"test_datas.yaml"):file_path os…...
ARMV8 RK3399 u-boot TPL启动流程分析 --start.S
上电后运行的第一支文件:arch/arm/cpu/armv8/start.S CONFIG_ENABLE_ARM_SOC_BOOT0_HOOK1 #include <asm/arch/boot0.h> 跳转到 arch/arm/include/asm/arch-rockchip/boot0.h CONFIG_SPL_BUILD1 b 1f ROCKCHIP_EARLYRETURN_TO_BROMno TINY_FRAMEWORKno …...

zst-2001 上午题-历年真题 计算机网络(16个内容)
网络设备 计算机网络 - 第1题 ac 计算机网络 - 第2题 d 计算机网络 - 第3题 集线器不能隔离广播域和冲突域,所以集线器就1个广播域和冲突域 交换机就是那么的炫,可以隔离冲突域,有4给冲突域,但不能隔离广播域…...

使用termius连接腾讯云服务器
使用termius连接腾讯云服务器 1.下载termius termius官网 安装配置教程 这里安装的window版本> 默认安装到C盘,不建议修改路径 可以选择谷歌登录,也可以不登录,软件是免费的,试用的是付费版本,不需要点 2.配置 这里…...
redis 命令大全整理
http://doc.redisfans.com/ 原网址 Redis 命令分类 Key(键) Key(键)命令 exists/del/keys/type/scanobject/move/dump/migratettl/pttl/persist/expireat/pexpireat/expire/pexpirerename/renamenxsort/randomkey/restoreexists 语法:exists key [key ...] 检查一个或多…...

实景三维建模软件应用场景(众趣科技实景三维建模)
实景三维建模软件应用场景概述 实景三维建模软件,作为数字化时代的重要工具,不仅能够真实、立体、时序化地反映和表达物理世界,还为国家的基础设施建设和数字化发展提供了有力的支撑。 在测绘与地理信息领域,实景三维建模软件是构…...
Mac M系列 安装 jadx-gui
安装 Homebrew在终端中执行以下命令(需管理员密码): 安装 Homebrew(官方源) /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"国内用户可用镜像源加速&…...
软考 系统架构设计师系列知识点之杂项集萃(56)
接前一篇文章:软考 系统架构设计师系列知识点之杂项集萃(55) 第91题 商业智能关注如何从业务数据中提取有用的信息,然后采用这些信息指导企业的业务开展。商业智能系统主要包括数据预处理、建立()、数据分…...
Ubuntu20.04 搭建Kubernetes 1.28版本集群
环境依赖 以下操作,无特殊说明,所有节点都需要执行 安装 ssh 服务安装 openssh-server复制代码 sudo apt-get install openssh-server修改配置文件复制代码 vim /etc/ssh/sshd_config找到配置项 复制代码 LoginGraceTime 120 PermitRootLogin prohibit-password StrictModes…...

【Linux】基础指令(Ⅱ)
目录 1. mv指令 2. cat指令 3.echo指令 补:输出重定向 4. more指令 5. less指令 6. head指令和tail指令 7.date指令 时间戳: 8. cal指令 9. alias指令 10.grep指令 1. mv指令 语法:mv [选项]... 源文件/目录 目标文件/目录 …...
RAG之大规模解析 PDF 文档全流程实战
PDF 文档在商业、学术和政府领域无处不在,蕴含着大量宝贵信息。然而,从 PDF 中提取结构化数据却面临着独特的挑战,尤其是在处理数千甚至数百万个文档时。本指南探讨了大规模解析 PDF 的策略和工具。 PDF解析挑战 PDF 的设计初衷是为了提供一致的视觉呈现,而非数据提取。这…...
vue-ganttastic甘特图label标签横向滚动固定方法
这个甘特图之前插件里,没有找到能固定label标签在屏幕上的办法,用css各种办法都没有实现,所以我我直接手写定位,用js监听滚动条滚动的距离,然后同步移动甘特图label标签,造成一种定位的错觉,以下…...
AcroForm JavaScript Promise 对象应用示例: 异步加载PDF文件
这段代码演示了在Adobe Acrobat DC Pro 的 JavaScript 环境中如何使用 Promise 对象处理异步操作。具体功能是: 定义了一个loadFile函数,模拟异步加载PDF文件的操作使用Promise对象封装异步操作,提供成功(resolve)和失败(reject)两种状态通过…...
MySQL 8.0 OCP 1Z0-908 题目解析(2)
题目005 Choose two. Which two actions can obtain information about deadlocks? □ A) Run the SHOW ENGINE INNODB MUTEX command from the mysql client. □ B) Enable the innodb_status_output_locks global parameter. □ C) Enable the innodb_print_all_deadlock…...

【ios越狱包安装失败?uniapp导出ipa文件如何安装到苹果手机】苹果IOS直接安装IPA文件
问题场景: 提示:ipa是用于苹果设备安装的软件包资源 设备:iphone 13(未越狱) 安装包类型:ipa包 调试工具:hbuilderx 问题描述 提要:ios包无法安装 uniapp导出ios包无法安装 相信有小伙伴跟我一样&…...

【嵌入模型与向量数据库】
目录 一、什么是向量? 二、为什么需要向量数据库? 三、向量数据库的特点 四、常见的向量数据库产品 FAISS 支持的索引类型 vs 相似度 五、常见向量相似度方法对比 六、应该用哪种 七、向量数据库的核心逻辑 🔍 示例任务:…...

【东枫科技】使用LabVIEW进行NVIDIA CUDA GPU 开发
文章目录 工具包 CuLab - LabVIEW 的 GPU 工具包特性和功能功能亮点类似 LabVIEW 的 GPU 代码开发支持的功能数值类型和维数开发系统要求授权售价 工具包 CuLab - LabVIEW 的 GPU 工具包 CuLab 是一款非常直观易用的 LabVIEW 工具包,旨在加速 Nvidia GPU 上的计算密…...

基于策略的强化学习方法之策略梯度(Policy Gradient)详解
在前文中,我们已经深入探讨了Q-Learning、SARSA、DQN这三种基于值函数的强化学习方法。这些方法通过学习状态值函数或动作值函数来做出决策,从而实现智能体与环境的交互。 策略梯度是一种强化学习算法,它直接对策略进行建模和优化,…...

1.Redis-key的基本命令
(一)Redis的基本类型 String,List,Set,Hash,Zset 三种特殊类型:geospatial(地理空间数据)、hyperloglog[基数估算(去重计数)]、bitmaps(位图&…...
JavaScript 中级进阶技巧之map函数
作为一名初级 JavaScript 开发者,你可能已经熟悉了基础语法、变量和简单的循环。但要从初级迈向中级,掌握一些高效、优雅的编码技巧是关键。其中,map 函数是中级开发者常用的工具,它不仅能简化代码,还能提升代码的可读…...

PROFIBUS DP转ModbusTCP网关模块于污水处理系统的成功应用案例解读
在当今的工业生产领域,众多企业在生产过程中会产生大量工业废水。若这些废水未经处理直接排放,将会引发严重的工业污染问题。因此,借助科技手段对污水进行有效处理显得尤为重要。在一个污水处理系统中,往往包含来自不同厂家、不同…...
Java实现桶排序算法
1. 桶排序原理图解 桶排序是一种基于分桶思想的非比较排序算法,适用于数据分布较为均匀的场景。其核心思想是将数据分散到有限数量的“桶”中,每个桶再分别进行排序(通常使用插入排序或其他简单的排序算法)。以下是桶排序的步骤&a…...
《Effective Python》第2章 字符串和切片操作——深入理解 Python 中 __repr__ 与 __str__
引言 本文基于学习《Effective Python》第三版 Chapter 2: Strings and Slicing 中的 Item 12: Understand the Difference Between repr and str When Printing Objects 后的总结与延伸。在 Python 中,__repr__ 和 __str__ 是两个与对象打印密切相关的魔术方法&am…...

电脑开机提示按f1原因分析及解决方法(6种解决方法)
经常有网友问到一个问题,我电脑开机后提示按f1怎么解决?不管理是台式电脑,还是笔记本,都有可能会遇到开机需要按F1,才能进入系统的问题,引起这个问题的原因比较多,今天小编在这里给大家列举了比较常见的几种电脑开机提示按f1的解决方法。 电脑开机提示按f1原因分析及解决…...