Chapter 1 - Item 2 : Prefer readonly to const

Effective C# (Covers C# 6.0), (includes Content Update Program): 50 Specific Ways to Improve Your C#, 3rd Edition By Bill Wagner 讀後心得

C# 提供了兩種不同型式的常數;分別為編譯階段常數與執行階段常數。作者再次使用了"prefer"而非"always",表示兩者各有其使用時機。

// 編譯階段常數
public const int Millennium = 2000;

// 執行階段常數
public static readonly int ThisYear = 2017;

兩者間有以下幾種不同:
           
1. const 可以在方法內部宣告,而 readonly 常數則不行。

    public void M1( )
    {
        // 編譯成功
        const int x = 10;

        // 編譯失敗
        static readonly int y = 10;
    }

2. const 編譯後的 IL Code 是取值,而 readonly 常數是取變數。

if( myDateTime.Year == Millennuim )

等同於

if( myDateTime.Year == 2000 ) // 取值

 

if( myDateTime.Year == ThisYear )

等同於

if( myDateTime.Year == ThisYear ) // 取變數


3. const 只能宣告為基本型別(e.g. int, float, decimal, double, string.),而 readonly 常數則無此限制。

    // 編譯失敗
    private const DateTime classCreation = new DateTime( 2000, 1, 1, 0, 0, 0 );

    // 編譯成功
    private static readonly DateTime classCreation = new DateTime( 2000, 1, 1, 0, 0, 0 );

4. 實務上 readonly 常數較 const 彈性。 
    
假設我們在組件中定義了以下類別:

    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 {i}" );

    輸出:
    Value is 5
    Value is 6
    ...
    Value is 9

若日後某次更新了組件且不重新編譯客戶端程式: 

    public class UsefulValues
    {
        public static readonly int StartValue = 105;
        public const int EndValue = 120;
    }

    預期輸出:
    Value is 105
    Value is 106
    ...
    Value is 119
    
實際上會得到無輸出的結果。原因承 2. 所提及,由於 const 經過客戶端程式編譯後 IL Code 保存的是實際的值(10);組件的 const 即使改變,因客戶端程式並沒有重新編譯,EndValue 不會變化(仍然為 10)。

5. const 執行效能比 readonly 常數好。

為了驗證此論點,準備測試程式碼。

    // 被參考的組件
    public class ConstReadonlyTest
    {
        public const int ConstInt = 1;
        public static readonly int ReadOnlyInt = 1;
    }

    // 客戶端測試程式碼
    public class ConstReadonlyTestClient
    {
        public int Result { get; set; }

        [Benchmark]
        public void TestConst( )
        {
            Result = ConstReadonlyTest.ConstInt;
        }

        [Benchmark]
        public void TestReadOnly( )
        {
            Result = ConstReadonlyTest.ReadOnlyInt;
        }
    }

    BenchmarkDotNet 輸出:
    
    // * Summary *
    
    BenchmarkDotNet=v0.10.1, OS=Microsoft Windows NT 6.1.7601 Service Pack 1
    Processor=Intel(R) Core(TM) i3-2100 CPU 3.10GHz, ProcessorCount=4
    Frequency=3118212 Hz, Resolution=320.6966 ns, Timer=TSC
      [Host]     : Clr 4.0.30319.42000, 64bit RyuJIT-v4.6.1590.0
      DefaultJob : Clr 4.0.30319.42000, 64bit RyuJIT-v4.6.1590.0
    
    Allocated=0 B  
    
           Method |      Mean  |    StdErr    |    StdDev |
       ------------- |-------------- |-------------- |-------------- |
       TestConst | 0.0144 ns | 0.0077 ns | 0.0298 ns |
TestReadOnly | 0.0344 ns | 0.0010 ns | 0.0037 ns |
    
    // * Diagnostic Output - MemoryDiagnoser *
    Note: the Gen 0/1/2 Measurements are per 1k Operations
    
    // ***** BenchmarkRunner: End *****
    
結果看來 const 執行效率比 readonly 常數些微優異;但標準差很大,表示實際運行效率是浮動的。

結論:
1. 除非需要確保常數在編譯期間就為有效(e.g. attribute parameters, switch case labels, enum definitions.),抑或確定在每一次的 release 之間,定義的常數皆不需改變;否則使用 readonly 常數會比 const 更加彈性。

2. 當程式以執行效能為優先考量時,使用 const 會比 readonly 常數來的好。