私はこのイディオムについていくつか言及しました( SO を含む):
// Deliberately empty subscriber
public event EventHandler AskQuestion = delegate {};
利点は明らかです。イベントを発生させる前にnullをチェックする必要がなくなります。
しかし、欠点があるかどうかを理解したいと思っています。たとえば、広く使用されており、十分に透明であるため、メンテナンスの頭痛の種ですか?空のイベントサブスクライバーコールのパフォーマンスにかなりの影響はありますか?
唯一の欠点は、余分な空のデリゲートを呼び出すため、パフォーマンスがわずかに低下することです。それ以外には、メンテナンスペナルティやその他の欠点はありません。
パフォーマンスのオーバーヘッドを引き起こす代わりに、両方の問題を軽減するために 拡張メソッドを使用 しないのはなぜですか?
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);
イベントを多用し、パフォーマンスが重要であるのシステムの場合、少なくとも考慮これを行わないことをお勧めします。 。空のデリゲートでイベントを発生させるためのコストは、最初に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);
}
}
}
}
/ lot /を実行している場合は、デリゲートインスタンスの量を減らすために、再利用する単一の静的/共有の空のデリゲートが必要になる場合があります。コンパイラはとにかく(静的フィールドに)イベントごとにこのデリゲートをキャッシュするため、イベント定義ごとに1つのデリゲートインスタンスしかないため、巨大節約にはなりませんが、おそらく価値があることに注意してください。
もちろん、各クラスのインスタンスごとのフィールドは同じスペースを使用します。
つまり.
internal static class Foo
{
internal static readonly EventHandler EmptyEvent = delegate { };
}
public class Bar
{
public event EventHandler SomeEvent = Foo.EmptyEvent;
}
それ以外は問題ないようです。
空のデリゲートはスレッドセーフですが、nullチェックはそうではないことを理解しています。
次のようなことをしたくなるので、少し危険な構成だと思います。
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
{ }
}
}
いくつかの極端な状況を除いて、話すべき意味のあるパフォーマンスペナルティはありません。
ただし、C#6.0では、nullの可能性があるデリゲートを呼び出すための代替構文が言語で提供されるため、このトリックの関連性が低くなることに注意してください。
delegateThatCouldBeNull?.Invoke(this, value);
上記では、null条件演算子?.
nullチェックと条件付き呼び出しを組み合わせます。