为啥结构不支持继承?

Posted

技术标签:

【中文标题】为啥结构不支持继承?【英文标题】:Why don't structs support inheritance?为什么结构不支持继承? 【发布时间】:2010-11-16 09:56:16 【问题描述】:

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

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

【问题讨论】:

我不会为这个功能而死,但我可以想到一些结构继承有用的情况:您可能希望将 Point2D 结构扩展到具有继承的 Point3D 结构,您可能想要要从 Int32 继承以将其值限制在 1 到 100 之间,您可能需要创建一个在多个文件中可见的 type-def(使用 typeA = typeB 技巧仅具有文件范围),等等。 你可能想阅读***.com/questions/1082311/…,它解释了更多关于结构的信息以及为什么它们应该被限制在一定的大小。如果你想在结构中使用继承,那么你可能应该使用一个类。 您可能想阅读***.com/questions/1222935/…,因为它深入了解了为什么它无法在 dotNet 平台中完成。他们冷酷地将其变成了 C++ 方式,同样的问题对于托管平台来说可能是灾难性的。 @Justin 类具有结构可以避免的性能成本。在游戏开发中,这真的很重要。所以在某些情况下,如果你能提供帮助,你不应该使用一个类。 @Dykam 我认为它可以在 C# 中完成。灾难是夸张的。当我不熟悉一种技术时,我今天可以用 C# 编写灾难性的代码。所以这不是一个真正的问题。如果 struct 继承可以解决一些问题,在某些场景下提供更好的性能,那我完全赞成。 【参考方案1】:

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

