ホームページ >バックエンド開発 >C#.Net チュートリアル >C# 開発者として知っておくべき 13 のこと

C# 開発者として知っておくべき 13 のこと

巴扎黑
巴扎黑オリジナル
2017-04-09 11:13:352031ブラウズ

C# 開発者が知っておくべき 13 のこと

1.開発プロセス

プログラムのバグや欠陥は、開発プロセス中に頻繁に発生します。ツールを上手に活用すると、プログラムをリリースする前に問題を発見したり回避したりするのに役立ちます。

標準化されたコード記述

標準化されたコード記述により、特に複数の開発者またはチームによってコードが開発および保守される場合、コードの保守が容易になります。コードの標準化を強制するための一般的なツールには、FxCop、StyleCop、ReSharper などがあります。

開発者向けメモ: エラーを隠す前にエラーについてよく考え、結果を分析してください。結果が実際のものとは大きく異なる可能性があるため、コード内のバグを見つけるためにこれらのツールに依存することを期待しないでください。

コードレビュー

コード レビューとパートナー プログラミングは、開発者が他の人が書いたコードを意図的にレビューする一般的な演習です。コーディング エラーや実行エラーなど、コード開発者側のバグを熱心に見つける人もいます。

コードのレビューは貴重な作業ですが、手作業に依存しているため定量化が難しく、精度も満足のいくものではありません。

静的解析

静的分析では、コードを実行する必要はありません。コードの不規則性や欠陥の存在を見つけるためにテスト ケースを作成する必要はありません。これは問題を見つけるのに非常に効果的な方法ですが、誤検知が多すぎないツールが必要です。 C# で一般的に使用される静的分析ツールには、Coverity、CAT、NET、Visual Studio Code Analysis などがあります。

動的分析

動的分析ツールは、コードの実行時にセキュリティの脆弱性、パフォーマンス、同時実行性の問題などのエラーを発見するのに役立ちます。このアプローチは実行時のコンテキストで分析を実行するため、その有効性はコードの複雑さによって制限されます。 Visual Studio は、同時実行ビジュアライザー、IntelliTrace、プロファイリング ツールなど、多数の動的分析ツールを提供します。

マネージャー/チーム リーダーの言葉: 開発プラクティスは、よくある落とし穴を回避するための最良の方法です。テストツールがニーズを満たしているかどうかにも注意してください。チームのコード診断レベルを管理下に置くようにしてください。

テスト

単体テスト、システム結合テスト、パフォーマンステスト、侵入テストなど、さまざまなテスト方法があります。開発段階では、プログラムが要件を満たすことができるように、ほとんどのテスト ケースが開発者またはテスターに​​よって作成されます。

テストは、正しいコードを実行した場合にのみ機能します。機能テストを実施する場合、開発者の開発とメンテナンスの速度に挑戦するためにも使用できます。

開発のベストプラクティス

ツールの選択により多くの時間を費やし、適切なツールを使用して関心のある問題を解決し、開発者に余分な作業を加えないようにしてください。分析ツールとテストを自動的かつスムーズに実行して問題を発見しますが、コードのアイデアが開発者の頭の中に明確に残っていることを確認してください。

診断された問題の場所をできるだけ早く特定します (コンパイル警告、標準違反、問題検出など、静的分析またはテストによって得られたエラーであるかどうか)。新しい問題が「気にしない」という理由で無視され、後で見つけるのが困難になると、コードレビュー作業者に多大な負担がかかるため、彼らがそのことでイライラしないように祈る必要があります。

コードの品質、セキュリティ、保守性を向上させると同時に、開発者の研究開発能力、調整能力、リリースされるコードの予測可能性を向上させるために、これらの有益な提案を受け入れてください。

