以下為《編寫(xiě)高質(zhì)量代碼:改善C#程序的157個(gè)建議》作者【陸陸敏技】的讀書(shū)總結(jié)捉捅,添加了筆者自己的理解或示例。
首先簡(jiǎn)述幾個(gè)概念:
FCL:(Framework Class Library)即Framework類庫(kù)习劫。
基元類型:.NET 中咆瘟,編譯器直接支持的數(shù)據(jù)類型稱為基元類型(primitive type).基元類型和.NET框架類型(FCL)中的類型有直接的映射關(guān)系,例如:在C#中诽里,int直接映射為System.Int32類型袒餐。
[編譯器]直接支持的類型。
sbyte / byte / short / ushort /int / uint / long / ulong / char / float / double / bool
友情提示:Linq配合Lambda谤狡,會(huì)讓你的Code簡(jiǎn)潔許多匿乃,這也是C#發(fā)展歷史上非常重要的升級(jí)之一
C# 3.0 版
string str = "海瀾"+666.ToString();
比string str = "海瀾"+666;
效率高,少了一次666值類型的裝箱豌汇。string.Format和StringBuilder對(duì)于字符串的拼接效率更高幢炸。(string.Format方法在內(nèi)部使用StringBuilder進(jìn)行字符串的格式化)類型轉(zhuǎn)換盡量使用FCL中自帶的轉(zhuǎn)換方式。例如:
int.TryParse("123")
as比is的效率高拒贱,as只需要做一次類型兼容和一次null檢查宛徊,null檢查要比類型兼容檢查快(因?yàn)閕s為true還要進(jìn)行as操作)。但是as操作符不能操作基元類型逻澳,需要通過(guò)is進(jìn)行判斷,例如:
obj is int
TryParse比Parse好闸天,主要好在兩點(diǎn):1.不會(huì)引發(fā)異常。2.轉(zhuǎn)換成功時(shí)TryParse比Parse性能有略微提升斜做。轉(zhuǎn)換失敗時(shí)苞氮,TryParse比Parse性能提升600倍左右。
對(duì)于這種
int i =-1;
-1代表未賦值的魔數(shù)(又稱魔法值)瓤逼,使用Nullable (可空類型)是一個(gè)不錯(cuò)的選擇笼吟。const 是天然的 static库物,編譯期常量,所以使用const 變量效率會(huì)高贷帮。readonly僅僅在構(gòu)造函數(shù)中可賦值或多次賦值戚揭。
將枚舉的默認(rèn)值設(shè)置為0.
避免給枚舉類型的元素提供顯式的值。例如
enum Week{Monday = 1,Tuesday = 2}
習(xí)慣重載運(yùn)算符
c = a + b;
比c= a.add(b);
要好創(chuàng)建對(duì)象時(shí)需要考慮是否實(shí)現(xiàn)比較器撵枢。不過(guò)筆者更喜歡一行Lambda配合Linq梭哈民晒。
區(qū)別對(duì)待==和Equals,或明確指出這是引用相等
Object.ReferenceEquals
重寫(xiě)Equals時(shí)也要重寫(xiě)GetHashCode并確保Hash值相等锄禽,例如:
(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType.FullName + "_" + this.IDCode).GetHashCode();
潜必。因?yàn)殒I值對(duì)集合,是根據(jù)Key 的HashCode來(lái)查找Value沃但。詳細(xì)請(qǐng)見(jiàn):Object.GetHashCode 方法為頻繁打印的類重寫(xiě)ToString刮便。
正確實(shí)現(xiàn)淺拷貝和深拷貝(序列化與反序列化)。
使用dynamic來(lái)簡(jiǎn)化反射實(shí)現(xiàn)
元素個(gè)數(shù)小且高頻訪問(wèn)的集合用數(shù)組绽慈,頻繁更改長(zhǎng)度的集合用List<T>
多數(shù)情況下使用foreach進(jìn)行循環(huán)遍歷,并且會(huì)自動(dòng)將代碼置入try-catch塊辈毯,若類型實(shí)現(xiàn)了IDispose接口坝疼,它會(huì)在循環(huán)結(jié)束后自動(dòng)調(diào)用Dispose方法。
foreach不能代替for谆沃,因?yàn)閒oreach使用迭代器進(jìn)行集合遍歷钝凶,foreach在FCL提供的迭代器內(nèi)部維護(hù)了一個(gè)對(duì)集合版本的控
制,任何增刪操作都會(huì)是版本號(hào)+1唁影,一旦在遍歷時(shí)MoveNext中檢測(cè)版本號(hào)有變化耕陷,就會(huì)拋出異常。使用更有效的對(duì)象和集合初始化据沈。例如:
var pTemp = from p in personList2 selectt new {p.Name,AgeScope = p.Age>20?"Old":"Young"};
使用泛型集合代替非泛型集合哟沫。例如:IList<T>代替ArrayList,泛型集合是在原有基礎(chǔ)之上的優(yōu)化锌介,這也是命名空間System.Collections(非泛型)和System.Collections.Generic(泛型)這種父子關(guān)系的原因嗜诀。
選擇正確的集合,每種集合都有他們的優(yōu)缺點(diǎn)孔祸,善用它們隆敢。詳見(jiàn):Unity 之?dāng)?shù)據(jù)集合解析
確保集合的線程安全。使用lock鎖或線程安全集合(如:
System.Collections.Concurrent命名空間下的ConcurrentDictionary
)是一個(gè)不錯(cuò)的選擇崔慧,當(dāng)然最好的解決方案就是沒(méi)有線程競(jìng)爭(zhēng)拂蝎。是如果需要自定義集合類,繼承IList<T>比List<T>要好惶室,盡量使用面向接口編程温自。
迭代器應(yīng)該是只讀的玄货。
如果類型的屬性中有集合屬性,那么應(yīng)該保證屬性對(duì)象是由類型本身產(chǎn)生的捣作。例如:
var school = new School(new List<Student>{...})比 school.setList(外部產(chǎn)生的集合)
要好誉结,因?yàn)橥獠慨a(chǎn)生的集合不可控。使用匿名類型存儲(chǔ)并配合Linq查詢券躁,會(huì)讓你的程序更加靈活惩坑。【前提與你協(xié)作的同事也能看懂】
在查詢中使用Lambda表達(dá)式也拜∫允妫【前提與你協(xié)作的同事也能看懂】
理解延遲求職和主動(dòng)求值之間的區(qū)別。延遲一切能延遲的慢哈。例如:一個(gè)類型中某個(gè)成員的初始化或者賦值蔓钟,只有在用到這個(gè)成員的時(shí)候,才進(jìn)行這些操作卵贱。
本地查詢用IEnumerabel<T>,數(shù)據(jù)庫(kù)查詢用IQueryable
使用Linq取代集合中的比較器和迭代器
在Linq查詢中避免不必要的迭代滥沫。例如:有時(shí)
(from c in list where c.Age>=20 select c).First()比f(wàn)rom c in list where c.Age==20 select c
要效率的多總是優(yōu)先考慮泛型〖悖可以有效的避免裝箱拆箱或重復(fù)代碼兰绣。
避免在泛型類型中聲明靜態(tài)成員。例如:
MyList<int>與 MyList< float>
中都含有static int count编振;
這很令人迷惑缀辩。為泛型參數(shù)設(shè)定約束。 添加約束的泛型產(chǎn)生的作用會(huì)更大踪央,例如
where T:Component
臀玄,所以T類型就具備了組件的性質(zhì)。使用default為泛型類型變量指定初始值畅蹂。不用擔(dān)心返回是值類型還是引用類型的困擾健无。
使用FCL中的委托聲明。Action液斜、Funtion讓代碼看上去更整潔統(tǒng)一睬涧。
使用Lambda表達(dá)式代替方法和匿名方法。
Action tempAction = ()=>{Debug.Log("菜鳥(niǎo)海瀾");}
更整潔小心閉包中的陷阱旗唁。例如:尤其是for循環(huán)中的局部
i
變量畦浓。委托的實(shí)質(zhì)就是一個(gè)含有方法引用的類。
使用event關(guān)鍵字為委托施加保護(hù)
實(shí)現(xiàn)標(biāo)準(zhǔn)的事件模型检疫。
public delegate void EvenHandler(object sender,EventArgs e)
使用泛型參數(shù)兼容泛型接口的不可變性讶请。如果按照如下示例調(diào)用
TestFuction(tempSon);
就會(huì)報(bào)錯(cuò):CS1503 C# 參數(shù) 1: 無(wú)法從“Base<Son>”轉(zhuǎn)換為“IBase<Father>”
,換句話說(shuō):編輯器認(rèn)為IBase<Father>
與IBase<Father>
沒(méi)有任何關(guān)系
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class NewBehaviourScript : MonoBehaviour
{
void Start()
{
Base<Son> tempSon = new Base<Son>();
TestFuction(tempSon);//報(bào)錯(cuò)
TestFuctionGeneric(tempSon);//正確
}
public void TestFuction(IBase<Father> parameter)
{
}
public void TestFuctionGeneric<T>(IBase<T> parameter)
{
}
public Father GetFather()
{
return new Son(); //Son是Fatrher的子類
}
}
public interface IBase<T>
{
void Play();
}
public class Base<T> : IBase<T>
{
public void Play()
{
}
}
public class Father { }
public class Son : Father { }
介紹幾個(gè)概念
協(xié)變:讓返回值類型返回比聲明的類型派生程度更大的類型,就是“協(xié)變”。例如:
public Father GetFather()
{
return new Son(); //Son是Fatrher的子類
}
實(shí)際上夺溢,只要泛型類型參數(shù)在一個(gè)接口聲明中不被用來(lái)作為方法的輸入?yún)?shù)论巍,我們都可姑且把它看成是“返回值”類型的。
讓接口中的泛型參數(shù)支持協(xié)變风响,也就是如果需要讓第42條技巧編譯通過(guò)嘉汰。需要為接口添加out關(guān)鍵字
public interface IBase<out T>
理解委托中的協(xié)變 協(xié)變和逆變 (C#)
為泛型類型參數(shù)指定逆變 協(xié)變和逆變 (C#)
顯示釋放資源需繼承接口IDisposable。補(bǔ)充兩個(gè)概念状勤,托管資源: 由CLR管理分配和釋放的資源鞋怀,即從CLR里new出來(lái)的對(duì)象。非托管資源:不受CLR管理的對(duì)象持搜,如windows內(nèi)核對(duì)象密似,或者文件、套接字等葫盼。
即使提供了顯示釋放方法残腌,也應(yīng)該在終結(jié)器(析構(gòu)函數(shù))中提供隱式清理(防止忘記)
Dispose方法應(yīng)允許被多次調(diào)用。因?yàn)槟銦o(wú)法保證調(diào)用者真的只會(huì)調(diào)用一次贫导。
在Dispose模式中應(yīng)提取一個(gè)受保護(hù)的虛方法抛猫,也就是在
public void Dispose()
中調(diào)用protected virtual void Dispose(bool disposing)
在Dispose模式中應(yīng)區(qū)別對(duì)待托管資源和非托管資源。也就是
protected virtual void Dispose(bool disposing)
如果是參數(shù)是True則清理托管和非托管資源孩灯,如果是False則只清理非托管資源闺金。托管資源由CLR自行清理。具有可釋放字段的類型或擁有本機(jī)資源的類型應(yīng)該是可釋放的钱反。可以理解為匣距,一個(gè)類A中持有類B面哥,在類B中有需要釋放的非托管資源,所以在類A中的Dispose要有釋放類B中非托管資源的操作毅待。
及時(shí)釋放資源尚卫,避免不要的資源浪費(fèi)或資源爭(zhēng)用(FileStream)
必要時(shí)應(yīng)將不再使用的對(duì)象引用賦值null,局部變量設(shè)置null毫無(wú)意義尸红,因?yàn)闊o(wú)論是否設(shè)置為null它都會(huì)被回收吱涉。而靜態(tài)字段如果不設(shè)置為null,則無(wú)法被回收外里。
為無(wú)用字段標(biāo)注不可序列化怎爵。[NonSerialized]
利用定特性減少可序列化的字段。如:OnDeserializedAttribute盅蝗、OnDeserializingAttribute鳖链、OnSerializedAttribute、OnSerializingAttribute墩莫。
使用繼承ISerializable接口更靈活地控制序列化過(guò)程(完全客制化定制序列化方式)芙委。
實(shí)現(xiàn)ISerializable的子類型應(yīng)負(fù)責(zé)父類的序列化逞敷。
用拋出異常代替返回錯(cuò)誤代碼。也就是用catch Exception代替 return errorMsg;并且在catch塊中僅僅是發(fā)送異常灌侣,并不處理異常推捐。
不要在不恰當(dāng)?shù)膱?chǎng)合下引發(fā)異常。一般在以下三種情況下才引發(fā)異常侧啼,對(duì)于可控(系統(tǒng)資源仍可用牛柒,資源狀態(tài)可恢復(fù))的錯(cuò)誤,根據(jù)情況自行處理慨菱,不要引發(fā)異常焰络。
- 第一類情況 如果運(yùn)行代碼后會(huì)造成內(nèi)存泄漏、資源不可用符喝,或者應(yīng)用程序狀態(tài)不可恢復(fù)闪彼,則引發(fā)異常。
- 第二類情況 在捕獲異常的時(shí)候协饲,如果需要包裝一些更有用的信息畏腕,則引發(fā)異常
- 第三類情況 如果底層異常在高層操作的上下文中沒(méi)有意義,則可以考慮捕獲這些底層異常茉稠,并引發(fā)新的有意義的異常
重新引發(fā)異常時(shí)使用 Inner Exception
避免在finally內(nèi)撰寫(xiě)無(wú)效代碼描馅。 補(bǔ)充說(shuō)明
避免嵌套異常,因?yàn)闀?huì)覆蓋掉原本有用的堆棧信息而线。
避免“吃掉”異常,這里的“吃掉”指的是需要捕獲有意義的異常铭污。
為循環(huán)增加Tester-Doer模式而不是將try-catch置于環(huán)內(nèi),Try-Parse 模式和Tester-Doer模式是兩種替代拋異常的優(yōu)化方式膀篮,起到優(yōu)化設(shè)計(jì)性能的作用嘹狞。
總是處理未捕獲的異常。未捕獲異常通常就是運(yùn)行時(shí)期的Bug誓竿,我們可以在AppDomain.CurrentDomain.UnhandledException的注冊(cè)事件方法CurrentDomain_UnhandledException中磅网,將未捕獲的異常信息記錄在日志中。UnhandledException提供的機(jī)制并不能阻止應(yīng)用程序終止筷屡,也就是說(shuō)涧偷,執(zhí)行CurrentDomain_UnhandledException方法后,應(yīng)用程序就會(huì)終止毙死。
正確捕獲多線程中的異常燎潮,例如:
- 正確
Thread t = new Thread((ThreadStart)delegate
{
try
{
throw new Exception("多線程異常");
}
catch (Exception error)
{
MessageBox.Show("工作線程異常:" + error.Message + Environment.NewLine + error.StackTrace);
}
});
t.Start();
- 錯(cuò)誤
try
{
Thread t = new Thread((ThreadStart)delegate
{
throw new Exception("多線程異常");
});
t.Start();
}
catch (Exception error)
{
MessageBox.Show(error.Message + Environment.NewLine + error.StackTrace);
}
慎用自定義異常
從System.Exception或其他常見(jiàn)的基本異常中派生異常
應(yīng)使用finally避免資源泄漏
避免在調(diào)用棧較低的位置記錄異常
區(qū)分異步和多線程應(yīng)用場(chǎng)景
在線程同步中使用信號(hào)量
避免鎖定不恰當(dāng)?shù)耐?/p>
警惕線程的IsBackgroud
警惕線程不會(huì)立即啟動(dòng)
警惕線程的優(yōu)先級(jí)
正確停止線程
應(yīng)避免線程數(shù)量過(guò)多
使用ThreadPool或BackgroundWorker代替Thread
用Task代替ThreadPool
使用Parallel 簡(jiǎn)化同步狀態(tài)Task的使用
Paralle簡(jiǎn)化但不等同于Task默認(rèn)行為
小心Parallel中的陷阱
使用PLINQ,LINQ最基本的功能就是對(duì)集合進(jìn)行遍歷查詢扼倘,并在此基礎(chǔ)上對(duì)元素進(jìn)行操作跟啤。仔細(xì)推敲會(huì)發(fā)現(xiàn),并行編程簡(jiǎn)直就是專門(mén)為這一類應(yīng)用準(zhǔn)備的。因此隅肥,微軟專門(mén)為L(zhǎng)INQ拓展了一個(gè)類ParallelEnumerable(該類型也在命名空間System.Linq中)竿奏,它所提供的擴(kuò)展方法會(huì)讓LINQ支持并行計(jì)算,這就是所謂的PLINQ腥放。
Task中的異常處理泛啸。 詳細(xì)說(shuō)明
Parallel中的異常處理
static void Main(string[] args)
{
try
{
var parallelExceptions = new ConcurrentQueue<Exception>();
Parallel.For(0, 1, (i) =>
{
try
{
throw new InvalidOperationException("并行任務(wù)中出現(xiàn)的異常");
}
catch (Exception e)
{
parallelExceptions.Enqueue(e);
}
if (parallelExceptions.Count > 0)
throw new AggregateException(parallelExceptions);
});
}
catch (AggregateException err)
{
foreach (Exception item in err.InnerExceptions)
{
Console.WriteLine("異常類型:{0}{1}來(lái)自:
{2}{3}異常內(nèi)容:{4}", item.InnerException.GetType(),
Environment.NewLine, item.InnerException.Source,
Environment.NewLine, item.InnerException.Message);
}
}
Console.WriteLine("主線程馬上結(jié)束");
Console.ReadKey();
}
- 區(qū)分WPE和WinForm的線程模型(untiy可忽略)
- 并行并不總是速度更快
- 在并行方法體中謹(jǐn)慎使用鎖,因?yàn)橛捎阪i的存在秃症,系統(tǒng)的開(kāi)銷也增加了候址,同步帶來(lái)的線程上下文切換,使我們犧牲了CPU時(shí)間與空間性能