前言:從上周開始看<<CLR via C#>>第三版,剛剛看完了第四和第七兩章,這兩章都算常識,但是有很多基礎知識和術語理解不是很深入,所以看得有點慢,計劃每兩周寫點心得,是以成文。
1、應用情境重現
一個簡單的應用程式解決方案,如下:
其中,類庫CSharpLib裡定義一個簡單的類SomeType如下:
namespace CSharpLib{ public class SomeType { public const int ConstField = 50; public static readonly int ReadonlyField = 50; }}
在控制台應用程式ConsoleApp中,引用類庫CSharpLib,然後寫下如下代碼:
using System;namespace ConsoleApp{ using CSharpLib; class Program { static void Main(string[] args) { Console.WriteLine("Const field is {0}.", SomeType.ConstField); Console.WriteLine("Readonly field is {0}.", SomeType.ReadonlyField); Console.ReadKey(); } }}
這樣這個控制台應用程式的輸出就都是50,這個結果應該是每個開發人員都預期的,沒有任何可疑之處。
當我們把類庫CSharpLib中的常量都改變時:
namespace CSharpLib{ public class SomeType { public const int ConstField = 55; public static readonly int ReadonlyField = 5555; }}
也就是說,我們預期的目標輸出是55和5555。當改好類庫,開發人員發現控制台應用程式沒做出任何修改,所以只將類庫重建,然後將.dll檔案拷到控制台應用程式bin目錄下(做過asp.net開發的應該都知道,當某個類庫修改時,我們通常都直接拷貝修改後的類庫.dll到網站下的bin目錄,除了習慣性偷懶,版本控制也是一個問題),然後雙擊運行ConsoleApp.exe檔案,我們驚訝地發現當前的輸出不是55和5555,而是50和5555,也就是說,const欄位修改後沒有達到預期目的。
2、注意點總結
為什麼會發生上面的現象呢?從c#的常量的本質說起。
(1)、什麼是常量(書中原話摘錄)
“常量(constant)是一個特殊的符號,它是一個從不變化的值。定義常量符號時,它的值必須能在編譯時間確定。確定之後,編譯器將常量的值儲存到程式集的中繼資料中。常量總是被視為靜態成員,而不是執行個體成員。代碼引用一個常量符號時,編譯器會在定義常量的程式集的中繼資料中尋找該符號,提取常量的值,並將值嵌入產生的IL代碼中。由於常量的值直接內嵌程式碼,所以在運行時,不需要為常量分配記憶體。除此之外,不能擷取常量的地址,也不能以傳引用的方式傳遞常量。這些限制同時意味著,常量沒有很好的跨程式集版本控制特性。“
(2)、靜態常量和動態常量
簡單來說,用const修飾的就是靜態常量(compile-time constants),而且它是隱式靜態。動態常量(runtime constants)的值是在運行時獲得的,IL中將其標為唯讀常量,而不是用常量的值代替。
下面對二者的特點進行簡單比較:
|
靜態常量 |
動態常量 |
記憶體配置 |
無 |
有 |
可使用類型 |
較少的C#基元類型,如String,Int32等等 |
任意類型 |
初始化賦值 |
聲明變數時同時賦值 |
可在建構函式中賦值 |
何時發揮作用 |
編譯時間進行替換,可產生中繼資料 |
相當於類中的資料成員 |
通過上述分析,我們可以理解1中所產生的輸出不一致現象,是因為const欄位ConstField已經嵌入到控制台應用程式的IL代碼中(擷取ConstField欄位並不從類庫的dll檔案中載入),從IL中我們可以看得很清楚:
.method private hidebysig static void Main(string[] args) cil managed{ .entrypoint // 代碼大小 47 (0x2f) .maxstack 8 IL_0000: nop IL_0001: ldstr "Const field is {0}." IL_0006: ldc.i4.s 50 //const欄位沒有變化 IL_0008: box [mscorlib]System.Int32 IL_000d: call void [mscorlib]System.Console::WriteLine(string, object) IL_0012: nop IL_0013: ldstr "Readonly field is {0}." IL_0018: ldsfld int32 [CSharpLib]CSharpLib.SomeType::ReadonlyField IL_001d: box [mscorlib]System.Int32 IL_0022: call void [mscorlib]System.Console::WriteLine(string, object) IL_0027: nop IL_0028: call valuetype [mscorlib]System.ConsoleKeyInfo [mscorlib]System.Console::ReadKey() IL_002d: pop IL_002e: ret} // end of method Program::Main
而不是像static readonly欄位一樣從記憶體載入類庫程式集。如果應用程式需擷取新值,也必須重新編譯,或者使用readonly欄位。
3、類比問題
我記得以前看過的一篇博文,是討論枚舉使用中輸出不一致情況的,google了一下。您可以參考這一篇小心枚舉陷阱進行比較,如果您對IL有瞭解,可以簡單從IL著手分析對比,更能加深理解。
總結:const和readonly在編程實踐中經常用到,理解它們的基本原理,對於開發和維護都是非常有必要的。在類庫開發定義常量的時候,個人偏向於使用static readonly組合關鍵字。