&Trait 和 impl Trait 用作方法参数时有什么区别?

What is the difference between &Trait and impl Trait when used as method arguments?

到目前为止,在我的项目中,我使用了许多特征来允许 mocking/stubbing 在注入依赖项的单元测试中。然而,到目前为止我正在做的事情的一个细节似乎非常可疑,以至于我很惊讶它甚至可以编译。我担心正在发生一些我看不到或不理解的危险事情。它基于这两个方法签名之间的差异:

fn confirm<T>(subject: &MyTrait<T>) ...
fn confirm<T>(subject: impl MyTrait<T>) ...

我只是在方法参数中发现了 impl ... 语法,这似乎是唯一记录在案的方法,但我的测试已经使用另一种方式通过了,这是我凭直觉得出的基于Go 如何解决同样的问题(编译时方法参数的大小,当参数可以是接口的任何实现者时,引用可以来拯救)。

这两者有什么区别?为什么他们都被允许?它们是否都代表合法的用例,或者我的参考语法 (&MyTrait<T>) 严格来说是一个更糟糕的主意?

确实不一样。 impl 版本等同于以下内容:

fn confirm<T, M: MyTrait<T>>(subject: M) ...

因此与第一个版本不同,subject 被移动(按值传递)到 confirm,而不是按引用传递。所以在 impl 版本中,confirm 取得了这个值的所有权。

两者不同,用途不同。两者都有用,视情况而定,一个或另一个可能是最佳选择。

第一种情况,&MyTrait<T>,现代Rust最好写成&dyn MyTrait<T>。它是所谓的特征对象。引用指向任何实现 MyTrait<T> 的类型,方法调用在运行时动态调度。为了使这成为可能,引用实际上是一个胖指针;除了指向对象的指针外,它还存储指向对象类型的虚方法 table 的指针,以允许动态调度。如果你的对象的实际类型只在运行时才知道,这是你唯一可以使用的版本,因为在这种情况下你需要使用动态调度。该方法的缺点是存在运行时成本,并且它仅适用于 object-safe.

的特征

第二种情况,impl MyTrait<T>,表示再次实现MyTrait<T>的任何类型,但在这种情况下,需要在编译时知道确切的类型。原型

fn confirm<T>(subject: impl MyTrait<T>);

相当于

fn confirm<M, T>(subject: M)
where
    M: MyTrait<T>;

对于代码中使用的每个类型 M,编译器都会在二进制文件中创建一个单独的 confim 版本,并且方法调用在编译时静态分派。如果在编译时知道所有类型,则此版本更可取,因为您无需支付动态分派到具体类型的运行时成本。

两个原型之间的另一个区别是第一个版本通过引用接受 subject,而第二个版本使用传入的参数。但这不是概念上的区别,而第一个版本不能写成消费对象,第二个版本可以很容易地写成接受 subject by reference:

fn confirm<T>(subject: &impl MyTrait<T>);

考虑到你引入的特征是为了方便测试,你可能更喜欢 &impl MyTrait<T>.