我知道.NET中的结构不支持继承,但是尚不清楚为什么要以这种方式进行限制。
什么技术原因阻止结构从其他结构继承?
我知道.NET中的结构不支持继承,但是尚不清楚为什么要以这种方式进行限制。
什么技术原因阻止结构从其他结构继承?
Answers:
值类型不支持继承的原因是由于数组。
问题是,出于性能和GC的原因,值类型的数组“内联”存储。例如,给定new FooType[10] {...}
,如果FooType
是引用类型,则将在托管堆上创建11个对象(一个用于数组,每个类型实例10个)。如果FooType
是值类型,则将在托管堆上为该数组本身创建一个实例(因为每个数组值将与该数组“内联”存储)。
现在,假设我们具有值类型的继承。与数组的上述“内联存储”行为结合使用时,会发生Bad Things,如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);
按照正常的转换规则,a Derived[]
可以转换为a Base[]
(好坏),因此,如果您在上面的示例中使用s / struct / class / g,它将按预期进行编译和运行,而不会出现问题。但是,如果Base
和Derived
是值类型,并且数组内联存储值,那么我们就会遇到问题。
我们有一个问题,因为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
!
尝试调试THAT!
想象一下结构支持的继承。然后声明:
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?
Foo
继承功能Bar
不应允许将a Foo
分配给Bar
,但是以这种方式声明一个结构可能会产生几个有用的效果:(1)创建一个类型特殊命名的成员Bar
作为中的第一项Foo
,并Foo
包含成员名称该别名来在这些成员Bar
,允许代码曾使用Bar
被适配为使用Foo
代替,而不必替换所有引用thing.BarMember
与 thing.theBar.BarMember
,和保留的读写所有的能力Bar
的作为一组字段; ...
结构不使用引用(除非将它们装箱,但应尽量避免使用引用),因此多态性没有意义,因为没有通过引用指针进行的间接调用。对象通常存在于堆中,并通过引用指针进行引用,但是结构是在堆栈上分配的(除非将它们装箱)或分配在堆上引用类型所占用的内存“内部”。
Foo
结构类型字段的类型(结构或类)Bar
能够将Bar
其成员视为自己的成员的方法,这样一个Point3d
类可以例如封装a,Point2d xy
但X
将该字段的称为xy.X
或X
。
这是文档所说的:
结构对于具有值语义的小型数据结构特别有用。复数,坐标系中的点或字典中的键-值对都是结构的良好示例。这些数据结构的关键是它们具有很少的数据成员,不需要使用继承或引用标识,并且可以使用值语义方便地实现它们,其中赋值复制值而不是引用。
基本上,它们应该保存简单的数据,因此没有继承等“额外功能”。从技术上讲,它们可能会支持某种有限的继承(由于它们位于堆栈中,因此不是多态性),但我认为不支持继承也是一种设计选择(与.NET中的许多其他事情一样)语言。)
另一方面,我同意继承的好处,并且我认为我们都已经达到了希望struct
从另一个继承的地步,并意识到这是不可能的。但是到那时,数据结构可能是如此高级,以至于无论如何它都应该是一个类。
Point3D
从a 创建一个Point2D
;您将无法使用a Point3D
而不是a Point2D
,但您不必Point3D
从头开始完全重新实现。)这就是我无论如何解释的方式……
class
在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的大小分配空间。
结构被分配在堆栈上。这意味着值语义几乎是免费的,并且访问结构成员非常便宜。这不会阻止多态。
您可以使每个结构都以指向其虚拟功能表的指针开头。这将是一个性能问题(每个结构都至少是指针的大小),但这是可行的。这将允许虚拟功能。
那么添加字段呢?
好吧,当您在堆栈上分配一个结构时,您会分配一定的空间。所需空间是在编译时确定的(无论是提前还是JITting时)。如果添加字段,然后分配给基本类型:
struct A
{
public int Integer1;
}
struct B : A
{
public int Integer2;
}
A a = new B();
这将覆盖堆栈的某些未知部分。
替代方法是运行时通过仅将sizeof(A)字节写入任何A变量来防止这种情况。
如果B覆盖A中的方法并引用其Integer2字段,会发生什么?运行时将抛出MemberAccessException,或者该方法将访问堆栈中的一些随机数据。这些都不是允许的。
只要您不多态使用结构,或者只要您在继承时不添加字段,就拥有结构继承是绝对安全的。但是这些并不是非常有用。
这似乎是一个非常常见的问题。我想补充一下,值类型存储在声明变量的“位置”;除了实现细节之外,这意味着没有任何对象标头说明有关该对象的信息,只有该变量知道驻留在其中的数据类型。
IL是一种基于堆栈的语言,因此使用自变量调用方法的过程如下:
该方法运行时,它会从堆栈中弹出一些字节以获取其参数。它知道究竟多少字节爆开,因为参数是任一个的提及类型的指针(在32位总是4个字节),或者它是一个值类型的量,大小始终精确地已知的。
如果它是引用类型指针,则该方法在堆中查找对象并获取其类型句柄,该句柄指向一个方法表,该表针对该确切类型处理该特定方法。如果是值类型,则无需查找方法表,因为值类型不支持继承,因此只有一种可能的方法/类型组合。
如果值类型支持继承,那么将存在额外的开销,因为该结构的特定类型及其值必须放置在堆栈中,这意味着对该类型的特定具体实例进行某种方法表查找。这将消除值类型的速度和效率优势。