Skip to content
Published at:

13. Utility Traits — 实用 Trait

Rust 标准库定义了一系列“实用 trait”——它们不像 AddIterator 那样对应具体的语言特性,而是为常见的类型操作提供统一的词汇和接口。这些 trait 大致分为三类:语言扩展 trait(影响编译器对类型的处理)、标记 trait(承诺某种属性)和公共词汇 trait(为常见转换和操作提供统一接口)。

掌握这些 trait 是用 Rust 高效编程的关键——它们构成了 Rust 生态系统的共同语言。

Drop:析构函数

Drop 是唯一一个编译器直接调用其方法的 trait——当值离开作用域时,Rust 自动调用 drop(&mut self)

rust
pub trait Drop {
    fn drop(&mut self);
}

Drop 的典型使用场景:

rust
// 管理 C 语言文件描述符
struct FileDesc {
    fd: i32,
}

impl Drop for FileDesc {
    fn drop(&mut self) {
        unsafe { libc::close(self.fd); }
    }
}

// 管理锁的自动释放
struct RaiiLock<'a> {
    mutex: &'a Mutex,
}

impl<'a> Drop for RaiiLock<'a> {
    fn drop(&mut self) {
        self.mutex.unlock();
    }
}

Rust 保证 drop 在以下情况下被调用:

  • 变量离开作用域
  • 表达式语句末尾(临时值)
  • panic! 展开栈时(除非 panic = 'abort'
  • 结构体字段(按声明顺序的逆序调用每个字段的 drop

std::mem::drop 是一个提前释放值的方便函数(已在 prelude 中):

rust
let data = vec![1, 2, 3];
// ... 使用 data ...
drop(data);  // 提前释放
// data 不可再访问

重要限制:DropCopy 互斥。编译器禁止同时实现两者,因为 Copy 意味着隐式按位复制,而 Drop 类型通常管理唯一资源,按位复制会破坏资源所有权。

Sized:编译时已知大小

Sized 是一个标记 trait,表示类型的大小在编译时已知。Rust 中几乎所有类型都是 Sized 的:

rust
// 以下都是 Sized
u64                    // 8 字节
(usize, char)          // 固定大小
Vec<String>            // Vec 本身是 3 个指针(24 字节),虽然它管理堆上的动态数据
Box<dyn Fn()>          // Box 是固定大小(1 个指针)

// 以下不是 Sized
str                    // 字符串切片——大小在运行时决定
[T]                    // 任意类型的切片——大小在运行时决定
dyn std::io::Write     // trait 对象——大小取决于具体类型

泛型类型参数默认带有隐式的 Sized 约束。用 ?Sized 放宽此约束:

rust
// T 默认是 Sized 的
fn process<T>(value: &T) { /* ... */ }
// 等价于
fn process<T: Sized>(value: &T) { /* ... */ }

// 放宽约束:允许 unsized 类型
fn process_unsized<T: ?Sized>(value: &T) {
    // 此时只能通过引用使用 T(因为 T 的大小未知)
}

结构体的最后一个字段可以是 unsized 的——这使结构体本身也成为 unsized:

rust
struct UnsizedExample {
    tag: u32,
    data: [u8],  // 最后一个字段是 unsized——结构体本身也成为 unsized
}

Clone:显式深拷贝

Clone 用于创建值的独立副本——通过显式的 clone() 方法调用:

rust
pub trait Clone: Sized {
    fn clone(&self) -> Self;
    fn clone_from(&mut self, source: &Self) { ... }
}

标准库中大多数类型都实现了 Clone

rust
let s = String::from("hello");
let s2 = s.clone();  // 深拷贝——独立的堆分配
assert_eq!(s, s2);

let v = vec![1, 2, 3];
let v2 = v.clone();  // 每个元素都克隆

clone_from 的默认实现是 *self = source.clone(),但可以覆写以复用已有分配:

rust
// 高效的 clone_from:复用 Vec 的缓冲区
let mut target = vec![0; 100];
target.clone_from(&source);  // 可能复用 target 的已有缓冲区

通常用 #[derive(Clone)] 自动生成实现——前提是所有字段都实现了 Clone。但有些类型不实现 CloneMutex<T>(共享状态不应轻易克隆)、File(文件句柄是唯一资源)。

File 提供 try_clone() 代替——它返回 Result,因为 OS 可能拒绝复制文件描述符:

rust
let file = File::open("data.txt")?;
let file2 = file.try_clone()?;  // 可能失败的克隆

Copy:隐式按位复制

CopyClone 的子 trait,标记那些可以通过按位复制安全复制的类型:

rust
pub trait Copy: Clone { }

Copy 类型的行为类似于 C 中的值类型——赋值时自动复制而非移动:

rust
let x = 42;
let y = x;    // x 被复制,而非移动
println!("{x}");  // x 仍然可用

let p = Point { x: 1.0, y: 2.0 };
let q = p;    // p 被复制
println!("{:?}", p);  // p 仍然可用

一个类型能成为 Copy 的条件:

  • 所有字段都实现 Copy
  • 类型未实现 Drop
  • 类型的任何组件都不持有唯一资源(如堆内存、文件句柄)

通常用 #[derive(Copy, Clone)] 同时派生两者:

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

#[derive(Copy, Clone, Debug)]
enum BinaryOp {
    Add,
    Sub,
    Mul,
    Div,
}

Debug 和 Display:格式化输出

Debug

Debug 用于调试输出——{:?}{:#?} 格式化说明符:

rust
use std::fmt;

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

几乎每个类型都应该实现 Debug。用 #[derive(Debug)] 自动生成:

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

let p = Point { x: 3.0, y: 4.0 };
println!("{:?}", p);    // Point { x: 3.0, y: 4.0 }
println!("{:#?}", p);   // 带缩进的多行格式

手动实现——当你需要控制调试输出格式时:

rust
impl fmt::Debug for Point {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "Point({}, {})", self.x, self.y)
    }
}

Display

Display 用于面向用户的输出——{} 格式化说明符:

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

Display 不能通过 #[derive] 自动生成——你需要决定如何向用户展示你的类型:

rust
impl fmt::Display for Point {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "({}, {})", self.x, self.y)
    }
}

