为什么结构不支持继承?

时间:2009-08-03 15:19:50

标签: .net inheritance struct

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

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

10 个答案:

答案 0 :(得分:118)

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

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

现在,假设我们有值类型的继承。当与数组的上述“内联存储”行为相结合时,坏事就会发生,可以看出in 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一无所知,它只使用指针算法来访问数组的每个元素,递增一个恒定的量({{1} })。大会模糊地说:

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

尝试调试 THAT

答案 1 :(得分:66)

想象一下结构支持继承。然后声明:

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?

答案 2 :(得分:14)

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

答案 3 :(得分:8)

以下是the docs所说的内容:

  

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

基本上,它们应该包含简单数据,因此不具有继承等“额外功能”。它们在技术上可能支持一些有限类型的继承(不是多态,因为它们在堆栈中),但我相信它也是不支持继承的设计选择(在.NET中还有很多其他东西)语言是。)

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

答案 4 :(得分:3)

类似继承的类是不可能的,因为结构直接放在堆栈上。继承结构将比它更大,但是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);

为sizeof A分配空间,而不是为sizeof B分配。

答案 5 :(得分:3)

有一点我想纠正。即使结构不能被继承的原因是因为它们生活在堆栈上是正确的,它是同一个半正确的解释。与任何其他值类型一样,结构可以存在于堆栈中。因为它将取决于声明变量的位置,所以它们将位于堆栈中。这将是它们分别是局部变量或实例字段。

在说这句话时,塞西尔有一个名字正确地钉了它。

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

答案 6 :(得分:3)

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

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

添加字段怎么样?

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

struct A
{
    public int Integer1;
}

struct B : A
{
    public int Integer2;
}

A a = new B();

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

另一种方法是让运行时通过只将sizeof(A)字节写入任何A变量来防止这种情况。

如果B覆盖A中的方法并引用其Integer2字段会发生什么?运行时抛出MemberAccessException,或者该方法访问堆栈上的一些随机数据。这些都不是允许的。

只要不以多态方式使用结构,或者只要在继承时不添加字段,就可以完全安全地进行结构继承。但这些并不是非常有用。

答案 7 :(得分:2)

这似乎是一个非常频繁的问题。我想添加值类型存储在“声明”变量的“就位”;除了实现细节之外,这意味着没有对象标题说明了对象,变量知道那里有哪种数据。

答案 8 :(得分:1)

结构支持接口,所以你可以用这种方式做一些多态的事情。

答案 9 :(得分:0)

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

  1. 将参数推入堆栈
  2. 调用方法。
  3. 当该方法运行时,它会从堆栈中弹出一些字节以获取其参数。它知道完全要弹出多少个字节,因为参数是一个引用类型指针(32位上总是4个字节),或者它是一个值类型,其大小总是准确地知道。 / p>

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

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