玩转Rust高级应用 如何让让运算符支持自定义类型,通过运算符重载的方式是针对自定义类型吗?

运算符重载

Rust 允许一部分运算符重载,用户可以让这些运算符支持自定义类型。运算符重载 的方式是:针对自定义类型, impl 一 些在标准库中预定义好的trait,这 些trait 都存在于std::ops模块中。比如前面已经讲过了的Deref trait就属于运算符重载。

本章我们以最基本的Add trait来做讲解。Add 代表的是加法运算符+重载。它的定义是:

js 复制代码
trait Add < RHS = Self > {
    type Output;
    fn add(self, rhs: RHS) - >Self: :Output;
}

它具备一个泛型参数RHS 和一个关联类型Output。 其 中RHS 有一个默认值Self。标准库早已经为基本数字类型实现了这个trait。比如:

js 复制代码
impl Add<i32>for i32 {
  type Output =i32;
}

而且还有:

js 复制代码
impl < 'a>Add<i32>for	&'a i32 type Output = <i32 as Add < i32 >> ::Output;
impl < 'a>Add<&'a i32 >
for i32 type Output = <i32 as Add < i32 >> ::Output;
impl < 'a,'b > Add < &'a	i32>for	&'b i32 type Output = <i32 as Add < i32 >> ::Output;

这意味着,不仅i32+i32 是允许的,而且i32+&i32 、&i32+i32 、&i32+&i32 这几种形式也都是允许的。它们的返回类型都是i32。

假如我们现在自己定义了一个复数类型,想让它支持加法运算符,示例如下:

