13 choses que les développeurs C# doivent savoir
Des bogues et des failles du programme apparaissent souvent au cours du processus de développement. Faire bon usage des outils peut vous aider à découvrir ou à éviter des problèmes avant de publier votre programme.
L'écriture de code standardisée peut faciliter la maintenance du code, en particulier lorsque le code est développé et maintenu par plusieurs développeurs ou équipes. Cet avantage est encore plus important. Les outils courants pour forcer la normalisation du code incluent : FxCop, StyleCop et ReSharper.
Note du développeur : réfléchissez bien aux erreurs avant de les dissimuler et analysez les résultats. Ne comptez pas sur ces outils pour trouver des bogues dans votre code, car les résultats peuvent être très différents des vôtres.
La révision du code et la programmation partenaire sont des exercices courants dans lesquels les développeurs examinent intentionnellement le code écrit par d'autres. D’autres sont désireux de trouver des bugs de la part des développeurs de code, comme des erreurs de codage ou des erreurs d’exécution.
La révision du code est un exercice précieux, difficile à quantifier et dont l'exactitude n'est pas satisfaisante en raison de sa dépendance à l'égard d'un effort manuel.
L'analyse statique ne nécessite pas d'exécuter le code. Vous n'avez pas besoin d'écrire des cas de test pour découvrir certaines irrégularités dans le code ou l'existence de défauts. C'est un moyen très efficace de détecter les problèmes, mais vous devez disposer d'un outil qui ne contient pas trop de faux positifs. Les outils d'analyse statique couramment utilisés pour C# incluent Coverity, CAT, NET et Visual Studio Code Analysis.
Les outils d'analyse dynamique peuvent vous aider à trouver ces erreurs lorsque vous exécutez votre code : vulnérabilités de sécurité, problèmes de performances et de concurrence. Cette approche effectue une analyse dans un contexte de temps d'exécution et, en tant que telle, son efficacité est limitée par la complexité du code. Visual Studio fournit un grand nombre d'outils d'analyse dynamique, notamment Concurrency Visualizer, IntelliTrace et des outils de profilage.
Citation du manager/chef d'équipe : Les pratiques de développement sont le meilleur moyen de s'entraîner à éviter les pièges courants. Faites également attention à savoir si l'outil de test répond à vos besoins. Essayez de garder le niveau de diagnostic du code de votre équipe sous contrôle.
Il existe différentes méthodes de tests : tests unitaires, tests d'intégration système, tests de performances, tests d'intrusion, etc. Pendant la phase de développement, la plupart des cas de test sont rédigés par des développeurs ou des testeurs afin que le programme puisse répondre aux exigences.
Les tests ne fonctionnent que s'ils exécutent le bon code. Lors des tests fonctionnels, il peut également être utilisé pour tester la vitesse de développement et de maintenance des développeurs.
Passez plus de temps sur la sélection des outils, utilisez les bons outils pour résoudre les problèmes qui vous intéressent et n'ajoutez pas de travail supplémentaire aux développeurs. Laissez les outils d'analyse et les tests s'exécuter automatiquement et en douceur pour détecter les problèmes, mais assurez-vous que l'idée du code reste clairement dans l'esprit du développeur.
Localisez le plus rapidement possible l'emplacement du problème diagnostiqué (qu'il s'agisse d'erreurs obtenues via une analyse statique ou des tests, telles que des avertissements de compilation, des violations de normes, une détection de problème, etc.). Si un problème qui vient de surgir est ignoré parce que vous « vous en fichez » et qu'il devient difficile à trouver plus tard, cela ajoutera beaucoup de charge de travail aux réviseurs de code, et vous devez prier pour qu'ils ne soient pas irrités par cela. .
Veuillez accepter ces suggestions utiles pour améliorer la qualité, la sécurité et la maintenabilité de votre code, tout en améliorant les capacités de R&D des développeurs, les capacités de coordination et la prévisibilité du code publié.
目标 | 工具 | 影响 |
一致性,可维护性 | 标准化代码书写,静态分析,代码审查 | 间距一致,命名标准,良好的可读格式,都会让开发者更易编写与维护代码。 |
准确性 | 代码审查,静态分析,动态分析,测试 | 代码不只是需要语法正确,还需要以开发者的思想来满足软件需求。 |
功能性 | 测试 | 测试可以验证大多数的需求是否得到满足:正确性,可拓展性,鲁棒性以及安全性。 |
安全性 | 标准化代码书写,代码审查,静态分析,动态分析,测试 | 安全性是一个复杂的问题,任何一个小的漏洞都是潜在的威胁。 |
开发者研发能力 | 标准化代码书写,静态分析,测试 | 开发者在工具的帮助下会很快速地更正错误。 |
发布可预测性 | 标准化代码书写,代码审查,静态分析,动态分析,测试 | 流线型后期阶段的活动、最小化错误定位循环,都可以让问题发现的更早。 |
L'un des principaux avantages de C# est son système de types flexible, et les types sûrs peuvent nous aider à détecter les erreurs plus tôt. En appliquant des règles de type strictes, le compilateur peut vous aider à maintenir de bonnes habitudes de codage. À cet égard, le langage C# et le framework .NET nous proposent un grand nombre de types pour répondre à la plupart des besoins. Bien que de nombreux développeurs aient une bonne compréhension des types généraux et soient conscients des besoins des utilisateurs, certains malentendus et abus existent encore.
Pour plus d'informations sur la bibliothèque de classes du framework .NTE, veuillez vous référer à la bibliothèque MSDN.
Des interfaces spécifiques impliquent des fonctionnalités C# communes. Par exemple, IDiposable permet l'utilisation d'un langage commun de gestion des ressources, tel que le mot-clé « using ». Une bonne compréhension des interfaces peut vous aider à écrire du code C# fluide et à en faciliter la maintenance.
Évitez d'utiliser l'interface ICloneable : les développeurs ne savent jamais si un objet copié est une copie complète ou une copie superficielle. Puisqu'il n'existe toujours pas de moyen standard de juger si l'opération de copie d'objets est correcte, il n'existe aucun moyen d'utiliser l'interface de manière significative en tant que contrat.
Essayez d'éviter d'écrire dans des structures et traitez-les comme des objets immuables pour éviter toute confusion. Le partage de mémoire dans des scénarios tels que le multithreading deviendra plus sûr. L'approche que nous adoptons avec les structures consiste à initialiser la structure lors de sa création. Si ses données doivent être modifiées, il est recommandé de générer une nouvelle entité.
Comprenez correctement quels types/méthodes standard sont immuables et peuvent renvoyer de nouvelles valeurs (telles que des chaînes, des dates), et utilisez-les pour remplacer ces objets mutables (tels que List.Enumerator).
La valeur de la chaîne peut être vide, certaines fonctions pratiques peuvent donc être utilisées le cas échéant. Des erreurs NullReferenceException peuvent se produire lors d'un jugement de valeur (s.Length==0), tandis que String.IsNullOrEmpty(s) et String.IsNullOrWhitespace(s) peuvent bien fonctionner avec null.
Les types d'énumération et les constantes peuvent rendre le code plus facile à lire, et en remplaçant les nombres magiques par des identifiants, la signification de la valeur peut être exprimée.
Si vous devez générer un grand nombre de types d'énumérations, les types d'énumérations balisés sont une option plus simple :
[Flag] public enum Tag { None =0x0, Tip =0x1, Example=0x2 }
La méthode suivante vous permet d'utiliser plusieurs balises dans un extrait :
snippet.Tag = Tag.Tip | Tag.Example
Cette méthode est propice à l'encapsulation des données, vous n'avez donc pas à vous soucier de la fuite d'informations de collection interne lorsque vous utilisez le getter de propriété Tag.
Il existe deux types d'égalité :
1. Égalité des références, c'est-à-dire que les deux références pointent vers le même objet.
2. L'égalité numérique, c'est-à-dire que deux objets de référence différents peuvent être considérés comme égaux.
De plus, C# fournit également de nombreuses méthodes de test d’égalité. Les méthodes les plus courantes sont les suivantes :
== et != opérations
Méthode équivalente d'héritage virtuel par objet
Objet statique.Méthode Equal
Méthode d'équivalence d'interface IEquatable
Méthode Static Object.ReferenceEquals
Il est parfois difficile de comprendre le but de l'utilisation d'une référence ou d'une égalité de valeurs. Pour en savoir plus à ce sujet et améliorer votre travail, veuillez consulter :
MSDNhttp://msdn.microsoft.com/en-us/library/dd183752.aspx
Si vous souhaitez écraser quelque chose, n'oubliez pas les outils fournis sur MSDN tels que IEquatable
Faites attention à l'impact des conteneurs non typés sur la surcharge et envisagez d'utiliser la méthode "myArrayList[0] == myString". Les éléments du tableau sont des "objets" de types au moment de la compilation, donc l'égalité des références fonctionne. Bien que C# vous alerte de ces erreurs potentielles, une égalité de référence inattendue ne sera pas alertée dans certains cas pendant le processus de compilation.
Les classes jouent un rôle important dans la gestion correcte des données. Pour des raisons de performances, les classes mettent toujours en cache les résultats partiels ou font des hypothèses sur la cohérence des données internes. Rendre publiques les autorisations de données vous oblige à mettre en cache ou à formuler des hypothèses dans une certaine mesure, et ces opérations se manifestent par des impacts potentiels sur les performances, la sécurité et la concurrence. Par exemple, exposer des membres mutables tels que des collections génériques et des tableaux permet aux utilisateurs de vous ignorer et de modifier directement la structure.
En plus de contrôler les objets via des modificateurs d'accès, les propriétés vous permettent de contrôler très précisément la manière dont les utilisateurs interagissent avec vos objets. En particulier, les attributs peuvent également vous permettre de connaître les conditions spécifiques de lecture et d'écriture.
Les propriétés peuvent vous aider à créer une API stable lors du remplacement de données en getters et setters via une logique de stockage, ou à fournir une ressource de liaison de données.
Ne lancez jamais d'exceptions dans les getters de propriétés et évitez de modifier l'état de l'objet. Il s'agit d'une exigence pour les méthodes, pas pour les getters pour les propriétés.
更多有关属性的信息,请参阅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 翻译:码农网
Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!