以前寫過《值類型不是值類型》一文。今天,就再來個語言游戲:public 的不public,fixed 的不能 fixed。本文將構造一個古怪類型:public字段無法訪問,標了fixed關鍵字卻無法fixed。
(1)從 fixed 說起
fixed 的經典用法是取分配到托管堆上的值類型或值類型數組的地址,如:
public class Test { private int Value; private Int32[] Array = new Int32[3]; public unsafe void Foo() { fixed (int* p = &Value) { *p = 30; } fixed (Int32* pArray = Array) { pArray[0] = 30; } } }
為什么要fixed呢?山不轉水轉,有GC存在,隨時可能把它所占的內存搬位置。fixed 就是告訴GC,這一塊內存正被指針操作,別動。
下面是 fixed 的另一種用法。在C中,我們可以在 struct 中嵌入數組,如:
typedef struct S { int buffer[4]; } S;
在C#下,如果像下面這個寫法,所得到的 buffer 實際上是在托管堆上,S本身只存儲了到buffer 的一個引用。
struct S { public int[] buffer = new int[3]; }
要想實現和C一樣在值類型內創建固定大小的內存,就必須用 fixed 關鍵字:
unsafe struct S { public fixed int buffer[3]; }
上面的 buffer 就是一個指針了。假設有 S 的實例s,當在托管堆上時,要使用 s.buffer 就必須先將 s fixed,如果不是在托管堆上,就不用fixed,參見代碼:
public unsafe class Example { S field = new S(); private bool example1() { // ERROR return field.buffer[2] == 0; } private bool example2() { // OK fixed (S* p = &field) { return (p->buffer[2] == 0); } } private S example1() { // OK S s = new S(); s.buffer[2] = 0; return s; } }
遺憾的是,上面fixed的用法(在值類型內創建固定大小的內存)只支持幾個基本類型:bool、byte、 char、 short、int、long、sbyte、ushort、uint、ulong、float 或 double。
如果想使用其它值類型,怎么辦呢?
通過反編譯我們看到這里的fixed實際是個語法糖:
下面,我們來模擬它,在值類型內部創建一個fixed不支持的類型的內存塊。
unsafe struct S2 { public Block block; [StructLayout(LayoutKind.Sequential, Size = 24)] public struct Block { public Rgb24 Val0; } } public unsafe class S2Example { S2 S = new S2(); private void example1() { fixed(S2* p = &S) { (&(p->block.Val0))[2].Blue = 25; } } }
(2)托管類型
通過對 fixed 的研究,發現了一個很有趣的事情。我以前一直以為,只要是值類型都可以用指針指:在托管堆里的fixed一下即可。今天發現不是這樣的,有一些值類型是無法使用指針指的。
什么類型呢?屬於托管類型(managed type)的值類型。
.net 里的類型可分為值類型和引用類型,也可以分為托管類型和非托管類型。這兩種說法有什么區別呢?我畫個圖來表示:
引用類型一定是托管類型,但是值類型不一定是非托管類型。只有非托管類型才能用 unsafe 指針操作,托管類型不能。
那么,屬於托管類型的值類型有哪些呢?
目前我只發現了兩種:
(1)泛型值類型!
(2)字段或字段的字段或字段的字段的字段(……)是托管類型!
例如:
public struct Size<T> where T : struct { public T Width; public T Height; public Size(T width, T height) { Width = width; Height = height; } public static Boolean operator ==(Size<T> lhs, Size<T> rhs) { return lhs.Equals(rhs); } public static Boolean operator !=(Size<T> lhs, Size<T> rhs) { return !lhs.Equals(rhs); } }
它是值類型,但是它是在運行期建立起來的類型,在編譯時它是不存在的!這樣的類型無法用指針操作。
public unsafe class SizeTExample { private void example1() { Size<Int32> size = new Size<Int32>(); //Error Size<Int32>* p = &size; p->Width = 200; } }
(3)Public的不public,fixed 的 不 fixed
有了上面的討論,下面,我們來構造一個奇怪的類型:
unsafe struct S3 { public Size<Int32> size; public fixed byte buffer[4]; }
這個類型有個字段size是泛型值類型 Size<Int32>,這就導致了S3本身是值類型,也是托管類型。是托管類型的話,就不能進行指針操作等unsafe操作。無法fixed,無法 sizeof等等。無法fixed的話,它標志為 fixed byte 的 buffer 就無法訪問。
public unsafe class S3Example { private void example1() { S3 S = new S3(); //Error fixed (Size<Int32>* p = &size) { } } }
這樣的話,我們就成功構造出一個有public字段但是無法訪問,有fixed內存區但是無法fixed的古怪類型。
當然,還有更簡單的做法:
unsafe struct S4 { public Object obj; public fixed byte buffer[4]; }