接受"impl Borrow<T>"来抽象 T 的引用和值的论点是惯用的锈病吗?

  • 本文关键字:引用 Borrow impl 接受 抽象 rust
  • 更新时间 :
  • 英文 :


我发现自己编写的函数接受参数作为Borrow<T>,以便它透明地接受值和引用。

例:

use std::borrow::Borrow;
#[derive(Debug, Copy)]
struct Point {
pub x: i32,
pub y: i32,
}
pub fn manhattan<T, U>(p1: T, p2: U) -> i32
where
T: Borrow<Point>,
U: Borrow<Point>,
{
let p1 = p1.borrow();
let p2 = p2.borrow();
(p1.x - p2.x + p1.y - p2.y).abs()
}

这对于实现像Add这样的std:ops很有用,否则需要大量的重复才能透明地支持引用。

这是惯用语吗?有缺点吗?

我认为这个问题有两个部分。

1.Borrow特征是 Rust 中抽象所有权的惯用方式吗?

是的。如果你打算编写一个采用Foo&Foo的函数,F: Borrow<Foo>是正确的使用。 另一方面,AsRef通常只为类似引用的东西实现,而不是为拥有的值实现。

2. 在 Rust 中抽象所有权是习惯用语吗?

有时。这是一个有趣的问题,因为像manhattan这样的函数和习惯性地使用Borrow之间有一个微妙但重要的区别。

在 Rust 中,函数是需要拥有它的参数还是仅仅借用它们是函数接口的重要组成部分。通常,Rustaceans不介意在函数调用中编写&,因为它是有关被调用函数的相关语义事实的语法标记。一个可以接受Point&Point的函数并不比只能接受&Point的函数更普遍有用:如果你有一个Point,你所要做的就是借用它。因此,使用更简单的签名是最准确地记录函数真正需要的类型是惯用的:&Point

但是等等!这些接受论点的方式之间还有其他区别。一个区别是调用开销:&Point通常在单个指针大小的寄存器中传递,而Point可以在多个寄存器或堆栈中传递,具体取决于 ABI。另一个区别是代码大小:<T: Borrow<Point>>的每个唯一实例化都表示函数的单态化,这会使二进制文件膨胀。第三个区别是删除顺序:如果Point具有析构函数,则接受T: Borrow<Point>的函数将在内部调用Point::drop,而接受&Point的函数会将对象保留在原位供调用方处理。这是好是坏取决于上下文;但是,对于性能而言,它通常无关紧要(如果您假设Point最终都会被删除)。

接受T: Borrow<Point>的函数表明它正在内部对T做一些事情,而对于这些事情,仅仅&Point可能是次优的。下降顺序可能是这样做的最佳理由(我在这个答案中写了更多关于这个的内容,尽管我用作示例的puts函数并不是一个特别强大的函数)。

manhattan的情况下,滴序是无关紧要的,因为PointCopy的(简单复制的类型可能没有滴胶)。因此,接受Point&Point都没有性能优势(尽管单个函数不太可能以一种或另一种方式产生太大差异,但如果泛型被广泛使用,代码大小的成本很可能是一个缺点)。

避免不必要地使用泛型还有一个原因:它们会干扰类型推断,并可能降低来自编译器的错误消息和建议的质量。例如,想象一下,如果Point只实现了Clone(而不是Copy),并且您编写了manhattan(p, q),然后稍后在同一函数中再次使用p。编译器会警告您p在移动到函数后被使用,并建议添加一个.clone()。事实上,更好的解决方案是借用p,如果manhattan引用,编译器将强制你这样做。

Point很小(因此将其用作函数参数的开销可能很小)和Copy(因此无需担心滴胶)这一事实提出了另一个问题:manhattan应该简单地接受Point而根本不使用引用吗?这是一个基于意见的问题,实际上归结为哪个更适合您的心智模型。要么接受&Point,当调用方具有拥有的值时使用&,要么接受Point,并在调用方有引用时使用*- 没有硬性规定。

那么,什么是Borrow的适当用途呢?

上面的论点在很大程度上取决于这样一个事实,即引用很容易在任何地方使用,因此您不妨在调用者中具体地获取它们,就像在泛型函数中抽象地获取它们一样。有一次不是这种情况,即借用或拥有的类型没有直接传递给函数,而是包装在另一个泛型数据结构中。考虑按与 (0, 0) 的距离对类似Point事物的切片进行排序:

fn sort_by_radius<T: Borrow<Point>>(points: &mut [T]) {
points.sort_by_key(|p| {
let Point { x, y } = p.borrow();
x * x + y * y
});
}

在这种情况下,绝对不是有&mut [Point]的呼叫者可以简单地借用它来获得&mut [&Point]。然而,我们希望sort_by_radius能够接受这两种切片(无需编写两个函数),以便Borrow<Point>来拯救。sort_by_radius和您的manhattan版本之间的区别在于,T不会直接传递给要立即借用的函数,而是sort_by_radius需要像Point一样对待的类型的一部分,以便执行最终与借用无关的任务(对切片进行排序)。

最新更新