67

コード レビューで、イベント ハンドラーの登録を解除するために、この (簡略化された) コード フラグメントに出くわしました。

 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);
    }

}

驚いたことに、次のことが起こりました。

  1. Fire("Hello 1");予想どおり、2 つのメッセージが生成されました。
  2. Fire("Hello 2");1つのメッセージを作成しました!これにより、デリゲートの
    登録解除が機能することが確信できました。new
  3. Fire("Hello 3");を投げたNullReferenceException
    コードをデバッグすると、イベントの登録を解除した後であることFireが示されました。null

イベント ハンドラーとデリゲートについては、コンパイラが舞台裏で多くのコードを生成することを知っています。しかし、なぜ私の推論が間違っているのか、まだ理解できません。

私は何が欠けていますか?

Fire追加の質問: イベントが登録されていないという事実から、nullイベントが発生するすべての場所でチェックnullが必要であると結論付けます。

4

2 に答える 2

85

Delegate.Combineイベント ハンドラーの呼び出しを追加し、イベント ハンドラーの呼び出しを削除するC# コンパイラの既定の実装Delegate.Remove:

Fire = (MyDelegate) Delegate.Remove(Fire, new MyDelegate(Program.OnFire));

フレームワークの の実装は、オブジェクト自体ではなく、デリゲートが参照するメソッド ( )Delegate.Removeを調べます。したがって、既存のイベント ハンドラーのサブスクライブを解除するときに、新しいオブジェクトを作成することは完全に安全です。このため、C# コンパイラでは、イベント ハンドラーを追加/削除するときに省略形の構文 (舞台裏でまったく同じコードを生成する) を使用できます。次の部分を省略できます。MyDelegateProgram.OnFireMyDelegatenew MyDelegate

Fire += OnFire;
Fire -= OnFire;

最後のデリゲートがイベント ハンドラーから削除されると、Delegate.Removenull が返されます。お気づきのように、イベントを発生させる前に null に対してイベントをチェックすることが不可欠です。

MyDelegate handler = Fire;
if (handler != null)
    handler("Hello 3");

これは、一時的なローカル変数に割り当てられ、他のスレッドでイベント ハンドラーのサブスクライブを解除することで発生する可能性のある競合状態を防ぎます。(イベント ハンドラーをローカル変数に割り当てるスレッド セーフの詳細については、私のブログ投稿を参照してください。) この問題を防ぐもう 1 つの方法は、常にサブスクライブされる空のデリゲートを作成することです。これは少し多くのメモリを使用しますが、イベント ハンドラーを null にすることはできません (そして、コードをより単純にすることができます)。

public static event MyDelegate Fire = delegate { };
于 2008-11-15T18:00:47.613 に答える
15

デリゲートを起動する前に、デリゲートにターゲットがない (値が null である) かどうかを常に確認する必要があります。前に述べたように、これを行う 1 つの方法は、削除されない何もしない匿名メソッドでサブスクライブすることです。

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");
于 2008-11-15T20:42:29.143 に答える