【问题标题】: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
}

这是一个好习惯还是坏习惯?为什么?有什么缺点?

【问题讨论】:

    标签: rust dereference


    【解决方案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; 
    }
    

    在等待一流的委托时,我们可以使用 delegateambassador 之类的 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

    1. actix_web::web::Json&lt;T&gt;(T,)implements Deref&lt;Target=T&gt; 的元组结构。

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

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

    【讨论】:

    • 虽然在流行的 crates 中使用不一定是“最佳实践”的好论据,但我同意 actix 的 Json 应该Deref,它只是作为框架其余部分的标记,它应该对用户的代码尽可能透明。
    猜你喜欢
    • 2015-10-29
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2022-12-22
    • 1970-01-01
    • 2011-06-11
    • 1970-01-01
    相关资源
    最近更新 更多