Skip to content

常用 Trait

很多 trait 你可能每天在用,但未必清楚它们背后的语义、相互关系,以及什么时候应该实现它们。

From / Into

在类型系统里,转换是绕不开的需求。Rust 里最核心的两个转换 Trait 就是 FromInto

  • From<T> for U:如何把 T 转成 U
  • Into<T> for U:如何把 U 转成 T

这两个Trait的功能刚好相反。

标准库规定了一个重要关系:

只要你为 U 实现了 From<T>,就自动T 实现了 Into<U>

也就是说,你几乎只需要实现 From,不要手写 Into

Into 通常用于调用方一侧,让调用变得简洁、统一;而 From 则是实现方声明“我知道怎么从某种类型安全地构造自己”。

rust
let s: String = String::from("hello"); // From<&str> for String
let s2: String = "world".into();       // Into<String> for &str

From包含一个核心方法,签名如下:

rust
pub trait From<T>: Sized {
    fn from(value: T) -> Self;
}

例如:

rust
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

自己实现时,一般这样写:

rust
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):

rust
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,获取某种类型的引用。

rust
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_refas_mut就可以拿到它的指定类型的借用。

那么有人可能要问了,我想要拿到一个借用,直接用&&mut不就好了?为什么非要去调用这方法?

其实这个地方就是对这两个Trait有误解,它的目的不是为了让你拿到一个借用,而是保证可以拿到指定类型的借用。区别在于,后者有一个”指定类型“的要求。

rust
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然后输出。外层分别传入了&strString,那么就会报错,因为String&str类型不兼容。

这个时候就可以通过as_ref拿到String内部的原生字符串借用,然后在传给函数,并且这个过程是借用,没有发生拷贝,当show_str输出完毕,原本的String还可以继续使用。

但是现在还不够优雅,因为外层的调用者需要考虑自己传的类型,做出对应的转换。

对于show_str来说,它其实并不关心接收到的参数是什么,而应该考虑接收到的参数能不能拿到一个&str进行输出,因此可以把s作为一个泛型:

rust
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,从而进行输出。

对于外层来说,直接传入&strString都是合法的,用户完全感知不到这个as_ref的存在,只需要知道任何类型的字符串传进去,它就可以输出。

因此AsRef其实本质上是一种类型转化和借用的综合语义。它非常类似于Into,不同的是Into类型转换完毕后会剥夺所有权,而AsRef只是把类型内部的某些东西借出来,用完就归还所有权。

Default

Default 用来给类型一个默认值。

签名:

rust
pub trait Default {
    fn default() -> Self;
}

实现之后,可以使用default函数直接拿到一个类型的默认值。

标准库很多地方会用到 T: Default,例如 Option<T>::default()None

rust
#[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)]

如果想自定义默认逻辑,可以手写:

rust
impl Default for Config {
    fn default() -> Self {
        Self {
            debug: false,
            retries: 3,
        }
    }
}

Debug

Debug 是最常用的调试打印 trait,对应 {:#?} / {:?} 这种格式化。

定义在 std::fmt 模块中:

rust
pub trait Debug {
    fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), Error>;
}

大部分时候不用自己实现,直接 #[derive(Debug)] 即可:

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

fn main() {
    let p = Point { x: 3, y: 4 };
    println!("{:?}", p);
    println!("{:#?}", p);
}

输出结果:

rust
Point { x: 3, y: 4 }
Point {
    x: 3,
    y: 4,
}

实现Debug后,结构体默认输出所有的键值对。可以看出{:?}输出更紧凑,而{#?}则会将结构体展开。

Display

Display 对应 {},它与 Debug 不同:

  • Debug:为开发者准备,通常结构化输出,可能包含实现细节
  • Display:为用户准备,通常是人类易读的字符串描述

Display 也是个格式化 trait:

rust
pub trait Display {
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result;
}

Debug 不同的是,Display 不能 derive,通常需要你自定义输出格式。

rust
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 为例:

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

    fn add(self, rhs: Rhs) -> Self::Output;
}
  • Rhs:右操作数类型(默认是 Self
  • Output:返回值类型(关联类型)

类似的有:

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

示例:实现向量加法

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

也可以让左右类型不同:

rust
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 为例:

rust
pub trait AddAssign<Rhs = Self> {
    fn add_assign(&mut self, rhs: Rhs);
}

相比于基本的计算Add,这个版本拿到的是&mut self,会对自己做修改,并且没有返回值。

其他类似:

rust
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用于控制表达式前的负号:

rust
pub trait Neg {
    type Output;
    fn neg(self) -> Self::Output;
}

示例:

rust
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 实现的。

rust
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这个泛型,并为其专门实现了方法,返回一个数组切片。

rust
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)等效。

