玩转Rust高级应用 如何让让运算符支持自定义类型,通过运算符重载的方式是针对自定义类型吗?
运算符重载
Rust 允许一部分运算符重载,用户可以让这些运算符支持自定义类型。运算符重载 的方式是:针对自定义类型, impl 一 些在标准库中预定义好的trait,这 些trait 都存在于std::ops模块中。比如前面已经讲过了的Deref trait就属于运算符重载。
本章我们以最基本的Add trait来做讲解。Add 代表的是加法运算符+重载。它的定义是:
trait Add < RHS = Self > {type Output;fn add(self, rhs: RHS) - >Self: :Output;
}
它具备一个泛型参数RHS 和一个关联类型Output。 其 中RHS 有一个默认值Self。标准库早已经为基本数字类型实现了这个trait。比如:
impl Add<i32>for i32 {type Output =i32;
}
而且还有:
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。
假如我们现在自己定义了一个复数类型,想让它支持加法运算符,示例如下:
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:
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 场景的例子:
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包 一 下,实现有缓冲的读取。
下面用一个示例来演示说明这些类型的使用方法:
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() 方法,在这个锁的期间内多次调用读取操作,来避免多次上锁。
示例如下:
use use fn let let letstd: :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()来指定的。示例如下:
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 都是很好的选择。
