程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> .NET網頁編程 >> C# >> 關於C# >> C# 程序員最常犯的 10 個錯誤

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提供了很多額外的集合類型,像持久化樹形數據結構,基於堆的優先級隊列,哈希索引的數組列表,鏈表等以及更多。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved