錯誤的 C# 代碼:C# 編程中最常見的 10 個錯誤
已發表: 2022-03-11關於 C Sharp
C# 是面向 Microsoft 公共語言運行時 (CLR) 的多種語言之一。 針對 CLR 的語言受益於諸如跨語言集成和異常處理、增強的安全性、組件交互的簡化模型以及調試和分析服務等功能。 在當今的 CLR 語言中,C# 最廣泛用於針對 Windows 桌面、移動或服務器環境的複雜專業開發項目。
C# 是一種面向對象的強類型語言。 C# 中嚴格的類型檢查,無論是在編譯時還是運行時,都會導致大多數典型的 C# 編程錯誤儘早被報告,並且它們的位置非常準確。 這可以在 C Sharp 編程中節省大量時間,相比之下,在使用類型安全執行更加自由的語言中,在違規操作發生很久之後可能會出現令人費解的錯誤。 但是,許多 C# 編碼人員無意中(或不小心)放棄了這種檢測的好處,這導致了本 C# 教程中討論的一些問題。
關於本 C Sharp 編程教程
本教程描述了 C# 程序員最常見的 10 個 C# 編程錯誤或要避免的問題,並為他們提供幫助。
雖然本文中討論的大多數錯誤都是特定於 C# 的,但有些錯誤也與針對 CLR 或使用框架類庫 (FCL) 的其他語言相關。
常見的 C# 編程錯誤 #1:使用像值這樣的引用,反之亦然
C++ 和許多其他語言的程序員習慣於控制他們分配給變量的值是簡單的值還是對現有對象的引用。 然而,在 C Sharp 編程中,這個決定是由編寫對象的程序員做出的,而不是由實例化對象並將其分配給變量的程序員做出的。 對於那些試圖學習 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
的值保持不變,而在分配新顏色時,修改了pen1
的值pen2
。 因此我們可以推斷出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# 關鍵字之間的許多相似之處所迷惑,從而產生了一種虛假的安全感,這種行為可能會讓您感到驚訝,可能會讓您從 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?) } }
為什麼point1
不為空? 答案是Point
是值類型, Point
的默認值為(0,0),而不是null。 未能認識到這一點是 C# 中一個非常容易(且常見)的錯誤。
許多(但不是全部)值類型都有一個IsEmpty
屬性,您可以檢查它是否等於其默認值:
Console.WriteLine(point1.IsEmpty); // True
當您檢查變量是否已初始化時,請確保您知道該類型的未初始化變量默認具有什麼值,並且不要依賴它為 null..
常見 C# 編程錯誤 #3:使用不正確或未指定的字符串比較方法
在 C# 中比較字符串有很多不同的方法。
儘管許多程序員使用==
運算符進行字符串比較,但它實際上是最不受歡迎的方法之一,主要是因為它沒有在代碼中明確指定需要哪種類型的比較。
相反,在 C# 編程中測試字符串相等性的首選方法是使用Equals
方法:
public bool Equals(string value); public bool Equals(string value, StringComparison comparisonType);
第一個方法簽名(即,沒有comparisonType
參數)實際上與使用==
運算符相同,但具有顯式應用於字符串的好處。 它執行字符串的序號比較,基本上是逐字節比較。 在許多情況下,這正是您想要的比較類型,尤其是在比較以編程方式設置值的字符串時,例如文件名、環境變量、屬性等。在這些情況下,只要序數比較確實是正確的類型在這種情況下進行comparisonType
時,使用沒有 compareType 的Equals
方法的唯一缺點是閱讀代碼的人可能不知道您正在進行哪種類型的比較。
但是,每次comparisonType
字符串時使用包含一個 compareType 的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
方法,它為您提供有關字符串相對順序的信息,而不僅僅是一個相等性測試。 這種方法優於<
、 <=
、 >
和>=
運算符,原因與上面討論的相同——以避免 C# 問題。
常見的 C# 編程錯誤 #4:使用迭代(而不是聲明)語句來操作集合
在 C# 3.0 中,向語言添加語言集成查詢 (LINQ) 永遠改變了查詢和操作集合的方式。 從那時起,如果您使用迭代語句來操作集合,那麼您可能沒有使用 LINQ。
一些 C# 程序員甚至不知道 LINQ 的存在,但幸運的是,這個數字正變得越來越小。 但是,許多人仍然認為,由於 LINQ 關鍵字和 SQL 語句之間的相似性,它的唯一用途是在查詢數據庫的代碼中。
雖然數據庫查詢是 LINQ 語句的一種非常普遍的用法,但它們實際上適用於任何可枚舉集合(即,任何實現 IEnumerable 接口的對象)。 因此,例如,如果您有一個 Accounts 數組,而不是編寫 C# List foreach:
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();
雖然這是一個關於如何避免這種常見 C# 編程問題的非常簡單的示例,但在某些情況下,單個 LINQ 語句可以輕鬆替換代碼中迭代循環(或嵌套循環)中的數十個語句。 更少的通用代碼意味著更少的引入錯誤的機會。 但是請記住,在性能方面可能會有所取捨。 在性能關鍵場景中,尤其是在您的迭代代碼能夠對您的集合做出 LINQ 無法做出假設的情況下,請務必在這兩種方法之間進行性能比較。
常見的 C# 編程錯誤 #5:未能考慮 LINQ 語句中的底層對象
LINQ 非常適合抽像操作集合的任務,無論它們是內存對象、數據庫表還是 XML 文檔。 在一個完美的世界中,您不需要知道底層對像是什麼。 但這裡的錯誤是假設我們生活在一個完美的世界中。 事實上,相同的 LINQ 語句在完全相同的數據上執行時可能會返回不同的結果,如果該數據恰好採用不同的格式。
例如,考慮以下語句:
decimal total = (from account in myAccounts where account.Status == "active" select account.Balance).Sum();
如果對象的其中一個account.Status
等於“Active”(注意大寫 A),會發生什麼? 好吧,如果myAccounts
是一個DbSet
對象(使用默認的不區分大小寫的配置設置),那麼where
表達式仍將匹配該元素。 但是,如果myAccounts
在內存中的數組中,它將不匹配,因此會產生不同的總計結果。
但是等一下。 當我們之前討論字符串比較時,我們看到==
運算符執行字符串的序數比較。 那麼為什麼在這種情況下==
運算符執行不區分大小寫的比較呢?
答案是,當 LINQ 語句中的基礎對像是對 SQL 表數據的引用時(如本示例中的 Entity Framework DbSet 對象的情況),該語句將轉換為 T-SQL 語句。 運算符然後遵循 T-SQL 編程規則,而不是 C# 編程規則,因此上述情況中的比較最終是不區分大小寫的。
一般來說,儘管 LINQ 是查詢對象集合的一種有用且一致的方法,但實際上您仍然需要知道您的語句是否會在後台轉換為 C# 以外的其他內容,以確保您的代碼行為將在運行時按預期進行。
常見的 C# 編程錯誤 #6:被擴展方法弄糊塗或偽裝
如前所述,LINQ 語句適用於任何實現 IEnumerable 的對象。 例如,以下簡單函數將累加任何帳戶集合的餘額:
public decimal SumAccounts(IEnumerable<Account> myAccounts) { return myAccounts.Sum(a => a.Balance); }
在上面的代碼中, myAccounts 參數的類型被聲明為IEnumerable<Account>
。 由於myAccounts
引用Sum
方法(C# 使用熟悉的“點符號”來引用類或接口上的方法),我們希望在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
類上定義的靜態方法(稱為“擴展方法”):

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# 編譯器中“語法糖”的一個例子,它允許我們編寫(通常)更清晰、更易於維護的代碼。 更清楚的是,如果您知道它們的用法。 否則,它可能會有點混亂,尤其是一開始。
雖然使用擴展方法當然有好處,但它們可能會導致問題,並為那些不了解它們或沒有正確理解它們的開發人員提供 C# 編程幫助的呼聲。 在在線查看代碼示例或任何其他預先編寫的代碼時尤其如此。 當這樣的代碼產生編譯器錯誤時(因為它調用的方法顯然沒有在它們被調用的類上定義),傾向於認為代碼適用於不同版本的庫,或者完全適用於不同的庫。 可能會花費大量時間來尋找不存在的新版本或幻像“缺失的庫”。
即使是熟悉擴展方法的開發人員也偶爾會被抓到,當對像上有一個同名的方法,但它的方法簽名與擴展方法的簽名有細微的不同時。 尋找不存在的錯字或錯誤可能會浪費大量時間。
在 C# 庫中使用擴展方法正變得越來越普遍。 除了 LINQ,Unity 應用程序塊和 Web API 框架是 Microsoft 使用擴展方法的兩個大量使用的現代庫的示例,還有許多其他庫。 框架越現代,它就越有可能包含擴展方法。
當然,您也可以編寫自己的擴展方法。 然而,請意識到,雖然擴展方法似乎像常規實例方法一樣被調用,但這實際上只是一種錯覺。 特別是,您的擴展方法不能引用它們正在擴展的類的私有或受保護成員,因此不能完全替代更傳統的類繼承。
常見的 C# 編程錯誤 #7:為手頭的任務使用錯誤類型的集合
C# 提供了種類繁多的集合對象,以下只是部分列表:
Array
, ArrayList
, BitArray
, BitVector32
, Dictionary<K,V>
, HashTable
, HybridDictionary
, List<T>
, NameValueCollection
, OrderedDictionary
, Queue, Queue<T>
, SortedList
, Stack, Stack<T>
, StringCollection
, StringDictionary
。
雖然在某些情況下,選擇過多與選擇不足一樣糟糕,但集合對象並非如此。 可用的選項數量絕對可以對您有利。 花一點額外的時間來研究並為您的目的選擇最佳的收藏類型。 它可能會帶來更好的性能和更少的出錯空間。
如果有專門針對您擁有的元素類型(例如字符串或位)的集合類型,則傾向於首先使用該類型。 當它針對特定類型的元素時,實現通常更有效。
為了利用 C# 的類型安全性,您通常應該更喜歡泛型接口而不是非泛型接口。 泛型接口的元素是您在聲明對象時指定的類型,而非泛型接口的元素是對像類型。 使用非泛型接口時,C# 編譯器無法對代碼進行類型檢查。 此外,在處理原始值類型的集合時,使用非泛型集合將導致這些類型的重複裝箱/拆箱,與適當類型的泛型集合相比,這可能會導致顯著的負面性能影響。
另一個常見的 C# 問題是編寫自己的集合對象。 這並不是說它永遠不合適,但是有了 .NET 提供的全面的選擇,您可能可以通過使用或擴展已經存在的選擇來節省大量時間,而不是重新發明輪子。 特別是,用於 C# 和 CLI 的 C5 通用集合庫提供了一系列“開箱即用”的附加集合,例如持久樹數據結構、基於堆的優先級隊列、哈希索引數組列表、鍊錶等等。
常見的 C# 編程錯誤 #8:忽視釋放資源
CLR 環境使用垃圾收集器,因此您無需顯式釋放為任何對象創建的內存。 事實上,你不能。 在 C 中沒有與 C++ delete
運算符或free()
函數等效的功能。 但這並不意味著您可以在使用完所有對像後忘記它們。 許多類型的對象封裝了一些其他類型的系統資源(例如,磁盤文件、數據庫連接、網絡套接字等)。 讓這些資源保持打開狀態會迅速耗盡系統資源的總數,降低性能並最終導致程序故障。
雖然可以在任何 C# 類上定義析構函數方法,但析構函數(在 C# 中也稱為終結器)的問題是您無法確定何時調用它們。 它們由垃圾收集器(在單獨的線程上,這可能導致額外的複雜性)在未來不確定的時間調用。 試圖通過使用GC.Collect()
強制垃圾收集來繞過這些限制並不是 C# 的最佳實踐,因為這會在線程收集所有符合收集條件的對象時阻塞線程一段未知的時間。
這並不是說終結器沒有很好的用途,但是以確定性的方式釋放資源並不是其中之一。 相反,當您在文件、網絡或數據庫連接上進行操作時,您希望在完成後立即顯式釋放底層資源。
資源洩漏幾乎在任何環境中都是一個問題。 但是,C# 提供了一種強大且易於使用的機制,如果使用該機制,可以使洩漏發生的機率大大降低。 .NET 框架定義了IDisposable
接口,該接口僅由Dispose()
方法組成。 任何實現IDisposable
的對像都希望在對象的使用者完成操作時調用該方法。 這導致了明確的、確定性的資源釋放。
如果您在單個代碼塊的上下文中創建和處置對象,那麼忘記調用Dispose()
基本上是不可原諒的,因為 C# 提供了一個using
語句,該語句將確保無論代碼塊如何調用Dispose()
退出(無論是異常、返回語句,還是簡單地關閉塊)。 是的,這與前面提到的用於在文件頂部包含 C# 命名空間的using
語句相同。 它還有第二個完全不相關的目的,許多 C# 開發人員不知道。 即,確保在退出代碼塊時調用對像上的Dispose()
:
using (FileStream myFile = File.OpenRead("foo.txt")) { myFile.Read(buffer, 0, 100); }
通過在上面的示例中創建using
塊,您可以確定myFile.Dispose()
將在您完成文件後立即調用,無論Read()
是否引發異常。
常見的 C# 編程錯誤 #9:迴避異常
C# 繼續在運行時強制執行類型安全。 這使您可以在 C# 中比在 C++ 等語言中更快地查明許多類型的錯誤,其中錯誤的類型轉換可能導致將任意值分配給對象的字段。 然而,程序員再一次浪費了這個偉大的特性,導致了 C# 的問題。 他們落入這個陷阱是因為 C# 提供了兩種不同的做事方式,一種可以拋出異常,另一種不會。 有些人會迴避異常路由,認為不必編寫 try/catch 塊可以節省一些編碼。
例如,這裡有兩種在 C# 中執行顯式類型轉換的不同方法:
// METHOD 1: // Throws an exception if account can't be cast to SavingsAccount SavingsAccount savingsAccount = (SavingsAccount)account; // METHOD 2: // Does NOT throw an exception if account can't be cast to // SavingsAccount; will just set savingsAccount to null instead SavingsAccount savingsAccount = account as SavingsAccount;
使用方法 2 可能發生的最明顯錯誤是無法檢查返回值。 這可能會導致最終的 NullReferenceException,它可能會在更晚的時間出現,從而使追查問題的根源變得更加困難。 相比之下,方法 1 會立即引發InvalidCastException
,從而使問題的根源更加明顯。
而且,即使你記得檢查方法2中的返回值,如果你發現它為null,你會怎麼做? 你寫的方法在合適的地方報錯嗎? 如果演員陣容失敗,你還有什麼可以嘗試的嗎? 如果不是,那麼拋出異常是正確的做法,所以你不妨讓它發生在盡可能靠近問題根源的地方。
以下是其他常見方法對的幾個示例,其中一個拋出異常,另一個不拋出:
int.Parse(); // throws exception if argument can't be parsed int.TryParse(); // returns a bool to denote whether parse succeeded IEnumerable.First(); // throws exception if sequence is empty IEnumerable.FirstOrDefault(); // returns null/default value if sequence is empty
一些 C# 開發人員非常“反對異常”,以至於他們自動假定不拋出異常的方法是優越的。 儘管在某些特定情況下這可能是正確的,但作為概括,它根本不正確。
作為一個具體的例子,如果您有一個替代的合法(例如,默認)操作要採取,如果一個異常已經生成,那麼非異常方法可能是一個合法的選擇。 在這種情況下,寫這樣的東西可能確實更好:
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
因此必然是“更好”的方法是不正確的。 有時是這樣,有時不是。 這就是為什麼有兩種方法可以做到這一點。 為您所處的環境使用正確的,記住異常當然可以成為您作為開發人員的朋友。
常見的 C# 編程錯誤 #10:允許編譯器警告累積
雖然這個問題絕對不是 C# 特有的,但它在 C# 編程中尤其嚴重,因為它放棄了 C# 編譯器提供的嚴格類型檢查的好處。
生成警告是有原因的。 儘管所有 C# 編譯器錯誤都表示代碼中存在缺陷,但許多警告也是如此。 兩者的區別在於,在出現警告的情況下,編譯器可以毫無問題地發出您的代碼所代表的指令。 即便如此,它還是會發現您的代碼有點可疑,並且您的代碼很可能無法準確反映您的意圖。
就本 C# 編程教程而言,一個常見的簡單示例是,當您修改算法以消除對正在使用的變量的使用,但您忘記刪除變量聲明時。 程序將完美運行,但編譯器會標記無用的變量聲明。 程序完美運行的事實導致程序員忽略修復警告的原因。 此外,編碼人員利用 Visual Studio 功能,可以輕鬆隱藏“錯誤列表”窗口中的警告,以便他們只關注錯誤。 沒過多久,就會出現幾十個警告,所有這些警告都被忽略了(或者更糟糕的是,被隱藏了)。
但是,如果您忽略這種類型的警告,遲早會在您的代碼中找到類似這樣的內容:
class Account { int myId; int Id; // compiler warned you about this, but you didn't listen! // Constructor Account(int id) { this.myId = Id; // OOPS! } }
並且以 Intellisense 允許我們編寫代碼的速度,這個錯誤並不像看起來那麼不可能。
您現在的程序中有一個嚴重錯誤(儘管編譯器只是將其標記為警告,原因已經解釋過),並且根據您的程序的複雜程度,您可能會浪費大量時間來跟踪這個錯誤。 如果您一開始就注意到了這個警告,那麼您就可以通過簡單的五秒鐘修復來避免這個問題。
請記住,C Sharp 編譯器會為您提供很多關於代碼健壯性的有用信息……如果您在聽的話。 不要忽視警告。 它們通常只需要幾秒鐘即可修復,而當它們發生時修復新問題可以為您節省數小時。 訓練自己期待 Visual Studio“錯誤列表”窗口顯示“0 個錯誤,0 個警告”,這樣任何警告都會讓您感到不舒服,以便立即解決它們。
當然,每條規則都有例外。 因此,有時您的代碼在編譯器看來會有些可疑,即使這正是您想要的樣子。 在那些非常罕見的情況下,僅在觸發警告的代碼周圍使用#pragma warning disable [warning id]
,並且僅針對它觸發的警告 ID。 這將抑制該警告,並且僅抑制該警告,以便您仍然可以對新警告保持警惕。
包起來
C# 是一種強大而靈活的語言,具有許多可以大大提高生產力的機制和範例。 然而,與任何軟件工具或語言一樣,對其功能的理解或評價有限有時可能更像是一種障礙而不是一種好處,從而使一個人處於眾所周知的“知道足夠危險”的狀態。
使用像這樣的 C Sharp 教程來熟悉 C# 的關鍵細微差別,例如(但不限於)本文中提出的問題,將有助於 C# 優化,同時避免它的一些更常見的陷阱語言。
進一步閱讀 Toptal 工程博客:
- 基本 C# 面試問題
- C# 與 C++:核心是什麼?