简体   繁体   English

无异常的错误处理

[英]Error Handling without Exceptions

While searching SO for approaches to error handling related to business rule validation, all I encounter are examples of structured exception handling.在搜索与业务规则验证相关的错误处理方法时,我遇到的只是结构化异常处理的示例。

MSDN and many other reputable development resources are very clear that exceptions are not to be used to handle routine error cases. MSDN 和许多其他有声望的开发资源都非常清楚,异常不能用于处理常规错误情况。 They are only to be used for exceptional circumstances and unexpected errors that may occur from improper use by the programmer (but not the user.) In many cases, user errors such as fields that are left blank are common, and things which our program should expect, and therefore are not exceptional and not candidates for use of exceptions.它们仅用于异常情况和程序员(但不是用户)不当使用可能导致的意外错误。在许多情况下,用户错误(例如留空的字段)很常见,而我们的程序应该期望,因此不是异常,也不是使用异常的候选者。

QUOTE:引用:

Remember that the use of the term exception in programming has to do with the thinking that an exception should represent an exceptional condition.请记住,在编程中使用术语异常与异常应该代表异常情况的想法有关。 Exceptional conditions, by their very nature, do not normally occur;就其本质而言,通常不会发生异常情况; so your code should not throw exceptions as part of its everyday operations.所以你的代码不应该在日常操作中抛出异常。

Do not throw exceptions to signal commonly occurring events.不要抛出异常来表示经常发生的事件。 Consider using alternate methods to communicate to a caller the occurrence of those events and leave the exception throwing for when something truly out of the ordinary happens.考虑使用替代方法将这些事件的发生传达给调用者,并在真正不寻常的事情发生时抛出异常。

For example, proper use:例如,正确使用:

private void DoSomething(string requiredParameter)
{
if (requiredParameter == null) throw new ArgumentExpcetion("requiredParameter cannot be null");
// Remainder of method body...
}

Improper use:使用不当:

// Renames item to a name supplied by the user.  Name must begin with an "F".
public void RenameItem(string newName)
{
   // Items must have names that begin with "F"
   if (!newName.StartsWith("F")) throw new RenameException("New name must begin with /"F/"");
   // Remainder of method body...
}

In the above case, according to best practices, it would have been better to pass the error up to the UI without involving/requiring .NET's exception handling mechanisms.在上述情况下,根据最佳实践,最好将错误传递给 UI,而不涉及/需要 .NET 的异常处理机制。

Using the same example above, suppose one were to need to enforce a set of naming rules against items.使用上面的相同示例,假设需要对项目执行一组命名规则。 What approach would be best?什么方法最好?

  1. Having the method return a enumerated result?让方法返回枚举结果? RenameResult.Success, RenameResult.TooShort, RenameResult.TooLong, RenameResult.InvalidCharacters, etc. RenameResult.Success、RenameResult.TooShort、RenameResult.TooLong、RenameResult.InvalidCharacters 等。

  2. Using an event in a controller class to report to the UI class?使用控制器类中的事件向 UI 类报告? The UI calls the controller's RenameItem method, and then handles an AfterRename event that the controller raises and that has rename status as part of the event args? UI 调用控制器的 RenameItem 方法,然后处理控制器引发的 AfterRename 事件,该事件具有作为事件参数的一部分的重命名状态?

  3. The controlling class directly references and calls a method from the UI class that handles the error, eg ReportError(string text).控制类直接引用和调用处理错误的 UI 类的方法,例如 ReportError(string text)。

  4. Something else... ?还有什么……?

Essentially, I want to know how to perform complex validation in classes that may not be the Form class itself, and pass the errors back to the Form class for display -- but I do not want to involve exception handling where it should not be used (even though it seems much easier!)本质上,我想知道如何在可能不是 Form 类本身的类中执行复杂的验证,并将错误传递回 Form 类进行显示——但我不想在不应该使用的地方涉及异常处理(尽管看起来要容易得多!)


Based on responses to the question, I feel that I'll have to state the problem in terms that are more concrete:根据对问题的回答,我觉得我必须用更具体的术语来说明问题:

UI = User Interface, BLL = Business Logic Layer (in this case, just a different class) UI = 用户界面,BLL = 业务逻辑层(在这种情况下,只是一个不同的类)

  1. User enters value within UI.用户在 UI 中输入值。
  2. UI reports value to BLL. UI 向 BLL 报告值。
  3. BLL performs routine validation of the value. BLL 执行值的例行验证。
  4. BLL discovers rule violation. BLL 发现违反规则。
  5. BLL returns rule violation to UI. BLL 将违反规则的情况返回给 UI。
  6. UI recieves return from BLL and reports error to user. UI 收到 BLL 的返回并向用户报告错误。

Since it is routine for a user to enter invalid values, exceptions should not be used.由于用户输入无效值是例行公事,因此不应使用异常。 What is the right way to do this without exceptions?没有例外的正确方法是什么?

I assume that you are creating your own business rules validation engine, since you haven't mentioned the one you're using.我假设您正在创建自己的业务规则验证引擎,因为您没有提到您正在使用的那个。

I would use exceptions, but I would not throw them.我会使用异常,但我不会抛出它们。 You will obviously need to be accumulating the state of the evaluation somewhere - to record the fact that a particular rule failed, I would store an Exception instance describing the failure.您显然需要在某处累积评估的状态 - 为了记录特定规则失败的事实,我将存储一个描述失败的 Exception 实例。 This is because:这是因为:

  1. Exceptions are serializable异常是可序列化的
  2. Exceptions always have a Message property that is human-readable, and can have additional properties to record details of the exception in machine-readable form.异常始终具有人类可读的Message属性,并且可以具有其他属性来以机器可读的形式记录异常的详细信息。
  3. Some of the business rules failures may in fact have been signaled by exceptions - a FormatException , for instance.某些业务规则失败实际上可能已由异常发出信号——例如FormatException You could catch that exception and add it to the list.您可以捕获该异常并将其添加到列表中。

In fact, this month's MSDN Magazine has an article that mentions the new AggregateException class in .NET 4.0, which is meant to be a collection of exceptions that occurred in a particular context.事实上,本月的 MSDN 杂志有一篇文章提到了 .NET 4.0 中的新AggregateException类,它是在特定上下文中发生的异常的集合。


Since you're using Windows Forms, you should use the built-in mechanisms for validation: the Validating event and the ErrorProvider component.由于您使用的是 Windows 窗体,因此您应该使用内置的验证机制: Validating事件和ErrorProvider组件。

I think you've gotten the wrong impression of the intended message.我认为您对预期的信息有错误的印象。 Here's a great quote I ran across yesterday from the current edition of Visual Studio magazine (Vol 19, No 8).这是我昨天从当前版本的 Visual Studio 杂志(第 19 卷,第 8 期)中看到的一个很好的引述。

Either a member fulfills its contract or it throws an excetion.成员要么履行其合同,要么引发异常。 Period.时期。 No middle ground.没有中间立场。 No return codes, no sometimes it works, sometimes it doesn't.没有返回码,不,有时有效,有时无效。

Exceptions should be used with care as they are expensive to create and throw--but they are, however, the .NET framework's way of notifying a client (by that I mean any calling component) of an error.应该小心使用异常,因为它们的创建和抛出成本很高——但它们是 .NET 框架通知客户端(我指的是任何调用组件)错误的方式。

The example you give is of UI validating inputs.您给出的示例是 UI 验证输入。

Therefore, a good approach is to separate the validation from the action.因此,一个好的方法是将验证与操作分开。 WinForms has a built in validation system, but in principle, it works along these lines: WinForms 有一个内置的验证系统,但原则上,它的工作方式如下:

ValidationResult v = ValidateName(string newName);
if (v == ValidationResult.NameOk)
    SetName(newName);
else
    ReportErrorAndAskUserToRetry(...);

In addition, you can apply the validation in the SetName method to ensure that the validity has been checked:此外,您可以在 SetName 方法中应用验证以确保已检查有效性:

public void SetName(string newName)
{
    if (ValidateName(newName) != ValidationResult.NameOk)
        throw new InvalidOperationException("name has not been correctly validated");

    name = newName;
}

