为什么结构不支持inheritance?

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

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

原因值types不支持inheritance是因为数组。

问题在于,出于性能和GC原因,值types数组以“内联”方式存储。 例如,给定new FooType[10] {...} ,如果FooType是一个引用types,将在托pipe堆上创build11个对象(一个用于arrays,10个用于每个types实例)。 如果FooType是一个值types,那么在托pipe堆上只会创build一个实例 – 对于数组本身(因为每个数组的值将与数组“内联”地存储)。

现在,假设我们有值types的inheritance。 结合上述数组的“内联存储”行为,就会发生不好的事情,正如在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是值types,并且数组以内联方式存储值,那么我们有一个问题。

我们有一个问题,因为Square()不知道Derived任何内容,它将只使用指针运算来访问数组中的每个元素,以一个常量( sizeof(A) )递增。 大会可能会隐约像:

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

(是的,这是可恶的程序集,但重点是我们将在已知的编译时常量中增加数组,而不知道正在使用派生types。)

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

尝试debugging!

想象一下结构支持inheritance。 然后宣布:

 BaseStruct a; InheritedStruct b; //inherits from BaseStruct, added fields, etc. a = b; //?? expand size during assignment? 

将意味着结构variables没有固定的大小,这就是为什么我们有引用types。

更好的是,考虑一下:

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

结构不使用引用(除非它们是装箱的,但是你应该尽量避免),因此多态性没有意义,因为没有通过引用指针的间接寻址。 对象通常位于堆上,通过引用指针引用,但是结构体被分配到堆栈上(除非它们被装箱),或者被分配到堆中引用types占用的内存“内部”。

以下是文档所说的内容:

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

基本上,它们应该保存简单的数据,因此不具有诸如inheritance之类的“额外function”。 在技​​术上可能会支持一些有限的inheritance(而不是多态,因为他们在堆栈上),但是我相信这也是不支持inheritance的deviseselect(在.NET中许多其他的东西语言是)。

另一方面,我同意inheritance的好处,我想我们都已经达到了希望我们的structinheritance另一个struct的地步,并意识到这是不可能的。 但是在那个时候,数据结构可能是非常先进的,无论如何它应该是一个类。

像inheritance这样的类是不可能的,因为一个结构直接放在堆栈上。 一个inheritance的结构会比它的父级更大,但是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的大小。

有一点我想纠正。 即使结构不能被inheritance的原因是因为他们住在堆栈上是正确的,所以它们也是一个正确的解释。 结构像任何其他值types都可以存在于堆栈中。 因为它将取决于variables的声明位置,所以它们将存在于堆栈中堆中 。 这将是当他们分别是本地variables或实例字段。

在说,塞西尔有一个名字正确钉牢。

我想强调一下,值types可以在栈上生存。 这并不意味着他们总是这样做。 局部variables,包括方法参数,将会。 所有其他人不会。 尽pipe如此,它仍然是他们不能inheritance的原因。 🙂

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

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

那么添加字段呢?

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

 struct A { public int Integer1; } struct B : A { public int Integer2; } A a = new B(); 

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

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

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

具有结构inheritance是完全安全的,只要不多次使用结构,或者只要在inheritance时不添加字段。 但这些不是非常有用的。

这似乎是一个非常频繁的问题。 我觉得像添加值types被存储“就地”你声明的variables; 除了实现细节之外,这意味着没有对象头部来描述对象的某些事情, 只有variables知道那里的数据types。

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

IL是一个基于堆栈的语言,所以用一个参数来调用一个方法就是这样的:

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

当这个方法运行的时候,它popup一些堆栈的字节来获取它的参数。 它知道究竟有多less个字节可以popup,因为参数是一个引用types的指针(在32位上总是4个字节),或者是一个总是知道大小的值types。

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

如果值types支持inheritance,那么将会有额外的开销,因为结构的特定types将不得不放置在堆栈上以及它的值,这意味着对于该types的特定具体实例的某种方法表查找。 这将消除价值types的速度和效率优势。