不鼓励简单地捕获 System.Exception
。相反,应该只捕获“已知”的异常。
现在,这有时会导致不必要的重复代码,例如:
try
{
WebId = new Guid(queryString["web"]);
}
catch (FormatException)
{
WebId = Guid.Empty;
}
catch (OverflowException)
{
WebId = Guid.Empty;
}
我想知道:有没有办法捕获两个异常并且只调用一次 WebId = Guid.Empty
调用?
给定的示例相当简单,因为它只是一个 GUID
。但是想象一下您多次修改对象的代码,如果其中一个操作预期失败,您想要“重置”object
。但是,如果出现意外异常,我仍然想将其抛出更高。
捕捉 System.Exception
并打开类型
catch (Exception ex)
{
if (ex is FormatException || ex is OverflowException)
{
WebId = Guid.Empty;
return;
}
throw;
}
编辑:我同意其他人的观点,从 C# 6.0 开始,异常过滤器现在是一个非常好的方法:catch (Exception ex) when (ex is ... || ex is ... )
除了我仍然有点讨厌单行布局,并且会像下面这样亲自布置代码。我认为这既实用又美观,因为我相信它可以提高理解力。有些人可能不同意:
catch (Exception ex) when (
ex is ...
|| ex is ...
|| ex is ...
)
原来的:
我知道我在这里聚会有点晚了,但是圣烟……
切入正题,这种方法重复了之前的答案,但是如果你真的想对几种异常类型执行一个通用的操作,并在一个方法的范围内保持整个事情的整洁,为什么不只使用一个 lambda /closure/inline 函数执行以下操作?我的意思是,很有可能你最终会意识到你只是想让这个闭包成为一个可以在任何地方使用的单独方法。但是,在不实际更改其余代码结构的情况下,将非常容易做到这一点。正确的?
private void TestMethod ()
{
Action<Exception> errorHandler = ( ex ) => {
// write to a log, whatever...
};
try
{
// try some stuff
}
catch ( FormatException ex ) { errorHandler ( ex ); }
catch ( OverflowException ex ) { errorHandler ( ex ); }
catch ( ArgumentNullException ex ) { errorHandler ( ex ); }
}
我不禁想知道(警告:前面有点讽刺/讽刺)为什么要付出所有这些努力基本上只是替换以下内容:
try
{
// try some stuff
}
catch( FormatException ex ){}
catch( OverflowException ex ){}
catch( ArgumentNullException ex ){}
...下一个代码气味的一些疯狂变化,我的意思是示例,只是为了假装你正在节省一些击键。
// sorta sucks, let's be honest...
try
{
// try some stuff
}
catch( Exception ex )
{
if (ex is FormatException ||
ex is OverflowException ||
ex is ArgumentNullException)
{
// write to a log, whatever...
return;
}
throw;
}
因为它当然不会自动更具可读性。
当然,我在第一个示例中留下了三个相同的 /* write to a log, whatever... */ return;
实例。
但这就是我的观点。你们都听说过函数/方法,对吧?严重地。编写一个通用的 ErrorHandler
函数,然后从每个 catch 块中调用它。
如果您问我,第二个示例(使用 if
和 is
关键字)的可读性明显降低,同时在项目的维护阶段更容易出错。
维护阶段,对于任何可能对编程相对较新的人来说,将占项目整个生命周期的 98.7% 或更多,而进行维护的可怜的笨蛋几乎肯定会是你以外的人。他们很有可能会在工作上花费 50% 的时间来诅咒你的名字。
当然,FxCop 会向您咆哮,因此您还必须在代码中添加一个属性,该属性与正在运行的程序具有精确的 zip,并且只是在那里告诉 FxCop 忽略一个问题,在 99.9% 的情况下它完全是正确的标记。而且,对不起,我可能弄错了,但“忽略”属性最终不会真正编译到您的应用程序中吗?
将整个 if
测试放在一行上会使其更具可读性吗?我不这么认为。我的意思是,很久以前我确实有另一位程序员激烈争辩说,将更多代码放在一行上会使其“运行得更快”。但当然,他是个十足的疯子。试图向他解释(板着脸——这很有挑战性)解释器或编译器如何将那长长的行分成离散的每行一条指令的语句——如果他继续前进的话,结果基本上是相同的只是让代码可读,而不是试图让编译器更聪明——对他没有任何影响。但我离题了。
从现在开始的一两个月后,再添加三种异常类型时,可读性会降低多少? (答案:它变得不那么可读了)。
真正的要点之一是,格式化我们每天都在查看的文本源代码的大部分要点是让其他人非常非常清楚地看到代码运行时实际发生的事情。因为编译器将源代码变成了完全不同的东西,并且不太关心您的代码格式样式。所以全线上也完全烂透了。
只是说...
// super sucks...
catch( Exception ex )
{
if ( ex is FormatException || ex is OverflowException || ex is ArgumentNullException )
{
// write to a log, whatever...
return;
}
throw;
}
正如其他人指出的那样,您可以在 catch 块中使用 if
语句来确定发生了什么。 C#6 支持异常过滤器,因此以下将起作用:
try { … }
catch (Exception e) when (MyFilter(e))
{
…
}
MyFilter
方法可能如下所示:
private bool MyFilter(Exception e)
{
return e is ArgumentNullException || e is FormatException;
}
或者,这可以全部内联完成(when 语句的右侧必须是一个布尔表达式)。
try { … }
catch (Exception e) when (e is ArgumentNullException || e is FormatException)
{
…
}
这与在 catch
块中使用 if
语句不同,使用异常过滤器不会展开堆栈。
您可以下载 Visual Studio 2015 进行检查。
如果要继续使用 Visual Studio 2013,可以安装以下 nuget 包:
安装包 Microsoft.Net.Compilers
At time of writing, this will include support for C# 6.
引用此包将导致使用包中包含的 C# 和 Visual Basic 编译器的特定版本构建项目,而不是任何系统安装的版本。
不幸的是,不在 C# 中,因为您需要一个异常过滤器来执行此操作,而 C# 不会公开 MSIL 的该功能。 VB.NET 确实有这种能力,例如
Catch ex As Exception When TypeOf ex Is FormatException OrElse TypeOf ex Is OverflowException
您可以做的是使用匿名函数来封装您的错误代码,然后在这些特定的 catch 块中调用它:
Action onError = () => WebId = Guid.Empty;
try
{
// something
}
catch (FormatException)
{
onError();
}
catch (OverflowException)
{
onError();
}
异常过滤器现在在 c# 6+ 中可用。你可以做
try
{
WebId = new Guid(queryString["web"]);
}
catch (Exception ex) when(ex is FormatException || ex is OverflowException)
{
WebId = Guid.Empty;
}
在 C# 7.0+ 中,您也可以将其与模式匹配结合使用
try
{
await Task.WaitAll(tasks);
}
catch (Exception ex) when( ex is AggregateException ae &&
ae.InnerExceptions.Count > tasks.Count/2)
{
//More than half of the tasks failed maybe..?
}
为了完整起见,从 .NET 4.0 开始,代码可以重写为:
Guid.TryParse(queryString["web"], out WebId);
TryParse 从不抛出异常并在格式错误时返回 false,将 WebId 设置为 Guid.Empty
。
从 C# 7 开始,您可以避免在单独的行中引入变量:
Guid.TryParse(queryString["web"], out Guid webId);
您还可以创建用于解析返回元组的方法,这些方法在 .NET Framework 4.6 版中尚不可用:
(bool success, Guid result) TryParseGuid(string input) =>
(Guid.TryParse(input, out Guid result), result);
并像这样使用它们:
WebId = TryParseGuid(queryString["web"]).result;
// or
var tuple = TryParseGuid(queryString["web"]);
WebId = tuple.success ? tuple.result : DefaultWebId;
当在 C# 12 中实现 out-parameters 的解构时,对这个无用的答案进行了下一个无用的更新。:)
如果您可以将您的应用程序升级到 C# 6,那么您是幸运的。新的 C# 版本实现了异常过滤器。所以你可以这样写:
catch (Exception ex) when (ex is FormatException || ex is OverflowException) {
WebId = Guid.Empty;
}
有些人认为这段代码与
catch (Exception ex) {
if (ex is FormatException || ex is OverflowException) {
WebId = Guid.Empty;
}
throw;
}
但事实并非如此。实际上,这是 C# 6 中唯一无法在早期版本中模拟的新功能。首先,重新投掷比跳过接球意味着更多的开销。其次,它在语义上不等价。当您调试代码时,新功能会完整地保留堆栈。如果没有此功能,故障转储就不太有用,甚至毫无用处。
查看 discussion about this on CodePlex 不再可用。还有一个example showing the difference。
使用 C# 7 the answer from Michael Stum 可以在保持 switch 语句的可读性的同时得到改进:
catch (Exception ex)
{
switch (ex)
{
case FormatException _:
case OverflowException _:
WebId = Guid.Empty;
break;
default:
throw;
}
}
多亏了 Orace 注释,这可以使用 C# 8 通过省略丢弃变量来简化:
catch (Exception ex)
{
switch (ex)
{
case FormatException:
case OverflowException:
WebId = Guid.Empty;
break;
default:
throw;
}
}
并使用 C# 8 作为 switch 表达式:
catch (Exception ex)
{
WebId = ex switch
{
_ when ex is FormatException || ex is OverflowException => Guid.Empty,
_ => throw ex
};
}
正如 Nechemia Hoffmann 指出的那样。后一个示例将导致堆栈跟踪丢失。这可以通过使用 Jürgen Steinblock 描述的扩展方法在抛出之前捕获堆栈跟踪来防止:
catch (Exception ex)
{
WebId = ex switch
{
_ when ex is FormatException || ex is OverflowException => Guid.Empty,
_ => throw ex.Capture()
};
}
public static Exception Capture(this Exception ex)
{
ExceptionDispatchInfo.Capture(ex).Throw();
return ex;
}
两种样式都可以通过 C# 9 的模式匹配增强来简化:
catch (Exception ex)
{
switch (ex)
{
case FormatException or OverflowException:
WebId = Guid.Empty;
break;
default:
throw;
}
}
catch (Exception ex)
{
WebId = ex switch
{
_ when ex is FormatException or OverflowException => Guid.Empty,
_ => throw ex.Capture()
};
}
throw ex
,您不会丢失堆栈跟踪吗?
_
C# 9 的更新
使用 C# 9 中的 new pattern matching enhancements,您可以缩短异常过滤器中的表达式。现在,捕获多个异常很简单:
try
{
WebId = new Guid(queryString["web"]);
}
catch (Exception e) when (e is FormatException or OverflowException)
{
WebId = Guid.Empty;
}
如果您不想在 catch
范围内使用 if
语句,在 C# 6.0
中,您可以使用已支持的 Exception Filters
语法预览版中的 CLR,但仅存在于 VB.NET
/MSIL
中:
try
{
WebId = new Guid(queryString["web"]);
}
catch (Exception exception) when (exception is FormatException || ex is OverflowException)
{
WebId = Guid.Empty;
}
只有当它是 InvalidDataException
或 ArgumentNullException
时,此代码才会捕获 Exception
。
实际上,您基本上可以在该 when
子句中放置任何条件:
static int a = 8;
...
catch (Exception exception) when (exception is InvalidDataException && a == 8)
{
Console.WriteLine("Catch");
}
请注意,与 catch
范围内的 if
语句相反,Exception Filters
不能抛出 Exceptions
,当它们抛出时,或者当条件不是 true
时,下一个 catch
条件将改为评估:
static int a = 7;
static int b = 0;
...
try
{
throw new InvalidDataException();
}
catch (Exception exception) when (exception is InvalidDataException && a / b == 2)
{
Console.WriteLine("Catch");
}
catch (Exception exception) when (exception is InvalidDataException || exception is ArgumentException)
{
Console.WriteLine("General catch");
}
输出:一般捕获。
如果有多个 true
Exception Filter
- 第一个将被接受:
static int a = 8;
static int b = 4;
...
try
{
throw new InvalidDataException();
}
catch (Exception exception) when (exception is InvalidDataException && a / b == 2)
{
Console.WriteLine("Catch");
}
catch (Exception exception) when (exception is InvalidDataException || exception is ArgumentException)
{
Console.WriteLine("General catch");
}
输出:抓住。
正如您在 MSIL
中看到的那样,代码没有转换为 if
语句,而是转换为 Filters
,并且 Exceptions
不能从标有 Filter 1
和 Filter 2
的区域内抛出,但过滤器抛出 Exception
将失败,而且在 endfilter
命令之前推送到堆栈的最后一个比较值将确定过滤器的成功/失败(Catch 1
XOR strong> Catch 2
将相应地执行):
https://i.stack.imgur.com/uEfE9.jpg
另外,特别是 Guid
有 Guid.TryParse
方法。
catch (Exception ex) when (ex is FormatException or OverflowException)
{
WebId = Guid.Empty;
}
或者
catch (Exception ex)
{
if (ex is not FormatException and not OverflowException)
throw;
WebId = Guid.Empty;
}
接受的答案似乎可以接受,但 CodeAnalysis/FxCop 会抱怨它正在捕获一般异常类型。
此外,似乎“is”运算符可能会稍微降低性能。
CA1800: Do not cast unnecessarily 说“考虑改为测试 'as' 运算符的结果”,但如果这样做,您将编写比单独捕获每个异常更多的代码。
无论如何,这就是我要做的:
bool exThrown = false;
try
{
// Something
}
catch (FormatException) {
exThrown = true;
}
catch (OverflowException) {
exThrown = true;
}
if (exThrown)
{
// Something else
}
在 C# 6 中,推荐的方法是使用异常过滤器,这是一个示例:
try
{
throw new OverflowException();
}
catch(Exception e ) when ((e is DivideByZeroException) || (e is OverflowException))
{
// this will execute iff e is DividedByZeroEx or OverflowEx
Console.WriteLine("E");
}
这是马特的答案的变体(我觉得这有点干净)......使用一种方法:
public void TryCatch(...)
{
try
{
// something
return;
}
catch (FormatException) {}
catch (OverflowException) {}
WebId = Guid.Empty;
}
将引发任何其他异常,并且不会命中代码 WebId = Guid.Empty;
。如果您不希望其他异常使您的程序崩溃,只需在其他两个捕获之后添加:
...
catch (Exception)
{
// something, if anything
return; // only need this if you follow the example I gave and put it all in a method
}
Joseph Daigle's Answer 是一个很好的解决方案,但我发现以下结构更整洁,更不容易出错。
catch(Exception ex)
{
if (!(ex is SomeException || ex is OtherException)) throw;
// Handle exception
}
反转表达式有几个优点:
不需要返回声明
代码没有嵌套
没有忘记约瑟夫解决方案中与表达式分离的“抛出”或“返回”语句的风险。
它甚至可以压缩成一行(虽然不是很漂亮)
catch(Exception ex) { if (!(ex is SomeException || ex is OtherException)) throw;
// Handle exception
}
编辑: C# 6.0 中的 exception filtering 将使语法更简洁,并且在任何当前解决方案上都带有 number of other benefits。 (最值得注意的是让堆栈安然无恙)
以下是使用 C# 6.0 语法的相同问题的外观:
catch(Exception ex) when (ex is SomeException || ex is OtherException)
{
// Handle exception
}
@迈克尔
您的代码的略微修改版本:
catch (Exception ex)
{
Type exType = ex.GetType();
if (exType == typeof(System.FormatException) ||
exType == typeof(System.OverflowException)
{
WebId = Guid.Empty;
} else {
throw;
}
}
字符串比较丑陋且缓慢。
怎么样
try
{
WebId = Guid.Empty;
WebId = new Guid(queryString["web"]);
}
catch (FormatException)
{
}
catch (OverflowException)
{
}
谨慎和警告:另一种实用风格。
链接中的内容不能直接回答您的问题,但将其扩展为如下所示很简单:
static void Main()
{
Action body = () => { ...your code... };
body.Catch<InvalidOperationException>()
.Catch<BadCodeException>()
.Catch<AnotherException>(ex => { ...handler... })();
}
(基本上提供另一个返回自身的空 Catch
重载)
更大的问题是为什么。我认为这里的成本不会超过收益:)
2015 年 12 月 15 日更新:请参阅 https://stackoverflow.com/a/22864936/1718702 了解 C#6。它是一种更清洁的语言,现在是语言的标准。
针对希望 more elegant solution 捕获一次并过滤异常的人,我使用如下所示的扩展方法。
我的库中已经有了这个扩展,最初是为其他目的而编写的,但它非常适合 type
检查异常。另外,恕我直言,它看起来比一堆 ||
语句更干净。此外,与公认的答案不同,我更喜欢显式异常处理,因此 ex is ...
具有不良行为,因为派生类可分配给那里的父类型)。
用法
if (ex.GetType().IsAnyOf(
typeof(FormatException),
typeof(ArgumentException)))
{
// Handle
}
else
throw;
IsAnyOf.cs 扩展(请参阅依赖项的完整错误处理示例)
namespace Common.FluentValidation
{
public static partial class Validate
{
/// <summary>
/// Validates the passed in parameter matches at least one of the passed in comparisons.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="p_parameter">Parameter to validate.</param>
/// <param name="p_comparisons">Values to compare against.</param>
/// <returns>True if a match is found.</returns>
/// <exception cref="ArgumentNullException"></exception>
public static bool IsAnyOf<T>(this T p_parameter, params T[] p_comparisons)
{
// Validate
p_parameter
.CannotBeNull("p_parameter");
p_comparisons
.CannotBeNullOrEmpty("p_comparisons");
// Test for any match
foreach (var item in p_comparisons)
if (p_parameter.Equals(item))
return true;
// Return no matches found
return false;
}
}
}
完整的错误处理示例(复制粘贴到新的控制台应用程序)
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Common.FluentValidation;
namespace IsAnyOfExceptionHandlerSample
{
class Program
{
static void Main(string[] args)
{
// High Level Error Handler (Log and Crash App)
try
{
Foo();
}
catch (OutOfMemoryException ex)
{
Console.WriteLine("FATAL ERROR! System Crashing. " + ex.Message);
Console.ReadKey();
}
}
static void Foo()
{
// Init
List<Action<string>> TestActions = new List<Action<string>>()
{
(key) => { throw new FormatException(); },
(key) => { throw new ArgumentException(); },
(key) => { throw new KeyNotFoundException();},
(key) => { throw new OutOfMemoryException(); },
};
// Run
foreach (var FooAction in TestActions)
{
// Mid-Level Error Handler (Appends Data for Log)
try
{
// Init
var SomeKeyPassedToFoo = "FooParam";
// Low-Level Handler (Handle/Log and Keep going)
try
{
FooAction(SomeKeyPassedToFoo);
}
catch (Exception ex)
{
if (ex.GetType().IsAnyOf(
typeof(FormatException),
typeof(ArgumentException)))
{
// Handle
Console.WriteLine("ex was {0}", ex.GetType().Name);
Console.ReadKey();
}
else
{
// Add some Debug info
ex.Data.Add("SomeKeyPassedToFoo", SomeKeyPassedToFoo.ToString());
throw;
}
}
}
catch (KeyNotFoundException ex)
{
// Handle differently
Console.WriteLine(ex.Message);
int Count = 0;
if (!Validate.IsAnyNull(ex, ex.Data, ex.Data.Keys))
foreach (var Key in ex.Data.Keys)
Console.WriteLine(
"[{0}][\"{1}\" = {2}]",
Count, Key, ex.Data[Key]);
Console.ReadKey();
}
}
}
}
}
namespace Common.FluentValidation
{
public static partial class Validate
{
/// <summary>
/// Validates the passed in parameter matches at least one of the passed in comparisons.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="p_parameter">Parameter to validate.</param>
/// <param name="p_comparisons">Values to compare against.</param>
/// <returns>True if a match is found.</returns>
/// <exception cref="ArgumentNullException"></exception>
public static bool IsAnyOf<T>(this T p_parameter, params T[] p_comparisons)
{
// Validate
p_parameter
.CannotBeNull("p_parameter");
p_comparisons
.CannotBeNullOrEmpty("p_comparisons");
// Test for any match
foreach (var item in p_comparisons)
if (p_parameter.Equals(item))
return true;
// Return no matches found
return false;
}
/// <summary>
/// Validates if any passed in parameter is equal to null.
/// </summary>
/// <param name="p_parameters">Parameters to test for Null.</param>
/// <returns>True if one or more parameters are null.</returns>
public static bool IsAnyNull(params object[] p_parameters)
{
p_parameters
.CannotBeNullOrEmpty("p_parameters");
foreach (var item in p_parameters)
if (item == null)
return true;
return false;
}
}
}
namespace Common.FluentValidation
{
public static partial class Validate
{
/// <summary>
/// Validates the passed in parameter is not null, throwing a detailed exception message if the test fails.
/// </summary>
/// <param name="p_parameter">Parameter to validate.</param>
/// <param name="p_name">Name of tested parameter to assist with debugging.</param>
/// <exception cref="ArgumentNullException"></exception>
public static void CannotBeNull(this object p_parameter, string p_name)
{
if (p_parameter == null)
throw
new
ArgumentNullException(
string.Format("Parameter \"{0}\" cannot be null.",
p_name), default(Exception));
}
}
}
namespace Common.FluentValidation
{
public static partial class Validate
{
/// <summary>
/// Validates the passed in parameter is not null or an empty collection, throwing a detailed exception message if the test fails.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="p_parameter">Parameter to validate.</param>
/// <param name="p_name">Name of tested parameter to assist with debugging.</param>
/// <exception cref="ArgumentNullException"></exception>
/// <exception cref="ArgumentOutOfRangeException"></exception>
public static void CannotBeNullOrEmpty<T>(this ICollection<T> p_parameter, string p_name)
{
if (p_parameter == null)
throw new ArgumentNullException("Collection cannot be null.\r\nParameter_Name: " + p_name, default(Exception));
if (p_parameter.Count <= 0)
throw new ArgumentOutOfRangeException("Collection cannot be empty.\r\nParameter_Name: " + p_name, default(Exception));
}
/// <summary>
/// Validates the passed in parameter is not null or empty, throwing a detailed exception message if the test fails.
/// </summary>
/// <param name="p_parameter">Parameter to validate.</param>
/// <param name="p_name">Name of tested parameter to assist with debugging.</param>
/// <exception cref="ArgumentException"></exception>
public static void CannotBeNullOrEmpty(this string p_parameter, string p_name)
{
if (string.IsNullOrEmpty(p_parameter))
throw new ArgumentException("String cannot be null or empty.\r\nParameter_Name: " + p_name, default(Exception));
}
}
}
两个示例 NUnit 单元测试
Exception
类型的匹配行为是精确的(即,子类型不匹配其任何父类型)。
using System;
using System.Collections.Generic;
using Common.FluentValidation;
using NUnit.Framework;
namespace UnitTests.Common.Fluent_Validations
{
[TestFixture]
public class IsAnyOf_Tests
{
[Test, ExpectedException(typeof(ArgumentNullException))]
public void IsAnyOf_ArgumentNullException_ShouldNotMatch_ArgumentException_Test()
{
Action TestMethod = () => { throw new ArgumentNullException(); };
try
{
TestMethod();
}
catch (Exception ex)
{
if (ex.GetType().IsAnyOf(
typeof(ArgumentException), /*Note: ArgumentNullException derrived from ArgumentException*/
typeof(FormatException),
typeof(KeyNotFoundException)))
{
// Handle expected Exceptions
return;
}
//else throw original
throw;
}
}
[Test, ExpectedException(typeof(OutOfMemoryException))]
public void IsAnyOf_OutOfMemoryException_ShouldMatch_OutOfMemoryException_Test()
{
Action TestMethod = () => { throw new OutOfMemoryException(); };
try
{
TestMethod();
}
catch (Exception ex)
{
if (ex.GetType().IsAnyOf(
typeof(OutOfMemoryException),
typeof(StackOverflowException)))
throw;
/*else... Handle other exception types, typically by logging to file*/
}
}
}
}
因为我觉得这些答案只是表面上,所以我试图更深入地挖掘。
所以我们真正想做的是不能编译的东西,比如:
// Won't compile... damn
public static void Main()
{
try
{
throw new ArgumentOutOfRangeException();
}
catch (ArgumentOutOfRangeException)
catch (IndexOutOfRangeException)
{
// ... handle
}
我们想要这个的原因是因为我们不希望异常处理程序捕获我们稍后在流程中需要的东西。当然,我们可以捕获一个异常并用“如果”检查要做什么,但老实说,我们并不是真的想要那样。 (FxCop,调试器问题,丑陋)
那么为什么这段代码不能编译——我们怎样才能破解它呢?
如果我们查看代码,我们真正想做的是转发呼叫。但是,根据 MS Partition II,IL 异常处理程序块不会像这样工作,这在这种情况下是有道理的,因为这意味着“异常”对象可以有不同的类型。
或者用代码编写它,我们要求编译器做这样的事情(虽然它并不完全正确,但我猜这是最接近的可能):
// Won't compile... damn
try
{
throw new ArgumentOutOfRangeException();
}
catch (ArgumentOutOfRangeException e) {
goto theOtherHandler;
}
catch (IndexOutOfRangeException e) {
theOtherHandler:
Console.WriteLine("Handle!");
}
这不会编译的原因很明显:'$exception' 对象将具有什么类型和值(这里存储在变量'e' 中)?我们希望编译器处理这个问题的方式是注意两个异常的公共基类型是“异常”,将其用于包含两个异常的变量,然后只处理捕获的两个异常。这在 IL 中实现的方式是“过滤器”,它在 VB.Net 中可用。
为了让它在 C# 中工作,我们需要一个具有正确“异常”基本类型的临时变量。为了控制代码的流动,我们可以添加一些分支。开始:
Exception ex;
try
{
throw new ArgumentException(); // for demo purposes; won't be caught.
goto noCatch;
}
catch (ArgumentOutOfRangeException e) {
ex = e;
}
catch (IndexOutOfRangeException e) {
ex = e;
}
Console.WriteLine("Handle the exception 'ex' here :-)");
// throw ex ?
noCatch:
Console.WriteLine("We're done with the exception handling.");
这样做的明显缺点是我们无法正确重新投掷,而且 - 老实说 - 这是一个非常丑陋的解决方案。可以通过执行分支消除来稍微修复丑陋,这使解决方案稍微好一些:
Exception ex = null;
try
{
throw new ArgumentException();
}
catch (ArgumentOutOfRangeException e)
{
ex = e;
}
catch (IndexOutOfRangeException e)
{
ex = e;
}
if (ex != null)
{
Console.WriteLine("Handle the exception here :-)");
}
剩下的只是“重新投掷”。为此,我们需要能够在 'catch' 块内执行处理 - 使这项工作的唯一方法是通过捕获 'Exception' 对象。
此时,我们可以添加一个单独的函数,使用重载决议来处理不同类型的异常,或者来处理异常。两者都有缺点。首先,这是使用辅助函数的方法:
private static bool Handle(Exception e)
{
Console.WriteLine("Handle the exception here :-)");
return true; // false will re-throw;
}
public static void Main()
{
try
{
throw new OutOfMemoryException();
}
catch (ArgumentException e)
{
if (!Handle(e)) { throw; }
}
catch (IndexOutOfRangeException e)
{
if (!Handle(e)) { throw; }
}
Console.WriteLine("We're done with the exception handling.");
另一个解决方案是捕获异常对象并相应地处理它。根据上面的上下文,最直接的翻译是这样的:
try
{
throw new ArgumentException();
}
catch (Exception e)
{
Exception ex = (Exception)(e as ArgumentException) ?? (e as IndexOutOfRangeException);
if (ex != null)
{
Console.WriteLine("Handle the exception here :-)");
// throw ?
}
else
{
throw;
}
}
所以得出结论:
如果我们不想重新抛出,我们可能会考虑捕获正确的异常,并将它们存储在临时文件中。
如果处理程序很简单,并且我们想重用代码,最好的解决方案可能是引入辅助函数。
如果我们想重新抛出,我们别无选择,只能将代码放在“异常”捕获处理程序中,这将破坏 FxCop 和调试器的未捕获异常。
这是每个 C# 开发人员最终都会面临的经典问题。
让我把你的问题分成2个问题。首先,
我可以一次捕获多个异常吗?
简而言之,没有。
这就引出了下一个问题,
鉴于我无法在同一个 catch() 块中捕获多个异常类型,如何避免编写重复代码?
鉴于您的特定样本,回退值构建起来很便宜,我喜欢按照以下步骤操作:
将 WebId 初始化为备用值。在临时变量中构造一个新的 Guid。将 WebId 设置为完全构造的临时变量。将此作为 try{} 块的最终语句。
所以代码看起来像:
try
{
WebId = Guid.Empty;
Guid newGuid = new Guid(queryString["web"]);
// More initialization code goes here like
// newGuid.x = y;
WebId = newGuid;
}
catch (FormatException) {}
catch (OverflowException) {}
如果抛出任何异常,则 WebId 永远不会设置为半构造值,并保持为 Guid.Empty。
如果构建后备值很昂贵,而重置值要便宜得多,那么我会将重置代码移动到它自己的函数中:
try
{
WebId = new Guid(queryString["web"]);
// More initialization code goes here.
}
catch (FormatException) {
Reset(WebId);
}
catch (OverflowException) {
Reset(WebId);
}
请注意,我确实找到了一种方法,但这看起来更像 The Daily WTF 的材料:
catch (Exception ex)
{
switch (ex.GetType().Name)
{
case "System.FormatException":
case "System.OverflowException":
WebId = Guid.Empty;
break;
default:
throw;
}
}
所以你在每个异常开关中都重复了很多代码?听起来像提取方法将是上帝的想法,不是吗?
所以你的代码归结为:
MyClass instance;
try { instance = ... }
catch(Exception1 e) { Reset(instance); }
catch(Exception2 e) { Reset(instance); }
catch(Exception) { throw; }
void Reset(MyClass instance) { /* reset the state of the instance */ }
我想知道为什么没有人注意到代码重复。
从 C#6 开始,您还拥有其他人已经提到的 exception-filters。因此,您可以将上面的代码修改为:
try { ... }
catch(Exception e) when(e is Exception1 || e is Exception2)
{
Reset(instance);
}
想在这个已经很长的线程中添加我的简短回答。没有提到的是 catch 语句的优先顺序,更具体地说,您需要了解您尝试捕获的每种异常类型的范围。
例如,如果您使用“catch-all”异常作为 Exception 它将在所有其他 catch 语句之前,并且您显然会得到编译器错误但是如果您颠倒顺序,您可以链接您的 catch 语句(我认为有点反模式) 您可以将 catch-all Exception 类型放在底部,这将捕获任何不适合您的 try..catch 块中更高级别的异常:
try
{
// do some work here
}
catch (WebException ex)
{
// catch a web excpetion
}
catch (ArgumentException ex)
{
// do some stuff
}
catch (Exception ex)
{
// you should really surface your errors but this is for example only
throw new Exception("An error occurred: " + ex.Message);
}
我强烈建议人们查看此 MSDN 文档:
也许尝试保持您的代码简单,例如将公共代码放在一个方法中,就像您在代码的任何其他部分中所做的那样,而不是在 catch 子句中?
例如:
try
{
// ...
}
catch (FormatException)
{
DoSomething();
}
catch (OverflowException)
{
DoSomething();
}
// ...
private void DoSomething()
{
// ...
}
我会怎么做,试图找到简单而美丽的模式
这里值得一提。您可以响应多种组合(异常错误和异常消息)。
我在尝试在数据网格中转换控件对象时遇到了一个用例场景,内容为 TextBox、TextBlock 或 CheckBox。在这种情况下,返回的异常是相同的,但消息不同。
try
{
//do something
}
catch (Exception ex) when (ex.Message.Equals("the_error_message1_here"))
{
//do whatever you like
}
catch (Exception ex) when (ex.Message.Equals("the_error_message2_here"))
{
//do whatever you like
}
我想建议最短的答案(另一种功能风格):
Catch<FormatException, OverflowException>(() =>
{
WebId = new Guid(queryString["web"]);
},
exception =>
{
WebId = Guid.Empty;
});
为此,您需要创建几个“Catch”方法重载,类似于 System.Action:
[DebuggerNonUserCode]
public static void Catch<TException1, TException2>(Action tryBlock,
Action<Exception> catchBlock)
{
CatchMany(tryBlock, catchBlock, typeof(TException1), typeof(TException2));
}
[DebuggerNonUserCode]
public static void Catch<TException1, TException2, TException3>(Action tryBlock,
Action<Exception> catchBlock)
{
CatchMany(tryBlock, catchBlock, typeof(TException1), typeof(TException2), typeof(TException3));
}
依此类推。但是你需要做一次,你可以在你的所有项目中使用它(或者,如果你创建了一个 nuget 包,我们也可以使用它)。
和 CatchMany 实现:
[DebuggerNonUserCode]
public static void CatchMany(Action tryBlock, Action<Exception> catchBlock,
params Type[] exceptionTypes)
{
try
{
tryBlock();
}
catch (Exception exception)
{
if (exceptionTypes.Contains(exception.GetType())) catchBlock(exception);
else throw;
}
}
ps 为了代码简单,我没有进行空检查,考虑添加参数验证。
ps2 如果要从 catch 中返回一个值,则需要执行相同的 Catch 方法,但在参数中使用 return 和 Func 而不是 Action。
try
{
WebId = new Guid(queryString["web"]);
}
catch (Exception ex)
{
string ExpTyp = ex.GetType().Name;
if (ExpTyp == "FormatException")
{
WebId = Guid.Empty;
}
else if (ExpTyp == "OverflowException")
{
WebId = Guid.Empty;
}
}
在 c# 6.0 中,Exception Filters 是对异常处理的改进
try
{
DoSomeHttpRequest();
}
catch (System.Web.HttpException e)
{
switch (e.GetHttpCode())
{
case 400:
WriteLine("Bad Request");
case 500:
WriteLine("Internal Server Error");
default:
WriteLine("Generic Error");
}
}
catch (HttpException e) when e.GetHttpCode() == 400 { WriteLine("Bad Request"; }