在C#事件处理程序中,为什么“ sender”参数必须是一个对象?


73

根据Microsoft事件命名准则senderC#事件处理程序中的参数“即使可能使用更特定的类型,也始终是对象类型”。

这导致许多事件处理代码,例如:

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

为什么约定不建议使用更具体的类型声明事件处理程序?

MyType
{
    public event MyEventHander MyEvent;
}

...

delegate void MyEventHander(MyType sender, MyEventArgs e);

我想念陷阱吗?

对于后人:我同意的答案一般的情绪,即该公约在使用对象(并通过传递数据EventArgs),即使它是可以使用更具体的类型,而在现实世界编程它按照重要惯例。

编辑:搜索诱饵:RSPEC-3906规则“事件处理程序应具有正确的签名”


实际上,C#中的所有类型都是对象……
Wim十Brink

2
尽管您显然是对的,但我的问题是,为什么约定尽可能使用更具体的类型。
伊恩·加洛韦

是的,事件处理程序中的返回值看起来很臭。如果有多个处理程序怎么办?
erikkallen

我对使用强类型的“发件人”参数进行了详细的讨论。简而言之,使用强类型没有任何缺点,并且从VB 2008开始,它对C#(所有版本)和VB.NET都是100%向后兼容的。请参见:stackoverflow.com/questions/1046016/…
Mike Rosenblum

2017年更新了指向Microsoft事件处理程序设计指南的链接(该链接取代了.NET 1.1的Microsoft事件命名指南)。
法比奥·伊奥蒂

Answers:


39

好吧,这是一种模式,而不是规则。这确实意味着一个组件可以从另一个组件转发事件,即使不是引发该事件的普通类型,也可以保留原始发送者。

我同意这有点奇怪-只是出于熟悉的缘故,值得遵循惯例。(也就是说,对其他开发人员很熟悉。)我从来没有特别热衷于EventArgs自己(因为它本身不传达任何信息),但这是另一个主题。(至少我们EventHandler<TEventArgs>现在知道了-尽管如果EventArgs<TContent>在普通情况下只需要传播一个值,这也会有所帮助。)

编辑:当然,这确实使委托更具通用性-单个委托类型可以在多个事件之间重用。我不确定我是否将其作为一个特别好的理由购买-尤其是鉴于仿制药-但我想这...


13
虽然我认为您几乎可以肯定是正确的,但我仍然不确定我是否会看到优势。事件处理程序本身仍然必须知道要传递的类型(因为必须将其强制转换为类型)。我不明白为什么强行键入它会是一件坏事–除了您对现有模式的熟悉程度以外……
Iain Galloway

8
@Iain:是的,我也不认为这种方式定义的模式有太多优势。这很可能是很多年前的一个错误决定,现在为时已晚,无法明智地进行更改。
乔恩·斯基特

1
好吧,它是从.NET 1.0开始实施的,所以它可能是一种向后兼容的代码,如果有的话。
乔恩·林贾普

11
上面的答案老!时代变!:)"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."来自MSDN
AnorZaken 2014年

2
@AnorZaken:我不确定您为什么认为我不喜欢该更改-我只是说它尚未在BCL中真正实现,并且值得理解旧模式的原因。
乔恩·斯基特

17

我认为这项约定有充分的理由。

让我们以@erikkallen的示例为例:

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

因为支持协方差,所以这是可能的(并且从.Net 1开始,在泛型之前)。

如果您要自上而下进行操作,那么您的问题就很有意义-即您需要代码中的事件,因此可以将其添加到控件中。

但是,习惯上是为了使编写组件时更容易。您知道,对于任何事件,基本模式(对象发送者,EventArgs e)都将起作用。

添加事件时,您不知道将如何使用该事件,也不想随意限制开发人员使用您的组件。

您的通用强类型事件示例在您的代码中很有意义,但不适合其他开发人员编写的其他组件。例如,如果他们想将您的组件与上述组件一起使用:

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

在此示例中,附加的类型约束只会给远程开发人员带来痛苦。他们现在必须为您的组件创建一个新的委托。如果他们使用大量的组件,则可能需要为每个组件委派一个代理。

我认为对于任何外部的或希望在紧密的团队之外使用的东西,该惯例都值得遵循。

我喜欢通用事件args的想法-我已经使用了类似的东西。


