前言html
此次分享的主要內容有五個, 分別是值類型和引用類型, 裝箱與拆箱,常量與變量,運算符重載,static字段和static構造函數. 後期的分享會針對於C#2.0 3.0 4.0 等新特性進行. 再會有三篇博客 這個系列的就會結束了. 也算是本身對園子中@Learning Hard出版的<<C#學習筆記>>的一個總結了. 博客內容基本上都是白天抽空在公司寫好的了, 可是因爲公司內部網絡不能登陸博客園因此只可以夜晚拿回來修改, 寫的很差或者不對的地方也請各位大神指出. 在下感激涕零了. 面試
1,值類型和引用類型
1.1 值類型與引用類型簡介
C#值類型數據直接在他自身分配到的內存中存儲數據,而C#引用類型只是包含指向存儲數據位置的指針。編程
C#值類型,咱們能夠把他概括成三類:
第一類: 基礎數據類型(string類型除外):包括整型、浮點型、十進制型、布爾型。
整型包括:sbyte、byte、char、short、ushort、int、uint、long、ulong這九種類型;
浮點型就包括 float 和 double 兩種類型;
十進制型就是 decimal ;
布爾型就是 bool 型了。
第二類:結構類型:就是struct型
第三類:枚舉類型:就是enum型
C#引用類型有五種:class、interface、delegate、object、string、Array。
上面說的是怎麼區分哪些C#值類型和C#引用類型,而使用上也是有區別的。全部值類型的數據都沒法爲null的(這裏可空類型是能夠爲空的),聲明後必須賦以初值;引用類型才容許 爲null。安全
1.2 值類型與引用類型的區別
值類型與引用類型的區別是面試中常常常常問到的問題,完美的回答固然不能只是簡單地重複二者的概念,由於面試官更但願你答出他們之間深層的區別--不一樣的內存分佈
值類型一般被分配到縣城的堆棧上,而引用類型則被分配到託管堆上。不一樣的分配位置致使了不用的管理機制,值類型的管理由操做系統負責,而引用類型的管理則由垃圾回收器(GC)負責。微信
1 class Program 2 { 3 static void Main() 4 { 5 //valueType是值類型 6 int valueType = 3; 7 //refType是引用類型 8 string regType = "abc"; 9 }10 }
圖1: 網絡
值類型和引用類型的區別在實際數據的存儲位置:值類型的變量和實際數據都存儲在堆棧中;
而引用類型則只有變量存儲在堆棧中,變量存儲實際數據的地址,實際數據存儲在與地址相
對應的託管堆中。ide
1.3引用類型中嵌套定義值類型
若是類的字段類型是值類型,它將做爲引用類型實例的一部分,被分配到託管堆中。但那些做爲局部變量
(例以下列代碼中的c變量)的值類型,則仍然會分配到線程堆棧中。函數
1 //引用類型嵌套值類型的狀況 2 public class NestedValueTypeInRef 3 { 4 //valueType做爲引用類型的一部分被分配到託管堆上 5 private int valueType = 3; 6 7 public void method() 8 { 9 //c被分配到線程堆棧上10 char c = 'c';11 }12 }13 14 class Program15 {16 static void Main(string[] args)17 {18 NestedRefTypeInValue refType = new NestedRefTypeInValue();19 }20 }
以上代碼的內存分配狀況如圖2所示:post
1.4 值類型中嵌套定義引用類型性能
1 public class TestClass 2 { 3 public int x; 4 public int y; 5 } 6 7 //值類型嵌套定義引用類型的狀況 8 public struct NestedRefTypeValue 9 {10 //結構體字段,注意,結構體字段不能被初始化11 private TestClass classinValueType;12 13 //結構體中的構造函數,注意,結構體中不能定義無參的構造函數14 public NestedRefTypeInValue(TestClass t)15 {16 calssinValueType.x = 3;17 calssinValueType.y = 5;18 calssinValueType = t;19 }20 }21 22 class Program23 {24 static void Main(string[] args)25 {26 //值類型變量27 NestedRefTypeInValue valueType = new NestedRefTypeInValue(new TestClass());28 }29 }
以上代碼的內存分配狀況如圖3所示:
從以上分析能夠得出結論:值類型實例中會被分配到它聲明的地方,聲明的是局部變量時,將被分配到棧上。而聲明爲引用類型時,則被分配到託管堆上。
而引用類型實例中是被分配到託管堆上。
上面只是分析了值類型與引用類型的內存分佈方面的區別, 除此以外,兩者還存在其餘幾個方面的區別,現總結以下:
1。值類型繼承自ValueType, ValueType又繼承自System.Object; 而引用類型則直接繼承於System.Object。
2。值類型的內存不受GC控制,做用域結束時,值類型會被操做系統自行釋放,從而減小託管堆的壓力;而引用類型的內存管理則由GC來完成。因此與引用類相比,只類型在性能上更具備優點。
3。若只類型的密封的(sealed), 你將不能把只類型做爲其餘任何類型的基類;而引用類型則通常具備繼承性,這裏指的是類和接口。
4。值類型不能爲null值(非空類型佔不討論),它會被默認初始化爲數值0; 而引用類型在默認狀況下會被初始化爲null值,表示不指向託管堆中的任何地址。對值null的引用類型的任何操做,都會引起空指針異常。
5。因爲值類型變量包含其實際數據,所以在默認狀況下,只類型之間的參數傳遞不會印象變量自己; 而引用類型變量保存的是數據的引用地址,它們做爲參數被傳遞時,參數會發生改變,從而影響應用類型變量的值。
2,兩大類型間的轉換--裝箱與拆箱
類型轉換主要分爲如下幾種方式:
1, 隱式類型轉換:由低級別類型向高級別類型的轉換過程。例如:派生類能夠隱式的轉換爲它的父類,裝箱過程就輸入這種隱式類型轉換。
2, 顯示類型轉換:也叫作強制類型轉換,可是這種轉換可能會致使精度損失或者出現運行時異常。
3, 經過is和as運算符進行安全的類型轉換。
4, 經過.Net 類庫中的Convert類來完成類型轉換。
下面主要介紹只類型與引用類型間的一種轉換:裝箱和拆箱
裝箱:值類型轉換爲引用類型的過程
拆箱:引用類型轉換爲值類型的過程
裝箱過程當中,系統會在託管堆中生成一份堆棧中值類型對象的副本。而拆箱則是從託管堆中將引用類型所指向的已裝箱數據複製回值類型對象的過程。
下面經過示例從內存的角度對兩個過程進行分入分析:
1 class Program 2 { 3 static void Main() 4 { 5 int i = 3; 6 //裝箱操做 7 object o = i; 8 //拆箱操做 9 int y = (int)o;10 }11 }
以上代碼分別執行了一次裝箱和拆箱操做。
裝箱操做能夠具體分爲如下3個步驟:
(1)內存分配: 在託管堆中分配好內存空間以存放複製的實際數據
(2)完成實際數據複製:將值類型實例的實際數據複製到新分配的內存中
(3)地址返回: 將託管堆中的對象地址返回給引用類型變量。
裝箱過程就是經過這3步完成的,如圖4所示。
在IL代碼中,裝箱過程是由box指令來實現的,上一段代碼所對應的IL 代碼以下所示:
在這段IL代碼中,除了有box指令外,咱們還看到了一個unbox指令,正如其字面意思所提示的同樣,該指令就是完成拆箱操做的IL指令。
拆箱過程也能夠具體分爲3個步驟:
(1)檢查實例:首先檢查要進行拆箱操做的引用類型變量是否爲null,若是爲null則拋出空指針異常,若是不爲null則繼續減產變量是否合拆箱後的類型是同一類型,若不是則會拋出InvalidCastExce異常
(2)地址返回:返回已裝箱變量的實際數據部分地址
(3)數據複製: 將託管堆中的實際數據複製到棧中
總結:對於拆箱與裝箱的理解之因此是如此重要,主要是由於裝箱和拆箱操做對性能有很大的影響。 若是程序代碼中存在過多的裝箱和拆箱操做,因爲兩個過程
都須要進行數據複製,該操做會消耗大量額外運行時間;而且裝箱和拆箱必然會產生多餘的對象,這進一步加劇了GC的負擔,致使程序的性能下降。此外,還會引發一些隱藏的bug。
因此咱們在寫代碼時,應儘可能避免裝箱拆箱操做,最好使用泛型來編程。固然泛型的好處不止於此,泛型還能夠增長程序的可讀性,使程序更容易被複用等等,至於泛型之後再作詳細介紹.
更多內容請參考:http://www.cnblogs.com/ludbul/p/4466522.html 《C#中如何正確的操做字符串?》
3,常量與變量
這裏主要講一下靜態常量const和動態常量readonly
1)const修飾的常量在聲明的時候必須初始化;readonly修飾的常量則能夠延遲到構造函數初始化
2)const修飾的常量在編譯期間就被解析,即常量值被替換成初始化的值;readonly修飾的常量則延遲到運行的時候
此外const常量既能夠聲明在類中也能夠在函數體內,可是static readonly常量只能聲明在類中。
1 using System; 2 class P 3 { 4 static readonly int A=B*10; 5 static readonly int B=10; 6 public static void Main(string[] args) 7 { 8 Console.WriteLine("A is {0},B is {1} ",A,B); 9 }10 }
Result:A is 0, B is 10;
1 using System; 2 class P 3 { 4 const int A=B*10; 5 const int B=10; 6 public static void Main(string[] args) 7 { 8 Console.WriteLine("A is {0},B is {1} ",A,B); 9 }10 }
Result:A is 100, B is 10;
解析:
那麼爲何是這樣的呢?其實在上面說了,const是靜態常量,因此在編譯的時候就將A與B的值肯定下來了(即B變量時10,而A=B*10=10*10=100),那麼Main函數中的輸出固然是A is 100,B is 10啦。而static readonly則是動態常量,變量的值在編譯期間不予以解析,因此開始都是默認值,像A與B都是int類型,故都是0。而在程序執行到A=B*10;因此A=0*10=0,程序接着執行到B=10這句時候,纔會真正的B的初值10賦給B。
4,運算符重載
運算符重載只能用於類或結構中,經過類或結構中聲明一個名爲operator x的方法,便可完成一個運算符的重載。
先來看幾行簡單的代碼:
1 static void Main(string[] args)2 {3 int x = 5;4 int y = 6;5 int sum = x + y;6 Console.WriteLine(sum);7 Console.ReadLine();8 }
一個int sum=x+y; 加法運算。
稍微封裝一下:
1 static void Main(string[] args) 2 { 3 int x = 5; 4 int y = 6; 5 int sum = Add(x, y); 6 Console.WriteLine(sum); 7 } 8 9 static int Add(int x, int y)10 {11 return x + y;12 }
若是如今有一個類,須要得知兩個類某個屬性的和,咱們可能會這樣:
1 public class Person 2 { 3 public string Name { get; set; } 4 public int Age { get; set; } 5 public Person(string name, int age) 6 { 7 this.Name = name; 8 this.Age = age; 9 }10 }11 12 class Program13 {14 static void Main(string[] args)15 {16 Person p1 = new Person("aehyok", 25);17 Person p2 = new Person("Leo", 24);18 int sum = Add(p1.Age, p2.Age);19 Console.WriteLine(sum);20 }21 22 static int Add(int x, int y)23 {24 return x + y;25 }26 }
咱們再來改動一下:
1 class Program 2 { 3 static void Main(string[] args) 4 { 5 Person p1 = new Person("aehyok", 25); 6 Person p2 = new Person("Leo", 24); 7 int sum = p1 + p2; 8 Console.WriteLine(sum); 9 }10 }11 12 public class Person13 {14 public string Name { get; set; }15 public int Age { get; set; }16 public Person(string name, int age)17 {18 this.Name = name;19 this.Age = age;20 }21 22 public static int operator +(Person p1,Person p2)23 {24 return p1.Age+p2.Age;25 }26 }
5。static字段和static構造函數
主要來講明執行的順序:
一、編譯器在編譯的時候,先分析所須要的靜態字段,若是這些靜態字段所在的類有靜態的構造函數,那麼就會忽略字段的初始化;若是沒有靜態的構造函數,那麼就會對靜態字段進行初始化。
二、若是存在多個靜態類,那麼初始化的靜態成員的順序會根據引用的順序,先引用到的先進行初始化,但若是類的靜態成員的初始化依賴於其餘類的靜態成員,則會先初始化被依賴的靜態成員。
三、而帶有靜態構造函數的類的靜態字段,只有在引用到的時候才進行初始化。
1 public class A 2 { 3 public static int X = B.Y+1; 4 static A() { } 5 } 6 7 public class B 8 { 9 public static int Y=A.X+1;10 }11 12 class Program13 {14 static void Main(string[] args)15 {16 Console.WriteLine("A.X={0},B.Y={1}",A.X,B.Y);17 Console.ReadLine();18 }19 }
執行結果是:A.X = 1, B.Y = 2;
結果如何呢?再來看第二個小例子:
1 public class A 2 { 3 public static int X = B.Y+1; 4 } 5 6 public class B 7 { 8 public static int Y=A.X+1; 9 static B() { }10 }11 12 class Program13 {14 static void Main(string[] args)15 {16 Console.WriteLine("A.X={0},B.Y={1}",A.X,B.Y);17 Console.ReadLine();18 }19 }
執行結果是:A.X = 2, B.Y = 1;
是否和你想的結果一致呢?其實分析這兩種狀況 只要記住第一條概念就好:若是這些靜態字段所在的類有靜態的構造函數,那麼就會忽略字段的初始化;若是沒有靜態的構造函數,那麼就會對靜態字段進行初始化。
看第一段代碼片段:
A:
public static int X = B.Y+1;
static A() { }
B:
public static int Y=A.X+1;
A.X B.X ==> 先調用A.X, A中int X = B.Y + 1; 因此會接着調用B.Y, 由於B中無靜態的構造函數,因此就會對靜態字段進行初始化。 int Y = 0; 故 X = 1; B.Y = 2;
看第二段代碼片段:
A:
public static int X = B.Y+1;
B:
public static int Y=A.X+1;
static B() { }
A.X B.X ==> 先調用A.X, A中int X = B.Y + 1; 因此會接着調用B.Y, 由於B中有靜態的構造函數,因此就會忽略字段的初始化。 int Y = 1; 故 X = 2; B.Y = 2;
你們若是有興趣的話也能夠設置斷點查看下代碼是如何運行的。