原創文章,轉載請註明出處! 以下總結參閱了:MSDN文檔、《C 高級編程》、《C 本質論》等資料,如有不正確的地方,請幫忙及時指出!以免誤導! 1.實現多態性的兩種方式:繼承抽象類、實現介面 其實就是協變的應用,通過把對象向上轉型為基類或介面類型,對它調用成員,可實現多態性,即運行時調用的是對應對象 ...
原創文章,轉載請註明出處! 以下總結參閱了: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
Person person = new Man();//自動向基類隱式轉換,但person的運行時類型仍為Man
Women women = (Women)person; //錯誤
Women women = person as Women; //women為null ,因為男人不能轉換為女人
但需註意"as"只能應用於引用類型或可為null類型。若目標可能為基元類型,則應該通過"is"操作符來過濾
if(!(person is int))
{
Women women = person as Women;
}
子類與子類之間的橫向轉換,應該定義轉換操作符(關鍵字implicit、explicit)
9.獲取一個可空類型Nullable的值,安全簡單的做法是用"??",如 int j = i ?? 0;
,普通做法:
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()返回的值也相同,這樣字典才能正常工作。 - 註意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()來獲得對象的一個淺拷貝副本。
深拷貝:通過系列化與反系列化來深拷貝一個對象。
通常做法,如下:介面ICloneable唯一成員是object Clone(),實現該介面只是為了表明該類型的實現可以被拷貝[Serializable] class Person : ICloneable { public string ID {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 等) - 多線程集合類:位於System.Collections.Concurrent命名空間中,如ConcurrentBag
對應於List 、ConcurrentDictionary 、ConcurrentStack 、ConcurrentQueue
實現自定義集合類時,不要繼承自內置的集合類,而應該自行實現相應的泛型介面:
IEnumerable
ICollection
IList
19.泛型
- 避免為自定義泛型定義靜態成員,在不同的類型之間共用靜態成員沒意義。
- 記得為泛型參數設定必要的約束,因為約束之後可以使泛型參數成為一個實實在在的"對象",可以訪問到約束類型的實例成員,而不做約束的話僅僅是一個object對象
- 必要時用default(T)為泛型類型變數指定預設值,如T param = default(T);
20.委托
預定義的委托類型能滿足大部分日常需求,我們沒有必要聲明自己的委托類型。
- Action,Action
:接受0個或多個輸入參數,無返回值 - Func,Func
:接受0個或多個輸入參數,帶返回值,類型是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),而委托通常在局部變數的作用域之外才執行。
也就是說,委托中註冊的方法持有了對局部變數的引用,形成了像JavaScript中的閉包一樣的效果,執行委托方法時,局部變數的值將是最新值,而不是給委托註冊方法時的局部變數值。public static void Main() { Action act=new Action(()=>Console.WriteLine("Begin")); for (int i = 0; i < 5; i++) { act += () => Console.WriteLine(i.ToString()); } act(); //Begin 5 5 5 5 5 因為委托方法持有了對i的引用,當前i的值為5 Console.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 4 Console.ReadKey(); }
25.賦值為null,大部分情況下不能提前垃圾回收。
- 沒有必要將沒用的實例成員顯式賦值為null,因為編譯器會忽略該語句。
- 只有對日後確實沒用的靜態欄位顯式賦值為null才有必要,但要確保不會再用到它(或者說不會再用到它的包含類)。
- 把一個對象賦值為null,它的靜態成員不會跟著變為null,因為靜態成員跟類的實例無關,它會一直留在記憶體中,除非顯式賦值為null。