何时会有值类型包含引用类型?

17

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

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

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 中是否有引用类型在值类型中的示例?什么情况下应该使用(如果有的话)?

这里有一些现有的讨论可以查看:https://dev59.com/03NA5IYBdhLWcg3wfNyO - Chris Sinclair
3个回答

20

值类型的实例从不包含引用类型的实例。引用类型对象位于托管堆中,而值类型对象可能包含对该对象的引用。这样的引用具有固定大小。这是很常见的——例如,在结构体内部使用字符串时,就会产生这样的引用。

但是,是的,你无法保证在 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.
}

嗯,由于引用类型的定义是“你只能拥有对它的引用,而不能拥有对象本身”,所以我认为“包含引用类型”是完全可以的,并且恰好暗示了您接下来要描述的内容。 - user395760
@delnan 精确的定义往往是棘手、微妙而且重要的;p - Marc Gravell
我还应该指出,这里的惰性初始化是危险的,可能会导致在评估完属性后实际上没有存储更新的值 - 例如,如果在属性评估之前刚好进行克隆。再次强调:结构体几乎总是不可变的。如果不确定,请将其设置为不可变。如果您认为有一个可变结构体有意义的场景,请再次检查:它可能没有。警告:是的,我已经(并且确实)使用可变结构体 - 但不是轻率地使用。 - Marc Gravell
在编程中,+1 表示对 不可变性 的重视。当有人试图将引用类型添加到结构体中时,这似乎总是被忽视。 - Joseph Yaduvanshi
这个答案中包含了很多有用的信息。除了字符串之外,有没有人有使用引用类型的结构体的.NET框架的例子? - P.Brian.Mackey
List<T>.Enumerator 是一个很好的例子,说明为了提高性能而选择使用结构体。它包含对正在枚举的列表的引用。 - Anders Forsgren

3
有两种主要的有用场景可以使用包含类类型字段的结构体:
  1. 结构体持有一个可能可变的不可变对象的引用(`String` 是迄今为止最常见的)。对于不可变对象的引用将表现为可空值类型和普通值类型之间的交叉;它没有前者的“Value”和“HasValue”属性,但它将具有 null 作为可能(并且默认)值。请注意,如果通过属性访问字段,则该属性可能在字段为 null 时返回非空默认值,但不应修改字段本身。
  2. 结构体持有一个“不可变”的引用到一个可能可变的对象,并用于包装对象或其内容。`List.Enumerator` 是可能使用此模式的最常见的结构体。使结构体字段假装是不可变的有点靠不住(*), 但在某些情况下,它可以很好地工作。在大多数应用此模式的实例中,结构体的行为将基本上像类一样,除了性能更好(**)。

(*) 语句 structVar = new structType(whatever); 将创建 structType 的新实例,将其传递给构造函数,然后通过将所有公共和私有字段从该新实例复制到 structVar 中来改变 structVar 的值;完成后,新实例将被丢弃。因此,所有结构体字段都是可变的,即使它们“假装”不可变;除非知道 structVar = new structType(whatever); 实际上是如何实现的,否则假装它们是不可变的可能会有问题。

(**) 在某些情况下,结构体的性能会更好;在其他情况下,类的性能会更好。通常,在预计它们执行得更好的情况下选择所谓的“不可变”结构体,而且它们的语义与类的语义不同的边缘情况不会成为问题时。

有些人喜欢假装结构体就像类一样,但更有效率,并且不喜欢以利用它们不是类的方式使用结构体。这样的人可能只倾向于使用上述场景(2)。场景1在可变结构体中非常有用,特别是对于像 `String` 这样的类型,其行为基本上类似于值。


0

我想要补充Marc的回答,但是我的评论太长了。

如果你查看C#规范,它对结构体构造函数说:

结构体构造函数使用new运算符调用,但这并不意味着正在分配内存。结构体构造函数只返回结构体值本身(通常在堆栈上的临时位置),而不是动态分配对象并返回对其的引用,然后必要时复制该值。

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

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

除此之外,结构体预期按值进行复制,因此:

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

我曾经在结构体中看到过引用类型,但都是字符串。这是因为字符串是不可变的。我猜测你的 Person 对象不是不可变的,可能会因为与结构体预期行为的偏差而引入非常奇怪和严重的错误。

话虽如此,你在结构体构造函数中遇到的错误可能是因为你有一个公共字段 p 与参数 p 同名,并且没有使用结构体的 this.p 引用它,或者你缺少了关键字 struct


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接