Heim >Backend-Entwicklung >C#.Net-Tutorial >Die 10 häufigsten Fehler, die C#-Programmierer machen
Über C#
C# ist eine der wenigen Sprachen, die die Microsoft Common Language Runtime (CLR) erreicht haben. Sprachen, die die CLR implementieren, können von den damit verbundenen Funktionen profitieren, wie z. B. sprachübergreifende Integration, Ausnahmebehandlung, Sicherheitsverbesserungen, einfache Modelle für die Komponentenzusammensetzung sowie Debugging- und Analysedienste. Als moderne CLR-Sprache ist C# die am weitesten verbreitete Sprache und ihre Anwendungsszenarien zielen auf komplexe und professionelle Entwicklungsprojekte wie Windows-Desktops, Mobiltelefone und Serverumgebungen ab.
C# ist eine objektorientierte, stark typisierte Sprache. Durch die starke Typprüfung von C# zur Kompilierungs- und Laufzeit können die meisten typischen Programmierfehler so früh wie möglich entdeckt werden, und die Position ist ziemlich genau. Dies kann Programmierern im Vergleich zu Sprachen, die sich nicht an Typen halten und nachvollziehbare, unerklärliche Fehler erst lange nach Auftreten des Verstoßes melden, viel Zeit sparen. Viele Programmierer verzichten jedoch absichtlich oder unabsichtlich auf die Vorteile dieser Erkennung, was zu einigen der in diesem Artikel diskutierten Probleme führt.
Über diesen Artikel
In diesem Artikel werden die 10 häufigsten Programmierfehler oder Fallen vorgestellt, die C#-Programmierer vermeiden sollten.
Obwohl der in diesem Artikel besprochene Fehler spezifisch für die C#-Umgebung ist, ist er auch für andere Sprachen relevant, die die CLR implementieren oder die Framework Class Library (FCL) verwenden.
Häufiger Fehler Nr. 1: Eine Referenz wie einen Wert oder darüber verwenden
Programmierer in C++ und vielen anderen Sprachen sind es gewohnt zu kontrollieren, ob der Wert, den sie einer Variablen zuweisen, ein einfacher Wert ist oder ein Literal Es gibt einen Verweis auf das Objekt. In C# würde dies durch den Programmierer bestimmt, der das Objekt geschrieben hat, und nicht durch den Programmierer, der das Objekt instanziiert und ihm Variablen zugewiesen hat. Dies ist ein häufiges „Problem“ unter C#-Programmieranfängern.
Wenn Sie nicht wissen, ob es sich bei dem Objekt, mit dem Sie arbeiten, um einen Werttyp oder einen Referenztyp handelt, kann es zu Überraschungen kommen. Beispiel:
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
Wie Sie sehen können, werden die Point- und Pen-Objekte zwar auf die gleiche Weise erstellt, aber wenn Punkt2 ein neuer X-Koordinatenwert zugewiesen wird, bleibt der Wert von Punkt1 unverändert. Wenn Stift2 ein neuer Farbwert zugewiesen wird, ändert sich auch Stift1 entsprechend. Daraus können wir schließen, dass point1 und point2 jeweils eine eigene Kopie des Point-Objekts enthalten, während pen1 und pen2 auf dasselbe Pen-Objekt verweisen. Wie können wir dieses Prinzip ohne diesen Test kennen?
Eine Möglichkeit besteht darin, sich anzusehen, wie das Objekt definiert ist (in Visual Studio können Sie den Cursor auf den Namen des Objekts setzen und die Taste F12 drücken)
public struct Point { … } // defines a “value” type public class Pen { … } // defines a “reference” type
Wie oben gezeigt, wird in C# das Schlüsselwort struct zum Definieren eines Werttyps und das Schlüsselwort class zum Definieren eines Referenztyps verwendet. Für diejenigen mit einem C++-Programmierhintergrund kann dieses Verhalten überraschend sein, wenn sie durch einige der ähnlichen Schlüsselwörter zwischen C++ und C# verwirrt sind.
Wenn Sie sich auf ein Verhalten verlassen möchten, das sich zwischen Werttypen und Referenztypen unterscheidet, beispielsweise wenn Sie ein Objekt als Parameter an eine Methode übergeben und den Status des Objekts in dieser Methode ändern möchten. Sie müssen sicherstellen, dass Sie es mit dem richtigen Objekttyp zu tun haben.
Häufiger Fehler Nr. 2: Missverständnis des Standardwerts nicht initialisierter Variablen
In C# dürfen Werttypen nicht null sein. Per Definition muss der Typwert eines Werts, sogar der Werttyp einer initialisierten Variablen, einen Wert haben. Dies wird als Standardwert des Typs bezeichnet. Dies führt bei der Prüfung, ob eine Variable nicht initialisiert ist, häufig zu folgenden, unerwarteten Ergebnissen:
class Program { static Point point1; static Pen pen1; static void Main(string[] args) { Console.WriteLine(pen1 == null); // True Console.WriteLine(point1 == null); // False (huh?) } }
Warum ist [Punkt 1] nicht leer? Die Antwort ist, dass point ein Werttyp ist und es wie beim Standardwert point (0,0) keinen Nullwert gibt. Nicht erkannt, dass dies ein sehr einfacher und häufiger Fehler in C# ist
Viele (aber nicht alle) Werttypen haben ein [IsEmpty]-Attribut, Sie können sehen, dass es dem Standardwert entspricht:
Console.WriteLine(point1.IsEmpty); // True
Wenn Sie überprüfen, ob eine Variable initialisiert wurde, stellen Sie sicher, dass Sie wissen, dass der nicht initialisierte Wert der Typ der Variablen ist, der standardmäßig nicht null ist.
常见错误 #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方法,可以提供字符串的相对顺序信息而不仅仅中测试是否相等。这个方法可以很好适用于be218b54e7c452811b5d72906a4b8a5d和>= 运算符,对上述讨论同样适用。
常见误区 #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参数的类型被声明为IEnumerable5788b468e1dd6be073fc218202eafec6,myAccounts引用了一个Sum 方法 (C# 使用类似的 “dot notation” 引用方法或者接口中的类),我们期望在IEnumerable8742468051c85b06f0a0af9e3e506b5c接口中定义一个Sum()方法。但是,IEnumerable8742468051c85b06f0a0af9e3e506b5c没有为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修饰符。这就是编译器知道它是一个扩展方法的“奥妙”。它所修饰的参数的类型(这个例子中的IEnumerable767cb18821eb42d0be2dbae71b5cc905)说明这个类或者接口将显得实现了这个方法。
(另外需要指出的是,定义扩展方法的IEnumerable接口和Enumerable类的名字间的相似性没什么奇怪的。这种相似性只是随意的风格选择。)
理解了这一点,我们可以看到上面介绍的sumAccounts方法能以下面的方式实现:
public decimal SumAccounts(IEnumerable<Account> myAccounts) { return Enumerable.Sum(myAccounts, a => a.Balance); }
事实上我们可能已经这样实现了这个方法,而不是问什么要有扩展方法。扩展方法本身只是C#的一个方便你无需继承、重新编译或者修改原始代码就可以给已存的在类型“添加”方法的方式。
扩展方法通过在文件开头添加using [namespace];引入到作用域。你需要知道你要找的扩展方法所在的名字空间。如果你知道你要找的是什么,这点很容易。
当C#编译器碰到一个对象的实例调用了一个方法,并且它在这个对象的类中找不到那个方法,它就会尝试在作用域中所有的扩展方法里找一个匹配所要求的类和方法签名的。如果找到了,它就把实例的引用当做第一个参数传给那个扩展方法,然后如果有其它参数的话,再把它们依次传入扩展方法。(如果C#编译器没有在作用域中找到相应的扩展方法,它会抛措。)
Für den C#-Compiler sind Erweiterungsmethoden „Syntaxzucker“, der es uns ermöglicht, Code klarer zu schreiben und (in den meisten Fällen) einfacher zu warten. Dies setzt natürlich voraus, dass Sie wissen, wie man es benutzt, sonst kann es vor allem am Anfang verwirrend sein.
Die Anwendung von Erweiterungsmethoden hat zwar Vorteile, kann aber auch Kopfschmerzen bereiten und Zeit für Entwickler verschwenden, die sie nicht oder nicht richtig verstehen. Vor allem, wenn man sich Online-Beispielcode oder anderen bereits geschriebenen Code ansieht. Wenn dieser Code einen Kompilierungsfehler generiert (weil er Methoden aufruft, die offensichtlich nicht im aufgerufenen Typ definiert sind), besteht die allgemeine Tendenz darin, zu prüfen, ob der Code für andere Versionen der referenzierten Bibliothek oder sogar für andere Bibliotheken gilt. Es wird viel Zeit damit verbracht, nach neuen Versionen oder Bibliotheken zu suchen, die als „verloren“ gelten.
Selbst Entwickler, die mit Erweiterungsmethoden vertraut sind, machen gelegentlich den oben genannten Fehler, wenn der Name der Erweiterungsmethode mit dem Namen der in der Klasse definierten Methode übereinstimmt und sich nur geringfügig in der Methodensignatur unterscheidet. Es wird viel Zeit damit verbracht, nach Rechtschreibfehlern zu suchen, die „nicht existieren“.
In C# wird die Verwendung von Erweiterungsmethoden immer beliebter. Neben LINQ werden Erweiterungsmethoden auch in zwei anderen weit verbreiteten Klassenbibliotheken von Microsoft verwendet: Unity Application Block und Web API Framework, und es gibt viele andere. Je neuer das Framework ist, desto wahrscheinlicher ist es, dass Erweiterungsmethoden verwendet werden.
Natürlich können Sie auch eigene Erweiterungsmethoden schreiben. Sie müssen sich jedoch darüber im Klaren sein, dass Erweiterungsmethoden zwar scheinbar wie andere Instanzmethoden aufgerufen werden, dies jedoch tatsächlich eine Illusion ist. Tatsächlich können Erweiterungsmethoden nicht auf private und geschützte Mitglieder der erweiterten Klasse zugreifen und können daher nicht als Ersatz für die traditionelle Vererbung verwendet werden.
Häufiger Fehler Nr. 7: Verwendung des falschen Sammlungstyps für die jeweilige Aufgabe
C# bietet eine große Anzahl von Sammlungstypobjekten, nur einige davon sind unten aufgeführt:
Array,ArrayList,BitArray,BitVector32,Dictionaryb77a8d9c3c319e50d4b02a976b347910,HashTable,HybridDictionary,List8742468051c85b06f0a0af9e3e506b5c,NameValueCollection,OrderedDictionary,Queue, Queue8742468051c85b06f0a0af9e3e506b5c,SortedList,Stack, Stack8742468051c85b06f0a0af9e3e506b5c,StringCollection,StringDictionary.
Aber in manchen Fällen ist es genauso schlimm, zu viele Auswahlmöglichkeiten zu haben, wie nicht genügend Auswahlmöglichkeiten zu haben, und das Gleiche gilt für Sammlungstypen. Die große Auswahl an Optionen sorgt mit Sicherheit für einen reibungslosen Ablauf Ihrer Arbeit. Es ist jedoch besser, sich vorab etwas Zeit mit der Suche und dem Kennenlernen der Sammlungsarten zu nehmen, damit Sie diejenige auswählen können, die Ihren Anforderungen am besten entspricht. Dies führt letztendlich zu einer besseren Leistung Ihres Programms und verringert die Wahrscheinlichkeit von Fehlern.
Wenn es eine Sammlung gibt, die einen Elementtyp (z. B. String oder Bit) angibt, der mit dem identisch ist, den Sie bearbeiten, sollten Sie ihn zuerst verwenden. Solche Sammlungen sind effizienter, wenn die entsprechenden Elementtypen angegeben werden.
Um die Typsicherheit in C# zu nutzen, ist es besser, eine generische Schnittstelle zu verwenden, als eine nicht generische Schnittstelle. Der Elementtyp in einer generischen Schnittstelle ist der Typ, den Sie bei der Deklaration des Objekts angeben, während die Elemente in einer nicht generischen Schnittstelle vom Objekttyp sind. Wenn Sie eine nicht generische Schnittstelle verwenden, kann der C#-Compiler Ihren Code nicht einer Typprüfung unterziehen. Wenn Sie mit Sammlungen nativer Typen arbeiten, führt die Verwendung nicht generischer Schnittstellen ebenfalls dazu, dass C# häufig Boxing- und Unboxing-Vorgänge für diese Typen durchführt. Dies hat im Vergleich zur Verwendung einer generischen Sammlung, die den entsprechenden Typ angibt, erhebliche Auswirkungen auf die Leistung.
Eine weitere häufige Gefahr besteht darin, selbst einen Sammlungstyp zu implementieren. Das heißt nicht, dass Sie es niemals tun sollten. Sie können viel Zeit sparen, indem Sie einige der weit verbreiteten Sammlungstypen von .NET verwenden oder erweitern, anstatt das Rad neu zu erfinden. Insbesondere die C5 Generic Collection Library und CLI von C# bieten viele zusätzliche Sammlungstypen, wie persistente Baumdatenstrukturen, Heap-basierte Prioritätswarteschlangen, Hash-indizierte Array-Listen, verknüpfte Listen und mehr.
Häufiger Fehler Nr. 8: Fehlende Ressourcenfreigabe
Die von CLR verwaltete Umgebung fungiert als Garbage Collector, sodass Sie den von erstellten Objekten belegten Speicher nicht explizit freigeben müssen. Tatsächlich können Sie es auch nicht explizit freigeben. In C# gibt es keinen Operator, der dem Löschen in C++ entspricht, oder eine Methode, die der Funktion free() in der Sprache C entspricht. Das heißt aber nicht, dass Sie alle verwendeten Objekte ignorieren können. Viele Objekttypen kapseln viele andere Arten von Systemressourcen (z. B. Festplattendateien, Datenverbindungen, Netzwerkports usw.). Die Nutzung dieser Ressourcen kann die Systemressourcen drastisch erschöpfen, die Leistung beeinträchtigen und letztendlich zu Programmfehlern führen.
尽管所有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#的一些关键的细微之处,像本文中所提到的那些(但不限于这些),可以帮助我们更好的去使用语言,从而避免一些常见的陷阱。