示例:

rust
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个元素,因此特地为这个类型实现了IndexIndexMut,让它可以正确拿到元素。

对于这种与操作符相关的Trait还有非常非常多,这里不一一介绍了,实现都是类似的,需要时查询即可:

运算符 / 语义对应 Trait签名(核心方法)备注
一元负号 -xstd::ops::Negfn neg(self) -> Output取负数,如 -x
逻辑/按位非 !xstd::ops::Notfn not(self) -> Outputbool:逻辑非;对整数:按位非
加法 a + bstd::ops::Addfn add(self, rhs: Rhs) -> Output二元 +
减法 a - bstd::ops::Subfn sub(self, rhs: Rhs) -> Output二元 -
乘法 a * bstd::ops::Mulfn mul(self, rhs: Rhs) -> Output*
除法 a / bstd::ops::Divfn div(self, rhs: Rhs) -> Output/
取余 a % bstd::ops::Remfn rem(self, rhs: Rhs) -> Output%
加并赋值 a += bstd::ops::AddAssignfn add_assign(&mut self, rhs: Rhs)对应 + 的原地修改版本
减并赋值 a -= bstd::ops::SubAssignfn sub_assign(&mut self, rhs: Rhs)对应 - 的原地修改版本
乘并赋值 a *= bstd::ops::MulAssignfn mul_assign(&mut self, rhs: Rhs)对应 * 的原地修改版本
除并赋值 a /= bstd::ops::DivAssignfn div_assign(&mut self, rhs: Rhs)对应 / 的原地修改版本
取余并赋值 a %= bstd::ops::RemAssignfn rem_assign(&mut self, rhs: Rhs)对应 % 的原地修改版本
按位与 a & bstd::ops::BitAndfn bitand(self, rhs: Rhs) -> Output&
按位或 `ab`std::ops::BitOrfn bitor(self, rhs: Rhs) -> Output
按位异或 a ^ bstd::ops::BitXorfn bitxor(self, rhs: Rhs) -> Output^
按位与并赋值 a &= bstd::ops::BitAndAssignfn bitand_assign(&mut self, rhs: Rhs)对应 & 的原地修改版本
按位或并赋值 `a= b`std::ops::BitOrAssignfn bitor_assign(&mut self, rhs: Rhs)
按位异或并赋值 a ^= bstd::ops::BitXorAssignfn bitxor_assign(&mut self, rhs: Rhs)对应 ^ 的原地修改版本
左移 a << bstd::ops::Shlfn shl(self, rhs: Rhs) -> Output<<
右移 a >> bstd::ops::Shrfn shr(self, rhs: Rhs) -> Output>>
左移并赋值 a <<= bstd::ops::ShlAssignfn shl_assign(&mut self, rhs: Rhs)对应 << 的原地修改版本
右移并赋值 a >>= bstd::ops::ShrAssignfn shr_assign(&mut self, rhs: Rhs)对应 >> 的原地修改版本
下标 a[b]std::ops::Index / IndexMutfn index(&self, Idx) -> &Output / fn index_mut(&mut self, Idx) -> &mut Output常见操作符
比较相等 a == b, a != bstd::cmp::PartialEq / Eqfn eq(&self, other: &Rhs) -> bool常见操作符
关系比较 <, <=, >, >=std::cmp::PartialOrd / Ordfn partial_cmp(&self, other: &Rhs) -> Option<Ordering>常见操作符

Rust 中,通过给自己的类型实现这些 Trait,来定制对应运算符的行为。

MIT Licensed