为新类型实现 Deref 是不是被认为是一种不好的做法?
Posted
技术标签:
【中文标题】为新类型实现 Deref 是不是被认为是一种不好的做法?【英文标题】:Is it considered a bad practice to implement Deref for newtypes?为新类型实现 Deref 是否被认为是一种不好的做法? 【发布时间】:2017-12-18 14:07:22 【问题描述】:我经常使用newtype模式,但是写my_type.0.call_to_whatever(...)
很累。我很想实现Deref
trait,因为它允许编写更简单的代码,因为我可以在某些情况下使用我的 newtype,就好像它是底层类型一样,例如:
use std::ops::Deref;
type Underlying = [i32; 256];
struct MyArray(Underlying);
impl Deref for MyArray
type Target = Underlying;
fn deref(&self) -> &Self::Target
&self.0
fn main()
let my_array = MyArray([0; 256]);
println!("", my_array[0]); // I can use my_array just like a regular array
这是一个好习惯还是坏习惯?为什么?有什么缺点?
【问题讨论】:
【参考方案1】:关于
Deref
和DerefMut
的规则是专门为适应智能指针而设计的。因此,Deref
应仅针对智能指针实现以避免混淆。——
std::ops::Deref
我认为这是一种不好的做法。
因为在某些情况下我可以像使用基础类型一样使用我的新类型
这就是问题所在——它可以被隐式地用作底层类型只要引用。如果你实现了DerefMut
,那么它也适用于需要可变引用的时候。
您无法控制底层类型中哪些是可用的,哪些是不可用的;一切都是。在您的示例中,您是否要允许人们致电as_ptr
? sort
呢?我当然希望你这样做,因为他们可以!
您所能做的就是尝试覆盖方法,但它们仍然必须存在:
impl MyArray
fn as_ptr(&self) -> *const i32
panic!("No, you don't!")
即便如此,它们仍然可以被显式调用 (<[i32]>::as_ptr(&*my_array);
)。
我认为这是不好的做法,原因与我认为使用继承来重用代码是不好的做法相同。在您的示例中,您实际上是从数组继承的。我永远不会写像下面这样的 Ruby:
class MyArray < Array
# ...
end
这又回到了面向对象建模中的 is-a 和 has-a 概念。 MyArray
是一个数组吗?它应该能够在数组可以使用的任何地方使用吗?它是否具有对象应该支持消费者不能破坏的先决条件?
但我已经厌倦了写
my_type.0.call_to_whatever(...)
与其他语言一样,我相信正确的解决方案是组合而非继承。如果您需要转接呼叫,请在 newtype 上创建一个方法:
impl MyArray
fn call_to_whatever(&self) self.0.call_to_whatever()
在 Rust 中造成这种痛苦的主要原因是缺少委托。 假设的委托语法可能类似于
impl MyArray
delegate call_to_whatever -> self.0;
在等待一流的委托时,我们可以使用 delegate 或 ambassador 之类的 crate 来帮助填补一些空白。
那么什么时候应该使用Deref
/ DerefMut
?我主张唯一有意义的时候是当你实现一个智能指针。
实际上,我确实将Deref
/ DerefMut
用于在我是唯一或主要贡献者的项目中不公开暴露的新类型。这是因为我相信自己并且非常了解我的意思。如果存在委托语法,我不会。
【讨论】:
我不得不不同意,至少在Deref
方面——我的大多数新类型仅作为花哨的构造函数存在,因此我可以通过静态保证来传递数据,它满足某些不变量。即,一旦构建了对象,我就不再真正关心新类型,仅底层数据;必须在任何地方都使用 match/.0
模式只是噪音,并且委托我可能关心的每种方法也是如此。我想有一个类型实现 Deref
而不是 DerefMut
可能会令人惊讶,但毕竟它们是独立的特征是有原因的......
@ildjarn 具有满足某些不变量的静态保证 — 如果您实施 DerefMut
,您将无法再静态保证这些不变量,因为任何人都可以轻松更改它们,无论新类型字段的可见性。如果你只实现Deref
,你仍然允许人们戳你的数据。这不会造成任何实质性损害,但通常会提供比您需要公开的更广泛的 API。
"这不会造成任何实质性损害,但通常会提供比您需要公开的更广泛的 API。" 不比 std::str
IMO 更是如此;例如,在协议工作中,您经常处理原始类型的序列,而掩盖(/尝试抽象掉)这一事实是毫无意义的,但是要维护严格的不变量(cf UTF -8)。我对此感觉不强烈;我只是觉得“不好的做法”说得相当强烈。 :-](编辑:如果有人可以让deref_mut
不安全,那么我可能会感到强烈,因为没有Deref
sans DerefMut
难题。)
我认为这个链接非常适合您的答案:rust-lang-nursery.github.io/api-guidelines/…
This comes back to the is-a and has-a concepts from object-oriented modeling. Is MyArray an array? Should it be able to be used anywhere an array can? Does it have preconditions that the object should uphold that a consumer shouldn't be able to break?
可能有点晚了,但是对于is-a
的情况来说,新类型确实是字面意思......只有当你确实想要一个充当旧类型的新类型时才使用它。如果暴露包装类型的所有功能是不安全的(不是生锈的不安全),则应使用通用组合,而不是新类型模式。您有正确的担忧,但出于错误的原因。【参考方案2】:
与接受的答案相反,我发现一些流行的 crates 为新类型而不是智能指针的类型实现了 Deref
:
actix_web::web::Json<T>
是 (T,)
和 implements Deref<Target=T>
的元组结构。
bstr::BString
有一个字段类型为 Vec<u8>
和 implements Deref<Target=Vec<u8>>
。
所以,只要不被滥用就可以了,例如模拟多级继承层次结构。我还注意到上面的两个示例要么有零个公共方法,要么只有一个返回内部值的into_inner
方法。保持包装器类型的方法数量最少似乎是个好主意。
【讨论】:
虽然在流行的 crates 中使用不一定是“最佳实践”的好论据,但我同意 actix 的Json
应该是 Deref
,它只是作为框架其余部分的标记,它应该对用户的代码尽可能透明。以上是关于为新类型实现 Deref 是不是被认为是一种不好的做法?的主要内容,如果未能解决你的问题,请参考以下文章