C#中的(plain)throw语句会导致异常吗?

时间:2012-06-25 19:21:50

标签: c# exception

问题: C#中的普通throw语句是否会导致新的异常?


请注意,我从好奇心中提出这个问题,并不是因为我有任何实际或现实世界的情况。还要注意我的直觉和经验告诉我答案是“不”,但我想以某种方式验证这个答案(进一步了解到目前为止我尝试过的来源)。

以下是一些示例代码来说明我的问题:

try
{
    int x = 0, y = 1 / x;
}
catch (Exception outerException)
{

    try
    {
        throw;
    }
    catch (Exception innerException)
    {
        // Q: Does this Assert ever fail??
        System.Diagnostics.Debug.Assert(outerException.Equals(innerException));
    }
}

我想知道是否有任何方法可以改变环境,使Assert失败,而不会触及内部try/catch块。

我尝试或试图回答这个问题:

  • 阅读MSDN上的throw (C# Reference)页面 - 没有明确的答案;
  • 检查C# Language Specification的第5.3.3.11部分 - 这可能是查找此类信息的错误位置;
  • 通过我可以尝试在throw语句上触发的异常来理解。我想到了OutOfMemoryException,但在throw时很难触发。
  • 打开ILDASM以检查生成的代码。我可以看到throw转换为rethrow指令,但我失去了进一步查看该语句是否可以抛出异常的地方。

这是ILDASM为内部try位显示的内容:

.try
{
  IL_000d:  nop
  IL_000e:  rethrow
}  // end .try

因此,总结一下:抛出语句(用于重新抛出异常)是否导致异常本身?

5 个答案:

答案 0 :(得分:15)

在我的诚实意见中,理论上断言可以“失败”(实际上我不这么认为)。

如何?

注意:以下是我之前对SSCLI进行的一些研究的“意见”。

  1. 可能会发生InvalidProgramException。这无疑是非常不可能的,但理论上是可能的(例如一些内部CLR错误可能导致throwable对象变得不可用!!!!)。
  2. 如果CLR没有找到足够的内存来处理're-throw'动作,它将抛出一个OutOfMemoryException(CLR的内部重新抛出逻辑需要分配一些内存,如果它不处理'预先分配'的异常,如OutOfMemoryException异常)。
  3. 如果CLR在某些其他主机(例如SQL服务器甚至您自己的主机)下运行,并且主机决定终止Exception重新抛出线程(基于某些内部逻辑)ThreadAbortException(称为粗鲁线程中止)在这种情况下)将被提出。但是,我不确定在这种情况下Assert是否会执行。
  4. 自定义主机可能已将升级策略应用于CLR(ICLRPolicyManager::SetActionOnFailure)。在这种情况下,如果您正在处理OutOfMemoryException,则升级策略可能会导致发生ThreadAbortException(再次粗鲁的线程中止。不确定如果策略指示正常的线程中止会发生什么)。
  5. 虽然@Alois Kraus澄清了'正常'线程中止异常是不可能的,但从SSCLI研究中我仍然怀疑(正常)ThreadAbortException可能发生。
  6. 编辑:

    正如我之前所说,断言在理论上可能会失败,但实际上它是非常不可能的。因此,为此开发POC非常困难。 为了提供更多“证据”,以下是SSCLI代码中用于处理rethow IL指令的片段,用于验证我的上述观点。

    警告:商业CLR与SSCLI的区别非常广泛。

    1. InvalidProgramException:

      if (throwable != NULL)
      {
       ...
      }
      else
      {
          // This can only be the result of bad IL (or some internal EE failure).
          RealCOMPlusThrow(kInvalidProgramException, (UINT)IDS_EE_RETHROW_NOT_ALLOWED);
      }
      
    2. Rude Thread Abort:

      if (pThread->IsRudeAbortInitiated())
      {
          // Nobody should be able to swallow rude thread abort.
          throwable = CLRException::GetPreallocatedRudeThreadAbortException();
      }
      

      这意味着如果启动了'粗鲁线程中止',则任何异常都会更改为粗鲁线程中止异常。

    3. 现在最有趣的是OutOfMemoryException。由于重新抛出IL指令实质上会重新抛出相同的Exception对象(即object.ReferenceEquals返回true),因此在重新抛出时可能无法发生OutOfMemoryException。但是,遵循SSCLI代码表明它是可能的:

       // Always save the current object in the handle so on rethrow we can reuse it. This is important as it
      // contains stack trace info.
      //
      // Note: we use SafeSetLastThrownObject, which will try to set the throwable and if there are any problems,
      // it will set the throwable to something appropiate (like OOM exception) and return the new
      // exception. Thus, the user's exception object can be replaced here.
      
      throwable = pThread->SafeSetLastThrownObject(throwable);
      

      SafeSetLastThrownObject调用SetLastThrownObject,如果失败则引发OutOfMemoryException。以下是SetLastThrownObject的摘录(添加了我的评论)

      ...
      if (m_LastThrownObjectHandle != NULL)
      {
         // We'll somtimes use a handle for a preallocated exception object. We should never, ever destroy one of
        // these handles... they'll be destroyed when the Runtime shuts down.
        if (!CLRException::IsPreallocatedExceptionHandle(m_LastThrownObjectHandle))
        {
           //Destroys the GC handle only but not the throwable object itself
           DestroyHandle(m_LastThrownObjectHandle);
        }
      }
      ...
      
      //This step can fail if there is no space left for a new handle
      m_LastThrownObjectHandle = GetDomain()->CreateHandle(throwable);
      

      上面的代码片段显示抛出了throwable对象的GC句柄(即释放GC表中的一个插槽),然后创建一个新句柄。由于刚刚发布了一个插槽,新的句柄创建永远不会失败,直到非常罕见的情况下,新线程在恰当的时间进行调度并消耗掉所有可用的GC句柄。

    4. 除此之外,所有例外(包括重新抛出)都是通过RaiseException win api提出的。捕获此异常以准备相应的托管异常的代码本身可以引发OutOfMemoryException

