web-dev-qa-db-ja.com

C#-匿名関数とイベントハンドラー

私は次のコードを持っています:

public List<IWFResourceInstance> FindStepsByType(IWFResource res)  
{  
    List<IWFResourceInstance> retval = new List<IWFResourceInstance>();  
    this.FoundStep += delegate(object sender, WalkerStepEventArgs e)   
                      {   
                        if (e.Step.ResourceType == res) retval.Add(e.Step);   
                      };  
    this.Start();  
    return retval;
}  

イベントメンバー(FoundStep)をローカルインプレース匿名関数に登録する方法に注意してください。

私の質問は:関数「FindStepByType」が終了するとき-匿名関数はイベントのデリゲートリストから自動的に削除されますか、それとも関数をステップアウトする前に手動で削除する必要がありますか? (そしてどうすればいいですか?)

私の質問が明確であることを望みます。

22
Adi Barda

あなたのコードにはいくつかの問題があります(あなたと他の人が特定したいくつか):

  • コーディングされているように、匿名デリゲートをイベントから削除することはできません。
  • thisのメンバーであるFoundStepに追加したため、匿名デリゲートはそれを呼び出すメソッドの寿命よりも長く存続します。
  • FindStepsByTypeへのすべてのエントリは、別の匿名デリゲートをFoundStepに追加します。
  • 匿名デリゲートはクロージャであり、事実上retvalの有効期間を延長するため、コードの他の場所でretvalの参照を停止した場合でも、匿名デリゲートによって保持されます。

これを修正し、匿名のデリゲートを引き続き使用するには、ローカル変数に割り当て、次にfinallyブロック内のハンドラーを削除します(ハンドラーが例外をスローする場合に必要です)。

  public List<IWFResourceInstance> FindStepsByType(IWFResource res)
  {
     List<IWFResourceInstance> retval = new List<IWFResourceInstance>();
     EventHandler<WalkerStepEventArgs> handler = (sender, e) =>
     {
        if (e.Step.ResourceType == res) retval.Add(e.Step);
     };

     this.FoundStep += handler;

     try
     {
        this.Start();
     }
     finally
     {
        this.FoundStep -= handler;
     }

     return retval;
  }

C#7.0以降では、匿名デリゲートをローカル関数に置き換えることができ、同じ効果が得られます。

    public List<IWFResourceInstance> FindStepsByType(IWFResource res)
    {
        var retval = new List<IWFResourceInstance>();

        void Handler(object sender, WalkerStepEventArgs e)
        {
            if (e.Step.ResourceType == res) retval.Add(e.Step);
        }

        FoundStep += Handler;

        try
        {
            this.Start();
        }
        finally
        {
            FoundStep -= Handler;
        }

        return retval;
    }
41
Kit

以下は、匿名メソッドでイベントをサブスクライブ解除する方法に関するアプローチです。

DispatcherTimer _timer = new DispatcherTimer();
_timer.Interval = TimeSpan.FromMilliseconds(1000);
EventHandler handler = null;

int i = 0;

_timer.Tick += handler = new EventHandler(delegate(object s, EventArgs ev)
{
    i++;
    if(i==10)
        _timer.Tick -= handler;
});

_timer.Start();
9
Amit

いいえ、自動的には削除されません。この意味で、匿名メソッドと「通常の」メソッドの間に違いはありません。必要に応じて、手動でイベントの登録を解除してください。

実際には、他の変数(例ではres)をキャプチャし、それらを存続させます(ガベージコレクターがそれらを収集しないようにします)。

5
Mehrdad Afshari

匿名デリゲート(またはラムダ式)を使用してイベントをサブスクライブすると、後でそのイベントのサブスクライブを簡単に解除できなくなります。イベントハンドラーが自動的にサブスクライブ解除されることはありません。

コードを見ると、関数でイベントを宣言してサブスクライブしても、サブスクライブするイベントはクラス上にあるため、サブスクライブすると、関数が終了した後も常にサブスクライブされます。もう1つ重要なことは、この関数が呼び出されるたびに、再びイベントをサブスクライブすることです。イベントは本質的にマルチキャストデリゲートであり、複数のサブスクライバーを許可するため、これは完全に合法です。 (これは、意図したものである場合とそうでない場合があります。)

関数を終了する前にデリゲートからサブスクライブを解除するには、匿名デリゲートをデリゲート変数に格納し、デリゲートをイベントに追加する必要があります。その後、関数が終了する前に、イベントからデリゲートを削除できます。

これらの理由により、後でイベントの登録を解除する必要がある場合は、匿名のデリゲートを使用することはお勧めしません。 方法:イベントのサブスクライブとサブスクライブの解除(C#プログラミングガイド)(特に、「イベントをサブスクライブするには匿名メソッドを使用して」)。

2
Scott Dorman