Warum muss der Parameter "sender" in einem C # -Ereignishandler ein Objekt sein?

73

Gemäß den Microsoft-Richtlinien zur Benennung von Ereignissen ist der senderParameter in einem C # -Ereignishandler " immer vom Typ Objekt, auch wenn es möglich ist, einen spezifischeren Typ zu verwenden".

Dies führt zu vielen Ereignisbehandlungscodes wie:

RepeaterItem item = sender as RepeaterItem;
if (item != null) { /* Do some stuff */ }

Warum rät die Konvention davon ab, einen Event-Handler mit einem spezifischeren Typ zu deklarieren?

MyType
{
    public event MyEventHander MyEvent;
}

...

delegate void MyEventHander(MyType sender, MyEventArgs e);

Vermisse ich eine Gotcha?

Für die Nachwelt: Ich bin mit der allgemeinen Stimmung in den Antworten , dass die Konvention ist zu genutztes Objekt (und Daten über das passieren EventArgs) , selbst wenn es möglich ist , eine bestimmte Art zu verwenden, und in der realen Welt der Programmierung es ist wichtig , zu folgen Das Treffen.

Bearbeiten: Köder für die Suche: RSPEC-3906-Regel "Event-Handler sollten die richtige Signatur haben"

Iain Galloway
quelle
Eigentlich sind alle Typen in C # Objekte ...
Wim ten Brink
2
Während Sie offensichtlich Recht haben, ist meine Frage, warum die Konvention ist, wenn möglich keinen spezifischeren Typ zu verwenden.
Iain Galloway
Ja, ein Rückgabewert im Ereignishandler sieht stinkend aus. Was ist, wenn es mehr als einen Handler gibt?
Erikkallen
Ich habe eine ausführliche Diskussion über die Verwendung eines stark typisierten "Absender" -Parameters. Kurz gesagt, die Verwendung eines starken Typs hat keine Nachteile und ist ab VB 2008 zu 100% abwärtskompatibel für C # (alle Versionen) und VB.NET. Siehe hier: stackoverflow.com/questions/1046016/… .
Mike Rosenblum
2017 aktualisierter Link zu den Designrichtlinien für Microsoft Event Handler (die die Richtlinien für die Benennung von Microsoft-Ereignissen für .NET 1.1 abgelöst haben).
Fabio Iotti

Antworten:

39

Nun, es ist eher ein Muster als eine Regel. Dies bedeutet, dass eine Komponente ein Ereignis von einer anderen weiterleiten kann, wobei der ursprüngliche Absender beibehalten wird, auch wenn es sich nicht um den normalen Typ handelt, der das Ereignis auslöst.

Ich bin damit einverstanden, dass es ein bisschen seltsam ist - aber es lohnt sich wahrscheinlich, sich nur aus Gründen der Vertrautheit an die Konvention zu halten. (Vertrautheit mit anderen Entwicklern.) Ich war noch nie besonders scharf auf EventArgsmich selbst (da es für sich genommen keine Informationen vermittelt), aber das ist ein anderes Thema. (Zumindest haben wir EventHandler<TEventArgs>jetzt - obwohl es hilfreich wäre, wenn es auch eine EventArgs<TContent>für die allgemeine Situation gibt, in der Sie nur einen einzigen Wert benötigen, um weitergegeben zu werden.)

BEARBEITEN: Dies macht den Delegaten natürlich allgemeiner - ein einzelner Delegatentyp kann für mehrere Ereignisse wiederverwendet werden. Ich bin mir nicht sicher, ob ich das als besonders guten Grund kaufe - besonders im Hinblick auf Generika -, aber ich denke, es ist etwas ...

Jon Skeet
quelle
13
Obwohl ich denke, dass Sie mit ziemlicher Sicherheit Recht haben, bin ich mir immer noch nicht sicher, ob ich den Vorteil sehe. Der Ereignishandler selbst muss noch wissen, welcher Typ übergeben wird (weil er ihn umwandeln muss). Ich kann nicht verstehen, warum es eine schlechte Sache wäre, es stark zu tippen - abgesehen von Ihrem Standpunkt zur Vertrautheit mit dem vorhandenen Muster ...
Iain Galloway
8
@Iain: Ja, ich sehe auch keinen großen Vorteil darin, dass das Muster so definiert wird. Es ist vielleicht nur eine schlechte Entscheidung von vor vielen Jahren, die jetzt zu spät ist, um sie sinnvoll zu ändern.
Jon Skeet
1
Nun, es wurde seit .NET 1.0 implementiert, so dass es möglicherweise nach Abwärtskompatibilität riecht, wenn überhaupt.
Jon Limjap
11
Die obige Antwort ist sehr alt! Zeiten werden zu ändern! :) "Generic delegates are especially useful in defining events based on the typical design pattern because the sender argument can be strongly typed and no longer has to be cast to and from Object."von MSDN
AnorZaken
2
@AnorZaken: Ich bin mir nicht sicher, warum du denkst, dass mir die Änderung nicht gefällt. Ich sage nur, dass sie noch nicht wirklich in der BCL implementiert ist, und es lohnt sich, den Grund für das alte Muster zu verstehen.
Jon Skeet
17

