如何使用`borrow::Borrow<...>`接受任何“以trait方式借用”的内容?

5

Rust的core::convert::AsRef特质文档中表示:

Borrow针对于任意类型T都有一个默认实现,因此可以用来接受引用或值。

随后它继续介绍了core::borrow::Borrow特质

这确实可以用于编写通用代码,以便可以通过值或引用接受参数,它代表了可以作为&T借用的任何东西,由于T可以作为&T借用,因此这个简单示例运行得非常完美:

fn report_by_either<T: Borrow<i32>>(either: T) {
    let x: i32 = *either.borrow();
    println!("x = {}", x);
}
⋮

report_by_either(5); // x = 5
report_by_either(&6); // x = 6

如果有人希望在更复杂的情况下使用Borrow<…>,具体来说,在具有泛型约束的通用代码中。如何除了表示借用任何东西为&T的概念外,还能表达T实现了一个特质的约束?

最近我想到了一个非常简单的例子,当我试图解决Rust的范围不都实现了Copy的事实时。

考虑这个函数,它接受任何可以提供RangeBounds<i32>的东西:

fn report_by_value<R: RangeBounds<i32> + Debug>(value: R) {
    println!("range-bounds: `{:?}`", value);
}

这会导致呼叫者体验不一致的问题:
  1. If they pass some ranges, of the types that implement Copy (e.g. RangeTo, RangeToInclusive, …), they'll be just fine.

    let range = ..100;
    report_by_value(range);
    report_by_value(range);
    report_by_value(range);
    
  2. But, for other ranges (e.g. Range, RangeFrom, …) they'd better call clone() or ownership of the range gets stolen:

    let range_from = 1..;
    report_by_value(range_from.clone());
    report_by_value(range_from.clone());
    
    report_by_value(range_from);
    report_by_value(range_from); // use of moved value: `range_from`
    

避免这种不一致的一种方法是仅通过引用接受范围:

fn report_by_reference<R: RangeBounds<i32> + Debug>(reference: &R) {
    println!("range-bounds: `{:?}`", reference);
}

但这也导致调用代码笨拙不优雅。
report_by_reference(&(4..));
report_by_reference(&(..5));
report_by_reference(&(6..7));

看起来显而易见的解决方案是使用borrow::Borrow

fn report_by_either<R: RangeBounds<i32> + Debug, T: Borrow<R>>(either: T) {
    println!("range-bounds: `{:?}`", either.borrow());
}

不幸的是,泛型类型的类型推断对此无效。

  • Both of the following calls yield an error: "cannot infer type of the type parameter R declared on the function report_by_either":

    report_by_either(5..); // cannot infer type of the type parameter `R` declared on the function `report_by_either`
    report_by_either(&(6..)); // cannot infer type of the type parameter `R` declared on the function `report_by_either`
    
  • These turbo-fish-blighted lines do work:

    report_by_either::<RangeFrom<i32>, _>(7..);
    report_by_either::<RangeFrom<i32>, _>(&(8..));
    
  • playground

在这种情况下,我不希望我的用户了解或甚至知道为什么范围类型没有实现 Copy或者存在不一致性导致并非所有范围类型都没有实现它们的原因。我希望我的用户能够向我传递任何给出RangeBounds<...>的东西。话虽如此,在这种情况下,仅接受范围边界的引用并不是我的API中的灾难性妥协-report_by_reference(&(6..7))很笨拙但还可以容忍。
然而,更普遍地说,我认为“任何借来的东西作为特质”的概念肯定是常见的需求。
我该如何实现呢?

“Borrow” 有点棘手,但你可以尝试使用 “AsRef”。 - Aleksander Krauze
3
并非所有类型都有 AsRef 的通用实现。事实上,core::convert::AsRef 的文档特别指向了 Borrow,就是为了这个原因。 - Xharlie
如果您不想拥有这些范围的所有权,那么您就不应该拥有它们。通过引用接受它们是可以的。 - Sven Marnach
1个回答

2
很遗憾,在这里你几乎没有机会成功;在6..7&i32之间有两个间接特征,编译器无法推断出中间类型。例如,我可以创建一个满足约束条件的类型作为中间类型:
#[derive(Debug)]
struct Gobbledygook;

impl RangeBounds<i32> for Gobbledygook {
    fn start_bound(&self) -> Bound<&i32> { todo!() }
    fn end_bound(&self) -> Bound<&i32> { todo!() }
}

impl Borrow<Gobbledygook> for Range<i32> {
    fn borrow(&self) -> &Gobbledygook { todo!() }
}

fn report_by_either<R: RangeBounds<i32> + Debug, T: Borrow<R>>(either: T) {
    println!("range-bounds: `{:?}`", either.borrow());
}

fn main() {
    report_by_either::<Gobbledygook, _>(0..7);
}

如果允许省略显式类型参数,编译器应该使用哪种实现呢?Range<i32> 还是我的 Gobbledygook 类型?为什么?编译器无法假定两者之一。
如果您有 T: Trait<U>, U: Borrow<V>T: Borrow<U>, U: Trait<V>,则始终需要指定中间类型。但是,如果 Trait 使用关联类型而不是泛型类型参数,则在前一种情况下可以明确推断出中间类型。

我的示例使用 RangeBounds,只是为了说明有时需要接受“任何我可以借用作为实际需要的特质”,而不是具体要求。正如你所说的(并且正如我在问题的结论中指出的那样),在这种情况下解决限制的瑕疵是微不足道的。 - Xharlie
然而,针对这个具体的例子,我很想打开一个rust-lang/rust问题建议,在core中为所有有意决定不实现Copy的内置T类型的&T显式实现RangeBoundsRangeFrom等等。但是我不确定这个建议会得到多少好评。 - Xharlie
我已经删除了关于范围的评论部分,并用一个关于特征的评论部分进行了替换。老实说,对我来说,“impl RangeBounds for &T where T: RangeBounds”并不那么离奇。许多其他特征也遵循类似的模式。虽然我不确定这是否会是一个破坏性的变化。 - kmdreko
我对此的担忧在于它为后续的开发设置了先例——是否应该为 &T 实现 core 中的 每一个 特征呢?如果是 RangeBounds,那么这个规则的界限在哪里?当然,我会辩称 RangeBounds 是一个合适的候选项,因为在那些本来非常适合使用 Copy 的结构体中,有意地删除了 Copy —— 删除 Copy 会极其明显地违反最小惊讶原则。 - Xharlie
1
"Stack没有显示任何差异" - 嗯,可能是Stack出了问题,或者我根本没有保存我的编辑(很可能是后者)。我已经再次编辑了它,以表达我想要说的内容。 - kmdreko
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接