"throw"和"throw ex"有什么区别吗?

553

已经有一些帖子询问这两者之间的区别了。(为什么我还要提到这个...)
但我的问题与众不同,我是在另一个神级错误处理方法中调用“throw ex”。

public class Program {
    public static void Main(string[] args) {
        try {
            // something
        } catch (Exception ex) {
            HandleException(ex);
        }
    }

    private static void HandleException(Exception ex) {
        if (ex is ThreadAbortException) {
            // ignore then,
            return;
        }
        if (ex is ArgumentOutOfRangeException) { 
            // Log then,
            throw ex;
        }
        if (ex is InvalidOperationException) {
            // Show message then,
            throw ex;
        }
        // and so on.
    }
}
如果在Main中使用了try & catch,那么我会使用throw;重新抛出错误。但在上述简化代码中,所有异常都通过HandleException处理。
HandleException内部调用throw ex;与调用throw是否具有相同的效果?

5
有一个区别,与堆栈跟踪出现在异常中的方式有关,但我现在不记得具体是哪一种了,所以我不会把这列为答案。 - Joel Coehoorn
@Joel:谢谢。我猜使用HandleError异常不是一个好主意。我只是想重构一些错误处理代码。 - dance2die
1
第三种方法是将其包装在新的异常中并重新抛出。http://timwise.blogspot.co.uk/2014/05/throw-vs-throw-ex-vs-wrap-and-throw-in.html - Tim Abell
可能是difference between throw and throw new Exception()的重复问题。 - Michael Freidgeim
13个回答

834

