周末空閑,選讀了一下一本很不錯的C#語言使用的書,特此記載下便於對項目代碼進行重構和優化時查看。
Standing On Shoulders of Giants,附上思維導圖,其中標記的顏色越深表示在實際中的實際意義越大。
名稱 | 內容和示例 |
提供API時盡量提供泛型接口 | Public interface IComparable<T>{ int CompareTo(T other) } |
泛型約束盡可能的嚴格並有效 | Public delegate T FactoryFunc<T>(); Public static T Factory<T>( FactoryFunc<T> newT) where T:new() { T t = newt();} |
通過運行時類型檢查具體化泛型算法 | 比如根據不同的集合類型優化相應算法 |
使用泛型強制執行編譯時類型推測 | Public static T ReadFromStream(XmlReader inputStream) { return (T)factory.Deserialize(inputStream) } |
保證自定義泛型類支持可析構的類型參數 | Public sealed class EngineDriver<T>:IDisposable where T:Engine, new() { public void Dispose(){ var resource = driver as IDisposable; if(resource != null) resource.Dispose(); } } |
通過委托在類型參數上定義方法約束 | Public static T Add<T>(T left, T right, Func<T,T,T> addFunc){ return addFunc(right, left); } |
不要在基類和接口上創建具體化的泛型類型 | 盡可能使的基類和接口的適用范圍更加的廣闊 |
推薦使用泛型方法,除非類型參數是實例字段 | Public static T Max<T>(T left, T right) { return Comparer<T>.Default.Compare(left, right) < 0 ? right : left } |
推薦使用泛型的Tuple作為輸出和引用參數 | 當設置方法的返回值,或者在需要使用ref參數的情形時,使用Tuple<>元組可以使代碼更清晰,當然如果參數比較復雜,還是選擇建立對應的DTO類型為宜 |
在泛型接口上增加對應的傳統接口 | 這個在大家基礎架構時非常重要,每個方法均提供泛型版本和object版本,使得代碼有很強的兼容性。 Public static bool CheckEquality(object left, object right) { return left.Equals(right); } Public static bool CheckEquality<T>(T left, T right) where T:IEquatable<T> { return left.Equals(right); } |
名稱 | 內容和示例 |
使用線程池代替創建線程 | 經過微軟的官方測試,由自己調度線程和使用線程池,在每10萬個計算消耗的平均時長比較中,前者所消耗時長為后者三倍,因而選用線程池作為默認多線程處理機制是合理的選擇 Private static double ThreadPoolThreads(int numThreads) { var start = new Stopwatch(); Using(var e = new AutoResetEvent(false)){ int workerThreads = numThreads; start.Start();//watch.ElapsedMilliseconds, watch.Restart(), watch.Stop(); for(var I = 0; I < numThreads; thread++) ThreadPool.QueueUserWorkItem( (x)=>{ // to do If(Interlocked.Decrement(ref workThreads) == 0) { e.Set(); } }); }} |
使用后台工作者組件對象用於處理多線程通信 | 現在已經不再使用后台Worker,而推薦使用Task任務模型替代它,其邏輯為 ![]() |
將lock作為優先級最高的同步原語 | 使用lock相當於使用了Monitor.Enter和Exit,不過要方便很多,使用的是臨界區的概念。Public int TotalNum { get{ lock(syncObj) return total; } set{ lock(syncObj) total++;} } |
Lock中方法體盡可能精簡 | 在使用lock時,一定不要使用lock(this)和lock(typeof(MyType))的形式,這會造成很多的問題,必須保證鎖的對象不是公開無法被外部使用的,常見的對方法加鎖的形式有: 1.使用特性,[MethodImpl(MethodImplOptions.Synchronized)] 2.使用私有變量作為鎖變量 private object syncHandler = new object(); 此外還有一種復雜點的形式如下。 Private object syncHandle; Private object GetSyncHandle(){ InterLocked.CompareExchange(ref syncHandle, new object(), null); } |
避免在臨界區中調用未知代碼 | 比如不要在臨界區中使用事件,因為事件的處理方法由調用方注冊,是未知的,會造成相關的問題,一定要保證臨界區中方法的確定性 |
理解在WinForm和WPF中的跨線程調用 | 做過WinForm編程的親,一定遇到過一個InvalidOperationException,內容為跨線程操作非法,訪問Control的線程不是創建線程,這其實是Winform、WPF等框架對UI的保護,避免多個不同線程修改UI值的情況。這種情況主要有一下三種方式來處理,最推薦的解決方案為第二種。
|
名稱 | 內容和示例 |
為序列創建可組合的API, yield return xxx | Public static IEnumerable<int> Square(IEnumerable<int> nums) { foreach(var num in nums) yield return num * num; } |
通過Action,Predicate,Functions解耦迭代器 | Public static IEnumerable<T> Filter<T>(IEnumerable<T> sequence, Predicate<T> filterFunc) { if(filterFunc(int)) yield return item; } |
根據請求生成序列 | [IEnumerable<int>].TakeWhile(num => num < 5); |
通過Function參數解耦 | Public static T Sum<T>(IEnumerable<T> sequence, T total, Func<T,T,T> accumulator) { foreach(T item in sequence){ total = accumulator(total, item); return total; } } |
創建清晰,最小化,完整的方法組 | 即在提供方法時,盡可能的保證完備性(支持主要的類型) |
推薦定義方法重載操作符 | 還記得在學習C++時,很推薦重載操作符,不過在面向對象語言的今天,使用可讀性更強的方法更合理 |
理解事件是如何增加對象運行時的耦合性 | public event EventHandler<WorkerEventArgs> OnProgress; public void DoLotsOfStuff() { for (var i = 0; i < 100; i++) { SomeWork(); var args = new WorkerEventArgs(); args.Percent = i; //關於這個=,我總是不算特別明白,不過記得是線程安全的代碼 //可以理解為,使用這個,其他調用這個事件的對象就不會被鎖定 var progHandler = OnProgress; if (progHandler != null) { //注意這里的this progHandler(this, args); } if (args.Cancel) return; } } 這里想補充的是,event屬於編譯時解耦,你可以看到,該事件的訂閱者都沒有入侵事件所屬的發布者(發布者-訂閱者默認),但實際上,在運行時,所有的訂閱者其實是和事件緊密關聯在一起的,訂閱者們修改共享數據的操作存在很大的不確定性。簡而言之,事件是編譯時解耦,運行時耦合的。 |
只聲明非虛事件對象 | 在.NET中,事件提供了類似屬性的簡易語法,通過add,remove方法添加相關事件處理程序,其實event就是delegate的包裝器,這個特殊的委托便於應用事件處理模型,同時提供線程安全性。由於事件的運行時耦合性,如果使用虛事件容易造成未知的錯誤, private EventHandler<WorkerEventArgs> progressEvent; public event EventHandler<WorkerEventArgs> OnProgress { [MethodImpl(MethodImplOptions.Synchronized)] add { progressEvent += value; } [MethodImpl(MethodImplOptions.Synchronized)] remove { progressEvent -= value; } } |
通過異常報告方法契約錯誤 | 當出現業務異常流程時,推薦拋出異常而不是使用TryXXX組合的方式,因為這樣代碼更加簡單易懂。當然在與業務無關的,如簡單數據轉換的場景下,使用TryXXX是很好的選擇 |
確定屬性的行為和數據一樣 | 讓屬性盡可能的簡單,不要將復雜邏輯放在屬性,如果需要可以通過提供相應方法的方式,使得代碼更加通俗易懂,且使得調用人堅信屬性的調用不會造成任何的性能影響 |
區分繼承和組合 | 在適當的場景下,用組合代替繼承是常見的代碼設計模式,這樣可以減少類的污染,在選用策略模式的場景下,組合使用的非常的多,常見的形式如下: public interface IContract{ void SampleImplMethod(); } public class MyInnerClass:IContract{ public void SampleImplMethod (){ //elided }} public class MyOuterClass:IContract{ private IContract impl = new MyInnerClass(); public void SampleImplMethod (){ impl.SampleImplMethod(); }} |
名稱 | 內容和示例 |
通過擴展方法擴展接口 | Public static bool LessThan<T>(this T left, T right) where T : IComparable<T> { return left.CompareTo(right) < 0; } |
通過擴展方法增強已經構建的類型 | 這部分很容易理解,比如你使用系統提供的相關類,無法修改源碼(雖然已開源),這時為了代碼的便捷性和可讀性,使用擴展方法增強該類變得非常有效 |
推薦隱式類型的本地變量 | 簡單方便 |
通過匿名類限制類的可見范圍 | 使得代碼的封裝性更好,更加健壯 |
為外部的組件創建可組合的API | 要求提供的API具有更好的健壯性,功能相對完整並獨立,復用性更強,例如盡量不要使用可空類型作為接口參數等 |
避免修改綁定的變量 | 這部分內容涉及閉包,通過以下的例子可以很容易的理解 public void Test() { int index = 0; Func<IEnumerable<int>> sequence = () => Generate(30, () => index ++); index = 20; foreach (var item in sequence()) { Console.WriteLine(item); } } private IEnumerable<int> Generate(int num, Func<int> act) { for (; num > 0; num--) { yield return act(); } } |
在匿名類型上定義本地函數 | public void Test01() { var randomNumbers = new Random(); var sequence = (from x in Generate(100, () => randomNumbers.NextDouble() * 100) let y = randomNumbers.NextDouble() * 100 select new { x, y }).TakeWhile(point => point.x < 75); foreach (var item in sequence) { Console.WriteLine(item); }} |
不要重載擴展方法 | 由於個人創建擴展方法的普遍性和完備性不強,重載此類方法容易降低程序的健壯性 |
名稱 | 內容和示例 |
理解查詢表達式如何映射到方法調用 | 簡單來說,我們所寫的LINQ語句都會先轉化為對應的擴展方法,然后再解析相關的表達式樹最后生成對應語句。 var people = from e in employees where e.Age > 30 orderby e.LastName, e.FirstName, e.Age select e; var people = employees.Where(e=>e.Age > 30).OrderBy(e=>e.LastName).ThenBy(e=>e.FirstName).ThenBy(e=>e.Age); |
推薦Lazy延遲加載查詢 | 延遲加載表示數據到真正使用時再去獲取,這個概念不太容易理解,簡單來說,我們的獲得集合函數調用實際上只是生成相應的查詢語句,但並未實際執行,獲得任何對象,只有在我們對其經行迭代等操作時,才真正的加載數據。這些概念其實都和委托緊密相關,從邏輯上講就是加了一個新的層次,函數本身(可以說是其指針、地址)是一個層次,函數的實際調用又是一個層次,在javascript也有相似的概念,就比如FunctionA和FunctionA()的區別。 Private static IEnumerable<TResult> Generate<TResult>(int number, Func<TResult> generator) { for(var i = 0; i < number; i++) yield return generator(); } 注意到Func<TResult>這個格式沒有,和Task<TResult>何其相似,一個是異步返回值,一個是延遲的返回值,僅僅是一個方便理解的小思路哈。 |
推薦使用lambda表達式代替方法 | 這兒的實際意思是指在使用LINQ時,由於每個查詢的局限性,不推薦在查詢中調用外部方法,而因盡可能通過LINQ自身來完成相應工作,減少各個查詢間的干擾 |
避免在Func和Action中拋出異常 | 這個也很好理解,由於Action等委托常用於集合操作中,而任何一個一場都會中斷整個集合的操作,給集合操作帶來了很大的不確定性,並且在並行運算時更加難以控制,因而在Action中把異常捕獲並處理掉更加的合理。相信大家在job中常常會遇到循環調用的場景,這是通過返回值將相關的異常信息帶回是更合理的處理方式,之后無論是記log還是給相關人發郵件都顯得非常的合理 |
區分預先執行和延遲執行 | 在實際應用時,將正常加載和延遲加載組合使用非常的常見 var method1 = MethodA(); var answer = DoSomething(()=>method1, ()=>MethodB(), ()=>MethodC()); 此外,想說的是,在項目中,比如大部分數據是正常加載,少部分數據使用延遲加載,而一些特殊的場景通過(比如緩存服務器)則使用預熱(預先加載)的方式,弄清這里面的邏輯會讓這部分的應用更加得心應手 |
避免捕獲昂貴的資源 | 之前介紹了C#編譯器如何生成委托和變量是如何在一個閉包的內部被捕獲的,下面是一個簡單的構建閉包的例子 int counter = 0; IEnumerable<int> numbers = Generate(30, ()=>counter++); 其實際生成的代碼如下: private class Closure { public int generatedCounter; public int generatorFunc(){ return generatedCounter ++; } } var c = new Closure(); c.generatedCounter = 0; IEnumerable<int> sequence = Generate(30, new Func<int>(c.generatorFunc)); 通過閉包的形式,我們可以發現其擴展了捕獲對象的生命周期,如果這個捕獲對象是一個昂貴的資源,比如說是個很大的文件流,那么就可能發生內存泄露的情況。因而在委托中使用本地的資源,一定要非常的當心,比較合理的方式是,將你所需要的內容緩存后釋放原始對象。 |
區別IEnumerable和IQueryable的數據源 | 由於IQueryable數據源其實是對IEnumerable數據源的封裝和增強,簡答來說,IQueryable對象的相關數據處理操作的性能要遠高於IEnumerable對象,因而如果實際的返回值為IQueryable對象,那么不要經行相關的轉化,當然也可以通過typeA as IQueryable來嘗試轉化,如果本來就是IQueryable對象則直接返回,反之對其進行封裝后返回 |
通過Single()和First()方法強行控制查詢的語義 | 這個就是讓我們的查詢語句通過語義來指導查詢,盡早的拋出異常 var stus = (from p in Students where p.Score > 60 orderby p.ID select p).Skip(2).First(); |
推薦存儲Expression<.>替代Func<> | 這部分很有意思,當然理解難度也不小,畢竟Expression完全可以實現一個簡單的編譯器了,真心強大。我們所使用的LINQ完全是建立在其上的,這兒只做個很粗略的學習,作為未來加強學習的引子,可以看到,Expression表達式樹是Func的抽象 Expression<Func<int, bool>> IsOdd = val % 2 == 1; Expression<Func<int, bool>> IsLargeNumber = val => val > 99; InvocationExpression callLeft = Expression.Invoke(IsOdd, Expression.Constant(5)); InvocationExpression callRight = Expression.Invoke(IsLargeNumber, Expression.Constant(5)); BinaryExpression Combined = Expression.MakeBinary(ExpressionType.Add, callLeft, callRight); Expression<Func<bool>> typeCombined = Expression.Lamda<Func<bool>>( Combined); Func<bool> compiled = typeCombined.Compile(); Bool result = compiled(); |
名稱 | 內容和示例 |
最小化可空類型的可見性 | 簡單來說,就是減少在公共方法API的輸入參數和輸出返回值中使用可空類型,因而這樣會加大方法的調用難度。當然在內部方法和實體類(包括代碼生成的實體類)中使用還是非常方便有效的 |
給部分類和部分方法建立構造器,設值器和事件處理器 | 這個主題常出現在有代碼生成器出現的場景,比如說使用代碼生成工具生成DAO層,其中只包含最基礎的CRUD操作,當擴展時,我們如果直接修改類文件,那么當下一次數據庫修改,再次生成代碼時就可能出現代碼覆蓋等錯誤,因而在這種情況下我們會考慮使用分布類(說實話分布方法,我自己也沒怎么用過,記得在以前做C++時用過類似external關鍵字引用外部方法的情形,形式上有點像)。這是需要注意的是,工具生成類和擴展類(一般來說類名相同,但文件名加上Ext並放入對應層次文件夾中)的設計,需要仔細考慮默認構造方法、屬性值設置器、事件處理器等類成員的構建。 |
將數組參數限制為參數數組 | 由於數組的不確定性,因而不推薦將數組作為參數(指的是不同類型的數據放入一個object[]中,使得方法的使用非常容易出錯,當然泛型的數據集合等除外),而推薦params的形式來傳遞相應數據,這樣API參數在不存在或者提供null值時也不會報錯。 Private static void Write(params object[] params) { foreach(object o in params) Console.WriteLine(o); } |
避免在構造器中調用虛方法 | 這其實是個很有用的建議,尤其是在構建集成關系復雜的基類及其派生類時,由於子類、父類構造方法調用順序原因,很容造成初始化和賦值的錯誤,用一個簡單的例子來說明這個問題,借用書中的一句原話,"一個對象在其所有構造器執行完成前並沒有完整的被構建" class A { protected A() { MethodA(); } protected virtual void MethodA(){ Console.WriteLine("MethodA in A"); } } class B : A{ private readonly string msg = "set by initializer"; public B(string msg){ this.msg = msg; } protected override void MethodA(){ Console.WriteLine(msg); } } class Program{ static void Main(string[] args){ B b = new B("Constructed in main"); } } 這兒的結果是"set by initializer",首先調用B的構造方法,由於msg是readonly賦值木有成功,然后調用父類無參構造方法,實際調用子類MethodA有以上結果。這部分在實際中我也曾犯過相似的錯誤,需要非常小心。 |
對大對象考慮使用弱引用 | 弱引用的概念接觸的相對較少,實際就是將直接引用轉化為間接引用 Var weakR = new WeakReference(largeObj); largeObj = null; 咋一看,感覺確實不太好明白,這兒的意圖是首先將大對象的引用(指針)放入一個包裝類型,成為弱引用,之后將直接引用對象釋放,這樣就形成弱引用,利於垃圾回收,其使用場景主要針對沒有提供IDispose接口的大對象。說實話,在實際中,我也沒有這樣使用過,之后嘗試后再給大家分享。 |
推薦對易變量和不可序列化的數據使用隱式屬性 | 簡單來說,就是在非Serializable對象中推薦使用priavte set,可以保護數據安全並便於提供驗證等方法。當然在支持序列化時,public的set方法和默認無參的構造函數都是必須的 |
謝謝大家的閱讀,希望自己早日成為一名合格的程序員!
少年辛苦終身事,莫向光陰惰寸功J
參考文獻:
-
[美]Bill, Wagner. More Effective C#[M]. 北京:人民郵電出版社, 2009.