前几天,我介绍了托管环境下struct实例的Layout和Size,其中介绍了StructLayoutAttribute特性,其实StructLayoutAttribute特性不只可以用在struct上,也可以用在class上,下面介绍下将StructLayoutAttribute运用在引用类型上时,对象实例的一些行为。
在.net托管环境下,CRL像一个黑箱一样,将我们创建的对象丢在这个暗箱中进行操作,我们不能直接获得对象实例中字段的布局(Layout)和对象实例的size,但是我们可以通过Visual Studio+SOS扩展来进行非托管代码调试,并获得对象的这些信息。如果对非托管代码调试还不了解,可以参考我以前写的一篇《使用SOS - 在Visual Studio中启用非托管代码调试来支持本机代码调试》。
默认情况下,C#编译器会在引用类型上运用[StructLayoutAttribute(LayoutKind.Auto)]特性,即按照CLR认为的最佳方式来排序实例中的字段顺序;当运用[StructLayout(LayoutKind.Sequential)]特性时,CLR会按照字段成员在被导出到非托管内存时出现的顺序依次布局,但我的测试结果是:貌似使用LayoutKind.Sequential与使用LayoutKind.Auto的结果相同;当运用[StructLayout(LayoutKind.Explicit)]时,我们可以自己设置实例中字段的位置。下面通过实例来进行分析:
namespace Debug
{
class ClassAuto//C#编译器会自动在上面运用[StructLayout(LayoutKind.Auto)]
{
public bool b1; //1Byte
public double d;//8byte
public bool b2; //1byte
}
[StructLayout(LayoutKind.Sequential)]
class ClassSeqt
{
public bool b1; //1Byte
public double d;//8byte
public bool b2; //1byte
}
[StructLayout(LayoutKind.Explicit)]
class ClassExpt1
{
[FieldOffset(0)]
public bool b1; //1Byte
[FieldOffset(0)]
public double d;//8byte
[FieldOffset(8)]
public bool b2; //1byte
}
[StructLayout(LayoutKind.Explicit)]
class ClassExpt2
{
[FieldOffset(0)]
public bool b1; //1Byte
[FieldOffset(0)]
public double d;//8byte
[FieldOffset(0)]
public bool b2; //1byte
}
static class Program
{
[STAThread]
static void Main()
{
ClassAuto deft = new ClassAuto();
ClassSeqt auto = new ClassSeqt();
ClassExpt1 expt1 = new ClassExpt1();
ClassExpt2 expt2 = new ClassExpt2();
return; //注意:在这一行设置断点
}
}
}
在进行测试之前,我先按照《托管环境下struct实例的Layout和Size》和《类型实例的创建位置、托管对象在托管堆上的结构》两篇文中的理论来猜测了下这四个对象的Size:
CLR为每个对象添加SyncblkIndex和TypeHandle两个字段各占了4byte空间,合计8byte(有关SyncblkIndex和TypeHandle两个字段的讨论,可以参考《类型实例的创建位置、托管对象在托管堆上的结构》);
对于在引用类型上应用默认的[StructLayout(LayoutKind.Auto)]特性情况,CLR应该会将两个bool型变量b1和b2放到相邻的位置中,因此ClassAuto的size应该是4(SyncblkIndex)+4(TypeHandle)+8(double d) + 1(bool b1) + 1(bool b2) + 2(4byte内存对齐)=20byte;
对于在引用类型上应用[StructLayout(LayoutKind.Sequential)]的情况,既然声明了布局顺序与声明顺序相同,则需要两个byte变量上的都要进行4byte的内存对齐,ClassSeqt的size应该是:4(SyncblkIndex)+4(TypeHandle)+1(bool b1) +3(4byte内存对齐) + 8(double d) + + 1(bool b2) + 3(4byte内存对齐)=24byte;
对于在引用类型上应用[StructLayout(LayoutKind.Explicit)]的情况,套用“在struct上应用[StructLayout(LayoutKind.Explicit)]时不会进行任何填充”的结论,ClassExpt1的size应该是4(SyncblkIndex)+4(TypeHandle)+8(double d,b1被d吃掉了) + 1(bool b2)=17byte;ClassExpt2的size应该是4(SyncblkIndex)+4(TypeHandle)+8(double d,b1和b2都被d吃掉了)=16byte;
我猜测这四个对象的内存布局图如下所示:
上面仅仅只是我猜测的结果,但实际的测试结果并不完全是这样的,下面是测试过程:
编译上面的代码,在Main函数的“return;”语句处设置断点,按F5进入Debug调试模式,程序运行到断点处中止;然后我们通过“菜单->Debug->Windows->Immediate”打开“Immediate Window”,在该窗口中先输入“.load sos”来启用非托管代码调试,提示已加载SOS.dll扩展后再输入“!DumpHeap -type Class”,此时会输出当前进程中类名中包含“Class”字符串的所有对象的信息,如下图所示:
第一个表的第一列(Address)列出了这些对象的起始地址,第二列(MT)列出了类型的方法表(Method Table)的起始地址;第二个表的最后一列(Class Name)列出了这些对象的类型名,第一列(MT)列出了类型的方法表地址(跟上面这张表的第二列相同),第二列(Count)列出了当前进程中该对象实例的数量,第三列(Totel Size)内出了该类型的所有实例的总Size。下面就具体对各个对象进行分析:
1. ClassAuto:[StructLayout(LayoutKind.Auto)]
Name: Debug.ClassAuto
MethodTable: 00a530b8
EEClass: 00a51524
Size: 20(0x14) bytes
(E:\Project2005\Debug\Debug\bin\Debug\Debug.exe)
Fields:
MT Field Offset Type VT Attr Value Name
79104f64 4000004 c System.Boolean 0 instance 0 b1
791059c0 4000005 4 System.Double 0 instance 0.000000 d
79104f64 4000006 d System.Boolean 0 instance 0 b2
其size和Layout跟猜想中的一致(其中第三列Offset列出了该字段的偏移位置),注意,这里是按4byte进行内存对齐,而不是像struct一样按照成员的最大size进行对齐!
2. ClassSeqt:[StructLayout(LayoutKind.Sequential)]
Name: Debug.ClassSeqt
MethodTable: 00a53150
EEClass: 00a51588
Size: 20(0x14) bytes
(E:\Project2005\Debug\Debug\bin\Debug\Debug.exe)
Fields:
MT Field Offset Type VT Attr Value Name
79104f64 4000007 c System.Boolean 0 instance 0 b1
791059c0 4000008 4 System.Double 0 instance 0.000000 d
79104f64 4000009 d System.Boolean 0 instance 0 b2
其size和Layout竟然跟使用[StructLayout(LayoutKind.Auto)]完全一致,也就是说,我上面猜测的24byte的布局是错误的,观察Offset列的值,我们可以看到,double d排在了第一个位置,bool b1排在了d的后面,也就是说CLR仍然按照[StructLayout(LayoutKind.Auto)]对字段进行布局。这是我在.net framework 2.0(Visual Studio 2005)上的测试结果,不知道其他版本的framework会不会是同样的结果-_-.
3. ClassExpt1:[StructLayout(LayoutKind.Explicit)]
Name: Debug.ClassExpt1
MethodTable: 00a531e8
EEClass: 00a51648
Size: 20(0x14) bytes
(E:\Project2005\Debug\Debug\bin\Debug\Debug.exe)
Fields:
MT Field Offset Type VT Attr Value Name
79104f64 400000a 4 System.Boolean 0 instance 0 b1
791059c0 400000b 4 System.Double 0 instance 0.000000 d
79104f64 400000c c System.Boolean 0 instance 0 b2
Layout与我们在类型定义中的设定值一致;size为20byte,说明CLR对其进行了4byte的内存对齐;
4. ClassExpt2:[StructLayout(LayoutKind.Explicit)]
Name: Debug.ClassExpt2
MethodTable: 00a53280
EEClass: 00a51708
Size: 16(0x10) bytes
(E:\Project2005\Debug\Debug\bin\Debug\Debug.exe)
Fields:
MT Field Offset Type VT Attr Value Name
79104f64 400000d 4 System.Boolean 0 instance 0 b1
791059c0 400000e 4 System.Double 0 instance 0.000000 d
79104f64 400000f 4 System.Boolean 0 instance 0 b2
其size和Layout跟猜想中的一致。
四个对象的实际Layout如下图所示(根据上面表中的Offset来排的):
最后补充一点和struct一样要注意的地方:如果在运用了[StructLayout(LayoutKind.Explicit)],计算FieldOffset一定要小心,例如我们使用上面ClassExpt1来进行下面的测试:
expt1.d = 0;
expt1.b1 = true;
Console.WriteLine(expt1.d);
输出的结果不再是0了,而是4.94065645841247E-324,这是因为expt1.b1和expt1.d共享了一个byte,执行“expt1.b1 = true;时”也改变了expt1.d,CPU在按照浮点数的格式解析expt1.d时就得到了这个结果。(有关浮点数讨论可以参考我以前写的《精确判断一个浮点数是否等于0》)。所以在运用LayoutKind.Explicit时千万别把FieldOffset算错了:)
结论:在32位的计算机上,默认情况下,对于引用类型的实例,CLR总是按4byte进行内存对齐。