(Note that this may not be the best approach for performance, but in the situation of applying a simple validation check to a UI input, it is unlikely that validating twice will be of any significance. Alternatively, the above check could be done purely as a debug-only assert check to catch any attempt by programmers to call the method without first validating the input. Once you know that all callers are abiding by their contract, there is often no need for a release runtime check at all) (请注意,这可能不是性能的最佳方法,但在对 UI 输入应用简单验证检查的情况下,验证两次不太可能有任何意义。或者,上述检查可以纯粹作为仅调试断言检查,以捕获程序员在未首先验证输入的情况下调用方法的任何尝试。一旦您知道所有调用者都遵守他们的合同,通常根本不需要发布运行时检查)

To quote another answer:引用另一个答案:

Either a member fulfills its contract or it throws an exception. Period.

The thing that this misses out is: What is the contract?遗漏的是:合同是什么? It is perfectly reasonable to state in the "contract" that a method returns a status value.在“合同”中声明方法返回状态值是完全合理的。 eg File.Exists() returns a status code, not an exception, because that is its contract.例如 File.Exists() 返回一个状态码,而不是异常,因为那是它的合约。

However, your example is different.但是,您的示例有所不同。 In it, you actually do two separate actions: validation and storing.在其中,您实际上执行了两个单独的操作:验证和存储。 If SetName can either return a status code or set the name, it is trying to do two tasks in one, which means that the caller never knows which behaviour it will exhibit, and has to have special case handling for those cases.如果 SetName 可以返回状态码设置名称,则它试图将两个任务合二为一,这意味着调用者永远不知道它将表现出哪种行为,并且必须对这些情况进行特殊情况处理。 However, if you split SetName into separate Validate and Store steps, then the contract for StoreName can be that you pass in valid inputs (as passed by ValidateName), and it throws an exception if this contract is not met.但是,如果您将 SetName 拆分为单独的 Validate 和 Store 步骤,则 StoreName 的约定可以是您传入有效输入(由 ValidateName 传递),如果不满足此约定,它将引发异常。 Because each method then does one thing and one thing only, the contract is very clear, and it is obvious when an exception should be thrown.因为每个方法然后只做一件事和一件事,契约非常明确,什么时候应该抛出异常也很明显。

I agree part of Henk's suggestion.我同意 Henk 的部分建议。

Traditionally, "pass/fail" operations were implemented as functions with an integer or bool return type that would specifiy the result of the call.传统上,“通过/失败”操作被实现为具有整数或布尔返回类型的函数,该类型将指定调用的结果。 However, some are opposed to this, stating that "A function or method should either perform an action or return a value, but not both."然而,有些人反对这一点,指出“一个函数或方法应该要么执行一个动作,要么返回一个值,但不能两者兼而有之。” In otherwords, a class memeber that returns a value should not also be a class memeber that changes the state of the object.换句话说,返回值的类成员不应该也是改变对象状态的类成员。

I've found the best solution to add a .HasErrors / .IsValid and an .Errors property within the class that generates the errors.我找到了在生成错误的类中添加.HasErrors / .IsValid.Errors属性的最佳解决方案。 The first two properties allow the client class to test wether or not errors exist, and if need be, can also read the .Errors property and report one or all of the errors contained.前两个属性允许客户端类测试是否存在错误,如果需要,还可以读取 .Errors 属性并报告包含的一个或所有错误。 Each method then must be aware of these properties and manage the error state appropriately.然后,每个方法都必须了解这些属性并适当地管理错误状态。 These properties can then be rolled into an IErrorReporting interface that various business rule layer facade classes can incorporate.然后可以将这些属性滚动到各种业务规则层外观类可以合并的 IErrorReporting 接口中。

In my opinion, if in doubt, throw exceptions on business rule validation.在我看来,如果有疑问,请在业务规则验证上抛出异常。 I know this is somewhat counter-intuitive and I may get flamed for this, but I stand by it because what is routine and what is not depends on the situation, and is a business decision, not a programming one.我知道这有点违反直觉,我可能会为此感到愤怒,但我坚持这一点,因为什么是例行公事,什么不是取决于情况,这是一个商业决策,而不是编程决策。

For example, if you have business domain classes that are used by a WPF app and a WCF service, invalid input of a field may be routine in a WPF app, but it would be disastrous when the domain objects are used in a WCF situation where you are handling service requests from another application.例如,如果您有 WPF 应用程序和 WCF 服务使用的业务域类,则字段的无效输入可能是 WPF 应用程序中的例行程序,但是当在 WCF 情况下使用域对象时,这将是灾难性的您正在处理来自另一个应用程序的服务请求。