let p = Point { x: 3.0, y: 4.0 };
println!("点坐标:{p}");  // 点坐标:(3, 4)
println!("{p}");          // (3, 4)

Formatter 提供了丰富的格式化选项——对齐、填充、精度、符号等:

rust
impl fmt::Display for Point {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        // 尊重调用者的精度和填充要求
        write!(f, "({:.2}, {:.2})", self.x, self.y)
    }
}

经验法则Debug#[derive](几乎每个类型都需要),Display 手工实现(面向用户的输出)。

Default:合理的默认值

Default 为类型提供“空或零”的默认值:

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

常见用法:

rust
// 集合类型返回空实例
let v: Vec<String> = Default::default();  // Vec::new()
let m: HashMap<String, i32> = Default::default();  // HashMap::new()

// 与结构体更新语法结合
#[derive(Default)]
struct Config {
    host: String,               // default: ""
    port: u16,                  // default: 0
    max_connections: u32,       // default: 0
    timeout_ms: u64,            // default: 0
}

let config = Config {
    host: "prod.example.com".to_string(),
    port: 443,
    ..Default::default()  // 其余字段用默认值
};

// Rust 标准库为常见类型自动提供 Default
// Option<T>: Default = None
// 数值类型: Default = 0
// bool: Default = false
// 引用: Default 不可用

#[derive(Default)] 要求所有字段都实现 Default

标准库中的空白et实现:

rust
// 智能指针自动获得 Default
// Rc<T>, Arc<T>, Box<T>, Cell<T>, RefCell<T>, Mutex<T>, RwLock<T>

// 元组自动获得 Default(当所有元素都是 Default 时)
let t: (i32, bool, String) = Default::default();  // (0, false, "")

PartialEq, Eq, PartialOrd, Ord, Hash

这组 trait 定义了两个值是否可以以及如何比较和哈希。它们是 Rust 集合类型(HashMapBTreeMapHashSet)的基础。