js 复制代码
use std: :ops: :Add;# [derive(Copy, Clone, Debug, PartialEq)] struct Complex {
    real: i32,
    imaginary: i32,
}
impl Add
for Complex {
    type Output = Complex;
    fn add(self, other: Complex) - >Complex {
        Complex {
            real: self.real + other.real,
            imaginary: self.imaginary + other.imaginary,
        }
    }

    fn main() {
        let c1 = Complex {
            real: 1,
            imaginary: 2
        };
        let c2 = Complex {
            real: 2,
            imaginary: 4
        };
        println ! ("{:?}", c1 + c2);
    }

在这个实现中,我们没有指定泛型参数RHS, 所以它就采用了默认值,在此示例中就相 当于Complex 这个类型。同理,如果我们希望让这个复数能支持与更多的类型求和,可以 继 续 写 多 个 impl:

js 复制代码
impl < 'a>Add<&'a Complex >
for Complex {
    type Output = Complex;
    fn add(self, other: &'a Complex{
real:self.real
imaginary:self.imaginary	Complex)->Complex{
  +other.real,
  +other.imaginary,
}
impl Add<i32>for Complex{
type  Output  =Complex;
fn add(self,other:i32)->Complex{
Complex{ 
  real:self.real+other,
  imaginary:self.imaginary,
}

I/O

标准库中也提供了一系列I/O相关的功能。虽然功能比较基础,但好在是跨平台的。如 果用户需要更丰富的功能,可以去寻求外部的开源库。

平台相关字符串

要跟操作系统打交道,首先需要介绍的是两个字符串类型:OsString 以及它所对应的 字符串切片类型OsStr。 它们存在于std::ffi 模块中。

Rust标准的字符串类型是String 和 str。 它们的一个重要特点是保证了内部编码是 统一的utf-8。但是,当我们和具体的操作系统打交道时,统一的 utf-8 编码是不够用的,某 些操作系统并没有规定一定是用的 utf-8编码。所以,在和操作系统打交道的时候,String /str 类型并不是一个很好的选择。比如在Windows 系统上,字符一般是用16位数字来表 示的。

为了应付这样的情况,Rust在标准库中又设计了OsString /OsStr来处理这样的情 况。这两种类型携带的方法跟String /str非常类似,用起来几乎没什么区别,它们之 间也可以相互转换。

举个需要用到OsStr 场景的例子:

js 复制代码
use std: :path: :PathBuf;
fn main() {
    let mut buf = PathBuf: :from("/");
    buf.set_file_name("bar");
    if let Some(s) = buf.to_str() {
        println ! ("{}", s);
    } else {
        println ! ("invalid path");
    }
}

上面这个例子是处理操作系统中的路径,就必须用OsString /OsStr这两个类型。 PathBuf 的 set_file_name 方法的签名是这样的:它要求,第二个参数必须满足ASRef 可以看到的约束。而查看 str 类型的文档,我们所以,&str 类型可以直接作为参数在这个方法中使用。

另外,当我们想把&PathBuf转为 &str 类型的时候,使用了to_str方法,返回的是 一 个Option<&str> 类型。这是为了错误处理。因为 PathBuf 内部是用Osstring 存储 的字符串,它未必能成功转为utf-8编码。而想要把&PathBuf 转 为 &OsStr 则简单多了, 这种转换不需要错误处理,因为它们是同样的编码。


文件和路径

Rust 标 准 库 中 用PathBuf 和 Path 两个类型来处理路径。它们之间的关系就类似 String 和 str 之间的关系: 一个对内部数据有所有权,还有一个只是借用。实际上,读源 码 可 知 ,PathBuf里面存的是 一个OsString,Path里面存的是一个OsStr 。 这两个类型定义在std::path 模块中。

Rust对文件操作主要是通过std::fs::File 来完成的。这个类型定义了一些成员方 法,可以实现打开、创建、复制、修改权限等文件操作。 std::fs模块下还有一些独立函 数,比如 remove_file 、soft_link等,也是非常有用的。

对文件的读写,则需要用到std::io 模块了。这个模块内部定义了几个重要的trait, 比 如Read/Write。File类型也实现了Read 和 Write 两 个trait, 因此它拥有一系列方便读写文件的方法,比如 read 、read_to_end 、read_to_string 等。这个模块还定义了 BufReader等类型。我们可以把任何一个满足Read trait的类型再用BufReader包 一 下,实现有缓冲的读取。

下面用一个示例来演示说明这些类型的使用方法:

js 复制代码
use std: :io: :prelude: :*;
use std: :io: :BufReader;
use std: :fs: :File;
fn test_read_file() - >Result < (),
std: :io: :Error > {
    let mut path = std: :env: :home_dir().unwrap();
    path.push(".rustup");
    path.push("settings");
    path.set_extension("toml");
    let file = File: :open( & path) ? ;
    let reader = BufReader: :new(file);
    for line in reader.lines() {
        println ! ("Read a line:{}", line ? );
    }
    Ok(())
}
fn main() {
    match test_read_file() {
        Ok(_) = >{}
        Err(e) = >{
            println ! ("Error occured:{}", e);
        }
    }

标准输入输出

前面我们已经多次使用了println!宏输出一些信息。这个宏很方便,特别适合在小程 序中随手使用。但是如果你需要对标准输入输出作更精细的控制,则需要使用更复杂一点的 办法。

在 C++ 里面,标准输入输出流cin 、cout是全局变量。在Rust 中,基于线程安 全的考虑,获取标准输入输出的实例需要调用函数,分别为std::io::stdin()和 std::io::stdout()。stdin()函数返回的类型是Stdin 结构体。这个结构体本身已经实现了Read trait,所以,可以直接在其上调用各种读取方法。但是这样做效率比较低,因 为为了线程安全考虑,每次读取的时候,它的内部都需要上锁。提高执行效率的办法是手动 调用lock() 方法,在这个锁的期间内多次调用读取操作,来避免多次上锁。

示例如下:

js 复制代码
use use fn let let let

std: :io: :prelude: :*;
std: :io: :BufReader;
test_stdin() - >Result < (),
std: :io: :Error > {
    stdin = std: :i: :stdin();
    handle = stdin.lock();
    reader = BufReader: :new(handle);

    for line in reader.lines() {
        let line = line ? ;
        if line.is_empty() {
            return Ok(());
            println ! ("Read a line:{}", line);
        }
        Ok(())
    }
    fn main() {
        match test_stdin() {
            Ok(_) = >{}
            Err(e) = >{
                println ! ("Error occured:{}", e);
            } {}
}

进程启动参数

大家应该注意到了,Rust的 main 函数的签名和C/C++不一致。在C/C++ 里面,一般 进程启动参数是直接用指针传递给main 函数的,进程返回值是通过main 函数的返回值来决 定 的 。

在Rust 中,进程启动参数是调用独立的函数std::env::args()来得到的,或者使 用std::env::args_os()来得到,进程返回值也是调用独立函数std::process::

exit()来指定的。示例如下:

js 复制代码
fn main() {
    if std: :env: :args().any( | arg | arg == "-kill") {
        std: :process: :exit(1);
    }
    for arg in std: :env: :args() {
        println ! ("{}", arg);
    }
}

同样,标准库只提供最基本的功能。如果读者需要功能更强大、更容易使用的命令行参 数解析器,可以到crates.io上搜索相关开源库,clap 或者getopts 都是很好的选择。

相关推荐
@小码农1 小时前
2025年北京海淀区中小学生信息学竞赛第一赛段试题(附答案)
人工智能·python·算法·蓝桥杯
laocooon5238578862 小时前
C语言 有关指针,都要学哪些内容
c语言·数据结构·算法
梦想平凡2 小时前
情怀源代码工程实践(加长版 1/3):确定性内核、事件回放与最小可运行骨架
开发语言·javascript·ecmascript
多多*2 小时前
牛客周赛 Round 114 Java题解
算法
程序猿阿越2 小时前
Kafka源码(七)事务消息
java·后端·源码阅读
笑我归无处2 小时前
强引用、软引用、弱引用、虚引用详解
java·开发语言·jvm
02苏_2 小时前
秋招Java面
java·开发语言
ytttr8732 小时前
64QAM信号的数字预失真处理(MATLAB实现)
开发语言·matlab