Ich denke, es gibt einen guten Grund für diese Konvention.

Nehmen wir das Beispiel von @ erikkallen:

void SomethingChanged(object sender, EventArgs e) {
    EnableControls();
}
...
MyRadioButton.Click += SomethingChanged;
MyCheckbox.Click += SomethingChanged;
MyDropDown.SelectionChanged += SomethingChanged;
...

Dies ist möglich (und seit .Net 1 vor Generika), da Kovarianz unterstützt wird.

Ihre Frage ist absolut sinnvoll, wenn Sie von oben nach unten gehen - dh Sie benötigen das Ereignis in Ihrem Code, also fügen Sie es Ihrer Kontrolle hinzu.

Die Konvention soll es jedoch einfacher machen, die Komponenten überhaupt zu schreiben. Sie wissen, dass für jedes Ereignis das Grundmuster (Objektabsender, EventArgs e) funktioniert.

Wenn Sie das Ereignis hinzufügen, wissen Sie nicht, wie es verwendet wird, und Sie möchten die Entwickler, die Ihre Komponente verwenden, nicht willkürlich einschränken.

Ihr Beispiel für ein generisches, stark typisiertes Ereignis ist in Ihrem Code sinnvoll, passt jedoch nicht zu anderen Komponenten, die von anderen Entwicklern geschrieben wurden. Zum Beispiel, wenn sie Ihre Komponente mit den oben genannten verwenden möchten:

//this won't work
GallowayClass.Changed += SomethingChanged;

In diesem Beispiel verursacht die zusätzliche Typbeschränkung nur Schmerzen für den Remote-Entwickler. Sie müssen jetzt einen neuen Delegaten nur für Ihre Komponente erstellen. Wenn sie eine Last Ihrer Komponenten verwenden, benötigen sie möglicherweise einen Delegaten für jede einzelne.

Ich bin der Meinung, dass es sich lohnt, die Konvention für alles zu befolgen, was extern ist oder von dem Sie erwarten, dass es außerhalb eines engen Teams verwendet wird.

Ich mag die Idee der generischen Ereignisargumente - ich verwende bereits etwas Ähnliches.

Keith
quelle
1
Sehr guter Punkt. Ich wünschte, ich könnte dir +5 geben. Wenn Sie ein Objekt für den Absender verwenden, muss die aufrufende Assembly nicht einmal wissen, an welchen Typ sie ein Ereignis anfügt - nur, dass sie von einem Typ stammt, den sie kennt (der relevante Typ ist normalerweise Control). Der Absenderparameter kann nicht sinnvoll verwendet werden (da er nicht weiß, in welchen Typ er umgewandelt werden soll), kann und wird jedoch alle erforderlichen Statusinformationen aus den EventArgs abrufen.
Iain Galloway
Ja - wenn Sie wissen, welcher Absender sein sollte, können Sie ihn zurückwerfen. Ich kann mir kein Szenario vorstellen, in dem Sie ein Ereignis für einen Werttyp haben würden, sodass Sie die Leistung der Besetzung nicht beeinträchtigen.
Keith
@IainGalloway und Keith - also ... möchten Sie, dass jemand den Ereignishandler in der Frage an ein Textfeld oder eine Schaltfläche (oder etwas anderes als a RepeaterItem) anfügt und dann ... was? Der Code trifft und versucht die Umwandlung, er gibt null zurück, dann passiert nichts ... Wenn der Handler nicht dafür ausgelegt ist, einen bestimmten Objekttyp als Absender zu behandeln, warum möchten Sie dann, dass er angehängt werden kann? Natürlich wird es Ereignisse geben, die allgemein genug sind, um die Verwendung objectmit Ihrer Logik vollständig zu rechtfertigen , aber ... oft nicht so gut (?) Ich vermisse möglicherweise etwas, also zögern Sie nicht, zu beleuchten, was das sein könnte.
Code Jockey
Diese Antwort ist völlig falsch. So wie ein Ereignis mit einem spezifischeren EventArgsTyp immer noch einen mit einem EventArgsParameter definierten Handler verwenden kann, kann auch ein Ereignis mit einem spezifischeren Absendertyp objecteinen mit einem objectParameter definierten Handler verwenden . Ihr Beispiel für das, was nicht funktioniert, funktioniert einwandfrei.
Mike Marynowski
@MikeMarynowskiIch habe kein "Beispiel dafür gegeben, was nicht funktioniert" , wir reden nicht wirklich darüber, ob etwas ausgeführt oder kompiliert wird, wir reden nur über Übung und warum ein Muster nützlich ist. Ereignisse sind kovariant : Sie können einen spezifischeren Typ (z. B. MyType) an einen object senderParameter übergeben. Wenn er jedoch einen expliziten Typ (z. B. ApiType sender) hat, können Sie ihn nicht übergeben MyType. Es gibt Gründe, warum Sie diese Einschränkung möglicherweise wünschen, und dies ist nicht der Fall schlechte Praxis, aber das OP fragte, warum object senderein allgemeines Muster von Microsoft als Standard verwendet wurde.
Keith
11

