【问题标题】:Throw same exception instance multiple times多次抛出同一个异常实例
【发布时间】:2018-09-07 03:07:29
【问题描述】:

面对产生一个异常实例然后可能多次抛出它的代码。

private readonly Exception exceptionInstance = new Exception("message");

多次抛出同一个异常实例是否正确?

【问题讨论】:

  • 出于好奇,您为什么只想使用特定实例?
  • 您的意思是代码中的throw exceptionInstance?一个例外通常只是一个数据对象。你可以一次又一次地使用同一个实例,但我看不出有任何理由这样做,因为它总是包含完全相同的消息,因此不会有非常通用的用途。
  • 单身?但为什么呢?
  • @Crowcoder,我不知道作者为什么要这样做。我的问题是关于此代码可能存在的问题。
  • 你当然应该重构这段代码。例如,我很确定这不是线程安全的,所以当你从多个线程抛出这个异常时——谁知道会发生什么。

标签: c# exception-handling


【解决方案1】:

由于各种已经说明的原因,这是一种不好的做法,但在多线程代码中它会特别失败,因为Exception 类(显然)不是线程安全的,而且它不是不可变的。

考虑这段代码:

class Program {
    static readonly Exception _test = new Exception("test");

    static void Main(string[] args) {
        ThreadPool.SetMinThreads(10, 8);
        var random = new Random();
        int num1 = 0;
        int num2 = 0;
        var tasks = new List<Task>();
        for (int i = 0; i < 10; i++) {
            tasks.Add(Task.Run(() => {
                try {
                    if (random.Next(0, 2) == 0) {
                        Interlocked.Increment(ref num1);
                        Throw1();
                    }
                    else {
                        Interlocked.Increment(ref num2);
                        Throw2();
                    }
                }
                catch (Exception ex) {
                    Console.WriteLine(ex);
                }
            }));
        }

        Task.WaitAll(tasks.ToArray());
        Console.WriteLine("num1: " + num1);
        Console.WriteLine("num2: " + num2);
        Console.ReadKey();
    }

    static void Throw1() {
        throw _test;
    }

    static void Throw2() {
        throw _test;
    }
}

这里我们有两个方法,Throw1()Throw2(),它们都从私有字段中抛出相同的异常实例。然后我们运行 10 个线程,它们随机调用 Throw1()Throw2() 并打印抛出的内容。此类代码的示例输出为:

System.Exception: test
   в ConsoleApp8.Program.Throw1() в H:\VSProjects\SoHelp\ConsoleApp8\Program.cs:строка 52
   в ConsoleApp8.Program.<>c__DisplayClass1_0.<Main>b__0() в H:\VSProjects\SoHelp\ConsoleApp8\Program.cs:строка 32
System.Exception: test
   в ConsoleApp8.Program.Throw1() в H:\VSProjects\SoHelp\ConsoleApp8\Program.cs:строка 52
   в ConsoleApp8.Program.<>c__DisplayClass1_0.<Main>b__0() в H:\VSProjects\SoHelp\ConsoleApp8\Program.cs:строка 32
System.Exception: test
   в ConsoleApp8.Program.Throw1() в H:\VSProjects\SoHelp\ConsoleApp8\Program.cs:строка 52
   в ConsoleApp8.Program.<>c__DisplayClass1_0.<Main>b__0() в H:\VSProjects\SoHelp\ConsoleApp8\Program.cs:строка 32
System.Exception: test
   в ConsoleApp8.Program.Throw1() в H:\VSProjects\SoHelp\ConsoleApp8\Program.cs:строка 52
   в ConsoleApp8.Program.<>c__DisplayClass1_0.<Main>b__0() в H:\VSProjects\SoHelp\ConsoleApp8\Program.cs:строка 32
System.Exception: test
   в ConsoleApp8.Program.Throw1() в H:\VSProjects\SoHelp\ConsoleApp8\Program.cs:строка 52
   в ConsoleApp8.Program.<>c__DisplayClass1_0.<Main>b__0() в H:\VSProjects\SoHelp\ConsoleApp8\Program.cs:строка 32
System.Exception: test
   в ConsoleApp8.Program.Throw1() в H:\VSProjects\SoHelp\ConsoleApp8\Program.cs:строка 52
   в ConsoleApp8.Program.<>c__DisplayClass1_0.<Main>b__0() в H:\VSProjects\SoHelp\ConsoleApp8\Program.cs:строка 32
