在值对象的不可变对象和结构之间进行选择

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

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

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

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

    • 易变的
    • 一成不变
  • 结构
    • 易变的
    • 一成不变

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

有几点需要考虑:

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

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

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

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

我喜欢用思想实验:

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

根据Richard E的要求编辑

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

例如,概率的有效范围为0-1。 使用小数来表示这种情况很容易出错,需要在每个使用点进行validation。

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

以下是struct表示概率的示例用法:

 public struct Probability : IEquatable, IComparable { 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; } } 

因素:建筑,记忆要求,拳击。

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

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

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


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

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

从那个POV,使用结构不是一个不成熟的优化。


所以,作为经验法则

如果大多数实例都被装箱,请使用不可变对象。
否则,对于小对象,只有在结构构造会导致界面笨拙并且您期望不超过数千个实例时才使用不可变对象。

在今天的世界里(我在想C#3.5)我认为不需要结构(编辑:除了一些利基场景)。

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

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

就个人而言,我不记得上次我在C#中使用了一个结构。

编辑

我建议出于性能原因在C#中使用结构是一个明显的早熟优化*

*除非应用程序已经过性能分析,并且已经将类的使用确定为性能瓶颈

编辑2

MSDN国家:

struct类型适用于表示轻量级对象,如Point,Rectangle和Color。 尽管可以将点表示为类,但在某些情况下结构更有效。 例如,如果声明1000个Point对象的数组,则将为引用每个对象分配额外的内存。 在这种情况下,结构更便宜。

除非您需要引用类型语义,否则系统可以更有效地将小于16字节的类作为结构进行处理。

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

我想说,当你拥有向公众发布的软件时,这就更加迫切。

对于简单类型,结构很好,这就是为什么你看到Microsoft使用大多数数据类型的结构。 以类似的方式,结构对于在堆栈上有意义的对象是好的。 在其中一个答案中提到的Point结构就是一个很好的例子。

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

你提到一个地址作为你的例子。 我们来看一个class级。

 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; } } 

将此对象视为结构。 在考虑中,如果您以这种方式编码,请考虑此地址“struct”中包含的类型。 你看到任何可能不符合你想要的方式吗? 考虑潜在的性能优势(即有一个)?

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

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

在这里,我将详细描述这个主题: Value Objects解释

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

如果高,则为不可变引用(class)类型,否则为value(struct)类型。

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

另一个问题是默认构造函数。 struct 总是有一个默认的(无参数和公共)构造函数,否则语句就像

 T[] array = new T[10]; // array with 10 values 

不行。

此外,结构覆盖==!=运算符并实现IEquatable接口是IEquatable

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

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

使用ref时,结构可以提供很好的性能,但是你必须看看它们正在使用什么内存。 谁控制记忆等

如果你不使用ref和out with structs他们是不值得的,如果你期待一些讨厌的错误:-)