如何在实现值对象(作为地址的规范示例)作为不可变对象或结构之间做出选择?
选择一个是否具有表现,语义或任何其他好处?
答案 0 :(得分:15)
有几件事需要考虑:
在堆栈上(通常)分配结构。它是一种值类型,因此如果数据太大,则跨方法传递数据可能会很昂贵。
在堆上分配一个类。它是一种引用类型,因此通过方法传递对象的成本并不高。
通常,我将结构用于不太大的不可变对象。我只在它们中存有有限数量的数据时才使用它们,或者我想要不变性。一个例子是DateTime
结构。我喜欢认为如果我的对象不像DateTime
那样轻量级,那么它可能不值得用作结构体。此外,如果我的对象没有意义作为值类型传递(也像DateTime
),那么它可能没有用作结构。不变性在这里是关键。另外,我想强调默认情况下结构不不可变。你必须通过设计使它们不变。
在我遇到的99%的情况中,一个班级是正确的使用方法。我发现自己不经常需要不可变的课程。在大多数情况下,我认为类是可变的更自然。
答案 1 :(得分:14)
我喜欢使用思想实验:
当只调用空构造函数时,此对象是否有意义?
按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;
}
}
答案 2 :(得分:13)
如何在实现值对象(作为地址的规范示例)作为不可变对象或结构之间做出选择?
我认为你的选择是错误的。不可变对象和结构不是对立的,它们也不是唯一的选择。相反,你有四个选择:
我认为在.NET中,默认选择应该是可变类来表示逻辑和不可变类来表示<强>实体即可。实际上,即使对于逻辑实现,我也倾向于选择不可变类,如果可行的话。应该为模拟值语义的小类型保留结构,例如:自定义Date
类型,Complex
数字类型类似的实体。这里强调的是 small ,因为你不想复制大量的数据,而通过引用的间接实际上很便宜(所以我们通过使用结构不会获得太多收益)。我倾向于使结构总是不可变(我现在想不到一个例外)。由于这最符合内在价值类型的语义,因此我认为这是一个很好的规则。
答案 3 :(得分:6)
因素:建筑,记忆要求,拳击。
通常,构造函数对结构体的限制 - 没有明确的无参数构造函数,没有base
构造 - 决定是否应该使用结构。例如。如果无参数构造函数不将成员初始化为默认值,则使用不可变对象。
如果您仍然可以在两者之间做出选择,请确定内存要求。小项应该存储在结构中,特别是如果你期望很多实例。
当实例被装箱时(例如,为匿名功能捕获或存储在非通用容器中),该优势将丢失 - 您甚至开始为拳击支付额外费用。
什么是“小”,什么是“很多”?
对象的开销是(IIRC)32位系统上的8字节。请注意,对于几百个实例,这可能已经决定内部循环是否在缓存中完全运行,或者调用GC。如果您期望成千上万的实例,这可能是运行与爬网之间的差异。
从那个POV,使用结构不是一个不成熟的优化。
所以,根据经验:
如果大多数实例都被装箱,请使用不可变对象 否则,对于小对象,只有在结构构造会导致界面和笨拙时,才会使用不可变对象。预计不会超过数千个实例。
答案 4 :(得分:2)
在今天的世界里(我正在考虑C#3.5)我认为不需要结构(编辑:除了在一些利基场景中)。
支持结构论证似乎主要基于感知性能优势。我想看一些说明这一点的基准(复制现实场景)。
对于“轻量级”数据结构使用结构的概念似乎对我来说太主观了。什么时候数据不再轻量级?此外,在为使用结构的代码添加功能时,您何时决定将该类型更改为类?
就个人而言,我不记得上次在C#中使用过结构体。
我建议出于性能原因在C#中使用结构是一个明显的早熟优化案例 *
*除非应用程序已经过性能分析,并且已经将类的使用确定为性能瓶颈
MSDN国家:
结构类型适合 表示轻量级对象 作为点,矩形和颜色。 虽然有可能代表一个 作为一个类,结构更多 在某些情况下有效。对于 例如,如果声明一个数组 1000 Point对象,您将分配 用于引用每个内存的额外内存 宾语。在这种情况下,结构是 更便宜。
除非您需要参考类型 语义,一个较小的类 超过16个字节可能更有效 由系统处理为结构。
答案 5 :(得分:2)
通常,我不建议使用业务对象的结构。虽然你可能通过朝着这个方向获得少量性能,但是当你在堆栈上运行时,你最终会在某些方面限制自己,并且在某些情况下默认构造函数可能是个问题。
我会说当你拥有向公众发布的软件时,这就更加迫切了。
对于简单类型,结构很好,这就是为什么你看到Microsoft使用大多数数据类型的结构。以类似的方式,结构对于在堆栈上有意义的对象是好的。其中一个答案中提到的Point结构就是一个很好的例子。
我该如何决定?我通常默认为对象,如果它似乎是一个受益于结构的东西,通常是一个相当简单的对象,只包含实现为结构的简单类型,那么我会考虑它并确定它是否使感。
你提到一个地址作为你的例子。我们来看一个班级。
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”中包含的类型。你看到任何可能不符合你想要的方式吗?考虑潜在的性能优势(即有一个)?
答案 6 :(得分:1)
答案 7 :(得分:0)
如果按值传递,复制实例的成本是多少。
如果为高,则为不可变引用(class)类型,否则为value(struct)类型。
答案 8 :(得分:0)
根据经验,结构大小不应超过16个字节,否则在方法之间传递它可能会比传递对象引用更加昂贵,对象引用只有4个字节(在32位机器上)很长。
另一个问题是默认构造函数。 struct 总是有一个默认的(无参数和公共)构造函数,否则像
这样的语句T[] array = new T[10]; // array with 10 values
无效。
此外,结构覆盖==
和!=
运算符以及实现IEquatable<T>
接口是有礼貌的。
答案 9 :(得分:0)
从对象建模的角度来看,我欣赏结构,因为它们让我使用编译器将某些参数和字段声明为非可空。当然,如果没有特殊的构造函数语义(如Spec#),这仅适用于具有自然“零”值的类型。 (因此布莱恩瓦特的'虽然实验'回答。)
答案 10 :(得分:-2)
结构严格用于高级用户(以及out和ref)。
使用ref时,结构可以提供很好的性能,但是你必须看看它们正在使用什么内存。谁控制记忆等。
如果你不使用ref和out with structs它们是不值得的,如果你期待一些讨厌的错误:-)