1
很好。希望我能给您+5。通过将对象用作发送方,调用程序集甚至不需要知道将事件附加到的类型-只需知道它是从它确实知道的某种类型派生的(相关的通常是Control)。它将无法有效地使用发件人参数(因为它不知道将其强制转换为哪种类型),但是它可以并且将从EventArgs中获取所需的任何状态信息。
伊恩·加洛韦

是的-同时,如果您知道应该是哪个发件人,则可以将其退回。我无法想象您会在值类型上发生事件的情况,因此不会损害演员表的性能。
Keith

@IainGalloway和Keith-所以...您希望有人将问题中的事件处理程序附加到文本框或按钮(或除a以外的其他任何东西RepeaterItem)上,然后...怎么办?代码点击并尝试强制类型转换,它返回null,然后什么也没有发生……如果处理程序不是为了处理作为发送者的特定类型的对象而设计的,那么您为什么希望它能够附加?显然,将会有一些事件足够通用,以至于完全可以object与您的逻辑一起使用,但是...通常不是那么好(?),我可能会遗漏一些东西,因此请随时阐明可能的情况。
赛马会2014年

这个答案是完全错误的。就像具有更特定EventArgs类型的事件仍可以使用由EventArgs参数定义的处理程序一样,具有与发送方类型更特定的事件也比object仍可以使用由object参数定义的处理程序相同。您的无效示例将很好地工作。
Mike Marynowski

@MikeMarynowski我没有给出“什么不起作用的示例”,我们并不是在真正讨论某个东西将运行还是编译,我们只是在谈论实践以及为什么某种模式有用。事件是协变的:您可以将更具体的类型(例如MyType)传递给object sender参数,但是如果它具有显式类型(例如ApiType sender),则无法传递MyType。不好的做法,但是OP询问为什么object senderMicrosoft使用默认的通用模式作为默认模式。
基思(Keith)

11

当我需要强类型的发件人时,可以使用以下委托。

/// <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;

可以按以下方式使用它:

public event EventHandler<TypeOfSender, TypeOfEventArguments> CustomEvent;

+1我同意这100%。我在这里对这种方法进行了详细的讨论:stackoverflow.com/questions/1046016/…
Mike Rosenblum

5

泛型和历史将起很大的作用,尤其是在暴露类似事件的控件(等)数量方面。没有泛型,最终将暴露出很多事件Control,这在很大程度上是没有用的:

  • 您仍然必须进行任何有用的操作(也许是参考检查,但您也可以使用object
  • 您不能在非控件上重复使用事件

如果我们考虑泛型,那么一切都很好,但是随后您就会遇到继承问题。如果是类B : A,则事件应AEventHandler<A, ...>,事件应BEventHandler<B, ...>?再次,非常混乱,难以使用工具,并且在语言方面有些混乱。

直到有一个更好的选择可以涵盖所有这些方法为止object;事件几乎总是发生在类实例上,因此没有拳击等-只是强制转换。而且投放不是很慢。


4

我想那是因为你应该能够做类似的事情

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

为什么要在代码中进行安全转换?如果知道仅将该函数用作转发器的事件处理程序,则知道该参数始终是正确的类型,并且可以使用引发转换,例如,使用(Repeater)sender代替(sender as Repeater)。


好吧,这就是我的意思。如果您知道参数始终是正确的类型,那么为什么不能只传递该类型呢?显然,有些情况下您想使用一种不太具体的类型(如您刚刚描述的那种)。演员对我来说看起来有点混乱。也许我只是太热心了。
伊恩·加洛韦

(+1)为例,我在自己的答案中进行了扩展。
基思

3

完全没有充分的理由,现在我认为可以使用协变和逆变使用强类型的Sender。查看此问题的讨论


1

存在惯例仅是为了强加一致性。

您可以根据需要强烈键入事件处理程序,但请问自己这样做是否可以提供任何技术优势?

您应该考虑到事件处理程序并不一定总是强制转换发送方...我在实际实践中看到的大多数事件处理代码都没有使用sender参数。如果有需要,就在那儿,但通常不是。

我经常看到在不同对象上的不同事件共享一个公共事件处理程序的情况,该事件处理程序之所以起作用是因为该事件处理程序与发送方是谁无关。

如果那些委托都是强类型的,即使巧妙地使用了泛型,共享这样的事件处理程序也将非常困难。实际上,通过强行键入它,您就假定处理程序应该关心发送者是什么,而这并非实际情况。

我猜您应该问的是为什么您要强烈键入事件处理委托?这样,您会增加任何重要的功能优势吗?您是否使用法更加“一致”?还是只是为了强类型化而施加假设和约束?


1

你说:

这导致许多事件处理代码,例如:

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

真的有很多代码吗?

我建议不要在sender事件处理程序中使用该参数。您已经注意到,它不是静态类型的。它不一定是事件的直接发送者,因为有时会转发事件。因此,相同的事件处理程序甚至在sender每次触发时都不会获得相同的对象类型。这是隐式耦合的不必要形式。

当您参加某个事件时,到那时,您必须知道事件所在的对象,而这正是您最可能感兴趣的对象:

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

与事件相关的其他任何事情都应该在EventArgs派生的第二个参数中。

基本上,该sender参数有点历史噪音,最好避免。

我在这里问了类似的问题。


点头更实用,更安全,并且更小臭到使用的MyType项= e.Item,与事件处理程序<TArgs>沿。我喜欢。
伊恩·加洛韦 Eain Galloway)2009年

