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



到目前为止,在我的项目中,我使用了许多特性来允许在单元测试中对注入的依赖项进行嘲讽/存根。然而,到目前为止,我所做的一个细节似乎非常可疑,以至于我很惊讶它竟然能编译出来。我担心发生了一些我看不到也不理解的危险事件。它基于这两种方法签名之间的差异:

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

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

这两者之间有什么区别?为什么他们都被允许?它们都代表合法的用例吗?还是我的引用语法(&MyTrait<T>(是一个更糟糕的想法?

两者不同,有不同的用途。两者都很有用,根据具体情况,其中一个可能是最佳选择。

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

第二种情况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

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

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

确实有所不同。impl版本等效于以下内容:

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

因此,与第一个版本不同,subject被移动(通过值传递(到confirm中,而不是通过引用传递。因此,在impl版本中,confirm拥有该值的所有权。

最新更新