是的,它们之间有区别。

  • throw ex 会重置堆栈跟踪(因此您的错误将显示为来自 HandleException

  • throw 不会重置堆栈跟踪 - 原始错误将被保留。

 static void Main(string[] args)
 {
     try
     {
         Method2();
     }
     catch (Exception ex)
     {
         Console.Write(ex.StackTrace.ToString());
         Console.ReadKey();
     }
 }

 private static void Method2()
 {
     try
     {
         Method1();
     }
     catch (Exception ex)
     {
         //throw ex resets the stack trace Coming from Method 1 and propogates it to the caller(Main)
         throw ex;
     }
 }

 private static void Method1()
 {
     try
     {
         throw new Exception("Inside Method1");
     }
     catch (Exception)
     {
         throw;
     }
 }

6
@Marc:看起来,如果抛出异常的方法与最初引发异常的方法不同,throw 只会保留原始异常。请参阅此问题:https://dev59.com/am435IYBdhLWcg3w4Uas - Brann
4
不,你是正确的。在这种情况下,throw;throw ex;抛出相同的对象,但其堆栈跟踪以不同的方式进行修改,就像你在答案中解释的那样。 - Jeppe Stig Nielsen
1
我刚刚读到,异常过滤器的好处之一是你可以避免解开堆栈,因为即使你只是使用 throw; 抛出异常,catch也会解开堆栈。这与你在这里所说的保留堆栈跟踪有什么不同呢?当我几个月前第一次阅读时,我认为那就是你所指的,或者说它们是相同的。 - xr280xr
8
看起来你的链接在博客迁移后没有被正确地转发。看起来它现在在这里(https://scottdorman.blog/2007/08/20/difference-between-throw-and-throw-ex-in-net/)。编辑:嘿,等等,那是_你的_博客!修复你自己的链接吧! ;^D - ruffin
1
使用 throw ex 的用例是什么? - Imad
显示剩余7条评论

116

(我之前发过帖子,@Marc Gravell纠正了我)

以下是差异的演示:

static void Main(string[] args) {
    try {
        ThrowException1(); // line 19
    } catch (Exception x) {
        Console.WriteLine("Exception 1:");
        Console.WriteLine(x.StackTrace);
    }
    try {
        ThrowException2(); // line 25
    } catch (Exception x) {
        Console.WriteLine("Exception 2:");
        Console.WriteLine(x.StackTrace);
    }
}

private static void ThrowException1() {
    try {
        DivByZero(); // line 34
    } catch {
        throw; // line 36
    }
}
private static void ThrowException2() {
    try {
        DivByZero(); // line 41
    } catch (Exception ex) {
        throw ex; // line 43
    }
}

private static void DivByZero() {
    int x = 0;
    int y = 1 / x; // line 49
}

这是输出结果:

Exception 1:
   at UnitTester.Program.DivByZero() in <snip>\Dev\UnitTester\Program.cs:line 49
   at UnitTester.Program.ThrowException1() in <snip>\Dev\UnitTester\Program.cs:line 36
   at UnitTester.Program.TestExceptions() in <snip>\Dev\UnitTester\Program.cs:line 19

Exception 2:
   at UnitTester.Program.ThrowException2() in <snip>\Dev\UnitTester\Program.cs:line 43
   at UnitTester.Program.TestExceptions() in <snip>\Dev\UnitTester\Program.cs:line 25

你可以看到在第一个异常中,堆栈跟踪返回到了DivByZero()方法,而在第二个异常中则没有。

需要注意的是,在ThrowException1()ThrowException2()中显示的行号是throw语句的行号,而不是调用DivByZero()的行号,现在想一想也许这样做有道理......

在发布模式下的输出结果

异常 1:

at ConsoleAppBasics.Program.ThrowException1()
at ConsoleAppBasics.Program.Main(String[] args)

异常 2:

at ConsoleAppBasics.Program.ThrowException2()
at ConsoleAppBasics.Program.Main(String[] args)

仅在调试模式下会保留原始的堆栈跟踪吗?


3
这是因为编译器的优化过程会将DevideByZero这样的短方法内联,所以堆栈跟踪是相同的。也许你应该将此作为一个单独的问题发布。 - Menahem

66

Throw 保留了堆栈跟踪信息。例如,Source1 抛出 Error1,被 Source2 捕获并且 Source2 使用 throw 抛出时,Source1 的 Error 和 Source2 的 Error 将在堆栈跟踪信息中可用。

Throw ex 不会保留堆栈跟踪信息。因此,Source1 的所有错误将被清除,只有 Source2 的错误将发送到客户端。

有时仅靠阅读并不清楚,建议观看这个视频演示以获得更多理解:C#中的 Throw vs Throw ex

Throw vs Throw ex


48
其他答案完全正确,但我认为这个答案提供了一些额外的细节。
考虑以下例子:
using System;

static class Program {
  static void Main() {
    try {
      ThrowTest();
    } catch (Exception e) {
      Console.WriteLine("Your stack trace:");
      Console.WriteLine(e.StackTrace);
      Console.WriteLine();
      if (e.InnerException == null) {
        Console.WriteLine("No inner exception.");
      } else {
        Console.WriteLine("Stack trace of your inner exception:");
        Console.WriteLine(e.InnerException.StackTrace);
      }
    }
  }

  static void ThrowTest() {
    decimal a = 1m;
    decimal b = 0m;
    try {
      Mult(a, b);  // line 34
      Div(a, b);   // line 35
      Mult(b, a);  // line 36
      Div(b, a);   // line 37
    } catch (ArithmeticException arithExc) {
      Console.WriteLine("Handling a {0}.", arithExc.GetType().Name);

      //   uncomment EITHER
      //throw arithExc;
      //   OR
      //throw;
      //   OR
      //throw new Exception("We handled and wrapped your exception", arithExc);
    }
  }

  static void Mult(decimal x, decimal y) {
    decimal.Multiply(x, y);
  }
  static void Div(decimal x, decimal y) {
    decimal.Divide(x, y);
  }
}

如果取消注释throw arithExc;这一行,你的输出结果将会是:

Handling a DivideByZeroException.
Your stack trace:
   at Program.ThrowTest() in c:\somepath\Program.cs:line 44
   at Program.Main() in c:\somepath\Program.cs:line 9

No inner exception.

可以肯定的是,你丢失了有关异常发生位置的信息。如果改为使用throw;代码行,则会得到以下结果:

Handling a DivideByZeroException.
Your stack trace:
   at System.Decimal.FCallDivide(Decimal& d1, Decimal& d2)
   at System.Decimal.Divide(Decimal d1, Decimal d2)
   at Program.Div(Decimal x, Decimal y) in c:\somepath\Program.cs:line 58
   at Program.ThrowTest() in c:\somepath\Program.cs:line 46
   at Program.Main() in c:\somepath\Program.cs:line 9

No inner exception.

这样做好多了,因为现在你可以看到是Program.Div方法导致了问题。但是仍然很难看出这个问题是来自try块中的第35行还是第37行。

如果你使用第三种方式,在外部包装一层异常,你将不会失去任何信息:

Handling a DivideByZeroException.
Your stack trace:
   at Program.ThrowTest() in c:\somepath\Program.cs:line 48
   at Program.Main() in c:\somepath\Program.cs:line 9

Stack trace of your inner exception:
   at System.Decimal.FCallDivide(Decimal& d1, Decimal& d2)
   at System.Decimal.Divide(Decimal d1, Decimal d2)
   at Program.Div(Decimal x, Decimal y) in c:\somepath\Program.cs:line 58
   at Program.ThrowTest() in c:\somepath\Program.cs:line 35

特别是你可以看到问题出现在第35行。然而,这需要人们搜索InnerException,在简单的情况下使用内部异常感觉有些间接。

这篇博客文章中,他们通过调用(利用反射)Exception对象上的internal实例方法InternalPreserveStackTrace()来保留行号(try块的行号)。但是像这样使用反射并不好(.NET Framework可能会在没有警告的情况下更改它们的internal成员)。


11
当你执行throw ex时,被抛出的异常将成为“原始”异常。因此,之前的所有堆栈跟踪都不会存在。
如果你执行throw,异常就沿着线路向下传递,你将获得完整的堆栈跟踪。

9

让我们了解throw和throw ex之间的区别。我听说在许多.net面试中,这个常见问题被问到。

简单来说,throw和throw ex都用于理解异常发生的位置。Throw ex会重写异常的堆栈跟踪,无论实际上抛出了什么异常。

让我们通过一个例子来理解。

先来了解Throw。

static void Main(string[] args) {
    try {
        M1();
    } catch (Exception ex) {
        Console.WriteLine(" -----------------Stack Trace Hierarchy -----------------");
        Console.WriteLine(ex.StackTrace.ToString());
        Console.WriteLine(" ---------------- Method Name / Target Site -------------- ");
        Console.WriteLine(ex.TargetSite.ToString());
    }
    Console.ReadKey();
}

static void M1() {
    try {
        M2();
    } catch (Exception ex) {
        throw;
    };
}

static void M2() {
    throw new DivideByZeroException();
}

上述代码的输出如下所示。
显示完整的层次结构和方法名称,指出异常实际抛出的位置。它是M2 -> M2。同时还包括行号。
其次...让我们通过throw ex来理解。只需在M2方法的catch块中用throw ex替换throw。如下所示。
throw ex代码的输出如下所示。
您可以看到输出中的差异。throw ex仅忽略所有先前的层次结构,并将堆栈跟踪重置为编写throw ex的行/方法。

7

Microsoft Docs的含义是

一旦抛出异常,它所携带的信息中包含了调用堆栈。调用堆栈是从抛出异常的方法开始并以捕获异常的方法结束的方法调用层次结构列表。如果通过在throw语句中指定异常来重新抛出异常,则调用堆栈将从当前方法重新开始,并且原始方法和当前方法之间的方法调用列表将丢失。要保留原始的调用堆栈信息,请使用没有指定异常的throw语句。

来源:https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/ca2200


6

不,这会导致异常具有不同的堆栈跟踪。只有在 catch 处理程序中使用没有任何异常对象的 throw 才会保持堆栈跟踪不变。

您可能希望从 HandleException 返回一个布尔值,指示是否应重新抛出异常。


3
看这里:http://blog-mstechnology.blogspot.de/2010/06/throw-vs-throw-ex.html Throw:
try 
{
    // do some operation that can fail
}
catch (Exception ex)
{
    // do some local cleanup
    throw;
}

它可以将堆栈信息与异常一起保存,

这被称为“重新抛出”(Rethrow)。

如果想要抛出新的异常,则需要:

throw new ApplicationException("operation failed!");

Throw Ex:

try
{
    // do some operation that can fail
}
catch (Exception ex)
{
    // do some local cleanup
    throw ex;
}

它不会发送异常的堆栈信息。

这被称为“破坏堆栈”。

如果想要抛出新的异常,

throw new ApplicationException("operation failed!",ex);

3

使用throw而不是throw ex更好。

throw ex会重置原始堆栈跟踪,并且无法找到先前的堆栈跟踪。

如果我们使用throw,我们将获得完整的堆栈跟踪。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接