CA1070的“不要把事件字段声明为virtual”到底是怎么回事?

编程语言 2026-07-09

The official documentation for DotNet Code Analysis Rule CA1070 "Do not declare event fields as virtual" says:

不要在基类中声明虚拟事件。在派生类中覆盖的事件具有未定义的行为。C#编译器对此处理不正确,订阅派生事件的订阅者是否实际订阅了基类事件,无法预测。

哈?

The official C# programming guide corroborates this, using very similar wording:

不要在基类中声明虚拟事件并在派生类中覆盖它们。C#编译器对这些情况的处理不正确,订阅派生事件的订阅者是否实际订阅了基类事件,无法预测。

哈?

另一方面:

The official C# language reference says for the 'event' keyword:

使用 [virtual] 关键字将事件标记为虚拟事件。派生类可以通过使用 [override] 关键字来覆盖事件的行为。欲了解更多信息,请参阅 [Inheritance]。

The official C# language specification says for events:

虚拟事件声明指定该事件的访问器是虚拟的。virtual 修饰符应用于事件的两个访问器。

and

除了声明和调用语法的差异外,虚拟、密封、覆盖和抽象访问器的行为与虚拟、密封、覆盖和抽象方法完全相同。

It seems to me that the above are irreconcilable: either the code analysis rule and the programming guide are hiding something, or the language specification and the language reference are wrong. So, what is it?

  • 语言规范和参考是在进行心愿式的设想,描述一个按所述不可能实现的特性吗?
  • 代码分析规则和编程指南是在保护程序员,免受编译器漏洞影响吗?
  • 是否有人能够重现代码分析规则和编程指南所警告的“未定义行为”、错误处理或不可预测性?

我真的很好奇。

解决方案

it is unpredictable whether a subscriber to the derived event will actually be subscribing to the base class event.

“不可预测”是指你不知道被覆盖的实现会做什么。这有点类似在构造函数中调用虚拟方法这种不良实践。不可预测的是,被覆盖的方法实现是否会访问你尚未初始化的状态。

假设基类是

public class Base {
    public virtual event EventHandler? E;

    public void RaiseE() => E?.Invoke(this, EventArgs.Empty);
}

而其他人可能会写出如下派生类:

public class DerivedA: Base {
    public override event EventHandler? E;
}

public class DerivedB: Base {
    public override event EventHandler? E {
        add => Console.WriteLine("add is called!");
        remove => Console.WriteLine("remove is called!");
    }
}

public class DerivedC: Base {
    public override event EventHandler? E {
        add => base.E += value;
        remove => base.E -= value;
    }
}

在这些实现中,只有 DerivedC 的实现是“订阅了基类事件”。

聚焦在 DerivedA 上。通过这种覆盖方式,DerivedA 现在为事件拥有了自己的后备委托字段(与 Base 中的那个不同),+=-= 现在将始终对 DerivedA 中的委托字段进行操作。然而, RaiseE 仍然会调用 Base 中的委托字段,而现在已经没有人可以对其进行订阅,因为事件的访问器已经被覆盖。

用代码来表示,

Base b = new DerivedA();
b.E += (_, _) => Console.WriteLine("Hi!");
b.RaiseE(); // doesn't print anything!

我们最终会处于这样的情况:基类中的事件实际上变得毫无用处——派生类无法触发它,没人能对它进行订阅,所以基类触发它也等于无所作为。确实是一个很怪异的情形。


这与当你用自动属性覆盖一个属性的情形类似。

public class Base {
    public virtual int X { get; set; }
}
public class Derived {
    public override int X { get; set; }
}

类似于事件,DerivedX 声明了自己的后备字段,而 Base 中的后备字段变得毫无用处。但至少基类仍然可以通过属性在派生类中获取并设置该后备字段,而不会发现任何异常。对于事件来说,基类在派生类中不能触发该事件。它所能做的只是触发它自己的事件,而该事件永远不会有订阅者。

站内所有文章版权归属LeftHeroAI导航站,无授权禁止任何主体转载、抄袭、复制内容,亦不得私自架设镜像站点。一经侵权,本站将通过法律途径追责。

相关文章