深入理解C#:编程技巧总结(一)
以下總結參閱了:MSDN文檔、《C#高級編程》、《C#本質論》、前輩們的博客等資料,如有不正確的地方,請幫忙及時指出!以免誤導!
1..實現多態性的兩種方式:繼承抽象類、實現接口
其實就是協變的應用,通過把對象向上轉型為基類或接口類型,對它調用成員,可實現多態性,即運行時調用的是對應對象的實現版本成員。這兩種方式的區別:
繼承抽象類:會用掉唯一1次的繼承機會,但可以繼承任何成員(包括字段),自由度高
實現接口:必須實現所有成員,不能包含字段,但可以實現多個接口
抽象類可以提供成員的具體實現,而接口只負責聲明,不能提供任何實現代碼
注意:
接口一旦被定義就不應該再被改變,否則所有實現該接口的類型都必須跟著修改。
而抽象類則可以隨時添加新的成員,不影響他的子類,還能提供新的額外功能。
多態性示例:(協變與逆變)
//可以返回Stream的任何子類類型Stream Method1(bool boo){ }//可以接收Stream的任何子類類型的參數void Method2(Stream stream){ }2.不要創建可變的值類型(結構、枚舉),若要改變,請用一個方法來返回一個新實例。要時刻注意頻繁的裝箱與拆箱對性能的影響
3.僅在能一眼看出變量的類型時,才使用var聲明
4.定義值類型時,它的大小不要超過16字節,否則影響性能(頻繁復制時),要么改為使用引用類型,要么讓它按ref引用傳遞
5.值類型數組之間不能直接互相轉換,可以通過一次中間轉換為Array來達到目的,如:
(int[])(Array)new uint[32]
但應注意可能在不同的CLR實現中表現不同!
6.數組與List
如果元素數量固定,且不涉及轉型,則使用數組效率更高。
在元素數量可能發生變化的情況下,就不應該使用數組,而應該使用List
無論是數組還是List
,元素個數也不能太多,避免成為占用內存超過85000字節的大對象,因為大對象將會被分配到單獨的堆進行處理,在回收大對象時效率較低。
7.字符串操作
字符串字面量、字符串常量,直接用"+"相連效率高,因為:string str = "srf"+"ttt"+"ccc";會直接編譯成string str = "srftttccc";,同樣適用于字符串常量。
盡量避免對變量的裝箱:字符串+變量,較好的做法是:字符串+變量.ToString()
頻繁操作字符串時用StringBuilder,并制定足夠大的容量,而string.Format("{0}{1}{2}",str1,str2,str3);內部也是用StringBuilder。
8.類型轉換
字符串轉其它基元類型:
默認十進制:用Parse()、TryParse(),如:int.TryParse("24");,其中TryParse效率更高
指定基數進制形式來解析:Convert.ToInt32("0xFF",16);
從字節數組中提取一段,轉為基元類型:BitConvert.ToInt32(Byte[] arr, int startIndex);
自定義類型之間的強制轉換:
從基類強制轉換為子類時,安全的做法是使用"as",若目標為null或類型不兼容轉換失敗,均會返回null,而不會引發錯誤,如基類Person,它的子類Man、Women
但需注意"as"只能應用于引用類型或可為null類型。若目標可能為基元類型,則應該通過"is"操作符來過濾
if(!(person is int)) {Women women = person as Women; }子類與子類之間的橫向轉換,應該定義轉換操作符(關鍵字implicit、explicit)
9.獲取一個可空類型Nullable的值,安全簡單的做法是用"??",如?int j = i ?? 0;,普通做法:
if(i.HasValue()) { int j = i.Value; }
10.常量const和只讀字段readonly的區別:
const是編譯期常量,它總是靜態的,編譯時直接用實際值填充。而readonly是一個運行時常量。
const只能修飾基元類型、枚舉類型、字符串類型,而readonly沒有限制。
const一經聲明就必須初始化,且之后就無法再改變。而readonly可顯式初始化,也可不初始化,它的值可以通過構造函數來改變(即每個實例有自己的readonly只讀字段值)
注意:除了構造函數之外,都無法改變readonly的值,對于引用類型是無法改變它的引用,即它只能引用同一對象。但該對象本身是可以被修改的。
11.枚舉類型
枚舉類型可以為從byte到ulong的基元類型,定義枚舉時應該始終為它定義一個零值,因為聲明一個枚舉變量而未初始化時的默認值將是0
除了0值,要么都不為成員顯式賦值,要么就全部賦值(如應用了Flags特性的標志枚舉),否則未賦值的成員將等于它前一個成員的值加1,因為枚舉成員的值默認是按順序逐個加1
對枚舉應用[Flags]特性,可以定義一個標志枚舉,它的成員值通常初始化為2的次冪,之后就可以通過按位運算來判斷、合并枚舉成員了。
定義一個枚舉來專門負責表示狀態的信息,這樣使代碼更易理解。如用枚舉成員on、off來代替true、false或0、1
12.如果需要,應該為類型重載常用的運算符和比較運算符,如重載">"以實現person1>person2
13.若該類型有泛型版本,則應該使用泛型版本,因為泛型類型效率更高(避免了裝箱、拆箱、類型轉換)
14.相等性
值類型:對于值相等的兩個值類型變量A、B,"A==B"和"A.Equals(B)"都返回true,而Object.ReferenceEquals(A,B)總是返回false。
引用類型:Object.ReferenceEquals(A,B)比較的是引用是否相等,而默認的A.Equals(B)也是比較的引用,需要重載Equals()方法來實現引用類型之間的"值相等性比較"(如:當person1.ID == person2.ID時,person1.Equals(person2)返回true,來表示他們相等)
注意1:重寫了Equals()方法,最好也一起重寫GetHashCode()方法,因為對于不同的對象,默認的GetHashCode()返回的值將永遠不同,而若把對象作為Dictionary
的TKey時,根據TKey取值時,會根據對象的HashCode來比較。所以需要重新GetHashCode(),使得Equals()方法返回true時,GetHashCode()返回的值也相同,這樣字典才能正常工作。</TKEY,TVALUE>
注意2:重寫了Equals()、GetHashCode()方法,同時也應該實現IEquatable
接口,該接口的成員bool Equals(T t1)比Object的Equals(object obj)類型更安全、更高效。
注意3:對于字符串,雖然它也是對象,但當兩個字符串所包含的字面值一樣時,運行時將只在內存中創建一個該字面值的字符串對象,也就是說所有字面值一樣的字符串對象都將引用同一個地址。
15.ToString()方法
應該總是為自定義類型重寫Object的ToString()方法,最好還要實現IFormattable接口,該接口的ToString(string format, IFormatProvider formatProvider)提供了根據參數來輸出特定的格式化形式。如:
public string ToString(string format, IFormatProvider formatProvider){ ??switch(format){ ? ? ? ?case "CH": ? ? ?
?? ? ? ? ?return this.ToString();
?? ? ? ? ? ?case "EN": ? ? ?
?? ? ? ? ???return string.Format("{0}{1}",FirstName,LastName);......} }//調用Console.WriteLine(person.ToString("EN",null));
16.對象的淺拷貝與深拷貝
淺拷貝:使用Object基類的實例方法MemberwiseClone()來獲得對象的一個淺拷貝副本。
深拷貝:通過系列化與反系列化來深拷貝一個對象。
[Serializable]class Person : ICloneable{public string ID {
通常做法,如下:接口ICloneable唯一成員是object Clone(),實現該接口只是為了表明該類型的實現可以被拷貝
get;set;}public int Age {get;set;}public Work work {get;set;} //實現ICloneable接口的Clone()public object Clone(){ ?
?return this.MemberwiseClone(); }//自定義深拷貝方法public Person DeepClone(){ ?
? ?using (Stream objectStream = new MemoryStream()){IFormatter formatter = new BinaryFormatter();formatter.Serialize(objectStream, this);objectStream.Seek(0, SeekOrigin.Begin); ? ?
?? ?return formatter.Deserialize(objectStream) as Person;}} }
17.集合的遍歷
for循環:采用索引器,for循環的優點是遍歷過程中可以修改集合的元素。
foreach循環:采用迭代器,遍歷過程中無法對集合增刪元素操作,因為迭代器只對原始版本的集合進行遍歷,每次迭代都會進行版本判斷,若集合發生變化,將拋出異常。- - - - foreach循環的優點是語法更簡潔,且迭代完畢后自動調用Dispose()(foreach循環內部使用了try...finally)
18.選擇正確的集合:詳解請參見《C#高級編程》,書中對集合講的很細
線性:集合的每個元素都是是1對1的,大部分常用集合都是線性集合
非線性:1對多、多對1、多對多(樹、集HashSet
、圖)
直接存取:具有索引器,元素按索引器排列,訪問、查找速度快,在末尾添加刪除速度也快,但在中間刪除、插入元素效率低(需要移動后面的所有元素)。(數組、List
、字符串、結構)
順序存取:即線性表,可動態擴大或縮小,通過對地址的引用來搜索元素,刪除、插入元素效率高,但查找效率低(需要遍歷查找)(Stack
、Queue、Dictionary、LinkedList等)</TKEY、TVALUE>
多線程集合類:位于System.Collections.Concurrent命名空間中,如ConcurrentBag
對應于List、ConcurrentDictionary、ConcurrentStack、ConcurrentQueue</TKEY,TVALUE>
實現自定義集合類時,不要繼承自內置的集合類,而應該自行實現相應的泛型接口:
IEnumerable:提供迭代功能
ICollection:提供常用操作
IList
19.泛型
避免為自定義泛型定義靜態成員,在不同的類型之間共享靜態成員沒意義。
記得為泛型參數設定必要的約束,因為約束之后可以使泛型參數成為一個實實在在的"對象",可以訪問到約束類型的實例成員,而不做約束的話僅僅是一個object對象
必要時用default(T)為泛型類型變量指定默認值,如T param = default(T);
20.委托
預定義的委托類型能滿足大部分日常需求,我們沒有必要聲明自己的委托類型。
Action,Action
:接受0個或多個輸入參數,無返回值</T1,...,T16>
Func,Func
:接受0個或多個輸入參數,帶返回值,類型是TResult</T1,...,T16,TRESULT>
Predicate:表示定義一組條件并判斷參數是否符合條件
具有特定用途的委托:事件委托:
public delegate void EventHandler(object sender, EventArgs e);
public delegate void EventHandler<TEventArgs>(object sender, TEventArgs e);線程中的委托:
public delegate void ThreadStart(); //無參數
public delegate void ParameterrizedThreadStart(object obj); //參數對象obj異步回調委托:
public delegate void AsyncCallback(IAsyncResult ar);
21.對于只用一次,且主體語句數量較少的方法,應該使用Lambda表達式,它通常用于注冊給委托、或作為其它方法的參數(參數類型是匹配的委托類型)
22.理解委托的本質:
委托是一個類
委托保存著對注冊方法的引用(方法指針),多播委托保存著一組方法指針
執行委托,將按順序調用方法指針指向的方法
對一個委托實例用"="賦值一個新的方法指針時,將會調用構造函數實例化一個新的委托對象
所以在實例化一個委托對象之后后,應該時刻記住使用"+="、"-="來增加、刪除新的方法指針
委托類的方法:Invoke()默認調用、在線程池中啟用一個新線程調用BeginInvoke()、停止EndInvoke()
23.事件也是委托,加了event關鍵字是為了限制委托:
禁止了在包含類外部對委托事件對象使用"="賦值,確保不會被覆蓋或賦值為null
禁止了在包含類外部對委托事件對象的直接調用,事件的調用應該是包含類的責任
參數1是觸發者對象的引用,參數2是EventArgs或其派生類的對象(可包含一些將在事件觸發時需要用到的數據)
24.當委托和Lambda小心閉包對象
(特別是在循環體中的循環變量,對于C#5.0的foreach則不必擔心)
當Lambda表達式引用了局部變量時,編譯器就會自動創建一個閉包對象(如TempClass),該對象的成員包含一個對局部變量的引用(如TempClass.i)、和一個與Lambda表達式等價的方法(如TempClass.add,該方法持有對局部變量的引用)。
而該閉包對象中的方法成員TempClass.add最終被賦給了委托(如MyDel),而委托通常在局部變量的作用域之外才執行。
public static void Main(){Action act=new Action(()=>Console.WriteLine("Begin")); ?
也就是說,委托中注冊的方法持有了對局部變量的引用,形成了像JavaScript中的閉包一樣的效果,執行委托方法時,局部變量的值將是最新值,而不是給委托注冊方法時的局部變量值。
?for (int i = 0; i < 5; i++){act += () => Console.WriteLine(i.ToString());}act(); //Begin 5 5 5 5 5 ?因為委托方法持有了對i的引用,當前i的值為5Console.ReadKey(); }public static void Main(){Action act=new Action(()=>Console.WriteLine("Begin")); ?
? ?for (int i = 0; i < 5; i++){ ? ? ? ?int temp = i; //每次都用一個新的temp變量來保存當前的i值act += () => Console.WriteLine(temp.ToString());}act(); //Begin 0 1 2 3 4Console.ReadKey(); }
25.賦值為null,大部分情況下不能提前垃圾回收。
沒有必要將沒用的實例成員顯式賦值為null,因為編譯器會忽略該語句。
只有對日后確實沒用的靜態字段顯式賦值為null才有必要,但要確保不會再用到它(或者說不會再用到它的包含類)。
把一個對象賦值為null,它的靜態成員不會跟著變為null,因為靜態成員跟類的實例無關,它會一直留在內存中,除非顯式賦值為null。
后續還有很多其它方面的,如系列化與反系列化,異常處理等,由于篇幅有限,只能等下一篇再發布了
原文地址:http://www.cnblogs.com/susufufu/p/6263122.html
.NET社區新聞,深度好文,微信中搜索dotNET跨平臺或掃描二維碼關注
總結
以上是生活随笔為你收集整理的深入理解C#:编程技巧总结(一)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 微软正式发布XAML Standard与
- 下一篇: 在CentOS上使用Jexus托管运行