ValueTypes如何从Object(ReferenceType)派生而仍然是ValueTypes?


Answers:


107

C#不允许结构派生自类

您的陈述不正确,因此造成您的困惑。C#确实允许结构从类派生。所有结构都派生自同一类System.ValueType,该类派生自System.Object。并且所有枚举都派生自System.Enum。

更新:一些(现在已删除)评论中有些混乱,有必要澄清。我会问一些其他问题:

结构是否衍生自基本类型?

显然是的。我们可以通过阅读规范的第一页看到这一点:

所有C#类型(包括基本类型(如int和double))都从单个根对象类型继承。

现在,我注意到规范在这里夸大了这种情况。指针类型不是从对象派生的,并且接口类型和类型参数类型的派生关系比此草图指示的要复杂。但是,显然所有结构类型都是从基本类型派生的。

我们还有其他方法可以知道结构类型是从基本类型派生的吗?

当然。结构类型可以覆盖ToString。如果不是其基本类型的虚拟方法,它覆盖什么?因此,它必须具有基本类型。该基本类型是一个类。

我可以从自己选择​​的类中派生用户定义的结构吗?

显然没有 这并不意味着结构不是从类派生的。结构派生自一个类,从而继承该类的可继承成员。实际上,必须从特定类派生Enum结构:要求枚举要从中派生,必须从派生中得到结构ValueType。由于这些是必需的,因此C#语言禁止您在代码中声明派生关系。

为什么要禁止它?

需要某种关系,语言设计者可以选择:(1)要求用户键入所需的咒语,(2)将其设为可选,或(3)禁止它。每个都有优缺点,C#语言设计师根据每个细节都选择了不同的内容。

例如,const字段必须是静态的,但禁止说它们是静态的,因为这样做首先是无意义的修饰,其次意味着存在非静态的const字段。但是,即使开发人员别无选择,重载运算符也必须标记为静态。开发人员很难相信操作员重载是实例方法。这消除了用户可能会相信“静态”暗示“虚拟”也是可能的担忧。

在这种情况下,要求用户说他们的结构是从ValueType派生的,这似乎仅仅是多余的词,这意味着该结构可以从另一种类型派生。为了消除这两个问题,C#禁止在代码中声明结构是从基本类型派生的,尽管很明显。

同样,所有委托类型都源自MulticastDelegate,但是C#要求您不要这样说。

因此,现在我们已经确定C#中的所有结构都派生自class

从类继承继承之间有什么关系?

许多人对C#中的继承关系感到困惑。继承关系非常简单:如果结构,类或委托类型D是从类类型B派生的,那么B的可继承成员也是D的成员。就这么简单。

当我们说一个结构体是从ValueType派生出来的时,关于继承是什么意思?简单地说,ValueType的所有可继承成员也是该结构的成员。例如,这就是结构获得其实现的方式ToString。它是从结构的基类继承的。

所有可继承成员?当然不会。私人成员可遗传吗?

是。基类的所有私有成员也是派生类型的成员。如果呼叫站点不在该成员的可访问域中则按名称呼叫这些成员当然是非法的。仅因为您拥有成员并不意味着您可以使用它!

现在,我们继续原始答案:


CLR如何处理此问题?

非常好。:-)

使值类型成为值类型的原因是它的实例按value复制。使引用类型成为引用类型的原因是其实例是通过引用复制的。您似乎相信值类型和引用类型之间的继承关系在某种程度上是特殊且不寻常的,但是我不理解该信念是什么。继承与事物的复制方式无关。

这样看。假设我告诉您以下事实:

  • 有两种盒子,红色盒子和蓝色盒子。

  • 每个红色框都是空的。

  • 有三个特殊的蓝色框,分别称为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)的内部(派生自该框)(蓝色框(引用类型),而仍然是红色框(值类型))?

当您这样说时,希望它是显而易见的。没有什么可以阻止您在框V内放置一个红色框,该框位于框O内,该框为蓝色。为什么会有?


附加更新:

琼最初的问题是关于如何可能的值类型是从引用类型派生的。我最初的回答并没有真正解释CLR用来解释以下事实的任何机制:我们在具有完全不同表示形式的两件事之间存在派生关系,即所引用的数据是否具有对象标头,同步块,是否为垃圾回收目的拥有自己的存储空间,等等。这些机制很复杂,太复杂了,无法一口气解释。CLR类型系统的规则比我们在C#中看到的稍微简化的样式要复杂得多,例如,在框式和未框式的类型之间没有明显的区别。泛型的引入还导致向CLR添加了很多额外的复杂性。


8
语言构造应该有意义。从任意引用类型派生出任意值类型意味着什么?这样的方案可以完成用户定义的隐式转换无法完成的任何事情吗?
Eric Lippert