Ich verwende den folgenden Delegaten, wenn ich einen stark typisierten Absender bevorzugen würde.

/// <summary>
/// Delegate used to handle events with a strongly-typed sender.
/// </summary>
/// <typeparam name="TSender">The type of the sender.</typeparam>
/// <typeparam name="TArgs">The type of the event arguments.</typeparam>
/// <param name="sender">The control where the event originated.</param>
/// <param name="e">Any event arguments.</param>
public delegate void EventHandler<TSender, TArgs>(TSender sender, TArgs e) where TArgs : EventArgs;

Dies kann auf folgende Weise verwendet werden:

public event EventHandler<TypeOfSender, TypeOfEventArguments> CustomEvent;
Chris schreit
quelle
+1 Ich stimme dem zu 100% zu. Ich habe eine detaillierte Diskussion dieses Ansatzes hier: stackoverflow.com/questions/1046016/… .
Mike Rosenblum
5

Generika und Verlauf würden eine große Rolle spielen, insbesondere bei der Anzahl der Steuerelemente (usw.), die ähnliche Ereignisse offenlegen. Ohne Generika würden Sie am Ende viele Ereignisse aufdecken Control, was weitgehend nutzlos ist:

  • Sie müssen noch Casting durchführen, um etwas Nützliches zu tun (außer vielleicht einer Referenzprüfung, mit der Sie genauso gut arbeiten können object).
  • Sie können die Ereignisse für Nicht-Steuerelemente nicht wiederverwenden

Wenn wir Generika in Betracht ziehen, ist wieder alles in Ordnung, aber Sie beginnen dann, Probleme mit der Vererbung zu bekommen. Wenn Klasse B : A, sollten dann Ereignisse Asein EventHandler<A, ...>und Ereignisse Bsein EventHandler<B, ...>? Wieder sehr verwirrend, schwer zu bearbeiten und ein bisschen chaotisch in Bezug auf die Sprache.

Bis es eine bessere Option gibt, die all dies abdeckt, objectfunktioniert; Ereignisse finden fast immer in Klasseninstanzen statt, daher gibt es kein Boxen usw. - nur eine Besetzung. Und das Casting ist nicht sehr langsam.

Marc Gravell
quelle
4

Ich denke, das liegt daran, dass Sie in der Lage sein sollten, so etwas zu tun

void SomethingChanged(object sender, EventArgs e) {
    EnableControls();
}
...
MyRadioButton.Click += SomethingChanged;
MyCheckbox.Click += SomethingChanged;
...

Warum machen Sie die sichere Besetzung in Ihrem Code? Wenn Sie wissen, dass Sie die Funktion nur als Ereignishandler für den Repeater verwenden, wissen Sie, dass das Argument immer vom richtigen Typ ist, und Sie können stattdessen einen Wurfcast verwenden, z. B. (Repeater-) Absender anstelle von (Absender als Repeater).

erikkallen
quelle
Nun, das ist irgendwie mein Punkt. Wenn Sie wissen, dass das Argument immer vom richtigen Typ ist, warum können Sie diesen Typ nicht einfach übergeben? Offensichtlich gibt es Situationen, in denen Sie einen weniger spezifischen Typ verwenden möchten (wie den gerade beschriebenen). Die Besetzung sieht für mich nur ein bisschen chaotisch aus. Vielleicht bin ich einfach zu übereifrig.
Iain Galloway
(+1) Für das Beispiel habe ich es in meiner eigenen Antwort erweitert.
Keith
3

