.NET - 如何更改异常对象的异常消息?

34

在C#中,我如何更改异常对象的Message

额外闲聊

ExceptionMessage属性是只读的:

public virtual string Message { get; }

进一步阅读

在PHP中,同样的问题被回答为"你不能",但提供了一个解决方法:

但是,您可以确定它的类名和代码,然后抛出一个新的相同类别、相同代码但不同消息的异常。

如何在C#中确定异常的类名,并抛出一个相同类别但具有不同消息的新异常?

例如:

catch (Exception e)
{
   Exception e2 = Activator.CreateInstance(e.GetType());
   throw e2;
}

不起作用是因为异常的Message属性在.NET中是只读的。请参见原问题。


更新

我尝试捕获我预期的每种异常类型:

try
{
    reader.Read();
}
catch (OleDbException e)
{
   throw new OleDbException(e, sql);
}
catch (SqlException e)
{
   throw new SqlException (e, sql);
}
catch (IBM.DbException e)
{
   throw new IBM.DbException(e, sql);
}
catch (OdbcException e)
{
   throw new OdbcException (e, sql);
}
catch (OracleException e)
{
   throw new OracleException (e, sql);
}

现在我的代码强制依赖于不会存在于每个解决方案中的程序集。

而且,现在异常似乎来自我的代码,而不是抛出它的行;我失去了异常的位置信息。

8个回答

40
你可以创建一个新的Exception(或更好的是特定的子类型),并将新的消息作为参数传入(同时将原始异常作为InnerException传递)。
例如:
throw new MyExceptionType("Some interesting message", originalException);

注意:如果你确实想使用Activator.CreateInstance,你可以使用一个可以传递参数的重载方法,但是不能指望不同的派生异常类型都有一个构造函数重载来处理(message, innerException)。


5
抛出新异常的问题在于,新异常被抛出的地方就是新异常“抛出”的位置。如果有一种方法可以用InnerException的堆栈替换我新throw的堆栈,或者有一种方法可以从当前异常来的地方追溯抛出异常的位置,那么它就能够解决问题。我一直在抛出自己的自定义异常,但这只是一个临时的解决办法,我需要一个真正的解决方案(因此提了这个 SO 问题)。 - Ian Boyd
2
InnerException 仍然可以被检索,这样就可以保留原始的堆栈跟踪(以及原始异常详细信息的整个上下文,如果您需要其他内容)。.InnerException 只是提供了底层的 Exception 实例。 - stormont
异常消息往往会误导人,特别是在自动翻译成其他语言时,如PT-BR。最令人困惑的是FormatException,它抛出“输入字符串不符合正确格式”,或者说“输入的字符串不是错误的格式”。 - Eric Wu
1
@EricWu 对于未来,我建议在 .NET 的 GitHub 存储库上创建一个问题:只有报告了这种错误,才会得到修复。 - Richard

16

我在博客文章中找到了解决方案,该文章是从新闻网站链接过来的:

catch (Exception e)
{
   Exception e2 = (Exception)Activator.CreateInstance(e.GetType(), message, e);
   throw e2;
}

它并不完美(会丢失堆栈跟踪),但这是.NET的本质。


12
理论上,该异常类型可能没有一个带有两个参数的构造函数。 - Ohad Schneider
您正在创建一个新的异常,并将一个新的异常和原始异常作为内部异常传递给它。这不会更改原始异常的消息。 - CodingYoshi

8
您可以通过反射来更改异常消息,方法如下...
Exception exception = new Exception("Some message.");
var type = typeof(Exception);
var flags = BindingFlags.Instance | BindingFlags.NonPublic;
var fieldInfo = type.GetField("_message", flags);
fieldInfo.SetValue(exception, message);

所以您可以创建一个扩展方法...
namespace ExceptionExample
{
    public static class ExceptionExtensions
    {
        public static void SetMessage(this Exception exception, string message)
        {
            if (exception == null)
                throw new ArgumentNullException(nameof(exception));

            var type = typeof(Exception);
            var flags = BindingFlags.Instance | BindingFlags.NonPublic;
            var fieldInfo = type.GetField("_message", flags);
            fieldInfo.SetValue(exception, message);
        }
    }
}

然后将其用于...

...
using static ExceptionExample.ExceptionExtensions;

public class SomeClass
{
    public void SomeMethod()
    {
        var reader = AnotherClass.GetReader();
        try
        {
            reader.Read();
        }
        catch (Exception ex)
        {
            var connection = reader?.Connection;
            ex.SetMessage($"The exception message was replaced.\n\nOriginal message: {ex.Message}\n\nDatabase: {connection?.Database}");
            throw; // you will not lose the stack trace
        }
    }
}

