【问题标题】:When to use value and reference types for immutable types? (.NET)何时对不可变类型使用值和引用类型? (。网)
【发布时间】:2011-08-23 17:42:12
【问题描述】:

对于可变类型,值类型和引用类型之间的行为差​​异很明显:

// Mutable value type
PointMutStruct pms1 = new PointMutStruct(1, 2);
PointMutStruct pms2 = pms1;
// pms1 == (1, 2); pms2 == (1, 2);
pms2.X = 3;
MutateState(pms1); // Changes the X property to 4.
// pms1 == (1, 2); pms2 == (3, 2);

// Mutable reference type
PointMutClass pmc1 = new PointMutClass(1, 2);
PointMutClass pmc2 = pmc1;
// pmc1 == (1, 2); pmc2 == (1, 2);
pmc2.X = 3;
MutateState(pmc1); // Changes the X property to 4.
// pmc1 == (4, 2); pmc2 == (4, 2);

但是,对于不可变类型,这种区别不太明确:

// Immutable value type
PointImmStruct pis1 = new PointImmStruct(1, 2);
PointImmStruct pis2 = pis1;
// pis1 == (1, 2); pis2 == (1, 2);
pis2 = new PointImmStruct(3, pis2.Y);
// Can't mutate pis1
// pis1 == (1, 2); pis2 == (3, 2);

// Immutable reference type
PointImmClass pic1 = new PointImmClass(1, 2);
PointImmClass pic2 = pic1;
// pic1 == (1, 2); pic2 == (1, 2);
pic2 = new PointImmClass(3, pic2.Y);
// Can't mutate pic1 either
// pic1 == (1, 2); pic2 == (3, 2);

不可变引用类型也经常使用值语义(例如规范示例System.String):

string s1 = GenerateTestString(); // Generate identical non-interned strings
string s2 = GenerateTestString(); // by dynamically creating them
// object.ReferenceEquals(strA, strB)) == false;
// strA.Equals(strB) == true
// strA == strB

Eric Lippert 之前在他的博客(例如here)上讨论过,值类型通常(什么时候对这个讨论并不重要)分配在堆栈上的事实是一个实现细节,它不应该通常决定您是使对象成为值类型还是引用类型。

考虑到不可变类型在行为上的这种模糊区别,这留给我们什么标准来决定是将不可变类型设为引用类型还是值类型?

此外,由于不可变强调值与变量,不可变类型是否应该始终实现值语义?

【问题讨论】:

    标签: c# .net immutability


    【解决方案1】:

    有一类重要的不可变类型(Eric Lippert 也写过一些篇幅)必须实现为崇敬类型:递归类型,如列表节点、树节点等。值类型不能有循环定义,例如,链表节点有:

    class IntNode
    {
        private readonly int value;
        private readonly IntNode next;
    }
    

    【讨论】:

      【解决方案2】:

      我会说你链接的 Eric 的博客文章给了你确切的答案:

      我很遗憾文档没有 不关注最相关的内容;经过 专注于一个基本上不相关的 实现细节,我们放大 该实施的重要性 细节和模糊的重要性 是什么使值类型在语义上 有用。我非常希望所有这些 解释什么是“堆栈”的文章 而是会花时间解释 “按值复制”究竟是什么意思 以及如何误解或误用 “按值复制”可能会导致错误。

      如果您的对象应该具有“按值复制”语义,则将它们设为值类型。如果它们应该具有“按引用复制”语义,请将它们设为引用类型。

      他也这么说,我同意:

      我总是会选择价值 类型与引用类型基于 类型是否在语义上 表示一个值或语义上 引用某事。

      【讨论】:

      • 值类型始终存储在代表它们的变量或字段中,无论该变量或字段恰好存储在何处。引用类型的对象永远不会实际存储在代表它们的变量或字段中;它们总是存储在堆的其他地方。如果我有某个类的对象 Foo1 包含某个引用类型的非空字段 Bar1,那么 Foo1 和 Bar1 是单独的堆对象。如果我有某个类的对象 Foo2,其中包含值类型的 Bar2,则 Bar2 将存储在堆对象 Foo2 中。
      • @supercat 这个问题围绕着一篇博文展开,该博文认为使用运行时的实现细节不应该是是否使用值类型的决定因素,而是值类型的“按值复制”语义优于引用类型的“按引用复制”语义。我不太确定您的评论添加了什么,除了对帖子建议程序员避免的实现细节的强化。
      • 如果一个类型是不可变的,复制一个引用和复制一个值在语义上是相同的。除非不可变类型需要与某个其他类具有继承关系(可以肯定,这是一种有用的模式),否则不可变值类型和不可变类类型之间的主要区别将与性能有关。主要问题应该是为该类型的每个不同实例创建一个单独的堆对象是否更好,但可能共享已知相同的实例,或者避免创建堆对象来保存实例。
      • 我个人碰巧认为,对可变值类型的蔑视很大程度上是 .net 处理它们的一些限制和怪癖的不幸结果,并且在许多情况下实现最佳语义和性能是通过引用传递可变值类型。
      • @supercat 我不认为我真的理解您的观点之间的关系。值类型甚至可以在堆栈上创建的全部原因是因为它们的按值复制语义。 (保证值对象永远不会泄漏到堆栈中。)也就是说,我认为这篇文章已经涵盖了这一点,甚至不能保证在堆栈上分配了值对象。唯一应该向程序员公开的是创建的类型是按值复制还是按引用复制。其他一切都应该被虚拟机视为程序员不透明的运行时优化。
      【解决方案3】:

      .NET 用String 类暗示了这个问题的答案。它是不可变的,但是是一个引用类型。尽可能让你的不可变类型act 像值类型。它是否真的是一个值类型并不重要。

      所以我能想到的唯一标准是:如果复制它会很昂贵(String 可能涉及大量复制!),请将其设为引用类型。如果可以快速复制,请选择值类型。还要考虑是否需要比较引用——这可能是不可变引用类型唯一棘手的部分。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2012-07-20
        • 2017-06-09
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多