Kein guter Grund, jetzt gibt es Kovarianz und Kontravarienz. Ich denke, es ist in Ordnung, einen stark typisierten Absender zu verwenden. Siehe Diskussion in dieser Frage

MarkJ
quelle
1

Konventionen existieren nur, um Konsistenz zu erzwingen.

Sie KÖNNEN Ihre Event-Handler stark eingeben, wenn Sie dies wünschen, fragen sich jedoch, ob dies einen technischen Vorteil bietet?

Sie sollten berücksichtigen, dass Ereignishandler den Absender nicht immer umwandeln müssen. Der größte Teil des Ereignishandlungscodes, den ich in der Praxis gesehen habe, verwendet den Absenderparameter nicht. Es ist da, WENN es gebraucht wird, aber oft nicht.

Ich sehe oft Fälle, in denen verschiedene Ereignisse auf verschiedenen Objekten einen einzigen gemeinsamen Ereignishandler gemeinsam nutzen. Dies funktioniert, da sich dieser Ereignishandler nicht darum kümmert, wer der Absender war.

Wenn diese Delegierten trotz typischer Verwendung von Generika stark typisiert wären, wäre es SEHR schwierig, einen solchen Event-Handler zu teilen. Wenn Sie es stark tippen, gehen Sie davon aus, dass sich die Handler darum kümmern sollten, was der Absender ist, wenn dies nicht die praktische Realität ist.

Ich denke, Sie sollten sich fragen, warum Sie die Event-Handling-Delegierten stark eingeben würden. Würden Sie auf diese Weise wesentliche funktionale Vorteile hinzufügen? Machen Sie die Verwendung "konsistenter"? Oder setzen Sie nur Annahmen und Einschränkungen auf, nur um stark zu tippen?

Stephen M. Redd
quelle
1

Du sagst:

Dies führt zu vielen Ereignisbehandlungscodes wie: -

RepeaterItem item = sender as RepeaterItem
if (RepeaterItem != null) { /* Do some stuff */ }

Ist es wirklich viel Code?

Ich würde empfehlen, den senderParameter niemals für einen Ereignishandler zu verwenden. Wie Sie bemerkt haben, ist es nicht statisch typisiert. Es ist nicht unbedingt der direkte Absender des Ereignisses, da manchmal ein Ereignis weitergeleitet wird. Daher erhält derselbe Ereignishandler möglicherweise nicht senderjedes Mal den gleichen Objekttyp, wenn er ausgelöst wird. Es ist eine unnötige Form der impliziten Kopplung.

Wenn Sie sich für ein Ereignis anmelden, müssen Sie zu diesem Zeitpunkt wissen, auf welchem ​​Objekt sich das Ereignis befindet, und das interessiert Sie am wahrscheinlichsten:

someControl.Exploded += (s, e) => someControl.RepairWindows();

Alles andere, was für das Ereignis spezifisch ist, sollte im von EventArgs abgeleiteten zweiten Parameter enthalten sein.

Grundsätzlich ist der senderParameter ein bisschen historisches Rauschen, das am besten vermieden wird.

Ich habe hier eine ähnliche Frage gestellt.

Daniel Earwicker
quelle
Nickt nützlicher, sicherer und weniger stinkend, um MyType item = e.Item zusammen mit EventHandler <TArgs> zu verwenden. Ich mag es.
Iain Galloway
1

Das liegt daran, dass Sie nie sicher sein können, wer das Ereignis ausgelöst hat. Es gibt keine Möglichkeit einzuschränken, welche Typen ein bestimmtes Ereignis auslösen dürfen.

Codymanix
quelle
1

Das Muster der Verwendung von EventHandler (Objektabsender, EventArgs e) soll für alle Ereignisse die Möglichkeit bieten, die Ereignisquelle (Absender) zu identifizieren und einen Container für alle spezifischen Nutzdaten des Ereignisses bereitzustellen. Der Vorteil dieses Musters besteht auch darin, dass mit demselben Delegatentyp eine Reihe verschiedener Ereignisse generiert werden können.

Was die Argumente dieses Standarddelegierten betrifft ... Der Vorteil einer einzigen Tasche für den gesamten Status, den Sie zusammen mit dem Ereignis übergeben möchten, liegt auf der Hand, insbesondere wenn sich in diesem Status viele Elemente befinden. Die Verwendung eines Objekts anstelle eines starken Typs ermöglicht die Weitergabe des Ereignisses, möglicherweise an Assemblys, die keinen Verweis auf Ihren Typ haben (in diesem Fall können Sie argumentieren, dass sie den Absender ohnehin nicht verwenden können, aber das ist eine andere Geschichte - Sie können immer noch die Veranstaltung bekommen).

