【Rust自学】19.2. 高级trait:关联类型、默认泛型参数和运算符重载、完全限定语法、supertrait和newtype

news/2025/2/3 11:39:10 标签: rust, 后端, 开发语言

喜欢的话别忘了点赞、收藏加关注哦(加关注即可阅读全文),对接下来的教程有兴趣的可以关注专栏。谢谢喵!(=・ω・=)
请添加图片描述

19.2.1. 在trait定义中使用关联类型来指定占位类型

我们首先在第10章的10.3. trait Pt.1:trait的定义、约束与实现 和 10.4. trait Pt.2:trait作为参数和返回类型、trait bound 中介绍了trait,但我们没有讨论更高级的细节。现在我们来深入了解

关联类型(associated type)是trait中的类型占位符,它可以用于trait方法的签名中。它用于定义出包某些类型的trait,而在实现前无需知道这些类型是什么。

看个例子:

rust">pub trait Iterator {
    type Item;

    fn next(&mut self) -> Option<Self::Item>;
}

标准库中的负责迭代器部分的iterator trait(详见 13.8. 迭代器 Pt.4:创建自定义迭代器)就是一个带有关联类型的trait,其定义如上。

Item就是关联类型。在迭代过程中使用Item类型来替代实际出现的值以完成逻辑和实际数据类型分离的目的。可以看到next方法的返回值Option<Self:Item>就出现了Item

Item就是所谓的类型占位符,其核心思想与泛型有点像,但区别也是有的:

泛型关联类型
每次实现 Trait 时标注类型无需标注类型
可以为一个类型多次实现某个 Trait(不同的泛型参数)无法为单个类型多次实现某个 Trait

19.2.2. 默认泛型参数和运算符重载

我们可以在使用泛型参数时为泛型指定一个默认的具体类型。它的语法是<PlaceholderType=ConcreteType>。这种技术常用于运算符重载(operator overloading)。

虽然Rust不允许创建自己的运算符及重载任意的运算符,但是可以通过实现std::ops中列出的那些trait来重载一部分相应的运算符。

看个例子:

rust">use std::ops::Add;

#[derive(Debug, Copy, Clone, 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,
        }
    }
}

fn main() {
    assert_eq!(
        Point { x: 1, y: 0 } + Point { x: 2, y: 3 },
        Point { x: 3, y: 3 }
    );
}
  • 我们在这个例子中为point结构体实现了Add trait,也就是重载了+这个运算符,具体来说就是Add trait下的add函数把每个字段分别相加
  • 主函数里就可以直接使用+来把两个Point类型相加

Add trait的定义如下:

rust">trait Add<Rhs=Self> {
    type Output;

    fn add(self, rhs: Rhs) -> Self::Output;
}

它就使用了默认的泛型参数类型Rhs=Self。也就是说当我们实现Add trait时如果没有为Rhs指定一个具体的类型,那么Rhs的类型就默认为Self,所以上文的例子中的Rhs就是Point

再看一个例子,这回我们想要实现毫米和米相加的例子:

rust">use std::ops::Add;

struct Millimeters(u32);
struct Meters(u32);

impl Add<Meters> for Millimeters {
    type Output = Millimeters;

    fn add(self, other: Meters) -> Millimeters {
        Millimeters(self.0 + (other.0 * 1000))
    }
}
  • 这里先通过结构体字段声明了MillimetersMeters,分别表示毫米和米。
  • 下文为Millimeters实现了Add trait,其中又通过<Meters>显式地指明了类型被设定为Meters了。add函数中通过本身的毫米和传进来的米乘1000相加得出来以毫米计数的值。

19.2.3. 默认泛型参数的主要应用场景

  • 扩展一个类型而不破坏现有代码
  • 允许在大部分用户都不需要的特定场景下进行自定义

19.2.4. 完全限定语法(Fully Qualified Syntax)如何调用同名方法

直接看例子:

rust">trait Pilot {
    fn fly(&self);
}

trait Wizard {
    fn fly(&self);
}

struct Human;

impl Pilot for Human {
    fn fly(&self) {
        println!("This is your captain speaking.");
    }
}

impl Wizard for Human {
    fn fly(&self) {
        println!("Up!");
    }
}