I thought long and hard, and came up with this solution.我想了很久,想出了这个解决方案。 Do the following to the domain classes:对域类执行以下操作:

  • Add a property: ThrowsOnBusinessRule.添加一个属性:ThrowsOnBusinessRule。 Default should be true to throw exceptions.默认应该是 true 以抛出异常。 Set it to false if you don't want to throw it.如果您不想抛出它,请将其设置为 false。
  • Add a private Dictionary collection to store exceptions with key as domain property that has business rule violation.添加私有 Dictionary 集合以存储异常,并将键作为具有业务规则违规的域属性。 (Of course, you can expose this publicly if you want) (当然,如果你愿意,你可以公开这个)
  • Add a method: ThrowsBusinessRule(string propertyName, Exception e) to handle tho logic above添加一个方法:ThrowsBusinessRule(string propertyName, Exception e) 来处理上面的逻辑
  • If you want, you can implement IDataErrorInfo and use the Dictionary collection.如果需要,您可以实现 IDataErrorInfo 并使用 Dictionary 集合。 Implementation of IDataErrorInfo is trivial given the setup above.鉴于上述设置,IDataErrorInfo 的实现是微不足道的。

I think I'm close to being convinced that throwing exceptions is actually the best course of action for validation type operations, and especially aggregate type operations.我想我几乎确信抛出异常实际上是验证类型操作的最佳做​​法,尤其是聚合类型操作。

Take as an example, updating a record in a database.以更新数据库中的记录为例。 This is an aggregate of many operations that may individually fail.这是许多可能单独失败的操作的集合。

Eg.例如。

  1. Check there is a record in the DB to update.检查数据库中是否有要更新的记录。 -- record may not exists -- 记录可能不存在
  2. Validate the fields to be updated.验证要更新的字段。 -- the new values may not be valid -- 新值可能无效
  3. Update the database record.更新数据库记录。 -- the db may raise additional errors -- db 可能会引发额外的错误

If we are tying to avoid exceptions, we may instead want to use an object that can hold a success or error state:如果我们想避免异常,我们可能想要使用一个可以保持成功或错误状态的对象:

public class Result<T> {
  public T Value { get; }
  public string Error { get; }

  public Result(T value) => Value = value;
  public Result(string error) => Error = error;

  public bool HasError() => Error != null;
  public bool Ok() => !HasError();
}

Now we can use this object in some of our helper methods:现在我们可以在我们的一些辅助方法中使用这个对象:

public Result<Record> FindRecord(int id) {
  var record = Records.Find(id);
  if (record == null) return new Result<Record>("Record not found");
  return new Result<Record>(record);
}

public Results<bool> RecordIsValid(Record record) {
  var validator = new Validator<Record>(record);
  if (validator.IsValid()) return new Result<bool>(true);
  return new Result<bool>(validator.ErrorMessages);
}

public Result<bool> UpdateRecord(Record record) {
  try {
    Records.Update(record);
    Records.Save();
    return new Result<bool>(true);
  }
  catch (DbUpdateException e) {
    new Result<bool>(e.Message);
  }
}

Now for our aggregate method that ties it all together:现在我们的聚合方法将它们联系在一起:

public Result<bool> UpdateRecord(int id, Record record) {
  if (id != record.ID) return new Result<bool>("ID of record cannot be modified");

  var dbRecordResults = FindRecord(id);
  if (dbRecordResults.HasError())
    return new Result<bool>(dbRecordResults.Error);

  var validationResults = RecordIsValid(record);
  if (validationResults.HasError())
    return validationResults;

  var updateResult = UpdateRecord(record);
  return updateResult;
}

Wow!哇! what a mess!真是一团糟!

We can take this one step further.我们可以更进一步。 We could create subclasses of Result<T> to indicate specific error types:我们可以创建Result<T>的子类来指示特定的错误类型:

public class ValidationError : Result<bool> {
  public ValidationError(string validationError) : base(validationError) {}
}

public class RecordNotFound: Result<Record> {
  public RecordNotFound(int id) : base($"Record not found: ID = {id}") {}
}