PartialEq 和 Eq

rust
pub trait PartialEq<Rhs: ?Sized = Self> {
    fn eq(&self, other: &Rhs) -> bool;
    fn ne(&self, other: &Rhs) -> bool { !self.eq(other) }
}

pub trait Eq: PartialEq<Self> {
    // 标记 trait——没有新方法
}

Eq 要求等价关系的三个性质都成立:自反性(x == x)、对称性(x == y => y == x)、传递性(x == y && y == z => x == z)。f32f64 因为 NaN 的特殊行为只实现了 PartialEq,未实现 Eq

PartialOrd 和 Ord

rust
pub trait PartialOrd<Rhs: ?Sized = Self>: PartialEq<Rhs> {
    fn partial_cmp(&self, other: &Rhs) -> Option<Ordering>;
    fn lt(&self, other: &Rhs) -> bool;
    fn le(&self, other: &Rhs) -> bool;
    fn gt(&self, other: &Rhs) -> bool;
    fn ge(&self, other: &Rhs) -> bool;
}

pub trait Ord: Eq + PartialOrd<Self> {
    fn cmp(&self, other: &Self) -> Ordering;
}

Ord 保证全序——任意两个值都可以比较出 LessEqualGreater。这是排序算法(sort())的要求。

Hash

rust
pub trait Hash {
    fn hash<H: Hasher>(&self, state: &mut H);
    fn hash_slice<H: Hasher>(data: &[Self], state: &mut H) { ... }
}

Hash 用于 HashMapHashSet。关键契约:如果 a == b,则 hash(a) == hash(b)。但反过来不成立——哈希冲突是允许的。

通常用 #[derive(PartialEq, Eq, Hash)] 自动生成:

rust
#[derive(Debug, PartialEq, Eq, Hash)]
struct Person {
    name: String,
    age: u32,
}

let mut people = HashMap::new();
people.insert(
    Person { name: "Alice".into(), age: 30 },
    "Engineer",
);

From 和 Into:类型转换

FromInto 是非对称的消耗性转换接口。给定 From,标准库自动提供反向的 Into

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

pub trait Into<T>: Sized {
    fn into(self) -> T;
}

// 标准库的空白et实现
impl<T, U> Into<U> for T
where
    U: From<T>,
{
    fn into(self) -> U {
        U::from(self)
    }
}

From

From<T> 作为泛型构造函数使用——它总是成功的(不返回 Result):

rust
// 从字符串切片创建 String
let s = String::from("hello");

// 从迭代器创建集合
let v = Vec::from([1, 2, 3]);

// 在泛型代码中
fn new_collection<T: From<i32>>(n: i32) -> T {
    T::from(n)
}

// 自定义实现
#[derive(Debug)]
struct Celsius(f64);

impl From<f64> for Celsius {
    fn from(f: f64) -> Celsius {
        Celsius(f)
    }
}

let temp: Celsius = 25.0.into();  // 使用 Into
let temp = Celsius::from(36.5);   // 使用 From

Into

函数参数中常用 Into 接受多种输入类型:

rust
fn save_data(path: impl Into<PathBuf>, data: &[u8]) -> io::Result<()> {
    let path: PathBuf = path.into();
    std::fs::write(path, data)
}

// 接受字符串切片、String、PathBuf 等各种类型
save_data("output.txt", b"hello")?;
save_data(String::from("output.txt"), b"hello")?;
save_data(PathBuf::from("output.txt"), b"hello")?;

注意字符串转换的成本差异:

  • String::from("hello") 需要分配新缓冲区并复制数据——有开销
  • String::from(vec![72, 69, 76, 76, 79]) 直接将 Vec<u8> 的缓冲区转为 String——零开销(前提是有效的 UTF-8)

? 运算符使用 From 自动转换错误类型:

rust
fn read_config() -> Result<Config, Box<dyn Error>> {
    let content = std::fs::read_to_string("config.toml")?;
    // read_to_string 返回 io::Error
    // ? 运算符自动用 From 转换为 Box<dyn Error>
    // ...
    Ok(config)
}

