我知道.NET中的结构不支持继承,但不清楚为什么它们以这种方式受到限制。
什么技术原因阻止结构从其他结构继承?
- 我并不渴望这种功能,但我可以考虑一些情况,当结构继承有用时:您可能希望将point2d结构扩展到具有继承的point3d结构,您可能希望从int32继承以将其值限制在1到100之间,您可能希望创建跨多个文件可见的类型def(在使用typea=typeb技巧只有文件范围),等等。
- 您可能需要阅读stackoverflow.com/questions/1082311/…,这可以解释更多关于结构的内容,以及为什么它们应该限制在某个大小。如果您想在结构中使用继承,那么您应该使用一个类。
- 您可能还需要阅读stackoverflow.com/questions/1222935/…,因为它深入了解了为什么不能在dotnet平台上完成它。他们的冷使它成为C++的方式,同样的问题对于管理平台来说可能是灾难性的。
- @justin类具有结构可以避免的性能成本。在游戏开发中,这才是真正重要的。所以在某些情况下,如果你能帮助的话,你不应该使用类。
- @Dykam我认为这可以用c来完成。灾难是一种夸大。当我不熟悉一种技术时,我现在可以用C语言编写灾难性的代码。所以这不是真正的问题。如果结构继承能够解决某些问题,并在某些场景下提供更好的性能,那么我完全赞成。
- @加文威廉斯,除非你真的知道怎么做…这基本上是不可能的,因为答案中提出的观点。灾难性的部分是指你开始打破记忆模型的事实。与您所说的相反,除非您使用不安全的/Marshall(以及相关的)API和功能,否则您无法破坏C的内存模型,这将使这一点非常简单。
值类型不支持继承的原因是数组。
问题是,由于性能和GC原因,值类型数组存储为"inline"。例如,给定new FooType[10] {...},如果FooType是引用类型,那么将在托管堆上创建11个对象(一个用于数组,每个类型实例创建10个对象)。如果FooType不是值类型,则在托管堆上只会为数组本身创建一个实例(因为每个数组值将与数组"内联"存储)。
现在,假设我们有了值类型的继承。当与上述数组的"内联存储"行为结合时,如C++所见,坏事就发生了。
考虑这个伪C代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| 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[](更好或更糟),因此,如果您在上面的示例中使用/struct/class/g,它将按预期编译和运行,不会出现任何问题。但是如果Base和Derived是值类型,数组以内联方式存储值,那么我们就有问题了。
我们遇到了一个问题,因为Square()对Derived一无所知,它只使用指针算法来访问数组的每个元素,递增一个常量(sizeof(A))。大会大致如下:
1 2 3 4 5
| 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!
试着调试一下!
- 该问题的合理解决方案是不允许将转换窗体基[]去掉[]。就像禁止从short[]到int[]的强制转换一样,尽管可以从short到int强制转换。
- +答:继承的问题直到你把它放在数组中才与我联系起来。另一个用户说,可以通过将结构"切片"到适当的大小来缓解这个问题,但是我认为切片是导致问题多于它所解决的问题的原因。
- 是的,但这"有意义",因为数组转换是用于隐式转换,而不是显式转换。短到int是可能的,但需要强制转换,因此很明显short[]不能转换为int[](缺少转换代码,如"a.select(x=>(int)x).toarray()"。如果运行时不允许从基到派生的转换,那么它将是一个"wart",因为这对于引用类型是允许的。所以我们有两个不同的"缺点":禁止结构继承或禁止将派生数组转换为基数组。
- 至少,通过防止结构继承,我们有一个单独的关键字,可以更容易地说"结构是特殊的",而不是对一组事物(类)而不是另一组事物(结构)有"随机"限制。我认为结构限制更容易解释("它们是不同的!").
- @乔恩:短到整数不需要演员表。这是一种隐式转换。
- 你说得对,那太糟糕了。实际上,我认为您需要一个C编译器来编译它;)
- 需要将函数名从"square"更改为"double"
- 在数组上下文中您在这里提到的所有内容都将同样地应用于堆栈上的结构;数组只是一个例子,说明为什么它不能工作。
- 即使数组不支持协方差,数组的问题也同样适用,因为理论上人们应该能够将派生的存储到基中,并且让系统仍然知道它是派生的。即使派生类型必须与基类型大小相同(可能通过让基类型预先声明派生结构的最大大小),系统知道对象是基类型还是派生类型的要求仍然是一个问题。但是,如果我有我的druchers,结构将支持多种形式的继承,比如行为:…
- …(1)允许将类型声明为扩展类型或别名其他类型(包括结构);扩展类型或别名类型将在运行时存储为基类型,但可以添加属性或函数(有点像扩展函数,但具有更清晰的作用域规则);(2)允许结构定义继承关系,该关系不会t应用于实例,但会应用于泛型类型,因此继承点和包含颜色的coloredpoint可以传递给例程MovePoint(ref T pt, int dx, int dy) where T:Point。对于后者来说是最有帮助的。
- …如果可能,系统还必须提供执行"byref cast"的方法(这样,如果使用T调用函数(该函数是Point的某些特定派生函数),则可以使用它的额外字段)。请注意,如果将封闭的内容存储为公开的公共字段,那么组合可以对类似于上述的泛型做一些可以做的事情,但有些人不喜欢这些。我怀疑这些功能是否会被添加到.NET中,因为有些人喜欢贬低价值类型,而不是试图修复它们的缺点。
- @jonp"s/struct/class/g"中的s和g是什么意思?
- @倒推戴夫:这是一个SED主义:在全球范围内用class替代struct的所有实例。
假设结构支持继承。然后声明:
1 2 3 4
| BaseStruct a;
InheritedStruct b; //inherits from BaseStruct, added fields, etc.
a = b; //?? expand size during assignment? |
这意味着结构变量没有固定的大小,这就是为什么我们有引用类型。
更好的是,考虑一下:
1 2 3
| BaseStruct[] baseArray = new BaseStruct[1000];
baseArray[500] = new InheritedStruct(); //?? morph/resize the array? |
- C++通过引入"切片"的概念来回答这个问题,所以这是一个可解决的问题。那么,为什么不支持结构继承呢?
- 考虑可继承结构的数组,并记住C是一种(内存)管理的语言。切片或任何类似的选择都会对clr的基本原理造成破坏。
- @乔恩:可以解决,是的。可取的?这里有一个思想实验:假设你有一个基类vector2d(x,y)和派生类vector3d(x,y,z)。这两个类都有一个分别计算sqrt(x^2+y^2)和sqrt(x^2+y^2+z^2)的量值属性。如果您写"vector3d a=vector3d(5,10,15);vector2d b=a;",那么"a.large==b.large"应该返回什么?如果我们写下"a=(vector3d)b",那么a.Magnitude在赋值之前的值和赋值之后的值是否相同?.NET设计者可能会自言自语:"不,我们不会有这些的"。
- 仅仅因为一个问题可以解决,并不意味着它应该被解决。有时,最好避免出现问题的情况。
- +1:简短、优雅、直截了当的回答。
- @kek444:拥有结构Foo继承Bar不应允许将Foo分配给Bar,但以这种方式声明结构可能会产生两种有用的效果:(1)创建一个类型为Bar的特殊命名成员作为Foo中的第一项,并使Foo包含别名为t的成员名。o Bar中的成员,允许使用Bar的代码改为使用Foo,而不必用thing.theBar.BarMember替换对thing.BarMember的所有引用,并保留将Bar的所有字段作为一个组读写的能力;
- (2)一般成员可以使用T:Bar型,在T上使用Bar型成员。如果没有这种能力,允许像Interlocked这样更新Bar成员的唯一方法就是让使用这种成员的接口成员变异this。虽然这当然可以做到,但是具有可变接口的结构的语义是非常奇怪的,特别是如果没有它也允许将表示转换转换为IFoo,就没有办法让结构满足IFoo约束,即使这种转换几乎不会按需要工作。
- 在c中,所有标识符都是引用,因此编译器没有理由不将b框起来,并且在a中保留对b的引用,可能会有linter的警告。
结构不使用引用(除非它们被装箱,但您应该尽量避免这样做),因此多态性没有意义,因为没有通过引用指针进行间接寻址。对象通常位于堆上,并通过引用指针引用,但结构是在堆栈上分配的(除非已装箱),或是在堆上引用类型占用的内存中"内部"分配的。
- 一个人不需要使用多态性来利用继承。
- 那么,你在.NET中有多少种不同类型的继承呢?
- 多态性确实存在于结构中,只需考虑在自定义结构上实现它时调用ToString()与在不存在ToString()的自定义实现时调用它之间的区别。
- 这是因为它们都是从System.Object派生的。它更多的是System.Object类型的多态性,而不是结构的多态性。
- 多态性对于用作泛型类型参数的结构可能有意义。多态性与实现接口的结构一起工作;接口最大的问题是它们不能向结构字段公开byrefs。否则,对于"继承"结构来说,我认为最重要的是有一种方法可以使具有结构类型字段的类型(结构或类)Foo能够将Bar的成员视为自己的成员,以便Point3D类可以例如封装Point2d xy但引用EDOCX1该字段的〔12〕可以是xy.X或X。
医生说:
Structs are particularly useful for small data structures that have value semantics. Complex numbers, points in a coordinate system, or key-value pairs in a dictionary are all good examples of structs. Key to these data structures is that they have few data members, that they do not require use of inheritance or referential identity, and that they can be conveniently implemented using value semantics where assignment copies the value instead of the reference.
基本上,它们应该保存简单的数据,因此没有继承等"额外特性"。从技术上讲,它们可能支持某种有限的继承(而不是多态性,因为它们位于堆栈中),但我认为,不支持继承也是一种设计选择(就像.NET语言中的许多其他东西一样)。
另一方面,我同意继承的好处,我认为我们都已经达到了我们希望我们的struct从另一个继承的地步,并且意识到这是不可能的。但是在那一点上,数据结构可能非常先进,无论如何它应该是一个类。
- 这不是没有遗产的原因。
- 不回答问题。
- 我认为,这里讨论的继承不能使用两个结构,一个从另一个可以互换继承,但可以重新使用并添加到一个结构到另一个结构的实现中(即,从Point2D创建Point3D;不能使用Point3D而不是Point2D,但是您不会不必完全从头开始重新实现Point3D。这就是我对它的解释。
- 简而言之:它可以支持没有多态性的继承。没有。我认为这是一个设计选择,帮助一个人在适当的时候选择class而不是struct。
结构是在堆栈上分配的。这意味着值语义非常自由,访问结构成员非常便宜。这并不能阻止多态性。
可以让每个结构以指向其虚拟函数表的指针开头。这将是一个性能问题(每个结构都至少有指针大小),但它是可行的。这将允许虚拟函数。
添加字段怎么样?
好吧,当您在堆栈上分配一个结构时,您将分配一定的空间。所需的空间在编译时确定(无论是提前还是在抖动时)。如果添加字段,然后分配给基类型:
1 2 3 4 5 6 7 8 9 10 11
| struct A
{
public int Integer1;
}
struct B : A
{
public int Integer2;
}
A a = new B(); |
这将覆盖堆栈的某些未知部分。
另一种方法是运行时仅将sizeof(a)字节写入任何变量,以防止出现这种情况。
如果B重写A中的一个方法并引用它的integer2字段,会发生什么?要么运行时抛出一个memberAccessException,要么该方法访问堆栈上的一些随机数据。这些都是不允许的。
拥有结构继承是完全安全的,只要您不使用多态结构,或者在继承时不添加字段。但这些并不是很有用。
- 这不是已经说过的吗?- 1。
- 几乎。没有其他人提到关于堆栈的切片问题,只提到了数组的切片问题。其他人也没有提到可用的解决方案。
- .NET中的所有值类型在creastion上都是零填充的,不管它们的类型或包含什么字段。向结构添加类似vtable指针的东西需要使用非零默认值初始化类型的方法。这样的功能可能对各种目的都很有用,并且在大多数情况下实现这样的功能可能并不太难,但是.NET中没有类似的功能。
- @user38001"结构是在堆栈上分配的"-除非它们是实例字段,在这种情况下,它们是在堆上分配的。
有一点我想纠正。尽管结构不能被继承的原因是因为它们位于堆栈上是正确的,但这是一个半正确的解释。结构,就像任何其他值类型一样,可以在堆栈中生存。因为它将取决于变量声明的位置,所以它们要么位于堆栈中,要么位于堆中。当它们分别是局部变量或实例字段时,就会出现这种情况。
说这句话,塞西尔有一个正确的名字。
我想强调一下,值类型可以存在于堆栈中。这并不意味着他们总是这么做。局部变量,包括方法参数,将。其他人都不会。然而,这仍然是他们不能被继承的原因。-)
类继承是不可能的,因为结构直接放置在堆栈上。继承结构比它的父结构要大,但JIT不知道这一点,它试图在太少的空间上放置太多的空间。听起来有点不清楚,我们来举个例子:
1 2 3 4 5 6 7
| struct A {
int property;
} // sizeof A == sizeof int
struct B : A {
int childproperty;
} // sizeof B == sizeof int * 2 |
如果可能,它将崩溃在以下代码片段上:
1 2 3 4 5 6
| void DoSomething(A arg){};
...
B b;
DoSomething(b); |
空间分配给sizeof a,而不是sizeof b。
- C++处理这种情况很好,IIrC。B的实例被切片以适合A的大小。如果它是纯数据类型,就像.NET结构一样,那么不会发生任何错误。在返回a的方法中确实遇到了一些问题,并且将返回值存储在b中,但这不应该被允许。简言之,如果他们愿意的话,.NET设计人员本可以处理这个问题,但出于某种原因他们没有处理。
- 对于您的DOMMETHONSE(),不存在问题(因为假设C++语义)"B"将被"切片"来创建一个实例。问题在于数组。考虑您现有的A和B结构,以及ADosomething(A[]arg)arg[1].property=1;.method。由于值类型的数组存储值"inline",DoSomething(actual=new b[2])将导致设置实际的[0].ChildProperty,而不是实际的[1].Property。这很糟糕。
- @约翰:我没有断言是的,我也不认为@jonp是。我们只是提到这个问题是老的并且已经解决了,所以.NET设计者选择不支持它是因为一些原因,而不是技术不可行。
- 应该注意的是,"派生类型的数组"问题对于C++来说并不陌生;请参阅PARASIFIF.COM/C++-FAQLite/适当继承。;-)
- @约翰:"派生类型和基类型的数组不能混合"问题的解决方案是,像往常一样,不要这样做。这就是为什么C++中的数组是邪恶的(更容易允许内存损坏),以及为什么.NET不支持用值类型继承(编译器和JIT确保它不可能发生)。
- 谢谢你提出我的观点:C++不是C语言。
- 对于编译器来说,我至少可以想出两种明智的方法来处理不会导致崩溃的问题。当然,我可以想出很多方法来实现它,但为什么要用这种方法来实现它呢?
这似乎是一个非常常见的问题。我想补充一点,值类型存储在您声明变量的"就地"位置;除了实现细节之外,这意味着没有对象头说明对象的某些内容,只有变量知道存在哪种类型的数据。
- 编译器知道那里有什么。引用C++不可能是答案。
- 你从哪里推断C++的?引用一篇msdn的博客文章,我会说到位是因为这是最符合行为的,栈是一个实现细节。
- 是的,提到C++是不好的,只是我的思路。但是除了需要运行时信息之外,为什么结构不应该有一个"对象头"?编译器可以按照自己的喜好将它们混合在一起。它甚至可以隐藏[structlayout]结构上的头。
- 因为结构是值类型,所以不必使用对象头,因为运行时总是像其他值类型(约束)一样复制内容。使用头是没有意义的,因为这就是引用类型类的用途:p
结构确实支持接口,所以您可以这样做一些多态的事情。
IL是一种基于堆栈的语言,因此使用参数调用方法的过程如下:
将参数推到堆栈上
调用方法。
当方法运行时,它会从堆栈中弹出一些字节以获取其参数。它确切知道要弹出多少个字节,因为参数要么是引用类型指针(32位上始终为4个字节),要么是始终准确知道大小的值类型。
如果它是一个引用类型指针,那么该方法将查找堆中的对象并获取其类型句柄,该句柄指向一个方法表,该表为该确切类型处理该特定方法。如果是值类型,则不需要查找方法表,因为值类型不支持继承,因此只有一种可能的方法/类型组合。
如果值类型支持继承,那么会有额外的开销,因为结构的特定类型必须放置在堆栈及其值上,这意味着对该类型的特定具体实例进行某种方法表查找。这将消除价值类型的速度和效率优势。
- C++解决了这个问题,为实际问题阅读了这个答案:StActPox.com /问题/ 1222935/& Helip;