public class DbUpdateError : Result<bool> {
  public DbUpdateError(DbUpdateException e) : base(e.Message) {}
}

Then we can test for specific error cases:然后我们可以测试特定的错误情况:

var result = UpdateRecord(id, record);
if (result is RecordNotFound) return NotFound();
if (result is ValidationError) return UnprocessableEntity(result.Error);
if (result.HasError()) return UnprocessableEntity(result.Error);
return Ok(result.Value);

However, in the above example, result is RecordNotFound will always return false as it is a Result<Record> , whereas UpdateRecord(id, record) return Result<bool> .但是,在上面的示例中, result is RecordNotFound将始终返回 false,因为它是Result<Record> ,而UpdateRecord(id, record)返回Result<bool>

Some positives: * It bascially does works * It avoids exceptions * It returns nice messages when things fail * The Result<T> class can be as complex as you need it to be.一些积极的方面: * 它基本上确实有效 * 它避免了异常 * 当事情失败时它会返回很好的消息 * Result<T>类可以像你需要的那样复杂。 Eg, Perhaps it could handle an array of error messages in the case of validation error messages.例如,也许它可以在验证错误消息的情况下处理一系列错误消息。 * Subclasses of Result<T> could be used to indicate common errors * Result<T>的子类可用于指示常见错误

The negatives: * There are conversion issues where T may be different.缺点: * 存在T可能不同的转换问题。 eg.例如。 Result<T> and Result<Record> * The methods are now doing multiple things, error handling, and the thing they are supposed to be doing * Its extremely verbose * Aggregate methods, such as UpdateRecord(int, Record) now need to concern them selves with the results of the methods they call. Result<T>Result<Record> * 这些方法现在正在做多种事情,错误处理以及它们应该做的事情 * 它非常冗长 * 现在需要关注聚合方法,例如UpdateRecord(int, Record)他们自己与他们调用的方法的结果。

Now using exceptions...现在使用异常...

public class ValidationException : Exception {
  public ValidationException(string message) : base(message) {}
}

public class RecordNotFoundException : Exception  {
  public RecordNotFoundException (int id) : base($"Record not found: ID = {id}") {}
}

public class IdMisMatchException : Exception {
  public IdMisMatchException(string message) : base(message) {}
}

public Record FindRecord(int id) {
  var record = Records.Find(id);
  if (record == null) throw new RecordNotFoundException("Record not found");
  return record;
}

public bool RecordIsValid(Record record) {
  var validator = new Validator<Record>(record);
  if (!validator.IsValid()) throw new ValidationException(validator.ErrorMessages)
  return true;
}

public bool UpdateRecord(Record record) {
  Records.Update(record);
  Records.Save();
  return true;
}

public bool UpdateRecord(int id, Record record) {
  if (id != record.ID) throw new IdMisMatchException("ID of record cannot be modified");

  FindRecord(id);
  RecordIsValid(record);
  UpdateRecord(record);
  return true;
}

Then in the controller action:然后在控制器动作中:

try {
  UpdateRecord(id, record)
  return Ok(record);
}
catch (RecordNotFoundException) { return NotFound(); }
// ...

The code is waaaay simpler... Each method either works, or raises a specific exception subclass... There are no checks to see if methods succeeded or failed... There are no type conversions with exception results... You could easily add an application wide exception handler that returns the the correct response and status code based on exception type... There are so many positives to using exceptions for control flow...代码更简单......每个方法都可以工作,或者引发特定的异常子类......没有检查方法是否成功或失败......没有异常结果的类型转换......您可以轻松添加一个应用程序范围的异常处理程序,该处理程序根据异常类型返回正确的响应和状态代码......使用控制流异常有很多好处......

I'm not sure what the negatives are... people say that they're like GOTOs... not really sure why that's bad... they also say that the performance is bad... but so what?我不确定负面因素是什么...人们说他们就像 GOTO...不太确定为什么这很糟糕...他们还说性能很差...但那又如何? How does that compare to the DB calls that are being made?这与正在进行的数据库调用相比如何? I'm not sure if the negatives are really valid reasons.我不确定负面是否真的是正当理由。