2
我猜不会。我只是以为您可以将某些成员提供给您作为一个组看到的许多值类型,您可以使用抽象类来派生该结构。我想您可以使用隐式转换,但是您将为此付出性能损失,对吗?如果您正在做数百万个。
Joan Venge

8
知道了 您不希望将继承用作建模“一种”关系的机制,而只是用作在一系列相关类型之间共享代码的机制。这似乎是一个合理的场景,尽管我个人试图避免纯粹将继承用作代码共享的便利。
埃里克·利珀特

3
琼一次定义行为,就可以创建一个接口,让要共享行为的结构实现该接口,然后创建在该接口上运行的扩展方法。这种方法的一个潜在问题是,在调用接口方法时,将首先对结构进行装箱,并将复制的装箱值传递给扩展方法。状态的任何更改都将在对象的副本上发生,这对于API用户而言可能是不直观的。
David Silva Smith,

2
@Sipo:现在,公平地说,问题包括“ CLR如何处理此问题?” 答案很好地描述了CLR如何执行这些规则。但这就是问题:我们应该期望实现一种语言的系统没有与该语言相同的规则!实现系统必定是较低级的,但不要让该较低级系统的规则与基于其构建的较高级系统的规则相混淆。当然,正如我在回答中所指出的那样,CLR类型系统区分了装箱和未装箱的值类型。但是C#不会
埃里克·利珀特

21

小修正,C#不允许结构自定义派生自任何东西,而不仅仅是类。一个结构体所能做的就是实现一个与派生非常不同的接口。

我认为回答这个问题的最好方法ValueType是特殊的。本质上,它是CLR类型系统中所有值类型的基类。很难知道如何回答“ CLR如何处理此问题”,因为这只是CLR的一条规则。


对于结构不衍生自任何事物的优点,+ 1是个好主意[除非隐式衍生自System.ValueType]。
里德·科普西

2
您说这ValueType很特殊,但是值得一提的是ValueType它本身实际上是一个引用类型。
路加福音

如果在内部结构可以从类派生,为什么它们不为每个人公开呢?
Joan Venge

1
@琼:他们不是,真的。这只是为了可以将结构转换为对象,并在那里进行实用。但是从技术上讲,与类的实现方式相比,CLR对值类型的处理完全不同。
里德·科普西

2
@JoanVenge我相信这里的困惑是说结构是从CLR中的ValueType类派生的。我认为,在CLR中并不真正存在结构是更正确的说法,CLR中“ struct”的实现实际上是ValueType类。因此,这并不像struct是从CLR中的ValueType继承的。
–wired_in

20

这是CLR维护的某种程度上人为的构造,以便将所有类型都视为System.Object。

值类型通过System.ValueType从System.Object派生,在System.ValueType处进行特殊处理(即:对于从ValueType派生的任何类型,CLR都会处理装箱/拆箱等操作)。


6

您的陈述不正确,因此您感到困惑。C#确实允许结构从类派生。所有结构都派生自同一类System.ValueType

因此,让我们尝试一下:

 struct MyStruct :  System.ValueType
 {
 }

这甚至不会编译。编译器会提醒您“接口列表中的类型'System.ValueType'不是接口”。

反编译作为结构的Int32时,您会发现:

公共结构Int32:IComparable,IFormattable,IConvertible {},更不用说它是从System.ValueType派生的。但是在对象浏览器中,您确实发现Int32确实继承自System.ValueType。

因此,所有这些使我相信:

我认为回答这个问题的最好方法是ValueType很特殊。本质上,它是CLR类型系统中所有值类型的基类。很难知道如何回答“ CLR如何处理此问题”,因为这只是CLR的一条规则。


.NET中使用相同的数据结构来描述值类型和引用类型的内容,但是当CLR看到定义为派生自的类型定义时ValueType,它将使用它来定义两种对象:堆对象类型行为类似于引用类型,以及实际上位于类型继承系统之外的存储位置类型。因为这两种事物在互斥上下文中使用,所以相同的类型描述符可以用于两者。在CLR级别上,将一个struct定义为其父级为System.ValueType,但C#为...的类
supercat 16-10-25

...禁止指定结构继承自任何东西,因为它们只能从(System.ValueType)继承,并且禁止类指定其继承自System.ValueType因为以这种方式声明的任何类的行为都将类似于值类型。
超级猫

3

装箱值类型实际上是一种引用类型(它走路像一只,而嘎嘎像一只,所以实际上是一种)。我建议ValueType并不是值类型的基本类型,而是将类型转换为Object类型时可以将值类型转换为的基本引用类型。非盒装值类型本身在对象层次结构之外。


1
我认为您的意思是,“ ValueType并不是类型的基本类型”
–wired_in

1
@wired_in:谢谢。已更正。
超级猫

