本文將探索c# readonly關鍵字在編譯以及運行時的一些關系,通過討論類中的值類型(即結構)字段的可修改性入手。
我們先編寫一個極其簡單的結構類型:
public struct StEntity { public int Val { get { _val++; return _val; } } private int _val; }
它只有一個int類型字段,以及訪問該字段的屬性,該屬性將在訪問時,將其值修改(+1),並返回。
隨后我們編寫一個具備該類型的一個字段及隨同的一個屬性的簡單引用類型。
public class StEntityWrapper { private readonly StEntity _stEntity; public int Val { get => _stEntity.Val; } }
最后,我們使用如下的控制台代碼以使用該引用類型的這個Val屬性(程序入口的主方法簽名被省略):
var wrapper = new StEntityWrapper(); var s1 = wrapper.Val; Console.WriteLine($"{nameof(s1)}:{s1}"); var s2 = wrapper.Val; Console.WriteLine($"{nameof(s2)}:{s2}");
結果輸出如下:
s1:1
s2:2
可以看到兩次訪問的值是不一樣的,說明在兩次操作中,我們在StEntityWrapper的Val屬性中所使用的StEntity是同一個實例,現在,我們為StEntity類型的字段_val加上readonly修飾,StEntityWrapper的代碼將被修改成如下的樣子:
public class StEntityWrapper { private readonly StEntity _stEntity; public int Val { get => _stEntity.Val; } }
再次運行程序,結果輸出如下:
s1:1
s2:1
看到兩次訪問的值是相同的,說明在兩次操作中,但是StEntityWrapper->Val的屬性代碼是一致的,結果卻和前者有所區別,編譯器和CLR究竟做了什么樣的PY交易,使得StEntityWrapper性♂情Dark♂變.
通過c#代碼無論是編譯前還是編譯后,兩個StEntityWrapper實現的Val屬性是一致的, 只能調查一下幕后煮屎者------MSIL,我們使用反編譯工具由淺探♂討一下:
這是未使用readonly的StEntityWrapper的MSIL代碼:
// Token: 0x17000002 RID: 2 .property instance int32 Val() { // Token: 0x06000002 RID: 2 RVA: 0x00002058 File Offset: 0x00000258 .get instance int32 FrameworkDemo.StEntityWrapper::get_Val() } // Token: 0x06000002 RID: 2 RVA: 0x00002058 File Offset: 0x00000258 .method public hidebysig specialname instance int32 get_Val () cil managed { // Header Size: 12 bytes // Code Size: 12 (0xC) bytes // LocalVarSig Token: 0x11000001 RID: 1 .maxstack 1 /* 0x00000264 02 */ IL_0000: ldarg.0 /* 0x00000265 7C01000004 */ IL_0001: ldflda valuetype FrameworkDemo.StEntity FrameworkDemo.StEntityWrapper::_stEntity /* 0x0000026A 2804000006 */ IL_0006: call instance int32 FrameworkDemo.StEntity::get_Val() /* 0x0000026F 2A */ IL_000B: ret } // end of method StEntityWrapper::get_Val
比♂跤一下加入了readonly修shit♂的MSIL屬性代碼:
.property instance int32 Val() { // Token: 0x06000001 RID: 1 RVA: 0x00002050 File Offset: 0x00000250 .get instance int32 FrameworkDemo.StEntityWrapper::get_Val() } // Token: 0x06000001 RID: 1 RVA: 0x00002050 File Offset: 0x00000250 .method public hidebysig specialname instance int32 get_Val () cil managed { // Header Size: 12 bytes // Code Size: 15 (0xF) bytes // LocalVarSig Token: 0x11000001 RID: 1 .maxstack 1 .locals init ( [0] valuetype FrameworkDemo.StEntity ) /* (14,20)-(14,33) E:\CilDemoSolution\FrameworkDemo\StEntityWrapper.cs */ /* 0x0000025C 02 */ IL_0000: ldarg.0 /* 0x0000025D 7B01000004 */ IL_0001: ldfld valuetype FrameworkDemo.StEntity FrameworkDemo.StEntityWrapper::_stEntity /* 0x00000262 0A */ IL_0006: stloc.0 /* 0x00000263 1200 */ IL_0007: ldloca.s V_0 /* 0x00000265 2803000006 */ IL_0009: call instance int32 FrameworkDemo.StEntity::get_Val() /* 0x0000026A 2A */ IL_000E: ret } // end of method StEntityWrapper::get_Val
可以看到,兩者的搞基代碼雖然是一致的,但是生成的MSIL代碼確實是雲泥之別,未使用readonly字段修飾的屬性方法直接將字段的地址通過ldflda載入到棧中,並調用其Val方法,而具備readonly修shit♂符的屬性方法使用了一個局部變量,江字段的內存使用ldfld尻貝到這個局部變量的內存中,並使用局部變量的地址調用Val方法,為什么編譯器在這里會出現這種差異呢?
一個原因可能是:被標記為readonly的字段內存塊不能在運行時通過訪問字段的方式修改其內容,引用類型在非空時,字段中存儲的是對應實例的地址,而值類型則存儲的是其整個內存塊,那么問題來♂了,這種行為約束發生在編譯時,在運行時,它是否仍然具備這種約束並拋出一個運行時異常呢?帶着學♂習的精♂神,我們直接修改了附加了readonly的屬性中msil的實現與其未附加readonly修飾的版本一致,並運行測試代碼,它的結果如下:
s1:1
s2:2
:):):):):):):):):):):):):):):):):):):):):):):):):):):):):):):):):):):):):):):):):):):):):):):):):):):):):):):):)
兩次結果發生了變化,由此我們可以下腚論,readonly修飾符的約束將只在編譯時發生,並影響到編譯結果,而運行時,在對readonly修飾的字段進行修改時,CLR並不會檢查其可修改性。
That is ♂ it.