TryFrom 和 TryInto:可失败转换

From/Into 假设转换总是成功。当转换可能失败时,使用 TryFrom/TryInto

rust
pub trait TryFrom<T>: Sized {
    type Error;
    fn try_from(value: T) -> Result<Self, Self::Error>;
}

pub trait TryInto<T>: Sized {
    type Error;
    fn try_into(self) -> Result<T, Self::Error>;
}

经典用例——整数类型之间的窄化转换:

rust
use std::convert::TryFrom;

let big: i64 = 1_000_000;
let small: i32 = i32::try_from(big)?;  // Ok(1000000)

let too_big: i64 = 3_000_000_000;
let result: Result<i32, _> = i32::try_from(too_big);
assert!(result.is_err());  // 溢出错误

注意 i32 实现了 TryFrom<i64>,但没有实现 From<i64>——因为窄化转换可能丢失数据。

使用 TryFrom 进行验证性构造:

rust
#[derive(Debug, PartialEq)]
struct Percentage(u8);

impl TryFrom<f64> for Percentage {
    type Error = &'static str;

    fn try_from(value: f64) -> Result<Self, Self::Error> {
        if value < 0.0 || value > 100.0 {
            Err("百分比必须在 0 到 100 之间")
        } else {
            Ok(Percentage(value as u8))
        }
    }
}

let p = Percentage::try_from(75.0)?;
assert_eq!(p, Percentage(75));

let err = Percentage::try_from(150.0);
assert!(err.is_err());

AsRef 和 AsMut:廉价借用转换

AsRefAsMut 用于从 &self&T(或 &mut self&mut T)的廉价引用转换:

rust
pub trait AsRef<T: ?Sized> {
    fn as_ref(&self) -> &T;
}

pub trait AsMut<T: ?Sized> {
    fn as_mut(&mut self) -> &mut T;
}

核心用途:让函数参数灵活接受多种引用类型:

rust
// std::fs::File::open 的真实签名
fn open<P: AsRef<Path>>(path: P) -> io::Result<File>;

// 因此可以接受多种类型
File::open("config.toml")?;           // &str
File::open(String::from("c.toml"))?;  // String
File::open(PathBuf::from("c.toml"))?; // PathBuf
File::open(&path_buf)?;               // &PathBuf

AsRef 是廉价操作——不涉及所有权转移或内存分配。与 From 不同,AsRef 只借用,不消耗值。

标准库中的常见实现:

rust
// String 实现多种 AsRef
impl AsRef<str> for String { ... }     // String -> &str
impl AsRef<[u8]> for String { ... }    // String -> &[u8]
impl AsRef<Path> for String { ... }    // String -> &Path
impl AsRef<OsStr> for String { ... }   // String -> &OsStr

// 注意:String 不实现 AsMut<[u8]>——因为这可能破坏 UTF-8 有效性
// 但 Vec<u8> 实现了 AsMut<[u8]>

// 空白et实现:&T 也会实现 AsRef<U>(当 T: AsRef<U> 时)
// 这意味着你可以传 &String 到接受 impl AsRef<str> 的函数

自定义实现:

rust
struct Document {
    content: String,
    author: String,
}

impl AsRef<str> for Document {
    fn as_ref(&self) -> &str {
        &self.content
    }
}

// 现在 Document 可以传给接受 &str 的函数
fn print_content(s: &str) {
    println!("{s}");
}

let doc = Document {
    content: "Hello World".into(),
    author: "Alice".into(),
};
print_content(doc.as_ref());

Borrow 和 BorrowMut:哈希一致借用

BorrowBorrowMutAsRef/AsMut 相似,但有一个关键附加契约:借用的值与原值的哈希和等价性必须一致

rust
pub trait Borrow<Borrowed: ?Sized> {
    fn borrow(&self) -> &Borrowed;
}

pub trait BorrowMut<Borrowed: ?Sized>: Borrow<Borrowed> {
    fn borrow_mut(&mut self) -> &mut Borrowed;
}

