8.rust运算符重载实现指南

一、前言

C/C++中有运算符重载这一概念,它的目的是让即使毫不相干的内容也能通过我们自定义的方法进行运算符操作运算。

比如字符串本身是不能相加的,但由于C++中的String重载了运算符+,所以我们就可以将两个字符串进行相加、但实际的含义其实是拼接。

而rust中同样存在类似的功能,其底层原理是前面章节便讲过的trait。

二、基本使用

首先我们来看看如何进行加法运算符的重载:

struct Point {
    x: i32,
    y: i32,
}

impl std::ops::Add for Point {
    type Output = Self;

    fn add(self, rhs: Self) -> Self::Output {
        Point {
            x: self.x + rhs.x,
            y: self.y + rhs.y,
        }
    }
}

其实现方法其实就是为我们想要重载运算符的结构实现对应的加法trait。

所有基本运算符操作的trait都在std::ops空间下,比如这里的加法Add

注意这些trait不需要我们去写它的实现,直接用vscode自动生成的即可:

image.png 当你想将一个trait为某个结构体实现,那么只需要写上基本的形式,然后鼠标放在上面、点击快速修复,让其自动补全即可。

这个Add内部有两个结构,一个是返回值类型Output,另一个就是函数add

image.png

其中Output类型需要我们指明,由于这里是两个点结构相加,返回值也应该还是一个点,所以这里就等于Self类型,Self类型指的就是for关键字后的结构,也就是这里的Point结构。

而add函数就比较简单了,它接受两个参数,第一个参数就是自己,注意它是首字母小写的self,实际是self:Self的简写,代表调用者自己,或者说是+符号左边的操作值。

而第二个参数就是想要进行相加的变量,它是Self类型,也就是Point类型。

其返回值为前面定义好的Output类型,而Output类型又被我们定义为了Self,所以实际上返回的依旧还是Self,也就是Point类型。

至于函数体内部的操作、计算过程,我们就可以随意写了,只要记得返回一个Self类型的值即可。

然后就可以直接使用了:

fn main() {
    let p1 = Point { x: 0, y: 0 };
    let p2 = Point { x: 1, y: 1 };
    let p3 = p1 + p2;
}

是不是还是很简单的!

三、常用运算符

有了上面的基础,其它都差不多的,所以我们就简单过一遍常见的运算符用法。

首先是+=运算符操作:

struct Point {
    x: i32,
    y: i32,
}

impl std::ops::AddAssign for Point {
    fn add_assign(&mut self, rhs: Self) {
        self.x += rhs.x;
        self.y += rhs.y;
    }
}

fn main() {
    let mut p1 = Point { x: 0, y: 0 };
    let p2 = Point { x: 1, y: 1 };
    p1 += p2;
}

--=和上面是完全一致的,只是换了一个名字:SubSubAssign

包裹*/乃至位运算等等,都是如此,你可以在vscode中输入std::opt::,就可以看到所有可用的运算符对应的trait了。

然后是比较运算符,和上面有点不一样:

struct Point {
    x: i32,
    y: i32,
}

impl PartialEq for Point {
    fn eq(&self, other: &Self) -> bool {
        self.x == other.x && self.y == other.y
    }
}

fn main() {
    let p1 = Point { x: 0, y: 0 };
    let p2 = Point { x: 1, y: 1 };
    if p1 == p2 {
        println!("p1等于p2")
    }

    if p1 != p2 {
        println!("p1不等于p2")
    }
}

想要重载==!=,那么你只需要实现PartialEq这个trait即可,它是已经被预定义导出的,所以可以直接用。

而>、<、>=、<=之类的运算符操作,同样如此,他们共用于PartialOrd,而PartialOrd是在PartialEq基础之上的。

为了简单,我们这里直接使用#[derive(PartialEq)]宏自动生成,然后再来实现PartialOrd

use std::cmp::Ordering;

#[derive(PartialEq)]
struct Point {
    x: i32,
    y: i32,
}

impl PartialOrd for Point {
    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
        let cmp_x = self.x.partial_cmp(&other.x).unwrap();
        let cmp_y = self.y.partial_cmp(&other.y).unwrap();
        if cmp_x == Ordering::Less && cmp_y == Ordering::Less {
            return Some(Ordering::Less);
        }
        if cmp_x == Ordering::Greater && cmp_y == Ordering::Greater {
            return Some(Ordering::Greater);
        }
        None
    }
}

fn main() {
    let p1 = Point { x: 0, y: 0 };
    let p2 = Point { x: 1, y: 1 };
    if p1 > p2 {
        println!("p1 is greater than p2");
    }

    if p1 <= p2 {
        println!("p2 is greater than p1");
    }
}

注意这里使用的比较方法均为partial_cmp,它的返回值是一个枚举,通过该枚举的值我们就可以得到比较的结果。

四、通用约束

有些时候我们想要写一些通用函数,比如一个加法的通用函数,就需要用到泛型:

fn add<T>(a: T, b: T) -> T {
    a + b
}

但你直接像上面这样写肯定是不行的,因为T类型太宽泛了,此时编译器无法确定T类型是否能够相加。

所以我们需要对其进行约束:

fn add<T>(a: T, b: T) -> T
where
    T: std::ops::Add<Output = T>,
{
    a + b
}

约束的方式就是使用where关键字,对T类型进行约束,比如这里的意思就是传入的T类型必须实现了std::ops::Add<Output = T>这个trait。

注意这里的写法,后面还添加了<Output=T>的语句,因为根据前面所学我们知道,Add这个trait内部需要为Output指定一个类型,所以这里意思就是指定Output的类型为T。

结合起来看,就是传入的T类型必须实现了Add这个Trait,并且其实现的返回值也必须是同样的T类型。

举个实例就是,如果传入的10和20,为两个i32类型,那么其返回值就必须也是i32类型。

因为根据前面的定义可以知道,我们实际上是可以通过定义Output为其它类型,使得两个数字相加甚至可以不等于数字的。

作者:余识
全部文章:0
会员文章:0
总阅读量:0
c/c++pythonrustJavaScriptwindowslinux