Nach meiner eigenen Erfahrung stimme ich Stephen Redd zu, sehr oft wird der Absender nicht verwendet. Die einzigen Fälle, in denen ich den Absender identifizieren musste, waren UI-Handler, bei denen viele Steuerelemente denselben Event-Handler verwenden (um das Duplizieren von Code zu vermeiden). Ich weiche jedoch von seiner Position ab, da ich kein Problem darin sehe, stark typisierte Delegaten zu definieren und Ereignisse mit stark typisierten Signaturen zu generieren, falls ich weiß, dass es dem Handler egal ist, wer der Absender ist (in der Tat sollte dies häufig nicht der Fall sein) Ich möchte nicht die Unannehmlichkeit haben, den Status in eine Tasche (EventArg-Unterklasse oder generisch) zu stopfen und sie auszupacken. Wenn ich nur 1 oder 2 Elemente in meinem Status habe, kann ich diese Signatur in Ordnung generieren. Für mich ist es eine Frage der Bequemlichkeit: Starkes Tippen bedeutet, dass der Compiler mich auf Trab hält.

Foo foo = sender as Foo;
if (foo !=null) { ... }

was den Code besser aussehen lässt :)

Dies ist nur meine Meinung. Ich bin oft von dem empfohlenen Muster für Ereignisse abgewichen und habe keine dafür erlitten. Es ist wichtig, immer klar zu machen, warum es in Ordnung ist, davon abzuweichen. Gute Frage! .

Sam Dahan
quelle
0

Das ist eine gute Frage. Ich denke, weil jeder andere Typ Ihren Delegaten verwenden könnte, um ein Ereignis zu deklarieren, können Sie nicht sicher sein, ob der Typ des Absenders wirklich "MyType" ist.

Maximilian Mayerl
quelle
Alles, was ich gelesen habe, deutet darauf hin. Stellen Sie sich jedoch vor, Sie haben ein Ereignis, bei dem der Absender vom Typ A oder B sein kann. Dann müssen Sie im EventHandler versuchen, in A umzuwandeln, dann eine A-spezifische Logik ausführen und dann versuchen, in B und umzuwandeln mache B-spezifische Logik. Alternativ können Sie eine gemeinsam genutzte Schnittstelle I aus A und B extrahieren und in diese umwandeln. Was ist im ersteren Fall der Vorteil gegenüber zwei separaten Ereignishandlern mit A-Absender und B-Absender? Was ist im letzteren Fall der Vorteil gegenüber einem stark tippenden Absender wie ich?
Iain Galloway
Ohne eine Instanz Ihrer Kontrolle könnten sie das Ereignis jedoch nicht auslösen (oder null für den Absender verwenden, aber das ist uninteressant).
Erikkallen
Ja, das ist richtig, sie können es nicht erhöhen. Was ich oben geschrieben habe, ist nur meine Meinung darüber, warum es empfohlen wird, ein Objekt für den Absender zu verwenden. Ich habe nicht gesagt, dass ich dem absolut zustimme. ;) Aber andererseits denke ich, dass man dem allgemein verwendeten Muster folgen sollte, und deshalb benutze ich immer Objekt.
Maximilian Mayerl
nickt ich stimme voll und ganz zu. Ich habe und werde Objekt verwenden, ich frage mich nur warum :)
Iain Galloway
0

Ich neige dazu, für jedes Ereignis einen bestimmten Delegatentyp (oder eine kleine Gruppe ähnlicher Ereignisse) zu verwenden. Der nutzlose Absender und Eventargs überladen einfach die API und lenken von den tatsächlich relevanten Informationen ab. Es ist noch nicht sinnvoll, Ereignisse klassenübergreifend "weiterleiten" zu können. Wenn Sie solche Ereignisse an einen Ereignishandler weiterleiten, der einen anderen Ereignistyp darstellt, müssen Sie das Ereignis einschließen sich selbst und die entsprechenden Parameter anzugeben ist wenig Aufwand. Außerdem hat der Spediteur tendenziell eine bessere Vorstellung davon, wie die Ereignisparameter "konvertiert" werden sollen als der endgültige Empfänger.

Kurz gesagt, wenn es keinen dringenden Interop-Grund gibt, geben Sie die nutzlosen, verwirrenden Parameter aus.

Eamon Nerbonne
quelle