web-dev-qa-db-ja.com

拡張メソッドを使用してC#イベントを発生させる-それは悪いことですか?

私たちは皆、C#イベント宣言である恐怖に精通しています。スレッドセーフを確保するために、 標準は次のようなものを書くことです

public event EventHandler SomethingHappened;
protected virtual void OnSomethingHappened(EventArgs e)
{            
    var handler = SomethingHappened;
    if (handler != null)
        handler(this, e);
}

最近、このボードに関する他のいくつかの質問(私は今は見つけることができません)で、誰かがこのシナリオで拡張メソッドをうまく使用できると指摘しました。これを行う1つの方法は次のとおりです。

static public class EventExtensions
{
    static public void RaiseEvent(this EventHandler @event, object sender, EventArgs e)
    {
        var handler = @event;
        if (handler != null)
            handler(sender, e);
    }
    static public void RaiseEvent<T>(this EventHandler<T> @event, object sender, T e)
        where T : EventArgs
    {
        var handler = @event;
        if (handler != null)
            handler(sender, e);
    }
}

これらの拡張メソッドを配置すると、イベントを宣言して発生させるために必要なのは、次のようなものだけです。

public event EventHandler SomethingHappened;

void SomeMethod()
{
    this.SomethingHappened.RaiseEvent(this, EventArgs.Empty);
}

私の質問:これは良い考えですか?標準のOnメソッドがないために何かが足りませんか? (私が気づいたことの1つは、明示的な追加/削除コードを持つイベントでは機能しないことです。)

50
Ryan Lundy

明示的な追加/削除があるイベントでも引き続き機能します。イベント名の代わりにデリゲート変数を使用する必要があります(またはデリゲートを保存した場合)。

ただし、スレッドセーフにする簡単な方法があります。no-opハンドラーで初期化します。

public event EventHandler SomethingHappened = delegate {};

追加のデリゲートを呼び出すことによるパフォーマンスへの影響はごくわずかであり、コードが確実に簡単になります。

ちなみに、拡張メソッドでは、追加のローカル変数は必要ありません。次のようにするだけです。

static public void RaiseEvent(this EventHandler @event, object sender, EventArgs e)
{
    if (@event != null)
        @event(sender, e);
}

static public void RaiseEvent<T>(this EventHandler<T> @event, object sender, T e)
    where T : EventArgs
{
    if (@event != null)
        @event(sender, e);
}

個人的にはパラメータ名としてキーワードを使用しませんが、呼び出し側はまったく変更されないので、必要なことを実行してください:)

編集:「OnXXX」メソッドについて:クラスの派生元を計画していますか?私の見解では、ほとんどのクラスは封印されるべきです。 doの場合、これらの派生クラスでイベントを発生させることができますか?これらの質問のいずれかに対する答えが「いいえ」の場合は、気にしないでください。両方の答えが「はい」の場合は、次のようにしてください:)

56
Jon Skeet

これでC#6が登場しました。イベントを発生させるための、よりコンパクトでスレッドセーフな方法があります。

_SomethingHappened?.Invoke(this, e);
_

Invoke()は、null条件演算子「?」のおかげで、デリゲートがイベントに登録されている場合(つまり、nullではない場合)にのみ呼び出されます。

質問の「ハンドラー」コードが解決しようとしているスレッドの問題は、このコードのようにSomethingHappenedに一度だけアクセスされるため、テストとテストの間にnullに設定される可能性がないため、ここでは回避されます。呼び出し。

この答えはおそらく元の質問に正接していますが、イベントを発生させるためのより簡単な方法を探している人にとっては非常に重要です。

10
Bob Sammers

[ここに考えがあります]

推奨される方法でコードを1回記述し、それで完了します。そうすれば、何か間違ったことをしたと思ってコードを見ている同僚を混乱させることはありませんか?

[イベントハンドラーの作成に費やすよりも、イベントハンドラーの作成方法を見つけようとする投稿をたくさん読んでいます。]

5
Robert Paulson

コードが少なく、読みやすくなっています。私が好きです。

パフォーマンスに興味がない場合は、次のようにイベントを宣言して、ヌルチェックを回避できます。

public event EventHandler SomethingHappened = delegate{};
3

ハンドラーをローカル変数に割り当てることで、スレッドセーフではありません"ensureing"。割り当て後もメソッドが中断される可能性があります。たとえば、イベントのリッスンに使用されていたクラスが中断中に破棄された場合、破棄されたクラスのメソッドを呼び出しています。

Null参照の例外から身を守ることができますが、Jon Skeetとcristianlibardoが回答で指摘しているように、これを行う簡単な方法があります。

もう1つのことは、封印されていないクラスの場合、OnFooメソッドは仮想である必要があるということです。これは、拡張メソッドでは不可能だと思います。

1
Isak Savo

上記の回答をさらに一歩進めるために、ハンドラーの1つが例外をスローするのを防ぐことができます。これが発生した場合、後続のハンドラーは呼び出されません。

同様に、ハンドラーをタスク化して、実行時間の長いハンドラーが後者のハンドラーに通知されるための過度の遅延を引き起こさないようにすることができます。これにより、実行時間の長いハンドラーによるハイジャックからソーススレッドを保護することもできます。

  public static class EventHandlerExtensions
  {
    private static readonly log4net.ILog _log = log4net.LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);

    public static void Taskify(this EventHandler theEvent, object sender, EventArgs args)
    {
      Invoke(theEvent, sender, args, true);
    }

    public static void Taskify<T>(this EventHandler<T> theEvent, object sender, T args)
    {
      Invoke(theEvent, sender, args, true);
    }

    public static void InvokeSafely(this EventHandler theEvent, object sender, EventArgs args)
    {
      Invoke(theEvent, sender, args, false);
    }

    public static void InvokeSafely<T>(this EventHandler<T> theEvent, object sender, T args)
    {
      Invoke(theEvent, sender, args, false);
    }

    private static void Invoke(this EventHandler theEvent, object sender, EventArgs args, bool taskify)
    {
      if (theEvent == null)
        return;

      foreach (EventHandler handler in theEvent.GetInvocationList())
      {
        var action = new Action(() =>
        {
          try
          {
            handler(sender, args);
          }
          catch (Exception ex)
          {
            _log.Error(ex);
          }
        });

        if (taskify)
          Task.Run(action);
        else
          action();
      }
    }

    private static void Invoke<T>(this EventHandler<T> theEvent, object sender, T args, bool taskify)
    {
      if (theEvent == null)
        return;

      foreach (EventHandler<T> handler in theEvent.GetInvocationList())
      {
        var action = new Action(() =>
        {
          try
          {
            handler(sender, args);
          }
          catch (Exception ex)
          {
            _log.Error(ex);
          }
        });

        if (taskify)
          Task.Run(action);
        else
          action();
      }
    }
  }
0
Bill Tarbell