C# 程式設計師最常犯的 10 個錯誤
關於C#
C#是達成微軟公共語言執行庫(CLR)的少數語言中的一種。達成CLR的語言可以受益於其帶來的特性,如跨語言整合、異常處理、安全性增強、部件組合的簡易模型以及除錯和分析服務。作為現代的CLR語言,C#是應用最為廣泛的,其應用場景針對Windows桌面、移動手機以及伺服器環境等複雜、專業的開發專案。
C#是種物件導向的強型別語言。C#在編譯和執行時都有的強型別檢查,使在大多數典型的程式設計錯誤能夠被儘早地發現,而且位置定位相當精準。相比於那些不拘泥型別,在違規操作很久後才報出可追蹤到莫名其妙錯誤的語言,這可以為程式設計師節省很多時間。然而,許多程式設計師有意或無意地拋棄了這個檢測的有點,這導致本文中討論的一些問題。
關於本文
本文介紹了10種最常見的程式設計錯誤,或是C#程式設計師要避免的陷阱。
儘管本文中討論的錯誤是C#環境下的,但對其他達成CLR或使用框架類庫(FCL)的語言也相關(FCL)。
常見錯誤 #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做不到,所以一定要在這兩種方法之間比較一下效能。
#5常見錯誤:在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#的一些關鍵的細微之處,像本文中所提到的那些(但不限於這些),可以幫助我們更好的去使用語言,從而避免一些常見的陷阱。
原文地址:http://www.toptal.com/c-sharp/top-10-mistakes-that-c-sharp-programmers-make
相關文章
- Java程式設計師常犯的10個錯誤Java程式設計師
- Python 程式設計師經常犯的 10 個錯誤Python程式設計師
- 程式設計師可能常犯的 6 個錯誤程式設計師
- 程式設計師準備面試時常犯的10個錯誤程式設計師面試
- 程式設計師做網頁設計常犯的8個錯誤程式設計師網頁
- Python 程式設計師最常犯的十個錯誤Python程式設計師
- PHP程式設計師最常犯的11個MySQL錯誤PHP程式設計師MySql
- Java程式設計師在寫SQL程式時候常犯的10個錯誤Java程式設計師SQL
- (網頁)Java程式設計師們最常犯的10個錯誤(轉)網頁Java程式設計師
- 你與其他程式設計師可能常犯的 6 個錯誤程式設計師
- 程式設計師常犯的 5 個非技術性錯誤程式設計師
- C# 程式設計師易犯的 10 個錯誤C#程式設計師
- 網頁設計師新手常犯的6個錯誤網頁
- 程式設計師準備面試時常犯11個錯誤,切記!程式設計師面試
- Java程式設計師容易犯的10個錯誤Java程式設計師
- 10個資料科學家常犯的程式設計錯誤(附解決方案)資料科學程式設計
- C#幾個經常犯錯誤彙總C#
- python開發者常犯的10個錯誤Python
- PHP開發者常犯的10個MySQL錯誤PHPMySql
- 每個程式設計師都會犯的10個錯誤程式設計師
- 程式設計師在打造影響力時常犯的 3 個錯程式設計師
- Python程式設計師的10個常見錯誤Python程式設計師
- 程式設計師最容易犯的幾個技術上的錯誤程式設計師
- Python開發者最常犯的10個錯誤Python
- 每個程式設計師都可能犯過的10個錯誤程式設計師
- 開發者常犯的 9 個錯誤
- AngularJS 開發中常犯的10個錯誤AngularJS
- Web開發人員常犯的10個錯誤Web
- [譯] 我在程式設計初級階段常犯的錯誤程式設計
- Coverity談“開發中測試”與程式設計師最常犯的編碼錯誤程式設計師
- 最危險的程式設計錯誤程式設計
- IT人士常犯的17個職場錯誤
- 程式設計師程式設計生涯中會犯的7個錯誤程式設計師
- 程式設計師看法上的幾個典型錯誤程式設計師
- C++程式設計人員容易犯的10個C#錯誤(轉)C++程式設計C#
- 程式設計師在頁面友好性上常犯的5種錯誤以及改正方法程式設計師
- PHP開發人員常犯的10個MysqL錯誤PHPMySql
- 編寫 SQL 程式碼時常犯的九個錯誤SQL