本文共 12286 字,大约阅读时间需要 40 分钟。
目录
异常处理并非易事。虽然try- catch 块看起来很简单,但是对异常所做的并不是。这篇文章的动力是基于我想要的,除了一个AWS lambda函数中的例外,但是这里没有任何特定于AWS lambda的东西。高级开发人员也应该意识到这里确实没有任何新东西,我所做的就是将一些概念打包成一篇文章并大量使用静态类,扩展方法,泛型参数,显式参数和条件延续操作符。另外,我不想讨论在线程或任务中捕获异常的更复杂的问题。
我将异常分为两类:
在任何一种情况下,某些地方都必须捕获异常。我们来看一个简单的异常测试例子:
using System;namespace ITakeException{ class Program { static void Main(string[] args) { try { SayHi("Hi"); } catch (Exception ex) { Console.WriteLine(ex); } } } static void SayHi(string msg) { throw new Exception("oops", new Exception("My Inner Self-Exception")); }}
上面的实现非常简单——它只是将异常写入控制台:
典型的日志记录(到文件,云记录应用程序等)看起来并没有多好。这实际上既不是人也不是机器可读的。当然,你可以编写一个解析它的程序,但实际上,这很糟糕。如果你提供了一个更加机器可读的异常形式,那么你的解析可以专注于识别问题,而不是解析什么样的人可读消息,但实际上不是吗?
和StackFrame类
输入System.Diagonistics中的StackTrace和StackFrameData类:
catch (Exception ex){ var st = new StackTrace(ex, true); foreach (var frame in st.GetFrames()) { var sfd = new StackFrameData(frame); Console.WriteLine(sfd.ToString() + "\r\n"); }}
类
在这里,我使用一个帮助类,StackFrameData,从StackFrame中提取我想要报告的内容:
public class StackFrameData{ public string FileName { get; private set; } public string Method { get; private set; } public int LineNumber { get; private set; } public StackFrameData(StackFrame sf) { FileName = sf.GetFileName(); Method = sf.GetMethod().Name; LineNumber = sf.GetFileLineNumber(); } public override string ToString() { return $"{FileName}\r\n{Method}\r\n{LineNumber}"; }}
现在,我们得到了一些内容:
类
使用另一个帮助程序,异常很容易可序列化为JSON,以获取机器可读的内容。如下:
catch (Exception ex){ var report = new ExceptionReport(ex); string json = JsonConvert.SerializeObject(report); Console.WriteLine(json);}
和ExceptionReport辅助类:
public static class ExceptionReportExtensionMethods{ public static ExceptionReport CreateReport(this Exception ex) { return new ExceptionReport(ex); }}public class ExceptionReport{ public DateTime When { get; } = DateTime.Now; [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] public string ApplicationMessage { get; set; } public string ExceptionMessage { get; set; } public ListCallStack { get; set; } = new List (); [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] public ExceptionReport InnerException { get; set; } public ExceptionReport(Exception ex) { ExceptionMessage = ex.Message; var st = new StackTrace(ex, true); var frames = st.GetFrames() ?? new StackFrame[0]; CallStack.AddRange(frames.Select(frame => new StackFrameData(frame))); InnerException = ex.InnerException?.CreateReport(); }}
我们有一个很好的JSON对象:
请注意,我们最终还报告了内部异常!
虽然这里可能会有一些踢腿和尖叫,但我会更进一步。我并不总是想编码:
try{ Stuff();}catch(Exception ex){ var report = new ExceptionReport(ex); string json = JsonConvert.SerializeObject(report); // Log somewhere}
而且我也不想依赖其他开发人员这样做。当然,我可以这样做:
try{ Stuff();}catch(Exception ex){ Logger.Log(ex);}
但我仍然有这样的try- catch声明“污染” 。那么(这里是踢和尖叫的部分)写这个呢:
Catcher.Try(() => SayHi("Hi"));
在一下内容的帮助下:
public static class Catcher{ public static T Try(Func fnc, Action final = null) { try { T ret = fnc(); return ret; } catch (Exception ex) { Log(ex); throw; } finally { final?.Invoke(); } } public static void Try(Action fnc, Action final = null) { try { fnc(); } catch (Exception ex) { Log(ex); throw; } finally { final?.Invoke(); } } private static void Log(Exception ex) { var report = new ExceptionReport(ex); string json = JsonConvert.SerializeObject(report, Formatting.Indented); Console.WriteLine(json); }}
现在我们得到:
这有两个问题:
第一个问题是这样解决的,在Catcher类中的异常中,我们告诉reporter跳过最后一个堆栈项:
Log(ex, 1);
然后reporter被这样调用:
private static void Log(Exception ex, int exceptLastN){ var report = new ExceptionReport(ex, exceptLastN); string json = JsonConvert.SerializeObject(report, Formatting.Indented); Console.WriteLine(json);}
和:
public static class ExceptionReportExtensionMethods{ ... public static T[] Drop(this T[] items, int n) { return items.Take(items.Length - 1).ToArray(); }}
在reporter中:
var frames = st.GetFrames()?.Drop(exceptLastN) ?? new StackFrame[0];
现在我们看到:
至于第二个问题,我们可以实现一个不会重新抛出异常的SilentTry static方法,例如:
public static void Try(Action fnc, Action final = null){ try { fnc(); } catch (Exception ex) { Log(ex, 1); } finally { final?.Invoke(); }}
扩展测试案例:
static void Main(string[] args){ Catcher.SilentTry(() => SayHi("Hi"), () => Console.WriteLine("Bye")); var ret = Catcher.SilentTry(() => Divide(0, 1), () => Console.WriteLine("NAN!")); Console.WriteLine(ret);}static void SayHi(string msg){ throw new Exception("oops", new Exception("My Inner Self-Exception"));}static decimal Divide(decimal a, decimal b){ return a / b;}
(你知道只有decimal和integer types会抛出被零除的异常吗? Double返回“无穷大”!)
所以现在,我们得到两个日志条目,最后执行:
请注意,因为这些是静默尝试,所以调用者不会抛出任何异常。嗯....
或垃圾处理(我听到更尖叫!)。因为Func<T>; 调用是最复杂的:
public static bool SilentTry(Func fnc, out T ret, Action final = null, T defaultValue = default(T), Action onException = null){ bool ok = false; ret = defaultValue; try { ret = fnc(); ok = true; } catch (Exception ex) { Log(ex, 1); onException?.Invoke(); } finally { final?.Invoke(); } return ok;}
我们的测试案例:
bool ok = (Catcher.SilentTry( () => Divide(5, 2), out decimal ret, defaultValue: decimal.MaxValue, onException: () => Console.WriteLine("Divide by zero!!!"), final: () => Console.WriteLine("The answer is not 42.")));Console.WriteLine($"Success? {ok} Result = {ret}");
没有异常:
并通过调用() => Divide(5, 0)除以零 ,
好吧,这非常疯狂,但它说明了你可以用可选参数做什么。
它变得更加疯狂:
第一个问题是通过在几个扩展方法的帮助下使用Catcher “轻松”处理:
public static void Try(this Action action){ Catcher.Try(action);}public static void SilentTry(this Action action){ Catcher.SilentTry(action);}
并将finally作为有条件的Try或在final操作上的SilentTry:
public static bool SilentTry(Func fnc, out T ret, Action final = null, T defaultValue = default(T), Action onException = null){ bool ok = false; ret = defaultValue; try { ret = fnc(); ok = true; } catch (Exception ex) { Log(ex, 1); SilentTry(() => onException?.Invoke()); } finally { final?.SilentTry(); } return ok;}
第二个问题的解决方法是在日志记录周围进行一次try- catch,然后尝试执行其他操作:
private static void Log(Exception ex, int exceptLastN){ try { var report = new ExceptionReport(ex, exceptLastN); string json = JsonConvert.SerializeObject(report, Formatting.Indented); Console.WriteLine(json); } catch (Exception loggerException) { // Log failure handler }}
第三个问题可以通过使用您特别想要捕获的类型的泛型参数来解决。再次,使用Func SilentTry实现:
public static bool SilentTry(Func fnc, out T ret, Action final = null, T defaultValue = default(T), Action onException = null) where E: Exception{ bool ok = false; ret = defaultValue; try { ret = fnc(); ok = true; } catch (Exception ex) { Log(ex, 1); onException?.Invoke(); if (ex.GetType().Name != typeof(E).Name) { throw; } } finally { final?.SilentTry(); } return ok;}
现在注意(注意显式参数用法的可读性):
bool ok = (Catcher.SilentTry( () => Divide(5, 0), out decimal ret, defaultValue: decimal.MaxValue, onException: () => Console.WriteLine("Divide by zero!!!"), final: () => Console.WriteLine("The answer is not 42.")));
向调用者抛出异常:
然而,如果我们得到我们想要的异常:
bool ok = (Catcher.SilentTry( () => Divide(5, 0), out decimal ret, defaultValue: decimal.MaxValue, onException: () => Console.WriteLine("Divide by zero!!!"), final: () => Console.WriteLine("The answer is not 42.")));
那不是:
上面代码的唯一问题是现在我们必须明确指定我们静默处理的异常类型和返回参数类型。或者您可能喜欢版本:
public static bool SilentTry(Action action, Action final = null, Func onOtherException = null, Func onSpecifiedException = null) where E : Exception{ bool ok = false; try { action(); ok = true; } catch (Exception ex) { Log(ex, 1); if (ex.GetType().Name == typeof(E).Name) { SilentTry(() => ok = onSpecifiedException?.Invoke() ?? false); } else { SilentTry(() => ok = onOtherException?.Invoke() ?? false); } } finally { final?.SilentTry(); } return ok;}
所以这允许你做这样的事情:
if (!Catcher.SilentTry(() => () => GetDataFromDatabase(), onSpecifiedException: () => GetDataFromLocalCache())){ HandleTotalFailureToGetData();}
疯了吧?我怀疑大多数人更喜欢相同的东西:
try{ GetDataFromDatabase();}catch(SqlException){ try { GetDataFromLocalCache(); } catch { HandleTotalFailureToGetData(); }}catch{ HandleTotalFailureToGetData();}
呸。我没有,主要是因为代码的意图很难用catch块读取。
这里有一个快速说明。几乎没有理由使用throw ex;,因为这会重置异常堆栈:
如下:
Catcher.Try(ThrowSomething);...static void ThrowSomething(){ throw new Exception("Foobar");}
throw 给你:
请注意,异常堆栈显示调用ThrowSomething。
如果我改变捕手使用throw ex;我们得到:
请注意,异常堆栈已重置,我们现在正在查看从Catcher.Try方法开始的堆栈。我们丢弃了抛出异常的ThrowSomething方法!
当您因处理另一个异常而抛出异常时,内部异常非常有用。使用上面的例子,我们有一个从本地缓存中获取数据的回退,我们可能会这样写:
try{ GetDataFromDatabase();}catch(SqlException ex){ if (!cacheExists) { throw new Exception("Cache doesn't exist", ex); }}
在这里,内部异常告诉我们最初引发的异常是从基础获取数据,并且因为我们没有本地缓存,所以我们抛出异常,因为回退也失败了。我们可以稍微改变一下Try方法,允许onException Action指定一个回退:
public static void Try(Action fnc, Action final = null, Action onException = null){ try { fnc(); } catch (Exception ex) { Log(ex, 1); try { onException?.Invoke(); } catch (Exception ex2) { Log(ex2, 1); var newException = new Exception(ex2.Message, ex); Log(newException, 1); throw newException; } finally { final?.Try(); } }}
看起来这个finally块只有在异常回退失败时才有意义。如果我们试试这个:
Catcher.Try(GetDataFromDatabase, onException: GetDataFromCache);
随着回退失败:
static void GetDataFromDatabase(){ throw new Exception("EX: GetDataFromDatabase");}static void GetDataFromCache(){ throw new Exception("EX: GetDataFromCache");}
我们看到具有内部异常的日志:
请注意,虽然我们没有得到外部异常的调用堆栈,文件名和方法,因为我们创建了一个新Exception对象,只传入了回退的异常消息。这真的很烦人,在我看来是Exception 类上的一个缺点。如屏幕截图所示,我们确实获得了内部和外部异常的相关日志,这些日志可以合并。首先,Try方法:
public static void Try(Action fnc, Action final = null, Action onException = null){ try { fnc(); } catch (Exception ex) { try { onException?.Invoke(); } catch (Exception ex2) { Log(ex2, ex, 1); var newException = new Exception(ex2.Message, ex); throw newException; } finally { final?.Try(); } }}
然后是一个结合了两个异常的Log方法:
private static void Log(Exception outer, Exception inner, int exceptLastN){ try { var outerReport = new ExceptionReport(outer, exceptLastN); var innerReport = new ExceptionReport(inner, exceptLastN); outerReport.InnerException = innerReport; string json = JsonConvert.SerializeObject(outerReport, Formatting.Indented); Console.WriteLine(json); } catch (Exception loggerException) { // Log failure handler }}
请注意,只有在exception回退失败时才会创建日志:
这似乎更有用,并且是一个很好的演示,演示了如何使用一些智能包装try- catch,实际上可以改善从日志中获得的结果。
当您将构建更改为“Release”时,Visual Studio仍会生成PDB文件:
这是用于在Exception类中提供行号和源文件名的文件,因此在通过StackTrace类生成堆栈跟踪时使用该文件。如果您不想在发行版中使用PDB文件,则必须更改“高级生成设置”。首先,从Build - > Configuration Manager对话框中将项目设置为发布模式:
然后右键单击项目并选择Build部分,然后单击“Advanced ”并为调试信息输出选择“None ”:
遗憾的是,使用IDE时,必须为解决方案中的每个项目执行此操作。在用于将应用程序移动到生产环境的任何进程中省略PDB文件更简单,或者,如果您在命令行上使用msbuild,则将其添加到发布设置配置文件中:
false None
或者,从命令行指定:
/p:DebugSymbols=false /p:DebugType=None
无论你如何操作,你现在都会注意到异常是以原始形式存在的,因此代码在这里生成的格式化JSON不再具有文件名或行号:
我们可以重构StackFrameDate类来删除null和0:
public class StackFrameData{ [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] public string FileName { get; private set; } public string Method { get; private set; } [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] public int? LineNumber { get; private set; } public StackFrameData(StackFrame sf) { FileName = sf.GetFileName(); Method = sf.GetMethod().Name; int ln = sf.GetFileLineNumber(); LineNumber = ln == 0 ? new int?() : ln; } public override string ToString() { return $"{FileName}\r\n{Method}\r\n{LineNumber}"; }}
结果:
对于处理异常的语法来说,这是一个非常复杂的过程(只是我喜欢的事情),基本上是语法上的糖衣(尽管您可能不喜欢)。虽然本文的重点最初更侧重于创建机器可读的日志报告上,但我希望已经为创建一致的异常处理方法埋下了种子。您可能不喜欢语法糖衣,但希望您能从本文中获得一些适用于您的解决方案的想法。
原文地址:
转载地址:http://uxzhj.baihongyu.com/