我目前正在开发一个使用 C# 和 WPF 构建的大型 Windows 桌面应用程序。我们一直面临一个持续存在的问题,在某些版本中,我们错过了在某些地方添加异常处理程序,导致未处理的异常,从而导致应用程序崩溃。这在我们团队内部引发了一些争论。一些同事指责 QA 团队没有发现这些案例,而另一些同事则认为这是开发人员的疏忽。就我个人而言,我认为这两种观点都有优点,但我们需要一个更强大的软件解决方案来防止轻微异常导致应用程序崩溃。
为了解决这个问题,我主张实施一个与遥测系统(如 Sentry)相结合的全局异常处理程序。目标是捕获未处理的异常,记录它们以进行监控,并防止应用程序崩溃。这将帮助我们跟踪问题并随着时间的推移提高稳定性。
然而,也有一些团队成员的反对。他们的担忧是:
全局捕获异常可能会导致应用程序无法正常运行(例如,对话框不显示或按钮不起作用)。 如果我们全局捕获异常并允许应用程序继续运行,应用程序状态可能会变得不一致。 我很想听听您对此事的经验和见解。您发现哪些方法可以有效处理未处理的异常?如何平衡大型桌面应用程序中的错误处理和应用程序稳定性?
感谢您的意见!
其中大部分建议对于 WPF 来说并不独特,但我采用了相当简单的
Result
模式,并结合了基于 ICommand
的异常处理方案。
结果模式
首先,我有一个
Result
和 Result<T>
类,只要方法可能失败,就必须始终返回它们。简化版:
public class Result
{
public Result(
string? error = null,
string? warning = null,
Exception? ex = null)
{
if (!string.IsNullOrEmpty(error) || ex != null)
{
this.Error = error;
this.InnerException = ex;
LogError?.Invoke(this, this);
IsSuccess = false;
}
else
{
this.IsSuccess = true;
}
}
public void Throw()
{
if (this.InnerException != null)
throw this.InnerException;
else if (!string.IsNullOrEmpty(this.Error))
throw new Exception(this.Error);
}
public bool IsSuccess { get; }
public string? Error { get; }
public string? Warning { get; }
public Exception? InnerException { get; }
// For global logging
public static event EventHandler<Result>? LogError;
public static event EventHandler<Result>? LogWarning;
// For the (hopefully) 99% case
public static readonly Result Success = new Result();
// Allows you to just return the Exception directly
public static implicit operator Result(Exception ex)
{
return new Result(ex: ex);
}
}
public class Result<T> : Result
{
public Result(T value, string? warning = null) : base(null, warning, null)
{
this.Value = value;
}
public Result(string? error = null, Exception? ex = null)
:base(error, null, ex)
{
}
T? _Value;
public T? Value
{
get
{
if (!this.IsSuccess)
// This prevents us from accidentally using
// the value if we neglected to check for
// failure.
this.Throw();
return _Value;
}
private set
{
_Value = value;
}
}
// These simplify usage (returning values, etc.)
public static implicit operator T? (Result<T> result)
{
return result.Value;
}
public static implicit operator Result<T>(T value)
{
return new Result<T>(value);
}
public static implicit operator Result<T>(Exception ex)
{
return new Result<T>(ex: ex);
}
}
实际效果如下:
public static Result<double> Divide(double a, double b)
{
if (b == 0)
return new Result<double>("Division by zero");
try
{
// can just return value thanks to implicit cast
return a / b;
}
catch (Exception ex)
{
// ditto
return ex;
}
}
public static void CheckResult()
{
var r = Divide(1, 2);
if (!r.IsSuccess)
r.Throw();
var r2 = Divide(2, 0);
if (r2.Value > 1)
{
// Oops, forgot to check for failure! Plain old
// exception will then be thrown.
}
}
或者更实际的例子:
public static Result<Stream> SafeFileOpen(string path)
{
try
{
return File.OpenRead(path);
}
catch (Exception ex)
{
return ex;
}
}
在我看来,.NET 异常过于疯狂。 corelib 和第三方库都会因各种不可预测和不一致的原因抛出异常,通常是因为不相关或容易恢复的事情。这就是为什么我在代码中可能失败的每个单独位中使用结果模式,并将对我未在异常处理程序中编写的代码的每个调用包装起来,该异常处理程序返回
Result
包装的异常。然后我的代码可以决定要做什么。
基于命令的错误处理
如果您使用 MVVM 模式,则此方法的第二个方面主要相关,因此如果没有,请随时在此停止。我实现了自己的
ICommand
类,将每个 Action
包装在异常处理程序中,同时还使用 Result
模式:
public class Command<T> : ICommand, INotifyPropertyChanged
{
private Func<T?, Result> _action;
public event EventHandler? CanExecuteChanged;
public event PropertyChangedEventHandler? PropertyChanged;
public Command(Func<T?, Result> action)
{
this._action = action;
}
// You can bind this to a UI element that will display
// the error. A subclassed Button with a textblock beneath
// it would work great for this.
string? _Error;
public string? Error
{
get
{
return _Error;
}
set
{
if (_Error == value)
return;
_Error = value;
PropertyChanged?.Invoke(
this,
new PropertyChangedEventArgs(nameof(Error)));
}
}
public void Execute(object? parameter)
{
Result r;
try
{
r = this._action(parameter is T tparam ? tparam : default(T));
}
catch (Exception ex)
{
r = new Result(ex: ex);
}
if (!r.IsSuccess)
{
this.Error = r.Error ?? r.InnerException?.Message;
}
}
public bool CanExecute(object? parameter)
{
return true;
}
}
public class Command : Command<object>
{
public Command(Func<Result> action) : base(discardParameter => action())
{
}
}
这做了两件事:(1) 强制您的命令处理程序通过
Result
返回其成功/失败状态,(2) 确保命令处理程序中未捕获的异常在执行时被捕获并进行相应处理。使用示例:
private Command? _saveFormCommand;
public ICommand SaveFormCommand
{
get
{
return _saveFormCommand ?? (_saveFormCommand = new Command(
() =>
{
Result isFormValid = ValidateForm();
if (!isFormValid.IsSuccess)
return isFormValid;
// This will never throw
Result<Stream> str = FileOps.SafeFileOpen("formdata.data");
if (str.IsSuccess)
return str;
try
{
// This might throw
using (str.Value)
{
str.Value.Write(...);
return Result.Success;
}
}
catch (Exception ex)
{
return ex;
}
}));
}
}
最终,大规模应用程序的技巧是采用这样的实践,“强制”您采用一种模式,最大限度地减少未捕获的异常或其他错误情况,并考虑如何处理它们,以及提供统一的日志记录方式和/或显示错误,这样您在编写时就不必考虑这一点。本质上,您可以制定自己的代码安全框架,并强迫自己和您的同事遵循它。