多窗体中的异常处理

10
我在调试和运行已编译的.exe文件时,发现异常捕获的行为不同。我的应用程序有两个窗体(Form1和Form2),Form1上有一个按钮,它实例化并调用Form2的ShowDialog方法。Form2上有一个按钮,它故意产生一个除以零错误。当我在调试时,Form1中的catch块被触发。但是当我运行编译后的.exe文件时,则没有被触发,而是会出现一个消息框,显示“应用程序中发生了未处理的异常。如果单击"继续",应用程序将忽略此错误并尝试继续运行。如果单击"退出",应用程序将立即关闭...Attempted to divide by zero”。我的问题是:为什么在调试和运行.exe文件时会得到不同的行为呢?如果这是预期的行为,那么是否需要在每个事件处理程序中都放置try/catch块呢?这似乎有点过度设计,对吗?
下面是Form1的代码:
public partial class Form1 : Form
{
    public Form1()
    {
            InitializeComponent();

    }

    private void button1_Click(object sender, EventArgs e)
    {
        try
        {
            Form2 f2 = new Form2();
            f2.ShowDialog();
        }
        catch(Exception eX)
        {
            MessageBox.Show( eX.ToString()); //This line hit when debugging only
        }
    }
}

以下是Form2的代码:

public partial class Form2 : Form
{
    public Form2()
    {
            InitializeComponent();
    }

    private void button1_Click(object sender, EventArgs e)
    {
            int x = 0;
            int y = 7 / x;

    }
}

是时候请Skeeter或Gravell出马了! - Jason Down
2个回答

12

是的,这是设计上的考虑,并与Windows Forms工作方式密切相关。在Winforms应用程序中,代码响应由Windows发送到活动窗口的消息而运行。每个本地的Windows应用程序都包含一个消息循环以检测这些消息。Winforms管道确保您的事件处理程序之一响应;例如,在您的示例代码中是button1_Click。

大多数Winforms控件实现了自己的事件处理程序。例如,PictureBox具有绘制事件处理程序,可确保其图像绘制到屏幕上。所有这些都是自动完成的,您不需要编写任何代码来使其工作。

然而,当此代码引发异常时存在问题,因为没有办法捕获这样的异常,因为没有涉及任何您自己编写的代码。换句话说,没有地方可以注入您自己的try块。涉及到您自己程序的代码的最后一部分是启动消息循环的代码。Application.Run()方法调用,通常在Program.cs中。或者如果您显示对话框,则是Form.ShowDialog()调用。其中任何一种方法都会启动消息循环。在Application.Run()调用周围放置try块没有意义,应用程序将在捕获异常后终止。

为解决这个问题,Winforms消息循环代码在派发事件的代码周围包含一个try块。它的catch子句显示您提到的对话框,由ThreadExceptionDialog类实现。

回答您的问题:当您调试时,这个catch子句确实会妨碍您解决问题。只有在没有处理异常的catch块时,调试器才会停止异常。但是当您的代码引发异常时,您希望在调试期间了解情况。上述消息循环中的代码知道是否附加了调试器。如果是,则派发事件而不使用try/catch块。现在,当您的代码引发异常时,没有catch子句来处理它,调试器将停止程序,让您有机会找出问题所在。

可能现在你已经明白为什么你的程序会表现出这样的行为。当你进行调试时,消息循环中的catch子句被禁用,使得Form1代码中的catch子句有机会捕获异常。没有进行调试时,消息循环的catch子句处理异常(通过显示对话框),并防止异常传递到Form1代码。

你可以通过调用Application.SetUnhandledExceptionMode()方法并传递UnhandledExceptionMode.ThrowException来完全避免使用消息循环的catch子句。在Main()方法中,在Application.Run()调用之前这样做。现在你的程序将以相同的方式运行。

一般来说,给用户提供在异常对话框中继续选项并不是个坏主意。在那种情况下,为AppDomain.UnhandledException事件实现一个事件处理程序,以便至少向用户提供一些诊断信息。


4
我遇到了和你一样的情况。我不知道为什么会出现这种情况,但是假设一个在表单中产生的异常会出现在ShowDialog()调用的堆栈中似乎是一个不好的想法。最好做这两件事情:
  • 在Form2的事件处理程序中捕获和处理异常,在那里有意义并且您可以对异常进行有意义的操作时这样做。
  • 为整个应用程序添加未处理异常处理程序(`Application_ThreadException`),以捕获任何未处理的异常。

