【问题标题】:When would a value type contain a reference type?值类型何时会包含引用类型?
【发布时间】:2012-05-02 13:52:04
【问题描述】:

我了解,使用值类型而不是引用类型的决定应该基于语义,而不是性能。我不明白为什么值类型可以合法地包含引用类型成员?这有几个原因:

首先,我们不应该构建一个需要构造函数的结构。

public struct MyStruct
{
    public Person p;
    // public Person p = new Person(); // error: cannot have instance field initializers in structs

    MyStruct(Person p)
    {
        p = new Person();
    }
}

第二,因为值类型语义:

MyStruct someVariable;
someVariable.p.Age = 2; // NullReferenceException

编译器不允许我在声明时初始化Person。我必须把它移到构造函数,依赖调用者,或者期待一个NullReferenceException。这些情况都不理想。

.NET Framework 是否有值类型中的引用类型示例?我们什么时候应该这样做(如果有的话)?

【问题讨论】:

标签: c# value-type reference-type


【解决方案1】:

值类型的实例从不包含引用类型的实例。引用类型的对象位于托管堆上的某个位置,而值类型的对象可能包含对对象的引用。这样的参考具有固定的大小。这样做很常见——例如每次在结构中使用字符串时。

但是,是的,您不能保证在 struct 中初始化引用类型字段,因为您不能定义无参数构造函数(如果您用 C# 以外的语言定义它,也不能保证它会被调用)。

您说您不应该“构建struct 来要求构造函数”。我说不然。由于值类型应该几乎总是是不可变的,您必须使用构造函数(很可能通过工厂到私有构造函数)。否则它永远不会有任何有趣的内容。

使用构造函数。构造函数很好。

如果您不想传入Person 的实例来初始化p,您可以通过属性使用延迟初始化。 (因为很明显公共字段p只是为了演示,对吧?对吧?)

public struct MyStruct
{
    public MyStruct(Person p)
    {
        this.p = p;
    }

    private Person p;

    public Person Person
    {
        get
        {
            if (p == null)
            {
                p = new Person(…); // see comment below about struct immutability
            }
            return p;
        }
    }

    // ^ in most other cases, this would be a typical use case for Lazy<T>;
    //   but due to structs' default constructor, we *always* need the null check.
}

【讨论】:

  • 好吧,因为引用类型的定义是“你只能引用它,而不是拥有对象本身”,我会说“包含引用类型”非常好,准确地暗示了你接下来要描述的内容。
  • @delnan 精确的定义有棘手、微妙和重要的;p
  • 我还应该注意,延迟初始化在这里很危险,并且可能导致它在评估后实际上不存储更新的值 - 例如,如果它在评估属性之前被克隆。我再次强调:结构应该几乎总是不可变的。如果不确定:使其不可变。如果你认为你有一个可变结构有意义的场景,请再次检查:它可能没有。警告警告:是的,我已经(并且确实)使用了可变结构 - 但不是以一种随意的方式。
  • +1 表示 不可变 备注。当有人试图向结构添加引用类型时,这似乎总是被忽视。
  • 这个答案包含很多很好的信息。有没有人有任何使用具有引用类型(字符串除外)的结构的 .NET 框架示例?
【解决方案2】:

对于包含类类型字段的结构有两个主要有用的场景:

  1. 该结构包含对不可变对象的可能可变引用(到目前为止,`String` 是最常见的)。对不可变对象的引用将表现为可空值类型和普通值类型之间的交叉;它没有前者的“Value”和“HasValue”属性,但它将 null 作为可能(和默认)值。请注意,如果通过属性访问该字段,则该属性可能会在该字段为空时返回非空默认值,但不应修改该字段本身。
  2. 该结构包含对可能可变对象的“不可变”引用,并用于包装对象或其内容。 `List.Enumerator` 可能是使用这种模式的最常见的结构。让结构字段假装是不可变的是一种狡猾的构造(*),但在某些情况下它可以很好地工作。在应用此模式的大多数情况下,结构的行为本质上类似于类的行为,只是性能会更好(**)。

(*) 语句structVar = new structType(whatever); 将创建structType 的新实例,将其传递给构造函数,然后通过将该新实例中的所有公共和私有字段复制到@987654324 中来改变structVar @;一旦完成,新实例将被丢弃。因此,所有结构字段都是可变的,即使它们“假装”其他;假装它们是不可变的可能是狡猾的,除非人们知道structVar = new structType(whatever); 的实际实现方式永远不会造成问题。

(**) 结构体在某些情况下会表现得更好;类将在其他方面表现更好。一般来说,所谓的“不可变”结构会在预期表现更好的情况下被选择而不是类,并且在它们的语义与类的语义不同的极端情况不会造成问题的情况下。

有些人喜欢假装结构体就像类,但效率更高,并且不喜欢利用结构体不是类这一事实来使用结构体。这样的人可能只会倾向于使用上面的场景(2)。场景 #1 对可变结构非常有用,尤其是像 String 这样本质上表现为值的类型。

【讨论】:

    【解决方案3】:

    我想补充 Marc 的答案,但我想说的太多了。

    如果您查看 C# 规范,就会发现结构构造函数:

    使用 new 运算符调用结构构造函数,但确实如此 并不意味着正在分配内存。而不是动态的 分配一个对象并返回一个对它的引用,一个结构 构造函数只是简单地返回结构值本身(通常在 堆栈上的临时位置),然后将此值复制为 必要的。

    (您可以在
    C:\Program Files (x86)\Microsoft Visual Studio 10.0\VC#\Specifications\1033 下找到规范的副本)

    因此,结构构造函数与类构造函数本质上是不同的。

    除此之外,结构体应该是按值复制的,因此:

    对于结构,每个变量都有自己的数据副本,并且 一个操作不可能影响另一个。

    每当我在结构中看到引用类型时,它都是字符串。这是有效的,因为字符串是不可变的。我猜您的 Person 对象不是不可变的,并且可能会引入非常奇怪和严重的错误,因为与结构的预期行为不同。

    话虽如此,您在结构的构造函数中看到的错误可能是您有一个公共字段p,与您的参数p 同名,而不是指结构的p this.p,或者您缺少关键字struct

    【讨论】:

      猜你喜欢
      • 2019-07-14
      • 1970-01-01
      • 2018-01-12
      • 2014-09-21
      • 1970-01-01
      • 2014-02-17
      • 2013-04-21
      • 2012-07-20
      • 2013-08-29
      相关资源
      最近更新 更多