C#法式員最易犯的編程毛病。本站提示廣大學習愛好者:(C#法式員最易犯的編程毛病)文章只能為提供參考,不一定能成為您想要的結果。以下是C#法式員最易犯的編程毛病正文
本文引見了10種最多見的編程毛病,或是C#法式員要防止的圈套。
罕見毛病1: 像應用值一樣應用參考或過去用
C++和很多其他說話的法式員習氣於掌握他們分派給變量的值能否為簡略單純的值或現有對象的援用。在C#中呢,這將由寫該對象的法式員決議,而不是由實例化該對象並對它停止變量賦值的法式員決議。這是老手C#法式員們的配合“成績”。
假如你不曉得你正在應用的對象能否是值類型或援用類型,你能夠會碰到一些欣喜。例如:
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對象的創立方法雷同,然則當一個新的X的坐標值被分派到point2時, point1的值堅持不變 。而當一個新的color值被分派到pen2,pen1也隨之轉變。是以,我們可以揣摸point1和point2每一個都包括本身的Point對象的正本,而pen1和pen2援用了統一個Pen對象 。假如沒有這個測試,我們怎樣可以或許曉得這個道理?
一種方法是去看一下對象是若何界說的(在Visual Studio中,你可以把光標放在對象的名字上,並按下F12鍵)
public struct Point { … } // defines a “value” type public class Pen { … } // defines a “reference” type
如上所示,在C#中,struct症結字是用來界說一個值類型,而class症結字是用來界說援用類型的。 關於那些有C++編程配景人來講,假如被C++和C#之間某些相似的症結字弄混,能夠會對以上這類行動覺得很受驚。
假如你想要依附的行動會因值類型和援用類型而異,舉例來講,假如你想把一個對象作為參數傳給一個辦法,並在這個辦法中修正這個對象的狀況。你必定要確保你在處置准確的類型對象。
罕見的毛病2:誤解未初始化變量的默許值
在C#中,值得類型不克不及為空。依據界說,值的類型值,乃至初始化變量的值類型必需有一個值。這就是所謂的該類型的默許值。這平日會招致以下,意想不到的成果時,檢討一個變量能否未初始化:
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】空?謎底是,點是一個值類型,和默許值點(0,0)一樣,沒有空值。未能熟悉到這是一個異常簡略和罕見的毛病,在C#中
許多(然則不是全體)值類型有一個【IsEmpty】屬性,你可以看看它等於默許值:
Console.WriteLine(point1.IsEmpty); // True
當你檢討一個變量能否曾經初始化,確保你曉得值未初始化是變量的類型,將會在默許情形下,不為空值。
罕見毛病3: 應用不適當或未指定的辦法比擬字符串
在C#中有許多辦法來比擬字符串。
固然有很多法式員應用==操作符來比擬字符串,然則這類辦法現實上是最不推舉應用的。重要緣由是因為這類辦法沒有在代碼中顯示的指定應用哪一種類型去比擬字符串。
相反,在C#中斷定字符串能否相等最好應用Equals辦法:
public bool Equals(string value); public bool Equals(string value, StringComparison comparisonType);
第一個Equals辦法(沒有comparisonType這參數)和應用==操作符的成果是一樣的,但利益是,它顯式的指清楚明了比擬類型。它會按次序逐字節的去比擬字符串。在許多情形下,這恰是你所希冀的比擬類型,特別是當比擬一些經由過程編程設置的字符串,像文件名,情況變量,屬性等。在這些情形下,只需按次序逐字節的比擬便可以了。應用不帶comparisonType參數的Equals辦法停止比擬的獨一一點欠好的處所在於那些讀你法式代碼的人能夠不曉得你的比擬類型是甚麼。
應用帶comparisonType的Equals辦法去比擬字符串,不只會使你的代碼更清楚,還會使你去斟酌清晰要用哪一種類型去比擬字符串。這類辦法異常值得你去應用,由於雖然在英語中,按次序停止的比擬和案語言區域停止的比擬之間並沒有太多的差別,然則在其他的一些語種能夠會有很年夜的分歧。假如你疏忽了這類能夠性,無疑是為你本身在將來的途徑上挖了許多“坑”。舉例來講:
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));
最平安的理論是老是為Equals辦法供給一個comparisonType的參數。
上面是一些根本的指點准繩:
當比擬用戶輸出的字符串或許將字符串比擬成果展現給用戶時,應用當地化的比擬(CurrentCulture 或許CurrentCultureIgnoreCase)。
當用於法式設計的比擬字符串時,應用原始的比擬(Ordinal 或許 OrdinalIgnoreCase)
InvariantCulture和InvariantCultureIgnoreCase普通其實不應用,除非在受限的情境之下,由於原始的比擬平日效力更高。假如與當地文明相干的比擬是必弗成少的,它應當被履行成基於以後的文明或許另外一種特別文明的比擬。
另外,對Equals 辦法來講,字符串也平日供給了Compare辦法,可以供給字符串的絕對次序信息而不只僅中測試能否相等。這個辦法可以很好實用於<, <=, >和>= 運算符,對上述評論辯論異樣實用。
罕見誤區4: 應用迭代式 (而不是聲明式)的語句去操作聚集
在C# 3.0中,LINQ的引入轉變了我們以往對聚集對象的查詢和修正操作。從這今後,你應當用LINQ去操作聚集,而不是經由過程迭代的方法。
一些C#的法式員乃至都不曉得LINQ的存在,好在不曉得的人正在慢慢削減。然則還有些人誤認為LINQ只用在數據庫查詢中,由於LINQ的症結字和SQL語句其實是太像了。
固然數據庫的查詢操作是LINQ的一個異常典范的運用,然則它異樣可以運用於各類可列舉的聚集對象。(如:任何完成了IEnumerable接口的對象)。舉例來講,假如你有一個Account類型的數組,不要寫成上面如許:
decimal total = 0; foreach (Account account in myAccounts) { if (account.Status == "active") { total += account.Balance; } }
你只需如許寫:
decimal total = (from account in myAccounts where account.Status == "active" select account.Balance).Sum();
固然這是一個很簡略的例子,在有些情形下,一個單一的LINQ語句可以隨意馬虎地調換失落你代碼中一個迭代輪回(或嵌套輪回)裡的幾十條語句。更少的代碼平日意味著發生Bug的機遇也會更少地被引入。但是,記住,在機能方面能夠要衡量一下。在機能很症結的場景,特別是你的迭代代碼可以或許對你的聚集停止假定時,LINQ做不到,所以必定要在這兩種辦法之間比擬一下機能。
罕見毛病:在LINQ語句當中沒有斟酌底層對象
關於處置籠統把持聚集義務,LINQ無疑是宏大的。不管他們是在內存的對象,數據庫表,或許XML文檔。在如斯一個完善世界當中,你不須要曉得底層對象。但是在這兒的毛病是假定我們生涯在一個完善世界當中。現實上,雷同的LINQ語句能前往分歧的成果,當在准確的雷同數據上履行時,假如該數據恰巧在一個分歧的格局當中。
例如,請斟酌上面的語句:
decimal total=(from accout in myaccouts where accout.status==‘active" select accout .Balance).sum();
想象一下,該對象之一的賬號會產生甚麼。狀況等於“有用的”(留意年夜寫A)?
好吧,假如myaccout是Dbset的對象。(默許設置了分歧辨別年夜小寫的設置裝備擺設),where表達式仍會婚配該元素。但是,假如myaccout是在內存陣列當中,那末它將不婚配,是以將發生分歧的總的成果。
等一會,在我們之前評論辯論過的字符串比擬中, 我們看見 == 操作符飾演的腳色就是簡略的比擬. 所以,為何在這個前提下, == 表示出的是別的的一個情勢呢 ?
謎底是,當在LINQ語句中的基本對象都援用到SQL表中的數據(如與在這個例子中,在實體框架為DbSet的對象的情形下),該語句被轉換成一個T-SQL語句。然後遵守的T-SQL的規矩,而不是C#的規矩,所以在上述情形下的比擬停止是不辨別年夜小寫的。
普通情形下,即便LINQ是一個無益的和分歧的方法來查詢對象的聚集,在實際中你還須要曉得你的語句能否會被翻譯成甚麼比C#的引擎或許是其他表達,來確保您的代碼的行動將如預期在運轉時。
罕見毛病6:對擴大辦法覺得迷惑或許被它的情勢誘騙
好像先條件到的,LINQ狀況依附於IEnumerable接口的完成對象,好比,上面的簡略函數匯合算帳戶聚集中的帳戶余額:
public decimal SumAccounts(IEnumerable<Account> myAccounts) { return myAccounts.Sum(a => a.Balance); }
在下面的代碼中,myAccounts參數的類型被聲明為IEnumerable<Account>,myAccounts援用了一個Sum 辦法 (C# 應用相似的 “dot notation” 援用辦法或許接口中的類),我們希冀在IEnumerable<T>接口中界說一個Sum()辦法。然則,IEnumerable<T>沒無為Sum辦法供給任何援用而且只要以下所示的簡練界說:
public interface IEnumerable<out T> : IEnumerable { IEnumerator<T> GetEnumerator(); }
然則Sum辦法應當界說到何處?C#是強類型的說話,是以假如Sum辦法的援用是有效的,C#編譯器會對其報錯。我們曉得它必需存在,然則應當在哪裡呢?另外,LINQ供給的供查詢和集合成果一切辦法在哪裡界說呢?
謎底是Sum其實不在IEnumerable接口內界說,而是一個界說在System.Linq.Enumerable類中的static辦法(叫做“extension method”)
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); ... } }
可是擴大辦法和其它靜態辦法有甚麼分歧的地方,是甚麼確保我們可以在其它類拜訪它?
擴大辦法的明顯特色是第一個形參前的this潤飾符。這就是編譯器曉得它是一個擴大辦法的“奇妙”。它所潤飾的參數的類型(這個例子中的IEnumerable<TSource>)解釋這個類或許接口將顯得完成了這個辦法。
(別的須要指出的是,界說擴大辦法的IEnumerable接口和Enumerable類的名字間的類似性沒甚麼奇異的。這類類似性只是隨便的作風選擇。)
懂得了這一點,我們可以看到下面引見的sumAccounts辦法能以上面的方法完成:
public decimal SumAccounts(IEnumerable<Account> myAccounts) { return Enumerable.Sum(myAccounts, a => a.Balance); }
現實上我們能夠曾經如許完成了這個辦法,而不是問甚麼要有擴大辦法。擴大辦法自己只是C#的一個便利你無需繼續、從新編譯或許修正原始代碼便可以給已存的在類型“添加”辦法的方法。
擴大辦法經由過程在文件開首添加using [namespace];引入到感化域。你須要曉得你要找的擴大辦法地點的名字空間。假如你曉得你要找的是甚麼,這點很輕易。
當C#編譯器碰著一個對象的實例挪用了一個辦法,而且它在這個對象的類中找不到誰人辦法,它就會測驗考試在感化域中一切的擴大辦法裡找一個婚配所請求的類和辦法簽名的。假如找到了,它就把實例的援用當作第一個參數傳給誰人擴大辦法,然後假如有其它參數的話,再把它們順次傳入擴大辦法。(假如C#編譯器沒有在感化域中找到響應的擴大辦法,它會拋措。)
對C#編譯器來講,擴大辦法是個“語法糖”,使我們能把代碼寫得更清楚,更容易於保護(多半情形下)。明顯,條件是你曉得它的用法,不然,它會比擬輕易讓人困惑,特別是一開端。
運用擴大辦法確切有優勢,但也會讓那些對它不懂得或許熟悉不准確的開辟者頭疼,糟蹋時光。特別是在看在線示例代碼,或許其它曾經寫好的代碼的時刻。當這些代碼發生編譯毛病(由於它挪用了那些明顯沒在被挪用類型中界說的辦法),普通的偏向是斟酌代碼能否運用於所援用類庫的其它版本,乃至是分歧的類庫。許多時光會被花在找新版本,或許被以為“喪失”的類庫上。
在擴大辦法的名字和類中界說的辦法的名字一樣,只是在辦法簽名上有渺小差別的時刻,乃至那些熟習擴大辦法的開辟者也偶然犯下面的毛病。許多時光會被花在尋覓“不存在”的拼寫毛病上。
在C#中,用擴大辦法變得愈來愈風行。除LINQ,在別的兩個出自微軟如今被普遍應用的類庫Unity Application Block和Web API framework中,也運用了擴大辦法,並且還有許多其它的。框架越新,用擴大辦法的能夠性越年夜。
固然,你也能夠寫你本身的擴大辦法。然則必需認識到固然擴大辦法看上去和其它實例辦法一樣被挪用,但這現實只是幻。現實上,擴大辦法不克不及拜訪所擴大類的公有和掩護成員,所以它不克不及被當作傳統繼續的替換品。
罕見毛病7: 敵手頭上的義務應用毛病的聚集類型
C#供給了年夜量的聚集類型的對象,上面只列出了個中的一部門:
Array,ArrayList,BitArray,BitVector32,Dictionary<K,V>,HashTable,HybridDictionary,List<T>,NameValueCollection,OrderedDictionary,Queue, Queue<T>,SortedList,Stack, Stack<T>,StringCollection,StringDictionary.
然則在有些情形下,有太多的選擇和沒有足夠的選擇一樣蹩腳,聚集類型也是如許。數目浩瀚的選擇余地確定可以包管是你的任務正常運轉。然則你最好照樣花一些時光提早搜刮並懂得一下聚集類型,以便選擇一個最合適你須要的聚集類型。這終究會使你的法式機能更好,削減失足的能夠。
假如有一個聚集指定的元素類型(如string或bit)和你正在操作的一樣,你最好優先選擇應用它。當指定對應的元素類型時,這類聚集的效力更高。
為了應用好C#中的類型平安,你最好選擇應用一個泛型接口,而不是應用非泛型的托言。泛型接口中的元素類型是你在在聲明對象時指定的類型,而非泛型中的元素是object類型。當應用一個非泛型的接口時,C#的編譯器不克不及對你的代碼停止類型檢討。異樣,當你在操作原生類型的聚集時,應用非泛型的接口會招致C#對這些類型停止頻仍的裝箱(boxing)和拆箱(unboxing)操作。和應用指定了適合類型的泛型聚集比擬,這會帶來很顯著的機能影響。
另外一個罕見的圈套是本身去完成一個聚集類型。這其實不是說永久不要如許做,你可以經由過程應用或擴大.NET供給的一些被普遍應用的聚集類型來節儉年夜量的時光,而不是去反復造輪子。 特殊是,C#的C5 Generic Collection Library 和CLI供給了許多額定的聚集類型,像耐久化樹形數據構造,基於堆的優先級隊列,哈希索引的數組列表,鏈表等和更多。
罕見毛病8:漏掉資本釋放
CLR 托管情況飾演了渣滓收受接管器的腳色,所以你不須要顯式釋放已創立對象所占用的內存。現實上,你也不克不及顯式釋放。C#中沒有與C++ delete對應的運算符或許與C說話中free()函數對應的辦法。但這其實不意味著你可以疏忽一切的應用過的對象。很多對象類型封裝了很多其它類型的體系資本(例如,磁盤文件,數據銜接,收集端口等等)。堅持這些資本應用狀況會急劇耗盡體系的資本,減弱機能而且終究招致法式失足。
雖然一切C#的類中都界說了析構辦法,然則燒毀對象(C#中也叫做終結器)能夠存在的成績是你不肯定它們時刻會被挪用。他們在將來一個不肯定的時光被渣滓收受接管器挪用(一個異步的線程,此舉能夠激發額定的並發)。試圖防止這類由渣滓收受接管器中GC.Collect()辦法所施加的強迫限制並不是一種好的編程理論,由於能夠在渣滓收受接管線程試圖收受接管合適收受接管的對象時,在弗成預知的時光內導致線程壅塞。
這並意味著最好不要用終結器,顯式釋放資本其實不會招致個中的任何一個效果。當你翻開一個文件、收集端口或許數據銜接時,當你不再應用這些資本時,你應當盡快的顯式釋放這些資本。
資本洩漏簡直在一切的情況中都邑激發存眷。然則,C#供給了一種硬朗的機制使資本的應用變得簡略。假如公道應用,可以年夜增削減洩漏湧現的機率。NET framework界說了一個IDisposable接口,僅由一個Dispose()組成。任何完成IDisposable的接口的對象都邑在對象性命周期停止挪用Dispose()辦法。挪用成果明白並且決議性的釋放占用的資本。
假如在一個代碼段中創立並釋放一個對象,卻忘卻挪用Dispose()辦法,這是弗成諒解的,由於C#供給了using語句以確保不管代碼以甚麼樣的方法加入,Dispose()辦法都邑被挪用(不論是異常,return語句,或許簡略的代碼段停止)。這個using和之條件到的在文件開首用來引入名字空間的一樣。它有別的一個許多C#開辟者都沒有發覺的,完整不相干的目標,也就是確保代碼加入時,對象的Dispose()辦法被挪用:
using (FileStream myFile = File.OpenRead("foo.txt")) { myFile.Read(buffer, 0, 100); }
在下面示例中應用using語句,你便可以肯定myFile.Dispose()辦法會在文件應用完以後被立刻挪用,不論Read()辦法有無拋異常。
罕見毛病9: 躲避異常
C#在運轉時也會強迫停止類型檢討。絕對於像C++如許會給毛病的類型轉換賦一個隨機值的說話來講,C#這可使你更快的找到失足的地位。但是,法式員再一次疏忽了C#的這一特征。因為C#供給了兩品種型檢討的方法,一種會拋出異常,而另外一種則不會,這極可能會使他們失落進這個“坑”裡。有些法式員偏向於躲避異常,而且以為不寫 try/catch 語句可以節儉一些代碼。
例如,上面演示了C#中停止顯示類型轉換的兩種分歧的方法:
// 辦法 1: // 假如 account 不克不及轉換成 SavingAccount 會拋出異常 SavingsAccount savingsAccount = (SavingsAccount)account; // 辦法 2: // 假如不克不及轉換,則不會拋出異常,相反,它會前往 null SavingsAccount savingsAccount = account as SavingsAccount;
很顯著,假如纰謬辦法2前往的成果停止斷定的話,終究極可能會發生一個 NullReferenceException 的異常,這能夠會湧現在稍晚些的時刻,這使得成績更難追蹤。比較來講,辦法1會立刻拋出一個 InvalidCastExceptionmaking,如許,成績的本源就很顯著了。
另外,即便你曉得要對辦法2的前往值停止斷定,假如你發明值為空,接上去你會怎樣做?在這個辦法中申報毛病適合嗎?假如類型轉換掉敗了你還有其他的辦法去測驗考試嗎?假如沒有的話,那末拋出這個異常是獨一准確的選擇,而且異常的拋出點離其產生點越近越好。
上面的例子演示了其他一組罕見的辦法,一種會拋出異常,而另外一種則不會:
int.Parse(); // 假如參數沒法解析會拋出異常 int.TryParse(); // 前往bool值表現解析能否勝利 IEnumerable.First(); // 假如序列為空,則拋出異常 IEnumerable.FirstOrDefault(); // 假如序列為空則前往 null 或默許值
有些法式員以為“異常無害”,所以他們天然而然的以為不拋出異常的法式顯得加倍“嵬峨上”。固然在某些情形下,這類不雅點是准確的,然則這類不雅點其實不實用於一切的情形。
舉個詳細的例子,某些情形下當異常發生時,你有另外一個可選的辦法(如,默許值),那末,選用不拋出異常的辦法是一個比擬好的選擇。在這類情形下,你最似乎上面如許寫:
if (int.TryParse(myString, out myInt)) { // use myInt } else { // use default value } 而不是如許: try { myInt = int.Parse(myString); // use myInt } catch (FormatException) { // use default value }
然則,這其實不解釋 TryParse 辦法更好。某些情形下合適,某些情形下則不合適。這就是為何有兩種辦法供我們選擇了。依據你的詳細情形選擇適合的辦法,並記住,作為一個開辟者,異常是完整可以成為你的同伙的。
罕見毛病10: 積累編譯器正告而不處置
這個毛病其實不是C#所獨有的,然則在C#中這類情形卻比擬多,特別是從C#編譯器棄用了嚴厲的類型檢討以後。
正告的湧現是有緣由的。一切C#的編譯毛病都注解你的代碼出缺陷,異樣,一些正告也是如許。這二者之間的差別在於,關於正告來講,編譯器可以依照你代碼的指導任務,然則,編譯器發明你的代碼有一點小成績,很有能夠會使你的代碼不克不及依照你的預期運轉。
一個罕見的例子是,你修正了你的代碼,並移除對某些變量的應用,然則,你忘了移除該變量的聲明。法式可以很好的運轉,然則編譯器會提醒有未應用的變量。法式可以很好的運轉使得一些法式員不去修復正告。更有甚者,有些法式員很好的應用了Visual Studio中“毛病列表”窗口的隱蔽正告的功效,很輕易的就把正告過濾了,以便專注於毛病。不消多長時光,就會積聚一堆正告,這些正告都被“舒服”的疏忽了(更糟的是,隱蔽失落了)。
然則,假如你疏忽失落這一類的正告,相似於上面這個例子早晚會湧現在你的代碼中。
class Account { int myId; int Id; // 編譯器曾經正告過了,然則你不聽 // Constructor Account(int id) { this.myId = Id; // OOPS! } }
再加上應用了編纂器的智能感知的功效,這類毛病就很有能夠產生。
如今,你的代碼中有了一個嚴重的毛病(然則編譯器只是輸入了一個正告,其緣由曾經說明過),這會糟蹋你年夜量的時光去查找這毛病,詳細情形由你的法式龐雜水平決議。假如你一開端就留意到了這個正告,你只須要5秒鐘便可以修正失落,從而防止這個成績。
記住,假如你細心看的話,你會發明,C#編譯器給了你許多關於你法式硬朗性的有效的信息。不要疏忽正告。你只需花幾秒鐘的時光便可以修復它們,當湧現的時刻就去修復它,這可認為你節儉許多時光。試著為本身造就一種“潔癖”,讓Visual Studio 的“毛病窗口”一向顯示“0毛病, 0正告”,一旦湧現正告就感到不舒暢,然後即刻把正告修復失落。
固然了,任何規矩都有破例。所以,有些時刻,固然你的代碼在編譯器看來是有點成績的,然則這恰是你想要的。在這類很少見的情形下,你最好應用 #pragma warning disable [warning id] 把激發正告的代碼包裹起來,並且只包裹正告ID對應的代碼。這會且只會壓抑對應的正告,所以當有新的正告發生的時刻,你照樣會曉得的。.
C#是一門壯大的而且很靈巧的說話,它有許多機制和說話標准來明顯的進步你的臨盆力。和其他說話一樣,假如對它才能的懂得無限,這極可能會給你帶來障礙,而不是利益。正如一句諺語所說的那樣“knowing enough to be dangerous”(譯者注:意思是自認為曾經懂得足夠了,可以做某事了,但其實不是)。
熟習C#的一些症結的纖細的地方,像本文中所提到的那些(但不限於這些),可以贊助我們更好的去應用說話,從而防止一些罕見的圈套。