答案 1 :(得分:7)

  

C#中的普通抛出语句本​​身是否会导致新的异常?

根据定义,它不会。 throw;的要点是保留活动异常(尤其是堆栈跟踪)。

理论上,一个实现可能会克隆异常,但重点是什么?

答案 2 :(得分:5)

我怀疑你缺少的那个可能是rethrow的规范,它在ECMA-335内,分区III,第4.24节:

  

4.24重新抛出 - 重新抛出当前异常

     

说明
  重新抛出指令只允许在catch处理程序的主体内(参见   分区I)。它会抛出此处理程序捕获的相同的异常。   重新抛出不会更改对象中的堆栈跟踪。

     

例外:
  抛出原始异常。

(强调我的)

所以是的,看起来您的断言可以保证按照规范运行。 (当然这是假设一个实现遵循规范...)

C#规范的相关部分是第8.9.5节(C#4版本):

  

没有表达式的throw语句只能在catch块中使用,在这种情况下,该语句会重新抛出当前正由该catch块处理的异常。

再一次,建议原始异常和该异常将被抛出。

(你所提到的第5.3.3.11节只是谈论明确的赋值,而不是throw语句本身的行为。)

当然,这些都不会使阿米特的观点无效,这些观点的情况有些超出了任何一个地方指定的范围。 (当主机应用其他规则时,语言规范很难考虑它们。)

答案 3 :(得分:1)

您的断言永远不会失败,因为重新抛出和断言之间没有代码。如果您捕获异常并导致另一个异常,则异常发生变化的唯一方法 - 例如。通过在您的catch子句中使用错误代码或“抛出新”,

答案 4 :(得分:0)

结合递归plain \\\/Date(1382459367723)\\\/很容易在64位平台上导致\/Date(1382459367723)\/

throw

在控制台中:

StackOverflowException

可能会找到一些解释here