17

问题: 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# 语言规范的第 5.3.3.11 部分——这可能是查找此类信息的错误位置;
  • 通过我可以尝试在 throw 语句上触发的异常进行修饰。OutOfMemoryException 浮现在脑海中,但在throw.
  • 打开 ILDASM 以检查生成的代码。我可以看到它throw转换为一条rethrow指令,但我不知道去哪里进一步检查该语句是否可以抛出异常。

这是 ILDASM 显示的内部try位:

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

所以,总结一下:一个 throw 语句(用于重新抛出异常)本身会导致异常吗?

4

5 回答 5

16

老实说,理论上断言可能会“失败”(实际上我不这么认为)。

如何?

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

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

编辑:

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

警告:商业 CLR 可能与 SSCLI 有很大不同。

  1. 无效程序异常:

    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. 粗鲁的线程中止:

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

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

  3. 现在最有趣的是,OutOfMemoryException. 由于 rethrow 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 ,如果失败则 raise OutOfMemoryExceptionSetLastThrownObject这是来自(添加了我的评论)的片段

    ...
    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);
    

    上面的代码片段显示可抛出对象的 GC 句柄被销毁(即释放 GC 表中的一个槽),然后创建一个新句柄。由于一个槽刚刚被释放,新的句柄创建将永远不会失败,直到在非常罕见的情况下,一个新线程被安排在正确的时间并消耗所有可用的 GC 句柄。

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

于 2012-06-26T10:21:18.440 回答
7

C# 中的普通 throw 语句本身会导致新的异常吗?

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

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

于 2012-06-25T19:24:30.847 回答
5

我怀疑您缺少的位可能rethrowECMA-335分区 III 第 4.24 节中的规范:

4.24 rethrow——重新抛出当前异常

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

异常:
抛出原始异常。

(强调我的)

所以是的,看起来你的断言可以保证按照规范工作。(当然这是假设实现遵循规范......)

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

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

这再次表明原始异常并且只会抛出该异常。

(您提到的第 5.3.3.11 节只是在谈论明确的分配,而不是throw语句本身的行为。)

当然,这一切都不会使 Amit 的观点无效,这些观点是针对某些超出任一地方规定范围的情况。(当主机应用附加规则时,语言规范很难考虑它们。)

于 2012-07-18T06:33:58.203 回答
1

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

于 2012-06-26T01:35:06.893 回答
0

在 64 位平台上结合递归平原throw很容易引起。StackOverflowException

class Program
{
    // expect it to be 10 times less in real code
    static int max = 455;

    static void Test(int i)
    {
        try {
            if (i >= max) throw new Exception("done");
            Test(i + 1);
        }
        catch {
            Console.WriteLine(i);
            throw;
        }
    }

    static void Main(string[] args)
    {
        try {
            Test(0);
        }
        catch {
        }
        Console.WriteLine("Done.");
    }
}

在控制台中:

...
2
1
0

Process is terminated due to StackOverflowException.

可以在这里找到一些解释。

于 2015-05-14T09:09:54.713 回答