你怎么选择将值对象(一个典型的例子是地址)实现为不可变对象或结构体?
选择其中一种有没有性能、语义或其他方面的好处?
你怎么选择将值对象(一个典型的例子是地址)实现为不可变对象或结构体?
选择其中一种有没有性能、语义或其他方面的好处?
有一些需要考虑的事情:
结构体通常在堆栈上分配。它是值类型,如果数据太大,则在方法之间传递数据可能会很昂贵。
类分配在堆上。它是引用类型,因此通过方法传递对象不会那么昂贵。
通常,我使用结构体来创建不是很大且不可变的对象。当它们保留有限数量的数据或我想要不可变性时,我仅在这种情况下使用它们。一个例子是DateTime
结构。我认为如果我的对象不像DateTime
那样轻巧,那么可能不值得使用作为结构体。此外,如果我的对象作为值类型没有意义(也像DateTime
那样),则可能不适合使用结构体。这里的关键是不可变性。此外,我要强调的是,结构体默认不是不可变的。你必须通过设计使其不可变。
在我遇到的99%的情况中,类是正确的选择。我发现自己很少需要不可变的类。在大多数情况下,我更自然地将类视为可变的。
memcpy
可能会自动优化为使用SSE)。 - yfeldblum我喜欢用一个思维实验:
只调用空构造函数时,这个对象是否有意义?
根据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;
}
}
Date
类型、类似实体的 Complex
数字类型。重点在于 小型,因为我们不希望复制大块数据,通过引用进行间接访问实际上很便宜(所以使用结构体并没有什么好处)。我倾向于始终将结构体设计为不可变的(目前无法想到任何一个例外情况)。由于这最符合内在值类型的语义,我认为这是一个好的规则要遵循。int i = 1; i = 3;
看作是将1替换为数字3的实例。再想想int i = 1; i = new int();
,它是改变了现有值还是用System.Int32的新实例替换了它?能够给变量赋值并不意味着类型是不可变或可变的。而是由于存在可变字段和/或修改器方法,而string和int都没有。此外,MSDN明确指出Int32是一个不可变类型。 - R. Martinho Fernandesconst
或readonly
,否则一个变量总是可变的。而另一方面,“值”则完全不同。在.NET中,所有基本类型的值都是不可变的。你不能改变一个值,就此结束。你所能做的只是将新值分配给现有的符号。(续……) - Konrad Rudolphs += "foo"
和i += 1
之间的区别,但实际上并不存在:在这两种情况下,你改变的都是变量(即符号),而不是对象。对于整数来说,由于它们是值类型,因此没有两个符号可以引用相同的对象(因此我们无法观察到不同的行为)。但从概念上讲,微软非常清楚地表明基本值类型(包括int
)是不可变的。请注意,这与C++不同,在C++中,您可以改变基本类型(并且您可以看到这种差异)。 - Konrad Rudolph因素:构造、内存需求、装箱。
通常,结构体的构造函数限制 - 不能有显式无参构造函数,不能使用base
构造函数 - 决定是否应该使用结构体。例如,如果无参构造函数不应将成员初始化为默认值,则使用不可变对象。
如果您仍然可以选择两者之间,请根据内存需求进行决策。特别是对于小型项目,应该使用结构体,尤其是如果您预计会有许多实例。
当实例被装箱时(例如,为匿名函数捕获或存储在非泛型容器中),这种好处就会丧失 - 您甚至开始为装箱支付额外费用。
什么是“小”,什么是“多”?
对象的开销是(如果我没记错)32位系统上的8个字节。请注意,对于几百个实例,这可能已经决定了一个内部循环是否完全在高速缓存中运行,或者调用GC。如果您预计有数万个实例,则这可能是运行与爬行之间的差异。
从这个角度来看,使用结构体并不是过早优化。
因此,以下是一些经验法则:
如果大多数实例将被装箱,请使用不可变对象。
否则,对于小型对象,仅在结构体构造会导致笨拙的接口且您预计不会超过数千个实例时,请使用不可变对象。
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; }
}
将这个对象视为一个结构体。在考虑时,要考虑这个地址“结构体”内包含的类型,如果您以这种方式编码它。您是否发现有任何可能不按照您想要的方式工作的问题?考虑潜在的性能优势(例如,是否有性能优势)。
我认为出于性能原因在C#中使用结构体是明显的过早优化*
*除非应用程序已进行性能分析,并确定使用类会成为性能瓶颈
MSDN声明:
结构体类型适合表示轻量级对象,如Point、Rectangle和Color。虽然可以将点表示为类,但在某些情况下,结构体更有效率。例如,如果声明一个包含1000个Point对象的数组,则会为引用每个对象分配额外的内存。在这种情况下,结构体的代价更小。除非需要引用类型语义,否则比16字节小的类可能更适合作为结构体由系统更高效地处理。T[] array = new T[10]; // array with 10 values
无法工作。
此外,对于结构体来说,覆盖 ==
和!=
运算符并实现 IEquatable<T>
接口是一种礼貌的做法。
如果按值传递实例,复制的成本是多少。
如果成本高,则使用不可变引用(类)类型,否则使用值(结构体)类型。