目標 ツール インパクト
一貫性、保守性 標準化されたコード作成、静的分析、コードレビュー 一貫した間隔、命名標準、および読みやすい形式により、開発者はコードの作成と保守が容易になります。
精度 コードレビュー、静的分析、動的分析、テスト コードは文法的に正しいだけでなく、開発者の考え方を念頭に置いてソフトウェア要件を満たす必要もあります。
機能的 テスト テストでは、正確性、拡張性、堅牢性、セキュリティなど、ほとんどの要件が満たされていることを確認できます。
セキュリティ 標準化されたコード作成、コードレビュー、静的分析、動的分析、テスト セキュリティは複雑な問題であり、どんな小さな脆弱性も潜在的な脅威となります。
開発者の研究開発能力 標準化されたコードの作成、静的分析、テスト 開発者はツールを使用してエラーを迅速に修正できます。
リリースの予測可能性 標準化されたコード作成、コードレビュー、静的分析、動的分析、テスト 後期段階のアクティビティを合理化し、エラー位置のループを最小限に抑えることで、問題を早期に発見できるようになります。

2. トラップの種類

C# の主な利点の 1 つは柔軟な型システムであり、安全な型はエラーを早期に発見するのに役立ちます。コンパイラーは、厳密な型ルールを強制することで、良好なコーディング習慣を維持するのに役立ちます。この点に関して、C# 言語と .NET Framework は、ほとんどのニーズを満たす多数の型を提供します。多くの開発者は一般的な型をよく理解しており、ユーザーのニーズを認識していますが、いくつかの誤解や誤用が依然として存在します。

.NTE フレームワーク クラス ライブラリの詳細については、MSDN ライブラリを参照してください。

標準インターフェースを理解して使用する

特定のインターフェイスには、一般的な C# 機能が含まれます。たとえば、IDiposable では、キーワード「using」などの共通のリソース管理言語を使用できます。インターフェイスをよく理解すると、流暢な C# コードを記述し、保守が容易になります。

ICloneable インターフェイスの使用は避けてください。開発者は、コピーされたオブジェクトが深いコピーであるか浅いコピーであるかを知りません。オブジェクトのコピー操作が正しいかどうかを判断する標準的な方法がまだないため、インターフェイスをコントラクトとして有意義に使用する方法はありません。

構造

混乱を避けるために、構造体への書き込みを避け、構造体を不変オブジェクトとして扱うようにしてください。マルチスレッドなどのシナリオでのメモリ共有がより安全になります。構造体に対して採用するアプローチは、構造体の作成時に構造体を初期化することです。そのデータを変更する必要がある場合は、新しいエンティティを生成することをお勧めします。

どの標準型/メソッドが不変であり、新しい値 (文字列、日付など) を返すことができるのかを正しく理解し、これらを使用してそれらの変更可能なオブジェクト (List.Enumerator など) を置き換えます。

文字列

文字列の値は空の場合があるため、必要に応じて便利な関数を使用できます。値の判定 (s.Length==0) では NullReferenceException エラーが発生することがありますが、String.IsNullOrEmpty(s) および String.IsNullOrWhitespace(s) は null でも正常に動作します。

タグ列挙型

列挙型と定数を使用するとコードが読みやすくなり、マジック ナンバーを識別子に置き換えることで値の意味を表現できます。

多数の列挙型を生成する必要がある場合は、タグ付き列挙型を使用する方が簡単なオプションです:

[Flag]
public enum Tag {
  None   =0x0,
  Tip    =0x1,
  Example=0x2
}

次のメソッドを使用すると、スニペット内で複数のタグを使用できます:

snippet.Tag = Tag.Tip | Tag.Example

この方法はデータのカプセル化に役立つため、Tag プロパティ ゲッターを使用するときに内部コレクション情報が漏洩することを心配する必要はありません。

等価比較

平等には 2 つのタイプがあります:

1. 参照の等価性、つまり、両方の参照が同じオブジェクトを指します。

2. 数値的等価性、つまり 2 つの異なる参照オブジェクトが等しいと見なすことができます。

