常用 Trait
很多 trait 你可能每天在用,但未必清楚它们背后的语义、相互关系,以及什么时候应该实现它们。
From / Into
在类型系统里,转换是绕不开的需求。Rust 里最核心的两个转换 Trait 就是 From 和 Into。
From<T> for U:如何把 T 转成 UInto<T> for U:如何把 U 转成 T
这两个Trait的功能刚好相反。
标准库规定了一个重要关系:
只要你为
U实现了From<T>,就自动为T实现了Into<U>。
也就是说,你几乎只需要实现 From,不要手写 Into。
Into 通常用于调用方一侧,让调用变得简洁、统一;而 From 则是实现方声明“我知道怎么从某种类型安全地构造自己”。
let s: String = String::from("hello"); // From<&str> for String
let s2: String = "world".into(); // Into<String> for &strFrom包含一个核心方法,签名如下:
pub trait From<T>: Sized {
fn from(value: T) -> Self;
}例如:
let s1 = String::from("hello"); // From<&str> for String
let v = Vec::<u8>::from("abc"); // From<&str> for Vec<u8>
let n = i64::from(10_i32); // From<i32> for i64我们常用到的构造方法from其实就来源于From这个Trait。
自己实现时,一般这样写:
struct Point {
x: i32,
y: i32,
}
impl From<(i32, i32)> for Point {
fn from(t: (i32, i32)) -> Self {
Self { x: t.0, y: t.1 }
}
}
let p = Point::from((3, 4));由于 From 自动生成了 Into,你可以在函数参数上直接要求 Into<T>,让调用方传什么都行(只要能被转成 T):
fn get_point<P: Into<Point>>(path: P) {
let p = path.into(); // 统一转成 Point
// ...
}
fn main() {
let p = Point::from((3, 4));
get_point((5, 6));
}AsRef / AsMut
AsRef / AsMut 是非常轻量级的转换 Trait,获取某种类型的引用。
pub trait AsRef<T: ?Sized> {
fn as_ref(&self) -> &T;
}
pub trait AsMut<T: ?Sized> {
fn as_mut(&mut self) -> &mut T;
}这里要注意,返回值是一个泛型,这意味着你可以把一个类型借用为另外一个其它的类型。比如说String就可以借用为&str,调用as_ref和as_mut就可以拿到它的指定类型的借用。
那么有人可能要问了,我想要拿到一个借用,直接用&和&mut不就好了?为什么非要去调用这方法?
其实这个地方就是对这两个Trait有误解,它的目的不是为了让你拿到一个借用,而是保证可以拿到指定类型的借用。区别在于,后者有一个”指定类型“的要求。
fn show_str(s: &str) {
print!("{}", s);
}
fn main() {
let s1 = "hello";
let s2 = String::from(" world");
show_str(s1);
show_str(s2); // err
show_str(s2.as_ref()); // err
}比如以上代码中,show_str拿到一个&str然后输出。外层分别传入了&str和String,那么就会报错,因为String和&str类型不兼容。
这个时候就可以通过as_ref拿到String内部的原生字符串借用,然后在传给函数,并且这个过程是借用,没有发生拷贝,当show_str输出完毕,原本的String还可以继续使用。
但是现在还不够优雅,因为外层的调用者需要考虑自己传的类型,做出对应的转换。
对于show_str来说,它其实并不关心接收到的参数是什么,而应该考虑接收到的参数能不能拿到一个&str进行输出,因此可以把s作为一个泛型:
fn show_str<T>(s: T)
where
T: AsRef<str>,
{
let r: &str = s.as_ref();
print!("{}", r);
}
fn main() {
let s1 = "hello";
let s2 = String::from(" world");
show_str(s1);
show_str(s2);
}这里s的类型是T,并且约束T: AsRef<str>,这意味着s.as_ref()一定可以拿到一个&str,从而进行输出。
对于外层来说,直接传入&str和String都是合法的,用户完全感知不到这个as_ref的存在,只需要知道任何类型的字符串传进去,它就可以输出。
因此AsRef其实本质上是一种类型转化和借用的综合语义。它非常类似于Into,不同的是Into类型转换完毕后会剥夺所有权,而AsRef只是把类型内部的某些东西借出来,用完就归还所有权。
Default
Default 用来给类型一个默认值。
签名:
pub trait Default {
fn default() -> Self;
}实现之后,可以使用default函数直接拿到一个类型的默认值。
标准库很多地方会用到 T: Default,例如 Option<T>::default() 是 None
#[derive(Default)]
struct Config {
debug: bool,
retries: u32,
}
let c = Config::default(); // debug = false, retries = 0
// 与结构体更新语法搭配:
let c2 = Config {
debug: true,
..Default::default()
};在 derive 时,Rust 会为所有字段调用各自的 Default。因此如果结构体全部字段都实现了 Default,可以直接 #[derive(Default)]。
如果想自定义默认逻辑,可以手写:
impl Default for Config {
fn default() -> Self {
Self {
debug: false,
retries: 3,
}
}
}Debug
Debug 是最常用的调试打印 trait,对应 {:#?} / {:?} 这种格式化。
定义在 std::fmt 模块中:
pub trait Debug {
fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), Error>;
}大部分时候不用自己实现,直接 #[derive(Debug)] 即可:
#[derive(Debug)]
struct Point {
x: i32,
y: i32,
}
fn main() {
let p = Point { x: 3, y: 4 };
println!("{:?}", p);
println!("{:#?}", p);
}输出结果:
Point { x: 3, y: 4 }
Point {
x: 3,
y: 4,
}实现Debug后,结构体默认输出所有的键值对。可以看出{:?}输出更紧凑,而{#?}则会将结构体展开。
Display
Display 对应 {},它与 Debug 不同:
Debug:为开发者准备,通常结构化输出,可能包含实现细节Display:为用户准备,通常是人类易读的字符串描述
Display 也是个格式化 trait:
pub trait Display {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result;
}与 Debug 不同的是,Display 不能 derive,通常需要你自定义输出格式。
use std::fmt::{self, Display, Formatter};
struct Point { x: i32, y: i32 }
impl Display for Point {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(f, "({},{})", self.x, self.y)
}
}
let p = Point { x: 3, y: 4 };
println!("{}", p); // (3,4)算术运算
这一组 trait 控制了运算符 + - * / % 的行为。
以 Add 为例:
pub trait Add<Rhs = Self> {
type Output;
fn add(self, rhs: Rhs) -> Self::Output;
}Rhs:右操作数类型(默认是Self)Output:返回值类型(关联类型)
类似的有:
trait Sub<Rhs = Self> { type Output; fn sub(self, rhs: Rhs) -> Self::Output; }
trait Mul<Rhs = Self> { type Output; fn mul(self, rhs: Rhs) -> Self::Output; }
trait Div<Rhs = Self> { type Output; fn div(self, rhs: Rhs) -> Self::Output; }
trait Rem<Rhs = Self> { type Output; fn rem(self, rhs: Rhs) -> Self::Output; }示例:实现向量加法
use std::ops::Add;
#[derive(Debug, Clone, Copy)]
struct Vec2 {
x: f64,
y: f64,
}
impl Add for Vec2 {
type Output = Vec2;
fn add(self, rhs: Vec2) -> Vec2 {
Vec2 {
x: self.x + rhs.x,
y: self.y + rhs.y,
}
}
}
let v1 = Vec2 { x: 1.0, y: 2.0 };
let v2 = Vec2 { x: 3.0, y: 4.0 };
let v3 = v1 + v2; // 调用 Add::add
println!("{:?}", v3);也可以让左右类型不同:
impl Add<f64> for Vec2 {
type Output = Vec2;
fn add(self, rhs: f64) -> Vec2 {
Vec2 { x: self.x + rhs, y: self.y + rhs }
}
}赋值运算
这一类 trait 控制 += -= *= /= %= 这样的原地修改运算符。
以 AddAssign 为例:
pub trait AddAssign<Rhs = Self> {
fn add_assign(&mut self, rhs: Rhs);
}相比于基本的计算Add,这个版本拿到的是&mut self,会对自己做修改,并且没有返回值。
其他类似:
trait SubAssign<Rhs = Self> { fn sub_assign(&mut self, rhs: Rhs); }
trait MulAssign<Rhs = Self> { fn mul_assign(&mut self, rhs: Rhs); }
trait DivAssign<Rhs = Self> { fn div_assign(&mut self, rhs: Rhs); }
trait RemAssign<Rhs = Self> { fn rem_assign(&mut self, rhs: Rhs); }
use std::ops::AddAssign;
impl AddAssign for Vec2 {
fn add_assign(&mut self, rhs: Vec2) {
self.x += rhs.x;
self.y += rhs.y;
}
}
let mut v = Vec2 { x: 1.0, y: 2.0 };
v += Vec2 { x: 3.0, y: 4.0 }; // 调用 add_assign一元负号 Neg
Neg用于控制表达式前的负号:
pub trait Neg {
type Output;
fn neg(self) -> Self::Output;
}示例:
use std::ops::Neg;
#[derive(Debug, Clone, Copy)]
struct Vec2 { x: f64, y: f64 }
impl Neg for Vec2 {
type Output = Vec2;
fn neg(self) -> Vec2 {
Vec2 { x: -self.x, y: -self.y }
}
}
let v = Vec2 { x: 1.0, y: -2.0 };
println!("{:?}", -v); // Vec2 { x: -1.0, y: 2.0 }Index / IndexMut
索引操作是通过 Index / IndexMut 实现的。
pub trait Index<Idx> {
type Output;
fn index(&self, index: Idx) -> &Self::Output;
}
pub trait IndexMut<Idx>: Index<Idx> {
fn index_mut(&mut self, index: Idx) -> &mut Self::Output;
}这里的索引类型可以是任意泛型。有人可能会觉得[]内部只能传usize,事实上它是非常灵活的。
比如说数组确实是需要一个usize,它也可以传一个范围拿到切片,比如arr[..],那么这里面的这个范围类型其实就是作为了Index的下标的Idx这个泛型,并为其专门实现了方法,返回一个数组切片。
let arr = [0; 10];
let x = arr[1];
let s = &arr[..2];
let x = arr.index(1);
let s = &arr.index(..2);以上代码中arr[1]和arr.index[1]是等效的,而&arr[..2]和&arr.index(..2)等效。
示例:
use std::ops::{Index, IndexMut};
struct Matrix {
data: Vec<f64>,
cols: usize,
}
impl Index<(usize, usize)> for Matrix {
type Output = f64;
fn index(&self, (r, c): (usize, usize)) -> &f64 {
&self.data[r * self.cols + c]
}
}
impl IndexMut<(usize, usize)> for Matrix {
fn index_mut(&mut self, (r, c): (usize, usize)) -> &mut f64 {
&mut self.data[r * self.cols + c]
}
}
let mut m = Matrix { data: vec![0.0; 9], cols: 3 };
m[(1, 1)] = 42.0;
println!("{}", m[(1, 1)]);这里Matrix是一个通过一维数组模拟的二维数组,以(usize, usize)作为索引。第一个元素是行,第二个元素是列。那么Matrix[(i, j)]实际上就是第i * row + j个元素,因此特地为这个类型实现了Index和IndexMut,让它可以正确拿到元素。
对于这种与操作符相关的Trait还有非常非常多,这里不一一介绍了,实现都是类似的,需要时查询即可:
| 运算符 / 语义 | 对应 Trait | 签名(核心方法) | 备注 |
|---|---|---|---|
一元负号 -x | std::ops::Neg | fn neg(self) -> Output | 取负数,如 -x |
逻辑/按位非 !x | std::ops::Not | fn not(self) -> Output | 对 bool:逻辑非;对整数:按位非 |
加法 a + b | std::ops::Add | fn add(self, rhs: Rhs) -> Output | 二元 + |
减法 a - b | std::ops::Sub | fn sub(self, rhs: Rhs) -> Output | 二元 - |
乘法 a * b | std::ops::Mul | fn mul(self, rhs: Rhs) -> Output | * |
除法 a / b | std::ops::Div | fn div(self, rhs: Rhs) -> Output | / |
取余 a % b | std::ops::Rem | fn rem(self, rhs: Rhs) -> Output | % |
加并赋值 a += b | std::ops::AddAssign | fn add_assign(&mut self, rhs: Rhs) | 对应 + 的原地修改版本 |
减并赋值 a -= b | std::ops::SubAssign | fn sub_assign(&mut self, rhs: Rhs) | 对应 - 的原地修改版本 |
乘并赋值 a *= b | std::ops::MulAssign | fn mul_assign(&mut self, rhs: Rhs) | 对应 * 的原地修改版本 |
除并赋值 a /= b | std::ops::DivAssign | fn div_assign(&mut self, rhs: Rhs) | 对应 / 的原地修改版本 |
取余并赋值 a %= b | std::ops::RemAssign | fn rem_assign(&mut self, rhs: Rhs) | 对应 % 的原地修改版本 |
按位与 a & b | std::ops::BitAnd | fn bitand(self, rhs: Rhs) -> Output | & |
| 按位或 `a | b` | std::ops::BitOr | fn bitor(self, rhs: Rhs) -> Output |
按位异或 a ^ b | std::ops::BitXor | fn bitxor(self, rhs: Rhs) -> Output | ^ |
按位与并赋值 a &= b | std::ops::BitAndAssign | fn bitand_assign(&mut self, rhs: Rhs) | 对应 & 的原地修改版本 |
| 按位或并赋值 `a | = b` | std::ops::BitOrAssign | fn bitor_assign(&mut self, rhs: Rhs) |
按位异或并赋值 a ^= b | std::ops::BitXorAssign | fn bitxor_assign(&mut self, rhs: Rhs) | 对应 ^ 的原地修改版本 |
左移 a << b | std::ops::Shl | fn shl(self, rhs: Rhs) -> Output | << |
右移 a >> b | std::ops::Shr | fn shr(self, rhs: Rhs) -> Output | >> |
左移并赋值 a <<= b | std::ops::ShlAssign | fn shl_assign(&mut self, rhs: Rhs) | 对应 << 的原地修改版本 |
右移并赋值 a >>= b | std::ops::ShrAssign | fn shr_assign(&mut self, rhs: Rhs) | 对应 >> 的原地修改版本 |
下标 a[b] | std::ops::Index / IndexMut | fn index(&self, Idx) -> &Output / fn index_mut(&mut self, Idx) -> &mut Output | 常见操作符 |
比较相等 a == b, a != b | std::cmp::PartialEq / Eq | fn eq(&self, other: &Rhs) -> bool | 常见操作符 |
关系比较 <, <=, >, >= | std::cmp::PartialOrd / Ord | fn partial_cmp(&self, other: &Rhs) -> Option<Ordering> | 常见操作符 |
在 Rust 中,通过给自己的类型实现这些 Trait,来定制对应运算符的行为。
