为什么 C# 要求您在每次触发事件时编写空检查?

发布于 2024-09-06 21:48:14 字数 149 浏览 4 评论 0原文

这对我来说似乎很奇怪——VB.NET 通过其 RaiseEvent 关键字隐式处理 null 检查。它似乎大大增加了围绕事件的样板数量,但我看不出它能带来什么好处。

我确信语言设计者有充分的理由这样做......但我很好奇是否有人知道为什么。

This seems odd to me -- VB.NET handles the null check implicitly via its RaiseEvent keyword. It seems to raise the amount of boilerplate around events considerably and I don't see what benefit it provides.

I'm sure the language designers had a good reason to do this.. but I'm curious if anyone knows why.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(9

り繁华旳梦境 2024-09-13 21:48:14

这确实是一个令人烦恼的地方。

当您编写访问类中类似字段的事件的代码时,您实际上是在访问该字段本身(对 C# 4 中的一些更改进行取模;我们暂时不去那里)。

因此,选项是:

  • 特殊情况的类似字段的事件调用,这样它们实际上并不直接引用该字段,而是添加了一个包装器
  • 以不同的方式处理所有委托调用,这样:

    操作; x = 空;
    x();
    

    不会抛出异常。

当然,对于非 void 委托(和事件),这两个选项都会引发一个问题:

Func<int> x = null;
int y = x();

是否应该默默返回 0? (int 的默认值。)或者它实际上掩盖了错误(更有可能)。让它默默地忽略您试图调用空委托的事实会有些不一致。在这种情况下,情况会更奇怪,因为它不使用 C# 的语法糖:

Func<int> x = null;
int y = x.Invoke();

基本上,无论你做什么,事情都会变得棘手并且与语言的其余部分不一致。我也不喜欢它,但我不确定实用但一致的解决方案可能是什么......

It's certainly a point of annoyance.

When you write code which accesses a field-like event within a class, you're actually accessing the field itself (modulo a few changes in C# 4; let's not go there for the moment).

So, options would be:

  • Special-case field-like event invocations so that they didn't actually refer to the field directly, but instead added a wrapper
  • Handle all delegate invocations differently, such that:

    Action<string> x = null;
    x();
    

    wouldn't throw an exception.

Of course, for non-void delegates (and events) both options raise a problem:

Func<int> x = null;
int y = x();

Should that silently return 0? (The default value of an int.) Or is it actually masking a bug (more likely). It would be somewhat inconsistent to make it silently ignore the fact that you're trying to invoke a null delegate. It would be even odder in this case, which doesn't use C#'s syntactic sugar:

Func<int> x = null;
int y = x.Invoke();

Basically things become tricky and inconsistent with the rest of the language almost whatever you do. I don't like it either, but I'm not sure what a practical but consistent solution might be...

岁吢 2024-09-13 21:48:14

我们通常通过这样声明我们的事件来解决这个问题:

public event EventHandler<FooEventArgs> Foo = delegate { };

这有两个优点。首先是我们没有检查 null 。第二个是我们避免了典型事件触发中普遍存在的关键部分问题:

// old, busted code that happens to work most of the time since
// a lot of code never unsubscribes from events
protected virtual void OnFoo(FooEventArgs e)
{
    // two threads, first checks for null and succeeds and enters
    if (Foo != null) {
        // second thread removes event handler now, leaving Foo = null
        // first thread resumes and crashes.
        Foo(this, e);
    }
}

// proper thread-safe code
protected virtual void OnFoo(FooEventArgs e)
{
     EventHandler<FooEventArgs> handler = Foo;
     if (handler != null)
         handler(this, e);
}

但是通过将 Foo 自动初始化为空委托,永远不需要任何检查,并且代码自动是线程安全的,并且更易于阅读boot:

protected virtual void OnFoo(FooEventArgs e)
{
    Foo(this, e); // always good
}

向《空手道小子》中的帕特·森田致歉,“避免 null 的最好方法就是不拥有一个。”

至于原因,C# 不像 VB 那样宠溺你。尽管 event 关键字隐藏了多播委托的大部分实现细节,它确实为您提供了比 VB 更好的控制。

we usually work around this by declaring our events like this:

public event EventHandler<FooEventArgs> Foo = delegate { };

this has two advantages. The first is that we don't have check for null. The second is that we avoid the critical section issue that is omnipresent in typical event firing:

// old, busted code that happens to work most of the time since
// a lot of code never unsubscribes from events
protected virtual void OnFoo(FooEventArgs e)
{
    // two threads, first checks for null and succeeds and enters
    if (Foo != null) {
        // second thread removes event handler now, leaving Foo = null
        // first thread resumes and crashes.
        Foo(this, e);
    }
}

// proper thread-safe code
protected virtual void OnFoo(FooEventArgs e)
{
     EventHandler<FooEventArgs> handler = Foo;
     if (handler != null)
         handler(this, e);
}

But with the automatic initialization of Foo to an empty delegate, there is never any checking necessary and the code is automatically thread-safe, and easier to read to boot:

protected virtual void OnFoo(FooEventArgs e)
{
    Foo(this, e); // always good
}

With apologies to Pat Morita in the Karate Kid, "Best way to avoid null is not have one."

As to the why, C# doesn't coddle you as much as VB. Although the event keyword hides most of the implementation details of multicast delegates, it does give you finer control than VB.

花期渐远 2024-09-13 21:48:14

您需要考虑如果首先设置引发事件的管道会很昂贵(如 SystemEvents),或者准备事件参数会很昂贵(如 Paint 事件),则需要哪些代码。

Visual Basic 风格的事件处理不允许您推迟支持此类事件的成本。您无法覆盖添加/删除访问器来延迟将昂贵的管道安装到位。而且您无法发现可能没有订阅任何事件处理程序,因此燃烧周期来准备事件参数是浪费时间。

在 C# 中不是问题。便利性和控制之间的经典权衡。

You need to consider what code would be required if setting up the plumbing to raise the event in the first place would be expensive (like SystemEvents) or when preparing the event arguments would be expensive (like the Paint event).

The Visual Basic style of event handling doesn't let you postpone the cost of supporting such an event. You cannot override the add/remove accessors to delay putting the expensive plumbing in place. And you cannot discover that there might not be any event handlers subscribed so that burning the cycles to prepare the event arguments is a waste of time.

Not an issue in C#. Classic trade-off between convenience and control.

佼人 2024-09-13 21:48:14

扩展方法提供了一种非常酷的方法来解决这个问题。考虑以下代码:

static public class Extensions
{
    public static void Raise(this EventHandler handler, object sender)
    {
        Raise(handler, sender, EventArgs.Empty);
    }

    public static void Raise(this EventHandler handler, object sender, EventArgs args)
    {
        if (handler != null) handler(sender, args);
    }

    public static void Raise<T>(this EventHandler<T> handler, object sender, T args)
        where T : EventArgs
    {
        if (handler != null) handler(sender, args);
    }
}

现在您可以简单地执行此操作:

class Test
{
    public event EventHandler SomeEvent;

    public void DoSomething()
    {
        SomeEvent.Raise(this);
    }
}

但是,正如其他人已经提到的,您应该意识到多线程场景中可能出现的竞争条件。

Extension methods provide a very cool way, to get around this. Consider the following code:

static public class Extensions
{
    public static void Raise(this EventHandler handler, object sender)
    {
        Raise(handler, sender, EventArgs.Empty);
    }

    public static void Raise(this EventHandler handler, object sender, EventArgs args)
    {
        if (handler != null) handler(sender, args);
    }

    public static void Raise<T>(this EventHandler<T> handler, object sender, T args)
        where T : EventArgs
    {
        if (handler != null) handler(sender, args);
    }
}

Now you can simply do this:

class Test
{
    public event EventHandler SomeEvent;

    public void DoSomething()
    {
        SomeEvent.Raise(this);
    }
}

However as others already mentioned, you should be aware of the possible race condition in multi-threaded scenarios.

季末如歌 2024-09-13 21:48:14

真的不知道为什么这样做,但是有一个 空对象模式 的变体对于代表:

private event EventHandler Foo = (sender, args) => {};

这样您就可以自由调用 Foo,而无需检查 null

Don't really know why is this done, but there's a variation of a Null Object pattern specifically for delegates:

private event EventHandler Foo = (sender, args) => {};

This way you can freely invoke Foo without ever checking for null.

温柔嚣张 2024-09-13 21:48:14

因为 RaiseEvent 会带来一些开销。

控制和易用性之间总是需要权衡。

  • VB.Net:易于使用,
  • C#:与 VB 一样具有更多控制能力
  • C++:更多控制,更少指导,更容易搬起石头砸自己的脚

Because RaiseEvent carries a some overhead.

There's always a tradeoff between control and ease of use.

  • VB.Net: ease of use,
  • C#: more control as VB
  • C++: even more control, less guidance, easier to shoot yourself in the foot
心如荒岛 2024-09-13 21:48:14

编辑:正如OP指出的那样,这个答案没有解决问题的正文。然而,有些人可能会发现它很有用,因为它确实提供了问题标题的答案(单独使用时):

为什么 C# 要求您在每次触发事件时编写空检查?

它还提供了问题正文的意图的背景,有些人可能会觉得有用。因此,出于这些原因以及 Meta 上的此建议,我将保留这个答案。


原文:

在其 MSDN 文章中 如何:发布符合 .NET Framework 准则的事件(C# 编程指南)(Visual Studio 2013),Microsoft 在其示例中包含以下注释:

// Make a temporary copy of the event to avoid possibility of 
// a race condition if the last subscriber unsubscribes 
// immediately after the null check and before the event is raised.

Here is a微软示例代码的较大摘录,提供了该评论的上下文。

// Wrap event invocations inside a protected virtual method 
// to allow derived classes to override the event invocation behavior 
protected virtual void OnRaiseCustomEvent(CustomEventArgs e)
{
    // Make a temporary copy of the event to avoid possibility of 
    // a race condition if the last subscriber unsubscribes 
    // immediately after the null check and before the event is raised.
    EventHandler<CustomEventArgs> handler = RaiseCustomEvent;

    // Event will be null if there are no subscribers 
    if (handler != null)
    {
        // Format the string to send inside the CustomEventArgs parameter
        e.Message += String.Format(" at {0}", DateTime.Now.ToString());

        // Use the () operator to raise the event.
        handler(this, e);
    }
}

Edit: As the OP points out, this answer does not address the body of the question. However, some may find it useful because it does provide an answer for the title of the question (when taken by itself):

Why does C# require you to write a null check every time you fire an event?

It also provides context for the intent of the body of the question which some may find useful. So, for those reasons and this advice on Meta, I'll let this answer stand.


Original Text:

In its MSDN article How to: Publish Events that Conform to .NET Framework Guidelines (C# Programming Guide) ( Visual Studio 2013), Microsoft includes the following comment in its example:

// Make a temporary copy of the event to avoid possibility of 
// a race condition if the last subscriber unsubscribes 
// immediately after the null check and before the event is raised.

Here is a larger excerpt from Microsoft's example code that gives context to that comment.

// Wrap event invocations inside a protected virtual method 
// to allow derived classes to override the event invocation behavior 
protected virtual void OnRaiseCustomEvent(CustomEventArgs e)
{
    // Make a temporary copy of the event to avoid possibility of 
    // a race condition if the last subscriber unsubscribes 
    // immediately after the null check and before the event is raised.
    EventHandler<CustomEventArgs> handler = RaiseCustomEvent;

    // Event will be null if there are no subscribers 
    if (handler != null)
    {
        // Format the string to send inside the CustomEventArgs parameter
        e.Message += String.Format(" at {0}", DateTime.Now.ToString());

        // Use the () operator to raise the event.
        handler(this, e);
    }
}
记忆之渊 2024-09-13 21:48:14

请注意,从 C# 6 开始,该语言现在提供了一种简洁的语法来方便地执行此 null 检查。例如:

public event EventHandler SomeEvent;

private void M()
{
    // raise the event:
    SomeEvent?.Invoke(this, EventArgs.Empty);
}

请参阅 空条件运算符

Note that as of C# 6, the language now provides a concise syntax to perform this null check conveniently. E.g.:

public event EventHandler SomeEvent;

private void M()
{
    // raise the event:
    SomeEvent?.Invoke(this, EventArgs.Empty);
}

See Null Conditional Operator

扬花落满肩 2024-09-13 21:48:14

原因实际上可以归结为 C# 为您提供了更多控制权。在 C# 中,如果您愿意,则不必执行 null 检查。在下面的代码中,MyEvent 永远不可能是 null 那么为什么还要进行检查呢?

public class EventTest
{
  private event EventHandler MyEvent = delegate { Console.WriteLine("Hello World"); }

  public void Test()
  {
    MyEvent(this, new EventArgs());
  }
}

The reason really boils down to C# giving you more control. In C# you do not have to do the null check if you so choose. In the following code MyEvent can never be null so why bother doing the check?

public class EventTest
{
  private event EventHandler MyEvent = delegate { Console.WriteLine("Hello World"); }

  public void Test()
  {
    MyEvent(this, new EventArgs());
  }
}
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文