impl Human {
    fn fly(&self) {
        println!("*waving arms furiously*");
    }
}
  • 定义了两个trait,分别叫PilotWizard,都有一个fly方法,没有具体实现
  • 有一个结构体叫Human,我们在下文为它分别实现了那两个trait,也就是分别为两个trait写了fly方法。除此之外,还通过impl块为结构体本身实现了fly方法。

这个时候一共有三个fly方法,如果我们在主函数中调用:

rust">fn main() {
    let person = Human;
    person.fly();
}

运行这段代码会打印出*waving arms furiously* ,表明Rust直接调用了Human上实现的fly方法。

要从Pilot trait或Wizard trait调用fly方法,我们需要使用更明确的语法来指定我们指的是哪个fly方法:

rust">fn main() {
    let person = Human;
    Pilot::fly(&person);
    Wizard::fly(&person);
    person.fly();
}

在方法名称之前指定特征名称可以向 Rust 阐明我们要调用哪个fly实现。person.fly()也可以写 Human::fly(&person)

输出:

This is your captain speaking.
Up!
*waving arms furiously*

但是,不是方法的关联函数没有self 范围。当有多个类型或trait定义的方法具有相同函数名时,Rust 并不总是知道指的是哪种类型,除非使用完全限定语法

rust">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")
    }
}

fn main() {
    println!("A baby dog is called a {}", Dog::baby_name());
}
  • Animal trait有baby_name方法。Dog是一个结构体,实现了Animal trait,同时也通过impl块实现了baby_name方法。这是一共就有两个baby_name方法。
  • 在主函数里使用了Dog::baby_name(),按照上文的逻辑就应该执行Dogimpl块实现的baby_name方法,也就是输出"Spot"。

输出:

A baby dog is called a Spot

那怎么实现Dog实现的Animal trait上的baby_name方法呢?我们试试使用上一个例子的逻辑:

rust">fn main() {
    println!("A baby dog is called a {}", Animal::baby_name());
}

输出:

error[E0790]: cannot call associated function on trait without specifying the corresponding `impl` type
  --> src/main.rs:20:43
   |
2  |     fn baby_name() -> String;
   |     ------------------------- `Animal::baby_name` defined here
...
20 |     println!("A baby dog is called a {}", Animal::baby_name());
   |                                           ^^^^^^^^^^^^^^^^^^^ cannot call associated function of trait
   |
help: use the fully-qualified path to the only available implementation
   |
20 |     println!("A baby dog is called a {}", <Dog as Animal>::baby_name());
   |                                           +++++++       +

For more information about this error, try `rustc --explain E0790`.
error: could not compile `traits-example` (bin "traits-example") due to 1 previous error

Animal trait上的baby_name函数的执行需要知道是哪个类型上的实现,但是baby_name这个方法又没有参数,所以不知道是哪个类型上的实现。

针对这种情况就得使用完全限定语法。其形式为:

rust"><Type as Trait>::function(receiver_if_method, next_arg, ...);

这种语法可以在任何调用函数或方法的地方使用,并且它允许忽略那些从其它上下文推导出来的部分。

但是这种语法只有在Rust无法区分你期望调用哪个具体实现的时候才需要使用这种语法,因为这种语法写起来太麻烦了,所以轻易不使用。

根据这个语法上面的代码就应该这么改:

rust">fn main() {
    println!("A baby dog is called a {}", <Dog as Animal>::baby_name());
}

输出:

A baby dog is called a puppy

19.2.5. 使用supertrait来要求trait附带其它trait的功能

有时候我们可能会需要在一个trait中使用其它trait的功能,也就是说间接依赖的trait也需要被实现。而那个被间接依赖的trait就是当前trait的supertrait。

看个例子:

rust">use std::fmt;

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

OutlinePrint实际上是用来在终端通过字符打印一个图形的。但是在打印过程中必须要求self实现了to_string方法,也就是要求self实现了Display trait(to_stringDisplay trait下的方法)。其写法就是trait关键字 + trait名字 + : + supertrait。

假如我们有一个结构体Point,想要通过OutlinePrint trait的outline_print函数在终端打印出来。又因为OutlinePrint trait需要Display trait的函数,所以得为它同时实现OutlinePrint trait和Display trait,不然就会报错:

rust">struct Point {
    x: i32,
    y: i32,
}

use std::fmt;

impl fmt::Display for Point {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "({}, {})", self.x, self.y)
    }
}

impl OutlinePrint for Point {}

