[读书笔记]C#学习笔记三: C#类型详解..
前言
這次分享的主要內(nèi)容有五個(gè), 分別是值類型和引用類型,?裝箱與拆箱,常量與變量,運(yùn)算符重載,static字段和static構(gòu)造函數(shù). 后期的分享會針對于C#2.0 3.0 4.0 等新特性進(jìn)行. 再會有三篇博客 ?這個(gè)系列的就會結(jié)束了. 也算是自己對園子中@Learning Hard出版的<<C#學(xué)習(xí)筆記>>的一個(gè)總結(jié)了. 博客內(nèi)容基本上都是白天抽空在公司寫好的了, 但是由于公司內(nèi)部網(wǎng)絡(luò)不能登錄博客園所以只能夠夜晚拿回來修改, ?寫的不好或者不對的地方也請各位大神指出. 在下感激不盡了.?
1,值類型和引用類型
1.1 值類型與引用類型簡介
C#值類型數(shù)據(jù)直接在他自身分配到的內(nèi)存中存儲數(shù)據(jù),而C#引用類型只是包含指向存儲數(shù)據(jù)位置的指針。
C#值類型,我們可以把他歸納成三類:
第一類: 基礎(chǔ)數(shù)據(jù)類型(string類型除外):包括整型、浮點(diǎn)型、十進(jìn)制型、布爾型。
?整型包括:sbyte、byte、char、short、ushort、int、uint、long、ulong這九種類型;
? 浮點(diǎn)型就包括 float 和 double 兩種類型;
? ? ?十進(jìn)制型就是 decimal ;
? 布爾型就是 bool 型了。
第二類:結(jié)構(gòu)類型:就是struct型
第三類:枚舉類型:就是enum型
?C#引用類型有五種:class、interface、delegate、object、string、Array。
上面說的是怎么區(qū)分哪些C#值類型和C#引用類型,而使用上也是有區(qū)別的。所有值類型的數(shù)據(jù)都無法為null的(這里可空類型是可以為空的),聲明后必須賦以初值;引用類型才允許 為null。
1.2 值類型與引用類型的區(qū)別
值類型與引用類型的區(qū)別是面試中經(jīng)常經(jīng)常問到的問題,完美的回答當(dāng)然不能只是簡單地重復(fù)兩者的概念,因?yàn)槊嬖嚬俑M愦鸪鏊麄冎g深層的區(qū)別--不同的內(nèi)存分布
值類型通常被分配到縣城的堆棧上,而引用類型則被分配到托管堆上。不同的分配位置導(dǎo)致了不用的管理機(jī)制,值類型的管理由操作系統(tǒng)負(fù)責(zé),而引用類型的管理則由垃圾回收器(GC)負(fù)責(zé)。
?
1 class Program 2 { 3 static void Main() 4 { 5 //valueType是值類型 6 int valueType = 3; 7 //refType是引用類型 8 string regType = "abc"; 9 } 10 }?
圖1:?
值類型和引用類型的區(qū)別在實(shí)際數(shù)據(jù)的存儲位置:值類型的變量和實(shí)際數(shù)據(jù)都存儲在堆棧中;
而引用類型則只有變量存儲在堆棧中,變量存儲實(shí)際數(shù)據(jù)的地址,實(shí)際數(shù)據(jù)存儲在與地址相
對應(yīng)的托管堆中。
1.3引用類型中嵌套定義值類型
如果類的字段類型是值類型,它將作為引用類型實(shí)例的一部分,被分配到托管堆中。但那些作為局部變量
(例如下列代碼中的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 Program 15 { 16 static void Main(string[] args) 17 { 18 NestedRefTypeInValue refType = new NestedRefTypeInValue(); 19 } 20 }?
以上代碼的內(nèi)存分配情況如圖2所示:
1.4 值類型中嵌套定義引用類型
1 public class TestClass 2 { 3 public int x; 4 public int y; 5 } 6 7 //值類型嵌套定義引用類型的情況 8 public struct NestedRefTypeValue 9 { 10 //結(jié)構(gòu)體字段,注意,結(jié)構(gòu)體字段不能被初始化 11 private TestClass classinValueType; 12 13 //結(jié)構(gòu)體中的構(gòu)造函數(shù),注意,結(jié)構(gòu)體中不能定義無參的構(gòu)造函數(shù) 14 public NestedRefTypeInValue(TestClass t) 15 { 16 calssinValueType.x = 3; 17 calssinValueType.y = 5; 18 calssinValueType = t; 19 } 20 } 21 22 class Program 23 { 24 static void Main(string[] args) 25 { 26 //值類型變量 27 NestedRefTypeInValue valueType = new NestedRefTypeInValue(new TestClass()); 28 } 29 }?
以上代碼的內(nèi)存分配情況如圖3所示:
從以上分析可以得出結(jié)論:值類型實(shí)例中會被分配到它聲明的地方,聲明的是局部變量時(shí),將被分配到棧上。而聲明為引用類型時(shí),則被分配到托管堆上。
而引用類型實(shí)例中是被分配到托管堆上。
上面只是分析了值類型與引用類型的內(nèi)存分布方面的區(qū)別, 除此之外,二者還存在其他幾個(gè)方面的區(qū)別,現(xiàn)總結(jié)如下:
1。值類型繼承自ValueType, ValueType又繼承自System.Object; 而引用類型則直接繼承于System.Object。
2。值類型的內(nèi)存不受GC控制,作用域結(jié)束時(shí),值類型會被操作系統(tǒng)自行釋放,從而減少托管堆的壓力;而引用類型的內(nèi)存管理則由GC來完成。所以與引用類相比,只類型在性能上更具有優(yōu)勢。
3。若只類型的密封的(sealed), 你將不能把只類型作為其他任何類型的基類;而引用類型則一般具有繼承性,這里指的是類和接口。
4。值類型不能為null值(非空類型占不討論),它會被默認(rèn)初始化為數(shù)值0; 而引用類型在默認(rèn)情況下會被初始化為null值,表示不指向托管堆中的任何地址。對值null的引用類型的任何操作,都會引發(fā)空指針異常。
5。由于值類型變量包含其實(shí)際數(shù)據(jù),因此在默認(rèn)情況下,只類型之間的參數(shù)傳遞不會印象變量本身; 而引用類型變量保存的是數(shù)據(jù)的引用地址,它們作為參數(shù)被傳遞時(shí),參數(shù)會發(fā)生改變,從而影響應(yīng)用類型變量的值。
2,兩大類型間的轉(zhuǎn)換--裝箱與拆箱
類型轉(zhuǎn)換主要分為以下幾種方式:
1, 隱式類型轉(zhuǎn)換:由低級別類型向高級別類型的轉(zhuǎn)換過程。例如:派生類可以隱式的轉(zhuǎn)換為它的父類,裝箱過程就輸入這種隱式類型轉(zhuǎn)換。
2, 顯示類型轉(zhuǎn)換:也叫做強(qiáng)制類型轉(zhuǎn)換,但是這種轉(zhuǎn)換可能會導(dǎo)致精度損失或者出現(xiàn)運(yùn)行時(shí)異常。
3, 通過is和as運(yùn)算符進(jìn)行安全的類型轉(zhuǎn)換。
4, 通過.Net 類庫中的Convert類來完成類型轉(zhuǎn)換。
下面主要介紹只類型與引用類型間的一種轉(zhuǎn)換:裝箱和拆箱
裝箱:值類型轉(zhuǎn)換為引用類型的過程
拆箱:引用類型轉(zhuǎn)換為值類型的過程
裝箱過程中,系統(tǒng)會在托管堆中生成一份堆棧中值類型對象的副本。而拆箱則是從托管堆中將引用類型所指向的已裝箱數(shù)據(jù)復(fù)制回值類型對象的過程。
下面通過示例從內(nèi)存的角度對兩個(gè)過程進(jìn)行分入分析:
以上代碼分別執(zhí)行了一次裝箱和拆箱操作。
裝箱操作可以具體分為以下3個(gè)步驟:
(1)內(nèi)存分配: 在托管堆中分配好內(nèi)存空間以存放復(fù)制的實(shí)際數(shù)據(jù)?
(2)完成實(shí)際數(shù)據(jù)復(fù)制:將值類型實(shí)例的實(shí)際數(shù)據(jù)復(fù)制到新分配的內(nèi)存中
(3)地址返回: 將托管堆中的對象地址返回給引用類型變量。
裝箱過程就是通過這3步完成的,如圖4所示。
在IL代碼中,裝箱過程是由box指令來實(shí)現(xiàn)的,上一段代碼所對應(yīng)的IL 代碼如下所示:
在這段IL代碼中,除了有box指令外,我們還看到了一個(gè)unbox指令,正如其字面意思所提示的一樣,該指令就是完成拆箱操作的IL指令。
拆箱過程也可以具體分為3個(gè)步驟:
(1)檢查實(shí)例:首先檢查要進(jìn)行拆箱操作的引用類型變量是否為null,如果為null則拋出空指針異常,如果不為null則繼續(xù)減產(chǎn)變量是否合拆箱后的類型是同一類型,若不是則會拋出InvalidCastExce異常
(2)地址返回:返回已裝箱變量的實(shí)際數(shù)據(jù)部分地址
(3)數(shù)據(jù)復(fù)制: 將托管堆中的實(shí)際數(shù)據(jù)復(fù)制到棧中
?
總結(jié):對于拆箱與裝箱的理解之所以是如此重要,主要是因?yàn)檠b箱和拆箱操作對性能有很大的影響。 如果程序代碼中存在過多的裝箱和拆箱操作,由于兩個(gè)過程
都需要進(jìn)行數(shù)據(jù)復(fù)制,該操作會消耗大量額外運(yùn)行時(shí)間;并且裝箱和拆箱必然會產(chǎn)生多余的對象,這進(jìn)一步加重了GC的負(fù)擔(dān),導(dǎo)致程序的性能降低。此外,還會引起一些隱藏的bug。
所以我們在寫代碼時(shí),應(yīng)盡量避免裝箱拆箱操作,最好使用泛型來編程。當(dāng)然泛型的好處不止于此,泛型還可以增加程序的可讀性,使程序更容易被復(fù)用等等,至于泛型以后再做詳細(xì)介紹.
?
更多內(nèi)容請參考:http://www.cnblogs.com/ludbul/p/4466522.html 《C#中如何正確的操作字符串?》
?
3,常量與變量
這里主要講一下靜態(tài)常量const和動態(tài)常量readonly
?
1)const修飾的常量在聲明的時(shí)候必須初始化;readonly修飾的常量則可以延遲到構(gòu)造函數(shù)初始化
2)const修飾的常量在編譯期間就被解析,即常量值被替換成初始化的值;readonly修飾的常量則延遲到運(yùn)行的時(shí)候
此外const常量既可以聲明在類中也可以在函數(shù)體內(nèi),但是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;
解析:
那么為什么是這樣的呢?其實(shí)在上面說了,const是靜態(tài)常量,所以在編譯的時(shí)候就將A與B的值確定下來了(即B變量時(shí)10,而A=B*10=10*10=100),那么Main函數(shù)中的輸出當(dāng)然是A is 100,B is 10啦。而static readonly則是動態(tài)常量,變量的值在編譯期間不予以解析,所以開始都是默認(rèn)值,像A與B都是int類型,故都是0。而在程序執(zhí)行到A=B*10;所以A=0*10=0,程序接著執(zhí)行到B=10這句時(shí)候,才會真正的B的初值10賦給B。
?
4,運(yùn)算符重載
運(yùn)算符重載只能用于類或結(jié)構(gòu)中,通過類或結(jié)構(gòu)中聲明一個(gè)名為operator x的方法,即可完成一個(gè)運(yùn)算符的重載。
?
先來看幾行簡單的代碼:
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 }?
?
一個(gè)int sum=x+y; 加法運(yùn)算。
?
稍微封裝一下:
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 }?
?
如果現(xiàn)在有一個(gè)類,需要得知兩個(gè)類某個(gè)屬性的和,我們可能會這樣:
?
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 Program 13 { 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 Person 13 { 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構(gòu)造函數(shù)
主要來說明執(zhí)行的順序:
1、編譯器在編譯的時(shí)候,先分析所需要的靜態(tài)字段,如果這些靜態(tài)字段所在的類有靜態(tài)的構(gòu)造函數(shù),那么就會忽略字段的初始化;如果沒有靜態(tài)的構(gòu)造函數(shù),那么就會對靜態(tài)字段進(jìn)行初始化。
2、如果存在多個(gè)靜態(tài)類,那么初始化的靜態(tài)成員的順序會根據(jù)引用的順序,先引用到的先進(jìn)行初始化,但如果類的靜態(tài)成員的初始化依賴于其他類的靜態(tài)成員,則會先初始化被依賴的靜態(tài)成員。
3、而帶有靜態(tài)構(gòu)造函數(shù)的類的靜態(tài)字段,只有在引用到的時(shí)候才進(jìn)行初始化。
?
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 Program 13 { 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 }?
?
執(zhí)行結(jié)果是:A.X = 1, B.Y = 2;
?
結(jié)果如何呢?再來看第二個(gè)小例子:
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 Program 13 { 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 }?
?
執(zhí)行結(jié)果是:A.X = 2, B.Y = 1;
是否和你想的結(jié)果一致呢?其實(shí)分析這兩種情況 只要記住第一條概念就好:如果這些靜態(tài)字段所在的類有靜態(tài)的構(gòu)造函數(shù),那么就會忽略字段的初始化;如果沒有靜態(tài)的構(gòu)造函數(shù),那么就會對靜態(tài)字段進(jìn)行初始化。
?
看第一段代碼片斷:
A:
public static int X = B.Y+1;
static A() { }
?
B:
public static int Y=A.X+1;
?
A.X B.X ==> 先調(diào)用A.X, A中int X = B.Y + 1; 所以會接著調(diào)用B.Y, 因?yàn)锽中無靜態(tài)的構(gòu)造函數(shù),所以就會對靜態(tài)字段進(jìn)行初始化。 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 ==> 先調(diào)用A.X, A中int X = B.Y + 1; 所以會接著調(diào)用B.Y, 因?yàn)锽中有靜態(tài)的構(gòu)造函數(shù),所以就會忽略字段的初始化。 int Y = 1; 故 X = 2; B.Y = 2;
大家如果有興趣的話也可以設(shè)置斷點(diǎn)查看下代碼是如何運(yùn)行的。
?
轉(zhuǎn)載于:https://www.cnblogs.com/wang-meng/p/5143732.html
總結(jié)
以上是生活随笔為你收集整理的[读书笔记]C#学习笔记三: C#类型详解..的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Divide and conquer:K
- 下一篇: C# 5.0中新增特性