&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>
.
到目前为止,在我的项目中,我使用了许多特征来允许 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>
.