さらに、C# には多くの等価性テスト方法も用意されています。最も一般的な方法は次のとおりです:

  • == と != 操作


  • オブジェクト等価メソッドによる仮想継承


  • 静的 Object.Equal メソッド


  • IEquatableインターフェース等価性メソッド


  • 静的 Object.ReferenceEquals メソッド

参照または値の等価性を使用する目的を理解するのが難しい場合があります。これらについて詳しく知り、作業を改善するには、次を参照してください:

MSDNhttp://msdn.microsoft.com/en-us/library/dd183752.aspx

何かを上書きしたい場合は、IEquatable、GetHashCode() など、MSDN で提供されているツールを忘れないでください。

型指定されていないコンテナーがオーバーロードに与える影響に注意し、「myArrayList[0] == myString」メソッドの使用を検討してください。配列要素はコンパイル時の型の「オブジェクト」であるため、参照の等価性が機能します。 C# はこれらの潜在的なエラーを警告しますが、コンパイル プロセス中に予期しない参照の等価性が警告されない場合があります。

3. クラスの罠

データをカプセル化します

クラスは、データを適切に管理する上で大きな役割を果たします。パフォーマンス上の理由から、クラスは常に部分的な結果をキャッシュするか、内部データの一貫性について何らかの仮定を置きます。データのアクセス許可を公開すると、ある程度キャッシュしたり仮定したりする必要があり、これらの操作は、パフォーマンス、セキュリティ、同時実行性に潜在的な影響を与える形で現れます。たとえば、ジェネリック コレクションや配列などの変更可能なメンバーを公開すると、ユーザーはスキップして構造を直接変更できるようになります。

プロパティ

アクセス修飾子を使用してオブジェクトを制御することに加えて、プロパティを使用すると、ユーザーがオブジェクトを操作する方法を非常に正確に制御できます。特に、属性によって読み取りと書き込みの特定の条件を知ることもできます。

プロパティは、ストレージ ロジックを通じてデータをゲッターおよびセッターにオーバーライドするとき、またはデータ バインディング リソースを提供するときに、安定した API を構築するのに役立ちます。

プロパティ ゲッターで例外をスローしたり、オブジェクトの状態を変更したりしないでください。これはメソッドの要件であり、プロパティのゲッターではありません。

更多有关属性的信息,请参阅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来说,我们更需要使集以表的形式表现。尽量少地选取特定的类型(诸如IEnumerable, ICollection此类)以保证你的方法效率的最大化。

4.泛型

泛型是一种在定义独立类型结构体与设计算法上一种十分有力的工具,它可以强制类型变得安全。

用像List这样的泛型集来替代数组列表这种无类型集,既可以提升安全性,又可以提升性能。

在使用泛型时,我们可以用关键词“default”来为类型获取缺省值(这些缺省值不可以硬编码写进implementation)。特别要指出的是,数字类型的缺省值是o,引用类型与空类型的缺省值为null。

T t = default(T);

5.类型转换

类型转换有两种模式。其一显式转换必须由开发者调用,另一隐式转换是基于环境下应用于编译器的。

常量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能力,还可以在采用合适的操作的情况下提高测试能力。

6.异常

异常并不是condition

异常不应该常出现在程序流程中。它们代表着开发者所不愿看到的运行环境,而这些很可能无法修复。如果你期望得到一个可控制的环境,那么主动去检查环境会比等待问题的出现要好得多。

利用TryParse()方法可以很方便地将格式化的串转换成数字。不论是否解析成功,它都会返回一个布尔型结果,这要比单纯返回异常要好很多。

注意使用exception handling scope

写代码时注意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的代码特别有用。异常是可视界面方法的一部分,它也被参数与返回值所控制。但这种扩散了很多异常的方法对于代码的鲁棒性与可维护性的解决来说十分麻烦。

抛出(Throw)与继续抛出(ReThrow)异常

如果你希望在更高层次上解决caught异常,那么就维持原异常状态,并且栈就是一个很好的debug方法。但需要注意维持好debug与安全考虑的平衡。

好的选择包括简单地将异常继续抛出:

Throw;

或者将异常视为内部异常重新抛出:

抛出一个新CustomException;

不要显式重新抛出类似于这样的caught异常:

Throw e;

这样的话会将异常的处理恢复至初始状态,并且阻碍debug。

有些异常发生于你代码的运行环境之外。与其使用caught块,你可能更需要向目标当中添加如ThreadException或UnhandledException之类的处理器。例如,Windows窗体异常并不是出现于窗体处理线程环境当中的。

原子性(数据完整性)

千万不要让异常影响到你数据模型的完整性。你需要保证你的对象处于比较稳定的状态当中——这样一来任何由类的执行的操作都不会出现违例。否则,通过“恢复”这一手段会使你的代码变得更加让人不解,也容易造成进一步的损坏。

考虑几种修改私有域顺序的方法。如果在修改顺序的过程当中出现了异常,那么你的对象可能并不处于非法状态下。尝试在实际更新域之前去得到新的值,这样你就可以在异常安全管理下,正常地更新你的域。

对特定类型的值——包括布尔型,32bit或者更小的数据类型与引用型——进行可变量的分配,确保可以是原子型。没有什么保障是给一些大型数据(double,long,decimal)使用的。可以多考虑这个:在共享多线程的变量时,多使用lock statements。

7.事件

事件与委托共同提供了一种关于类的方法,这种方法在有特殊的事情发生时向用户进行提醒。委托事件的值在事件发生时应被调用。事件就像是委托类型的域,当对象生成时,其自动初始化为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

不要忘记将事件处理器Unhook

使用一种事件处理器为事件资源生成一个由处理器的资源对象到接收对象的引用,可以保护接收端的garbage collection。

适当的unhook处理器可以确保你不必因委托不再工作而去调用它浪费时间,也不会使内存存储无用委托与不可引用的对象。

8.属性

属性提供了一种向程序集、类与其信息属性中注入元数据的方法。它们经常用来提供信息给代码的消费者——比如debugger、框架测试、应用——通过反射这一方式。你也可以向你的用户定义属性,或是使用预定义属性,详见下表:

属性 使用对象 目的
DebuggerDisplay Debugger Debugger display 格式
InternalsVisibleTo Member access 使用特定类来暴露内部成员去指定其他的类。基于此方法,测试方法可以用来保护成员,并且persistence层可以用一些特殊的隐蔽方法。
DefaultValue Properties 为属性指定一个缺省值

一定要对DebuggerStepThrough多重视几分——否则它会在这个方法应用的地方让寻找bug变得十分困难,你也会因此而跳过某步或是推倒而重做它。

9.Debug

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的属性值表现改变了你的对象的状态。

10.性能优化

早做计划,不断监测,后做优化

在设计阶段,制定切实可行的目标。在开发阶段,专注于代码的正确性要比去做微调整有意义的多。对于你的目标,你要在开发过程中多进行监测。只需要在你没有达到预期的目标的时候,你才应该去花时间对程序做一个调整。

请记住用合适的工具来确保性能的经验性测量,并且使测试处于这样一种环境当中:可反复多次测试,并且测试过程尽量与现实当中用户的使用习惯一致。

当你对性能进行测试的时候,一定要注意你真正所关心的测试目标是什么。在进行某一项功能的测试时,你的测试有没有包含这项功能的调用或者是回路构造的开销?

我们都听说过很多比别人做得快很多的项目神话,不要盲目相信这些,试验与测试才是实在的东西。

由于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);

11.资源管理

垃圾收集器(garbage collector)可以自动地清理内存。即使这样,一切被抛弃的资源也需要适当的处理——特别是那些垃圾收集器不能管理的资源。

资源管理问题的常见来源  
内存碎片 如果没有足够大的连续的虚拟地址存储空间,可能会导致分配失败
进程限制 进程通常都可以读取内存的所有子集,以及系统可用的资源。
资源泄露 垃圾收集器只管理内存,其他资源需要由应用程序正确管理。
不稳定资源 那些依赖于垃圾收集器与终结器(finalizers)的资源在很久没用过的时候,不可被立即调用。实际上它们可能永远不可能被调用。
   

利用try/finally block来确保资源已被合理释放,或是让你的类使用IDisposable,以及更方便更安全的声明方式。

using (StreamReader reader=new StreamReader(file)) 
{ 
 //your code here

在产品代码中避免garbage collector

除了用调用GC.Collect()干扰garbage collector之外,也可以考虑适当地释放或是抛弃资源。在进行性能测试时,如果你可以承担这种影响带来的后果,你再去使用garbage collector。

避免编写finalizers

与当前一些流传的谣言不同的是,你的类不需要Finalizers,而这只是因为IDisposable的存在!你可以让IDisposable赋予你的类在任何已拥有的组合实例中调用Dispose的能力,但是finalizers只能在拥有未管理的资源类中使用。

Finalizers主要对交互式Win32位句柄API有很大作用,并且SafeHandle句柄是很容易利用的。

不要总是设想你的finalizers(总是在finalizer线程上运行的)会很好地与其他对象进行交互。那些其他的对象可能在该进程之前就被终止掉了。

12.并发性

处理并发性与多线程编程是件复杂的、困难的事情。在将并发性添加进你的程序之前,请确保你已经明确了解你的做的是什么——因为这里面有太多门道了!

多线程软件的情况很难进行预测,比如很容易产生如竞争条件与死锁的问题,而这些问题并不是仅仅影响单线程应用。基于这些风险,你应该将多线程视为最后一种手段。如果不得不使用多线程,尽量缩减多线程同时使用内存的需求。如果必须使线程同步,请尽可能地使用最高等级的同步机制。在最高等级的前提下,包括了这些机制:

  • Async-await/Task Parallel Library/Lazy


  • Lock/monitor/AutoResetEvent


  • Interlocked/Semaphore


  • 可变域与显式barrier

以上的这些很难解释清楚C#/.NET的复杂之处。如果你想开发一个正常的并发应用,可以去参阅O’Reilly的《Concurrency in C# Cookboo》。

使用Volatile

将一个域标记为“volatile”是一种高级特性,而这种设置也经常被专家所误解。C#的编译器会保证目标域可以被获取与释放语义,但是被lock的域就不适用于这种情况。如果你不知道获取什么,不知道释放什么语义,以及它们是怎样影响CPU层次的优化,那么久避免使用volatile域。取而代之的可以用更高层次的工具,比如Task Parallel Library或是CancellationToken。

线程安全与内置方法

标准库类型常提供使对象线程安全更容易的方法。例如Dictionary.TryGetValue()。使用此类方法一般可以使你的代码变得更加清爽,并且你也不必担心像TOCTOU(time-of-check-time-of-use竞争危害的一种)这样的数据竞争。

不要锁住“this”、字符串,或是其他普通public的对象

当使用在多线程环境下的一些类时,多注意lock的使用。锁住字符串常量,或是其他公共对象,会阻止你锁状态下的封装,还可能会导致死锁。你需要阻止其他代码锁定在同一使用的对象上,当然你最好的选择是使用private对象成员项。

13.避免常见的错误

Null

滥用null是一种常见的导致程序错误的来源,这种非正常操作可能会使程序崩溃或是其他的异常。如果你试图获取一个null的引用,就好像它是某对象的有效引用值(例如通过获取一个属性或是方法),那么在运行时就会抛出一个NullReferenceException。

静态与动态分析工具可以在你发布代码之前为你检查出潜在的NullReferenceException。在C#当中,引用型为null通常是由于变量没有引用到某个对象而造成的。对于值可为空的类型与引用型来说,是可以使用null的。例如:Nullable,空委托,已注销的事件,“as”转化失败的,以及一些其他的情况。

每个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.RemoveAll:

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 翻译:码农网

以上がC# 開発者として知っておくべき 13 のことの詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

声明:
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。