请记住,如果使用"throw ex;",则堆栈跟踪将会丢失。

为了避免这种情况,必须使用不带异常的"throw;"


7

我想为Ian的回答提供一些帮助。如果您使用博客文章中介绍的技术,您将不会丢失堆栈信息。是的,在最终异常的StackTrace成员中,您将会丢失堆栈信息,但由于内部异常,您不会丢失整个堆栈信息。请看这里:

class Program
{
    static void Main(string[] args)
    {
        try
        {
            Test1();
        }
        catch (Exception ex)
        {
            Console.WriteLine(ex);
        }
    }

    public static void Test1()
    {
        try
        {
            Test2();
        }
        catch (Exception ex)
        {
            throw ExceptionUtil.Rethrow(ex, "caught in test1");
        }
    }

    public static void Test2()
    {
        throw new Exception("test2");
    }

    public static class ExceptionUtil
    {
        public static Exception Rethrow(Exception ex, string message)
        {
            if (ex == null)
            {
                ex = new Exception("Error rethrowing exception because original exception is <null>.");
            }

            Exception rethrow;

            try
            {
                rethrow = (Exception)Activator.CreateInstance(ex.GetType(), message, ex);
            }
            catch (Exception)
            {
                rethrow = new Exception(message, ex);
            }
            return rethrow;
        }

        public static Exception Rethrow(Exception ex, string message, params object[] args)
        {
            string formatted;

            try
            {
                formatted = String.Format(message, args);
            }
            catch (Exception ex2)
            {
                formatted = message + "\r\n\r\nAn error occurred filling in exception message details:\r\n\r\n" + ex2;
            }
            return Rethrow(ex, formatted);
        }
    }

}

如果您获取异常的完整字符串,将会得到以下结果:
System.Exception: caught in test1 ---> System.Exception: test2
at ScratchPad2.Program.Test2() in C:\Projects\Experiments\ScratchPad2\Program.cs:line 36
at ScratchPad2.Program.Test1() in C:\Projects\Experiments\ScratchPad2\Program.cs:line 26
--- End of inner exception stack trace ---
at ScratchPad2.Program.Test1() in C:\Projects\Experiments\ScratchPad2\Program.cs:line 30
at ScratchPad2.Program.Main(String[] args) in C:\Projects\Experiments\ScratchPad2\Program.cs:line 14

所以您已经获取了整个堆栈,还有额外的信息。

5

您可以将先前的异常与新消息一起包装在一个新的异常中,并利用内部异常来获取堆栈跟踪等信息。

try
{
    throw new Exception("This error message sucks");
}
catch (Exception e)
{
    throw new Exception("There error message is prettier", e);
}

问题在于我的新异常堆栈跟旧异常的堆栈不匹配。 - Ian Boyd
3
应保留内部异常的堆栈跟踪,以便您可以在那里查找它。 - Reddog

2

我知道这是一个非常古老的问题,但我认为现有的答案都不太理想(它们将异常详细信息隐藏在innerExceptions中或掩盖行号)。我使用的方法是使用静态工厂方法而不是构造函数来实例化异常:

public class CustomException {
   public string SampleProperty { get; set; }

    public static CustomException FromAlert(Alert alert)
    {
        var ex = new CustomException(string.Format("{0}-{1}", alert.propertyA, alert.propertyB));
        ex.SampleProperty = "somethign else";
        return ex;
    }
}

现在你可以抛出以下异常: throw CustomException.FromAlert(theAlert); 并根据需要修改该消息。

是的。奇怪的是之前没有人建议使用工厂方法。就这么简单。 - Liam Kernighan

1
在我个人看来,最好的方法是继承Exception(或其他适当的框架提供的异常类),然后将消息字符串放入调用以Message为参数的基础构造函数中,如下所示:
public class InvalidGraphicTypeException : Exception
{

   public InvalidGraphicTypeException(Graphic badType) : 
      base("Invalid Graphic Type: " + badType.GetType().Name)
   {
   }
}

但是,你只能在一行/内联中完成你所能做的事情。如果你需要做一些额外的工作来确定消息应该是什么,那么你就没有运气了。 - Arbiter

1

处理这种情况的最佳方法是编写适合此特定情况的自定义异常类。然后捕获异常并抛出您自己的异常。您可能想在此处查看更多信息:Exception Constructors -> Exception(String) | MS Docs

如果您觉得不需要自定义异常,那么在抛出异常时应该能够将自定义字符串传递给异常构造函数:设计自定义异常 | MS Docs


1
有没有一种方法可以将我的新异常抛出到旧异常的位置,而不是被追踪到我的“throw”位置? - Ian Boyd

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