【问题标题】:Example of "using exceptions to control flow" [closed]“使用异常来控制流程”的示例 [关闭]
【发布时间】:2011-03-16 15:17:54
【问题描述】:

“使用异常来控制流程”的一段代码会是什么样子?我试图找到一个直接的 C# 示例,但找不到。为什么不好?

谢谢

【问题讨论】:

  • 我一直认为流程控制异常通常都是自定义异常,而不是像 FileNotFoundException 这样的内置异常——我认为 Kirien 的回答是最正确的。但是,这很糟糕,因为处理所有这些需要开销。
  • 作为一般经验法则,您应该假设抛出和处理异常所需的时间是普通代码的 1000 倍。

标签: c# exception exception-handling


【解决方案1】:

一个例子是使用异常从递归方法返回结果:

public void Search(Node node, object data)
{
    if(node.Data.Equals(data))
    {
        throw new ResultException(node);
    }
    else
    {
        Search(node.LeftChild, data);
        Search(node.RightChild, data);
    }    
}

出于多种原因,这样做会造成问题。

  1. 这完全违反直觉。例外是为特殊情况设计的。按预期工作的事情不应该(我们希望)成为例外情况。
  2. 您不能总是依赖抛出并传播给您的异常。例如,如果抛出异常的代码在单独的线程中运行,则需要一些额外的代码来捕获它。
  3. 这是一个潜在的性能问题。存在与异常相关的开销,如果您抛出大量异常,您可能会发现应用程序的性能下降。

关于这个主题here,还有更多示例和一些有趣的讨论。

免责声明:上面的代码改编自该 wiki 页面上的第一个示例,以将其转换为 C#。

【讨论】:

  • 好吧,这有什么问题?如果GetValueOfX 是一个调用大量函数的复杂例程,每个函数都可能失败,那么如果您需要x 的合理默认值,您可能有一个代码用例。
  • @Alex 感谢您的评论。我实际上继续前进并完全重写了我的答案。不过,This answerthis one 也可以向我提出您的问题。希望对您有所帮助。
  • 很好。实际上这个成语在方案中很常见(使用call/cc),在一定程度上在C中(使用setjmp/longjmp)。如果 C# 有轻量级的异常处理(例如,像 C++ 一样),我会发现它是可以接受的。毕竟,您确实希望在找到正确的项目后立即跳上调用堆栈。但是,C# 中的异常带有很多与调试相关的状态(例如,调用跟踪),因此这是不受欢迎的。
【解决方案2】:

合作伙伴开发的模块导致我们的应用程序需要很长时间才能加载。仔细检查后,该模块在应用程序启动时寻找配置文件。这本身并不太令人反感,但这样做的方式非常糟糕:

对于 app 目录中的每个文件,它都会打开该文件并尝试将其解析为 XML。如果文件抛出异常(因为它不是 XML),它会捕获异常,压制它,然后尝试下一个文件!

小伙伴在测试这个模块的时候,他们的app目录下只有3个文件。傻瓜配置文件搜索对测试应用程序启动没有明显影响。当我们将它添加到我们的应用程序时,应用程序目录中有 100 多个文件,并且应用程序在启动时冻结了近一分钟。

为了给伤口加盐,模块正在搜索的配置文件的名称是预先确定的并且是恒定的。无需进行任何类型的文件搜索。

天才有其局限性。愚蠢是无限的。

【讨论】:

  • 所以 (typeof(Genius) == "Array") == (typeof(Stupidity) == "List") ?
  • 是否修复了与查找已翻译资源有关的类似控制流程?
  • 是的,可能和这个情况一样。
【解决方案3】:

我目前正在使用执行此操作的第 3 方程序。他们有一个“光标”接口(基本上是一个 IEnumerable 替代方案),告诉程序您完成的唯一方法是引发异常。代码基本上是这样的:

// Just showing the relevant section
bool finished = false;

public bool IsFinished()
{
    return finished;
}

// Using something like:
// int index = 0;
// int count = 42;

public void NextRecord()
{
    if (finished)
        return;

    if (index >= count)
        throw new APIProgramSpecificException("End of cursor", WEIRD_CONSTANT);
    else
        ++index;
}

// Other methods to retrieve the current value

不用说,我讨厌 API - 但它是流控制异常的一个很好的例子(以及一种疯狂的工作方式)。

