C# 程序员最常犯的 10 个错误
關(guān)于C#
C#是達(dá)成微軟公共語言運(yùn)行庫(CLR)的少數(shù)語言中的一種。達(dá)成CLR的語言可以受益于其帶來的特性,如跨語言集成、異常處理、安全性增強(qiáng)、部件組合的簡(jiǎn)易模型以及調(diào)試和分析服務(wù)。作為現(xiàn)代的CLR語言,C#是應(yīng)用最為廣泛的,其應(yīng)用場(chǎng)景針對(duì)Windows桌面、移動(dòng)手機(jī)以及服務(wù)器環(huán)境等復(fù)雜、專業(yè)的開發(fā)項(xiàng)目。
C#是種面向?qū)ο蟮膹?qiáng)類型語言。C#在編譯和運(yùn)行時(shí)都有的強(qiáng)類型檢查,使在大多數(shù)典型的編程錯(cuò)誤能夠被盡早地發(fā)現(xiàn),而且位置定位相當(dāng)精準(zhǔn)。相比于那些不拘泥類型,在違規(guī)操作很久后才報(bào)出可追蹤到莫名其妙錯(cuò)誤的語言,這可以為程序員節(jié)省很多時(shí)間。然而,許多程序員有意或無意地拋棄了這個(gè)檢測(cè)的有點(diǎn),這導(dǎo)致本文中討論的一些問題。
關(guān)于本文
本文介紹了10種最常見的編程錯(cuò)誤,或是C#程序員要避免的陷阱。
盡管本文中討論的錯(cuò)誤是C#環(huán)境下的,但對(duì)其他達(dá)成CLR或使用框架類庫(FCL)的語言也相關(guān)(FCL)。
常見錯(cuò)誤 #1: 像使用值一樣使用參考或過來用
C++以及許多其他語言的程序員習(xí)慣于控制他們分配給變量的值是否為簡(jiǎn)易的值或現(xiàn)有對(duì)象的引用。在C#中呢,這將由寫該對(duì)象的程序員決定,而不是由實(shí)例化該對(duì)象并對(duì)它進(jìn)行變量賦值的程序員決定。這是新手C#程序員們的共同“問題”。
如果你不知道你正在使用的對(duì)象是否是值類型或引用類型,你可能會(huì)遇到一些驚喜。例如:
| 1 2 3 4 5 6 7 8 9 10 11 | Point point1 = new Point(20, 30); Point point2 = point1; point2.X = 50; Console.WriteLine(point1.X);?????? // 20 (does this surprise you?) Console.WriteLine(point2.X);?????? // 50 Pen pen1 = new Pen(Color.Black); Pen pen2 = pen1; pen2.Color = Color.Blue; Console.WriteLine(pen1.Color);???? // Blue (or does this surprise you?) Console.WriteLine(pen2.Color);???? // Blue |
如你所見,盡管Point和Pen對(duì)象的創(chuàng)建方式相同,但是當(dāng)一個(gè)新的X的坐標(biāo)值被分配到point2時(shí), point1的值保持不變 。而當(dāng)一個(gè)新的color值被分配到pen2,pen1也隨之改變。因此,我們可以推斷point1和point2每個(gè)都包含自己的Point對(duì)象的副本,而pen1和pen2引用了同一個(gè)Pen對(duì)象 。如果沒有這個(gè)測(cè)試,我們?cè)趺茨軌蛑肋@個(gè)原理?
一種辦法是去看一下對(duì)象是如何定義的(在Visual Studio中,你可以把光標(biāo)放在對(duì)象的名字上,并按下F12鍵)
| 1 2 | ??public?struct?Point?{?…?}?????//?defines?a?“value”?type ??public?class?Pen?{?…?}????????//?defines?a?“reference”?type |
如上所示,在C#中,struct關(guān)鍵字是用來定義一個(gè)值類型,而class關(guān)鍵字是用來定義引用類型的。?對(duì)于那些有C++編程背景人來說,如果被C++和C#之間某些類似的關(guān)鍵字搞混,可能會(huì)對(duì)以上這種行為感到很吃驚。
如果你想要依賴的行為會(huì)因值類型和引用類型而異,舉例來說,如果你想把一個(gè)對(duì)象作為參數(shù)傳給一個(gè)方法,并在這個(gè)方法中修改這個(gè)對(duì)象的狀態(tài)。你一定要確保你在處理正確的類型對(duì)象。
常見的錯(cuò)誤#2:誤會(huì)未初始化變量的默認(rèn)值
在C#中,值得類型不能為空。根據(jù)定義,值的類型值,甚至初始化變量的值類型必須有一個(gè)值。這就是所謂的該類型的默認(rèn)值。這通常會(huì)導(dǎo)致以下,意想不到的結(jié)果時(shí),檢查一個(gè)變量是否未初始化:
| 1 2 3 4 5 6 | ??class?Program?{ ??????static?Point?point1;??????static?Pen?pen1;??????static?void?Main(string[]?args)?{ ??????????Console.WriteLine(pen1?==?null);??????//?True ??????????Console.WriteLine(point1?==?null);????//?False?(huh?) ??????} ??} |
為什么不是【point 1】空?答案是,點(diǎn)是一個(gè)值類型,和默認(rèn)值點(diǎn)(0,0)一樣,沒有空值。未能認(rèn)識(shí)到這是一個(gè)非常簡(jiǎn)單和常見的錯(cuò)誤,在C#中
很多(但是不是全部)值類型有一個(gè)【IsEmpty】屬性,你可以看看它等于默認(rèn)值:
| 1 | Console.WriteLine(point1.IsEmpty);????????//?True |
當(dāng)你檢查一個(gè)變量是否已經(jīng)初始化,確保你知道值未初始化是變量的類型,將會(huì)在默認(rèn)情況下,不為空值。
常見錯(cuò)誤?#3: 使用不恰當(dāng)或未指定的方法比較字符串
在C#中有很多方法來比較字符串。
雖然有不少程序員使用==操作符來比較字符串,但是這種方法實(shí)際上是最不推薦使用的。主要原因是由于這種方法沒有在代碼中顯示的指定使用哪種類型去比較字符串。
相反,在C#中判斷字符串是否相等最好使用Equals方法:
| 1 | ??public?bool?Equals(string?value);??public?bool?Equals(string?value,?StringComparison?comparisonType); |
第一個(gè)Equals方法(沒有comparisonType這參數(shù))和使用==操作符的結(jié)果是一樣的,但好處是,它顯式的指明了比較類型。它會(huì)按順序逐字節(jié)的去比較字符串。在很多情況下,這正是你所期望的比較類型,尤其是當(dāng)比較一些通過編程設(shè)置的字符串,像文件名,環(huán)境變量,屬性等。在這些情況下,只要按順序逐字節(jié)的比較就可以了。使用不帶comparisonType參數(shù)的Equals方法進(jìn)行比較的唯一一點(diǎn)不好的地方在于那些讀你程序代碼的人可能不知道你的比較類型是什么。
使用帶comparisonType的Equals方法去比較字符串,不僅會(huì)使你的代碼更清晰,還會(huì)使你去考慮清楚要用哪種類型去比較字符串。這種方法非常值得你去使用,因?yàn)楸M管在英語中,按順序進(jìn)行的比較和按語言區(qū)域進(jìn)行的比較之間并沒有太多的區(qū)別,但是在其他的一些語種可能會(huì)有很大的不同。如果你忽略了這種可能性,無疑是為你自己在未來的道路上挖了很多“坑”。舉例來說:
| 1 2 3 4 5 6 7 8 9 10 11 12 | ??string?s?=?"strasse"; ??? ??//?outputs?False: ??Console.WriteLine(s?==?"straße"); ??Console.WriteLine(s.Equals("straße")); ??Console.WriteLine(s.Equals("straße",?StringComparison.Ordinal)); ??Console.WriteLine(s.Equals("Straße",?StringComparison.CurrentCulture));???????? ??Console.WriteLine(s.Equals("straße",?StringComparison.OrdinalIgnoreCase)); ??? ??//?outputs?True: ??Console.WriteLine(s.Equals("straße",?StringComparison.CurrentCulture)); ??Console.WriteLine(s.Equals("Straße",?StringComparison.CurrentCultureIgnoreCase)); |
最安全的實(shí)踐是總是為Equals方法提供一個(gè)comparisonType的參數(shù)。
下面是一些基本的指導(dǎo)原則:
當(dāng)比較用戶輸入的字符串或者將字符串比較結(jié)果展示給用戶時(shí),使用本地化的比較(CurrentCulture?或者CurrentCultureIgnoreCase)。
當(dāng)用于程序設(shè)計(jì)的比較字符串時(shí),使用原始的比較(Ordinal?或者?OrdinalIgnoreCase)
InvariantCulture和InvariantCultureIgnoreCase一般并不使用,除非在受限的情境之下,因?yàn)樵嫉谋容^通常效率更高。如果與本地文化相關(guān)的比較是必不可少的,它應(yīng)該被執(zhí)行成基于當(dāng)前的文化或者另一種特殊文化的比較。
此外,對(duì)Equals?方法來說,字符串也通常提供了Compare方法,可以提供字符串的相對(duì)順序信息而不僅僅中測(cè)試是否相等。這個(gè)方法可以很好適用于<,?<=,?>和>=?運(yùn)算符,對(duì)上述討論同樣適用。
常見誤區(qū) #4: 使用迭代式 (而不是聲明式)的語句去操作集合
在C# 3.0中,LINQ的引入改變了我們以往對(duì)集合對(duì)象的查詢和修改操作。從這以后,你應(yīng)該用LINQ去操作集合,而不是通過迭代的方式。
一些C#的程序員甚至都不知道LINQ的存在,好在不知道的人正在逐步減少。但是還有些人誤以為L(zhǎng)INQ只用在數(shù)據(jù)庫查詢中,因?yàn)長(zhǎng)INQ的關(guān)鍵字和SQL語句實(shí)在是太像了。
雖然數(shù)據(jù)庫的查詢操作是LINQ的一個(gè)非常典型的應(yīng)用,但是它同樣可以應(yīng)用于各種可枚舉的集合對(duì)象。(如:任何實(shí)現(xiàn)了IEnumerable接口的對(duì)象)。舉例來說,如果你有一個(gè)Account類型的數(shù)組,不要寫成下面這樣:
| 1 2 3 4 | ??decimal?total?=?0;??foreach?(Account?account?in?myAccounts)?{????if?(account.Status?==?"active")?{ ??????total?+=?account.Balance; ????} ??} |
你只要這樣寫:
| 1 2 3 | ??decimal?total?=?(from?account?in?myAccounts ???????????????where?account.Status?==?"active" ??????????????? select?account.Balance).Sum(); |
雖然這是一個(gè)很簡(jiǎn)單的例子,在有些情況下,一個(gè)單一的LINQ語句可以輕易地替換掉你代碼中一個(gè)迭代循環(huán)(或嵌套循環(huán))里的幾十條語句。更少的代碼通常意味著產(chǎn)生Bug的機(jī)會(huì)也會(huì)更少地被引入。然而,記住,在性能方面可能要權(quán)衡一下。在性能很關(guān)鍵的場(chǎng)景,尤其是你的迭代代碼能夠?qū)δ愕募线M(jìn)行假設(shè)時(shí),LINQ做不到,所以一定要在這兩種方法之間比較一下性能。
#5常見錯(cuò)誤:在LINQ語句之中沒有考慮底層對(duì)象
對(duì)于處理抽象操縱集合任務(wù),LINQ無疑是龐大的。無論他們是在內(nèi)存的對(duì)象,數(shù)據(jù)庫表,或者XML文檔。在如此一個(gè)完美世界之中,你不需要知道底層對(duì)象。然而在這兒的錯(cuò)誤是假設(shè)我們生活在一個(gè)完美世界之中。事實(shí)上,相同的LINQ語句能返回不同的結(jié)果,當(dāng)在精確的相同數(shù)據(jù)上執(zhí)行時(shí),如果該數(shù)據(jù)碰巧在一個(gè)不同的格式之中。
例如,請(qǐng)考慮下面的語句:
| 1 2 3 | decimal?total=(from?accout?in?myaccouts where?accout.status==‘a(chǎn)ctive" ???????????????????select?accout?.Balance).sum(); |
想象一下,該對(duì)象之一的賬號(hào)會(huì)發(fā)生什么。狀態(tài)等于“有效的”(注意大寫A)?
好吧,如果myaccout是Dbset的對(duì)象。(默認(rèn)設(shè)置了不同區(qū)分大小寫的配置),where表達(dá)式仍會(huì)匹配該元素。然而,如果myaccout是在內(nèi)存陣列之中,那么它將不匹配,因此將產(chǎn)生不同的總的結(jié)果。
等一會(huì),在我們之前討論過的字符串比較中, 我們看見 == 操作符扮演的角色就是簡(jiǎn)單的比較. 所以,為什么在這個(gè)條件下, == 表現(xiàn)出的是另外的一個(gè)形式呢 ?
答案是,當(dāng)在LINQ語句中的基礎(chǔ)對(duì)象都引用到SQL表中的數(shù)據(jù)(如與在這個(gè)例子中,在實(shí)體框架為DbSet的對(duì)象的情況下),該語句被轉(zhuǎn)換成一個(gè)T-SQL語句。然后遵循的T-SQL的規(guī)則,而不是C#的規(guī)則,所以在上述情況下的比較結(jié)束是不區(qū)分大小寫的。
一般情況下,即使LINQ是一個(gè)有益的和一致的方式來查詢對(duì)象的集合,在現(xiàn)實(shí)中你還需要知道你的語句是否會(huì)被翻譯成什么比C#的引擎或者是其他表達(dá),來確保您的代碼的行為將如預(yù)期在運(yùn)行時(shí)。
常見錯(cuò)誤 #6:對(duì)擴(kuò)展方法感到困惑或者被它的形式欺騙
如同先前提到的,LINQ狀態(tài)依賴于IEnumerable接口的實(shí)現(xiàn)對(duì)象,比如,下面的簡(jiǎn)單函數(shù)會(huì)合計(jì)帳戶集合中的帳戶余額:
| 1 2 | ??public?decimal?SumAccounts(IEnumerable<Account>?myAccounts)?{??????return?myAccounts.Sum(a?=>?a.Balance); ??} |
在上面的代碼中,myAccounts參數(shù)的類型被聲明為IEnumerable<Account>,myAccounts引用了一個(gè)Sum?方法 (C# 使用類似的 “dot notation” 引用方法或者接口中的類),我們期望在IEnumerable<T>接口中定義一個(gè)Sum()方法。但是,IEnumerable<T>沒有為Sum方法提供任何引用并且只有如下所示的簡(jiǎn)潔定義:
| 1 2 3 | ?public?interface?IEnumerable<out?T>?:?IEnumerable?{ ??????IEnumerator<T>?GetEnumerator(); ??} |
但是Sum方法應(yīng)該定義到何處?C#是強(qiáng)類型的語言,因此如果Sum方法的引用是無效的,C#編譯器會(huì)對(duì)其報(bào)錯(cuò)。我們知道它必須存在,但是應(yīng)該在哪里呢?此外,LINQ提供的供查詢和聚集結(jié)果所有方法在哪里定義呢?
答案是Sum并不在IEnumerable接口內(nèi)定義,而是一個(gè)
定義在System.Linq.Enumerable類中的static方法(叫做“extension method”)
| 1 2 3 4 5 6 7 8 | ?namespace?System.Linq?{ ????public?static?class?Enumerable?{??????... ??????//?the?reference?here?to?“this?IEnumerable<TSource>?source”?is ??????//?the?magic?sauce?that?provides?access?to?the?extension?method?Sum ??????public?static?decimal?Sum<TSource>(this?IEnumerable<TSource>?source, ?????????????????????????????????????????Func<TSource,?decimal>?selector);??????... ????} ??} |
可是擴(kuò)展方法和其它靜態(tài)方法有什么不同之處,是什么確保我們可以在其它類訪問它?
擴(kuò)展方法的顯著特點(diǎn)是第一個(gè)形參前的this修飾符。這就是編譯器知道它是一個(gè)擴(kuò)展方法的“奧妙”。它所修飾的參數(shù)的類型(這個(gè)例子中的IEnumerable<TSource>)說明這個(gè)類或者接口將顯得實(shí)現(xiàn)了這個(gè)方法。
(另外需要指出的是,定義擴(kuò)展方法的IEnumerable接口和Enumerable類的名字間的相似性沒什么奇怪的。這種相似性只是隨意的風(fēng)格選擇。)
理解了這一點(diǎn),我們可以看到上面介紹的sumAccounts方法能以下面的方式實(shí)現(xiàn):
| 1 2 3 | ??public?decimal?SumAccounts(IEnumerable<Account>?myAccounts)?{ ??????return?Enumerable.Sum(myAccounts,?a?=>?a.Balance); ??} |
事實(shí)上我們可能已經(jīng)這樣實(shí)現(xiàn)了這個(gè)方法,而不是問什么要有擴(kuò)展方法。擴(kuò)展方法本身只是C#的一個(gè)方便你無需繼承、重新編譯或者修改原始代碼就可以給已存的在類型“添加”方法的方式。
擴(kuò)展方法通過在文件開頭添加using [namespace];引入到作用域。你需要知道你要找的擴(kuò)展方法所在的名字空間。如果你知道你要找的是什么,這點(diǎn)很容易。
當(dāng)C#編譯器碰到一個(gè)對(duì)象的實(shí)例調(diào)用了一個(gè)方法,并且它在這個(gè)對(duì)象的類中找不到那個(gè)方法,它就會(huì)嘗試在作用域中所有的擴(kuò)展方法里找一個(gè)匹配所要求的類和方法簽名的。如果找到了,它就把實(shí)例的引用當(dāng)做第一個(gè)參數(shù)傳給那個(gè)擴(kuò)展方法,然后如果有其它參數(shù)的話,再把它們依次傳入擴(kuò)展方法。(如果C#編譯器沒有在作用域中找到相應(yīng)的擴(kuò)展方法,它會(huì)拋措。)
對(duì)C#編譯器來說,擴(kuò)展方法是個(gè)“語法糖”,使我們能把代碼寫得更清晰,更易于維護(hù)(多數(shù)情況下)。顯然,前提是你知道它的用法,否則,它會(huì)比較容易讓人迷惑,尤其是一開始。
應(yīng)用擴(kuò)展方法確實(shí)有優(yōu)勢(shì),但也會(huì)讓那些對(duì)它不了解或者認(rèn)識(shí)不正確的開發(fā)者頭疼,浪費(fèi)時(shí)間。尤其是在看在線示例代碼,或者其它已經(jīng)寫好的代碼的時(shí)候。當(dāng)這些代碼產(chǎn)生編譯錯(cuò)誤(因?yàn)樗{(diào)用了那些顯然沒在被調(diào)用類型中定義的方法),一般的傾向是考慮代碼是否應(yīng)用于所引用類庫的其它版本,甚至是不同的類庫。很多時(shí)間會(huì)被花在找新版本,或者被認(rèn)為“丟失”的類庫上。
在擴(kuò)展方法的名字和類中定義的方法的名字一樣,只是在方法簽名上有微小差異的時(shí)候,甚至那些熟悉擴(kuò)展方法的開發(fā)者也偶爾犯上面的錯(cuò)誤。很多時(shí)間會(huì)被花在尋找“不存在”的拼寫錯(cuò)誤上。
在C#中,用擴(kuò)展方法變得越來越流行。除了LINQ,在另外兩個(gè)出自微軟現(xiàn)在被廣泛使用的類庫Unity Application Block和Web API framework中,也應(yīng)用了擴(kuò)展方法,而且還有很多其它的。框架越新,用擴(kuò)展方法的可能性越大。
當(dāng)然,你也可以寫你自己的擴(kuò)展方法。但是必須意識(shí)到雖然擴(kuò)展方法看上去和其它實(shí)例方法一樣被調(diào)用,但這實(shí)際只是幻。事實(shí)上,擴(kuò)展方法不能訪問所擴(kuò)展類的私有和保護(hù)成員,所以它不能被當(dāng)做傳統(tǒng)繼承的替代品。
常見錯(cuò)誤?#7: 對(duì)手頭上的任務(wù)使用錯(cuò)誤的集合類型
C#提供了大量的集合類型的對(duì)象,下面只列出了其中的一部分:
Array,ArrayList,BitArray,BitVector32,Dictionary<K,V>,HashTable,HybridDictionary,List<T>,NameValueCollection,OrderedDictionary,Queue, Queue<T>,SortedList,Stack, Stack<T>,StringCollection,StringDictionary.
但是在有些情況下,有太多的選擇和沒有足夠的選擇一樣糟糕,集合類型也是這樣。數(shù)量眾多的選擇余地肯定可以保證是你的工作正常運(yùn)轉(zhuǎn)。但是你最好還是花一些時(shí)間提前搜索并了解一下集合類型,以便選擇一個(gè)最適合你需要的集合類型。這最終會(huì)使你的程序性能更好,減少出錯(cuò)的可能。
如果有一個(gè)集合指定的元素類型(如string或bit)和你正在操作的一樣,你最好優(yōu)先選擇使用它。當(dāng)指定對(duì)應(yīng)的元素類型時(shí),這種集合的效率更高。
為了利用好C#中的類型安全,你最好選擇使用一個(gè)泛型接口,而不是使用非泛型的借口。泛型接口中的元素類型是你在在聲明對(duì)象時(shí)指定的類型,而非泛型中的元素是object類型。當(dāng)使用一個(gè)非泛型的接口時(shí),C#的編譯器不能對(duì)你的代碼進(jìn)行類型檢查。同樣,當(dāng)你在操作原生類型的集合時(shí),使用非泛型的接口會(huì)導(dǎo)致C#對(duì)這些類型進(jìn)行頻繁的裝箱(boxing)和拆箱(unboxing)操作。和使用指定了合適類型的泛型集合相比,這會(huì)帶來很明顯的性能影響。
另一個(gè)常見的陷阱是自己去實(shí)現(xiàn)一個(gè)集合類型。這并不是說永遠(yuǎn)不要這樣做,你可以通過使用或擴(kuò)展.NET提供的一些被廣泛使用的集合類型來節(jié)省大量的時(shí)間,而不是去重復(fù)造輪子。?特別是,C#的C5 Generic Collection Library?和CLI提供了很多額外的集合類型,像持久化樹形數(shù)據(jù)結(jié)構(gòu),基于堆的優(yōu)先級(jí)隊(duì)列,哈希索引的數(shù)組列表,鏈表等以及更多。
常見錯(cuò)誤#8:遺漏資源釋放
CLR 托管環(huán)境扮演了垃圾回收器的角色,所以你不需要顯式釋放已創(chuàng)建對(duì)象所占用的內(nèi)存。事實(shí)上,你也不能顯式釋放。C#中沒有與C++?delete對(duì)應(yīng)的運(yùn)算符或者與C語言中free()函數(shù)對(duì)應(yīng)的方法。但這并不意味著你可以忽略所有的使用過的對(duì)象。許多對(duì)象類型封裝了許多其它類型的系統(tǒng)資源(例如,磁盤文件,數(shù)據(jù)連接,網(wǎng)絡(luò)端口等等)。保持這些資源使用狀態(tài)會(huì)急劇耗盡系統(tǒng)的資源,削弱性能并且最終導(dǎo)致程序出錯(cuò)。
盡管所有C#的類中都定義了析構(gòu)方法,但是銷毀對(duì)象(C#中也叫做終結(jié)器)可能存在的問題是你不確定它們時(shí)候會(huì)被調(diào)用。他們?cè)谖磥硪粋€(gè)不確定的時(shí)間被垃圾回收器調(diào)用(一個(gè)異步的線程,此舉可能引發(fā)額外的并發(fā))。試圖避免這種由垃圾回收器中GC.Collect()方法所施加的強(qiáng)制限制并非一種好的編程實(shí)踐,因?yàn)榭赡茉诶厥站€程試圖回收適宜回收的對(duì)象時(shí),在不可預(yù)知的時(shí)間內(nèi)致使線程阻塞。
這并意味著最好不要用終結(jié)器,顯式釋放資源并不會(huì)導(dǎo)致其中的任何一個(gè)后果。當(dāng)你打開一個(gè)文件、網(wǎng)絡(luò)端口或者數(shù)據(jù)連接時(shí),當(dāng)你不再使用這些資源時(shí),你應(yīng)該盡快的顯式釋放這些資源。
資源泄露幾乎在所有的環(huán)境中都會(huì)引發(fā)關(guān)注。但是,C#提供了一種健壯的機(jī)制使資源的使用變得簡(jiǎn)單。如果合理利用,可以大增減少泄露出現(xiàn)的機(jī)率。NET framework定義了一個(gè)IDisposable接口,僅由一個(gè)Dispose()構(gòu)成。任何實(shí)現(xiàn)IDisposable的接口的對(duì)象都會(huì)在對(duì)象生命周期結(jié)束調(diào)用Dispose()方法。調(diào)用結(jié)果明確而且決定性的釋放占用的資源。
如果在一個(gè)代碼段中創(chuàng)建并釋放一個(gè)對(duì)象,卻忘記調(diào)用Dispose()方法,這是不可原諒的,因?yàn)镃#提供了using語句以確保無論代碼以什么樣的方式退出,Dispose()方法都會(huì)被調(diào)用(不管是異常,return語句,或者簡(jiǎn)單的代碼段結(jié)束)。這個(gè)using和之前提到的在文件開頭用來引入名字空間的一樣。它有另外一個(gè)很多C#開發(fā)者都沒有察覺的,完全不相關(guān)的目的,也就是確保代碼退出時(shí),對(duì)象的Dispose()方法被調(diào)用:
| 1 2 3 | ??using?(FileStream?myFile?=?File.OpenRead("foo.txt"))?{ ????myFile.Read(buffer,?0,?100); ??} |
在上面示例中使用using語句,你就可以確定myFile.Dispose()方法會(huì)在文件使用完之后被立即調(diào)用,不管Read()方法有沒有拋異常。
常見錯(cuò)誤?#9: 回避異常
C#在運(yùn)行時(shí)也會(huì)強(qiáng)制進(jìn)行類型檢查。相對(duì)于像C++這樣會(huì)給錯(cuò)誤的類型轉(zhuǎn)換賦一個(gè)隨機(jī)值的語言來說,C#這可以使你更快的找到出錯(cuò)的位置。然而,程序員再一次無視了C#的這一特性。由于C#提供了兩種類型檢查的方式,一種會(huì)拋出異常,而另一種則不會(huì),這很可能會(huì)使他們掉進(jìn)這個(gè)“坑”里。有些程序員傾向于回避異常,并且認(rèn)為不寫 try/catch 語句可以節(jié)省一些代碼。
例如,下面演示了C#中進(jìn)行顯示類型轉(zhuǎn)換的兩種不同的方式:
| 1 2 3 4 5 6 7 | ??//?方法?1: ??//?如果?account?不能轉(zhuǎn)換成?SavingAccount?會(huì)拋出異常 ??SavingsAccount?savingsAccount?=?(SavingsAccount)account; ??? ??//?方法?2: ??//?如果不能轉(zhuǎn)換,則不會(huì)拋出異常,相反,它會(huì)返回?null ??SavingsAccount?savingsAccount?=?account?as?SavingsAccount; |
很明顯,如果不對(duì)方法2返回的結(jié)果進(jìn)行判斷的話,最終很可能會(huì)產(chǎn)生一個(gè) NullReferenceException 的異常,這可能會(huì)出現(xiàn)在稍晚些的時(shí)候,這使得問題更難追蹤。對(duì)比來說,方法1會(huì)立即拋出一個(gè)?InvalidCastExceptionmaking,這樣,問題的根源就很明顯了。
此外,即使你知道要對(duì)方法2的返回值進(jìn)行判斷,如果你發(fā)現(xiàn)值為空,接下來你會(huì)怎么做?在這個(gè)方法中報(bào)告錯(cuò)誤合適嗎?如果類型轉(zhuǎn)換失敗了你還有其他的方法去嘗試嗎?如果沒有的話,那么拋出這個(gè)異常是唯一正確的選擇,并且異常的拋出點(diǎn)離其發(fā)生點(diǎn)越近越好。
下面的例子演示了其他一組常見的方法,一種會(huì)拋出異常,而另一種則不會(huì):
| 1 2 3 4 5 | ??int.Parse();?????//?如果參數(shù)無法解析會(huì)拋出異常 ??int.TryParse();??//?返回bool值表示解析是否成功 ??? ??IEnumerable.First();???????????//?如果序列為空,則拋出異常 ??IEnumerable.FirstOrDefault();??//?如果序列為空則返回?null?或默認(rèn)值 |
有些程序員認(rèn)為“異常有害”,所以他們自然而然的認(rèn)為不拋出異常的程序顯得更加“高大上”。雖然在某些情況下,這種觀點(diǎn)是正確的,但是這種觀點(diǎn)并不適用于所有的情況。
舉個(gè)具體的例子,某些情況下當(dāng)異常產(chǎn)生時(shí),你有另一個(gè)可選的措施(如,默認(rèn)值),那么,選用不拋出異常的方法是一個(gè)比較好的選擇。在這種情況下,你最好像下面這樣寫:
| 1 2 3 | ??if?(int.TryParse(myString,?out?myInt))?{????//?use?myInt ??}?else?{????//?use?default?value ??} |
而不是這樣:
| 1 2 3 4 | ??try?{ ????myInt?=?int.Parse(myString);????//?use?myInt ??}?catch?(FormatException)?{????//?use?default?value ??} |
但是,這并不說明 TryParse 方法更好。某些情況下適合,某些情況下則不適合。這就是為什么有兩種方法供我們選擇了。根據(jù)你的具體情況選擇合適的方法,并記住,作為一個(gè)開發(fā)者,異常是完全可以成為你的朋友的。
常見錯(cuò)誤 #10: 累積編譯器警告而不處理
這個(gè)錯(cuò)誤并不是C#所特有的,但是在C#中這種情況卻比較多,尤其是從C#編譯器棄用了嚴(yán)格的類型檢查之后。
警告的出現(xiàn)是有原因的。所有C#的編譯錯(cuò)誤都表明你的代碼有缺陷,同樣,一些警告也是這樣。這兩者之間的區(qū)別在于,對(duì)于警告來說,編譯器可以按照你代碼的指示工作,但是,編譯器發(fā)現(xiàn)你的代碼有一點(diǎn)小問題,很有可能會(huì)使你的代碼不能按照你的預(yù)期運(yùn)行。
一個(gè)常見的例子是,你修改了你的代碼,并移除了對(duì)某些變量的使用,但是,你忘了移除該變量的聲明。程序可以很好的運(yùn)行,但是編譯器會(huì)提示有未使用的變量。程序可以很好的運(yùn)行使得一些程序員不去修復(fù)警告。更有甚者,有些程序員很好的利用了Visual Studio中“錯(cuò)誤列表”窗口的隱藏警告的功能,很容易的就把警告過濾了,以便專注于錯(cuò)誤。不用多長(zhǎng)時(shí)間,就會(huì)積累一堆警告,這些警告都被“愜意”的忽略了(更糟的是,隱藏掉了)。
但是,如果你忽略掉這一類的警告,類似于下面這個(gè)例子遲早會(huì)出現(xiàn)在你的代碼中。
| 1 2 3 4 5 6 7 8 9 | ??class?Account?{ ??? ??????int?myId;??????int?Id;???//?編譯器已經(jīng)警告過了,但是你不聽 ??? ??????//?Constructor ??????Account(int?id)?{??????????this.myId?=?Id;?????//?OOPS! ??????} ??? ??} |
再加上使用了編輯器的智能感知的功能,這種錯(cuò)誤就很有可能發(fā)生。
現(xiàn)在,你的代碼中有了一個(gè)嚴(yán)重的錯(cuò)誤(但是編譯器只是輸出了一個(gè)警告,其原因已經(jīng)解釋過),這會(huì)浪費(fèi)你大量的時(shí)間去查找這錯(cuò)誤,具體情況由你的程序復(fù)雜程度決定。如果你一開始就注意到了這個(gè)警告,你只需要5秒鐘就可以修改掉,從而避免這個(gè)問題。
記住,如果你仔細(xì)看的話,你會(huì)發(fā)現(xiàn),C#編譯器給了你很多關(guān)于你程序健壯性的有用的信息。不要忽略警告。你只需花幾秒鐘的時(shí)間就可以修復(fù)它們,當(dāng)出現(xiàn)的時(shí)候就去修復(fù)它,這可以為你節(jié)省很多時(shí)間。試著為自己培養(yǎng)一種“潔癖”,讓Visual Studio 的“錯(cuò)誤窗口”一直顯示“0錯(cuò)誤, 0警告”,一旦出現(xiàn)警告就感覺不舒服,然后即刻把警告修復(fù)掉。
當(dāng)然了,任何規(guī)則都有例外。所以,有些時(shí)候,雖然你的代碼在編譯器看來是有點(diǎn)問題的,但是這正是你想要的。在這種很少見的情況下,你最好使用?#pragma warning disable [warning id] 把引發(fā)警告的代碼包裹起來,而且只包裹警告ID對(duì)應(yīng)的代碼。這會(huì)且只會(huì)壓制對(duì)應(yīng)的警告,所以當(dāng)有新的警告產(chǎn)生的時(shí)候,你還是會(huì)知道的。.
總結(jié)
C#是一門強(qiáng)大的并且很靈活的語言,它有很多機(jī)制和語言規(guī)范來顯著的提高你的生產(chǎn)力。和其他語言一樣,如果對(duì)它能力的了解有限,這很可能會(huì)給你帶來阻礙,而不是好處。正如一句諺語所說的那樣“knowing enough to be dangerous”(譯者注:意思是自以為已經(jīng)了解足夠了,可以做某事了,但其實(shí)不是)。
熟悉C#的一些關(guān)鍵的細(xì)微之處,像本文中所提到的那些(但不限于這些),可以幫助我們更好的去使用語言,從而避免一些常見的陷阱。
原文地址:http://www.toptal.com/c-sharp/top-10-mistakes-that-c-sharp-programmers-make
轉(zhuǎn)載于:https://www.cnblogs.com/gc2013/p/3737221.html
總結(jié)
以上是生活随笔為你收集整理的C# 程序员最常犯的 10 个错误的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 【struts2】action中使用通配
- 下一篇: Jira 6.0.5的详细安装及汉化授权