【问题标题】:Unreachable code, but reachable with an exception无法访问的代码,但可以通过异常访问
【发布时间】:2019-08-02 15:38:58
【问题描述】:

此代码是读取和写入 ODBC 连接数据库的应用程序的一部分。它在数据库中创建一条记录,然后检查一条记录是否已成功创建,然后返回true

我对控制流的理解如下:

command.ExecuteNonQuery() 被记录为在“方法调用对于对象的当前状态无效”时抛出 Invalid​Operation​Exception。因此,如果发生这种情况,try 块的执行将停止,finally 块将被执行,然后将执行底部的return false;

但是,我的 IDE 声称 return false; 是无法访问的代码。这似乎是真的,我可以删除它,它编译时没有任何抱怨。但是,对我来说,抛出上述异常的代码路径似乎没有返回值。

private static bool createRecord(String table,
                                 IDictionary<String,String> data,
                                 System.Data.IDbConnection conn,
                                 OdbcTransaction trans) {

    [... some other code ...]

    int returnValue = 0;
    try {
        command.CommandText = sb.ToString();
        returnValue = command.ExecuteNonQuery();

        return returnValue == 1;
    } finally {
        command.Dispose();
    }

    return false;
}

我在这里的理解错误是什么?

【问题讨论】:

标签: c# exception unreachable-code


【解决方案1】:

finally块会被执行,然后会执行return false;在底部。

错了。 finally 不会吞下异常。它尊重它,异常将照常抛出。它只会在块结束之前执行 finally 中的代码(有或没有异常)。

如果你想吞下异常,你应该使用一个 catch 块,其中没有 throw

【讨论】:

  • 如果出现异常,上面的sinppet是否会编译,返回什么?
  • 它确实可以编译,但它永远不会命中return false,因为它会抛出异常而不是@EhsanSajjad
  • 看起来很奇怪,可以编译,因为它要么在没有异常的情况下返回 bool 的值,而在出现异常的情况下什么都不会,所以满足方法的返回类型是合法的?
  • 编译器将忽略该行,这就是警告的目的。那为什么会奇怪呢? @EhsanSajjad
  • 有趣的事实:实际上保证如果程序中没有捕获到异常,finally 块将运行。规范不保证这一点,早期的 CLR 确实 NOT 执行 finally 块。我认为从 4.0(可能更早)开始,行为发生了变化,但其他运行时可能仍然表现不同。造成相当令人惊讶的行为。
【解决方案2】:

当抛出异常时,堆栈将展开(执行将移出函数)而不返回值,并且函数上方的堆栈帧中的任何 catch 块将改为捕获异常。

因此,return false 永远不会执行。

尝试手动抛出异常以了解控制流:

try {
    command.CommandText = sb.ToString();
    returnValue = command.ExecuteNonQuery();

    // Try this.
    throw new Exception("See where this goes.");

    return returnValue == 1;
} finally {
    command.Dispose();
}

