常量是值從不變化的符號,在編譯以前值就必須肯定。編譯後,常量值會保存到程序集元數據中。因此,常量必須是編譯器識別的基元類型的常量,如:Boolean,Char,Byte,SByte,...,...,...,UInt64,Single,Double,Decimal,String。另外,C#是能夠定義非基元類型的常量的,前提是值必須爲null。c#
public sealed class SomeType { public const SomeType Empty=null; }
常量成員將建立元數據,它是直接嵌入在代碼內部,運行時不須要額外分配內存。安全
常量被視爲靜態成員,而不是實例成員。ide
不能獲取常量的地址函數
不能以引用的方式傳遞常量性能
參考上面的特性,若是跨程序引用,嘗試改變常量初始值,不只dll須要從新編譯,引用者也須要編譯spa
字段是一種數據成員,它能夠是值類型的實例也能夠是引用類型的引用。線程
CLR支持類型字段和實例字段,什麼是類型字段?它其實就是咱們熟悉的靜態字段,實例字段就是非靜態字段。3d
類型對象(靜態對象)是在類型加載到一個AppDomain時建立的,而所需內存也是在內型對象中分配的。指針
接着上面的問題,那麼,何時將類型加載到AppDomain中內?當第一次對引用到該類型的方法進行JIT編譯時,code
實例字段的內存,是在構造容納字段的類型進行實例構造時分配的。
字段存儲在動態內存中,它不像常量,因此只能在程序運行時,纔可以獲取到它的值。字段能夠是任何類型,不像常量有類型上的限制。
Static | static | 指定字段爲類型的一部分,而不是對象的一部分 |
---|---|---|
Instance | 默認 | 指定字段與實例關聯,而不是和類自己關聯 |
InitOly | readonly | 只能在構造器方法中進行值的寫入,不然只讀 |
Volatile | volatile | 表示,編譯器和CLR以及硬件,不會對這種字段標識的代碼執行「線程不安全的措施」,只有CLR中的基元類型能使用這個修飾符。 |
一般,字段都是read/write,便可讀可寫的,這也意味着,字段的值會隨着運行可能發生值得變化。而當你把字段標記爲readonly,那麼你就只能在構造函數中,對它進行賦值,編譯器是不會容許你在構造器(構造函數)覺得的任何方法寫入值,或變動值。
固然,C#提供了一種內聯初始化的語法糖來進行readonly值的初始化,這種語法也能夠對常量和其餘形式的字段進行賦值。
public readonly int =250;
固然,使用內聯語法,而不是在構造器中構造,濫用的話可能會有一些性能問題(代碼膨脹等)。
構造器是將類型的實例初始化到良好狀態的特殊方法。在「方法定義元數據表」中始終叫.ctor(constructor的簡稱)。
首先爲實例的數據字段分配內存空間,而後是爲初始化對象的附加字段(沒錯,就是咱們常常會提到的同步塊索引和類型對象指針)分配內存,而後最後開闢一個空間來調用實例構造函數進行對象的初始化。
在調用構造器以前,爲對象分配的內存老是先被歸零,爲了保證那些被構造器顯示重寫的字段都得到0或者null的值。
實例構造器永遠不能被繼承,類必須執行本身的構造函數。若是沒有,系統默認會構造一個無參的。
因此,實例構造器不能用new ,override,sealed和abstract修飾
若是類的修飾符爲abstract,那麼構造器可訪問性默認爲protected,不然默認爲public。
若是基類沒有提供無參構造函數(意味着顯示的實現了有參的構造函數),那麼派生類必須顯示調用一個基類的構造器(及爲了保證參數一致),不然編譯報錯。
static(sealed和abstract)修飾的類,編譯器不會爲它生成默認的構造函數
一般狀況下,不管如何實例化派生類,基類的構造函數必定會被調用,因此object的構造函數必定會被先調用,可是實時上它什麼也不會幹。
極少數狀況下,對象實例不會調用構造函數。如,Object的MemberwiseClone方法,它是用來分配內存,初始化對象的附加字段的,而後將源對象的字節數據複製到新對象中。
Notice:不要在構造器中調用虛方法。由於,假如被實例化的類型重寫了虛方法,就會執行派生類型中的實現,但這個時候,倒是沒有初始化的,因此,容易致使沒法預測的行爲。
內聯語法(在字段一節提到過)方式實現初始化實例字段,其實也是轉換成構造器方法中的代碼來實現。
CLR是容許值類型建立實例,可是c#編譯器是不會默認爲值類型構建構造函數的,而且值類型構造器必須顯示調用才執行。如上面所說,即便你本身定義了一個構造函數,無論它是有參仍是無參,編譯器都不會去自動調用它,若是你想執行,必須本身顯示進行調用。
然而,上面說那麼多,在C#中,編譯器根本不容許你定義值類型的無參構造函數,它會報:error CS0568:結構不能包含顯示的無參構造函數。
同理,你不能對值類型的字段成員進行內聯賦值,由於內聯語句其實是經過構造器進行賦值,以下面的代碼:
internal struct SomeValType { private int m=5; }
上面的代碼,會報:結構中不能有實例字段初始值設定項。
因此,值類型的字段老是被初始化爲0或null,由於沒有真正意義上的構造函數爲它初始化其餘值,只有你手動去調用構造函數(因此這裏咱們不理解爲初始化)。
當你提供一個有參構造函數時,你須要爲全部的字段進行賦值,不然會報:error CS0171:在控制返回到調用方法以前,字段XXX必須徹底賦值。
實例構造器是爲了讓類的實例有一個良好的可驗證的初始值。而類型構造器是爲靜態類型服務,顧名思義,類型構造器則是爲了讓類型有良好的初始狀態。
默認沒有構造函數
(類型)靜態構造器永遠不能有參數
必須標記爲static,由於靜態類型的成員必須爲靜態成員
不能賦予任何訪問修飾符,默認爲隱式類型,C#默認爲private
類型構造器中的代碼只能訪問類型的靜態字段(常規用途就是初始化這些字段)
類型構造器調用過程大體以下:
JIT編譯器在編譯到一個靜態方法時,會查看引用了哪些靜態類型。若是這個靜態類型定義了一個構造函數,JIT編譯器會檢查當前AppDomain,是否已經執行過了這個類型構造器。若是已經執行過,就不添加對它的調用。若是從未執行過,JIT編譯器會在它的本機代碼中添加對類型構造器的調用。
重要的是:爲何靜態類型的特性是十分適合作單例呢?由於CLR經常是確保每個AppDomain中,一個類型構造器都只執行一次,那麼上述的機制不足以很好的支撐這個特性,由於,多個線程下如何保證呢?爲了保證這一點,調用類型構造器時,每個調用線程都會獲取一個互斥線程同步鎖,在這樣的機制下,若是多個線程試圖同時調用某個類型的靜態構造器,只有一個線程能夠得到鎖,其餘的線程會被阻塞。只有第一個線程會執行靜態構造器的代碼。當一個線程離開構造器後,正在等待的線程纔會被喚醒,後面的線程會發現,類型構造器已經被執行過了,將直接從構造方法返回。這樣就能確保不會被再次調用。而且以上是線程安全的。
因此,單例模式就是藉助上面的特性,你想構建的單例對象,則也應該放到類型構造器中進行初始化。
注意:值類型中也能夠定義類型(靜態)構造器,可是是不推薦這麼作的,由於有時候CLR有時不會調用值類型的靜態類型構造器。
internal struct StructValType { //雖然值類型的構造函數必須有參數,可是這個是靜態構造函數,因此它是必定沒有參數的,也不用遵照,必須初始化全部成員的值 static StructValType() { Console.WriteLine("我會出現嗎?"); } public int x; } class BaseClass { public string ClassName { get; set; } static BaseClass() { Console.WriteLine("I'm BaseClass static Constructor without param"); } public BaseClass() { Console.WriteLine("I'm BaseClass Constructor without param"); } }
上述代碼,BaseClass中和StructValType中都有static構造函數,再對兩個類進行實例時,你能夠發現值類型的靜態函數是沒有被調用的。
注意:單個線程中,兩個類型構造器包含互相引用的代碼可能出問題,由於你沒法把握二者的實現順序,也就沒法保證能正確的引用。由於是CLR負責類型構造器的調用,因此不能要求以特定的順序調用類型構造器。
若是,類型構造器拋出未處理的異常,CLR會認爲類型不可用。試圖訪問該類型的任何字段和方法都會拋出System.TypeInitializationException異常。