为什么结构不支持继承?

我知道结构。NET 不支持继承,但它不完全清楚的 为什么,他们是有限的这种方式。

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

38038 次浏览

那些文件是这么说的:

结构对于具有值语义的小型数据结构特别有用。复数、坐标系中的点或字典中的键-值对都是结构的好例子。这些数据结构的关键在于它们只有很少的数据成员,不需要使用继承或引用标识,并且可以方便地使用值语义实现它们,其中赋值复制值而不是引用。

基本上,它们应该保存简单的数据,因此不具有继承之类的“额外特性”。从技术上来说,它们可能支持某种有限的继承(不是多态性,因为它们在堆栈上) ,但我相信不支持继承也是一种设计选择(就像。NET 语言)

另一方面,我同意继承的好处,我认为我们都已经达到了我们希望我们的 struct从另一个继承的点,并意识到这是不可能的。但是在这一点上,数据结构可能是如此先进,以至于它应该是一个类。

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

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

struct A {
int property;
} // sizeof A == sizeof int


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

如果可能的话,它将在下面的代码片段中崩溃:

void DoSomething(A arg){};


...


B b;
DoSomething(b);

空间是按照 A 的大小分配的,而不是按照 B 的大小分配的。

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

BaseStruct a;
InheritedStruct b; //inherits from BaseStruct, added fields, etc.


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

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

更妙的是,想想这个:

BaseStruct[] baseArray = new BaseStruct[1000];


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

这似乎是一个非常常见的问题。我觉得应该添加值类型存储在声明变量的地方; 除了实现细节,这意味着有一个 没有对象头表示关于对象的内容,只有变量知道哪种类型的数据驻留在那里。

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

说到这里,塞西尔的名字说得很对。

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

IL 是一种基于堆栈的语言,因此使用参数调用方法是这样的:

  1. 将参数推到堆栈上
  2. 调用方法。

当方法运行时,它从堆栈中弹出一些字节来获取它的参数。它知道 没错要弹出多少字节,因为参数要么是引用类型指针(32位上总是4字节) ,要么是值类型,其大小总是已知的。

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

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

结构支持接口,因此可以通过这种方式实现一些多态性。

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

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

现在,假设我们有带值类型的继承。当结合上述数组的“内联存储”行为时,坏事情就会发生,如 在 C + + 中所示。

考虑一下这个伪 C # 代码:

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[](无论是好是坏) ,所以如果上面的示例使用 s/struct/class/g,它将按照预期编译并运行,没有问题。但是如果 BaseDerived是值类型,并且数组内联存储值,那么我们就有问题了。

我们有一个问题,因为 Square()Derived一无所知,它只使用指针算法来访问数组中的每个元素,并以常量递增(sizeof(A))。集会大概是这样的:

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

尝试调试 那个

结构是在堆栈上分配的。这意味着值语义几乎是免费的,而访问 struct 成员非常便宜。这不能阻止多态性。

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

添加字段怎么样?

当你在堆栈上分配一个结构体时,你分配了一定的空间。所需的空间是在编译时确定的(无论是在编译前还是在 JITting 时)。如果添加字段并将其分配给基类型:

struct A
{
public int Integer1;
}


struct B : A
{
public int Integer2;
}


A a = new B();

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

另一种方法是运行库通过仅向任何 A 变量写入 sizeof (A)字节来防止这种情况发生。

如果 B 重写 A 中的方法并引用它的 Integer2字段会发生什么?运行库要么引发 MemberAccessException,要么该方法改为访问堆栈上的某些随机数据。这两者都是不允许的。

只要不以多态方式使用 struct,或者只要在继承时不添加字段,那么使用 struct 继承是完全安全的。但这些并不是特别有用。