更新:以下是堆栈跟踪。调试版本:

System.DivideByZeroException: Attempted to divide by zero.
   at WindowsFormsApplication1.Form2.button1_Click(Object sender, EventArgs e) in ...\WindowsFormsApplication1\Form2.cs:line 27
   at System.Windows.Forms.Control.OnClick(EventArgs e)
   at System.Windows.Forms.Button.OnClick(EventArgs e)
   at System.Windows.Forms.Button.OnMouseUp(MouseEventArgs mevent)
   at System.Windows.Forms.Control.WmMouseUp(Message& m, MouseButtons button, Int32 clicks)
   at System.Windows.Forms.Control.WndProc(Message& m)
   at System.Windows.Forms.ButtonBase.WndProc(Message& m)
   at System.Windows.Forms.Button.WndProc(Message& m)
   at System.Windows.Forms.Control.ControlNativeWindow.OnMessage(Message& m)
   at System.Windows.Forms.Control.ControlNativeWindow.WndProc(Message& m)
   at System.Windows.Forms.NativeWindow.DebuggableCallback(IntPtr hWnd, Int32 msg, IntPtr wparam, IntPtr lparam)
   at System.Windows.Forms.UnsafeNativeMethods.DispatchMessageW(MSG& msg)
   at System.Windows.Forms.Application.ComponentManager.System.Windows.Forms.UnsafeNativeMethods.IMsoComponentManager.FPushMessageLoop(Int32 dwComponentID, Int32 reason, Int32 pvLoopData)
   at System.Windows.Forms.Application.ThreadContext.RunMessageLoopInner(Int32 reason, ApplicationContext context)
   at System.Windows.Forms.Application.ThreadContext.RunMessageLoop(Int32 reason, ApplicationContext context)
   at System.Windows.Forms.Application.RunDialog(Form form)
   at System.Windows.Forms.Form.ShowDialog(IWin32Window owner)
   at System.Windows.Forms.Form.ShowDialog()
   at WindowsFormsApplication1.Form1.button1_Click(Object sender, EventArgs e) in ...\WindowsFormsApplication1\Form1.cs:line 45

发布:

System.DivideByZeroException: Attempted to divide by zero.
   at WindowsFormsApplication1.Form2.button1_Click(Object sender, EventArgs e) in ...\WindowsFormsApplication1\Form2.cs:line 27
   at System.Windows.Forms.Control.OnClick(EventArgs e)
   at System.Windows.Forms.Button.OnClick(EventArgs e)
   at System.Windows.Forms.Button.OnMouseUp(MouseEventArgs mevent)
   at System.Windows.Forms.Control.WmMouseUp(Message& m, MouseButtons button, Int32 clicks)
   at System.Windows.Forms.Control.WndProc(Message& m)
   at System.Windows.Forms.ButtonBase.WndProc(Message& m)
   at System.Windows.Forms.Button.WndProc(Message& m)
   at System.Windows.Forms.Control.ControlNativeWindow.OnMessage(Message& m)
   at System.Windows.Forms.Control.ControlNativeWindow.WndProc(Message& m)
   at System.Windows.Forms.NativeWindow.Callback(IntPtr hWnd, Int32 msg, IntPtr wparam, IntPtr lparam)

请注意,在发布模式下,System.Windows.Forms.Form.ShowDialog()不在堆栈跟踪中,这就是为什么您的try {} catch {}没有起作用的原因。值得注意的是,在调试情况下,它使用的是NativeWindow.DebuggableCallback,这可能是为了帮助调试而不会破坏堆栈,而在发布模式下则使用NativeWindow.Callback

有趣的是,如果我在调试模式下编译并运行.exe文件,我仍然看不到消息框(catch块没有被触发)。所以无论我是在发布版还是调试版,行为都是相同的...异常未被处理。但是通过代码步进,你会遇到catch块。我想这很合理,因为ShowDialog实际上不在发生错误的同一线程上。当调试时,你遇到异常处理程序的事实对我来说似乎是Visual Studio的一个bug。 - user216122
是的,看起来是Visual Studio改变了行为,尽管我不确定这是否是一个错误。它似乎是有意为之,以帮助调试。然而,这个特性会让你误以为从ShowDialog捕获异常是可以的,但实际上似乎并不起作用。 - Mark Byers

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