13 Dinge, die jeder C#-Entwickler wissen muss
Während des Entwicklungsprozesses treten häufig Programmfehler und Mängel auf. Wenn Sie die Tools sinnvoll einsetzen, können Sie Probleme erkennen oder vermeiden, bevor Sie Ihr Programm veröffentlichen.
Das Schreiben standardisierten Codes kann die Wartung des Codes erleichtern, insbesondere wenn der Code von mehreren Entwicklern oder Teams entwickelt und verwaltet wird. Dieser Vorteil ist noch deutlicher. Zu den gängigen Tools zum Erzwingen der Codestandardisierung gehören: FxCop, StyleCop und ReSharper.
Anmerkung des Entwicklers: Denken Sie sorgfältig über Fehler nach, bevor Sie sie vertuschen, und analysieren Sie die Ergebnisse. Erwarten Sie nicht, dass Sie sich auf diese Tools verlassen, um Fehler in Ihrem Code zu finden, da die Ergebnisse möglicherweise stark von Ihren Ergebnissen abweichen.
Codeüberprüfung und Partnerprogrammierung sind gängige Übungen, bei denen Entwickler absichtlich von anderen geschriebenen Code überprüfen. Andere sind daran interessiert, Fehler seitens der Codeentwickler zu finden, etwa Codierungsfehler oder Ausführungsfehler.
Die Überprüfung von Code ist eine wertvolle Übung, die schwer zu quantifizieren ist und deren Genauigkeit aufgrund des manuellen Aufwands unbefriedigend ist.
Bei der statischen Analyse müssen Sie den Code nicht ausführen. Sie müssen keine Testfälle schreiben, um einige Unregelmäßigkeiten im Code oder das Vorhandensein von Fehlern herauszufinden. Dies ist eine sehr effektive Möglichkeit, Probleme zu finden, Sie benötigen jedoch ein Tool, das nicht zu viele Fehlalarme enthält. Zu den häufig verwendeten statischen Analysetools für C# gehören Coverity, CAT, NET und Visual Studio Code Analysis.
Mithilfe dynamischer Analysetools können Sie diese Fehler beim Ausführen Ihres Codes finden: Sicherheitslücken, Leistungs- und Parallelitätsprobleme. Dieser Ansatz führt die Analyse im Kontext der Ausführungszeit durch und daher ist seine Wirksamkeit durch die Komplexität des Codes begrenzt. Visual Studio bietet eine große Anzahl dynamischer Analysetools, darunter Concurrency Visualizer, IntelliTrace und Profiling Tools.
Zitat des Managers/Teamleiters: Entwicklungspraktiken sind der beste Weg, um die Vermeidung häufiger Fallstricke zu üben. Achten Sie auch darauf, ob das Testtool Ihren Anforderungen entspricht. Versuchen Sie, den Grad der Codediagnose Ihres Teams unter Kontrolle zu halten.
Es gibt viele Möglichkeiten zum Testen: Unit-Tests, Systemintegrationstests, Leistungstests, Penetrationstests usw. Während der Entwicklungsphase werden die meisten Testfälle von Entwicklern oder Testern geschrieben, damit das Programm die Anforderungen erfüllen kann.
Tests sind nur dann effektiv, wenn sie den richtigen Code ausführen. Bei der Durchführung von Funktionstests kann es auch dazu verwendet werden, die Entwicklungs- und Wartungsgeschwindigkeit der Entwickler herauszufordern.
Nehmen Sie sich mehr Zeit für die Tool-Auswahl, nutzen Sie die richtigen Tools, um die Probleme zu lösen, die Ihnen am Herzen liegen, und verursachen Sie keine zusätzliche Arbeit für die Entwickler. Lassen Sie Analysetools und Tests automatisch und reibungslos laufen, um Probleme zu finden, aber stellen Sie sicher, dass die Idee des Codes klar im Kopf des Entwicklers bleibt.
Lokalisieren Sie so schnell wie möglich den Ort des diagnostizierten Problems (unabhängig davon, ob es sich um Fehler handelt, die durch statische Analyse oder Tests ermittelt wurden, z. B. Kompilierungswarnungen, Standardverstöße, Problemerkennung usw.). Wenn ein Problem, das gerade auftritt, ignoriert wird, weil es Ihnen „egal“ ist, und es später schwer zu finden ist, bedeutet dies eine große Arbeitsbelastung für die Codeüberprüfungsmitarbeiter, und Sie müssen beten, dass sie dadurch nicht irritiert werden Es.
Bitte akzeptieren Sie diese nützlichen Vorschläge, um die Qualität, Sicherheit und Wartbarkeit Ihres Codes zu verbessern und gleichzeitig die F&E-Fähigkeiten der Entwickler, die Koordinierungsfähigkeiten und die Vorhersehbarkeit des veröffentlichten Codes zu verbessern.
目标 | 工具 | 影响 |
一致性,可维护性 | 标准化代码书写,静态分析,代码审查 | 间距一致,命名标准,良好的可读格式,都会让开发者更易编写与维护代码。 |
准确性 | 代码审查,静态分析,动态分析,测试 | 代码不只是需要语法正确,还需要以开发者的思想来满足软件需求。 |
功能性 | 测试 | 测试可以验证大多数的需求是否得到满足:正确性,可拓展性,鲁棒性以及安全性。 |
安全性 | 标准化代码书写,代码审查,静态分析,动态分析,测试 | 安全性是一个复杂的问题,任何一个小的漏洞都是潜在的威胁。 |
开发者研发能力 | 标准化代码书写,静态分析,测试 | 开发者在工具的帮助下会很快速地更正错误。 |
发布可预测性 | 标准化代码书写,代码审查,静态分析,动态分析,测试 | 流线型后期阶段的活动、最小化错误定位循环,都可以让问题发现的更早。 |
Einer der Hauptvorteile von C# ist sein flexibles Typsystem, und sichere Typen können uns dabei helfen, Fehler früher zu finden. Durch die Durchsetzung strenger Typregeln kann der Compiler Ihnen dabei helfen, gute Codierungsgewohnheiten beizubehalten. In dieser Hinsicht stellen uns die C#-Sprache und das .NET-Framework eine große Anzahl von Typen zur Verfügung, die den meisten Anforderungen gerecht werden. Obwohl viele Entwickler über ein gutes Verständnis allgemeiner Typen verfügen und sich der Benutzerbedürfnisse bewusst sind, bestehen immer noch einige Missverständnisse und Missbrauch.
Weitere Informationen zur .NTE-Framework-Klassenbibliothek finden Sie in der MSDN-Bibliothek.
Bestimmte Schnittstellen umfassen allgemeine C#-Funktionen. IDiposable ermöglicht beispielsweise die Verwendung einer gemeinsamen Ressourcenverwaltungssprache, etwa des Schlüsselworts „using“. Ein gutes Verständnis von Schnittstellen kann Ihnen helfen, flüssigen C#-Code zu schreiben und ihn einfacher zu warten.
Vermeiden Sie die Verwendung der ICloneable-Schnittstelle – Entwickler wissen nie, ob es sich bei einem kopierten Objekt um eine tiefe oder flache Kopie handelt. Da es noch keine Standardmethode gibt, um zu beurteilen, ob der Vorgang des Kopierens von Objekten korrekt ist, gibt es keine Möglichkeit, die Schnittstelle sinnvoll als Vertrag zu verwenden.
Vermeiden Sie das Schreiben in Strukturen und behandeln Sie sie als unveränderliche Objekte, um Verwirrung zu vermeiden. Die gemeinsame Nutzung des Speichers in Szenarien wie Multithreading wird sicherer. Der Ansatz, den wir bei Strukturen verfolgen, besteht darin, die Struktur beim Erstellen zu initialisieren. Wenn ihre Daten geändert werden müssen, wird empfohlen, eine neue Entität zu generieren.
Verstehen Sie richtig, welche Standardtypen/-methoden unveränderlich sind und neue Werte (z. B. Zeichenfolgen, Datumsangaben) zurückgeben können, und verwenden Sie diese, um diese veränderlichen Objekte (z. B. List.Enumerator) zu ersetzen.
Der Wert der Zeichenfolge kann leer sein, sodass bei Bedarf einige praktische Funktionen verwendet werden können. Bei der Wertbeurteilung (s.Length==0) können NullReferenceException-Fehler auftreten, während String.IsNullOrEmpty(s) und String.IsNullOrWhitespace(s) gut mit Null funktionieren können.
Aufzählungstypen und Konstanten können das Lesen von Code erleichtern, und durch das Ersetzen magischer Zahlen durch Bezeichner kann die Bedeutung des Werts ausgedrückt werden.
Wenn Sie eine große Anzahl von Aufzählungstypen generieren müssen, sind mit Tags versehene Aufzählungstypen eine einfachere Option:
[Flag] public enum Tag { None =0x0, Tip =0x1, Example=0x2 }
Mit der folgenden Methode können Sie mehrere Tags in einem Snippet verwenden:
snippet.Tag = Tag.Tip | Tag.Example
Diese Methode begünstigt die Datenkapselung, sodass Sie sich keine Sorgen machen müssen, dass interne Sammlungsinformationen verloren gehen, wenn Sie den Tag-Eigenschafts-Getter verwenden.
Es gibt zwei Arten von Gleichheit:
1. Referenzgleichheit, das heißt, beide Referenzen verweisen auf dasselbe Objekt.
2. Numerische Gleichheit, das heißt, zwei verschiedene Referenzobjekte können als gleich betrachtet werden.
Darüber hinaus bietet C# auch viele Methoden zum Testen der Gleichheit. Die gebräuchlichsten Methoden sind wie folgt:
== und != Operationen
Äquivalente Methode der virtuellen Vererbung nach Objekt
Statische Object.Equal-Methode
IEquatable
Statische Object.ReferenceEquals-Methode
Manchmal ist es schwierig, den Zweck der Verwendung von Referenz oder Wertgleichheit herauszufinden. Um mehr darüber zu erfahren und Ihre Arbeit zu verbessern, lesen Sie bitte:
MSDNhttp://msdn.microsoft.com/en-us/library/dd183752.aspx
Wenn Sie etwas überschreiben möchten, vergessen Sie nicht die auf MSDN bereitgestellten Tools wie IEquatable
Achten Sie auf die Auswirkungen untypisierter Container auf die Überladung und erwägen Sie die Verwendung der Methode „myArrayList[0] == myString“. Array-Elemente sind „Objekte“ von Typen zur Kompilierzeit, daher funktioniert die Referenzgleichheit. Obwohl C# Sie auf diese potenziellen Fehler aufmerksam macht, wird eine unerwartete Referenzgleichheit in einigen Fällen während des Kompilierungsprozesses nicht gemeldet.
Klassen spielen eine große Rolle bei der ordnungsgemäßen Datenverwaltung. Aus Leistungsgründen speichern Klassen immer Teilergebnisse zwischen oder treffen einige Annahmen über die Konsistenz interner Daten. Wenn Sie Datenberechtigungen öffentlich machen, müssen Sie bis zu einem gewissen Grad zwischenspeichern oder Annahmen treffen, und diese Vorgänge machen sich durch potenzielle Auswirkungen auf Leistung, Sicherheit und Parallelität bemerkbar. Wenn Sie beispielsweise veränderliche Elemente wie generische Sammlungen und Arrays verfügbar machen, können Benutzer Sie überspringen und die Struktur direkt ändern.
Neben der Steuerung von Objekten durch Zugriffsmodifikatoren ermöglichen Ihnen Eigenschaften auch eine sehr genaue Steuerung der Art und Weise, wie Benutzer mit Ihren Objekten interagieren. Insbesondere können Attribute Sie auch über die spezifischen Lese- und Schreibbedingungen informieren.
Eigenschaften können Ihnen beim Aufbau einer stabilen API beim Überschreiben von Daten in Getter und Setter durch Speicherlogik helfen oder eine Datenbindungsressource bereitstellen.
Lösen Sie niemals Ausnahmen in Eigenschafts-Gettern aus und vermeiden Sie es, den Objektstatus zu ändern. Dies ist eine Anforderung für Methoden, nicht für Getter für Eigenschaften.
更多有关属性的信息,请参阅MSDN:
http://msdn.microsoft.com/en-us/library/ms229006(v=vs.120).aspx
同时也要注意getter的一些副作用。开发者也习惯于将成员体的存取视为一种常见的操作,因此他们在代码审查的时候也常常忽视那些副作用。
你可以为一个新创建的对象根据它创建的表达形式赋予属性。例如为Foo与Bar属性创建一个新的具有给定值的C类对象:
new C {Foo=blah, Bar=blam}
你也可以生成一个具有特定属性名称的匿名类型的实体:
var myAwesomeObject = new {Name=”Foo”, Size=10};
初始化过程在构造函数体之前运行,因此需要保证在输入至构造函数之前,将这一域给初始化。由于构造函数还没有运行,所以目标域的初始化可能不管怎样都不涉及“this”。
为了使一些特殊方法更加容易控制,最好在你使用的方法当中使用最少的特定类型。比如在一种方法中使用 List
public void Foo(List<Bar> bars) { foreach(var b in bars) { // do something with the bar... } }
对于其他IEnumerable
泛型是一种在定义独立类型结构体与设计算法上一种十分有力的工具,它可以强制类型变得安全。
用像List
在使用泛型时,我们可以用关键词“default”来为类型获取缺省值(这些缺省值不可以硬编码写进implementation)。特别要指出的是,数字类型的缺省值是o,引用类型与空类型的缺省值为null。
T t = default(T);
类型转换有两种模式。其一显式转换必须由开发者调用,另一隐式转换是基于环境下应用于编译器的。
常量o可由隐式转换至枚举型数据。当你尝试调用含有数字的方法时,可以将这些数据转换成枚举类型。
类型转换 | 描述 |
Tree tree = (Tree)obj; | 这种方法可以在对象是树类型时使用;如果对象不是树,可能会出现InvalidCast异常。 |
Tree tree = obj as Tree; | 这种方法你可以在预测对象是否为树时使用。如果对象不是树,那么会给树赋值null。你可以用“as”的转换,然后找到null值的返回处,再进行处理。由于它需要有条件处理的返回值,因此记住只在需要的时候才去用这种转换。这种额外的代码可能会造成一些bug,还可能会降低代码的可读性。 |
转换通常意味着以下两件事之一:
1.RuntimeType的表现可比编译器所表现出来的特殊的多,Cast转换命令编译器将这种表达视为一种更特殊的类型。如果你的设想不正确的话,那么编译器会向你输出一个异常。例如:将对象转换成串。
2.有一种完全不同的类型的值,与Expression的值有关。Cast命令编译器生成代码去与该值相关联,或者是在没有值的情况下报出一个异常。例如:将double类型转换成int类型。
以上两种类型的Cast都有着风险。第一种Cast向我们提出了一个问题:“为什么开发者能很清楚地知道问题,而编译器为什么不能?”如果你处于这个情况当中,你可以去尝试改变程序让编译器能够顺利地推理出正确的类型。如果你认为一个对象的runtime type是比compile time type还要特殊的类型,你就可以用“as”或者“is”操作。
第二种cast也提出了一个问题:“为什么不在第一步就对目标数据类型进行操作?”如果你需要int类型的结果,那么用int会比double更有意义一些。
获取额外的信息请参阅:
http://blogs.msdn.com/b/ericlippert/archive/tags/cast+operator/
在某些情况下显式转换是一种正确的选择,它可以提高代码可阅读性与debug能力,还可以在采用合适的操作的情况下提高测试能力。
异常不应该常出现在程序流程中。它们代表着开发者所不愿看到的运行环境,而这些很可能无法修复。如果你期望得到一个可控制的环境,那么主动去检查环境会比等待问题的出现要好得多。
利用TryParse()方法可以很方便地将格式化的串转换成数字。不论是否解析成功,它都会返回一个布尔型结果,这要比单纯返回异常要好很多。
写代码时注意catch与finally块的使用。由于这些不希望得到的异常,控制可能进入这些块中。那些你期望的已执行的代码可能会由于异常而跳过。如:
Frobber originalFrobber = null; try { originalFrobber = this.GetCurrentFrobber(); this.UseTemporaryFrobber(); this.frobSomeBlobs(); } finally { this.ResetFrobber(originalFrobber); }
如果GetCurrentFrobber()报出了一个异常,那么当finally blocks被执行时originalFrobber的值仍然为空。如果GetCurrentFrobber不能被扔掉,那么为什么其内部是一个try block?
要注意有针对性地处理你的目标异常,并且只去处理目标代码当中的异常部分。尽量不要去处理所有异常,或者是根类异常,除非你的目的是记录并重新处理这些异常。某些异常会使应用处于一种接近崩溃的状态,但这也比无法修复要好得多。有些试图修复代码的操作可能会误使情况变得更糟糕。
关于致命的异常都有一些细微的差异,特别是注重finally blocks的执行,可以影响到异常的安全与调试。更多信息请参阅:
http://incrediblejourneysintotheknown.blogspot.com/2009/02/fatal-exceptions-and-why-vbnet-has.html
使用一款顶级的异常处理器去安全地处理异常情况,并且会将debug的一些问题信息暴露出来。使用catch块会比较安全地定位那些特殊的情况,从而安全地解决这些问题,再将一些问题留给顶级的异常处理器去解决。
如果你发现了一个异常,请做些什么去解决它,而不要去将这个问题搁置。搁置只会使问题更加复杂,更难以解决。
将异常包含至一个自定义异常中,对面向公共API的代码特别有用。异常是可视界面方法的一部分,它也被参数与返回值所控制。但这种扩散了很多异常的方法对于代码的鲁棒性与可维护性的解决来说十分麻烦。
如果你希望在更高层次上解决caught异常,那么就维持原异常状态,并且栈就是一个很好的debug方法。但需要注意维持好debug与安全考虑的平衡。
好的选择包括简单地将异常继续抛出:
Throw;
或者将异常视为内部异常重新抛出:
抛出一个新CustomException;
不要显式重新抛出类似于这样的caught异常:
Throw e;
这样的话会将异常的处理恢复至初始状态,并且阻碍debug。
有些异常发生于你代码的运行环境之外。与其使用caught块,你可能更需要向目标当中添加如ThreadException或UnhandledException之类的处理器。例如,Windows窗体异常并不是出现于窗体处理线程环境当中的。
千万不要让异常影响到你数据模型的完整性。你需要保证你的对象处于比较稳定的状态当中——这样一来任何由类的执行的操作都不会出现违例。否则,通过“恢复”这一手段会使你的代码变得更加让人不解,也容易造成进一步的损坏。
考虑几种修改私有域顺序的方法。如果在修改顺序的过程当中出现了异常,那么你的对象可能并不处于非法状态下。尝试在实际更新域之前去得到新的值,这样你就可以在异常安全管理下,正常地更新你的域。
对特定类型的值——包括布尔型,32bit或者更小的数据类型与引用型——进行可变量的分配,确保可以是原子型。没有什么保障是给一些大型数据(double,long,decimal)使用的。可以多考虑这个:在共享多线程的变量时,多使用lock statements。
事件与委托共同提供了一种关于类的方法,这种方法在有特殊的事情发生时向用户进行提醒。委托事件的值在事件发生时应被调用。事件就像是委托类型的域,当对象生成时,其自动初始化为null。
事件也像值为“组播”的域。这也就是说,一种委托可以依次调用其他委托。你可以将一个委托分配给一个事件,你也可以通过类似-=于+=这样的操作来控制事件。
如果一个事件被多个线程所共享,另一个线程就有可能在你检查是否为null之后,在调用其之前而清除所有的用户信息——并抛出一个NullReferenceException。
对于此类问题的标准解决方法是创建一个该事件的副本,用于测试与调用。你仍然需要注意的是,如果委托没有被正确调用的话,那么在其他线程里被移除的用户仍然可以继续操作。你也可以用某种方法将操作按顺序锁定,以避免一些问题。
public event EventHandler SomethingHappened; private void OnSomethingHappened() { // The event is null until somebody hooks up to it // Create our own copy of the event to protect against another thread removing our subscribers EventHandler handler = SomethingHappened; if (handler != null) handler(this,new EventArgs()); }
更多关于事件与竞争的信息请参阅:
http://blogs.msdn.com/b/ericlippert/archive/2009/04/29/events-and-races.aspx
使用一种事件处理器为事件资源生成一个由处理器的资源对象到接收对象的引用,可以保护接收端的garbage collection。
适当的unhook处理器可以确保你不必因委托不再工作而去调用它浪费时间,也不会使内存存储无用委托与不可引用的对象。
属性提供了一种向程序集、类与其信息属性中注入元数据的方法。它们经常用来提供信息给代码的消费者——比如debugger、框架测试、应用——通过反射这一方式。你也可以向你的用户定义属性,或是使用预定义属性,详见下表:
属性 | 使用对象 | 目的 |
DebuggerDisplay | Debugger | Debugger display 格式 |
InternalsVisibleTo | Member access | 使用特定类来暴露内部成员去指定其他的类。基于此方法,测试方法可以用来保护成员,并且persistence层可以用一些特殊的隐蔽方法。 |
DefaultValue | Properties | 为属性指定一个缺省值 |
一定要对DebuggerStepThrough多重视几分——否则它会在这个方法应用的地方让寻找bug变得十分困难,你也会因此而跳过某步或是推倒而重做它。
Debug是在开发过程中必不可少的部分。除了使运行环境不透明的部分变得可视化之外,debugger也可以侵入运行环境,并且如果不使用debugger的话会导致应用程序变现有所不同。
为了观察当前框架异常状态,你可以将“$exception”这一表达添加进Visual Studio Watch窗口。这种变量包含了当前异常状态,类似于你在catch block中所看见的,但其中不包含在debugger中看见的不是代码中的真正存在的异常。
如果你的属性有副作用,那么考虑你是否应使用特性或者是debugger设置去避免debugger自动地调用getter。例如,你的类可能有这样一个属性:
private int remainingAccesses = 10; private string meteredData; public string MeteredData { get { if (remainingAccesses-- > 0) return meteredData; return null; } }
你第一次在debugger中看见这个对象时,remainingAccesses会获得一个值为10的整型变量,并且MeteredData为null。然而如果你hover结束了remainingAccesses,你会发现它的值会变成9.这样一来debugger的属性值表现改变了你的对象的状态。
早做计划,不断监测,后做优化
在设计阶段,制定切实可行的目标。在开发阶段,专注于代码的正确性要比去做微调整有意义的多。对于你的目标,你要在开发过程中多进行监测。只需要在你没有达到预期的目标的时候,你才应该去花时间对程序做一个调整。
请记住用合适的工具来确保性能的经验性测量,并且使测试处于这样一种环境当中:可反复多次测试,并且测试过程尽量与现实当中用户的使用习惯一致。
当你对性能进行测试的时候,一定要注意你真正所关心的测试目标是什么。在进行某一项功能的测试时,你的测试有没有包含这项功能的调用或者是回路构造的开销?
我们都听说过很多比别人做得快很多的项目神话,不要盲目相信这些,试验与测试才是实在的东西。
由于CLR优化的原因,有时候看起来效率不高的代码可能会比看起来效率高的代码运行的更快。例如,CLR优化循环覆盖了一个完整的数组,以避免在不可见的per-element范围里的检查。开发者经常在循环一个数组之前先计算一下它的长度:
int[] a_val = int[4000]; int len = a_val.Length; for (int i = 0; i < len; i++) a_val[i] = i;
通过将长度存储进一个变量当中,CLR会不去识别这一部分,并且跳过优化。但是有时手动优化会反人类地导致更糟糕的性能表现。
如果你打算将大量的字符串进行连接,可以使用System.Text.StringBuilder来避免生成大量的临时字符串。
如果你打算生成并填满集合中已知的大量数据,由于再分配的存在,可以用保留空间来解决生成集合的性能与资源问题。你可以用AddRange方法来进一步对性能进行优化,如下在List
Persons.AddRange(listBox.Items);
垃圾收集器(garbage collector)可以自动地清理内存。即使这样,一切被抛弃的资源也需要适当的处理——特别是那些垃圾收集器不能管理的资源。
资源管理问题的常见来源 | |
内存碎片 | 如果没有足够大的连续的虚拟地址存储空间,可能会导致分配失败 |
进程限制 | 进程通常都可以读取内存的所有子集,以及系统可用的资源。 |
资源泄露 | 垃圾收集器只管理内存,其他资源需要由应用程序正确管理。 |
不稳定资源 | 那些依赖于垃圾收集器与终结器(finalizers)的资源在很久没用过的时候,不可被立即调用。实际上它们可能永远不可能被调用。 |
利用try/finally block来确保资源已被合理释放,或是让你的类使用IDisposable,以及更方便更安全的声明方式。
using (StreamReader reader=new StreamReader(file)) { //your code here
除了用调用GC.Collect()干扰garbage collector之外,也可以考虑适当地释放或是抛弃资源。在进行性能测试时,如果你可以承担这种影响带来的后果,你再去使用garbage collector。
与当前一些流传的谣言不同的是,你的类不需要Finalizers,而这只是因为IDisposable的存在!你可以让IDisposable赋予你的类在任何已拥有的组合实例中调用Dispose的能力,但是finalizers只能在拥有未管理的资源类中使用。
Finalizers主要对交互式Win32位句柄API有很大作用,并且SafeHandle句柄是很容易利用的。
不要总是设想你的finalizers(总是在finalizer线程上运行的)会很好地与其他对象进行交互。那些其他的对象可能在该进程之前就被终止掉了。
处理并发性与多线程编程是件复杂的、困难的事情。在将并发性添加进你的程序之前,请确保你已经明确了解你的做的是什么——因为这里面有太多门道了!
多线程软件的情况很难进行预测,比如很容易产生如竞争条件与死锁的问题,而这些问题并不是仅仅影响单线程应用。基于这些风险,你应该将多线程视为最后一种手段。如果不得不使用多线程,尽量缩减多线程同时使用内存的需求。如果必须使线程同步,请尽可能地使用最高等级的同步机制。在最高等级的前提下,包括了这些机制:
Async-await/Task Parallel Library/Lazy
Lock/monitor/AutoResetEvent
Interlocked/Semaphore
可变域与显式barrier
以上的这些很难解释清楚C#/.NET的复杂之处。如果你想开发一个正常的并发应用,可以去参阅O’Reilly的《Concurrency in C# Cookboo》。
将一个域标记为“volatile”是一种高级特性,而这种设置也经常被专家所误解。C#的编译器会保证目标域可以被获取与释放语义,但是被lock的域就不适用于这种情况。如果你不知道获取什么,不知道释放什么语义,以及它们是怎样影响CPU层次的优化,那么久避免使用volatile域。取而代之的可以用更高层次的工具,比如Task Parallel Library或是CancellationToken。
标准库类型常提供使对象线程安全更容易的方法。例如Dictionary.TryGetValue()。使用此类方法一般可以使你的代码变得更加清爽,并且你也不必担心像TOCTOU(time-of-check-time-of-use竞争危害的一种)这样的数据竞争。
不要锁住“this”、字符串,或是其他普通public的对象
当使用在多线程环境下的一些类时,多注意lock的使用。锁住字符串常量,或是其他公共对象,会阻止你锁状态下的封装,还可能会导致死锁。你需要阻止其他代码锁定在同一使用的对象上,当然你最好的选择是使用private对象成员项。
滥用null是一种常见的导致程序错误的来源,这种非正常操作可能会使程序崩溃或是其他的异常。如果你试图获取一个null的引用,就好像它是某对象的有效引用值(例如通过获取一个属性或是方法),那么在运行时就会抛出一个NullReferenceException。
静态与动态分析工具可以在你发布代码之前为你检查出潜在的NullReferenceException。在C#当中,引用型为null通常是由于变量没有引用到某个对象而造成的。对于值可为空的类型与引用型来说,是可以使用null的。例如:Nullable
每个null引用异常都是一个bug。相比于找到NullReferenceException这个问题来说,不如尝试在你使用该对象之前去为null进行测试。这样一来可以使代码更易于最小化的try/catch block读取。
当从数据库表中读取数据时,注意缺失值可以表示为DBNull 对象,而不是作为空引用。不要期望它们表现得像潜在的空引用一样。
Float与double都可以表示十进制实数,但不能表示二进制实数,并且在存储十进制值的时候可以在必要时用二进制的近似值存储。从十进制的角度来看,这些二进制的近似值通常都有不同的精度与取舍,有时在算数操作当中会导致一些不期望的结果。由于浮点型运算通常在硬件当中执行,因此硬件条件的不可预测会使这些差异更加复杂。
在十进制精度很重要的时候,就要使用十进制了——比如经济方面的计算。
有一种常见的错误就是忘记了结构是值类型,意即其复制与通过值传递。例如你可能见过这样的代码:
struct P { public int x; public int y; } void M() { P p = whatever; … p.x = something; … N(p);
忽然某一天,代码维护人员决定将代码重构成这样:
void M() { P p = whatever; Helper(p); N(p); } void Helper(P p) { … p.x = something;
现在当N(p)在M()中被调用,p就有了一个错误的值。调用Helper(p)传递p的副本,并不是引用p,于是在Helper()中的突变便丢失掉了。如果被正常调用,那么Helper应该传递的是调整过的p的副本。
C#编译器可以保护在运算过程中的常量溢出,但不一定是计算值。使用“checked”与“unchecked”两个关键词来标记你想对变量进行什么操作。
与结构体不同的是,类是引用类型,并且可以适当地修改引用对象。然而并不是所有的对象方法都可以实际修改引用对象,有一些返回的是一个新的对象。当开发者调用后者时,他们需要记住将返回值分配给一个变量,这样才可以使用修改过的对象。在代码审查阶段,这些问题的类型通常会逃过审查而不被发现。像字符串之类的对象,它们是不可变的,因此永远不可能修改这些对象。即便如此,开发者还是很容易忘记这些问题。
例如,看如下 string.Replace()代码:
string label = “My name is Aloysius”; label.Replace(“Aloysius”, “secret”);
这两行代码运行之后会打印出“My name is Aloysius” ,这是因为Raeplace方法并没改变该字符串的值。
注意不要在遍历时去修改集合
List<Int> myItems = new List<Int>{20,25,9,14,50}; foreach(int item in myItems) { if (item < 10) { myItems.Remove(item); // iterator is now invalid! // you’ll get an exception on the next iteration
如果你运行了这个代码,那么它一在下一项的集合中进行循环,你就会得到一个异常。
正确的处理方法是使用第二个list去保存你想删除的这一项,然后在你想删除的时候再遍历这个list:
List<Int> myItems = new List<Int>{20,25,9,14,50}; List<Int> toRemove = new List<Int>(); foreach(int item in myItems) { if (item < 10) { toRemove.Add(item); } } foreach(int item in toRemove) {
如果你用的是C#3.0或更高版本,可以尝试List
myInts.RemoveAll(item => (item < 10));
在实现属性时,要注意属性的名称和在类当中用的成员项的名字有很大差别。很容易在不知情的情况下使用了相同的名称,并且在属性被获取的时候还会触发死循环。
// The following code will trigger infinite recursion private string name; public string Name { get { return Name; // should reference “name” instead.
在重命名间接属性时同样要小心。例如:在WPF中绑定的数据将属性名称指定为字符串。有时无意的改变属性名称,可能会不小心造成编译器无法解决的问题。
英文原文:13 Things Every C# Developer Should Know 翻译:码农网
Das obige ist der detaillierte Inhalt von13 Dinge, die Sie als C#-Entwickler wissen müssen. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!