试图理解c#中的异常

本文关键字:异常 | 更新日期: 2023-09-27 18:07:13

我并没有真正在我的代码中使用任何try/catch,但我正试图打破这个习惯,现在开始使用异常。

我认为在我的应用程序中最重要的地方是读取文件,我现在正试图实现,但我不确定这样做的"最佳实践"。目前我正在做这样的事情:

private void Parse(XDocument xReader)
{
    IEnumerable<XElement> person = xReader.Descendants("Person").Elements();
    foreach (XElement e in person)
        personDic[e.Name.ToString()] = e.Value;
    if (personDic["Name"] == null || personDic["Job"] == null || personDic["HairColor"] == null)
        throw new KeyNotFoundException("Person element not found.");
}

但我不确定这是否正确。我有这个来处理它:

try
{
    personsReader.Read(filename, persons);
}
catch (KeyNotFoundException e)
{
    MessageBox.Show(e.Message);
    return;
}
// Do stuff after reading in the file..

然而,当显示e.Message时,它只显示通用的KeyNotFoundException错误消息,而不是由自定义错误消息。此外,我不确定是否在一般情况下,我正在正确地处理整个"异常处理的东西"。我确实在catch中返回,因为如果文件没有成功读取,显然我只是想假装用户从未尝试打开一个文件,让他再尝试打开另一个文件。

我这样做正确吗?同样,我对使用异常相当陌生,我想确保在继续并将其应用于我的程序的其余部分之前,我把它写下来了。

还有,为什么人们说不要做catch (Exception e) ?在这种情况下,我似乎想要这样做,因为无论读取文件时发生什么错误,如果有错误,我想要停止读取文件,显示错误消息,然后返回。这不总是这样吗?如果你想根据异常处理不同的东西,我可以理解不想处理异常e但在这种情况下,我不想处理基异常类以防出现问题吗?

试图理解c#中的异常

当您可以处理条件并做一些有用的事情时,您应该捕获异常。否则,您应该让它在调用堆栈中冒泡,也许您上面的人可以处理它。有些应用程序在最外层有未处理的异常处理程序来处理它,但一般来说,除非你知道你有一些有用的方法来处理它,否则就让它去吧。

在您的示例中,您正在处理无法读取资源并通知用户。你在处理它。对于泛型异常,您可以做的一件事是捕获并重新抛出一个更好的异常。如果这样做,请确保将根本原因异常作为内部异常包含在内。如果合适的话,您还可以跟踪或记录详细信息。

throw new MyGoodExceptionType ("Could not read file", e);  // e is caught inner root cause.

现在UI显示了一个很好的错误,也许内部根本原因是在日志等…

典型错误:

  • 在泛型库方法的堆栈深处处理异常:记住,一个通用的库函数可能在许多不同的代码路径中被调用。你可能不知道它是否应该被处理,以及处理它是否合适。堆栈中较高的调用者可能有上下文,并且知道处理它是否安全。通常,这意味着更高层的代码决定处理。

  • 吞咽异常:一些代码捕获异常(特别是在堆栈中较低的位置),然后根条件就消失了,使调试变得疯狂。再说一次,如果你能处理好,就去做吧。

  • 异常应该是例外的:不要使用异常进行流控制。例如,如果您正在读取资源,不要尝试读取,然后捕获异常并做出决策点。相反,调用ifexists,检查bool值并在代码中做出决定。当您将调试器设置为在异常时中断时,这尤其有用。您应该能够干净地运行,如果调试器坏了,这应该是一个真正的问题。当调试出现问题时,让调试器不断中断。我个人很少喜欢抛出异常,并且总是尽量避免流控制。

希望有帮助。

好的,首先…

…这不是KeynotFoundException,它应该是ArgumentException....提供的参数无效。

文档清楚地说明:

当为访问集合中的元素指定的键不正确时引发的异常不匹配集合中的任何键。

比较

当提供给方法的参数之一无效时引发的异常

:

还有,为什么人们说不要做catch (Exception e)?

