触发事件时出现了NullReferenceException异常

22

考虑以下内容:

class Client
{
    public static event EventHandler connectFailed;

    private Socket socket;

    public Client()
    {
        socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
        IPEndPoint endpoint = new IPEndpoint(
            IPAddress.Parse("192.168.1.100"),
            7900
            );

        try
        {
            socket.Connect(endpoint);
        }
        catch(Exception e)
        {
            connectFailed(e, new EventArgs());
        }
    }
}

假设剩下的代码已经实现(在Program.cs中的事件处理程序等)。

我遇到了一个问题,connectFailed(e, new EventArgs()); 这一行出现了NullRefrerenceException异常,而我无法理解原因。我的其他事件都能正常触发,我不明白这个与其他事件有什么不同。

有什么想法吗?


你的 connectFailed += <EventHandlerMethod>; 在哪里? - Christo
它在 Program.cs 文件中,并且已经完整实现。 - Skudd
4个回答

35

你需要进行空值检查 - 在C#中,当事件上没有已注册的处理程序时,你不能调用该事件。

通常的做法是实现一个 OnConnectFailed 方法:

protected virtual void OnConnectFailed(e as EventArgs) {
    EventHandler tmp = connectFailed;
    if(tmp != null)
        tmp(this,e);
}

另外,事件处理程序的第一个参数应该是 this 而不是异常。如果您需要将异常传递给事件处理程序,请创建一个带有异常属性的 EventArgs 类。

此外,在构造函数中引发事件没有意义... 因为没有机会将处理程序添加到它上面。


2
关键是最后一句话,即在构造函数完成之前,没有任何方式可以让任何东西订阅事件。您应该重构连接代码并将其命名为Connect()方法。 - Chuck Savage
1
你的解决方案现在已经安全地避免了可能导致空引用异常的竞态条件,但是使用try/catch不是更明智吗?使用当前的解决方案,有人可能会取消订阅事件,但在几个周期后仍然会收到它。这不太可能成为问题,但很可能在取消订阅后立即销毁处理事件所需的数据。 - Alex Whittemore
1
如果您使用try/catch,那么如果事件处理程序中出现异常,您将隐藏错误。与VB的区别仅在于语言设计上的差异 - 尽管我会注意到,如果您不定义On...方法,则更难以在继承类中更改行为。 - Random832
1
这是一个很好的观点,我认为是正确的:如果在事件处理程序中出现了空引用异常,它将被隐藏。但另外,有人解释说捕获异常比简单的空比较要昂贵得多,因为如果处理程序为空,您必须经过创建异常对象等工作。对于像控件这样的频繁发生事件且经常未处理的事物,这会影响性能。 - Alex Whittemore
1
第二个竞态条件是当线程A(我们称之为工作者)去触发一个事件并创建一个临时副本。线程B(一个窗体)取消订阅了A的事件,但仅在稍后的一刻,A仍然发送了该事件,因为该副本仍然包含对B处理程序的引用。B会在其不期望的情况下被调用。这种情况可能随时发生,通常被认为是“使处理程序能够抵御不应被调用的情况”。 - Alex Whittemore
显示剩余7条评论

13

在C# 6中,您可以通过以下方式进行空值检查

connectFailed?.Invoke(this, e); 

9

找到了,

public delegate void OnRequestReceivedHandler(object sender);
public event OnRequestReceivedHandler ReqeustReceived = delegate { };

1
'connectFailed'是一个事件。 如果没有人订阅该事件,它将为null,因此您必须检查null情况。
为了使其安全,您需要进行空值检查,即:
if (connectFailed != null)
    connectFailed(e, new EventArgs()); 

然而,由于多线程的原因,仅仅使用这个模式是不够的。推荐的方法是:

EventHandler temp = connectFailed;
if (temp != null)
    temp(e, new EventArgs()); 

这不仅检查了空条件,还将事件首先复制以确保它是线程安全的(如果一个线程在另一个线程处理事件时更改了事件队列,则行为可能未定义。通过首先复制它,您可以确保订阅者列表在事件处理过程的持续时间内保持不变)


3
错误的事件触发方式。考虑竞态条件。http://blogs.msdn.com/b/ericlippert/archive/2009/04/29/events-and-races.aspx - spender

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接