代码之家  ›  专栏  ›  技术社区  ›  Joan Venge

值类型是如何从对象(referenceType)派生并仍然是值类型的?

  •  75
  • Joan Venge  · 技术社区  · 15 年前

    C不允许结构从类派生,但所有值类型都从对象派生。这种区别在哪里产生的?

    CLR是如何处理这个问题的?

    6 回复  |  直到 6 年前
        1
  •  98
  •   Eric Lippert    9 年前

    C不允许结构从类派生

    你的陈述不正确,因此你很困惑。C.* 允许结构从类派生。所有结构都派生自从System.Object派生的同一类System.ValueType。所有枚举都从System.Enum派生。

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

    结构是否派生自基类型?

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

    所有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的所有可继承成员也是结构的成员。这就是结构如何获得其 弦线 例如,它是从结构的基类继承的。

    所有可继承成员?当然不是。私人成员可以继承吗?

    对。基类的所有私有成员也是派生类型的成员。当然,如果调用站点不在 成员的可访问域 成员的。仅仅因为你有一个成员并不意味着你可以使用它!

    我们现在继续使用原始答案:


    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)是完全合法的。

    因此,让我们重新表述您的问题:

    值类型是如何从对象(referenceType)派生并仍然是值类型的?

    作为

    每个红框(值类型)是如何位于(从)O框(System.Object)中的?O框是蓝框(引用类型),还是红框(值类型)?

    当你这样说的时候,我希望它是显而易见的。没有什么能阻止你把一个红色的盒子放在盒子v里面,盒子v在盒子o里面,盒子是蓝色的。为什么会有?


    其他更新:

    琼最初的问题是怎么回事 可能的 值类型派生自引用类型。我最初的回答并没有真正解释clr使用的任何机制来解释这样一个事实:我们在两个具有完全不同表示形式的事物之间有一个派生关系——即,被引用的数据是否有一个对象头、一个同步块,以及它是否拥有自己的用于垃圾处理的存储回忆等等。这些机制很复杂,太复杂,无法用一个答案来解释。例如,clr类型系统的规则比我们在c中看到的稍微简化的风格要复杂得多,在c中,对类型的装箱版本和未装箱版本没有很强的区别。泛型的引入也导致了大量额外的复杂性添加到CLR中。有关详细信息,请参阅CLI规范,特别注意装箱和受约束虚拟调用的规则。

        2
  •  19
  •   Reed Copsey    15 年前

    这是由clr维护的有点人为的构造,以便将所有类型视为System.Object。

    值类型从System.Object派生到 System.ValueType ,这是发生特殊处理的地方(即:对于从valuetype派生的任何类型,clr处理装箱/取消装箱等)。

        3
  •  19
  •   JaredPar    15 年前

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

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

        4
  •  5
  •   Servy    10 年前

    你的陈述不正确,因此你很困惑。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的一个规则。

        5
  •  2
  •   supercat    9 年前

    装箱值类型实际上是一个引用类型(它走起来像一个,嘎嘎叫起来像一个,所以它是一个)。我建议,value type不是真正的值类型的基类型,而是在转换为类型对象时可以转换为值类型的基引用类型。未装箱的值类型本身在对象层次结构之外。

        6
  •  0
  •   MulleDK19    6 年前

    理论基础

    在所有答案中,@supercat的答案最接近实际答案。由于其他的答案并不能真正回答问题,并且完全错误地声明(例如值类型继承自任何东西),所以我决定回答这个问题。

    开场白

    这个答案基于我自己的逆向工程和CLI规范。

    struct class 是C关键字。就CLI而言,所有类型(类、接口、结构等)都是由类定义定义的。

    例如,对象类型(在c中称为 )定义如下:

    .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
            }
        }
    }
    

    以下是该结构的类定义:

    .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.ICloneable .

    解释是,当类定义扩展 系统值类型 它实际上定义了两个东西:值类型和值类型对应的装箱类型。 类定义的成员为值类型和对应的装箱类型定义了表示形式。 它不是扩展和实现的值类型,而是相应的装箱类型。这个 extends implements 关键字仅适用于装箱类型。

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

    1. 用3个字段(和一个方法)定义值类型。它不继承任何东西,也不实现任何接口(值类型两者都不能)。
    2. 定义一个对象类型(装箱类型),具有3个字段(并实现一个接口方法),继承自 系统值类型 和执行 系统.icloneable 接口。

    还要注意,任何类定义都会扩展 系统值类型 也本质上是密封的,无论 sealed 是否指定了关键字。

    由于值类型只是简单的结构,不继承、不实现和不支持多态性,因此它们不能与类型系统的其余部分一起使用。 为了解决这个问题,在值类型的顶部,clr还定义了一个具有相同字段的对应引用类型,称为装箱类型。 因此,虽然值类型不能传递给 object ,其对应的装箱类型 可以 .

    现在,如果你要用C语言定义一个方法

    public static void BlaBla(MyNamespace.MyValueType x) ,

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

    上面,我们了解到类定义是由 结构 C中的关键字实际上定义了值类型和对象类型。 在C中,我们只能引用定义的值类型。 然而 ,这只是C的限制。在IL我们 可以 实际上指的是两者。

    当引用IL中的类型时,支持两个约束,其中包括 valuetype . 如果我们使用 valuetype MyNamespace.MyValueType 我们将类型约束为类定义的值类型。 同样,我们可以使用 class MyNamespace.MyValueType 将类型约束到类定义的装箱类型。

    也就是说, .method static void Print(valuetype MyNamespace.MyValueType test) cil managed 采用由 myNamespace.myValueType 类定义,

    虽然 .method static void Print(class MyNamespace.MyValueType test) cil managed 采用由 myNamespace.myValueType 类定义。

    因此,您基本上可以在整个IL程序中实例化和使用相应的装箱类型,就像它被定义为C类一样。 这将初始化valuetype

    newobj void valuetype MyNamespace.MyValueType::.ctor()
    

    运行时将拒绝通过类似newobj的

    newobj void class MyNamespace.MyValueType::.ctor()
    

    然而, box 指令将实例化值类型的装箱类型,并将值类型中的值复制到其中。 这将为您提供一个装箱类型的实例,您可以将其存储在任何 系统值类型 , 对象 类MyNamespace.MyValueType 变量,并传递给 系统值类型 , 对象 类MyNamespace.MyValueType 作为一个论点, 不管出于何种目的,它都会像其他参考类型一样工作。它不是值类型,而是值类型的对应装箱类型。

    例子

    下面是我用IL编写的一个小程序来演示这一点。我一直在评论。特别注意使用 值类型 关键词。

    // Class definition for a class.
    .class MyNamespace.Program
    {
        // Entry point method definition.
        .method static void Main() cil managed
        {
            .entrypoint // This is the entry point of the application.
            .maxstack 8
            .locals init
            (
                [0] valuetype MyNamespace.MyValueType, // Local variable able to hold the value type of the MyNamespace.MyValueType class definition.
                [1] class MyNamespace.MyValueType // Local variable able to hold the boxed type of the MyNamespace.MyValueType class definition.
            )
    
            ldloca.s 0 // Load the address of local variable at index 0 onto the evaluation stack.
            initobj MyNamespace.MyValueType // Set all fields of our value type to 0 (required by the CLI for the assembly to be verifiable).
    
            // The following sets fields A, B and C to 1, 2 and 3 in the value type, respectively.
            ldloca.s 0
            ldc.i4 1
            stfld int32 MyNamespace.MyValueType::A
            ldloca.s 0
            ldc.i4 2
            stfld int32 MyNamespace.MyValueType::B
            ldloca.s 0
            ldc.i4 3
            stfld int32 MyNamespace.MyValueType::C
    
            ldloc.0 // Load a copy of our value type onto the evaluation stack as an argument for the call below.
            call void MyNamespace.Program::Print(valuetype MyNamespace.MyValueType) // Call the overload of Print() that takes a value type.
    
            ldloc.0 // Load a copy of our value type onto the evaluation stack.
            box MyNamespace.MyValueType // Create the corresponding boxed type of our value type.
            stloc.1 // Store it in the local variable that takes the boxed version of our value type.
            ldloc.1 // Load it back onto the evaluation stack.
            call void MyNamespace.Program::Print(class MyNamespace.MyValueType) // Call the overload of Print() that takes a reference type.
            ret // Return.
        }
    
        // This method takes the value type of our MyNamespace.MyValueType class definition.
        // This is equivalent to "static void Print(MyNamespace.MyValueType test)" in C#.
        .method static void Print(valuetype MyNamespace.MyValueType test) cil managed
        {
            .maxstack 8
            // Equivalent to "Console.WriteLine(test.ToString()); test.PrintA();"
            ldarga.s test
            constrained. MyNamespace.MyValueType // callvirt prefix that will box 'test' for the ToString() call below. (Remember, 'test' is a value type, which has no ToString() method, but its corresponding boxed type does)
            callvirt instance string [mscorlib]System.Object::ToString()
            call void [mscorlib]System.Console::WriteLine(string)
            ldarga.s test
            call instance string MyNamespace.MyValueType::PrintA()
            ret
        }
    
        // This method takes the boxed type of our MyNamespace.MyValueType class definition.
        // This is not possible in C#.
        // The closest you can get to this, is to accept the parent class System.ValueType or System.Object,
        // which of course will allow callers to pass ANY boxed value type or object, and won't allow you to call PrintB() directly.
        // This method will only allow the boxed type of this specific value type.
        .method static void Print(class MyNamespace.MyValueType test) cil managed noinlining
        {
            .maxstack 8
            .locals init
            (
                [0] valuetype MyNamespace.MyValueType // Local variable for unboxing operation below.
            )
    
            ldarg.0
            callvirt instance string [mscorlib]System.Object::ToString() // No 'constrained' prefix to box necessary, since 'test' is already a reference type.
            call void [mscorlib]System.Console::WriteLine(string)
    
            // Now, methods in value types operate on the value type, not the boxed type,
            // so even though we can call PrintB() directly because 'test' is a class MyNamespace.MyValueType and not a class System.Object,
            // we have to unbox it and call PrintB() on the unboxed type.
            // (without unboxing, the call will still succeed, but since PrintB() expects the value type and not the boxed type, it will read the wrong field offset
            // (because offset 0 of an object is the object header pointer, and offset 0 of a value type is its first field))
    
            // I'll call PrintB() twice to show two different ways.
            // This first one unboxes, then passes the resulting value type (which is a copy) to PrintB().
            ldarg.0 // Push our MyNamespace.MyValueType boxed type instance onto the evaluation stack.
            unbox.any MyNamespace.MyValueType // Unboxes and pushes a copy of the value type onto the evaluation stack.
            stloc.0 // Store in local variable.
            ldloca.s 0 // Pass the address of the local variable to PrintB() (for the 'this' pointer).
            call instance void MyNamespace.MyValueType::PrintB()
    
            // Now, the above is a full unboxing, so PrintB() receives a copy of 'test'. So if PrintB() were to make changes to the boxed type, it would be on the copy,
            // not the instance that was passed to Print().
            // This can be fixed by using the 'unbox' instruction instead of the 'unbox.any' instruction.
            // 'unbox.any' will calculate the address of the value type part of the boxed type and then copy the data and push it onto the evaluation stack.
            // 'unbox' will just calculate the address of the value type part of the boxed type and push the address onto the evaluation stack.
            // Using just unbox, PrintB will get a pointer to the value type part of the boxed type (object address + size of object header pointer), and thus perform its operations on the actual boxed type instance.
            ldarg.0
            unbox MyNamespace.MyValueType
            call instance void MyNamespace.MyValueType::PrintB()
    
            ret
        }
    }
    
    // Class definition for a value type, defining both the value type and the corresponding boxed type.
    .class MyNamespace.MyValueType extends [mscorlib]System.ValueType
    {
        .field public int32 A;
        .field public int32 B;
        .field public int32 C;
    
        .method void PrintA() cil managed
        {
            // Equivalent to "Console.WriteLine(this.B.ToString());"
            // Identical to PrintB, so I've put the comments in that.
            ldarg.0
            ldflda int32 MyNamespace.MyValueType::A
            ldstr "x8"
            call instance string [mscorlib]System.Int32::ToString(string)
            call void [mscorlib]System.Console::WriteLine(string)
            ret
        }
    
        .method void PrintB() cil managed
        {
            // Equivalent to "Console.WriteLine(this.B.ToString());"
            ldarg.0 // Load the value in the 'this' pointer onto the evaluation stack (Load the address of the struct instance).
            ldflda int32 MyNamespace.MyValueType::B  // Load the address of the field 'B' onto the evaluation stack ('this' pointer for ToString() call below).
            ldstr "x8" // Load constant string "x8" onto the evaluation stack (Format specifier).
            call instance string [mscorlib]System.Int32::ToString(string) // Du'h.
            call void [mscorlib]System.Console::WriteLine(string) // Print it.
            ret
        }
    }
    

    正如你可能注意到的,这两个 Print() 方法是重载,可以是,因为它们没有相同的签名。一个采用的值类型是 myNamespace.myValueType 类定义,另一个则采用 myNamespace.myValueType 类定义。

    如果要组装上面的代码并使用ilspy进行反编译,即使输出为il,也会得到一些令人困惑的结果,因为ilspy(无法对其他反编译程序进行注释)将根据类型假定约束,因此即使一个方法采用引用类型,而另一个方法采用值类型,两者都将显示为 值类型 关键字。当反编译为c时,两个重载签名将显示相同。

    这是上面组装的IL代码的ILSPY输出(注释是我的):

    internal class Program
    {
        private static void DoIt()
        {
            MyNamespace.MyValueType myValueType = default(MyNamespace.MyValueType);
            myValueType.A = 1;
            myValueType.B = 2;
            myValueType.C = 3;
            MyNamespace.Program.Print(myValueType); // The overload taking a value type.
            MyNamespace.MyValueType test = (MyNamespace.MyValueType)(object)myValueType; // ILSpy tries to understand the boxing operation as best as it can, but ends up boxing and unboxing, despite the IL only boxing (because C# boxes/unboxes by casting and cannot differentiate between the value type and boxed type).
            MyNamespace.Program.Print(test); // The overload taking the boxed type.
        }
    
        // The overload taking a value type.
        private static void Print(MyNamespace.MyValueType test)
        {
            Console.WriteLine(test.ToString());
            test.PrintA();
        }
    
        // The overload taking the boxed type.
        private static void Print(MyNamespace.MyValueType test)
        {
            Console.WriteLine(test.ToString());
            ((MyNamespace.MyValueType)(object)test).PrintB();
            ((MyNamespace.MyValueType)test).PrintB();
        }
    }
    
    [StructLayout(LayoutKind.Auto)]
    internal struct MyValueType
    {
        public int A;
        public int B;
        public int C;
    
        private void PrintA()
        {
            Console.WriteLine(A.ToString("x8"));
        }
    
        private void PrintB()
        {
            Console.WriteLine(B.ToString("x8"));
        }
    }
    

    不要把上面的反编译与C混淆,就像如何在C中那样。这是不可能的,而且解压是非常非常错误的。 这只是为了说明ILSPY和其他反编译程序是如何误导用户的。 (就像他们在看诸如 System.Int32 似乎包含自身(而实际上包含 int32 ,这是内置类型,对于 系统32 是相应的CLS类型(就像两个重载方法可以采用值类型和具有相同类定义的装箱类型一样,两个重载可以采用 英特32 系统32 和共存(Ilspy将显示它们都取“int”))。

    ILDASM将正确显示 值类型 约束关键字。

    总结

    所以,总而言之,要回答这个问题:

    值类型是 引用类型和do 继承自 系统值类型 或者其他类型的,他们 不能 实现接口。 相应的 装箱的 类型: 定义 继承自 系统值类型 可以 实现接口。

    .class 定义根据环境定义不同的事物。

    • 如果 界面 指定了语义属性,类定义定义了一个接口。
    • 如果 界面 未指定语义属性,且定义未扩展 系统值类型 类定义定义一个对象类型(类)。
    • 如果 界面 未指定语义属性,定义 延伸 系统值类型 类定义定义了一个值类型。 其对应的装箱类型(结构)。

    内存布局

    本节假设32位进程

    如前所述,值类型没有类型信息,因此无法从其内存位置标识值类型表示的内容。 结构描述了一个简单的数据类型,并且只包含它定义的字段:

    public struct MyStruct
    {
        public int A;
        public short B;
        public int C;
    }
    

    如果我们设想在地址0x1000处分配了一个mystrut实例,那么这就是内存布局:

    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() 定义于 System.Object ,运行时必须对结构进行装箱。

    MyStruct myStruct = new MyStruct();
    Console.WriteLine(myStruct.ToString()); // ToString() call causes boxing of MyStruct.
    

    但是,如果结构重写 托斯特林() 然后调用将被静态绑定,运行时将调用 MyStruct.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.
    

    但是,记住 托斯特林() 在结构中定义,因此对结构值进行操作,因此它需要值类型。与任何其他类一样,装箱类型具有对象头。如果 托斯特林() 在结构上定义的方法是使用中的装箱类型直接调用的。 this 指针,当试图访问字段时 A 在里面 MyStruct ,它将访问偏移量0,在装箱类型中,偏移量0将是对象头指针。 因此装箱类型具有一个隐藏方法,该方法实际重写 托斯特林() . 此隐藏方法取消绑定(仅限地址计算,如“unbox”)装箱类型,然后静态调用 托斯特林() 在结构上定义。

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

    CLI规范

    拳击

    I.82.4 对于每个值类型,CTS都定义了一个对应的引用类型,称为装箱类型。反向不是真的:一般来说,引用类型没有对应的值类型。装箱类型(装箱值)值的表示是一个可以存储该值类型值的位置。装箱类型是对象类型,装箱值是对象。

    定义值类型

    I.87.7 并非所有由类定义定义的类型都是对象类型(参见_ I.8.2.3);特别是,值类型不是对象类型,但它们是使用类定义定义定义的。值类型的类定义定义了(未绑定的)值类型和关联的装箱类型(参见_§I.8.2.4)。类定义的成员定义了这两者的表示。

    2.1.1.3 类型语义属性指定是否定义接口、类或值类型。interface属性指定接口。如果不存在此属性,并且定义扩展(直接或间接)System.ValueType,并且定义不适用于System.Enum,则应定义值类型(_§II.13)。否则,应定义一个类别(_§II.11)。

    值类型不继承

    I.8·9 在未绑定的形式中,值类型不从任何类型继承。装箱值类型应直接从System.ValueType继承,除非它们是枚举,在这种情况下,它们应从System.Enum继承。装箱值类型应密封。

    II.13 未绑定的值类型不被视为其他类型的子类型,对未绑定的值类型使用ISISist指令(请参阅分区III)是无效的。然而,isist指令可以用于装箱值类型。

    I.8·9 值类型不继承;而是类定义中指定的基类型定义装箱类型的基类型。

    值类型不实现接口

    I.87.7 值类型不支持接口约定,但其关联的装箱类型支持。

    II.13 值类型应实现零个或多个接口,但这仅在装箱形式中有意义(_§II.13.3)。

    I.82.4 接口和继承仅在引用类型上定义。因此,尽管值类型定义(_§I.8.9.7)可以指定由值类型实现的两个接口以及它继承的类(System.ValueType或System.Enum),但这些仅适用于装箱值。

    引用值类型与装箱类型

    二、13.1 值类型的未装箱形式应使用value type关键字和类型引用来引用。值类型的装箱形式应使用装箱关键字后跟类型引用来引用。

    注:这里规格不对,没有 boxed 关键字。关键字是 .

    后记

    我认为,值类型似乎是如何继承的一部分混淆源于这样一个事实,即C使用强制转换语法来执行装箱和取消装箱,这使得它看起来像是在执行强制转换,而事实并非如此。 (object)myStruct 在C中,创建值类型的装箱类型的新实例;它不执行任何强制转换。 同样地, (MyStruct)obj 在C中,取消对装箱类型的装箱,复制值部分;它不执行任何强制转换。