因为这将吞下异常,使中央错误处理/日志记录不可能到位。只处理你期望的异常,除非它是一个catch/close或log/rethrow(即throw;)。然后有一个中央appdomain处理程序,获取每个未捕获的异常并记录它;)它不能处理任何事情——因为那个级别的异常是不可预料的。它基本上应该将异常写入文件并完成,可能使用UI(应用程序必须重新启动)。

就你所做的而言,看起来基本没问题。我不能说你是否应该在那个特定的点抛出异常,但是抛出和捕获是正确完成的。就信息而言,它应该按照它的方式工作。尝试显示e.ToString()以查看调用堆栈。这可能是简单地做person["Name"]先抛出KeyNotFoundException

关于捕获Exception的问题,它并不总是坏的。有时您无法预测所有可能的异常,有时处理任何可能的故障是一件好事。然而,它没有给你不同的方法来处理特定的异常。

作为一个例子,如果您得到KeyNotFoundException,您可能会提到文件的格式是如何不正确的,并且可能会在屏幕上向用户显示该文件。如果您得到FileNotfoundException,您可以向他们显示路径并打开OpenFileDialog,让他们选择一个新文件。由于安全权限引起的异常,您可以显示指令以让它们提升您的权限。有些异常甚至可能是可恢复的(可能一个元素格式错误,但其他元素还可以;它应该失败吗?)

但是,如果你想要这样设计它,那么捕获所有内容是可以的。最可靠的程序会捕获每一个可能的异常,并以非常具体的方式处理它,而不是将原始异常呈现给用户。它可以提供更好的用户体验,并为您提供解决可能发生的问题的方法。

大多数情况下,您可能不关心异常的类型,因此捕获泛型Exception是可以的,但是在某些特定情况下,您实际上想要捕获相关异常(而不仅仅是泛型Exception)。

一个特别的例子是,如果你有一个线程,你想从阻塞调用中中断它,在这种情况下,你必须区分InterruptExceptionException

考虑这个例子:你有一个线程每分钟运行一次Read,持续5分钟(这不是一个非常现实的例子,但它应该让你知道为什么你想处理不同的异常)。你必须在5分钟后停止线程,因为你的应用程序将要关闭,你不想再等一分钟来读取running标志…毕竟,您不希望用户为了关闭应用程序而等待整整一分钟。为了立即停止线程,您将该标志设置为false,并在线程上调用Interrupt。在这种情况下,您必须捕获ThreadInterrupted异常,因为它告诉您应该退出循环。如果捕获到另一个异常,则说明执行任务失败,但您不想完全放弃该任务,并且希望在下一分钟尝试再次读取。这描述了您的需求如何决定您需要处理的异常类型。下面是代码中的示例:

bool running = true;
Thread t = new Thread(()=>
{
    while(running)
    {
        try
        {
            // Block for 1 minute
            Thread.Sleep(60*1000); 
            // Perform one read per minute
            personsReader.Read(filename, persons);
        }
        catch (KeyNotFoundException e)
        {
            // Perform a specific exception handling when the key is not found
            // but do not exit the thread since this is not a fatal exception
            MessageBox.Show(e.Message);
        }
        catch(InterruptException)
        {
            // Eat the interrupt exception and exit the thread, because the user
            // has signalled that the thread should be interrupted.
            return;
        }
        catch(Exception e)
        {
            // Perform a genetic exception handling when another exception occurs
            // but do not exit the thread since this is not a fatal error.
            MessageBox.Show("A generic message exception: " + e.Message);
        }
    }
});
t.IsBackground = true;
t.Start();
// Let the thread run for 5 minutes
Thread.Sleep(60*5000);
running = false;
// Interrupt the thread
t.Interrupt();
// Wait for the thread to exit
t.Join();

现在轮到你的另一个问题,异常没有出现:请注意,你正在访问person[e.Name.ToString()] = e.Value,这需要一个键查找,如果键不在映射中,那么你可能会得到一个KeyNotFoundException。这将是您正在捕获的通用异常,您的自定义异常将永远不会被抛出,因为person[e.Name.ToString()]可能会在您甚至到达代码之前抛出。

