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

Bil*_*eal 21 c# null events

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

我确信语言设计师有充分的理由这样做..但我很好奇,如果有人知道为什么.

Jon*_*eet 18

这当然是一个烦恼的问题.

当你编写一个访问类中类字段事件的代码时,你实际上是在访问字段本身(在C#4中以模数进行一些修改;暂时不要去那里).

所以,选项将是:

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

    Action<string> x = null;
    x();
    
    Run Code Online (Sandbox Code Playgroud)

    不会抛出异常.

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

Func<int> x = null;
int y = x();
Run Code Online (Sandbox Code Playgroud)

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

Func<int> x = null;
int y = x.Invoke();
Run Code Online (Sandbox Code Playgroud)

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

  • 坦率地说,我认为这是一个非常小的烦恼.我更喜欢异常而非沉默错误. (4认同)

pli*_*nth 13

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

public event EventHandler<FooEventArgs> Foo = delegate { };
Run Code Online (Sandbox Code Playgroud)

这有两个好处.首先是我们没有检查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);
}
Run Code Online (Sandbox Code Playgroud)

但是,通过将Foo自动初始化为空委托,从不需要任何检查,代码自动是线程安全的,并且更容易读取引导:

protected virtual void OnFoo(FooEventArgs e)
{
    Foo(this, e); // always good
}
Run Code Online (Sandbox Code Playgroud)

在空手道小子向Pat Morita道歉,"避免null的最佳方法就是没有."

至于为什么,C#并没有像VB那样溺爱你.尽管event关键字隐藏了多播委托的大部分实现细节,但它确实提供了比VB更精细的控制.

  • 请注意,此技术仅消除代码中两个竞争条件中的**.默认情况下,这永远不会取消引用null,但是一个线程仍然可以删除事件处理程序并销毁它需要的状态,并且第二个线程可以调用已删除的事件处理程序,然后崩溃因为缺少必需的状态. (7认同)

Han*_*ant 8

如果设置管道以首先引发事件将是昂贵的(如SystemEvents),或者在准备事件参数时将是昂贵的(如Paint事件),您需要考虑需要什么代码.

事件处理的Visual Basic风格不允许您推迟支持此类事件的成本.您无法覆盖添加/删除访问器以延迟将昂贵的管道放置到位.并且您无法发现可能没有订阅任何事件处理程序,因此刻录循环以准备事件参数是浪费时间.

在C#中不是问题.方便和控制之间的经典权衡.


oll*_*llb 5

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

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);
    }
}
Run Code Online (Sandbox Code Playgroud)

现在你可以简单地这样做:

class Test
{
    public event EventHandler SomeEvent;

    public void DoSomething()
    {
        SomeEvent.Raise(this);
    }
}
Run Code Online (Sandbox Code Playgroud)

但是正如其他人已经提到的那样,您应该了解多线程场景中可能存在的竞争条件.