Rust:Trait 标签 & 常见特征

Rust:Trait 标签 & 常见特征


标签

Rust中存在一种Trait,它内部没有任何方法,关联常量或者关联类型。它仅用作一种标签,标识某个类型具有某种性质,这种Trait就叫做标签。

Rust 一共提供了几个重要的标签:

  • Sized,用来标识编译期可确定大小的类型。
  • Unsize,目前该 trait 为实验特性,用于标识动态大小类型(DST)。
  • Copy,用来标识可以按位复制其值的类型。
  • Send,用来标识可以跨线程安全通信的类型。
  • Sync,用来标识可以在线程间安全共享引用的类型。
  • Unpin,类型没有"自引用"结构,可被安全地移动

除此之外,Rust 标准库还在增加新的标签以满足变化的需求。此处很多标签都涉及到后面的复杂机制,本博客只深入讲解SizedCopy


Sized

编译器用Sized识别可以在编译期确定大小的类型。

Sized 源代码如下:

rust 复制代码
#![lang = "sized"]
pub trait Sized {
    // 代码为空,无具体实现方法
}

Sized 是一个空 Trait,其作用仅作为标签 Trait 供编译器识别使用。真正赋予其"标签"功能的是 #![lang = "sized"] 这一属性。

lang 属性表明 SizedRust 语言内部使用的特殊项,称为语言项 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的写法。

?SizedSized 的一种特殊语法形式。要理解它们的关系,得先分清SizedUnsize

  • 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语言中的malllocrealloc来操作。你需要在堆区手动开一块内存,然后放入这个结构体,并且内存的大小至少应该是结构除去最后一个字段之前的所有字段的总大小。

超出这些部分的内存,全部变为最后一个元素的动态数组的大小。你可以对这个内存扩容缩容,最终影响到的都是结构体最后一个字段的大小。


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自动实现CloneCopy

Rust 为很多基本数据类型实现了 Copy,比如常用的数字类型、字符(Char)、布尔类型、单元值、不可变引用等。

但比如说String 这种集合类型类型往往没有实现 Copy,它们涉及到堆区的分配,需要进行深拷贝。

CopyClone涉及到的拷贝相关性质,会在所有权章节深入讲解。


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


对于这种与操作符相关的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,来定制对应运算符的行为。


相关推荐
我命由我123451 小时前
微信小程序 - 避免在 data 初始化中引用全局变量
开发语言·前端·javascript·微信小程序·小程序·前端框架·js
liulilittle1 小时前
C++ SSE/AVX/SHA/AES指令集检查,用于程序定向优化。
开发语言·c++·cpu·asm·detect·il·features
小龙在山东1 小时前
基于C++空项目运行汇编语言
开发语言·c++
MM_MS1 小时前
WinForm+C#小案例--->写一个记事本程序
开发语言·计算机视觉·c#·visual studio
韩立学长1 小时前
基于Springboot儿童福利院规划管理系统o292y1v8(程序、源码、数据库、调试部署方案及开发环境)系统界面展示及获取方式置于文档末尾,可供参考。
数据库·spring boot·后端
y1y1z1 小时前
Spring国际化
java·后端·spring
郝学胜-神的一滴1 小时前
Linux信号屏蔽字详解:原理、应用与实践
linux·服务器·开发语言·c++·程序人生
沐知全栈开发1 小时前
CSS 创建:从基础到实践
开发语言
weixin_307779131 小时前
Jenkins ASM API 插件:详解与应用指南
java·运维·开发语言·后端·jenkins