理论基础
在所有答案中,@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
关键字仅适用于装箱类型。
为了澄清,上面的类定义做了两件事:
-
用3个字段(和一个方法)定义值类型。它不继承任何东西,也不实现任何接口(值类型两者都不能)。
-
定义一个对象类型(装箱类型),具有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中,取消对装箱类型的装箱,复制值部分;它不执行任何强制转换。