【讨论】:

    【解决方案3】:

    Compiler Warning (level 2) CS0162

    检测到无法访问的代码

    编译器检测到永远不会执行的代码。

    这就是说,编译器通过静态分析理解得足够多,无法达到它并从编译的IL中完全省略它(因此你的警告)。

    注意:您可以通过使用调试器尝试 Step on the Unreachable Code 或使用 IL Explorer 来向自己证明这一事实。

    finally 可能会在 Exception 上运行,(尽管除此之外)它不会改变事实(在这种情况下)它仍然是 Uncaught Exception。因此,最后一个return 无论如何都不会被击中。

    • 如果您希望代码继续到最后一个return,您唯一的选择是Catch Exception;

    • 如果您不这样做,请保持原样并删除 return

    示例

    try 
    {
        command.CommandText = sb.ToString();
        returnValue = command.ExecuteNonQuery();
    
        return returnValue == 1;
    }
    catch(<some exception>)
    {
       // do something
    }
    finally 
    {
        command.Dispose();
    }
    
    return false;
    

    引用文档

    try-finally (C# Reference)

    通过使用 finally 块,您可以清理任何资源 在 try 块中分配,即使出现异常也可以运行代码 发生在 try 块中。通常,finally 块的语句 当控制离开 try 语句时运行。控制权转移可以 由于正常执行、执行中断而发生, continue、goto 或 return 语句,或异常传播 在 try 语句之外。

    在已处理的异常中,保证关联的 finally 块 要运行。 但是,如果异常未处理,则执行 finally 块取决于异常展开操作的方式 触发。这又取决于您的计算机的设置方式。

    通常,当未处理的异常结束应用程序时,无论是 不是 finally 块运行并不重要。 但是,如果您有 即使在这种情况下也必须运行的 finally 块中的语句, 一种解决方案是在 try-finally 语句中添加一个 catch 块。 或者,您可以捕获可能在 调用堆栈更高层的 try-finally 语句的 try 块。那 就是,可以在调用方法的方法中捕获异常 包含 try-finally 语句,或在调用的方法中 该方法,或调用堆栈中的任何方法。如果异常是 没有被捕获,finally 块的执行取决于 操作系统选择触发异常展开操作。

    最后

    当使用任何支持IDisposable 接口(旨在释放非托管资源)的东西时,您可以将其包装在using 语句中。编译器将生成一个try {} finally {} 并在对象上内部调用Dispose()

    【讨论】:

    • 前几句中的 IL 是什么意思?
    • @Clockwork IL 是用高级 .NET 语言编写的代码编译的产物。编译用其中一种语言编写的代码后,您将获得由 IL 制成的二进制文件。请注意,中间语言有时也称为通用中间语言 (CIL) 或 Microsoft 中间语言 (MSIL)。
    • 简而言之,因为他没有抓住可能性是:要么尝试运行直到它达到返回,因此最终忽略下面的返回,要么抛出异常并且永远不会达到返回,因为由于抛出异常,函数将退出。
    【解决方案4】:

    最后一条语句 return false 无法访问,因为 try 块缺少将处理异常的 catch 部分,因此在 finally 块之后重新抛出异常,并且执行永远不会到达最后一条语句。

    【讨论】:

      【解决方案5】:

      在您的代码上:

      private static bool createRecord(String table, IDictionary<String,String> data, System.Data.IDbConnection conn, OdbcTransaction trans) {
      
          [... some other code ...]
      
          int returnValue = 0;
          try {
              command.CommandText = sb.ToString();
              returnValue = command.ExecuteNonQuery();
      
              return returnValue == 1; // You return here in case no exception is thrown
          } finally {
              command.Dispose(); //You don't have a catch so the exception is passed on if thrown
          }
      
          return false; // This is never executed because there was either one of the above two exit points of the method reached.
      }
      

      finally块会被执行,然后会执行return false;在底部

      这是您的逻辑中的缺陷,因为 finally 块不会捕获异常并且它永远不会到达最后一个 return 语句。

      【讨论】:

        【解决方案6】:

        看来,您正在寻找这样的东西:

        private static bool createRecord(string table,
                                         IDictionary<String,String> data,
                                         System.Data.IDbConnection conn,
                                         OdbcTransaction trans) {
          [... some other code ...]
        
          // Using: do not call Dispose() explicitly, but wrap IDisposable into using
          using (var command = ...) {
            try {
              // Normal flow:
              command.CommandText = sb.ToString();
        
              // True if and only if exactly one record affected
              return command.ExecuteNonQuery() == 1;
            }
            catch (DbException) {
              // Exceptional flow (all database exceptions)
              return false;
            }
          }
        }
        

        请注意,finally 不会吞下任何异常

        finally {
          // This code will be executed; the exception will be efficently re-thrown
        }
        
        // And this code will never be reached
        

        【讨论】:

          【解决方案7】:

          警告是因为你没有使用catch,而你的方法基本上是这样写的:

          bool SomeMethod()
          {
              return true;
              return false; // CS0162 Unreachable code detected
          }
          

          由于您仅使用finally 进行处理,因此首选的解决方案是使用using 模式:

          using(var command = new WhateverCommand())
          {
               ...
          }
          

          这足以确保将调用Dispose。保证在成功执行代码块之后或(之前)调用堆栈中的某些catch down 时调用它(父调用已关闭,对吗?)。

          如果不是关于处置,那么

          try { ...; return true; } // only one return
          finally { ... }
          

          就足够了,因为您将永远必须在方法结束时返回 false(不需要该行)。您的方法要么是命令执行的返回结果(truefalse),要么会抛出异常否则


          还可以考虑通过包装预期的异常来抛出自己的异常(查看InvalidOperationException constructor):

          try { ... }
          catch(SomeExpectedException e)
          {
              throw new SomeBetterExceptionWithExplanaition("...", e);
          }
          

          这通常用于对调用者说一些比嵌套调用异常更有意义(有用)的东西。


          大多数时候,您并不真正关心未处理的异常。有时您需要确保调用finally,即使异常未处理。在这种情况下,您只需自己抓住它并重新抛出(参见this answer):

          try { ... }
          catch { ...; throw; } // re-throw
          finally { ... }
          

          【讨论】:

            【解决方案8】:

            您没有catch 块,因此仍然抛出异常,从而阻止返回。

            finally块会被执行,然后会执行return false;在底部。

            这是错误的,因为finally块会被执行,然后会有一个未被捕获的异常。

            finally 块用于清理,它们不会捕获异常。异常是在return之前抛出的,因此永远不会到达return,因为之前抛出了异常。

            你的 IDE 是正确的,它永远不会被访问,因为会抛出异常。只有catch 块能够捕获异常。

            读自the documentation

            通常,当未处理的异常结束应用程序时,finally 块是否运行并不重要。但是,如果即使在这种情况下也必须在 finally 块中运行语句,则一种解决方案是将 catch 块添加到 try-finally 语句。或者,您可以捕获调用堆栈更高的 try-finally 语句的 try 块中可能引发的异常。也就是说,您可以在调用包含 try-finally 语句的方法的方法中,或在调用该方法的方法中,或在调用堆栈中的任何方法中捕获异常。 如果没有捕获到异常,finally块的执行取决于操作系统是否选择触发异常展开操作

            这清楚地表明 finally 并非旨在捕获异常,如果在 finally 语句之前有一个空的 catch 语句,您将是正确的。

            【讨论】:

              【解决方案9】:

              您的代码中有两个返回路径,其中第二个由于第一个而无法访问。 try 块中的最后一条语句 return returnValue == 1; 提供了正常返回,因此您永远无法到达方法块末尾的 return false;

              FWIW,与finally块相关的执行顺序是:首先计算try块中提供返回值的表达式,然后执行finally块,然后返回计算出的表达式值(在 try 块内)。

              关于异常流程...如果没有catchfinally 将在异常被重新抛出方法之前执行;没有“返回”路径。

              【讨论】:

                猜你喜欢
                • 2019-07-18
                • 2015-08-20
                • 1970-01-01
                • 2013-05-18
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 2016-05-28
                • 2016-06-30
                相关资源
                最近更新 更多