Rust入门之高级Trait

news2025/5/15 19:35:16

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::TargetFrom<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是可以有相同名称的方法声明的,甚至在结构体上定义的方法也可能同名。

代码示例如下,定义PilotWizard 两个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 时,必须满足以下条件之一

  1. 类型(Type) 是在当前 crate 中定义的
  2. 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 ... ok

successes:

---- tests::test_newtype stdout ----
[hello, world]


successes:
    tests::test_newtype

test 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);

    }

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

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

相关文章

Qt/C++开发监控GB28181系统/录像文件查询/录像回放/倍速播放/录像文件下载

一、前言 搞定了实时预览后&#xff0c;另一个功能就是录像回放&#xff0c;录像回放和视频点播功能完全一致&#xff0c;唯一的区别就是发送点播的sdp信息中携带了开始时间和结束时间&#xff0c;因为是录像文件&#xff0c;所以有这个时间&#xff0c;而实时视频预览这个对应…

季报中的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/ 适用于任何边缘设备的人工智能&#xff1a; Gateways - 网关 Sensors & Cameras - 传感器和摄像头 Docker Containers - Docker容器 MCUs, NPUs, CPUs, GPUs 构建数据集、训练模型并优化库以…

zst-2001 上午题-历年真题 计算机网络(16个内容)

网络设备 计算机网络 - 第1题 ac 计算机网络 - 第2题 d 计算机网络 - 第3题 集线器不能隔离广播域和冲突域&#xff0c;所以集线器就1个广播域和冲突域 交换机就是那么的炫&#xff0c;可以隔离冲突域&#xff0c;有4给冲突域&#xff0c;但不能隔离广播域&#xf…

使用termius连接腾讯云服务器

使用termius连接腾讯云服务器 1.下载termius termius官网 安装配置教程 这里安装的window版本> 默认安装到C盘&#xff0c;不建议修改路径 可以选择谷歌登录&#xff0c;也可以不登录&#xff0c;软件是免费的&#xff0c;试用的是付费版本&#xff0c;不需要点 2.配置 这里…

实景三维建模软件应用场景(众趣科技实景三维建模)

实景三维建模软件应用场景概述 实景三维建模软件&#xff0c;作为数字化时代的重要工具&#xff0c;不仅能够真实、立体、时序化地反映和表达物理世界&#xff0c;还为国家的基础设施建设和数字化发展提供了有力的支撑。 在测绘与地理信息领域&#xff0c;实景三维建模软件是构…

【Linux】基础指令(Ⅱ)

目录 1. mv指令 2. cat指令 3.echo指令 补&#xff1a;输出重定向 4. more指令 5. less指令 6. head指令和tail指令 7.date指令 时间戳&#xff1a; 8. cal指令 9. alias指令 10.grep指令 1. mv指令 语法&#xff1a;mv [选项]... 源文件/目录 目标文件/目录 …

【ios越狱包安装失败?uniapp导出ipa文件如何安装到苹果手机】苹果IOS直接安装IPA文件

问题场景&#xff1a; 提示&#xff1a;ipa是用于苹果设备安装的软件包资源 设备&#xff1a;iphone 13(未越狱) 安装包类型&#xff1a;ipa包 调试工具&#xff1a;hbuilderx 问题描述 提要&#xff1a;ios包无法安装 uniapp导出ios包无法安装 相信有小伙伴跟我一样&…

【嵌入模型与向量数据库】

目录 一、什么是向量&#xff1f; 二、为什么需要向量数据库&#xff1f; 三、向量数据库的特点 四、常见的向量数据库产品 FAISS 支持的索引类型 vs 相似度 五、常见向量相似度方法对比 六、应该用哪种 七、向量数据库的核心逻辑 &#x1f50d; 示例任务&#xff1a;…

【东枫科技】使用LabVIEW进行NVIDIA CUDA GPU 开发

