- 什麼是靜态常量(Const)和動态常量(Readonly)
先解釋下什麼是靜态常量(Const)以及什麼是動态常量(Readonly)。
靜态常量(Const)是指編譯器在編譯時候會對常量進行解析,并将常量的值替換成初始化的那個值。
動态常量(Readonly)的值則是在運作的那一刻才獲得的,編譯器編譯期間将其标示為隻讀常量,而不用常量的值代替,這樣動态常量不必在聲明的時候就初始化,而可以延遲到構造函數中初始化。
靜态常量(Const)和動态常量(Readonly)之間的差別
靜态常量(Compile-time Constant) | 動态常量(Runtime Constant) | |
定義 | 聲明的同時要設定常量值。 | 聲明的時候可以不需要進行設定常量值,可以在類的構造函數中進行設定。 |
類型限制 | 隻能修飾基元類型,枚舉類型或者字元串類型。 | 沒有限制,可以用它定義任何類型的常量。 |
對于類對象而言 | 對于所有類的對象而言,常量的值是一樣的。 | 對于類的不同對象而言,常量的值可以是不一樣的。 |
記憶體消耗 | 無。 | 要配置設定記憶體,儲存常量實體。 |
綜述 | 性能要略高,無記憶體開銷,但是限制頗多,不靈活。 | 靈活,友善,但是性能略低,且有記憶體開銷。 |
- Const修飾的常量在聲明的時候必須初始化;Readonly修飾的常量則可以延遲到構造函數初始化 。
- Const常量既可以聲明在類中也可以在函數體内,但是Static Readonly常量隻能聲明在類中。Const是靜态常量,是以它本身就是Static的,是以不能手動再為Const增加一個Static修飾符。
- Const修飾的常量在編譯期間就被解析,即:經過編譯器編譯後,我們都在代碼中引用Const變量的地方會用Const變量所對應的實際值來代替; Readonly修飾的常量則延遲到運作的時候。
舉個例子來說明一下:
public static readonly int NumberA = NumberB * 10;
public static readonly int NumberB = 10;
public const int NumberC = NumberD*10;
public const int NumberD = 10;
static void Main(string[] args)
{
Console.WriteLine("NumberA is {0}, NumberB is {1}.", NumberA, NumberB);//NumberA is 0, NumberB is 10.
Console.WriteLine("NumberC is {0}, NumberD is {1}.", NumberC, NumberD);//NumberC is 100, NumberD is 10.
Console.ReadKey();
}
以上是文法方面的應用,那在實際的用法上,還是有些微妙的變化,通常不易發覺.
舉個例子來說明一下:
在程式集DoTestConst.dll 中有一個類MyClass,定義了一個公開的靜态變量Count
public static class MyClass
{
public const int Count = 10;
}
然後另外一個應用程式中引用DoTestConst.dll,并在代碼中作如下調用:
public static void Main(string[] args)
{
Console.WriteLine(DoTestConst.MyClass.Count);//輸出10
Console.ReadKey();
}
毫無疑問,非常簡單的代碼,直接輸出10。
接下來更新MyClass的Count的值為20,然後重新編譯DoTestConst.dll,并更新到應用程式的所在目錄中,注意不要編譯應用程式。那麼這時候的輸出結果按預期那麼想應該是20才對,但實際上還是10,為什麼呢?
這就是Const的特别之處,有多特别還是直接看生成的IL,檢視IL代碼(假設這時候Count的值為10)
IL_0000: nop
IL_0001: ldc.i4.s 10
IL_0003: call void [mscorlib]System.Console::WriteLine(int32)
紅色代碼很明顯的表明了,直接加載10,沒有通過任何類型的加載然後得到對應變量的,也就是說在運作時沒有去加載DoTestConst.dll,那麼是否意味着沒有DoTestConst.dll也可以運作呢?答案是肯定的,删除DoTestConst.dll也可以運作,是否很詭異呢?也就解釋了之前的實驗,為什麼更新Const變量的值之後沒有調用新的值,因為程式在運作的時候根本不會去加載DoTestConst.dll。那麼10這個值是從哪來的呢?實際上CLR對于Const變量做了特殊處理,是将Const的值直接嵌入在生成的IL代碼中,在執行的時候不會再去從dll加載。這也帶來了一個不容易發覺的Bug,是以在引用其他程式集的Const變量時,需考慮到版本更新問題,要解決這個問題就是把調用的應用程式再編譯一次就ok了。但實際程式部署更新時可能隻更新個别檔案,這時候就必須用Readonly關鍵字來解決這個問題。
接下來看Readonly的版本:
public static class MyClass
{
public static readonly int Count = 10;
}
調用方代碼不變,接着看生成的IL代碼:
IL_0000: nop
IL_0001: ldsfld int32 [DoTestConst]DoTestConst.MyClass::Count
IL_0006: call void [mscorlib]System.Console::WriteLine(int32)
很明顯加載代碼變了,一個很常見的ldsfld動作,請求了DoTestConst.MyClass的Count變量,是通過強制要求加載DoTestConst來實作的。是以這時候更新Count的值重新編譯之後,還是不編譯調用程式,然後再執行就會看到新的值。而這時候如果删除DoTestConst.dll那麼,會出現找不到dll之類的異常。這也充分說明了對于Readonly定義的變量是在運作時加載的。
動态常量(Readonly)被指派後不可以改變
ReadOnly 變量是運作時變量,它在運作時第一次指派後将不可以改變。其中“不可以改變”分為兩層意思:
- 對于值類型變量,值本身不可以改變(Readonly, 隻讀)
- 對于引用類型變量,引用本身(相當于指針)不可改變。
值類型變量,舉個例子說明一下:
public class Student
{
public readonly int Age;
public Student(int age)
{
this.Age = age;
}
}
Student的執行個體Age在構造函數中被指派以後就不可以改變,下面的代碼不會編譯通過:
Student student = new Student(20);
student.Age = 21; //錯誤資訊:無法對隻讀的字段指派(構造函數或變量初始化器中除外)
引用類型變量,舉個例子說明一下:
public class Student
{
public int Age; //注意這裡的Age是沒有readonly修飾符的
public Student(int age)
{
this.Age = age;
}
}
public class School
{
public readonly Student Student;
public School(Student student)
{
this.Student = student;
}
}
School執行個體的Student是一個引用類型的變量,指派後,變量不能再指向其他任何的Student執行個體,是以,下面的代碼将不會編譯通過:
School school = new School(new Student(10));
school.Student = new Student(20);//錯誤資訊:無法對隻讀的字段指派(構造函數或變量初始化器中除外)
引用本身不可以改變,但是引用說指向的執行個體的值是可以改變的。是以下面的代碼是可以編譯通過的:
School school = new School(new Student(10));
school.Student.Age = 20;
在構造方法中,我們可以多次對Readonly修飾的常量指派。舉個例子說明一下:
public class Student
{
public readonly int Age = 20;//注意:初始化器實際上是構造方法的一部分,它其實是一個文法糖
public Student(int age)
{
this.Age = age;
this.Age = 25;
this.Age = 30;
}
}
總結
Const和Readonly的最大差別(除文法外)
Const的變量是嵌入在IL代碼中,編譯時就加載好,不依賴外部dll(這也是為什麼不能在構造方法中指派)。Const在程式集更新時容易産生版本不一緻的情況。
Readonly的變量是在運作時加載,需請求加載dll,每次都擷取最新的值。Readonly指派引用類型以後,引用本身不可以改變,但是引用所指向的執行個體的值是可以改變的。在構造方法中,我們可以多次對Readonly指派。