为新类型实现 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】:

关于DerefDerefMut 的规则是专门为适应智能指针而设计的。因此,Deref 应仅针对智能指针实现以避免混淆

——std::ops::Deref


我认为这是一种不好的做法

因为在某些情况下我可以像使用基础类型一样使用我的新类型

这就是问题所在——它可以被隐式地用作底层类型只要引用。如果你实现了DerefMut,那么它也适用于需要可变引用的时候。

您无法控制底层类型中哪些是可用的,哪些是不可用的;一切都是。在您的示例中,您是否要允许人们致电as_ptrsort 呢?我当然希望你这样做,因为他们可以!

您所能做的就是尝试覆盖方法,但它们仍然必须存在:

impl MyArray 
    fn as_ptr(&self) -> *const i32 
        panic!("No, you don't!")
    

即便如此,它们仍然可以被显式调用 (<[i32]>::as_ptr(&*my_array);)。

我认为这是不好的做法,原因与我认为使用继承来重用代码是不好的做法相同。在您的示例中,您实际上是从数组继承的。我永远不会写像下面这样的 Ruby:

class MyArray < Array
  # ...
end

这又回到了面向对象建模中的 is-ahas-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&lt;T&gt;(T,) 和 implements Deref&lt;Target=T&gt; 的元组结构。

    bstr::BString 有一个字段类型为 Vec&lt;u8&gt; 和 implements Deref&lt;Target=Vec&lt;u8&gt;&gt;

所以,只要不被滥用就可以了,例如模拟多级继承层次结构。我还注意到上面的两个示例要么有零个公共方法,要么只有一个返回内部值的into_inner 方法。保持包装器类型的方法数量最少似乎是个好主意。

【讨论】:

虽然在流行的 crates 中使用不一定是“最佳实践”的好论据,但我同意 actix 的 Json 应该Deref,它只是作为框架其余部分的标记,它应该对用户的代码尽可能透明。

以上是关于为新类型实现 Deref 是不是被认为是一种不好的做法?的主要内容,如果未能解决你的问题,请参考以下文章

为啥单身人士被认为是一种不好的做法? [复制]

Java为参数分配新值,这被认为是一种不好的做法吗?

打开和关闭 PHP 是不是被认为是不好的做法? [关闭]

Rust Deref与自动解引用

ERD是不是被认为是一种UML图?

在 Window 对象上设置属性是不是被认为是不好的做法?