イベント宣言に匿名の空のデリゲートを追加することには欠点がありますか?
-
05-07-2019 - |
質問
このイディオムについていくつか言及されています( 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つのデリゲートインスタンスのみであるため、巨大の保存ではありませんが、価値がある場合があります。
各クラスのインスタンスごとのフィールドは、もちろん同じスペースを使用します。
i.e。
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チェックと条件付き呼び出しを組み合わせています。
これまでのところ、この質問に対する回答として1つ欠落しています。 null値のチェックを避けるのは危険です。
public class X
{
public delegate void MyDelegate();
public MyDelegate MyFunnyCallback = delegate() { }
public void DoSomething()
{
MyFunnyCallback();
}
}
X x = new X();
x.MyFunnyCallback = delegate() { Console.WriteLine("Howdie"); }
x.DoSomething(); // works fine
// .. re-init x
x.MyFunnyCallback = null;
// .. continue
x.DoSomething(); // crashes with an exception
問題は、誰がどのようにコードを使用するかわからないことです。数年後にコードのバグ修正中にイベント/ハンドラーがnullに設定された場合、あなたは決して知りません。
常に、ifチェックを記述します。
役立つ希望;)
ps:パフォーマンス計算をありがとう。
pps:イベントケースとコールバックの例から編集しました。フィードバックをありがとう...私は「コーディング」しました;この例はVisual Studioなしで、念頭に置いた例をイベントに合わせて調整しました。混乱して申し訳ありません。
ppps:スレッドにまだ適合するかどうかわからない...しかし、それは重要な原則だと思う。 スタックフローの別のスレッド
も確認してください