程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> .NET網頁編程 >> C# >> C#基礎知識 >> C#庖丁解牛之const與readonly

C#庖丁解牛之const與readonly

編輯:C#基礎知識

一.const與readonly的爭議

      你一定寫過const,也一定用過readonly,但說起兩者的區別,並說出何時用const,何時用readonly,你是否能清晰有條理地說出個一二三?       const與readonly之所以有如此爭議,是因為彼此都存在"不可改變"這一特性,對於二者而言,我們需要關心的是,什麼時候開始不可變?什麼是不可改變的?這就引出了我們下面要討論的話題.  

二.什麼時候開始不可變?

      我們先拋出結論.       const在程序運行的任何時候都是不可變的,無論什麼時候開始,什麼時候結束,它的值是固化在代碼中的,我們稱之為編譯期常量;       readonly在某個具體實例第一次初始時指定它的值(出了構造函數後,對於這個實例而言,它就不能改變)或者是作為靜態成員在運行時加載它的值,我們稱之為運行時常量.         我們先談const:       1.const由於其值從不變化,我們稱之為常量,常量總是靜態的,因此const是天然static的,我們不能再用static修飾const.如下圖所示:                 正確的定義應該是const float PI=3.14159F;              2.const既然是靜態的,因此它屬於整個類,而不屬於某個實例,我們可以直接通過類名來調用,如下所示:                   3.由於常量的值是直接嵌入代碼的,因此在運行時不需要為常量分配任何內存,也不能獲取常量的地址,也不能以傳引用的方式傳遞常量.       什麼叫直接嵌入代碼?即:在編譯的過程中,編譯器首先將常量值保存到程序集元數據中,在引用常量的地方,編譯器將提取這個常量值並嵌入生成的IL代碼中,這也就是為什麼常量不需要分配任何內存的原因.       我們來驗證一下上面的結論,首先我們定義一個常量:
1 public class MathHelper
2 {
3     public const float PI= 3.14159F;
4 }
調用:
1 static void Main(string[] args)
2 {
3      float pi= MathHelper.PI;
4 }
我們查看生成的IL代碼,如下:       標紅的那一行,即是將PI的值直接嵌入代碼之中.理解這一點不難,但是這種寫法會帶來潛在的問題:const不能支持很好支持程序集的跨版本.為了說明這個問題,我們需要對我們的代碼進行如下的改造:               第一步:我們將MathHelper單獨放到一個項目中,並生成一個單獨的程序集(程序集版本:1.0).       第二步:我們編譯應用程序為exe文件,采用上面的方法來查看IL代碼,我們看到const的值仍然嵌入了代碼之中.       第三步:我們修改PI的值為3.14,重新編譯MathHelper,生成一個單獨的程序集(程序集版本:2.0).       第四步:因為我們只是重新編譯了MathHelper所在的程序集,沒有重新編譯exe文件,我們查看exe的IL代碼,發現嵌入代碼的值仍為3.14159.         也就是在跨程序集的引用中,當改變了常量時,除非重新編譯所有引用了常量的程序集,否則改變不能體現在引用當中.       雖然有了這樣的bug隱患,也不是說const就一無是處,由於const在程序中不占用內存,所以它的速度非常之快,於是我們在設計程序時,如果一個值從不變化,我們可以將其定義常量來尋求速度上的效率上的提升.比如我們程序需要國際化的時候,簡體中文的編碼為2052,美國英語的編碼為1033,我們可以將它們定義為常量.      另外,我們說過常量是沒有地址的,因而不能以傳引用的方式傳遞常量,即下面的寫法是錯誤的:   說完const,我們來說readonly 1.readonly是實例的,因此通過類名是不可直接訪問readonly變量的 定義:
1  public class MathHelper
2 {
3       public readonly float PI;
4 }
訪問:   2.readonly出了構造函數,對於這個實例而言就不可改變,因此下面的寫法也是錯誤的      既然,我們強調"出了構造函數",那是不是意味著,我們在構建函數內部,可以一次或多次改變它的值?為了驗證我們的猜想,我們對MathHelper改造如下:
1 public class MathHelper
2 {
3       public MathHelper()
4       {
5             this.PI = 3.15F;
6             this.PI = 3.14F;
7        }
8       public readonly float PI;
9 }
調用代碼:
1 static void Main(string[] args)
2 {
3       MathHelper m = new MathHelper();
4        Console.WriteLine(m.PI);
5 }
輸出結果: 從以上的結果,我們可以看出,在構造函數中可以對readonly變量多次賦值,但一旦出了構建函數則是只讀的.   3.有了第2點的支撐,下面我們可以驗證readonly是實例的(不可變的第一種情況)這一結論,我們現在來驗證這個結論.    我們改造MathHelper如下:    
1 public class MathHelper
2 {
3    public MathHelper(float pi)
4    {
5       this.PI = pi;
6    }
7    public readonly float PI;
8 }
調用如下:
 1 static void Main(string[] args)
 2 {
 3      MathHelper m1 = new MathHelper(3.14F);
 4      Console.WriteLine(m1.PI);
 5 
 6      MathHelper m2 = new MathHelper(3.15F);
 7      Console.WriteLine(m2.PI);
 8 
 9      Console.Read();
10 }
輸出結果: 我們實例化了兩個不同的MathHelper,給PI賦予了不同的值,PI的值屬於不同的實例,這也就驗證了我們的結論.   4.readonly的內聯寫法 那有的童鞋說了,我還用過這樣的寫法,這說明了readonly可以在構建方法外賦值.如下所示:
1 public class MathHelper
2 {
3     public readonly float PI=3.15F;
4 }
     其實,這是一種內聯寫法,是C#的一種語法糖,只是一種語法上的簡化,實際它們也是在構造方法中進行初始化的.C#允許使用這種簡化的內聯初始化語法來初始化類的常量、read/write字段和readonly字段。   5.readonly賦值的第二種情況:如果我用static修飾readonly會發生什麼?      前面講const時,我們說過const是靜態的,這種靜態不可以顯式指定,因此在const前加static會導致編譯器編譯失敗.那我們把static修飾readonly會發生什麼樣的結果?      首先,我們確定,靜態的是屬於類的,此時的readonly我們不能通過構造函數來指定.