19.2.6. 使用newtype模式在外部类型上实现外部trait

我们之前讲过一个孤儿规则:只有当trait或类型定义在本地包时,才能为该类型实现这个trait。而我们可以使用newtype模式来绕过这一规则,具体来说就是利用元组结构体来构建一个新的类型放在本地。

看个例子:

我们想为Vector实现Display trait,但是VectorDisplay trait都定义在在外部包中,所以无法直接为Vector实现。所以把Vector包裹在自己创建的元组结构体Wrapper里,然后用Wrapper来实现Display trait:

rust">use std::fmt;

struct Wrapper(Vec<String>);

impl fmt::Display for Wrapper {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "[{}]", self.0.join(", "))
    }
}

fn main() {
    let w = Wrapper(vec![String::from("hello"), String::from("world")]);
    println!("w = {w}");
}

http://www.niftyadmin.cn/n/5840754.html

相关文章

用结构加法3ax+1预测第4点的分布

有1个点在19*19的平面上在某种力的作用下运动&#xff0c;轨迹为 共移动了90步&#xff0c;按照&#xff08;0&#xff0c;1&#xff0c;2&#xff0c;3&#xff09;&#xff0c;&#xff08;1&#xff0c;2&#xff0c;3&#xff0c;4&#xff09;&#xff0c;…&#xff0c;&…

【最长上升子序列Ⅱ——树状数组,二分+DP,纯DP】

题目 代码&#xff08;只给出树状数组的&#xff09; #include <bits/stdc.h> using namespace std; const int N 1e510; int n, m; int a[N], b[N], f[N], tr[N]; //f[i]表示以a[i]为尾的LIS的最大长度 void init() {sort(b1, bn1);m unique(b1, bn1) - b - 1;for(in…

玩转Docker | 使用Docker部署SSCMS内容管理系统

玩转Docker | 使用Docker部署SSCMS内容管理系统 前言一、项目介绍SSCMS 简介主要特点二、系统要求环境要求环境检查Docker版本检查检查操作系统版本三、部署SSCMS系统下载镜像创建容器检查容器状态检查服务端口安全设置四、访问SSCMS应用初始化配置访问SSCMS管理后台六、配置SS…

课题介绍:基于惯性与单目视觉信息融合的室内微小型飞行器智能自主导航研究

室内微小型飞行器在国防、物流和监测等领域中应用广泛&#xff0c;但在复杂的非合作环境中实时避障和导航仍面临诸多挑战。由于微小型飞行器的载荷和能源限制&#xff0c;迫切需要开发高效的智能自主导航系统。本项目旨在研究基于惯性导航与单目视觉信息融合的技术&#xff0c;…

Python-基于PyQt5,pdf2docx,pathlib的PDF转Word工具(专业版)

前言:日常生活中,我们常常会跟WPS Office打交道。作表格,写报告,写PPT......可以说,我们的生活已经离不开WPS Office了。与此同时,我们在这个过程中也会遇到各种各样的技术阻碍,例如部分软件的PDF转Word需要收取额外费用等。那么,可不可以自己开发一个小工具来实现PDF转…

SAP SD学习笔记28 - 请求计划(开票计划)之2 - Milestone请求(里程碑开票)

上一章讲了请求计划&#xff08;开票计划&#xff09;中的 定期请求。 SAP SD学习笔记27 - 请求计划(开票计划)之1 - 定期请求-CSDN博客 本章继续来讲请求计划&#xff08;开票计划&#xff09;的其他内容&#xff1a; Milestone请求(里程碑请求)。 目录 1&#xff0c;Miles…

鸿蒙 组件封装使用

效果 项目目录截图 一、GoodItem代码如下 import { Item } from ../entity/Item/*** GoodItem 组件用于展示商品信息* 它包含商品的图片、名称、价格和优惠信息*/ Component export struct GoodItem {// 商品项&#xff0c;包含商品的基本信息public goodsItem: Item new Item…

Visual Studio Code应用本地部署的deepseek

1.打开Visual Studio Code&#xff0c;在插件中搜索continue&#xff0c;安装插件。 2.添加新的大语言模型&#xff0c;我们选择ollama. 3.直接点connect&#xff0c;会链接本地下载好的deepseek模型。 参看上篇文章&#xff1a;deepseek本地部署-CSDN博客 4.输入需求生成可用…