24

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

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

4

11 回答 11

16

有几点需要考虑:

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

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

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

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

于 2009-02-22T22:51:15.843 回答
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;
    }
}
于 2009-02-22T22:40:58.567 回答
14

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

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

  • 班级
    • 可变的
    • 不可变
  • 结构
    • 可变的
    • 不可变

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

于 2009-02-25T18:31:44.087 回答
6

因素:结构、内存要求、拳击。

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

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

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


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

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

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


所以,作为经验法则

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

于 2009-02-23T00:10:25.077 回答
3

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

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

这里我详细描述这个主题:Value Objects Explained

于 2015-01-03T19:44:23.003 回答
2

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

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

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

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

编辑

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

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

编辑 2

MSDN 状态

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

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

于 2009-02-22T22:59:29.973 回答
2

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

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

结构体适用于简单类型,这就是为什么你看到微软对大多数数据类型使用结构体的原因。同样,结构体也适用于在堆栈上有意义的对象。答案之一中提到的 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; }
}

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

于 2009-02-22T23:35:08.530 回答
0

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

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

于 2009-02-22T22:50:56.640 回答
0

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

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

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

行不通。

此外,结构覆盖==!=运算符并实现IEquatable<T>接口是很有礼貌的。

于 2009-02-22T22:55:37.533 回答
0

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

于 2009-02-23T03:05:18.480 回答
-2

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

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

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

于 2010-02-15T14:54:19.487 回答