【讨论】:

    【解决方案4】:

    不好

    下面的代码捕获了一个可以很容易完全避免的异常。这使得代码更难理解,并且通常也会产生性能成本。

    int input1 = GetInput1();
    int input2 = GetInput2();
    
    try
    {
        int result = input1 / input2;
        Output("{0} / {1} = {2}", input1, input2, result);
    }
    catch (OverflowException)
    {
        Output("There was an overflow exception. Make sure input2 is not zero.");
    }
    

    更好

    此代码检查抛出异常的条件,并在错误发生之前纠正这种情况。这种方式根本没有例外。代码可读性更强,性能很有可能会更好。

    int input1 = GetInput1();
    int input2 = GetInput2();
    
    while (input2 == 0)
    {
        Output("input2 must not be zero. Enter a new value.");
        input2 = GetInput2();
    }
    
    int result = input1 / input2;
    Output("{0} / {1} = {2}", input1, input2, result);
    

    【讨论】:

    • 所以归结为不使用异常来执行逻辑来治愈异常并事先进行验证?因此,异常应该用于真正异常且无法预见的事情。
    • @dotnetdev:通常是的。虽然有时这可以通过其他方式掩盖......
    • @dotnetdev:是的,尽管您通常知道可能会出现什么样的异常。如果您调用对磁盘上的文件执行某些操作的 fn,则可能会出现文件异常。等
    【解决方案5】:

    可能是我见过的最严重的违规行为:

    // I haz an array...
    public int ArrayCount(object[] array)
    {
        int count = 0;
        try
        {
            while (true)
            {
                var temp = array[count];
                count++;
            }
        }
        catch (IndexOutOfRangeException)
        {
            return count;
        }
    }
    

    【讨论】:

    • 天哪,你有认真看到这个吗?
    • 其实我想我是从 The Daily WTF 那里偷来的。所以我只在网站上“看到”了。
    • 信不信由你,在 VBA 中查询维数或数组的唯一方法是尝试直到发生错误。
    【解决方案6】:

    我不喜欢 C#,但您可以看到 try-catch-finally 语句和普通控制流语句 if-then-else 之间的一些相似之处。 p>

    请考虑一下,每当您 throw 出现异常时,您会强制将控制权传递给 catch 子句。所以如果你有

    if (doSomething() == BAD) 
    {
      //recover or whatever
    }
    

    你可以很容易地从 try-catch 的角度来思考它:

    try
    {
      doSomething();
    }
    catch (Exception e)
    {
      //recover or do whatever
    }
    

    异常的强大之处在于您不必在同一个主体中更改程序的流程,您可以随时抛出异常,并保证控制流会突然发散并到达捕获条款。这很强大,但同时也很危险,因为您可能已经完成了需要一些备份的操作,这就是存在 finally 语句的原因。

    此外,您还可以对while 语句进行建模,而无需有效地使用它的条件:

    while (!finished)
    {
      //do whatever
    }
    

    可以变成

    try
    {
      while (true)
      {
         doSomethingThatEventuallyWillThrowAnException();
      }
    }
    catch (Exception e)
    {
      //loop finished
    }
    

    【讨论】:

    • 关于异常捕获可能是堆栈中的很多父亲的优秀评论。
    【解决方案7】:

    根据定义,异常是在软件正常流程之外发生的事件。我想到的一个简单示例是使用FileNotFoundException 来查看文件是否存在。

    try
    {
        File.Open(@"c:\some nonexistent file.not here");
    }
    catch(FileNotFoundException)
    {
        // do whatever logic is needed to create the file.
        ...
    }
    // proceed with the rest of your program.
    

    在这种情况下,您没有使用File.Exists() 方法,该方法实现了相同的结果,但没有异常的开销。

    除了使用不当之外,还有与异常、填充属性、创建堆栈跟踪等相关的开销。

    【讨论】:

    • 哎呀,这不是一个很好的例子。 File.Exists() 不是多任务操作系统上的可靠替代方案。
    • 是的。尽管如此,依赖异常与使用框架提供的方法(双关语)之间的对比是有效的。
    • @Malfist,“方法”的意思是(在这个词的一般英语意义上)完成某事的方法与(在面向对象的上下文中)类支持的操​​作。不是我最好的作品,但我把它们带到我能得到它们的地方。 :-)
    • Hans:如果你刚刚检查了一个文件是否存在,然后你去打开它并得到一个FileNotFoundException,是不是最好尝试在它的位置创建一个新文件,或者抛出例外?
    • 如果我需要测试一个对象是否被创建了怎么办?示例:创建您未编写的类的对象失败,这会破坏系统/应用程序,我需要将控制流切换到回滚某些操作以修复系统/应用程序的方法?在这种情况下,我唯一的选择是将其作为异常捕获,然后切换流程。对吗?
    【解决方案8】:

    它大致相当于一个 goto,除了 Exception 这个词更糟,而且开销更大。您是在告诉代码跳转到 catch 块:

    bool worked;
    try
    {
        foreach (Item someItem in SomeItems)
        {
            if (someItem.SomeTestFailed()) throw new TestFailedException();
        }
        worked = true;
    }
    catch(TestFailedException testFailedEx)
    {
        worked = false;
    }
    if (worked) // ... logic continues
    

    如您所见,它正在运行一些(编造的)测试;如果失败,则抛出异常,worked 将被设置为 false

    当然,直接更新bool worked 要容易得多!

    希望有帮助!

    【讨论】:

    • +1 虽然其他一些示例也在控制流量,但我认为这是需要警告的噩梦场景。
    【解决方案9】:

    这是一个常见的:

    public bool TryParseEnum<T>(string value, out T result)
    {
        result = default(T);
    
        try
        {
            result = (T)Enum.Parse(typeof(T), value, true);
            return true;
        }
        catch
        {
            return false;
        }
    }
    

    【讨论】:

    • 这是一个很好的例子,尤其是当一个很好的替代方案 - TryParse - 已经存在时
    • +1 - 我在我处理过的代码中看到了这一点 - 它在排序过程中被调用了很多次并导致性能问题。像 ChrisF 一样使用 TryParse 说消除了性能问题。但是,我相信这是 .NET 2.0 之前的唯一方法。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-12-01
    • 2015-08-24
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-01-05
    相关资源
    最近更新 更多