【问题标题】:Choosing between immutable objects and structs for value objects在值对象的不可变对象和结构之间进行选择
【发布时间】:2010-10-09 05:24:51
【问题描述】:

在将值对象(规范示例是地址)实现为不可变对象或结构之间如何选择?

选择其中一个是否有性能、语义或任何其他好处?

【问题讨论】:

  • 您的意思是不可变对象或“不可变”结构?结构也可以是可变的。..
  • 我相当肯定它们不能在 C# 中。随意证明我错了:)
  • 结构可以是可变的。但是它们是按值传递的,因此更改不会像类一样传播到它们被引用的任何地方(通过引用传递)。
  • +1 on configurator - c# 结构是可变的,除非你使用只有 get 而没有 set 的属性,或者将字段标记为“只读”

标签: c# oop domain-driven-design


【解决方案1】:

我实际上不建议使用 .NET 结构来实现值对象。有两个原因:

  • 结构不支持继承
  • ORM 不能很好地处理到结构的映射

这里我详细描述一下这个话题:Value Objects explained

【讨论】:

    【解决方案2】:

    结构严格用于高级用户(以及 out 和 ref)。

    是的,结构在使用 ref 时可以提供出色的性能,但您必须查看它们使用的内存。谁控制内存等。

    如果你不使用带有结构的 ref 和 outs,那么它们是不值得的,如果你预计会出现一些讨厌的错误 :-)

    【讨论】:

      【解决方案3】:

      在将值对象(规范示例是地址)实现为不可变对象或结构之间如何选择?

      我认为你的选择是错误的。不可变对象和结构不是对立的,也不是唯一的选择。相反,您有四个选择:

        • 可变
        • 不可变
      • 结构
        • 可变
        • 不可变

      我认为在 .NET 中,默认选择应该是 mutable class 来表示 logicimmutable class 来表示 实体。如果可行的话,我实际上倾向于选择不可变类,即使是逻辑实现也是如此。结构应该保留给模拟值语义的小类型,例如一个自定义的Date 类型,一个Complex 数字类型的类似实体。这里的重点是small,因为您不想复制大块数据,而且通过引用进行间接引用实际上很便宜(所以我们不会通过使用结构获得太多收益)。我倾向于使结构 always 不可变(目前我想不出一个例外)。由于这最符合内在值类型的语义,我发现这是一个很好的规则。

      【讨论】:

      • “表示逻辑”是什么意思?你能举个例子吗?许多实体(例如人)不是由那里的属性定义的,那么您为什么认为它们应该是不可变的?
      • @Avid: int (System.Int32), bool (System.Boolean), double (System.Double)等都是不可变的。与旧版本的 FORTRAN 不同,您不能在 C# 中更改 3 的值。如果您有readonly int x = 3;x 将始终具有值 3,并且您无法更改该值。但是,如果您有struct Point { int x; int y; }readonly Point a = new Point();,您可以更改a 中包含的值:a.x = 42;
      • @Avid:为什么 int 的情况不同?您可以看到int i = 1; i = 3; 将 1 抛出并用数字 3 的实例替换它。想想int i = 1; i = new int();。它是更改现有值,还是用 System.Int32 的新实例替换它?您可以为变量赋值这一事实并不会使类型不可变或可变。这是可变字段和/或突变方法的(不)存在,它们既不是 string 也不是 int 。此外,MSDN 明确指出 Int32 是不可变类型。
      • @AviD:这个讨论没有实际意义。您将价值与符号混淆了。这些在计算机科学中具有非常明确的含义,微软因为对术语的草率而感到羞耻。但事实是:变量是一个符号,除非你在.NET 中声明constreadonly,否则变量总是可变的。另一方面,价值是完全不同的东西。 .NET 中的值对于所有基本类型都是不可变的。你不能改变一个值,故事的结尾。您所能做的就是为现有符号分配一个新值。 (续……)
      • @Konrad,可变与否的不是值或符号,而是对象s += "foo"i+= 1 非常 不同,当您意识到要更改的不是 s 或 i 而是它所指的 object 时。对于 s,你不能,所以你必须分配变量来引用一个新对象。对于 i,您为同一个对象分配一个新值,因此您不需要更改 i 的引用。 (续...)
      【解决方案4】:

      我喜欢用思想实验:

      当只调用一个空的构造函数时,这个对象是否有意义?

      Richard E 的要求编辑

      struct 的一个很好的用途是包装原语并将它们限定在有效范围内。

      例如,概率的有效范围为 0-1。在任何地方使用小数来表示这一点很容易出错,并且需要在每个使用点进行验证。

      相反,您可以使用验证和其他有用的操作来包装原语。这通过了思想实验,因为大多数基元具有自然的 0 状态。

      这是一个使用struct 表示概率的示例:

      public struct Probability : IEquatable<Probability>, IComparable<Probability>
      {
          public static bool operator ==(Probability x, Probability y)
          {
              return x.Equals(y);
          }
      
          public static bool operator !=(Probability x, Probability y)
          {
              return !(x == y);
          }
      
          public static bool operator >(Probability x, Probability y)
          {
              return x.CompareTo(y) > 0;
          }
      
          public static bool operator <(Probability x, Probability y)
          {
              return x.CompareTo(y) < 0;
          }
      
          public static Probability operator +(Probability x, Probability y)
          {
              return new Probability(x._value + y._value);
          }
      
          public static Probability operator -(Probability x, Probability y)
          {
              return new Probability(x._value - y._value);
          }
      
          private decimal _value;
      
          public Probability(decimal value) : this()
          {
              if(value < 0 || value > 1)
              {
                  throw new ArgumentOutOfRangeException("value");
              }
      
              _value = value;
          }
      
          public override bool Equals(object obj)
          {
              return obj is Probability && Equals((Probability) obj);
          }
      
          public override int GetHashCode()
          {
              return _value.GetHashCode();
          }
      
          public override string ToString()
          {
              return (_value * 100).ToString() + "%";
          }
      
          public bool Equals(Probability other)
          {
              return other._value.Equals(_value);
          }
      
          public int CompareTo(Probability other)
          {
              return _value.CompareTo(other._value);
          }
      
          public decimal ToDouble()
          {
              return _value;
          }
      
          public decimal WeightOutcome(double outcome)
          {
              return _value * outcome;
          }
      }
      

      【讨论】:

      • 我不明白。您可以创建没有默认构造函数的对象。
      • 结构总是有一个默认的构造函数,即使你没有定义一个。因此,结构总是可以实例化为“空”实例(例如 new Int32())。如果一个对象没有特定的构造函数就没有意义,它可能应该是一个不可变的类。
      • 在 C# 中,类也有一个默认构造函数,即使没有声明。
      • 但是如果你在一个类中声明了一个非默认的构造函数,默认的就会消失,你可以only使用非默认的构造函数。对于结构,默认值始终存在且无法删除。
      • @Garry Shutler:您不能在结构中声明默认构造函数(因此无法设置其可见性)。 没有办法阻止某人使用带有结构的默认构造函数。
      【解决方案5】:

      从对象建模的角度来看,我很欣赏结构,因为它们让我可以使用编译器将某些参数和字段声明为不可为空。当然,如果没有特殊的构造函数语义(如Spec#),这仅适用于具有自然“零”值的类型。 (因此 Bryan Watt 给出了“虽然实验”的答案。)

      【讨论】:

        【解决方案6】:

        有几点需要考虑:

        一个结构被分配在堆栈上(通常)。它是一种值类型,因此如果数据太大,跨方法传递数据的成本可能会很高。

        在堆上分配了一个类。它是一种引用类型,因此通过方法传递对象的成本并不高。

        通常,我将结构用于不是很大的不可变对象。我只在其中包含有限数量的数据或者我想要不变性时使用它们。一个例子是DateTime 结构。我喜欢认为,如果我的对象不像DateTime 那样轻量级,那么它可能不值得用作结构。此外,如果我的对象作为值类型传递没有意义(也像DateTime),那么用作结构可能没有用。不变性是这里的关键。另外,我想强调一下,默认情况下结构不是不可变的。您必须通过设计使它们不可变。

        在我遇到的 99% 的情况下,使用类是正确的。我发现自己并不经常需要不可变的类。在大多数情况下,我认为类是可变的更自然。

        【讨论】:

        • 您错过的其中一个好处是结构体享有按值传递的语义。 (对象是按引用传递的,而对象引用是按值传递的,对于所有挑剔的人来说。)
        • 这是我暗示的第一点,当我说“[a struct] 是一个值类型”和“[a class] 是一个引用类型”时。
        • @DanHerbert :回复:“在堆栈上分配了一个结构(通常)。”。这个“通常”令人困惑! Struct 总是分配在堆栈上。
        • @Amby 阅读了这篇文章。它应该可以帮助您理解。 blogs.msdn.com/b/ericlippert/archive/2010/09/30/… 结构不需要装箱即可分配到堆中。
        • @BornToCode - 它不需要分配来创建或取消引用即可使用。它具有缓存局部性。可以使用 SSE 优化副本(memcpy 可以优化为自动使用 SSE)。
        【解决方案7】:

        因素:结构、内存要求、装箱。

        通常,结构的构造函数限制——没有显式的无参数构造函数,没有base 构造——决定了是否应该使用结构。例如。如果无参数构造函数应该将成员初始化为默认值,请使用不可变对象。

        如果您仍然可以在两者之间进行选择,请确定内存要求。小项目应该存储在结构中,特别是如果您期望有很多实例。

        当实例被装箱(例如,为匿名函数捕获或存储在非通用容器中)时,这种好处就消失了 - 你甚至开始为装箱支付额外费用。


        什么是“小”,什么是“多”?

        在 32 位系统上,对象的开销是 (IIRC) 8 个字节。请注意,对于几百个实例,这可能已经决定了内部循环是否完全在缓存中运行,或者调用 GC。如果您期望有数万个实例,这可能是运行与爬行之间的区别。

        从那个 POV 来看,使用结构并不是过早的优化。


        所以,根据经验

        如果大多数实例会被装箱,请使用不可变对象。
        否则,对于小对象,仅当结构构造会导致尴尬的接口并且您预计不会超过数千个实例时,才使用不可变对象。

        【讨论】:

          【解决方案8】:

          一般来说,我不建议将结构用于业务对象。虽然朝着这个方向前进可能会获得少量性能,但当您在堆栈上运行时,您最终会在某些方面限制自己,并且默认构造函数在某些情况下可能会成为问题。

          我会说,当您拥有向公众发布的软件时,这一点就更加重要了。

          结构体适用于简单类型,这就是为什么你看到微软对大多数数据类型使用结构体的原因。同样,结构体也适用于在堆栈上有意义的对象。其中一个答案中提到的 Point 结构就是一个很好的例子。

          我该如何决定?我通常默认为 object,如果它似乎是一个结构体,它通常是一个相当简单的对象,只包含实现为结构体的简单类型,那么我会仔细考虑并确定它是否使感觉。

          你提到一个地址作为你的例子。让我们作为一个班级来研究一下。

          public class Address
          {
              public string AddressLine1 { get; set; }
              public string AddressLine2 { get; set; }
              public string City { get; set; }
              public string State { get; set; }
              public string PostalCode { get; set; }
          }
          

          将此对象视为结构。在考虑中,如果您以这种方式编码,请考虑此地址“结构”中包含的类型。您是否看到任何可能无法按照您想要的方式进行的事情?考虑潜在的性能优势(即,有没有)?

          【讨论】:

          • 将我的回答中的思想实验应用于您的示例:如果地址的所有字段都为空,地址是否有意义?在这种情况下我会说不。
          • 实际上,对于数据对象,在至少有一项不为空之前,您不会创建对象,但我同意 100% 为空的对象是无效的。我的主要观点是结构不是编码地址的最佳方法。
          【解决方案9】:

          在当今世界(我在想 C# 3.5)我认为不需要结构(编辑:除了在某些特定场景中)。

          支持结构的论点似乎主要基于感知的性能优势。我希望看到一些基准(复制真实场景)来说明这一点。

          将结构用于“轻量级”数据结构的概念对我来说似乎过于主观。数据何时不再是轻量级的?另外,在向使用结构的代码添加功能时,您什么时候决定将该类型更改为类?

          就我个人而言,我不记得上次在 C# 中使用结构体是什么时候了。

          编辑

          出于性能原因,我建议在 C# 中使用结构是过早优化的明显案例*

          * 除非已对应用程序进行性能分析并且已将类的使用确定为性能瓶颈

          编辑 2

          MSDN 州:

          struct 类型适用于 表示轻量级对象,例如 如点、矩形和颜色。 虽然可以代表一个 点作为一个类,结构更 在某些情况下有效。为了 例如,如果您声明一个数组 1000 个点对象,您将分配 用于引用每个的额外内存 目的。在这种情况下,结构是 更便宜。

          除非你需要引用类型 语义,一个更小的类 超过 16 个字节可能更有效 由系统作为结构处理。

          【讨论】:

          • 您最近使用过 Int32 或 DateTime 吗?这些是拥有结构的很好的理由:-)“类与结构”与“实体与价值”的概念相同,用语言术语表示。区别在于身份,而不是感知的性能优势。
          • 永远不要定义你自己的结构体,你会活得更长更快乐。始终使用类(互操作字段布局除外)。
          • Bryan - 你能澄清一下你在评论中指的是哪一点吗?
          • Bryan - 我的观点是双重的:1)一些受访者将性能作为使用结构的原因 2)其他人建议它应该用于轻量级结构。我觉得这两种观点都非常主观,需要澄清一下。
          • 过早的优化只有在你妥协其他任何东西的情况下才是邪恶的——例如。可读性、界面优雅、开发时间。
          【解决方案10】:

          根据经验,结构大小不应超过 16 个字节,否则在方法之间传递它可能比传递对象引用更昂贵,对象引用只有 4 个字节(在 32 位机器上)长。

          另一个问题是默认构造函数。一个结构 always 有一个默认的(无参数和公共的)构造函数,否则像

          这样的语句
          T[] array = new T[10]; // array with 10 values
          

          不会工作。

          此外,结构体可以覆盖==!= 运算符并实现IEquatable&lt;T&gt; 接口。

          【讨论】:

            【解决方案11】:

            如果按值传递,复制实例的成本是多少。

            如果高,则为不可变引用(类)类型,否则为值(结构)类型。

            【讨论】:

            • 我不确定类与结构的感知性能优势是否真的相关。
            • 考虑 System.String。几个字节是对的,但是当它开始跨越许多缓存行时,成本开始增加。
            • 所以一个结构不应该包含字符串类型的属性?
            • 不如考虑 /implementing/ 一个你想要不可变但直接包含重要数据的字符串类。
            • 附言。 MS 的建议是值类型不应超过 16 个字节。
            猜你喜欢
            • 1970-01-01
            • 2011-06-07
            • 1970-01-01
            • 1970-01-01
            • 2014-10-02
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2011-11-05
            相关资源
            最近更新 更多