4.7.5 泛化借用

译文 · 基于 Learning Rust

泛化借用

原文链接: https://quinedot.github.io/rust-learning/dyn-trait-borrow.html

许多核心 trait 围绕某种字段投影构建:实现类型包含另一种类型 T,你可以将 &self 转换为 &T,或将 &mut self 转换为 &mut T。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
pub trait Deref {
    type Target: ?Sized;
    fn deref(&self) -> &Self::Target;
}

pub trait Index<Idx: ?Sized> {
    type Output: ?Sized;
    fn index(&self, index: Idx) -> &Self::Output;
}

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

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

// `DerefMut`、`IndexMut`、`AsMut`……

若你想返回的类型并非包含在 Self 内,通常无法实现这些 trait(除非返回对某个静态值等的引用,这很少是你想要的)。

然而,有时你拥有自定义借用类型,它实际上并不包含在拥有类型内:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// 我们希望能实现 `Borrow<DataRef<'?>>`,但做不到。
pub struct Data {
    first: usize,
    others: Vec<usize>,
}

pub struct DataRef<'a> {
    first: usize,
    others: &'a [usize],
}

pub struct DataMut<'a> {
    first: usize,
    others: &'a mut Vec<usize>,
}

在与库和数据结构(例如 std::collections::HashSet)交互时,这可能成问题。HashSet 依赖 Borrow trait 以便在不取得所有权的情况下查找条目。

解决此问题的一种方法是使用更灵活的其他库或类型。 然而,也可以通过一些间接和类型擦除来解决。

你的类型包含一个借用者

此处我们呈现 Eric Michael Sumner 对该问题的解决方案,他慷慨允许将其纳入本指南。我已为呈现目的重写了原文,任何错误由我承担。

该方法背后的主要思路是利用以下 trait,它封装了以自定义借用类型形式借用 self 的能力:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# pub struct Data { first: usize, others: Vec<usize> }
# pub struct DataRef<'a> { first: usize, others: &'a [usize] }
pub trait Lend {
    fn lend(&self) -> DataRef<'_>;
}

impl Lend for Data {
    fn lend(&self) -> DataRef<'_> {
        DataRef {
            first: self.first,
            others: &self.others,
        }
    }
}

impl Lend for DataRef<'_> {
    fn lend(&self) -> DataRef<'_> {
        DataRef {
            first: self.first,
            others: self.others,
        }
    }
}

// impl Lend for DataMut<'_> ...

关键洞察是:任何实现者都可以从 &self 强制转换为 &dyn Lend。因此我们可以实现 Borrow 等 trait,因为每个实现者都「包含」一个 dyn Lend——它自己!

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# pub struct Data { first: usize, others: Vec<usize> }
# pub struct DataRef<'a> { first: usize, others: &'a [usize] }
# pub trait Lend { fn lend(&self) -> DataRef<'_>; }
# impl Lend for Data {
#    fn lend(&self) -> DataRef<'_> {
#        DataRef {
#            first: self.first,
#            others: &self.others,
#        }
#    }
# }
# impl Lend for DataRef<'_> {
#    fn lend(&self) -> DataRef<'_> {
#        DataRef {
#            first: self.first,
#            others: self.others,
#        }
#    }
# }
use std::borrow::Borrow;

