为什么结构不支持继承?


128

我知道.NET中的结构不支持继承,但是尚不清楚为什么要以这种方式进行限制。

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


6
我并不渴望这种功能,但是我可以想到在某些情况下结构继承很有用:您可能希望将Point2D结构扩展为具有继承的Point3D结构,您可能想从Int32继承以限制其值在1到100之间,您可能想要创建一个在多个文件中可见的type-def(使用typeA = typeB技巧仅具有文件作用域),等等。–
Juliet

4
您可能需要阅读stackoverflow.com/questions/1082311/…,它解释了有关结构的更多信息以及为什么应将它们限制为一定的大小。如果要在结构中使用继承,则可能应该使用类。
贾斯汀2009年

1
您可能想阅读stackoverflow.com/questions/1222935/…,因为它深入探讨了为什么在dotNet平台中无法做到这一点。他们冷酷地使它成为C ++方式,而同样的问题对于托管平台来说可能是灾难性的。
Dykam,

@Justin类具有结构可以避免的性能成本。在游戏开发中,这确实很重要。因此,在某些情况下,如果可以帮助您,则不应该使用课程。
加文·威廉姆斯

@Dykam我认为可以用C#完成。灾难是夸张的。当我不熟悉某项技术时,今天我可以用C#编写灾难性的代码。因此,这并不是真正的问题。如果结构继承可以解决某些问题并在某些情况下提供更好的性能,那么我全力以赴。
加文·威廉姆斯

Answers:


121

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

问题是,出于性能和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,它将按预期进行编译和运行,而不会出现问题。但是,如果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

尝试调试THAT


11
解决该问题的明智方法是禁止将Base []强制转换为Detived []。就像从short []到int []的转换一样,尽管从short到int的转换也是可以的。
Niki

3
+答案:关于继承的问题直到我把它放在数组上之后才发现。另一位用户表示,可以通过将结构“切片”到适当的大小来缓解此问题,但是我认为切片是导致问题超出解决范围的原因。
朱丽叶

4
是的,但这是“有意义的”,因为数组转换用于隐式转换,而不是显式转换。可以将short转换为int,但需要强制转换,因此明智的是,short []不能转换为int [](转换代码的短缺,例如'a.Select(x =>(int)x))。ToArray( )')。如果运行时不允许从Base到Derived进行强制转换,那将是“ wart”,因为引用类型允许使用。因此,我们有两种不同的“缺陷”-禁止结构继承或禁止将派生数组转换为基数组。
09年

3
至少通过防止结构继承,我们有一个单独的关键字,可以更轻松地说“结构是特殊的”,而不是在对一组事物(类)有效但对另一组事物(类)无效的事物中具有“随机”限制。我认为结构限制要容易解释得多(“它们不同!”)。
09年

2
需要将函数的名称从“ square”更改为“ double”
John

69

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

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?

5
C ++通过引入“切片”的概念回答了这一问题,因此这是一个可解决的问题。那么,为什么不应该支持结构继承呢?
09年

1
考虑可继承结构的数组,并记住C#是一种(内存)托管语言。切片或任何类似的选择都会严重破坏CLR的基本面。
科南EK,2009年

4
@jonp:可以解决,是的。好吗 这是一个思想实验:假设您是否有基类Vector2D(x,y)和派生类Vector3D(x,y,z)。这两个类均具有Magnitude属性,该属性分别计算sqrt(x ^ 2 + y ^ 2)和sqrt(x ^ 2 + y ^ 2 + z ^ 2)。如果您写'Vector3D a = Vector3D(5,10,15); Vector2D b = a;','a.Magnitude == b.Magnitude'应该返回什么?如果再写“ a =(Vector3D)b”,则赋值前的a.Magnitude是否具有与赋值后相同的值?.NET设计师可能对自己说:“不,我们将一无所有”。
朱丽叶

1
仅仅因为问题可以解决,并不意味着就应该解决。有时最好避免出现问题的情况。
丹·迪普洛

@ kek444:具有struct Foo继承功能Bar不应允许将a Foo分配给Bar,但是以这种方式声明一个结构可能会产生几个有用的效果:(1)创建一个类型特殊命名的成员Bar作为中的第一项Foo,并Foo包含成员名称该别名来在这些成员Bar,允许代码曾使用Bar被适配为使用Foo代替,而不必替换所有引用thing.BarMemberthing.theBar.BarMember,和保留的读写所有的能力Bar的作为一组字段; ...
超级猫

14

结构不使用引用(除非将它们装箱,但应尽量避免使用引用),因此多态性没有意义,因为没有通过引用指针进行的间接调用。对象通常存在于堆中,并通过引用指针进行引用,但是结构是在堆栈上分配的(除非将它们装箱)或分配在堆上引用类型所占用的内存“内部”。


8
一个不需要使用多态来利用继承
rmeador

那么,您在.NET中会有多少种不同类型的继承?
约翰·桑德斯

多态确实存在于结构中,只需考虑在自定义结构上实现ToString()或不存在ToString()的自定义实现时调用ToString()之间的区别。
Kenan EK,

那是因为它们都派生自System.Object。System.Object类型的多态性胜于结构。
约翰·桑德斯