Exceptions are just that: a way to handle exceptional scenarios, scenarios which should not ordinarily happen in your application.异常就是这样:一种处理异常场景的方法,这些场景通常不应该在您的应用程序中发生。 Both examples provided are reasonable examples of how to use exceptions correctly.提供的两个示例都是如何正确使用异常的合理示例。 In both instances they are identifying that an action has been invoked which should not be allowed to take place and is exceptional to the normal flow of the application.在这两种情况下,他们都在识别调用了一个不应被允许发生的动作,并且对于应用程序的正常流程来说是异常的。

The misinterpretation is that the second error, the renaming method, is the only mechanism to detect the renaming error.误解是第二个错误,重命名方法,是检测重命名错误的唯一机制。 Exceptions should never be used as a mechanism for passing messages to a user interface.永远不应将异常用作将消息传递到用户界面的机制。 In this case, you would have some logic checking the name specified for the rename is valid somewhere in your UI validation.在这种情况下,您将有一些逻辑检查为重命名指定的名称在您的 UI 验证中的某处是否有效。 This validation would make it so that the exception would never be part of normal flow.这种验证将使异常永远不会成为正常流程的一部分。

Exceptions are there to stop "bad things" happening, they act as last-line-of-defence to your API calls to ensure that errors are stopped and that only legal behaviour can take place.例外是为了阻止“坏事”的发生,它们充当 API 调用的最后一道防线,以确保停止错误并且只发生合法行为。 They are programmer-level errors and should only ever indicate one of the following has occured:它们是程序员级别的错误,应该只表明发生了以下情况之一:

  • Something catastrophic and systemic has occurred, such as running out of memory.发生了灾难性和系统性的事情,例如内存不足。
  • A programmer has gone and programmed something wrong, be it a bad call to a method or a piece of illegal SQL.程序员已经编写了错误的程序,无论是对方法的错误调用还是一段非法 SQL。

They are not supposed to be the only line of defence against user error.它们不应该是防止用户错误的唯一防线。 Users require vastly more feedback and care than an exception will ever offer, and will routinely attempt to do things which are outside of the expected flow of your applications.用户需要比异常提供的更多的反馈和关心,并且会经常尝试做超出应用程序预期流程的事情。

Exceptions are a expensive in consume time and resources for the application.异常在消耗应用程序的时间和资源方面是昂贵的。 and in my opinion should be relegate to those problem that you code can not detect.在我看来,应该归结为您的代码无法检测到的那些问题。 if a problem is detected you can return to the UI the error simple by creating a simple class with the list of the error response and a code (the code is nor recommended when you handling exception, but in this case you are not handling exception)如果检测到问题,您可以通过创建一个包含错误响应列表和代码的简单类将错误简单地返回到 UI(在处理异常时也不建议使用该代码,但在这种情况下您不处理异常)

for example something like that:例如这样的:

list<Error> errors

public class Error
{
    public int code {get;set;}
    public string description {get;set;}
}

You can return this class inside your object or you can create a tuple and return the values as a second element of the tuple.您可以在对象中返回此类,也可以创建一个元组并将值作为元组的第二个元素返回。

If you generate exceptions it is very difficult to know if your application is ok, if you don't generate, any tool that report your exception in your application, give you the idea of how much problem your code has.如果您生成异常,则很难知道您的应用程序是否正常,如果您不生成,任何在您的应用程序中报告您的异常的工具,都会让您了解您的代码有多少问题。

Of course also all depend of the size of your application, from the point of view of performance, if you handle 100 or less users in your application you don't note a big difference in use exception or not, but if you are in a web or api service that handle more users that begin to be a big performance problem, maybe it is difficult to see.当然也取决于你的应用程序的大小,从性能的角度来看,如果你在你的应用程序中处理 100 个或更少的用户,你不会注意到使用异常有很大的不同,但是如果你在一个处理更多用户的 web 或 api 服务开始成为一个很大的性能问题,也许很难看到。 but if you are in azure for example you can pay a lot of money because the necessity of memory and processor is increased by handle the exceptions.但是,例如,如果您使用天蓝色,则可以花很多钱,因为处理异常会增加内存和处理器的必要性。

In this case in my opinion is better avoid exception.在这种情况下,我认为最好避免异常。 a little more complicate code in our side avoid a lot of code execution inside the system.在我们这边稍微复杂一点的代码避免了系统内部的大量代码执行。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM