web-dev-qa-db-ja.com

イベント宣言に匿名の空のデリゲートを追加することの欠点はありますか?

私はこのイディオムについていくつか言及しました( SO を含む):

// Deliberately empty subscriber
public event EventHandler AskQuestion = delegate {};

利点は明らかです。イベントを発生させる前にnullをチェックする必要がなくなります。

しかし、欠点があるかどうかを理解したいと思っています。たとえば、広く使用されており、十分に透明であるため、メンテナンスの頭痛の種ですか?空のイベントサブスクライバーコールのパフォーマンスにかなりの影響はありますか?

82
serg10

唯一の欠点は、余分な空のデリゲートを呼び出すため、パフォーマンスがわずかに低下することです。それ以外には、メンテナンスペナルティやその他の欠点はありません。

36
Maurice

パフォーマンスのオーバーヘッドを引き起こす代わりに、両方の問題を軽減するために 拡張メソッドを使用 しないのはなぜですか?

public static void Raise(this EventHandler handler, object sender, EventArgs e)
{
    if(handler != null)
    {
        handler(sender, e);
    }
}

一度定義すると、別のnullイベントチェックを再度実行する必要はありません。

// Works, even for null events.
MyButtonClick.Raise(this, EventArgs.Empty);
46

イベントを多用し、パフォーマンスが重要であるのシステムの場合、少なくとも考慮これを行わないことをお勧めします。 。空のデリゲートでイベントを発生させるためのコストは、最初にnullチェックで発生させる場合の約2倍です。

これが私のマシンでベンチマークを実行しているいくつかの数字です:

For 50000000 iterations . . .
No null check (empty delegate attached): 530ms
With null check (no delegates attached): 249ms
With null check (with delegate attached): 452ms

そして、これらの数字を取得するために使用したコードは次のとおりです。

using System;
using System.Diagnostics;

namespace ConsoleApplication1
{
    class Program
    {
        public event EventHandler<EventArgs> EventWithDelegate = delegate { };
        public event EventHandler<EventArgs> EventWithoutDelegate;

        static void Main(string[] args)
        {
            //warm up
            new Program().DoTimings(false);
            //do it for real
            new Program().DoTimings(true);

            Console.WriteLine("Done");
            Console.ReadKey();
        }

        private void DoTimings(bool output)
        {
            const int iterations = 50000000;

            if (output)
            {
                Console.WriteLine("For {0} iterations . . .", iterations);
            }

            //with anonymous delegate attached to avoid null checks
            var stopWatch = Stopwatch.StartNew();

            for (var i = 0; i < iterations; ++i)
            {
                RaiseWithAnonDelegate();
            }

            stopWatch.Stop();

            if (output)
            {
                Console.WriteLine("No null check (empty delegate attached): {0}ms", stopWatch.ElapsedMilliseconds);
            }


            //without any delegates attached (null check required)
            stopWatch = Stopwatch.StartNew();

            for (var i = 0; i < iterations; ++i)
            {
                RaiseWithoutAnonDelegate();
            }

            stopWatch.Stop();

            if (output)
            {
                Console.WriteLine("With null check (no delegates attached): {0}ms", stopWatch.ElapsedMilliseconds);
            }


            //attach delegate
            EventWithoutDelegate += delegate { };


            //with delegate attached (null check still performed)
            stopWatch = Stopwatch.StartNew();

            for (var i = 0; i < iterations; ++i)
            {
                RaiseWithoutAnonDelegate();
            }

            stopWatch.Stop();

            if (output)
            {
                Console.WriteLine("With null check (with delegate attached): {0}ms", stopWatch.ElapsedMilliseconds);
            }
        }

        private void RaiseWithAnonDelegate()
        {
            EventWithDelegate(this, EventArgs.Empty);
        }

        private void RaiseWithoutAnonDelegate()
        {
            var handler = EventWithoutDelegate;

            if (handler != null)
            {
                handler(this, EventArgs.Empty);
            }
        }
    }
}
42
Kent Boogaart

/ lot /を実行している場合は、デリゲートインスタンスの量を減らすために、再利用する単一の静的/共有の空のデリゲートが必要になる場合があります。コンパイラはとにかく(静的フィールドに)イベントごとにこのデリゲートをキャッシュするため、イベント定義ごとに1つのデリゲートインスタンスしかないため、巨大節約にはなりませんが、おそらく価値があることに注意してください。

もちろん、各クラスのインスタンスごとのフィールドは同じスペースを使用します。

つまり.

internal static class Foo
{
    internal static readonly EventHandler EmptyEvent = delegate { };
}
public class Bar
{
    public event EventHandler SomeEvent = Foo.EmptyEvent;
}

それ以外は問題ないようです。

7
Marc Gravell

空のデリゲートはスレッドセーフですが、nullチェックはそうではないことを理解しています。

2

次のようなことをしたくなるので、少し危険な構成だと思います。

MyEvent(this, EventArgs.Empty);

クライアントが例外をスローした場合、サーバーはそれに伴います。

それで、多分あなたはそうします:

try
{
  MyEvent(this, EventArgs.Empty);
}
catch
{
}

しかし、複数のサブスクライバーがいて、1つのサブスクライバーが例外をスローした場合、他のサブスクライバーはどうなりますか?

そのために、私はnullチェックを実行し、サブスクライバー側からの例外をすべて飲み込む静的ヘルパーメソッドをいくつか使用しています(これはidesignからのものです)。

// Usage
EventHelper.Fire(MyEvent, this, EventArgs.Empty);


public static void Fire(EventHandler del, object sender, EventArgs e)
{
    UnsafeFire(del, sender, e);
}
private static void UnsafeFire(Delegate del, params object[] args)
{
    if (del == null)
    {
        return;
    }
    Delegate[] delegates = del.GetInvocationList();

    foreach (Delegate sink in delegates)
    {
        try
        {
            sink.DynamicInvoke(args);
        }
        catch
        { }
    }
}
2
Scott P

いくつかの極端な状況を除いて、話すべき意味のあるパフォーマンスペナルティはありません。

ただし、C#6.0では、nullの可能性があるデリゲートを呼び出すための代替構文が言語で提供されるため、このトリックの関連性が低くなることに注意してください。

delegateThatCouldBeNull?.Invoke(this, value);

上記では、null条件演算子?.nullチェックと条件付き呼び出しを組み合わせます。

2
dasblinkenlight

「空のデリゲート」アプローチの代わりに、イベントハンドラーをnullに対してチェックする従来の方法をカプセル化するための単純な拡張メソッドを定義できます。 ここ および ここ と記述されています。

0
vkelman