# 原則2:偏愛 readonly 而不是 const
**By D.S.Qiu**
尊重他人的勞動,**支持原創,轉載請注明[出處](/blog/1976703):[http://dsqiu.iteye.com](http://dsqiu.iteye.com)**
C# 有兩種常量:編譯時常量和運行時常量。它們有不同的行為,不當使用會花費性能或出現錯誤。如果不得不選擇其中一個,寧可是一個慢一點但正確的程序,而不是一個快速但會出錯的程序。出于這個考慮,你應該更偏向于運行時常量而不是編譯時常量。編譯時常量會比編譯時常量稍微快點,但更不靈活。只有當性能是一個致命因素而且要求常量不隨版本發生改變時才會保留編譯時常量。
你可以使用 readonly 關鍵字聲明運行時常量。使用 const 關鍵字聲明編譯器常量:
```
// Compile time constant:
public const int Millennium = 2000;
// Runtime constant:
public static readonly int ThisYear = 2004;
```
上面的代碼可以出現類或結構體的作用域(scope)中。編譯時常量還可以在方法體中聲明。運行時常量不能在方法體重聲明。
編譯時常量和運行時常量訪問方式不同導致不同的行為。在目標代碼中編譯時常量會被替換成常量值。比如下面代碼:
```
// Compile time constant:
public const int Millennium = 2000;
// Runtime constant:
public static readonly int ThisYear = 2004;
```
會和下面寫法的編譯的 IL 代碼是一樣的:
```
if (myDateTime.Year == 2000)
```
運行時常量的值是在運行時得到的。當你引用一個只讀(read-only)常量, IL 會引用一個 readonly 變量而不是直接使用值。
使用編譯時常量和運行時常量還有不同的限制。編譯時常量只能在基本類型(內建整數和浮點數類型),枚舉類型,或字符串。編譯時常量要求類能用有意義的常量賦值初始化。而只有基本類型才能在 IL 代碼中使用常量(literal values)來替換。不能使用使用 new 操作法初始化編譯時常量,即使它是一個值類型:
```
// Does not compile, use readonly instead:
private const DateTime classCreation = new
DateTime(2000, 1, 1, 0, 0, 0);
```
編譯時常量只能使用與數字和字符串。只讀(Read-only)變量也是常量,即不能在構造函數完成之后再修改。但只讀變量是在運行時賦值。這會比編譯時常量更靈活。首先,運行時常量可以是任何類型。你必須在構造函數或者直接初始化。你可以讓 DateTime 結構體變為 readonly 值;但不能使用 const 創建 DateTime 值。
readonly 值可以是實例類型(instance)常量,讓一個類的實例存儲不同值。而編譯時常量則是被定義為 static 常量的。
只讀變量最重要的不同在于運行時才確定值。當你引用一個只讀變量, IL 會為你產生一個指向只讀變量的引用,而不是值。這種差異將對維護上產生深遠的影響。編譯時常量產生的 IL 代碼就跟直接使用數值變量時一樣的,即使是跨程序集:一個程序集的常量在另一個程序集還是被替換為數值。
編譯時常量和運行時常量的賦值方式會影響運行時的兼容性。假設你在程序集 Infrastructure 中同時定義了 const 和 readonly 域:
```
public class UsefulValues
{
public static readonly int StartValue = 5;
public const int EndValue = 10;
}
```
在另外一個程序集,你引用這兩個值:
```
for (int i = UsefulValues.StartValue;
i < UsefulValues.EndValue; i++)
Console.WriteLine("value is {0}", i);
```
如果你運行這個簡單的測試程序,很明顯你會得到下面的輸出:
```
Value is 5
Value is 6
...
Value is 9
```
一段時間后,你發布新版本的 Infrastructure 程序集并作下面的改動:
```
public class UsefulValues
{
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替換應用程序集的 const 變量而不是指向存儲 EndValue 的引用。 StartValue 的情況趨勢截然不同。因為它被聲明為 readonly :在運行時確定值。因此,應用程序集能不用重新編譯就能充分利用新值;只要很簡單地按照更新版本的 Infrastructure 程序集就可以改變使用該變量的值。更新 public const 變量的值應該當做接口的變化。更新只讀常量的值只是實現的改變,兼容客戶端的二進制代碼。
另一方法,有時候某些值的確需要是編譯時常量。例如:考慮使用編譯時常量標記對象的序列化版本(查看原則27)。標記特定版本號的持久化值要使用編譯時常量,它們決不會發生改變。但當前的版本號應該是一個運行時常量,隨著版本不同而改變。
```
private const int Version1_0 = 0x0100;
private const int Version1_1 = 0x0101;
private const int Version1_2 = 0x0102;
// major release:
private const int Version2_0 = 0x0200;
// check for the current version:
private static readonly int CurrentVersion =
Version2_0;
```
你會使用運行時常量存儲每個文件的當前版本號:
```
// Read from persistent storage, check
// stored version against compile-time constant:
protected MyType(SerializationInfo info,
StreamingContext cntxt)
{
int storedVersion = info.GetInt32("VERSION");
switch (storedVersion)
{
case Version2_0:
readVersion2(info, cntxt);
break;
case Version1_1:
readVersion1Dot1(info, cntxt);
break;
// etc.
}
}
```
```
// Write the current version:
[SecurityPermissionAttribute(SecurityAction.Demand,
SerializationFormatter = true)]
void ISerializable.GetObjectData(SerializationInfo inf,
StreamingContext cxt)
{
// use runtime constant for current version:
inf.AddValue("VERSION", CurrentVersion);
// write remaining elements...
}
```
相比 readonly ,使用 const 最后的一個優勢就是性能:已知的常量值會比使用變量訪問的 readonly 變量產生稍微高效的代碼。然而,性能上甚微的收效和靈活性的減小應該做一個很好的權衡。放棄靈活性之前一定要剖析性能差異。可選參數的默認值會在調用時會像編譯時變量(聲明為 const 的變量)一樣被替換成默認值。和使用 readonly 和 const 變量一樣,你要非常認真對待可選參數值的不同。(查看原則10。)
當你使用命名(named)參數和可選(optial)參數時,你會遇到和使用運行時常量和編譯時常量一樣的權衡。
當在編譯時期必須要獲得變量的值時必須使用 const :特性(attribute)參數和枚舉定義,以及當你定義一個不隨版本的變化而變化的值得罕見的時候。無論如何,更偏愛于只讀常量的更強的靈活性。
小結:
第二節字數相對少些,所以今天雖然頸椎有點不舒服(千萬不要有事呀,我還沒有瘋夠),根據以前的節奏(時間還早),還可以干點別的,或者躲進被窩理順事情,每天給自己思考的事件太少了,沒有思考,積淀就會來的慢,這個跟前面強調的不同哈。
歡迎各種不爽,各種噴,寫這個純屬個人愛好,秉持”分享“之德!
有關本書的其他章節翻譯請[點擊查看](/category/297763),轉載請注明出處,尊重原創!
如果您對D.S.Qiu有任何建議或意見可以在文章后面評論,或者發郵件(gd.s.qiu@gmail.com)交流,您的鼓勵和支持是我前進的動力,希望能有更多更好的分享。
轉載請在**文首**注明出處:[http://dsqiu.iteye.com/blog/1976703](/blog/1976703)
更多精彩請關注D.S.Qiu的 博客 和微博(ID:靜水逐風)
- 第一章 C# 語言習慣
- 原則1:使用 屬性(Poperty)代替可直接訪問的數據成員(Data Member)
- 原則2:偏愛 readonly 而不是 const
- 原則3:選擇 is 或 as 而不是強制類型轉換
- 原則4:使用條件特性(conditional attribute)代替 #if
- 原則5:總是提供 ToString()
- 原則6:理解幾個不同相等概念的關系
- 原則7:明白 GetHashCode() 的陷阱
- 原則8:優先考慮查詢語法(query syntax)而不是循環結構
- 原則9:在你的 API 中避免轉換操作
- 原則10:使用默認參數減少函數的重載
- 原則11:理解小函數的魅力
- 第二章 .NET 資源管理
- 原則12:選擇變量初始化語法(initializer)而不是賦值語句
- 原則13:使用恰當的方式對靜態成員進行初始化
- 原則14:減少重復的初始化邏輯
- 原則15:使用 using 和 try/finally 清理資源
- 原則16:避免創建不需要的對象
- 原則17:實現標準的 Dispose 模式
- 原則17:實現標準的 Dispose 模式
- 原則18:值類型和引用類型的區別
- 原則19:確保0是值類型的一個有效狀態
- 原則20:更傾向于使用不可變原子值類型
- 第三章 用 C# 表達設計
- 原則21:限制你的類型的可見性
- 原則22:選擇定義并實現接口,而不是基類
- 原則23:理解接口方法和虛函數的區別
- 原則24:使用委托來表達回調
- 原則25:實現通知的事件模式
- 原則26:避免返回類的內部對象的引用
- 原則27:總是使你的類型可序列化
- 原則28:創建大粒度的網絡服務 APIs
- 原則29:讓接口支持協變和逆變
- 第四章 和框架一起工作
- 原則30:選擇重載而不是事件處理器
- 原則31:用 IComparable&lt;T&gt; 和 IComparer&lt;T&gt; 實現排序關系
- 原則32:避免 ICloneable
- 原則33:只有基類更新處理才使用 new 修飾符
- 原則34:避免定義在基類的方法的重寫
- 原則35:理解 PLINQ 并行算法的實現
- 原則36:理解 I/O 受限制(Bound)操作 PLINQ 的使用
- 原則37:構造并行算法的異常考量
- 第五章 雜項討論
- 原則38:理解動態(Dynamic)的利與弊
- 原則39:使用動態對泛型類型參數的運行時類型的利用
- 原則40:使用動態接收匿名類型參數