文章目录 工具包 CuLab - LabVIEW 的 GPU 工具包特性和功能功能亮点类似 LabVIEW 的 GPU 代码开发支持的功能数值类型和维数开发系统要求授权售价 工具包 CuLab - LabVIEW 的 GPU 工具包 CuLab 是一款非常直观易用的 LabVIEW 工具包&#xff0c;旨在加速 Nvidia GPU 上的计算密…

基于策略的强化学习方法之策略梯度(Policy Gradient)详解

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

1.Redis-key的基本命令

&#xff08;一&#xff09;Redis的基本类型 String&#xff0c;List&#xff0c;Set&#xff0c;Hash&#xff0c;Zset 三种特殊类型&#xff1a;geospatial&#xff08;地理空间数据&#xff09;、hyperloglog[基数估算&#xff08;去重计数&#xff09;]、bitmaps(位图&…

PROFIBUS DP转ModbusTCP网关模块于污水处理系统的成功应用案例解读​

在当今的工业生产领域&#xff0c;众多企业在生产过程中会产生大量工业废水。若这些废水未经处理直接排放&#xff0c;将会引发严重的工业污染问题。因此&#xff0c;借助科技手段对污水进行有效处理显得尤为重要。在一个污水处理系统中&#xff0c;往往包含来自不同厂家、不同…

电脑开机提示按f1原因分析及解决方法(6种解决方法)

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

复现:DemoGen 用于数据高效视觉运动策略学习的 合成演示生成 (RSS) 2025

https://github.com/TEA-Lab/DemoGen?tabreadme-ov-file 复现步骤很简单&#xff0c;按照readme配置好conda环境即可运行。 运行&#xff1a; cd demo_generation bash run_gen_demo.sh 等待生成&#xff1a; 查看data文件夹

本地部署firecrawl的两种方式,自托管和源码部署

网上资料很多 AI爬虫黑科技 firecrawl本地部署-CSDN博客 源码部署 前提条件本地安装py&#xff0c;node.js环境,嫌弃麻烦直接使用第二种 使用git或下载压缩包 git clone https://github.com/mendableai/firecrawl.git 设置环境参数 cd /firecrawl/apps/api 复制环境参数 …

2023年12月中国电子学会青少年软件编程(Python)等级考试试卷(六级)答案 + 解析

青少年软件编程&#xff08;Python&#xff09;等级考试试卷&#xff08;六级&#xff09; 分数&#xff1a;100 题数&#xff1a;38 一、单选题(共25题&#xff0c;共50分) 1. 运行以下程序&#xff0c;输出的结果是&#xff1f;&#xff08; &#xff09; class A(): …

Spring @Lazy注解详解

文章目录 Lazy注解主要作用工作原理使用方法注意事项总结 Lazy注解主要作用 首先&#xff0c;让我们看看Lazy注解的源码&#xff0c;截图如下&#xff1a; 源码注释翻译如下 通过源码&#xff0c;我们可以看到&#xff1a;Lazy注解是一个标记注解&#xff0c;用于标记 bean会…

中国品牌日 | 以科技创新为引领,激光院“风采”品牌建设结硕果

品牌&#xff0c;作为企业不可或缺的隐形财富&#xff0c;在当今竞争激烈的市场环境中&#xff0c;其构建与强化已成为推动企业持续繁荣的关键基石。为了更好地保护自主研发产品&#xff0c;激光院激光公司于2020年3月7日正式注册“风采”商标&#xff0c;创建拥有自主知识产权…

GNU Screen 曝多漏洞:本地提权与终端劫持风险浮现

SUSE安全团队全面审计发现&#xff0c;广泛使用的终端复用工具GNU Screen存在一系列严重漏洞&#xff0c;包括可导致本地提权至root权限的缺陷。这些问题同时影响最新的Screen 5.0.0版本和更普遍部署的Screen 4.9.x版本&#xff0c;具体影响范围取决于发行版配置。 尽管GNU Sc…