原創文章,轉載請注明出處! 以下總結參閱了: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,TValue>的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<TKey、TValue>、LinkedList 等) - 多線程集合類:位於System.Collections.Concurrent命名空間中,如ConcurrentBag
對應於List 、ConcurrentDictionary<TKey,TValue>、ConcurrentStack 、ConcurrentQueue
實現自定義集合類時,不要繼承自內置的集合類,而應該自行實現相應的泛型接口:
IEnumerable
ICollection
IList
19.泛型
- 避免為自定義泛型定義靜態成員,在不同的類型之間共享靜態成員沒意義。
- 記得為泛型參數設定必要的約束,因為約束之后可以使泛型參數成為一個實實在在的"對象",可以訪問到約束類型的實例成員,而不做約束的話僅僅是一個object對象
- 必要時用default(T)為泛型類型變量指定默認值,如T param = default(T);
20.委托
預定義的委托類型能滿足大部分日常需求,我們沒有必要聲明自己的委托類型。
- Action,Action<T1,...,T16>:接受0個或多個輸入參數,無返回值
- Func,Func<T1,...,T16,TResult>:接受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。