前幾天,我介紹了托管環境下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)]
>!dumpobj 013e1a88
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)]
>!dumpobj 013e1a9c
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)]
>!dumpobj 013e1ab0
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 b2Layout與我們在類型定義中的設定值一致;size為20byte,說明CLR對其進行了4byte的內存對齊;
4. ClassExpt2:[StructLayout(LayoutKind.Explicit)]
>!dumpobj 013e1ac4
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來進行下面的測試:
ClassExpt1 expt1 = new 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進行內存對齊。



浙公網安備 33010602011771號