編寫高質量程式碼改善C#程式的建議(Day3)
建議32:總是優先考慮泛型
泛型具備可重用性,型別安全和高效率(裝箱拆箱)
建議33:避免在泛型型別中宣告靜態成員
基於泛型類:根據T指定不同的資料型別,Class<T>相應地變成了不同的資料型別,相同Class不同T型別之間是不共享靜態成員的。
基於泛型方法:非泛型型別中的泛型方法並不會在執行時的原生代碼中生成不同的型別。
Console.WriteLine(MyList.Func<int>()); Console.WriteLine(MyList.Func<int>()); Console.WriteLine(MyList.Func<string>()); class MyList { static int count; public static int Func<T>() { return count++; } } 輸出: 0 1 2
建議34:為泛型引數設定約束
沒有設定約束的引數僅僅具有object的屬性
1)指定值型別(Nullable除外)
public void Method<T>(T t) where T : struct {}
2)指定引用型別
public void Method<T>(T t) where T : class{}
3)指定引數具有無引數的公共構造方法
public void Method<T>(T t) where T : new() {}
建議35:使用default為泛型型別變數指定初始值
值型別的預設初始值是0,而引用型別變數的預設初始值是null,對於泛型型別引數指定初始值最妥當的方法是使用default。
因為如果T是不可為null的值型別,賦值null會報錯,而直接賦值0會報無法將int隱式轉換為T型別。
public T Func<T>() { T t = default(T); return t; }
在FCL中的泛型型別,很多地方用到了default關鍵字,參考List<T>的Find方法
[__DynamicallyInvokable] public T Find(Predicate<T> match) { if (match == null) { ThrowHelper.ThrowArgumentNullException(ExceptionArgument.match); } for (int i = 0; i < this._size; i++) { if (match(this._items[i])) { return this._items[i]; } } return default(T); }
建議36:儘量使用FCL中自帶的委託宣告
Action表示接受0個或多個輸入引數,執行一段程式碼,沒有任何返回值;
public delegate void Action<in T>(T obj);
Func表示接受0個或多個輸入引數,執行一段程式碼,帶返回值;
public delegate TResult Func<in T, out TResult>(T arg);
Predicate表示定義一組條件判斷引數是否符合條件。
public delegate bool Predicate<in T>(T obj);
建議37:使用Lambda表示式代替方法和匿名方法
Lambda本質是匿名方法(不需要在外部宣告的方法),以List<T>的Find為例,寫法比較簡潔
// 第一種寫法 retrun this.Find(new Predicate<Student>(delegate(Student target) { //.... } )); // 第二種寫法 return this.Find(new Predicate<Student>((target)=>{//.... })); // 第三種寫法 return this.Find((target)=>{//.... })); // 第四種寫法 return this.Find(target=>target.Name==name);
建議38:小心閉包中的陷阱
閉包物件指的是編譯器預設為我們建立的類物件,在迴圈內部每次會為這個類的一個例項變數賦值,如果匿名方法(Lambda表示式)引用了某個區域性變數,編譯器會將該引用提升到該閉包物件中,即使程式碼執行後離開了原區域性變數的作用於(for迴圈),包含該閉包物件的作用域還在,所以會出現預期之外的輸出。
List<Action> lists = new List<Action>(); for(int i = 0; i < 5; i++) { Action t = ()=>{ Console.WriteLine(i.ToString()); }; lists.Add(t); } foreach(Action t in lists) { t(); } 輸出: 5 5 5 5 5
等效於
List<Action> lists = new List<Action>(); TempClass tempClass = new TempClass(); for(tempClass.i = 0; tempClass.i<5; tempClass.i++) { Action t = tempClass.TempFunc; lists.Add(t); } //... class TempClass { public int i; public void TempFunc() { Console.WriteLine(i.ToString()); } }
建議,將閉包物件的產生放在for迴圈內部
int temp = i; Action t = ()=>{ Console.WriteLine(temp.ToString()); };
建議39:瞭解委託的實質
1)委託是方法指標,將具有和委託宣告相同的宣告方法名賦值給委託變數,執行委託的時候就是在執行指標指向的方法;
2)委託是一個類。
public delegate void FileUploadedHandler(int progress); == class FileUpLoadedHandler:System.MulticastDelegate {....} // 呼叫委託方法 FileUploadedHandler(fileProgress); 實質是 FileUploaded.Invoke(fileProgress);
建議42:使用泛型引數相容泛型介面的不可變性
讓返回值型別返回比宣告的型別派生程度更大的型別,就是“協變”。
public Employee GetAEmployee(string name) { return new Programmer(){Name=name}; // Programmer是Employee的子類 }
static void Main(string[] args) { ISalary<Programmer> s = new BaseSalaryCounter<Programmer>(); PrintSalary(s); } // 雖然Programmer是Employee的子類,但是這裡卻無法接收 //private static void PrintSalary(ISalary<Employee> s) //{ // s.Pay(); //}
// 使用泛型型別引數以完成需求 private static void PrintSalary<T>(ISalary<T> s) { s.Pay(); } interface ISalary<T> { void Pay(); }
建議43:讓介面中的泛型引數支援協變
除了上面的方法之外,還可以在泛型宣告加上out關鍵字來支援協變(FCL 4.0)。通過協變,可以使用比宣告的引數派生型別更大的引數。
static void Main(string[] args) { ISalary<Programmer> s = new BaseSalaryCounter<Programmer>(); PrintSalary(s); } private static void PrintSalary(ISalary<Employee> s) { s.Pay(); } interface ISalary<out T> { void Pay(); }
建議44:理解委託中的協變
委託中的泛型變數天然是部分支援協變的,除非考慮到該委託宣告肯定不會用於可變性,否則建議為委託中的泛型引數指定out關鍵字;
public delegate T GetEmployeeHandler<T>(string name); static void Main() { GetEmployeeHandler<Employee> getAEmployee = GetAManager; Employee e = getAEmployee ("Lance"); } static Employee GetAEmployee(string name) { return new Employee() { Name = name }; } static Manager GetAManager(string name) { return new Manager() { Name = name }; }
如果不加out關鍵字的話,下面無法編譯通過
GetEmployeeHandler<Manager> getAManager = GetAManager; GetEmployeeHandler<Employee> getAEmployee = getAManager;
建議45:為泛型型別引數指定逆變
逆變是指方法的引數可以是委託或泛型介面的引數型別的基類。如果不為介面IMyComparable的泛型引數T指定in關鍵字,會導致Test(p,m)編譯錯誤:無法將Programmer轉換為Manager
class Program { static void Main(string[] args) { Programmer p = new Programmer { Name = "Lance" }; Manager m = new Manager { Name = "Yuer" }; Test(p, m); } static void Test<T>(IMyComparable<T> t1,T t2) { t1.Compare(t2); } } interface IMyComparable<in T> { int Compare(T other); }
建議46:顯式釋放資源需繼承介面IDisposable
託管資源:由CLR管理分配和釋放的資源,即從CLR裡new出來的物件
非託管資源:不受CLR管理的物件,如Windows核心物件,或者檔案、套接字、COM等。
建議47:提供析構器隱式清理
在.NET中每次使用new操作符建立物件時,CLR都會為該物件在堆上分配記憶體,一旦這些物件不再被引用,就會回收它們的記憶體。對於沒有繼承IDisposable介面的型別物件,垃圾回收器會直接釋放物件所佔用的記憶體;而對於實現了Dispose模式的型別,在每次建立物件的時候,CLR都會將該物件的一個指標放到終結列表中,垃圾回收器在回收該物件的記憶體前,會首先將終結列表中的指標放到一個freachable queue(可達佇列)中。同時,CLR還會分配專門的執行緒讀取freachable佇列,並呼叫物件的終結器,只有到這個時候,物件才會真正被標識為垃圾,並且在下一次進行垃圾回收時釋放物件佔用的記憶體。 實現Dispose模式的型別物件,起碼要經過兩次垃圾回收才能真正地被回收掉,因為垃圾回收機制會首先安排CLR呼叫終結器。基於這個特點,我們的型別提供了顯式釋放的方法來減少一次垃圾回收,同時也在終結器中提供隱式清理,以避免呼叫者忘記主動呼叫Dispose方法。
建議48:Dispose方法允許多次呼叫
建議49:Dispose模式中提供受保護的虛方法Dispose()
是為了子類能夠重寫實現自己的清理工作,同時呼叫父類的base.Dispose()
建議50:在Dispose模式中應區別對待託管資源和非託管資源
public void Dispose() { Dispose(true); }
// 這表明需要同時處理託管資源和非託管資源 ~SampleClass() {
// 這表明只需要處理非託管資源 Dispose(false); } protected virtual void Dispose(bool disposing) { if (disposed) return;
// 清理託管資源 if(disposing) { if(managedResource!=null) { managedResource.Dispose(); managedResource = null; } } // 清理非託管資源
if (nativeResource != IntPtr.Zero) { Marshal.FreeHGlobal(nativeResource); nativeResource = IntPtr.Zero; } disposed = true;// 讓型別知道自己已經被釋放 }
如果顯示呼叫了Dispose方法,那麼型別就按部就班地將自己的資源全部釋放。如果忘記呼叫Dispose方法(垃圾回收器自動執行終結器也就是解構函式),那麼型別就假定自己的所有託管資源會全部交給垃圾回收器回收,不進行手工處理,只對非託管資源手動回收。
建議51:具有可釋放欄位的型別應該是可釋放的
如果一個普通託管類中包含一個非普通型別的欄位,那麼該普通類也應該實現IDisposable介面,即使該類沒有任何顯示的非託管資源。
建議52:及時釋放資源
垃圾回收器執行垃圾回收有一定的條件,如果完全靠垃圾回收會給程式埋下隱患。
private void btnOpen_Click(object sender,EventArgs e) { FileStream fs = new FileStream(@"C:\test.txt",FileMode.Open); } private void btnGC_Click(object sender,EventArgs e) { GC.Collect(); }
如果FileStream沒有釋放資源之前又點選了按鈕,則會報錯,因為你不知道垃圾回收器什麼時候對FileStream執行回收。