这个额外保证使 Borrow 成为哈希表查找的关键 trait:

rust
// HashMap::get 的真实签名
impl<K, V> HashMap<K, V> {
    pub fn get<Q>(&self, k: &Q) -> Option<&V>
    where
        K: Borrow<Q>,
        Q: Hash + Eq + ?Sized,
    { ... }
}

为什么这很重要:

rust
let mut map = HashMap::new();
map.insert(String::from("key"), 42);

// 不需要构造 String 就能查找——直接用 &str
let value = map.get("key");  // "key" 是 &str,不是 String
assert_eq!(value, Some(&42));

Borrow 保证 "key".hash() == String::from("key").hash()"key" == String::from("key")——所以用 &str 查找 HashMap<String, V> 能正确工作。

标准实现:

  • T: Borrow<T> —— 每个类型都是自己的借用——这是 trivial 实现
  • String: Borrow<str> —— 字符串可以借为 &str
  • Vec<T>: Borrow<[T]> —— 向量可以借为切片
  • [T; N]: Borrow<[T]> —— 数组可以借为切片
  • PathBuf: Borrow<Path> —— 路径缓冲可以借为路径

ToOwned:从引用到拥有值

ToOwnedClone 的泛化——它允许从引用产生拥有值,且拥有值的类型可能不同于引用类型:

rust
pub trait ToOwned {
    type Owned: Borrow<Self>;
    fn to_owned(&self) -> Self::Owned;
    fn clone_into(&self, target: &mut Self::Owned) { ... }
}

标准库的关键实现:

rust
// str -> String(to_owned 返回 String,不同于 clone 返回 str)
impl ToOwned for str {
    type Owned = String;
    fn to_owned(&self) -> String { ... }
}

// 这与 Clone 不同!
// str::clone() 返回 &str(引用),不分配
// str::to_owned() 返回 String(拥有值),分配新内存

// [T] -> Vec<T>
impl<T: Clone> ToOwned for [T] {
    type Owned = Vec<T>;
    fn to_owned(&self) -> Vec<T> { ... }
}

// Path -> PathBuf
impl ToOwned for Path {
    type Owned = PathBuf;
    fn to_owned(&self) -> PathBuf { ... }
}

Clone vs ToOwned

方面CloneToOwned
返回类型必须为 Self可以是其他类型(Self::Owned
适用场景同类型拷贝从引用产生拥有值
str::clone()返回 &str(不分配)N/A
str::to_owned()N/A返回 String(分配)

Cow:Clone-on-Write

Cow<'a, B>(Clone-on-Write)利用 ToOwned 实现延迟克隆——只有需要修改时才克隆:

rust
pub enum Cow<'a, B: ?Sized + 'a>
where
    B: ToOwned,
{
    Borrowed(&'a B),
    Owned(<B as ToOwned>::Owned),
}

Cow 实现了 Deref<Target = B>——像共享引用一样使用它。只有调用 to_mut() 时才会触发克隆:

rust
use std::borrow::Cow;

fn process(data: Cow<'_, str>) -> Cow<'_, str> {
    if data.contains("secret") {
        // to_mut():如果当前是 Borrowed,克隆为 Owned(String)
        // 如果已经是 Owned,直接返回 &mut String
        let mut owned = data.into_owned();
        owned = owned.replace("secret", "***");
        Cow::Owned(owned)
    } else {
        data  // 没有修改——不分配
    }
}

// 传入引用——不分配,除非需要修改
let result = process(Cow::Borrowed("hello world"));
assert_eq!(result, "hello world");  // 仍然 Borrowed,无分配

let result = process(Cow::Borrowed("the secret is 42"));
assert_eq!(result, "the *** is 42");  // 变成了 Owned

Cow 的常见应用:返回静态字符串常量或动态格式化的字符串,避免不必要的分配:

rust
fn describe_error(code: i32) -> Cow<'static, str> {
    match code {
        0 => Cow::Borrowed("Success"),
        1 => Cow::Borrowed("Not Found"),
        2 => Cow::Borrowed("Permission Denied"),
        _ => Cow::Owned(format!("Unknown error code: {code}")),
    }
}

实战:构建类型转换生态

以下是一个综合示例,展示多个实用 trait 如何协同工作:

rust
use std::borrow::Cow;
use std::convert::{From, TryFrom};
use std::fmt;
use std::hash::Hash;

#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)]
struct UserId(u64);

impl fmt::Display for UserId {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "user:{}", self.0)
    }
}

