4

我有一个带有按钮、标签和进度条的表单,因此当我单击按钮时,它会创建一个 b 类实例来运行进程。一旦该过程完成,它将调用 EventHandler 以在主窗体的标签中显示“完成”!

我创建了一个委托 (SetStatus) 的事件 (SetStatusEvent) 来执行此操作。当我在 EventHandler (usbforProcessExited) 之外调用这个事件时似乎很好,但是当我从 usbforProcessExited 调用它时它会给出一个错误 -

object reference not set to an instance of an object

主要形式

public partial class main : Form
{
    b rsSet = new b();

    public main()
    {
        InitializeComponent();
        rsSet.SetStatusEvent += new RemoteS.SetStatus(updateStatus);
    }

    private void button1_Click(object sender, EventArgs e)
    {
        rsSet.FormatUSB();
    }

    private delegate void UpdateStatus(int i, string str, Color clr);

    private void SetStatus(int i, string str, Color clr)
    {
        this.progressBar1.Value = i;
        this.lbl_status.ForeColor = clr;
        this.lbl_status.Text = str;
    }

    private void updateStatus(int i, String msg, Color color)
    {
        object[] p = GetInokerPara(i, msg, color);
        BeginInvoke(new UpdateStatus(SetStatus), p);
    }

    private object[] GetInokerPara(int progress, string msg, Color color)
    {
        object[] para = new object[3];
        para[0] = progress;
        para[1] = msg;
        para[2] = color;

        return para;
    }
}

b类

class b
{
    public delegate void SetStatus(int i, string msg, Color color);
    public event SetStatus SetStatusEvent;

    System.Diagnostics.Process usbfor = new System.Diagnostics.Process();

    public void FormatUSB()
    {

        usbfor.StartInfo.FileName = @"usbformat.bat";
        usbfor.EnableRaisingEvents = true;
        usbfor.Exited += new EventHandler(usbforProcessExited);
        usbfor.Start();
    }

    public void usbforProcessExited(object sender, EventArgs f)
    {
        SetStatusEvent(100, "DONE", Color.Green); //ERROR HERE! (object reference not set to an instance of an object
    }
}

问题出在哪里?

4

4 回答 4

10

如果没有订阅者,则该事件为空。

有两种解决方案:

  1. 声明时初始化事件(虚拟订阅者什么都不做):

    public event SetStatus SetStatusEvent = delegate { };
    
  2. 在引发之前检查事件是否为 null(以线程安全的方式):

    public void usbforProcessExited(object sender, EventArgs f)
    {
        SetStatus setStatus = SetStatusEvent;
        if (setStatus != null)
        {
            setStatus(100, "DONE", Color.Green);
        }
    }
    
于 2013-07-31T07:39:54.450 回答
4

你有一个竞争条件:

usbforProcessExited在 的构造函数中被订阅,b并且可能在您调用rsSet.SetStatusEvent += new RemoteS.SetStatus(updateStatus).

你应该只usbfor.Start()在你订阅后打电话SetStatusEvent

一个相关的问题是该事件将在另一个线程上运行。您应该rsSet.SynchronizingObject在开始流程之前进行设置,以便您的事件处理程序可以修改表单而无需手动调用Invoke/ BeginInvoke

于 2013-07-31T07:34:32.647 回答
2

Jon Skeet 告诉我,在 c# 6.0 中,您还可以使用:

SetStatusEvent?.Invoke(100, "DONE", Color.Green);;
于 2016-12-15T13:33:31.183 回答
1

该事件是null,如果还没有人订阅它。因此,控制null平等是一个很好的做法,例如:

    public void usbforProcessExited(object sender, EventArgs f)
    {
        if(SetStatusEvent!=null)
            SetStatusEvent(100, "DONE", Color.Green);
    }

这就是为什么它在外面运行良好的原因,正如你所拥有的,这条线:

 rsSet.SetStatusEvent += new RemoteS.SetStatus(updateStatus);

所以订阅和初始化事件。

当你从里面调用它时,没有任何订阅,所以 event 是null

编辑

在评论之后,让我们提供更多线程安全的方法来处理事件的空引用检查:

   public void usbforProcessExited(object sender, EventArgs f)
    {
        var ev = SetStatusEvent; //[1]
        if(ev!=null) //[2]
            ev(100, "DONE", Color.Green);
    }

请记住,CLR 中的赋值操作是原子的,因此即使在第 [1] 和 [2] 行之间,其他人将事件重置为 null,您的事件ev仍然有效并且代码将执行而不会崩溃。如果这是所需的行为,则取决于您的具体情况,因此这只是以线程安全方式管理事件的空引用控制的另一种选择。

于 2013-07-31T07:33:36.433 回答