Rust:Trait 标签 & 常见特征
标签
在Rust中存在一种Trait,它内部没有任何方法,关联常量或者关联类型。它仅用作一种标签,标识某个类型具有某种性质,这种Trait就叫做标签。
Rust 一共提供了几个重要的标签:
Sized,用来标识编译期可确定大小的类型。Unsize,目前该 trait 为实验特性,用于标识动态大小类型(DST)。Copy,用来标识可以按位复制其值的类型。Send,用来标识可以跨线程安全通信的类型。Sync,用来标识可以在线程间安全共享引用的类型。Unpin,类型没有"自引用"结构,可被安全地移动
除此之外,Rust 标准库还在增加新的标签以满足变化的需求。此处很多标签都涉及到后面的复杂机制,本博客只深入讲解Sized和Copy。
Sized
编译器用Sized识别可以在编译期确定大小的类型。
Sized 源代码如下:
rust
#![lang = "sized"]
pub trait Sized {
// 代码为空,无具体实现方法
}
Sized 是一个空 Trait,其作用仅作为标签 Trait 供编译器识别使用。真正赋予其"标签"功能的是 #![lang = "sized"] 这一属性。
lang 属性表明 Sized 是 Rust 语言内部使用的特殊项,称为语言项 Lang Item,通过这种声明,编译器才能知道 Sized trait 是语言内置的特殊标识,从而理解它的作用和使用方式。
简单说,这个 lang 属性就像给 Sized 盖了个官方印章,告诉编译器:"这是我(Rust 语言)自己用的特殊 trait,编译器要有识别它的能力,并且按照我规定的逻辑去处理带有这个 trait 的类型"。
这样编译器在遇到 Sized 时,就知道它是用来标记"编译期可确定大小的类型"的,而不是一个普通的自定义 Trait。
类似的机制也体现在加号操作中:当执行 a + b 这样的整数相加时,编译器会自动关联到 Add::add(a, b) 实现,这正是因为加号操作对应着 #![lang="add"] 语言项,使得编译器能够正确解析加法运算的逻辑。
在Rust里,多数类型默认都具备Sized特性。这意味着在定义泛型结构体时,即便不特意写明Sized的限制,编译器也会默认加上。
示例:
rust
struct Foo<T>(T);
struct Bar<T: ?Sized>(T);
这里的Foo虽然没显式标注,但实际上等同于Foo<T: Sized>。如果想让结构体支持动态大小的类型,就得用<T: ?Sized>来限定,比如Bar的写法。
?Sized是Sized 的一种特殊语法形式。要理解它们的关系,得先分清Sized和Unsize:
Sized用于标记那些在编译时就能确定大小的类型Unsize相反,专门标记动态大小类型
?Sized的作用范围更宽,它既包含Sized类型,也涵盖Unsize类型,可以理解为:不保证这个类型大小,既可能是动态大小,也可能是非动态大小。
不过动态大小类型的使用有严格限制,必须遵守这三条规则:
- 只能通过胖指针(比如
&[T]或&Trait)来操作Unsize类型 - 变量、函数参数和枚举成员都不能直接使用动态大小类型
- 结构体中,只有最后一个字段允许使用动态大小类型,其他字段则不行
rust
#[repr(C)]
struct WithTail {
a: u32,
b: [u8],
}
fn main() {
// let x = WithTail { a: 1, b: [1, 2, 3] };
let _p: &WithTail;
let _p_mut: &mut WithTail;
}
以上代码中,WithTail结构体的最后一个字段是 [u8],这是一个动态大小类型,那么WithTail这个结构体也就是一个动态大小类型。
你无法通过let直接接收它,从而把它放在栈区,只能拿到它的借用。对于这种结构体,需要通过非常原始的内存操作,类似于C语言中的mallloc和realloc来操作。你需要在堆区手动开一块内存,然后放入这个结构体,并且内存的大小至少应该是结构除去最后一个字段之前的所有字段的总大小。

超出这些部分的内存,全部变为最后一个元素的动态数组的大小。你可以对这个内存扩容缩容,最终影响到的都是结构体最后一个字段的大小。
Copy
Copy 用来标记可以按位复制其值的类型,按位复制等价于 C 语言中的 memcpy。
Copy源码:
rust
#![lang = "copy"]
pub trait Copy: Clone {
// 代码为空,无具体实现方法
}
与Sized相同是一个空Trait,它通过语言项#![lang = "copy"]来完成功能。不一样的是,它继承了Clone。
Clone源码:
rust
pub trait Clone: Sized {
fn clone(&self) -> Self;
fn clone_from(&mut self, source: &Self) {
*self = source.clone()
}
}
Clone 继承自 Sized,意味着要实现 Clone 的对象必须是 Sized 类型。
Clone内含两个方法,clone_from 方法有默认的实现,并且其默认实现是调用 clone 方法,所以对于要实现 Clone 的对象,只需要实现 clone 方法就可以了。
如果想让一个类型实现 Copy,就必须同时实现 Clone。
rust
struct MyStruct;
impl Copy for MyStruct {}
impl Clone for MyStruct {
fn clone(&self) -> MyStruct {
*self
}
}
在实现Copy时,Clone一律返回*self。如果clone涉及到深拷贝等问题,那么就不能实现Copy进行按位拷贝。
由于对于所有类型,如果要实现copy时,clone的实现是相同的,因此Rust提供了更简单的属性来快速实现:
rust
#[derive(Copy, Clone)]
struct MyStruct;
此处的 #[derive(Copy, Clone)]属性,就会给MyStruct自动实现Clone和Copy。
Rust 为很多基本数据类型实现了 Copy,比如常用的数字类型、字符(Char)、布尔类型、单元值、不可变引用等。
但比如说String 这种集合类型类型往往没有实现 Copy,它们涉及到堆区的分配,需要进行深拷贝。
Copy和Clone涉及到的拷贝相关性质,会在所有权章节深入讲解。
常用 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 则是实现方声明"我知道怎么从某种类型安全地构造自己"。
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_ref和as_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然后输出。外层分别传入了&str和String,那么就会报错,因为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,从而进行输出。
对于外层来说,直接传入&str和String都是合法的,用户完全感知不到这个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); }
rust
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个元素,因此特地为这个类型实现了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,来定制对应运算符的行为。