1

这是因为您无法确定是谁触发了该事件。没有办法限制允许哪些类型引发某个事件。


1

使用EventHandler(object sender,EventArgs e)的模式旨在为所有事件提供识别事件源(发送者)的方式,并为所有事件的特定有效负载提供容器。这种模式的优点还在于,它允许使用相同类型的委托来生成许多不同的事件。

至于这个默认委托的参数...对于要随事件传递的所有状态都拥有一个包非常明显,特别是在该状态中有许多元素的情况下。使用对象而不是强类型可以将事件传递给可能不引用您类型的程序集(在这种情况下,您可能会争辩说它们将始终无法使用发送者,但这是另一回事了-他们仍然可以参加活动)。

以我自己的经验,我同意斯蒂芬·雷德(Stephen Redd)的观点,通常不使用发件人。我需要识别发送者的唯一情况是UI处理程序,其中许多控件共享同一事件处理程序(以避免重复代码)。但是,我偏离了他的立场,因为在我知道处理程序将永远不在乎发件人是谁的情况下,我确实认为定义强类型的委托并生成具有强类型签名的事件没有问题(事实上,通常不应对此类型具有任何范围),并且我不希望将状态填充到包(EventArg子类或泛型)并将其拆包带来的不便。如果我的状态中只有1个或2个元素,则可以生成该签名。这对我来说是一个方便的问题:强大的键入功能意味着编译器使我保持警惕,

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

这确实使代码看起来更好:)

话虽如此,这只是我的意见。我经常偏离建议的活动模式,因此也没有遇到任何困难。始终清楚为什么可以偏离它很重要。好问题!。


0

好吧,这是一个好问题。我认为,因为任何其他类型都可以使用您的委托来声明事件,所以您不能确定发送者的类型确实是“ MyType”。


我阅读的所有内容都表明了这一点。但是...想象一下,您遇到了一些事件,发件人可以是A型或B型。然后在EventHandler中,您必须尝试将其强制转换为A,然后执行特定于A的逻辑,然后尝试将其强制转换为B并做特定于B的逻辑。从A和B交替提取一个共享接口I并强制转换为该接口。在前一种情况下,与使用A发送方和B发送方的两个单独的事件处理程序相比,优点是什么。在后一种情况下,与像我一样强类型发送者相比有什么优势?
伊恩·加洛韦

但是如果没有您的控件实例,他们将无法引发事件(或将null用作发送方,但这没意思)。
erikkallen

是的,没错,他们无法提出。我上面写的只是我对为什么建议对发送者使用对象的看法,我并不是说我完全同意。;)但另一方面,我认为应该遵循常用的模式,因此我总是使用对象。
Maximilian Mayerl 09年

我完全同意点头。我曾经并且将会使用对象,我只是想知道为什么:)
Iain Galloway

0

我倾向于为每个事件(或一小组类似的事件)使用特定的委托类型。无用的发送者和eventargs只会使api混乱,并分散信息的实际相关位。能够跨类“转发”事件并不是我发现有用的东西,并且,如果您要将这样的事件转发到表示不同类型事件的事件处理程序中,那么将不得不包装该事件您自己并提供适当的参数不费吹灰之力。同样,与最终接收者相比,转发者往往对如何“转换”事件参数有更好的了解。

简而言之,除非有一些紧急的互操作原因,否则请转储无用的,令人困惑的参数。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.