Gibt es einen Nachteil beim Hinzufügen eines anonymen leeren Delegaten zur Ereignisdeklaration?


82

Ich habe einige Erwähnungen dieser Redewendung gesehen (auch auf SO ):

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

Der Vorteil ist klar - es wird vermieden, dass vor dem Auslösen des Ereignisses nach Null gesucht werden muss.

Ich bin jedoch sehr gespannt, ob es Nachteile gibt. Ist es beispielsweise weit verbreitet und transparent genug, um keine Wartungsprobleme zu verursachen? Gibt es einen nennenswerten Leistungseinbruch beim leeren Teilnehmeranruf?

Antworten:


36

Der einzige Nachteil ist eine geringfügige Leistungsminderung, da Sie einen zusätzlichen leeren Delegierten anrufen. Ansonsten gibt es keine Wartungsstrafe oder andere Nachteile.


19
Wenn das Ereignis ansonsten genau einen Abonnenten hätte (ein sehr häufiger Fall), wird es vom Dummy-Handler auf zwei Teilnehmer festgelegt . Ereignisse mit einem Handler werden viel effizienter behandelt als Ereignisse mit zwei.
Superkatze

46

Verwenden Sie eine Erweiterungsmethode , um beide Probleme zu lösen, anstatt Leistungsaufwand zu verursachen:

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

Einmal definiert, müssen Sie nie wieder eine Null-Ereignisprüfung durchführen:

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

2
Eine generische Version finden Sie hier: stackoverflow.com/questions/192980/…
Benjol

1
Genau das, was meine Helfer-Trinity-Bibliothek übrigens macht: thehelpertrinity.codeplex.com
Kent Boogaart

3
Verschiebt dies nicht einfach das Thread-Problem der Nullprüfung in Ihre Erweiterungsmethode?
PatrickV

6
Der Handler wird an die Methode übergeben. Zu diesem Zeitpunkt kann diese Instanz nicht mehr geändert werden.
Judah Gabriel Himango

@PatrickV Nein, Judah hat recht, der handlerobige Parameter ist ein Wertparameter für die Methode. Es wird sich nicht ändern, während die Methode ausgeführt wird. Dazu müsste es sich um einen refParameter handeln (offensichtlich darf ein Parameter nicht sowohl den refals auch den thisModifikator enthalten) oder natürlich um ein Feld.
Jeppe Stig Nielsen

42

Bei Systemen, die Ereignisse stark nutzen und leistungskritisch sind , sollten Sie dies zumindest in Betracht ziehen nicht . Die Kosten für das Auslösen eines Ereignisses mit einem leeren Delegierten sind ungefähr doppelt so hoch wie die Kosten für das Auslösen mit einem Null-Check.

Hier sind einige Zahlen, die Benchmarks auf meinem Computer ausführen:

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

Und hier ist der Code, mit dem ich diese Zahlen erhalten habe:

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);
            }
        }
    }
}

11
Du machst Scherze, oder? Der Aufruf fügt 5 Nanosekunden hinzu und Sie warnen davor? Ich kann mir keine unvernünftigere allgemeine Optimierung vorstellen.
Brad Wilson

8
Interessant. Nach Ihren Erkenntnissen ist es schneller, nach Null zu suchen und einen Delegaten anzurufen, als ihn nur ohne Prüfung aufzurufen. Klingt für mich nicht richtig. Trotzdem ist dies ein so kleiner Unterschied, dass ich glaube, dass er nur in den extremsten Fällen spürbar ist.
Maurice

22
Brad, sagte ich speziell für leistungskritische Systeme, die Ereignisse stark nutzen. Wie ist das allgemein?
Kent Boogaart

3
Sie sprechen von Leistungseinbußen, wenn Sie leere Delegierte anrufen. Ich frage Sie: Was passiert, wenn jemand die Veranstaltung tatsächlich abonniert? Sie sollten sich um die Leistung der Abonnenten und nicht der leeren Delegierten sorgen.
Liviu Trifoi

2
Die hohen Leistungskosten entstehen nicht, wenn es keine "echten" Abonnenten gibt, sondern wenn es einen gibt. In diesem Fall sollten Abonnement, Abmeldung und Aufruf sehr effizient sein, da sie nichts mit Aufruflisten zu tun haben, aber die Tatsache, dass das Ereignis (aus Sicht des Systems) zwei Abonnenten anstelle von einem hat, erzwingt die Verwendung des Codes, der "n" Abonnenten behandelt, anstatt Code, der für den Fall "Null" oder "Eins" optimiert ist.
Supercat

7

Wenn Sie es mit / lot / tun, möchten Sie möglicherweise einen einzelnen statischen / gemeinsam genutzten leeren Delegaten haben, den Sie wiederverwenden, um einfach das Volumen der Delegateninstanzen zu verringern. Beachten Sie, dass der Compiler diesen Delegaten ohnehin pro Ereignis (in einem statischen Feld) zwischenspeichert, sodass es sich nur um eine Delegateninstanz pro Ereignisdefinition handelt. Dies ist also keine große Einsparung - aber möglicherweise sinnvoll.

Das Feld pro Instanz in jeder Klasse nimmt natürlich immer noch den gleichen Platz ein.

dh

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

