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

Why wasn't TEventArgs made contravariant in the standard event pattern in the .NET ecosystem?

在详细了解 .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);

但是在 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);

我这里有两个问题:

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

如果我没记错的话,建议使在委托签名中作为正式参数出现的类型参数是逆变的,而类型参数将是委托签名中的 return 类型协变。

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

If you’re defining a generic delegate type, it’s good practice to:

  • Mark a type parameter used only on the return value as covariant (out).
  • Mark any type parameters used only on parameters as contravariant (in).

Doing so allows conversions to work naturally by respecting inheritance relationships between types.

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

如下:

public delegate void EventHandler<TEventArgs>  (object source, TEventArgs e) where TEventArgs : EventArgs; 

提前致谢。

编辑以澄清第二个问题:

似乎对 TEventArgs 的通用约束(where TEventArgs : EventArgs)之前就存在,但被微软删除了,所以设计团队似乎意识到它没有很有实际意义。

我编辑了我的答案以包含来自

的一些屏幕截图

.NET reference source

首先,要解决问题评论中的一些问题:我通常会强烈反对 "why not" 问题,因为很难找到为什么 世界上每个人都选择这样做的简明原因不做这项工作,因为默认情况下不做所有工作。相反,你必须找到一个理由来工作,并从其他工作中拿走一些不那么重要的资源。

此外,"why not" 这种形式的问题,询问在特定公司工作的人的动机和选择,可能只能由做出该决定的人回答,他们可能不在附近.

但是,在这种情况下,我们可以对关闭 "why not" 问题的一般规则进行例外处理,因为 该问题说明了我以前从未写过的关于委托协变的重要观点。

我没有做出让事件委托保持不变的决定,但如果我能够这样做,我会保持事件委托不变,原因有两个。

第一个纯粹是"encourage good practices"点。事件处理程序通常是专门为处理特定事件而构建的,我没有充分的理由使它比现在更容易使用签名中不匹配的委托作为处理程序,即使这些不匹配可能是通过方差处理。一个在各个方面都与它应该处理的事件完全匹配的事件处理程序让我更加相信开发人员在构建事件驱动的工作流时知道他们在做什么。

这是一个很站不住脚的理由。更强的理由也是更难过的理由

正如我们所知,泛型委托类型可以在它们的 return 类型中实现协变,在它们的参数类型中实现逆变;我们通常在分配兼容性的背景下考虑方差。也就是说,如果我们手头有一个 Func<Mammal, Mammal>,我们可以将它分配给一个类型为 Func<Giraffe, Animal> 的变量,并且知道底层函数将始终采用哺乳动物——因为现在它只会得到长颈鹿—— - 并且总是 return 一种动物 - 因为它 return 是哺乳动物。

但我们也知道代表可能会加在一起;代表是不可变的,因此将两个代表加在一起会产生第三个代表;总和是被加数的顺序组合。

Field-like事件是使用委托求和实现的;这就是为什么向事件添加处理程序表示为 +=。 (我不是这种语法的忠实粉丝,但我们现在坚持使用它。)

虽然这两个功能彼此独立运行良好,但它们结合使用时效果不佳。当我实现委托方差时,我们的测试很快发现 CLR 中存在许多关于委托添加的错误,其中底层委托类型由于启用方差的转换而不匹配。这些错误从 CLR 2.0 开始就存在,但是直到 C# 4.0,没有任何主流语言暴露过这些错误,也没有为它们编写测试用例,等等。

遗憾的是,我不记得这些错误的重现者是什么;那是十二年前的事了,我不知道我是否还有关于它的笔记藏在某个地方的磁盘上。

我们当时与 CLR 团队合作,试图为下一版本的 CLR 解决这些错误,但与它们的风险相比,它们的优先级不够高。 IEnumerable<T>IComparable<T> 等许多类型在这些版本中都变成了变体,FuncAction 类型也是如此,但是 很少见使用变体转换 将两个不匹配的 Func 加在一起。但对于活动代表来说,他们人生唯一的目的就是加在一起;它们会一直加在一起,如果它们是变体,就会有将这些错误暴露给大量用户的风险。

我在 C# 4 之后不久就忘记了这些问题,老实说,我不知道它们是否得到了解决。尝试以各种组合将一些不匹配的代表加在一起,看看是否会发生什么不好的事情!

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