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

时间:2009-02-22 22:35:51

标签: c# oop domain-driven-design

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

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

11 个答案:

答案 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)

  

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

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

    • 可变
    • 不可变
  • STRUCT
    • 可变
    • 不可变

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

答案 3 :(得分:6)

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

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

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

当实例被装箱时(例如,为匿名功能捕获或存储在非通用容器中),该优势将丢失 - 您甚至开始为拳击支付额外费用。


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

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

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


所以,根据经验

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

答案 4 :(得分:2)

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

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

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

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

修改

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

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

编辑2

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)

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

  • 结构不支持继承
  • ORM不能正确处理到结构的映射

我在这里详细描述了这个主题:Value Objects explained

答案 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它们是不值得的,如果你期待一些讨厌的错误:-)