1 public class MathHelper
2 {
3     public static readonly float PI=3.14F;
4 }
調用:
1 static void Main(string[] args)
2 {
3      Console.WriteLine(MathHelper.PI);
4      Console.Read();
5 }
結果與我們預期的一致:   但我們的疑問不會就此打住:既然static readonly也是屬於類的,而且它的值也不能通過構造函數來賦值,那麼編譯器會像const一樣把它的值寫入IL代碼中麼?我們反編譯其IL代碼如下:         可以看到,這裡並沒有將值嵌入到代碼當中.       因此,我們可以大膽地預測,這種寫法不會造成支持程序集的跨版本問題.這裡就不寫驗證的過程,留給各位讀者朋友自行探索.       既然沒有嵌入代碼中,那麼在程序運行的時候,它的值是在什麼時候分配內存的呢?       我們引用《CLR via C#(第4版)》中的一句話來說明這個問題:對於靜態字段,其動態內存是在類中分配的,而類是在類型加載到AppDomain時創建的,那麼,什麼時候將類型加載到AppDomain中呢?答案是:通常是在引用了該類型的任何方法首次進行JIT編譯的時候.而對於前面第3點中的實例字段來說,其動態內存是在構造類型的實例時分配的.        

三.什麼是不可變的?

      前面我們花了大量的篇幅說明const與readonly的變量什麼時候才開始不可變,有的從一開始就不可變,有的是第一次加載的時候不可變,有的是出了構造函數後不可變,但是我們有一個十分關鍵的問題沒有弄清楚:什麼東西不可變?也許童鞋們很疑惑,值不可變呗!這話不完全對.       要想理解這個問題,我們需要明白const與readonly修飾的對象,也就是我們不變的內容.       const可以修飾基元類型:Boolean、Char、Byte、SByte、Int16、UInt16、Int32、UInt32、Int64、UInt64、Single、Double、Decimal和String。也可以修改類class,但要把值設置為null。不可以修飾struct,因為struct是值類型,不可以為null.       對於基元類型來說,值是存儲在棧上的,因此我們可以認為不變的是值本身,這裡string是一個特殊的引用類型,這裡它也存在值類型的特征,因此也可以認為它不變的是值本身.       對於readonly而言,readonly可以修飾任何類型.對於基元類型而言,我們可以認為它與const無異,但是對於引用類型,我們需要謹慎對待,不可想當然,下面我們通過實驗來得出結論:
1 public class Alphabet
2 {
3         public static readonly Char[] Letters = new Char[] {'A','B','C','D','E','F' };
4 }
調用:
 1 static void Main(string[] args)
 2 {
 3      Alphabet.Letters[0] = 'a';
 4      Alphabet.Letters[0] = 'b';
 5      Alphabet.Letters[0] = 'c';
 6      Alphabet.Letters[0] = 'd';
 7      Alphabet.Letters[0] = 'e';
 8      Alphabet.Letters[0] = 'f';
 9      Console.WriteLine(Alphabet.Letters.Length);
10      Console.Read();
11  }
可賦值!!! 輸出結果如下:   現在,我們給它賦予一個新的對象: 不可賦值!!! 看到這裡你是不是心裡有答案了?   結論:對於引用類型而言,我們可以賦值,而不可以賦予一個新的對象,因為這裡不變的是引用,而不是引用的對象.  

四:總結

     到此,我們的const與readonly的庖丁解牛式的解析也就告一段落了,說了這麼多,我們其實也就是想說明以下2點:     1.const任何時候都不變,比readonly快,但不能解決跨版本程序集問題,readonly靜態時在第一次JIT編譯後不變,實例時在出了實例的構造函數後不可變.     2.const修飾基元類型,不變的是值;readonly修飾值類型時,其值不變,修改引用類型時,其引用不變.     以上.   

參考文檔:

    《CLR via C#(第4版)》     《Effice C#:改進C#代碼的50個行之有效的辦法》     《編寫高質量代碼:改善C#程序的157個建議》
  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved