翻訳者から
多くの場合、初心者の開発者は、ハンドラーを呼び出すときに、ハンドラーをローカル変数にコピーする必要がある理由を尋ねます。 C#6では、言語開発者は、ヌル条件演算子(ヌル条件演算子またはエルビス演算子-
?。 )を含む多くの構文糖を追加しました
。これにより、不要な(一見)割り当てを取り除くことができます。 カットの下で、ジョン・スキートからの説明-最も有名なピルボックスの1つはグルではありません。
問題
サブスクライバーを持たないイベントはnull参照として表されるため、C#でのハンドラーの呼び出しには常に、最も明白なコードは含まれていません。 このため、通常次のように書きました。
public event EventHandler Foo; public void OnFoo() { EventHandler handler = Foo; if (handler != null) { handler(this, EventArgs.Empty); } }
ローカル変数ハンドラーを使用する必要があります。ローカル変数ハンドラーがない場合、Fooイベントハンドラーへのアクセスは2回アクセスされます(nullのチェック時および呼び出し中)。 この場合、これらのFooへのアクセスの間に最後のサブスクライバーが削除される可能性があります。
このメソッドは、拡張メソッドを作成することにより簡素化できます。
public static void Raise(this EventHandler handler, object sender, EventArgs args) { if (handler != null) { handler(sender, args); } }
次に、この拡張メソッドを使用して、最初の呼び出しが書き換えられます。
public void OnFoo() { Foo.Raise(this, EventArgs.Empty); }
このアプローチの欠点は、ハンドラーのタイプごとに拡張メソッドを作成する必要があることです。
C#6は私たちを救います!
C#6で導入されたnull条件演算子(
?。 )は、プロパティへのアクセスだけでなく、メソッドの呼び出しにも使用できます。 コンパイラは式を一度だけ評価するため、拡張メソッドを使用せずにコードを記述できます。
public void OnFoo() { Foo?.Invoke(this, EventArgs.Empty); }
やった! このコードはNullReferenceExceptionをスローすることはなく、ヘルパークラスは必要ありません。
もちろん、Foo?(これ、EventArgs.Empty)を記述できれば良いのですが、そうではありませ
ん。 演算子は、言語を少し複雑にします。 したがって、追加のInvoke呼び出しはあまり気にしません。
このスレッドセーフティとは何ですか?
私たちが書いたコードは、他のスレッドが何をするかを気にしないという意味で「スレッドセーフ」です。NullReferenceExceptionを受け取ることはありません。 ただし、他のストリームがイベントをサブスクライブまたはサブスクライブ解除する場合、イベントサブスクライバーのリストに最新の変更が表示されない場合があります。 これは、共通メモリモデルの実装が困難なためです。
C#4では、イベントはInterlocked.CompareExchangeメソッドを使用して実装されているため、正しいInterlocked.CompareExchangeメソッドを使用するだけで、最新の値を取得できます。 これで、これら2つのアプローチを組み合わせて記述できます。
public void OnFoo() { Interlocked.CompareExchange(ref Foo, null, null)?.Invoke(this, EventArgs.Empty); }
これで、
追加のコードを記述することなく、NullReferenceExceptionに陥るリスクなしに、最新のサブスクライバーセットに通知できます。 この機会を思い出させてくれた
David Fowlerに感謝します。
もちろん、CompareExchangeの呼び出しは見苦しくなります。 .NET 4.5以降では、問題
を解決
できる Volatile.Readメソッドがありますが、(ドキュメントを読んだ場合)このメソッドが必要なことを行うかどうかは完全にはわかりません。 (メソッドの説明では、このメソッドの前に後続の読み取り/書き込み操作を設定することは禁止されています。この場合
、この可変読み取りの
後に 以前の書き込み操作を設定することを禁止する必要があります)。
public void OnFoo() {
私はすべてを予見したかどうかわからないため、このアプローチは好きではありません。 上級読者は、このアプローチが真実ではなく、BCLに入らなかった理由を提案できるかもしれません。
代替アプローチ
以前は、この代替ソリューションを使用していました。ラムダ式よりも優れている匿名メソッドの利点の1つ、パラメーターのリストを指定しない機能を使用して、空のダミーイベントハンドラーを作成します。
public event EventHandler Foo = delegate {} public void OnFoo() {
このアプローチでは、サブスクライバーの最新のリストを呼び出せない可能性があるという事実にはまだ問題がありますが、nullおよびNullReferenceExceptionのチェックについて心配する必要はありません。
MSILの探索
翻訳者から:この部分はジョンの記事にはありません。これはildasmでの私の個人的な研究です。
さまざまなケースでどのMSILコードが生成されるかを見てみましょう。
悪いコード public event EventHandler Foo; public void OnFoo() { if (Foo != null) { Foo(this, EventArgs.Empty); } } .method public hidebysig instance void OnFoo() cil managed {
このコードでは、Fooフィールドを2回参照しています。NULL(IL_0002:ldfld)と実際の呼び出し(IL_0010:ldfld)との比較のためです。 一方、Fooでnullの等価性とそのアクセス方法を確認し、スタックに配置してメソッドを呼び出すと、最後のサブスクライバーがイベントからサブスクライブを解除でき、nullが再度読み込まれます(hello、NullReferenceException)。
追加のローカル変数を使用して問題を解決する方法を見てみましょう。
変数を使用する public event EventHandler Foo; public void OnFoo() { EventHandler handler = Foo; if (handler != null) { handler(this, EventArgs.Empty); } } .method public hidebysig instance void OnFoo() cil managed {
この場合、すべてが単純です。Fooへのアクセスは1回発生し(IL_0002:ldfld)、すべての作業は変数ハンドラーで行われるため、NullReferenceExceptionが発生する危険はありません。
次に、
?演算子を使用したソリューション
。 。
C#6 public event EventHandler Foo; public void OnFoo() { Foo?.Invoke(this, EventArgs.Empty); } .method public hidebysig instance void OnFoo() cil managed {
C#6では
?演算子を使用し
ます。 すべてがより面白くなります。 Fooフィールドをスタックに配置して複製し(IL_0007:dup-すべての魔法はここにあります)、それがnullでない場合は、IL_000dに移動してInvokeメソッドを呼び出します。 Foo == nullの場合、スタックをクリアして終了します(IL_000b:br.s IL_0019)。 実際にFooを読み取るのは1回だけなので、NullReferenceExceptionは発生しません。
演算子
?を使用し
ます。 およびInterlocked.CompareExchange。
Interlocked.CompareExchange public event EventHandler Foo; public void OnFoo() { Interlocked.CompareExchange(ref Foo, null, null)?.Invoke(this, EventArgs.Empty); } .method public hidebysig instance void OnFoo() cil managed {
このコードは、Interlocked.CompareExchange(IL_0009:call !! 0 [mscorlib] System.Threading.Interlocked :: CompareExchange)を呼び出すことのみが前のコードと異なり、コードは前のメソッド(IL_000eで始まる)とまったく同じです。
演算子
?を使用し
ます。 と揮発性。
揮発性 public event EventHandler Foo; public void OnFoo() { Volatile.Read(ref Foo)?.Invoke(this, EventArgs.Empty); } .method public hidebysig instance void OnFoo() cil managed {
この場合、Interlocked.CompareExchange呼び出しはVolatile.Read呼び出しに変更され、その後(IL_000c:dupで始まる)すべてが変更されません。
?を使用するすべてのソリューション フィールドに1回アクセスするだけで異なり、コピーを使用してハンドラーを呼び出します(MSIL dupコマンド)。したがって、Invokeを呼び出してオブジェクトの正確なコピーを作成し、nullと比較してNullReferenceExceptionを発生させることはできません。 それ以外の場合、メソッドは、マルチスレッド環境で変更をキャッチする速さのみが異なります。
おわりに
はい、C#6ドライブ-初めてではありません。 そして、私たちはすでに安定したバージョンが利用可能です!