2

基本原理

在所有答案中,@ 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它实际上定义了两件事:值类型和值类型的对应的框式。类定义的成员定义值类型和相应的框式类型的表示形式。不是扩展和实现的值类型,而是相应的框式类型。在extendsimplements关键字仅适用于盒装类型。

为了澄清,上面的类定义做了两件事:

  1. 定义具有3个字段(和一个方法)的值类型。它不继承任何东西,也不实现任何接口(值类型不能做任何事情)。
  2. 定义一个对象类型(带框的类型),该对象类型具有3个字段(并实现一个接口方法),并继承System.ValueType并实现该System.ICloneable接口。

还要注意System.ValueType,无论是否sealed指定了关键字,任何扩展的类定义都在本质上是密封的。

由于值类型只是简单的结构,不继承,不实现且不支持多态,因此它们不能与其他类型系统一起使用。要解决此问题,CLR在值类型的顶部还定义了具有相同字段的对应引用类型,称为盒装类型。因此,虽然不能将值类型传递给采用an的方法object,但可以将其对应的框式类型传递给。

 

现在,如果您要在C#中定义一个方法,例如

public static void BlaBla(MyNamespace.MyValueType x)

您知道该方法将采用值类型MyNamespace.MyValueType

上面,我们了解了structC#中关键字产生的类定义实际上定义了值类型和对象类型。但是,我们只能引用定义的值类型。即使CLI规范指出boxed可以使用constraint关键字来引用类型的盒装版本,但该关键字不存在(请参阅ECMA-335,II.13.1引用值类型)。但是让我们想象一下它确实会起作用。

在IL中引用类型时,支持两个约束,其中包括classvaluetype。如果使用,valuetype MyNamespace.MyType我们将指定名为MyNamespace.MyType的值类型类定义。同样,我们可以class MyNamespace.MyType用来指定名为MyNamespace.MyType的对象类型类定义。这意味着在IL中,您可以具有相同名称的值类型(结构)和对象类型(类),并且仍可以区分它们。现在,如果boxed实际上实现了CLI规范指出的关键字,我们将能够使用它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.ValueTypeobject或者boxed MyNamespace.MyValueType作为一个参数,它会为所有意图和目的,像其他任何引用类型的工作。它不是值类型,而是值类型的相应框式类型。

 

概要

因此,总而言之,并回答这个问题:

值类型不是引用类型,并且不能继承System.ValueType或任何其他类型,并且它们不能实现接口。定义了相应的框式类型,它们确实可以继承接口并可以实现接口。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个字节。

偏移量0包含对象标头的地址,该对象标头包含类型信息,虚拟方法表等。这使运行时可以识别地址处的数据代表什么,与值类型不同。

因此,值类型不支持继承,接口或多态性。

方法

值类型没有虚拟方法表,因此不支持多态。 但是,它们对应的盒装类型却可以

当您具有结构的实例并尝试调用类似ToString()on的虚拟方法时System.Object,运行时必须将结构装箱。

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()是在struct中定义的,因此对struct值进行操作,因此它需要一个值类型。与其他任何类一样,盒装类型具有对象标头。如果在ToString()结构体上定义的方法是使用this指针中的框式类型直接调用的,则当尝试访问中的字段AMyStruct,它将访问偏移量0,在框式中它将是对象标头指针。因此,盒装类型具有一个隐藏的方法,可以对进行实际的覆盖ToString()。此隐藏方法取消装箱(仅地址计算,如unboxIL指令),然后将装箱的类型静态调用ToString()结构上的定义。

同样,装箱的类型对于每个实现的接口方法都有一个隐藏的方法,该方法执行相同的拆箱操作,然后静态调用结构中定义的方法。

 

CLI规范

拳击

I.8.2.4对于每种值类型,CTS都定义了一个相应的引用类型,称为盒装类型。反之则不成立:通常,引用类型没有对应的值类型。装箱类型的值(装箱值)的表示形式是可以存储值类型的值的位置。装箱类型是对象类型,装箱值是对象。

定义值类型

I.8.9.7并非由类定义定义的所有类型都是对象类型(请参见§I.8.2.3);参见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关键字。

结语

我认为,对值类型的继承方式感到困惑的部分原因是,C#使用强制转换语法执行装箱和拆箱操作,这似乎使您似乎在执行强制转换,但实际上并非如此(尽管如果尝试取消装箱错误的类型,则CLR将抛出InvalidCastException。 (object)myStruct在C#中创建值类型的框式类型的新实例;它不执行任何强制转换。同样,(MyStruct)obj在C#中,取消装箱类型,将值部分复制出去。它不执行任何强制转换。


1
最后,一个明确描述其工作原理的答案!这个应该是公认的答案。做得好!
Just Shadow
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.