到目前为止,在我的项目中,我使用许多特性来允许在单元测试中进行模拟/存根以注入注入的依赖项。但是,到目前为止,我正在做的一个细节似乎太可疑了,甚至令我惊讶。我担心发生了一些看不见或不了解的危险。它基于这两个方法签名之间的区别:
fn confirm<T>(subject: &MyTrait<T>) ...
fn confirm<T>(subject: impl MyTrait<T>) ...
我只是在方法参数中发现了impl ...
语法,这似乎是唯一记录在案的方法,但是我的测试已经通过另一种方式通过了,根据Go的方式,我凭直觉得出解决了相同的问题(方法参数的大小在编译时,此时参数可以是接口的任何实现者,而引用可以解救)。
两者之间有什么区别?为什么都允许他们呢?它们都代表合法的用例,还是我的引用语法(&MyTrait<T>
)严格来说是个更差的主意?
答案 0 :(得分:6)
两者是不同的,并且用途不同。两者都是有用的,并且视情况而定,一个或另一个可能是最佳选择。
第一种情况&MyTrait<T>
,最好用现代Rust语言写成&dyn MyTrait<T>
。这是所谓的特征对象。该引用指向实现MyTrait<T>
的任何类型,并且在运行时动态调度方法调用。为了使之成为可能,引用实际上是一个胖指针;除了指向对象的指针之外,它还存储指向对象类型的虚拟方法表的指针,以允许动态分配。如果仅在运行时才知道对象的实际类型,则这是可以使用的唯一版本,因为在这种情况下需要使用动态分配。该方法的缺点是存在运行时成本,并且仅适用于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
:
fn confirm<T>(subject: &impl MyTrait<T>);
鉴于您引入了这些特性以方便测试,因此您可能更希望使用&impl MyTrait<T>
。
答案 1 :(得分:1)
确实不一样。 revision, err := srv.Revisions.Get(fileId, revisionId).Do() //fieldId and revisionId are fatched using proper calls
if err != nil {
log.Fatalf("Unable to retrieve revision: %v", err)
}
fmt.Println("Revision:")
fmt.Printf("%+v\n", revision.LastModifyingUser)
版本等效于以下内容:
impl
因此,与第一个版本不同,fn confirm<T, M: MyTrait<T>>(subject: M) ...
被移动(通过值传递)到subject
中,而不是通过引用传递。因此,在confirm
版本中,impl
拥有此值的所有权。