System.Exception: test
   в ConsoleApp8.Program.Throw1() в H:\VSProjects\SoHelp\ConsoleApp8\Program.cs:строка 52
   в ConsoleApp8.Program.<>c__DisplayClass1_0.<Main>b__0() в H:\VSProjects\SoHelp\ConsoleApp8\Program.cs:строка 32
System.Exception: test
   в ConsoleApp8.Program.Throw1() в H:\VSProjects\SoHelp\ConsoleApp8\Program.cs:строка 52
   в ConsoleApp8.Program.<>c__DisplayClass1_0.<Main>b__0() в H:\VSProjects\SoHelp\ConsoleApp8\Program.cs:строка 32
System.Exception: test
   в ConsoleApp8.Program.Throw1() в H:\VSProjects\SoHelp\ConsoleApp8\Program.cs:строка 52
   в ConsoleApp8.Program.<>c__DisplayClass1_0.<Main>b__0() в H:\VSProjects\SoHelp\ConsoleApp8\Program.cs:строка 32
System.Exception: test
   в ConsoleApp8.Program.Throw1() в H:\VSProjects\SoHelp\ConsoleApp8\Program.cs:строка 52
   в ConsoleApp8.Program.<>c__DisplayClass1_0.<Main>b__0() в H:\VSProjects\SoHelp\ConsoleApp8\Program.cs:строка 32
num1: 6
num2: 4

因此,虽然 Throw1() 已被调用 6 次,Throw2() 已被调用 4 次 - 我们打印的所有 10 个堆栈跟踪都引用了 Throw1() 方法。

所以永远不要那样做,因为绝对没有理由这样做。

【讨论】:

  • 只读字段是不可变的。静态变量保证在被调用之前被初始化。只是碰巧初始化静态的线程获胜。之后状态永远不会改变。
  • @P.Brian.Mackey 我不同意,因为如果您尝试从我的示例中依次调用Throw1()Throw2()(来自同一个线程)-catch 将打印 2不同的堆栈跟踪,即使异常是相同的。而且异常有很多可变字段,一个例子是Message 本身,但还有更多。
  • @P.Brian.Mackey 请注意,问题不是关于重新抛出捕获的异常 - 当然这很好。问题是关于将一​​个异常实例存储在一个字段中,然后从多个完全不相关的地方抛出它,而不仅仅是“抛出新异常(“测试”)”。这个不好。
  • @P.Brian.Mackey 字段初始化已经在静态构造函数中。静态字段初始化器的行为方式相同。无论如何,问题不在于Exception 实例是如何创建的。它只在一个线程上创建一次。问题是它稍后会从多个线程进行修改。例如,当您执行Console.WriteLine(ex) 时,这与Console.WriteLine(ex.ToString()) 相同。 ToString()Exception 调用各种方法,这些方法在异常时修改私有字段。这不是线程安全的。
  • @P.Brian.Mackey 例如,ToString() 调用 GetClassName 私有方法,它大致执行以下操作:if (_className == null) _className = GetClassNameIntl(); return _className。这样,对ToString() 的无辜调用会修改Exception 的私有字段。在这种情况下,我们是从多个线程执行此操作的,这会导致观察到的行为。这一切都是因为Exception 不是线程安全的,而且不应该是,因为你不应该这样使用它。
【解决方案2】:

没有。不建议在代码中的多个位置抛出相同的 Exception 实例(不包括重新抛出!)。

异常包含的不仅仅是它的消息 - 它包含用于调试的有价值的信息,例如堆栈跟踪。 (更新:刚刚测试过。堆栈跟踪可能已添加到 throw 语句中的异常中,因此与此答案无关)和 TargetSite(从我的测试来看,似乎正在填充第一次抛出异常,但此后不再抛出)。

Exception 的相同实例放在代码的不同位置会导致您无法使用其中的某些数据。

【讨论】:

  • 堆栈跟踪实际上会有所不同。如果您从不同的方法抛出相同的异常 - 您将在 catch 块中看到堆栈跟踪(以及 TargetSite 等属性)每次都会更改。并不是说我认为这是一种不好的做法......
  • @Evk 是的,现在刚刚测试过。堆栈跟踪可能已添加到 throw 语句中的异常
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2012-09-12
  • 2021-02-04
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-02-17
  • 2014-12-26
相关资源
最近更新 更多