对于用作通用类型参数的结构,多态可能是有意义的。多态与实现接口的结构一起使用。接口的最大问题是它们不能将byref公开给结构字段。否则,就“继承”结构而言,我认为最大的帮助是使一种具有Foo结构类型字段的类型(结构或类)Bar能够将Bar其成员视为自己的成员的方法,这样一个Point3d类可以例如封装a,Point2d xyX将该字段的称为xy.XX
2013年

8

这是文档所说的:

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

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

另一方面,我同意继承的好处,并且我认为我们都已经达到了希望struct从另一个继承的地步,并意识到这是不可能的。但是到那时,数据结构可能是如此高级,以至于无论如何它都应该是一个类。


4
这不是没有继承的原因。
Dykam,2009年

我相信这里讨论的继承不能使用两种结构,其中一种结构可互换地继承,而重用一种结构的实现并将其添加到另一种结构中(即Point3D从a 创建一个Point2D;您将无法使用a Point3D而不是a Point2D,但您不必Point3D从头开始完全重新实现。)这就是我无论如何解释的方式……
Blixt

简而言之:它可以支持继承而无需多态。没有。我相信这是一个设计选择,以帮助一个人选择classstruct适当的时候。
Blixt

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);

将为A的大小而不是B的大小分配空间。


2
C ++处理这种情况很好,IIRC。将B的实例切成适合A的大小。如果它是纯数据类型,如.NET结构,则不会发生任何不良情况。返回A的方法确实遇到了一些问题,并且将该返回值存储在B中,但是不应该这样做。简而言之,.NET设计人员可以根据需要进行处理,但是由于某些原因而没有。
rmeador

1
对于您的DoSomething(),可能不会出现问题,因为(假设C ++语义)“ b”将被“切片”以创建A实例。问题出在<i>数组</ i>上。考虑您现有的A和B结构,以及<c> DoSomething(A []​​ arg){arg [1] .property = 1;} </ c>方法。由于值类型的数组存储的值是“内联”,因此DoSomething(actual = new B [2] {})将导致实际值[0] .childproperty被设置,而不是实际值[1] .property。这不好。
09

2
@John:我没有断言是,我也不认为@jonp也是。我们只是在提到这个问题已经存在并且已经解决,因此.NET设计人员出于技术上的不可行之外的其他原因选择不支持它。
rmeador

应该注意的是,“派生类型的数组”问题对于C ++来说并不新鲜;参见parashift.com/c++-faq-lite/proper-inheritance.html#faq-21.4(C++中的 数组很邪恶!;-)
2009年

@John:“派生类型和基本类型的数组不混合”问题的解决方案与往常一样是“不要这样做”。这就是为什么C ++中的数组是邪恶的(更容易导致内存损坏),以及.NET不支持带有值类型的继承(编译器和JIT确保它不会发生)的原因。
09

3

我想纠正一点。尽管无法继承结构的原因是因为它们存在于堆栈中是正确的,但这也是正确的一半解释。像任何其他值类型一样,结构也可以存在于堆栈中。因为这将取决于声明变量的位置,所以它们将驻留在堆栈中堆中。它们分别是局部变量或实例字段时。

话虽如此,塞西尔有一个名字正确地钉了它。

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


3

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

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

那么添加字段呢?

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

struct A
{
    public int Integer1;
}

struct B : A
{
    public int Integer2;
}

A a = new B();

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

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

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

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


1
几乎。没有人提到堆栈中的切片问题,只提到数组。没有其他人提到可用的解决方案。
user38001

1
.net中的所有值类型在创建时都填充零,无论它们的类型或它们包含什么字段。向结构添加类似vtable指针的东西将需要一种使用非零默认值初始化类型的方法。此类功能可能对多种用途很有用,并且在大多数情况下实现此类功能可能并不难,但.net中没有紧密的关系。
2012年

@ user38001“结构分配在堆栈上”-除非它们是实例字段,在这种情况下,它们将分配在堆上。
David Klempfner '17

2

这似乎是一个非常常见的问题。我想补充一下,值类型存储在声明变量的“位置”;除了实现细节之外,这意味着没有任何对象标头说明有关该对象的信息,只有该变量知道驻留在其中的数据类型。


编译器知道那里有什么。引用C ++不能解决问题。
亨克·霍尔特曼

您从哪里推断C ++?我会就地说,因为这是与行为最匹配的东西,堆栈是实现细节,引用了MSDN博客文章。
Cecil的名字是2009年

是的,提到C ++不好,只是我的想法。但是,除了是否需要运行时信息的问题之外,为什么结构不应该具有“对象标头”?编译器可以按自己喜欢的方式将它们混合在一起。它甚至可以在[Structlayout]结构上隐藏标题。
亨克·霍尔特曼

因为结构是值类型,所以不需要对象标头,因为运行时总是像其他值类型一样复制内容(约束)。标头没有意义,因为那是引用类型的类:P
Cecil的名字是


0

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

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

该方法运行时,它会从堆栈中弹出一些字节以获取其参数。它知道究竟多少字节爆开,因为参数是任一个的提及类型的指针(在32位总是4个字节),或者它是一个值类型的量,大小始终精确地已知的。

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

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


1
C ++已经解决了,看了这个答案真正的问题:stackoverflow.com/questions/1222935/...
Dykam
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.