foreach (XElement e in person)
    person[e.Name.ToString()] = e.Value; // <-- May be throwing the KeyNotFoundException
if (person["Name"] == null || person["Job"] == null || person["HairColor"] == null)
    throw new KeyNotFoundException("Person element not found.");

此外,当您实际找到键但没有找到相应的值时,您不想抛出KeyNotFoundException:如果person["Name"] == null的计算结果为true,则键"Name"实际上是在person字典中找到的,因此抛出KeyNotFoundException会误导捕获该异常的任何人。如果你的值是null,那么抛出异常可能是不明智的……这真的不是特例。您可以返回一个标志,表示没有找到密钥:

public bool PerformRead(/*... parameters ...*/)
{
    foreach (XElement e in person)
    {
        // Avoid getting the KeyNotFoundException
        if(!person.ContainsKey(e.Name.ToString()))
        {
            person.Add(e.Name.ToString(), "some default value");
        }
        person[e.Name.ToString()] = e.Value;
    }
    if (person["Name"] == null || person["Job"] == null || person["HairColor"] == null)
    {
        return false;
    }
    else
    {
        return true;
    }
}

我不太确定为什么您没有获得自定义错误消息。这应该发生(除非是else抛出KeyNotFoundException,而不是你显式抛出的那个)。

此外,通常应该将所有依赖于文件读取是否成功的代码放在try中,这通常是方法主体的其余部分。您不再需要在catch块中返回,并且不依赖于成功读取文件的后续代码仍然可以在失败后执行。

的例子:

public static void Main()
{
    var filename = "whatever";
    try
    {
        personsReader.Read(filename, persons);
        var result = personsReader.DoSomethingAfterReading();
        result.DoSomethingElse();
    }
    catch (KeyNotFoundException e)
    {
        MessageBox.Show(e.Message);
    }
    finally
    {
        personsReader.CloseIfYouNeedTo();
    }
    DoSomeUnrelatedCodeHere();
}

不捕获任何旧的Exception e是一个好习惯的原因是因为你只想捕获和处理你期望得到的异常。如果你得到了一种你没有预料到的不同类型的异常,这通常意味着一些新奇的东西以你没有预料到的方式失败了,你希望这种行为被注意到,而不是被所有常规的错误处理代码掩盖。

许多生产级系统在整个程序周围都有一个大的try/catch,它捕获任何异常,并在优雅地崩溃之前执行日志记录和清理。通过在代码的更深处使用特定的try/catch块,以良好定义的方式处理预期的异常,可以补充这一点。对于意想不到的异常,您总是可以让CLR不优雅地爆炸,并从中找出发生了什么。

下面是一个新奇异常的例子。如果出现了严重的问题,在这一行中:

IEnumerable<XElement> person = xReader.Descendants("Person").Elements();

…你有OutOfMemoryException吗?您真的应该只是向用户显示一个弹出窗口,并允许您的程序尝试像正常一样运行,即使它根本无法做到这一点吗?如果因为OutOfMemoryException上静默失败,您稍后尝试解引用一个空引用,并得到一个导致程序崩溃的NullReferenceException,该怎么办?您将花费很长时间来查找引用为null的根本原因。

找出bug的最好方法是快速失败,并且失败的声音很大。

"Exceptions for Exceptional circumstances" - unknown

不要使用方法将消息从方法传递给调用方法。永远试着优雅地处理事情。当真正奇怪的事情发生时,抛出异常。这是开发人员不太熟悉如何使用异常的事情。

在您的代码中,当您计算if语句中的条件时,您将触发xxx

如果这些键不在你的字典中,请求person["Name"] == null || person["Job"] == null || person["HairColor"] == null将失败。

你需要这样做:

if (!person.ContainsKey("Name"] ||
    !person.ContainsKey("Job"] ||
    !person.ContainsKey("HairColor"))

所以,抛出异常的调用永远不会被执行!这就是为什么你从来没有看到你的消息。

对于这种编码,我会保持你不做异常的习惯。异常代价高昂,并且可能导致隐藏代码中的实际问题。

不要捕获一般异常,也不要为非异常情况创建异常。