程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> .NET網頁編程 >> C# >> 關於C# >> Effective C#原則2:為你的常量選擇readonly而不是const

Effective C#原則2:為你的常量選擇readonly而不是const

編輯:關於C#

對於常量,C#裡有兩個不同的版本:運行時常量和編譯時常量。

因為 他們有不同的表現行為,所以當你使用不當時,將會損傷程序性能或者出現錯誤 。

兩害相權取其輕,當我們不得不選擇一個的時候,我們寧可選擇一個 運行慢一點但正確的那一個,而不是運行快一點但有錯誤的那個。基於這個理由 ,你應該選擇運行時常量而不是編譯時常量(譯注:這裡隱藏的說明了編譯時常 量效率更高,但可能會有錯誤)。

編譯時常量更快更直接,但在可維護性 上遠不及運行時常量。保留編譯時常量是為了滿足那些對性能要求克刻,且隨著 程序運行時間的過去,其值永遠不發生改變的常量使用的(譯注:這說明編譯時 常量是可以不被C#采用的,但考慮到性能問題,還是做了保留)。

你可以 用關鍵字readonly來聲明(declare)一個運行時常量,編譯時常量是用關鍵字 const聲明的。

//Compile time constant:
public cocnst int _Millennium = 2000;
//Runtime constant:
public static readonly int _ThisYear = 2007;//

(譯注:原文為2004)

編譯時常量與運行時常量不同之處表現在如何對他們的訪問上。

一個編譯時常量會被目標代碼中的值直接取代。下面的代碼:

if (myDateTime.Year == _Millennium)

會與下面寫的代碼編譯成完 全相同的IL代碼:

if(myDateTime.Year == 2000)

運行時常量的值是在運行時確定的。當你引用一個只讀常量時(read-only)IL會 為你引用一個運行時常量的變量,而不是直接使用該值。

當你任意的使 用其中一個常量時,這些區別就在一些限制上表現出來。編譯時常量只能是基本 類型(primitive types)(built-in integral and floating-poing types),枚舉 或者是字符串。這些就是你只能給運行時常量在初始化時賦值的類型。這些基本 類就是可以被編譯器在編譯IL代碼時直接用真實的值所取代的數據類型。下面的 代碼塊(construct)不能通過編譯。你不能用new運算符初始化一個編譯時常量, 即使這個數據類型是值類型。

//Does not complie, use readonly instead:

private const DateTime _classCreation = new DateTime (2000,1,1,0,0,0);

(譯注:DateTime是一個值類型數據,但上面的代碼 因為用了new運算符,編譯器無法在編譯確定具體的對象應該用什麼樣的實際值 來取代,所以無法通過編譯。)

編譯時常量僅限於數字和字符串。只讀變 量,也就是運行時常量,在構造函數(constructor)執行完成後它們是不以能被 修改的。但只讀變量是所有不同的,因為他們是在運行時才賦值的。當你使用運 行時常量時,你有更大的可伸縮性。有一點要注意的是,運行時常量可以是任何 類型的數據。而且你必須在構造函數裡對他們初始化,或者你可以用任何一個初 始化函數來完成。你可以添加一個DateTime結構的只讀變量(--運行時常量),但 你不能添加一個DateTime結構的(編譯時)常量。

你可以把每一個實例(的 常量)指定為只讀的,從而為每一個類的實例存放不同的值。與編譯時常量不同 的是,它只能是靜態的。

(譯注:簡單的講,運行時常量可以是一個類的 實例成員,也可以是一個類型的靜態成員,而編譯時常量只能是靜態成員,因此 類似:static const string m_name;的代碼是不能通過編譯的。)

只讀 數據最重要的區別是他們在運行時才確定值。當你使用只讀變量 時,IL會為你 產生一個對只讀變量引用,而不是直接產生數值。隨著時間的推移,這個區別在 (系統)維護上有深遠的潛在影響。

編譯時常量生成的IL代碼就跟直接使 用數值時生成的IL是一樣的,即使是在跨程序集時:一個程序集裡的編譯時常量 在另一個程序集會保留著同樣的值(譯注:這裡說的不是很清楚,看後面的這個 例子可能會更清楚一些)。

編譯時常量和運行時常量的賦值方法對運行時 的兼容性有所影響。

假設你已經在程序集Infrastructure中同時定義了 一個const和一個readonly變量:

public class UserfulValues {
public static readonly int StartValue = 5;
public const int EndValue = 10;
}

同時,在另一個程序集(譯注:這個 程序集認為是我們做測試的應用程序的程序集,下面所說的應用程序的程序集都 是指的這個程序集)中,你引用了這些值:

for(int i=UserfulValues.StartValue;i<UserfulValues.EndValue;i++){
Console.WriteLine("value is {0}",i);
}
如果你運 行這個簡單測試程序,你可以看到下面明顯的結果:

value is 5
value is 6
...
value is 9

過後,你又為程 序集Infrastructure發布了個新的版本,並做了如下的修改:

public class UserfulValues{
public static readonly int StartValue = 105;
public const int EndValue = 120;
}
你單獨的發布了程序集Infrastructure而沒有全部編譯你的程序,你希 望得到下面的:

value is 105
value is 106
...
value is 119
事實上,你什麼也得不到。上面的循環已經是用105開始 而用10來結束。C#編譯器(在編譯時)把常量用10來代替應用程序的程序集中的使 用,而不是用常量EndValue所存儲的值。而常量StartValue的值,它是被申明為 只讀的,它可以在運行時重新讀取該常量的值。因此,應用程序的程序集可以在 不用重新編譯的情況下使用新的數據,簡單的編譯一下Infrastructure程序集, 然後重新布署安裝一下,就足夠讓你的客戶可能使用這些新的數據了。更新的編 譯時常量應該看成是接口的變化。你必須重新編譯所有引用到編譯時常量的代碼 。更新的運行時常量則可以當成是實現的改變,這於在客戶端已經存在的二進制 代碼是兼容的。用MSIL解釋一下前面的那個循環裡發生了什麼:

IL_0000: ldsfld int32 Chapter1.UserfulValues::StartValue
IL_0005: stloc.0
IL_0006: br.s IL_001c
IL_0008: ldstr "value is {0}"
IL_000d: ldloc.0
IL_000e: box [mscrolib]System.Int32
IL_0013: call void [mscrolib]System.Console::WriteLine (string,object)
IL_0018: ldloc.0
IL_0019: ldc.i4.1
IL_001a: add
IL_001b: stloc.0
IL_001c: ldloc.0
IL_001d: ldc.i4.s 10
IL_001f: blt.s IL_0008

從MSIL 命令清單的最上面一行你可以看到,StartValue(的值)是動態載入的。

但是,在MSIL命令的最後,結束條件是把值10當成硬代碼(hard-coded)使用的。

另一方面,有些時候你也須要為某些值使用編譯時常量。例如:考慮一 個須要識別不同版本的續列化情形。用來標識一個特殊版本號的常量應該是一個 編譯時常量,它們決不會發生改變。而當前版本號則應該是一個運行時常量,在 不同的版本發布後會有所改變。

private const int VERSION_1_0 = 0x0100;
private const int VERSION_1_1 = 0x0101;
private const int VERSION_1_2 = 0x0102;
//major release;
private const int VERSION_2_0 = 0x0200;
//Chech for the current version:
private static readonly int CURRENT_VERSION = VERSION_2_0;

在每次存盤時,你用運行常量來保存當前版本號。

//Read fom persistent storage, check stored version against complie-time constant:
protected MyType(SerializationInfo info, StreamingContext cntxt){
int storedVersion = info.GetInt32 ("VERSION");
switch(storedVersion){
case VERSION_2_0:
readVersion2(info,cntxt);
break;
case VERSION_1_1:
readVersion1(info,cntxt);
break;
//etc.
}
}
  
//Write the current version:
[SecurityPermissionAttribute (SecurityAction.Demand,SerializationFormatter = true)]
void ISerializable.GetObjectData(SerializationInfo inf,StreamingContext cxt){
//use runtime constant for currnet version
inf.AddValue("VERSION",CURRENT_VERSION);
//
//write remaining delements...
}

最後一個有利的原 因而使我們要使用編譯時常量,就是它的性能。比起運行時常量,已知的編譯時 常量可以更直接有效的被訪問。然而,性能上的功效是甚微的,並且應該與可伸 縮性的降低進行一個權衡。Be sure to profile performace differences before giveing up the flexibility.

const的值必須在編譯時被確定, (它們可以是):屬性參數,枚舉定義,以及一小部份你認為應該定義一個值且該 值不能在不同的版本發布時發生改變的常量。

無論如何,寧願選擇伸縮 性更強的運行時常量。

============================

小結:第 一次翻譯。昨天花了半天時間,今天又花了幾個小時。終於完成了Item2的翻譯 。感覺很吃力。或者我的翻譯很糟糕,或者已經有更好的翻譯了,但不管怎樣, 這也算是我自己辛苦勞動的結果。希望對讀者有所幫助。本想在翻譯中添加一我 自己的看法,那樣的話對原文的改動可能比較大,所以沒做太多注解。我會繼續 努力翻譯完後面的Items,不管翻譯的怎樣,也不管是不是有其它的翻譯已經完 成了。這對自己也是一個學習過程。我會在後面的翻譯中一些我自己的看法,當 然盡可能的保留原文的樣子,實在不好辦的,我就會以讀書筆記的形式另外寫文 章。

另外,我也不知道這樣的文章能不能發在首頁,如果不行,以後的 就都發到其它區吧。

返回教程目錄

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved