【发布时间】:2010-12-13 12:36:26
【问题描述】:
C# 不允许结构派生自类,但所有 ValueTypes 都派生自 Object。这种区别在哪里?
CLR 如何处理这个问题?
【问题讨论】:
-
CLR类型系统中
System.ValueType类型的黑魔法结果。
标签: c# .net clr value-type reference-type
C# 不允许结构派生自类,但所有 ValueTypes 都派生自 Object。这种区别在哪里?
CLR 如何处理这个问题?
【问题讨论】:
System.ValueType类型的黑魔法结果。
标签: c# .net clr value-type reference-type
这是一个由 CLR 维护的有点人为的构造,以便允许将所有类型视为 System.Object。
值类型通过System.ValueType 从 System.Object 派生,这是发生特殊处理的地方(即:CLR 处理从 ValueType 派生的任何类型的装箱/拆箱等)。
【讨论】:
小修正,C# 不允许结构自定义派生自任何东西,而不仅仅是类。结构所能做的就是实现一个与派生非常不同的接口。
我认为回答这个问题的最好方法是ValueType 很特别。它本质上是 CLR 类型系统中所有值类型的基类。很难知道如何回答“CLR 如何处理这个问题”,因为这只是 CLR 的规则。
【讨论】:
ValueType很特别,但值得一提的是ValueType本身实际上是一个引用类型。
C# 不允许结构派生自类
您的陈述不正确,因此您感到困惑。 C# 确实 允许结构派生自类。所有结构都派生自同一个类 System.ValueType,后者派生自 System.Object。并且所有枚举都派生自 System.Enum。
更新:某些(现已删除)cmets 存在一些混淆,需要澄清。我会问一些额外的问题:
结构是否派生自基类型?
显然是的。我们可以通过阅读规范的第一页看到这一点:
所有 C# 类型,包括 int 和 double 等基本类型,都继承自一个根对象类型。
现在,我注意到规范夸大了这里的情况。指针类型不是从对象派生的,接口类型和类型参数类型的派生关系比这个草图所表明的要复杂。但是,很明显,所有结构类型都派生自基类型。
还有其他我们知道结构类型从基类型派生的方式吗?
当然。结构类型可以覆盖ToString。如果不是其基本类型的虚拟方法,它会覆盖什么?因此它必须有一个基本类型。该基类型是一个类。
我可以从我选择的类中派生一个用户定义的结构吗?
显然没有。 这并不意味着结构不派生自类。结构派生自一个类,因此继承了该类的可遗传成员。事实上,结构是必需从特定类派生的:枚举需要从Enum派生,结构需要从ValueType派生。因为这些是必需的,C#语言禁止你在代码中陈述派生关系。
为什么要禁止?
当关系是必需时,语言设计者有以下选择:(1) 要求用户键入所需的咒语,(2) 使其成为可选的,或 (3) 禁止。每个都有优点和缺点,C# 语言设计者根据每个的具体细节做出了不同的选择。
例如,const 字段必须是静态的,但禁止说它们是,因为这样做首先是毫无意义的废话,其次,意味着存在非静态 const 字段。但是重载的操作符需要标记为静态,即使开发者别无选择;开发人员很容易相信运算符重载是实例方法。这超越了用户可能会相信“静态”意味着说“虚拟”也是一种可能性的担忧。
在这种情况下,要求用户说他们的结构派生自 ValueType 似乎只是多余的废话,这意味着结构 可以 派生自另一种类型。为了消除这两个问题,C# 规定在代码中声明结构派生自基类型是非法,尽管很明显它确实如此。
类似地,所有委托类型都派生自 MulticastDelegate,但 C# 要求您不要这么说。
所以,现在我们已经确定C# 中的所有结构都派生自一个类。
继承和类派生有什么关系?
很多人对C#中的继承关系感到困惑。继承关系非常简单:如果结构、类或委托类型 D 派生自类类型 B,那么 B 的可继承成员也是 D 的成员。就这么简单。
当我们说结构派生自 ValueType 时,继承是什么意思?简单地说,ValueType 的所有可遗传成员也是结构的成员。例如,这就是结构获得ToString 实现的方式;它继承自结构的基类。
所有可遗传的成员?肯定不是。私人成员可以遗传吗?
是的。基类的所有私有成员也是派生类型的成员。如果调用站点不在成员的可访问域 中,则通过名称调用这些成员当然是非法的。有会员不代表可以使用!
我们现在继续原来的答案:
CLR 如何处理这个问题?
非常好。 :-)
使值类型成为值类型的原因在于它的实例按值复制。使引用类型成为引用类型的原因在于它的实例通过引用复制。您似乎相信值类型和引用类型之间的继承关系有些特殊和不寻常,但我不明白这种信念是什么。 继承与复制的方式无关。
这样看。假设我告诉你以下事实:
有两种盒子,红色的 框和蓝框。
每个红框都是空的。
有三个特殊的蓝色框,分别称为 O、V 和 E。
O 不在任何盒子内。
V 在 O 内。
E 在 V 内。
V 内没有其他蓝色框。
E 内没有蓝色框。
每个红框都在 V 或 E 中。
除 O 之外的每个蓝色盒子本身都在蓝色盒子内。
蓝色框为引用类型,红色框为值类型,O为System.Object,V为System.ValueType,E为System.Enum,“内部”关系为“派生自”。
这是一套完全一致且简单明了的规则,如果您有大量的硬纸板和足够的耐心,您可以轻松地自己实施这些规则。盒子是红色还是蓝色与里面的东西无关;在现实世界中,将一个红色盒子放在一个蓝色盒子里是完全可能的。在 CLR 中,创建从引用类型继承的值类型是完全合法的,只要它是 System.ValueType 或 System.Enum。
所以让我们重新表述你的问题:
ValueTypes 如何从 Object (ReferenceType) 派生并且仍然是 ValueTypes?
作为
怎么可能每个红框(值类型)都在框 O(System.Object)内部(派生自)框 O(System.Object),它是一个蓝框(引用类型)并且仍然是红框(值类型) ?
当你这样说时,我希望它是显而易见的。没有什么能阻止你把一个红色的盒子放在盒子 V 里面,盒子 O 里面是蓝色的。为什么会有?
附加更新:
Joan 最初的问题是关于值类型如何可能从引用类型派生。我最初的回答并没有真正解释 CLR 使用的任何机制来解释我们在具有完全不同表示的两个事物之间存在派生关系这一事实——即,所引用的数据是否具有对象标头、同步块,它是否拥有自己的用于垃圾收集的存储,等等。这些机制很复杂,太复杂了,无法用一个答案来解释。 CLR 类型系统的规则比我们在 C# 中看到的稍微简化的规则要复杂得多,例如,在 C# 中,类型的装箱和未装箱版本之间没有明显的区别。泛型的引入也给 CLR 添加了大量额外的复杂性。有关详细信息,请参阅 CLI 规范,特别注意装箱和受限虚拟调用的规则。
【讨论】:
装箱的值类型实际上是一种引用类型(它走起来像一,嘎嘎声像一,所以实际上它是一)。我建议 ValueType 不是真正的值类型的基本类型,而是基本引用类型,在转换为 Object 类型时可以将值类型转换为该基本引用类型。非装箱值类型本身在对象层次结构之外。
【讨论】:
您的陈述不正确,因此您感到困惑。 C# 确实允许结构派生自类。所有结构都派生自同一个类 System.ValueType
让我们试试这个:
struct MyStruct : System.ValueType
{
}
这甚至不会编译。编译器会提醒你“接口列表中的类型'System.ValueType'不是接口”。
反编译Int32是一个结构体,你会发现:
public struct Int32 : IComparable, IFormattable, IConvertible {}, 更不用说它是从 System.ValueType 派生的。但是在对象浏览器中,您确实会发现 Int32 确实继承自 System.ValueType。
所以所有这些都让我相信:
我认为回答这个问题的最好方法是 ValueType 是特殊的。它本质上是 CLR 类型系统中所有值类型的基类。很难知道如何回答“CLR 如何处理这个问题”,因为这只是 CLR 的规则。
【讨论】:
ValueType 的类型定义时,它使用它来定义两种对象:一种行为类似于引用类型的堆对象类型,以及一种有效地在类型继承系统之外的存储位置类型。因为这两种事物在互斥上下文中使用,所以可以对两者使用相同的类型描述符。在 CLR 级别,结构定义为父类为 System.ValueType,但 C#...
System.ValueType) 继承一件事,并禁止类指定它们从 System.ValueType 继承,因为任何类以这种方式声明将表现得像一个值类型。
在所有答案中,@supercat 的答案最接近实际答案。由于其他答案并没有真正回答这个问题,并且完全不正确的声明(例如值类型继承自任何东西),我决定回答这个问题。
这个答案基于我自己的逆向工程和 CLI 规范。
struct 和 class 是 C# 关键字。就 CLI 而言,所有类型(类、接口、结构等)都由类定义定义。
例如,一个对象类型(在C#中称为class)定义如下:
.class MyClass
{
}
接口由具有interface语义属性的类定义定义:
.class interface MyInterface
{
}
结构可以从System.ValueType 继承并且仍然是值类型的原因是......它们不是。
值类型是简单的数据结构。值类型不从任何东西继承,它们不能实现接口。 值类型不是任何类型的子类型,它们没有任何类型信息。给定值类型的内存地址,无法识别值类型代表什么,这与在隐藏字段中具有类型信息的引用类型不同。
如果我们想象以下 C# 结构:
namespace MyNamespace
{
struct MyValueType : ICloneable
{
public int A;
public int B;
public int C;
public object Clone()
{
// body omitted
}
}
}
以下是该结构的 IL 类定义:
.class MyNamespace.MyValueType extends [mscorlib]System.ValueType implements [mscorlib]System.ICloneable
{
.field public int32 A;
.field public int32 B;
.field public int32 C;
.method public final hidebysig newslot virtual instance object Clone() cil managed
{
// body omitted
}
}
那么这里发生了什么?它显然扩展了System.ValueType,这是一个对象/引用类型,并且实现了System.ICloneable。
解释是,当类定义扩展System.ValueType 时,它实际上定义了两件事:值类型,以及值类型对应的装箱类型。
类定义的成员定义了值类型和相应的装箱类型的表示。
扩展和实现的不是值类型,而是相应的盒装类型。 extends 和 implements 关键字仅适用于盒装类型。
为了澄清,上面的类定义做了两件事:
System.ValueType,实现System.ICloneable接口。另请注意,任何扩展 System.ValueType 的类定义在本质上也是密封的,无论是否指定了 sealed 关键字。
由于值类型只是简单的结构,不继承,不实现也不支持多态,它们不能与类型系统的其余部分一起使用。
为了解决这个问题,在值类型之上,CLR 还定义了具有相同字段的相应引用类型,称为装箱类型。
因此,虽然值类型不能传递给采用 object 的方法,但其对应的盒装类型可以。
现在,如果你要在 C# 中定义一个方法,比如
public static void BlaBla(MyNamespace.MyValueType x),
您知道该方法将采用值类型MyNamespace.MyValueType。
在上面,我们了解到由 C# 中的 struct 关键字产生的类定义实际上同时定义了值类型和对象类型。
但是,我们只能引用定义的值类型。尽管 CLI 规范声明约束关键字 boxed 可用于引用类型的盒装版本,但该关键字并不存在(参见 ECMA-335,II.13.1 引用值类型)。
但让我们暂时想象一下。
当在 IL 中引用类型时,支持几个约束,其中包括 class 和 valuetype。
如果我们使用valuetype MyNamespace.MyType,我们将指定名为MyNamespace.MyType 的值类型类定义。
同样,我们可以使用class MyNamespace.MyType 来指定名为MyNamespace.MyType 的对象类型类定义。
这意味着在 IL 中,您可以拥有同名的值类型(结构)和对象类型(类),并且仍然可以区分它们。
现在,如果 CLI 规范中提到的 boxed 关键字确实实现了,我们就可以使用 boxed MyNamespace.MyType 来指定名为 MyNamespace.MyType 的值类型类定义的装箱类型。
所以,.method static void Print(valuetype MyNamespace.MyType test) cil managed 采用由名为 MyNamespace.MyType 的值类型类定义定义的值类型,
而.method static void Print(class MyNamespace.MyType test) cil managed 采用名为MyNamespace.MyType 的对象类型类定义所定义的对象类型。
同样,如果 boxed 是关键字,.method static void Print(boxed MyNamespace.MyType test) cil managed 将采用名为 MyNamespace.MyType 的类定义定义的值类型的装箱类型。
然后您就可以像任何其他对象类型一样实例化装箱类型,并将其传递给以System.ValueType、object 或boxed MyNamespace.MyValueType 作为参数的任何方法,
出于所有意图和目的,它会像任何其他引用类型一样工作。它不是值类型,而是值类型对应的装箱类型。
所以,总结一下,回答这个问题:
值类型是非引用类型并且不继承自System.ValueType或任何其他类型,并且它们不能实现接口。
对应的boxed类型也定义do继承自System.ValueType并且可以实现接口。
.class 定义根据情况定义不同的事物。
interface语义属性,则类定义定义一个接口。interface语义属性,并且定义没有扩展System.ValueType,则类定义定义了一个对象类型(类)。interface语义属性,并且定义确实扩展System.ValueType,则类定义定义值类型和其对应的装箱类型(结构)。本节假设一个 32 位进程
如前所述,值类型没有类型信息,因此无法从其内存位置识别值类型所代表的内容。 结构描述了一种简单的数据类型,并且只包含它定义的字段:
public struct MyStruct
{
public int A;
public short B;
public int C;
}
如果我们想象 MyStruct 的一个实例被分配在地址 0x1000,那么这就是内存布局:
0x1000: int A;
0x1004: short B;
0x1006: 2 byte padding
0x1008: int C;
结构默认为顺序布局。字段在其自身大小的边界上对齐。添加填充来满足这一点。
如果我们以完全相同的方式定义一个类,如:
public class MyClass
{
public int A;
public short B;
public int C;
}
想象同一个地址,内存布局如下:
0x1000: Pointer to object header
0x1004: int A;
0x1008: int C;
0x100C: short B;
0x100E: 2 byte padding
0x1010: 4 bytes extra
类默认为自动布局,JIT 编译器会以最优化的顺序排列它们。字段在其自身大小的边界上对齐。添加填充来满足这一点。 我不知道为什么,但是每个类最后总是有额外的 4 个字节。
Offset 0 包含对象头的地址,其中包含类型信息、虚方法表等。 与值类型不同,这允许运行时识别地址处的数据代表什么。
因此,值类型不支持继承、接口或多态。
值类型没有虚方法表,因此不支持多态性。 然而,它们对应的盒装类型可以。
当您有一个结构实例并尝试调用在System.Object 上定义的ToString() 之类的虚拟方法时,运行时必须将结构装箱。
MyStruct myStruct = new MyStruct();
Console.WriteLine(myStruct.ToString()); // ToString() call causes boxing of MyStruct.
但是,如果结构覆盖ToString(),则调用将被静态绑定,运行时将调用MyStruct.ToString(),无需装箱,也无需查看任何虚拟方法表(结构没有)。
因此,它还可以内联ToString() 调用。
如果结构覆盖ToString() 并被装箱,则调用将使用虚方法表解决。
System.ValueType myStruct = new MyStruct(); // Creates a new instance of the boxed type of MyStruct.
Console.WriteLine(myStruct.ToString()); // ToString() is now called through the virtual method table.
但是,请记住 ToString() 是在结构中定义的,因此对结构值进行操作,因此它需要一个值类型。与任何其他类一样,盒装类型具有对象标头。如果在结构上定义的ToString() 方法直接使用this 指针中的装箱类型调用,当尝试访问MyStruct 中的字段A 时,它将访问偏移量0,在装箱类型中将是对象头指针。
所以盒装类型有一个隐藏的方法来实际覆盖ToString()。这个隐藏的方法将装箱的类型拆箱(仅地址计算,如unbox IL 指令)然后静态调用结构上定义的ToString()。
同样,装箱类型对每个实现的接口方法都有一个隐藏方法,该方法执行相同的拆箱然后静态调用结构中定义的方法。
I.8.2.4 对于每个值类型,CTS 定义了一个相应的引用类型,称为装箱类型。反之则不然:通常,引用类型没有对应的值类型。装箱类型的值(装箱值)的表示是可以存储值类型的值的位置。装箱类型是对象类型,装箱值是对象。
I.8.9.7 并非所有由类定义定义的类型都是对象类型(参见 §I.8.2.3);特别是,值类型不是对象类型,而是使用类定义定义的。值类型的类定义定义了(未装箱的)值类型和相关的装箱类型(参见 §I.8.2.4)。类定义的成员定义了两者的表示。
II.10.1.3 类型语义属性指定是否应定义接口、类或值类型。 interface 属性指定一个接口。如果此属性不存在并且定义扩展(直接或间接)System.ValueType,并且定义不是针对 System.Enum,则应定义值类型(第 II.13 节)。否则,应定义一个类(§II.11)。
I.8.9.10 在未装箱的形式中,值类型不继承自任何类型。盒装值类型应直接从 System.ValueType 继承,除非它们是枚举,在这种情况下,它们应从 System.Enum 继承。装箱的值类型应密封。
II.13 未装箱的值类型不被视为另一种类型的子类型,并且对未装箱的值类型使用 isinst 指令(参见第 III 部分)是无效的。然而,isinst 指令可用于盒装值类型。
I.8.9.10 值类型不继承;而是类定义中指定的基类型定义了装箱类型的基类型。
I.8.9.7 值类型不支持接口契约,但它们关联的盒装类型支持。
II.13 值类型应实现零个或多个接口,但这仅在其盒装形式中有意义(第 II.13.3 节)。
I.8.2.4 接口和继承仅在引用类型上定义。因此,虽然值类型定义(§I.8.9.7)可以指定应由值类型实现的接口和它继承的类(System.ValueType 或 System.Enum),但这些仅适用于装箱值.
II.13.1 值类型的未装箱形式应使用 valuetype 关键字后跟类型引用来引用。值类型的装箱形式应使用 boxed 关键字后跟类型引用来引用。
注意:这里的规范是错误的,没有boxed关键字。
我认为值类型似乎如何继承的部分困惑源于 C# 使用强制转换语法来执行装箱和拆箱这一事实,这使得您看起来像是在执行强制转换,但事实并非如此(但是,如果尝试拆箱错误的类型,CLR 将抛出 InvalidCastException)。
C# 中的(object)myStruct 创建值类型的装箱类型的新实例;它不执行任何强制转换。
同样,C# 中的(MyStruct)obj 将装箱类型拆箱,将值部分复制出来;它不执行任何强制转换。
【讨论】: