コードレビューで、この(簡略化された)コードフラグメントを見つけて、イベントハンドラーの登録を解除しました。
_ Fire -= new MyDelegate(OnFire);
_
これは、イベントハンドラーの登録を解除しないと考えました。これは、これまで登録されたことのない新しいデリゲートを作成するためです。しかし、MSDNを検索すると、このイディオムを使用するコードサンプルがいくつか見つかりました。
だから私は実験を始めました:
_internal class Program
{
public delegate void MyDelegate(string msg);
public static event MyDelegate Fire;
private static void Main(string[] args)
{
Fire += new MyDelegate(OnFire);
Fire += new MyDelegate(OnFire);
Fire("Hello 1");
Fire -= new MyDelegate(OnFire);
Fire("Hello 2");
Fire -= new MyDelegate(OnFire);
Fire("Hello 3");
}
private static void OnFire(string msg)
{
Console.WriteLine("OnFire: {0}", msg);
}
}
_
驚いたことに、次のことが起こりました。
Fire("Hello 1");
は、予想どおり2つのメッセージを生成しました。Fire("Hello 2");
は1つのメッセージを生成しました!new
デリゲートの登録解除が機能することがわかりました。Fire("Hello 3");
はNullReferenceException
をスローしました。Fire
がnull
であることが示されました。イベントハンドラーとデリゲートについては、コンパイラーが舞台裏で多くのコードを生成することを知っています。しかし、私はまだ私の推論が間違っている理由を理解していません。
私は何が欠けていますか?
追加の質問:Fire
がnull
であるという事実から、イベントが登録されていない場合、イベントが発生するすべての場所で、null
に対するチェックが必要であると結論付けます。
イベントハンドラを追加するC#コンパイラのデフォルトの実装はDelegate.Combine
を呼び出し、イベントハンドラを削除するとDelegate.Remove
を呼び出します。
Fire = (MyDelegate) Delegate.Remove(Fire, new MyDelegate(Program.OnFire));
フレームワークのDelegate.Remove
の実装は、MyDelegate
オブジェクト自体ではなく、デリゲートが参照するメソッド(Program.OnFire
)を参照します。したがって、既存のイベントハンドラのサブスクライブを解除するときに、新しいMyDelegate
オブジェクトを作成してもまったく安全です。このため、C#コンパイラーでは、イベントハンドラーを追加/削除する際に、簡単な構文(舞台裏でまったく同じコードを生成する)を使用できます。new MyDelegate
部分は省略できます。
Fire += OnFire;
Fire -= OnFire;
最後のデリゲートがイベントハンドラーから削除されると、Delegate.Remove
はnullを返します。お気付きのとおり、イベントを発生させる前にイベントをnullと照合することが不可欠です。
MyDelegate handler = Fire;
if (handler != null)
handler("Hello 3");
一時的なローカル変数に割り当てられ、他のスレッドでサブスクライブを解除するイベントハンドラーによる競合状態を防ぎます。 (イベントハンドラーをローカル変数に割り当てるスレッドセーフの詳細については、 私のブログ投稿 を参照してください。)この問題を防ぐ別の方法は、常にサブスクライブされる空のデリゲートを作成することです。これはもう少し多くのメモリを使用しますが、イベントハンドラーをnullにすることはできません(コードはより単純になります)。
public static event MyDelegate Fire = delegate { };
デリゲートを起動する前に、デリゲートにターゲットがない(値がnullである)かどうかを常に確認する必要があります。前述したように、これを行う1つの方法は、削除されないdo-nothing匿名メソッドでサブスクライブすることです。
public event MyDelegate Fire = delegate {};
ただし、これはNullReferenceExceptionsを回避するための単なるハックです。
呼び出し前にデリゲートがnullであるかどうかをチェックするだけではスレッドセーフではありません。他のスレッドはnullチェック後に登録解除でき、呼び出し時にnullにすることができます。他の解決策は、デリゲートを一時変数にコピーすることです。
public event MyDelegate Fire;
public void FireEvent(string msg)
{
MyDelegate temp = Fire;
if (temp != null)
temp(msg);
}
残念ながら、JITコンパイラはコードを最適化し、一時変数を削除し、元のデリゲートを使用する場合があります。 (Juval Lowy-.NETコンポーネントのプログラミングによる)
したがって、この問題を回避するには、デリゲートをパラメーターとして受け入れるメソッドを使用できます。
[MethodImpl(MethodImplOptions.NoInlining)]
public void FireEvent(MyDelegate fire, string msg)
{
if (fire != null)
fire(msg);
}
MethodImpl(NoInlining)属性がないと、JITコンパイラーはメソッドをインライン化して価値がないことに注意してください。デリゲートは不変なので、この実装はスレッドセーフです。このメソッドは次のように使用できます。
FireEvent(Fire,"Hello 3");