问题在于,出于性能和 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 一无所知,它只会使用指针算法来访问数组的每个元素,并以一个常数递增 (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!

尝试调试那个

【讨论】:

该问题的明智解决方案是禁止将 Base[] 转换为 Detived[]。就像从 short[] 转换为 int[] 是被禁止的,尽管从 short 转换为 int 是可能的。 +answer: 继承问题直到您将其放在数组方面,我才发现它。另一位用户表示,可以通过将结构“切片”到适当的大小来缓解这个问题,但我认为切片导致的问题比它解决的问题多。 是的,但这“很有意义”,因为数组转换用于隐式转换,而不是显式转换。 short to int 是可能的,但需要强制转换,因此 short[] 不能转换为 int[] 是明智的(缺少转换代码,如 'a.Select(x => (int) x).ToArray( )')。如果运行时不允许从 Base 到 Derived 的转换,那将是一个“疣”,因为它允许引用类型。所以我们有两种可能的“缺点”——禁止结构继承或禁止将派生数组转换为基数组。 至少通过防止结构继承,我们有一个单独的关键字,并且可以更容易地说“结构是特殊的”,而不是在适用于一组事物(类) 但不适用于另一个(结构)。我想结构限制更容易解释(“它们不同!”)。 需要把函数名从'square'改成'double'【参考方案2】:

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

【讨论】:

不需要使用多态来利用继承 那么,.NET 中有多少种不同类型的继承? 结构中确实存在多态性,只需考虑在自定义结构上实现 ToString() 或不存在 ToString() 的自定义实现时调用 ToString() 之间的区别。 那是因为它们都派生自 System.Object。它更多的是 System.Object 类型的多态性,而不是结构体。 多态性对于用作泛型类型参数的结构可能是有意义的。多态性适用于实现接口的结构;接口的最大问题是它们不能将 byrefs 暴露给结构字段。否则,就“继承”结构而言,我认为最有帮助的事情是拥有一个类型(结构或类)Foo 具有结构类型Bar 的字段的方法能够考虑Bar的成员作为自己的成员,因此 Point3d 类可以例如封装Point2d xy,但将该字段的X 称为xy.XX【参考方案3】:

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

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

添加字段怎么样?

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

struct A

    public int Integer1;


struct B : A

    public int Integer2;


A a = new B();

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

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

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

结构继承是完全安全的,只要你不使用多态结构,或者继承时不添加字段。但这些并不是非常有用。

【讨论】:

差不多。没有其他人提及堆栈的切片问题,仅提及数组。没有其他人提到可用的解决方案。 .net 中的所有值类型在创建时都是零填充的,无论它们的类型或它们包含什么字段。将诸如 vtable 指针之类的东西添加到结构将需要一种初始化具有非零默认值的类型的方法。这样的功能可能对各种用途都很有用,并且在大多数情况下实现这样的东西可能不会太难,但在 .net 中不存在任何接近的东西。 @user38001 "结构在堆栈上分配" - 除非它们是实例字段,在这种情况下它们被分配在堆上。【参考方案4】:

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

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?

【讨论】:

C++ 通过引入“切片”的概念来回答这个问题,所以这是一个可以解决的问题。那么,为什么不支持结构继承呢? 考虑可继承结构的数组,并记住 C# 是一种(内存)托管语言。切片或任何类似选项都会对 CLR 的基本原理造成严重破坏。 @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 设计者可能对自己说,“不,我们什么都没有”。 一个问题可以解决,并不意味着它就应该解决。有时最好避免出现问题的情况。 @kek444:让结构Foo 继承Bar 不应允许将Foo 分配给Bar,但以这种方式声明结构可能会产生一些有用的效果: (1) 在Foo 中创建一个Bar 类型的特殊名称成员作为第一项,并让Foo 包含与Bar 中的成员别名的成员名称,允许使用Bar 的代码改为使用Foo,无需将所有对thing.BarMember 的引用替换为thing.theBar.BarMember,并保留作为一个组读取和写入Bar 的所有字段的能力; ...【参考方案5】:

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

【讨论】:

【参考方案6】:

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

    将参数压入堆栈 调用方法。

当方法运行时,它会从堆栈中弹出一些字节以获取其参数。它确切地知道要弹出多少字节,因为参数要么是引用类型指针(在 32 位上始终为 4 个字节),要么是始终确切知道大小的值类型。

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

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

【讨论】:

C++ 已经解决了这个问题,请阅读这个答案以了解真正的问题:***.com/questions/1222935/…【参考方案7】:

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

Cecil Has a Name 说得很对。

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

【讨论】:

“结构不能被继承的原因是因为它们存在于堆栈中是正确的” - 不,这不是原因。 ref 类型的变量将包含对堆中对象的引用。值类型的变量将包含数据本身的值。数据的大小必须在编译时知道。这包括局部变量,其中包括参数,它们都存在于堆栈中。考虑一下,在对象分配期间也必须知道所有对象字段的大小。所以,我接受堆栈是一个一般原因的特殊情况,但这仍然是一个原因。 你这么说,我同意。我在考虑继承的另一半,因为数据不包含指向类 ref 的指针,所以无法处理数据,因此不知道数据来自哪个子类(子结构?) .它只是一个毫无意义的位序列。【参考方案8】:

the docs 是这样说的:

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

基本上,它们应该保存简单的数据,因此不具有诸如继承之类的“额外功能”。它们在技术上可能支持某种有限的继承(不是多态,因为它们在堆栈上),但我相信不支持继承也是一种设计选择(就像 .NET 中的许多其他东西一样语言是。)

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

【讨论】:

这不是没有继承的原因。 我相信这里所讨论的继承不能使用两个结构,其中一个可互换地从另一个继承,而是重用并将一个结构的实现添加到另一个结构(即创建一个Point3D 来自Point2D;您将无法使用Point3D 而不是Point2D,但您不必完全从头开始重新实现Point3D。)这就是我无论如何解释它的方式... 简而言之:它可以支持没有多态性的继承。它没有。我相信这是一种设计选择,可以帮助人们在适当的时候选择class 而不是struct @Blixt - 不,它不能支持继承,因为结构故意缺少必要的方法引用指针。设计标准是结构使用尽可能少的内存。特别是当嵌入另一个实体或数组时。所以它只能通过牺牲结构存在的唯一原因来“支持继承”! @ToolmakerSteve 您可以使用堆栈分配的类型进行简单的继承。看看 Go 中的嵌入式类型。我同意不可能进行您所说的多态继承(上面也提到过)。【参考方案9】:

这似乎是一个非常常见的问题。我想添加值类型存储在您声明变量的“就地”位置;除了实现细节之外,这意味着没有对象头说明了对象的某些内容,只有变量知道那里存在什么样的数据。

【讨论】:

编译器知道那里有什么。引用 C++ 这不是答案。 你从哪里推断出 C++?我会说就地,因为这与行为最匹配,堆栈是一个实现细节,引用 MSDN 博客文章。 是的,提到 C++ 很糟糕,这只是我的思路。但是除了是否需要运行时信息的问题之外,为什么结构不应该有一个“对象头”?编译器可以随意混合它们。它甚至可以隐藏 [Structlayout] 结构上的标题。 因为结构是值类型,所以不需要对象头,因为运行时总是像其他值类型一样复制内容(约束)。使用标头没有意义,因为这就是引用类型类的用途:P【参考方案10】:

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

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

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

如果可能的话,它会在以下 sn-p 上崩溃:

void DoSomething(A arg);

...

B b;
DoSomething(b);

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

【讨论】:

C++ 可以很好地处理这种情况,IIRC。 B 的实例被切片以适应 A 的大小。如果它是纯数据类型,就像 .NET 结构一样,那么不会发生任何不好的事情。您确实遇到了返回 A 的方法的一些问题,并且您将该返回值存储在 B 中,但这是不允许的。简而言之,.NET 设计者本可以处理这个问题,但他们出于某种原因没有这样做。 对于您的 DoSomething(),不太可能出现问题,因为(假设 C++ 语义)“b”将被“切片”以创建 A 实例。问题在于数组。考虑您现有的 A 和 B 结构,以及 DoSomething(A[] arg)arg[1].property = 1; 方法。由于值类型数组存储值“内联”,DoSomething(actual = new B[2]) 将导致设置实际[0].childproperty,而不是实际[1].property。这很糟糕。 @John:我没有断言它是,我也不认为@jonp 是。我们只是提到这个问题很老而且已经解决了,所以 .NET 设计者选择不支持它,除了技术不可行之外的其他原因。 应该注意,“派生类型的数组”问题对 C++ 来说并不新鲜。请参阅parashift.com/c++-faq-lite/proper-inheritance.html#faq-21.4(C++ 中的数组是邪恶的!;-) @John:“派生类型和基类型的数组不能混合”问题的解决方案是,像往常一样,不要那样做。这就是为什么 C++ 中的数组是邪恶的(更容易允许内存损坏),以及为什么 .NET 不支持值类型的继承(编译器和 JIT 确保它不会发生)。

以上是关于为啥结构不支持继承?的主要内容,如果未能解决你的问题,请参考以下文章

为啥大多数编程语言不支持多重继承?

为啥 C++11 不支持匿名结构,而 C11 支持?

java中为啥要使用继承

为啥 C++ 继承机制不透明?

为啥 WPF 支持绑定到对象的属性,但不支持绑定字段?

三:继承