Davon abgesehen scheint es in Ordnung zu sein.


1
Aus der Antwort hier geht hervor: stackoverflow.com/questions/703014/…, dass der Compiler die Optimierung bereits für eine einzelne Instanz durchführt.
Govert

3

Es gibt keine sinnvollen Leistungseinbußen, über die man sprechen könnte, außer möglicherweise für einige extreme Situationen.

Beachten Sie jedoch, dass dieser Trick in C # 6.0 weniger relevant wird, da die Sprache eine alternative Syntax zum Aufrufen von Delegaten bietet, die möglicherweise null ist:

delegateThatCouldBeNull?.Invoke(this, value);

Oben ?.kombiniert der bedingte Nulloperator die Nullprüfung mit einem bedingten Aufruf.



2

Ich würde sagen, es ist ein gefährliches Konstrukt, weil es Sie dazu verleitet, etwas zu tun wie:

MyEvent(this, EventArgs.Empty);

Wenn der Client eine Ausnahme auslöst, geht der Server mit.

Vielleicht tun Sie es also:

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

Was passiert jedoch mit den anderen Abonnenten, wenn Sie mehrere Abonnenten haben und ein Abonnent eine Ausnahme auslöst?

Zu diesem Zweck habe ich einige statische Hilfsmethoden verwendet, die die Nullprüfung durchführen und jede Ausnahme von der Abonnentenseite verschlucken (dies ist von 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
        { }
    }
}

Nicht zu picken, aber nicht <code> if (del == null) {return; } Delegate [] delegates = del.GetInvocationList (); </ code> ist ein Kandidat für eine Rennbedingung?
Cherian

Nicht ganz. Da Delegaten Werttypen sind, ist del tatsächlich eine private Kopie der Delegatenkette, auf die nur der UnsafeFire-Methodenkörper zugreifen kann. (Vorsichtsmaßnahme: Dies schlägt fehl, wenn UnsafeFire inliniert wird. Sie müssen daher das Attribut [MethodImpl (MethodImplOptions.NoInlining)] verwenden, um dagegen vorzugehen.)
Daniel Fortunov

1) Delegierte sind Referenztypen 2) Sie sind unveränderlich, es handelt sich also nicht um eine Rennbedingung. 3) Ich glaube nicht, dass Inlining das Verhalten dieses Codes ändert. Ich würde erwarten, dass der Parameter del eine neue lokale Variable wird, wenn er inline ist.
CodesInChaos

Ihre Lösung für "Was passiert, wenn eine Ausnahme ausgelöst wird" besteht darin, alle Ausnahmen zu ignorieren. Aus diesem Grund verpacke ich immer oder fast immer alle Ereignisabonnements in einem Versuch (mit einer praktischen Try.TryAction()Funktion, nicht mit einem expliziten try{}Block), aber ich ignoriere die Ausnahmen nicht, ich melde sie ...
Dave Cousineau

0

Anstelle des Ansatzes "leerer Delegat" kann eine einfache Erweiterungsmethode definiert werden, um die herkömmliche Methode zum Überprüfen des Ereignishandlers gegen Null zu kapseln. Es wird hier und hier beschrieben .


-2

Als Antwort auf diese Frage fehlt bisher eines: Es ist gefährlich, die Prüfung auf den Nullwert zu vermeiden .

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

Die Sache ist: Sie wissen nie, wer Ihren Code auf welche Weise verwenden wird. Sie wissen nie, ob in einigen Jahren während einer Fehlerbehebung Ihres Codes das Ereignis / der Handler auf null gesetzt ist.

Schreiben Sie immer den if-Check.

Hoffentlich hilft das ;)

ps: Danke für die Leistungsberechnung.

pps: Bearbeitet von einem Ereignisfall zu einem Rückrufbeispiel. Vielen Dank für das Feedback ... Ich habe das Beispiel ohne Visual Studio "codiert" und das Beispiel, das ich mir vorgestellt hatte, an ein Ereignis angepasst. Entschuldigung für die Verwirrung.

ppps: Weiß nicht, ob es noch zum Thread passt ... aber ich denke, es ist ein wichtiges Prinzip. Bitte überprüfen Sie auch einen anderen Thread von Stackflow


1
x.MyFunnyEvent = null; <- Das wird nicht einmal kompiliert (außerhalb der Klasse). Der Punkt eines Ereignisses unterstützt nur + = und - =. Und Sie können nicht einmal x.MyFunnyEvent- = x.MyFunnyEvent außerhalb der Klasse ausführen, da der Event-Getter quasi ist protected. Sie können das Ereignis nur von der Klasse selbst (oder von einer abgeleiteten Klasse) trennen.
CodesInChaos

Sie haben Recht ... wahr für Ereignisse ... hatten einen Fall mit einem einfachen Handler. Es tut uns leid. Ich werde versuchen zu bearbeiten.
Thomas

Wenn Ihr Delegierter öffentlich ist, wäre dies natürlich gefährlich, da Sie nie wissen, was der Benutzer tun wird. Wenn Sie jedoch die leeren Delegaten für eine private Variable festlegen und das + = und - = selbst behandeln, ist dies kein Problem und die Nullprüfung ist threadsicher.
ForceMagic
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.