impl<'a> Borrow<dyn Lend + 'a> for Data {
    fn borrow(&self) -> &(dyn Lend + 'a) { self }
}

impl<'a, 'b: 'a> Borrow<dyn Lend + 'a> for DataRef<'b> {
    fn borrow(&self) -> &(dyn Lend + 'a) { self }
}

// impl<'a, 'b: 'a> Borrow<dyn Lend + 'a> for DataMut<'b> ...

这为我们的拥有类型和自定义借用数据结构提供了共同的 Borrow 类型。例如,要在 HashSet 中查找借用条目,我们可以将 &DataRef<'_> 转换为 &dyn Lend 并传给 set.contains;HashSet 可以对 dyn Lend 进行哈希,并将拥有的 Data 条目也借用为 dyn Lend,以进行必要的查找比较。

这意味着我们需要为 dyn Lend 实现 PartialEq 和 Hash 等必要功能。但这与上一节的一般解决方案是不同的用例。在那情况下,我们希望为已类型擦除的 dyn Trait 实现 PartialEq,以便比较任意实现类型的值。

这里我们不关心任意类型,而且我们有能力产生引用实际数据的具体类型。我们可以用它来实现功能;无需向下转型或类似手段来为 dyn Lend 实现必要的 trait。我们并不真正关心 dyn Lend 是否实现 PartialEq 和 Hash,那只是达到目的的手段:尽管有 Borrow trait 约束,仍让 HashSet 等能够比较我们的自定义具体借用类型。

首先,我们需要让具体类型自身实现必要的 trait。主要要注意的是维持 Borrow 所期望的不变性。 对本示例而言,我们足够幸运,借用类型可以直接 derive 所有必要功能:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
pub struct DataRef<'a> {
    first: usize,
    others: &'a [usize],
}

#[derive(Debug, Clone)]
pub struct Data {
    first: usize,
    others: Vec<usize>,
}

#[derive(Debug)]
pub struct DataMut<'a> {
    first: usize,
    others: &'a mut Vec<usize>,
}

然而,我们尚未为其他类型 derive 对 Borrow 语义重要的 trait。技术上本例可以 derive,因为:

  • 字段顺序与借用类型相同
  • 每个字段在两种类型中都存在
  • 与借用类型字段比较时,每个字段都有 Borrow 关系
  • 我们理解 derive 的工作原理

但这些条件对你的用例可能并不成立,即使成立,依赖它们也会形成非常脆弱的安排。仅添加字段甚至重新排列字段顺序都很容易意外破坏。

相反,我们通过委托给借用类型来直接实现这些 trait:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
# struct Data; impl Data { fn lend(&self) {} }
// 留给读者的练习:为我们的所有拥有与借用类型实现 `PartialEq` :-)
impl std::cmp::PartialEq for Data {
    fn eq(&self, other: &Self) -> bool {
        self.lend() == other.lend()
    }
}

impl std::cmp::Eq for Data {}

impl std::hash::Hash for Data {
    fn hash<H: std::hash::Hasher>(&self, hasher: &mut H) {
        self.lend().hash(hasher)
    }
}

// 对 `DataMut<'_>` 同理

事实上,这正是我们对 dyn Lend 也要采用的方法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
# pub struct DataRef<'a> { first: usize, others: &'a [usize] }
# pub trait Lend { fn lend(&self); }
// (注意我们不要求 `dyn` 生命周期相同。)
impl std::cmp::PartialEq<dyn Lend + '_> for dyn Lend + '_ {
    fn eq(&self, other: &(dyn Lend + '_)) -> bool {
        self.lend() == other.lend()
    }
}

impl std::cmp::Eq for dyn Lend + '_ {}

impl std::hash::Hash for dyn Lend + '_ {
    fn hash<H: std::hash::Hasher>(&self, hasher: &mut H) {
        self.lend().hash(hasher)
    }
}

呼,样板代码很多。但我们终于可以将在 HashSet 中存储 Data,并在只有 DataRef 时查找条目:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
use std::collections::HashSet;
let set = [
    Data { first: 3, others: vec![5,7]},
].into_iter().collect::<HashSet<_>>();

assert!(set.contains::<dyn Lend>(&DataRef { first: 3, others: &[5,7]}))

// turbofish 的替代写法
let data_ref = DataRef { first: 3, others: &[5,7]};
assert!(set.contains(&data_ref as &dyn Lend));

这是包含完整示例的 playground。

强制转换或 turbofish 的另一种选择是添加 as_lend(&self) -> &dyn Lend + '_ 方法,类似于许多先前手动超 trait 向上转型的示例。

最后修改 August 23, 2026: 更新 (499855b16)