关于.net:为什么不支持继承?

Why don't structs support inheritance?

我知道.NET中的结构不支持继承,但不清楚为什么它们以这种方式受到限制。

什么技术原因阻止结构从其他结构继承?


值类型不支持继承的原因是数组。

问题是,由于性能和GC原因,值类型数组存储为"inline"。例如,给定new FooType[10] {...},如果FooType是引用类型,那么将在托管堆上创建11个对象(一个用于数组,每个类型实例创建10个对象)。如果FooType不是值类型,则在托管堆上只会为数组本身创建一个实例(因为每个数组值将与数组"内联"存储)。

现在,假设我们有了值类型的继承。当与上述数组的"内联存储"行为结合时,如C++所见,坏事就发生了。

考虑这个伪C代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
struct Base
{
    public int A;
}

struct Derived : Base
{
    public int B;
}

void Square(Base[] values)
{
  for (int i = 0; i < values.Length; ++i)
      values [i].A *= 2;
}

Derived[] v = new Derived[2];
Square (v);

按照正常的转换规则,Derived[]可以转换为Base[](更好或更糟),因此,如果您在上面的示例中使用/struct/class/g,它将按预期编译和运行,不会出现任何问题。但是如果BaseDerived是值类型,数组以内联方式存储值,那么我们就有问题了。

我们遇到了一个问题,因为Square()Derived一无所知,它只使用指针算法来访问数组的每个元素,递增一个常量(sizeof(A))。大会大致如下:

1
2
3
4
5
for (int i = 0; i < values.Length; ++i)
{
    A* value = (A*) (((char*) values) + i * sizeof(A));
    value->A *= 2;
}

(是的,这是一个令人讨厌的程序集,但关键是我们将在已知的编译时常量处通过数组递增,而不知道正在使用派生类型。)

所以,如果这真的发生了,我们会有内存损坏的问题。具体来说,在Square()中,values[1].A*=2实际上是在修改values[0].B

试着调试一下!


假设结构支持继承。然后声明:

1
2
3
4
BaseStruct a;
InheritedStruct b; //inherits from BaseStruct, added fields, etc.

a = b; //?? expand size during assignment?

这意味着结构变量没有固定的大小,这就是为什么我们有引用类型。

更好的是,考虑一下:

1
2
3
BaseStruct[] baseArray = new BaseStruct[1000];

baseArray[500] = new InheritedStruct(); //?? morph/resize the array?


结构不使用引用(除非它们被装箱,但您应该尽量避免这样做),因此多态性没有意义,因为没有通过引用指针进行间接寻址。对象通常位于堆上,并通过引用指针引用,但结构是在堆栈上分配的(除非已装箱),或是在堆上引用类型占用的内存中"内部"分配的。


医生说:

Structs are particularly useful for small data structures that have value semantics. Complex numbers, points in a coordinate system, or key-value pairs in a dictionary are all good examples of structs. Key to these data structures is that they have few data members, that they do not require use of inheritance or referential identity, and that they can be conveniently implemented using value semantics where assignment copies the value instead of the reference.

基本上,它们应该保存简单的数据,因此没有继承等"额外特性"。从技术上讲,它们可能支持某种有限的继承(而不是多态性,因为它们位于堆栈中),但我认为,不支持继承也是一种设计选择(就像.NET语言中的许多其他东西一样)。

另一方面,我同意继承的好处,我认为我们都已经达到了我们希望我们的struct从另一个继承的地步,并且意识到这是不可能的。但是在那一点上,数据结构可能非常先进,无论如何它应该是一个类。


结构是在堆栈上分配的。这意味着值语义非常自由,访问结构成员非常便宜。这并不能阻止多态性。

可以让每个结构以指向其虚拟函数表的指针开头。这将是一个性能问题(每个结构都至少有指针大小),但它是可行的。这将允许虚拟函数。

添加字段怎么样?

好吧,当您在堆栈上分配一个结构时,您将分配一定的空间。所需的空间在编译时确定(无论是提前还是在抖动时)。如果添加字段,然后分配给基类型:

1
2
3
4
5
6
7
8
9
10
11
struct A
{
    public int Integer1;
}

struct B : A
{
    public int Integer2;
}

A a = new B();

这将覆盖堆栈的某些未知部分。

另一种方法是运行时仅将sizeof(a)字节写入任何变量,以防止出现这种情况。

如果B重写A中的一个方法并引用它的integer2字段,会发生什么?要么运行时抛出一个memberAccessException,要么该方法访问堆栈上的一些随机数据。这些都是不允许的。

拥有结构继承是完全安全的,只要您不使用多态结构,或者在继承时不添加字段。但这些并不是很有用。


有一点我想纠正。尽管结构不能被继承的原因是因为它们位于堆栈上是正确的,但这是一个半正确的解释。结构,就像任何其他值类型一样,可以在堆栈中生存。因为它将取决于变量声明的位置,所以它们要么位于堆栈中,要么位于堆中。当它们分别是局部变量或实例字段时,就会出现这种情况。

说这句话,塞西尔有一个正确的名字。

我想强调一下,值类型可以存在于堆栈中。这并不意味着他们总是这么做。局部变量,包括方法参数,将。其他人都不会。然而,这仍然是他们不能被继承的原因。-)


类继承是不可能的,因为结构直接放置在堆栈上。继承结构比它的父结构要大,但JIT不知道这一点,它试图在太少的空间上放置太多的空间。听起来有点不清楚,我们来举个例子:

1
2
3
4
5
6
7
struct A {
    int property;
} // sizeof A == sizeof int

struct B : A {
    int childproperty;
} // sizeof B == sizeof int * 2

如果可能,它将崩溃在以下代码片段上:

1
2
3
4
5
6
void DoSomething(A arg){};

...

B b;
DoSomething(b);

空间分配给sizeof a,而不是sizeof b。


这似乎是一个非常常见的问题。我想补充一点,值类型存储在您声明变量的"就地"位置;除了实现细节之外,这意味着没有对象头说明对象的某些内容,只有变量知道存在哪种类型的数据。


结构确实支持接口,所以您可以这样做一些多态的事情。


IL是一种基于堆栈的语言,因此使用参数调用方法的过程如下:

  • 将参数推到堆栈上
  • 调用方法。
  • 当方法运行时,它会从堆栈中弹出一些字节以获取其参数。它确切知道要弹出多少个字节,因为参数要么是引用类型指针(32位上始终为4个字节),要么是始终准确知道大小的值类型。

    如果它是一个引用类型指针,那么该方法将查找堆中的对象并获取其类型句柄,该句柄指向一个方法表,该表为该确切类型处理该特定方法。如果是值类型,则不需要查找方法表,因为值类型不支持继承,因此只有一种可能的方法/类型组合。

    如果值类型支持继承,那么会有额外的开销,因为结构的特定类型必须放置在堆栈及其值上,这意味着对该类型的特定具体实例进行某种方法表查找。这将消除价值类型的速度和效率优势。