为什么TEventArgs在.NET生态系统的标准事件模式中没有逆变?

Zac*_*OIR 23 .net c# contravariance .net-core

在.NET中了解有关标准事件模型的更多信息时,我发现在引入C#中的泛型之前,处理事件的方法由此委托类型表示:

//
// Summary:
//     Represents the method that will handle an event that has no event data.
//
// Parameters:
//   sender:
//     The source of the event.
//
//   e:
//     An object that contains no event data.
public delegate void EventHandler(object sender, EventArgs e);
Run Code Online (Sandbox Code Playgroud)

但是在C#2中引入泛型之后,我认为这个委托类型是使用泛型重写的:

//
// Summary:
//     Represents the method that will handle an event when the event provides data.
//
// Parameters:
//   sender:
//     The source of the event.
//
//   e:
//     An object that contains the event data.
//
// Type parameters:
//   TEventArgs:
//     The type of the event data generated by the event.
public delegate void EventHandler<TEventArgs>(object sender, TEventArgs e);
Run Code Online (Sandbox Code Playgroud)

我这里有两个问题:

首先,为什么不TEventArgs 类型参数作出逆变

如果我没有弄错,建议在委托的签名逆变中创建作为形式参数的类型参数,并在委托签名协变中作为返回类型的类型参数.

在Joseph Albahari的书中,C#in a Nutshell,我引述:

如果您要定义通用委托类型,那么最好:

  • 将仅在返回值上使用的类型参数标记为协变(out).
  • 将仅用于参数的任何类型参数标记为逆变量(in).

这样做可以通过尊重类型之间的继承关系来自然地进行转换.

第二个问题:为什么没有通用约束来强制执行TEventArgs派生自System.EventArgs

如下:

public delegate void EventHandler<TEventArgs>  (object source, TEventArgs e) where TEventArgs : EventArgs; 
Run Code Online (Sandbox Code Playgroud)

提前致谢.

编辑澄清第二个问题:

看起来像TEventArgs(TEventArgs:EventArgs)之前存在的泛型约束并且它被Microsoft删除了,所以看起来设计团队意识到它没有太多实际意义.

我编辑了我的答案,其中包含了一些截图

.NET参考源

在此输入图像描述

Eri*_*ert 39

首先,为了解决问题评论中的一些问题:我一般都在努力回答"为什么不"的问题,因为很难找到世界上每个人都选择不做这项工作的简明理由,因为所有的工作都不是默认完成.相反,你必须找到一个理由工作,并从带走资源等工作,也就是做它不那么重要了.

此外,这种形式的"为什么不"问题,询问在特定公司工作的人的动机和选择,可能只能由做出该决定的人负责,他们可能不在这里.

但是,在这种情况下,我们可以对我关闭"为什么不"问题的一般规则作出例外,因为这个问题说明了我之前从未写过的关于代表协方差的重要观点.

我没有决定让事件代表保持非变量,但如果我有能力这样做,我会让事件代表保持非变量,原因有两个.

第一个纯粹是"鼓励良好做法"的观点.事件处理程序通常是专门为处理特定事件而构建的,并且没有任何理由让我更容易使用签名中不匹配的代理作为处理程序,即使这些不匹配可能是通过差异处理.一个事件处理程序在每个方面完全匹配它应该处理的事件,这使我更有信心开发人员在构建事件驱动的工作流时知道他们在做什么.

这是一个非常弱的原因.更强烈的原因也是令人悲伤的原因.

我们知道,泛型委托类型的返回类型可以是协变的,参数类型也可以是逆变的.我们通常会考虑赋值兼容性的上下文中的差异.也就是说,如果我们有一个Func<Mammal, Mammal>,我们可以将它分配给一个类型的变量,Func<Giraffe, Animal>并知道潜在的功能将永远需要一个哺乳动物 - 因为现在它只会得到长颈鹿 - 并将永远返回动物 - 因为它回归哺乳动物.

但我们也知道代表可以加在一起; 代表是不可变的,所以将两个代表加在一起会产生第三个代表; sum是summands的顺序组合.

类似于字段的事件使用委托求和来实现; 这就是为事件添加处理程序的原因+=.(我不是这种语法的忠实粉丝,但我们现在坚持使用它.)

虽然这两种功能在彼此独立的情况下运作良好,但它们组合起来效果不佳.当我实现委托方差时,我们的测试在短时间内发现CLR中存在许多关于委托添加的错误,其中由于启用了方差的转换,底层委托类型不匹配.这些错误自CLR 2.0以来一直存在,但直到C#4.0,没有主流语言暴露过这些错误,没有为它们编写测试用例,等等.

可悲的是,我不记得虫子的繁殖者是什么了; 这是十二年前我不知道我是否还有任何记录隐藏在磁盘上的某个地方.

我们当时与CLR团队合作,尝试为下一版本的CLR解决这些问题,但与风险相比,他们的优先级不高.的类型,如地段IEnumerable<T>IComparable<T>等在这些版本作了变种,因为是FuncAction类型,但它是罕见在一起的两个不匹配的加FuncS使用的变量转换.但对于活动代表来说,他们生活中唯一的目的就是加在一起; 它们会一直加在一起,如果它们是变体的,那么就有可能将这些错误暴露给许多用户.

在C#4之后不久我就忘记了这些问题,老实说我不知道​​他们是否曾经被解决过.尝试以不同的组合将一些不匹配的代表加在一起,看看是否有任何不良事件发生!

因此,为什么不在 C#4.0发布时间框架中使事件委托变体是一个很好但不幸的原因.是否还有充分的理由,我不知道.您必须向CLR团队的某个人询问.

  • 非常感谢Eric,回答这个问题.我阅读了关于差异的非常精彩且解释良好的博客文章,并开始在.NET框架中查看,直到我找到了这篇文章的事件代理示例.我认为这个伟大且令人信服的答案完成了那些博客文章,因为没有文章讨论你在这个答案中提到的要点. (4认同)
  • @Dai:当然,并且要明确,我并不是说从方法组到委托*的*变体转换有任何问题.这在webforms场景中非常常见,而且经过了充分测试.当代理类型本身存在差异时,代理组合就会出现错误.如果我有空闲时间,我会看看我是否可以在12年前重新创建我的测试用例,看看他们是否还在复制. (4认同)
  • F#GitHub上存在一个开放性问题,需要为语言增加差异支持,其中明确提到事件是可能的增强。如果有人在使用此功能,他们可能知道CLR错误的当前状态。https://github.com/fsharp/fslang-suggestions/issues/162 (2认同)