impl From<u64> for UserId {
    fn from(id: u64) -> Self {
        UserId(id)
    }
}

impl TryFrom<i64> for UserId {
    type Error = &'static str;
    fn try_from(id: i64) -> Result<Self, Self::Error> {
        if id < 0 {
            Err("User ID must be non-negative")
        } else {
            Ok(UserId(id as u64))
        }
    }
}

impl AsRef<u64> for UserId {
    fn as_ref(&self) -> &u64 {
        &self.0
    }
}

// 使用
let id = UserId::from(42);
println!("{id}");           // user:42
println!("{:?}", id);       // UserId(42)

let raw: &u64 = id.as_ref();
assert_eq!(*raw, 42);

let id = UserId::default();
assert_eq!(id, UserId(0));

let result = UserId::try_from(100i64);
assert!(result.is_ok());

let result = UserId::try_from(-5i64);
assert!(result.is_err());

Trait 关系图

Sized(编译时已知大小)
  └── Clone(显式克隆)
       └── Copy(隐式按位复制,与 Drop 互斥)

Drop(析构函数——与 Copy 互斥)

PartialEq(部分等价关系——可能不可比较)
  └── Eq(完全等价关系)

PartialEq + PartialOrd(部分顺序关系)
  └── Eq + PartialOrd + Ord(全序关系)

Hash(哈希——要求与 Eq 一致)

Debug(调试输出:{:?})
Display(面向用户输出:{})

Default(默认值)

From<T> ──自动提供──> Into<T>
TryFrom<T> ──自动提供──> TryInto<T>

AsRef<T> / AsMut<T>(廉价借用转换)
Borrow<T> / BorrowMut<T>(哈希一致借用)

Borrow<T> ──关联──> ToOwned(引用到拥有值)
    └── 组合为 Cow<'a, B>(Clone-on-Write)

与其它语言的对比

概念RustC++Java
析构函数Drop trait~ClassName()finalize() (deprecated) / try-with-resources
拷贝控制Clone/Copy 显式分离拷贝构造 + 赋值运算符clone() / Cloneable
调试输出Debug trait({:?}operator<<(或自定义)toString()
类型转换From/Into/TryFrom(trait)转换构造 / operator T()无标准接口
默认值Default trait默认构造无参构造
借用AsRef/Borrow隐式转换 / string_view无直接对应
延迟克隆Cow 类型std::shared_ptr + const无直接对应

小结

  • Drop 是析构函数——自动在值离开作用域时调用,用于释放 Rust 不了解的外部资源。与 Copy 互斥。
  • Clone 和 Copy 分离显式深拷贝和隐式按位复制。Copy 类型没有所有权问题——像 C 的值类型一样工作。
  • Debug 和 Display 提供两种格式化输出:{:?} 用于调试(可 derive),{} 用于面向用户(手动实现)。
  • Default 提供“空或零”的默认值,广泛用于结构体更新语法和泛型容器初始化。
  • PartialEq/Eq/PartialOrd/Ord/Hash 构成值的比较和哈希体系。f32/f64 因 NaN 的“不自反”特性只实现了 PartialEq,未实现 Eq
  • From/Into 是所有权转换的标准接口——From 用作泛型构造函数。TryFrom/TryInto 处理可能失败的转换。
  • AsRef/AsMut 提供廉价引用借用转换,让函数签名更灵活(接受多种引用类型)。Borrow/BorrowMut 附加强大的哈希一致性契约,是 HashMap::get 支持异构查找的关键。
  • ToOwned 泛化 Clone——从引用生成拥有值(如 str -> String)。Cow 利用它实现 Clone-on-Write 模式,在不需要修改时避免分配。