当您不使用异常来控制流时,代码如何显示?

时间:2009-05-28 03:58:50

标签: c# exception

我已经在其他回答的问题中看到了关于何时抛出异常的建议,但现在我的API有了新的噪音。我没有调用try / catch块中包含的方法(烦恼的异常),而是使用可能在处理过程中发生的错误集合的参数参数。我理解为什么在try / catch中包装所有内容是控制应用程序流程的一种不好的方法,但我很少看到任何能反映这种想法的代码。

这就是为什么这件事对我来说似乎很奇怪。这种做法应该是正确的编码方式,但我没有在任何地方看到它。除此之外,我还不太清楚当“坏”行为发生时如何与客户端代码相关。

以下是我正在讨论的一些代码片段,用于保存由网络应用用户上传的图片。不要惹恼细节(这很丑陋),只要看看我将这些输出参数添加到所有内容以获取错误消息的方式。

public void Save(UserAccount account, UserSubmittedFile file, out IList<ErrorMessage> errors)
{
    PictureData pictureData = _loader.GetPictureData(file, out errors);

    if(errors.Any())
    {
        return;
    }

    pictureData.For(account);

    _repo.Save(pictureData);
}

这是正确的想法吗?我可以合理地期望用户提交的文件在某种程度上是无效的,所以我不应该抛出异常,但是我想知道文件有什么问题,所以我产生错误消息。同样,任何现在使用此保存方法的客户端也希望找出整个图片保存操作的错误。

我还有其他一些关于返回包含结果和其他错误消息的状态对象的想法,但这感觉很奇怪。我知道在任何地方都有参数很难维护/重构/等等。

我会喜欢这方面的指导!

编辑:我认为用户提交的文件片段可能会让人们想到加载无效图片和其他“硬”错误所产生的异常。我认为这段代码片段更好地说明了我认为不鼓励抛出异常的想法。

有了这个,我只是保存一个新的用户帐户。我在用户帐户上进行状态验证,然后点击持久存储以查明是否已使用用户名。

public UserAccount Create(UserAccount account, out IList<ErrorMessage> errors)
{
    errors = _modelValidator.Validate(account);

    if (errors.Any())
    {
        return null;
    }

    if (_userRepo.UsernameExists(account.Username))
    {
        errors.Add(new ErrorMessage("Username has already been registered."));
        return null;
    }

    account = _userRepo.CreateUserAccount(account);

    return account;
}

我应该抛出某种验证异常吗?或者我应该返回错误消息吗?

6 个答案:

答案 0 :(得分:9)

尽管存在性能问题,但我认为允许异常被抛出方法实际上更加清晰。如果在您的方法中有任何可以处理的异常,您应该适当地处理它们,否则,让它们冒泡。

返回out参数中的错误,或返回状态代码感觉有点笨拙。有时遇到这种情况时,我会想象.NET框架如何处理错误。我不相信有很多.NET框架方法可以在out参数中返回错误,或者返回状态代码。

答案 1 :(得分:7)

根据定义,“例外”是指例行程序无法恢复的特殊情况。在您提供的示例中,看起来这意味着图像无效/损坏/不可读/等。这应该被抛出并冒泡到最顶层,那里决定如何处理异常。异常本身包含有关出错的最完整信息,必须在较高级别提供。

当人们说你不应该使用异常来控制程序流时,他们的意思是:(例如)如果用户试图创建一个帐户但该帐户已经存在,你不应该抛出一个AccountExistsException,然后再抓住它在应用程序中能够向用户提供该反馈,因为已存在的帐户不是特例。您应该期望这种情况并将其作为正常程序流程的一部分来处理。如果您无法连接到数据库,则 是一种例外情况。

您的用户注册示例的部分问题在于您尝试将太多内容封装到单个例程中。如果你的方法试图做不止一件事,那么你必须跟踪多个事物的状态(因此事情变得很丑陋,比如错误信息列表)。在这种情况下,您可以做的是:

UsernameStatus result = CheckUsernameStatus(username);
if(result == UsernameStatus.Available)
{
    CreateUserAccount(username);
}
else
{
    //update UI with appropriate message
}

enum UsernameStatus
{
    Available=1,
    Taken=2,
    IllegalCharacters=3
}

显然这是一个简化的例子,但我希望这一点很清楚:你的例程应该只尝试做一件事,并且应该有一个有限的/可预测的操作范围。这使得更容易停止和重定向程序流以处理各种情况。

答案 2 :(得分:3)

我认为这是错误的做法。是的,很可能会偶尔出现无效图像。但这仍然是特殊情况。在我看来,例外是正确的选择。

答案 3 :(得分:3)

在像你这样的情况下,我通常会向调用者抛出一个自定义异常。我对异常的看法可能比其他人有所不同:如果方法不能做它想做的事情(即方法名称所说的内容:创建用户帐户)那么它应该抛出异常 - 到我:不做你应该做的事情是特殊的。

对于您发布的示例,我的内容如下:

public UserAccount Create(UserAccount account)
{
    if (_userRepo.UsernameExists(account.Username))
        throw new UserNameAlreadyExistsException("username is already in use.");
    else
        return _userRepo.CreateUserAccount(account);
}

至少对我来说,好处是我的用户界面很愚蠢。我只是尝试/捕获任何函数和消息框的异常消息,如:

try
{
    UserAccount newAccount = accountThingy.Create(account);
}
catch (UserNameAlreadyExistsException unaex)
{
    MessageBox.Show(unaex.Message);
    return; // or do whatever here to cancel proceeding
}
catch (SomeOtherCustomException socex)
{
    MessageBox.Show(socex.Message);
    return; // or do whatever here to cancel proceeding
}
// If this is as high up as an exception in the app should bubble up to, 
// I'll catch Exception here too

这与一些示例类似于许多System.IO方法(http://msdn.microsoft.com/en-us/library/d62kzs03.aspx)。

如果它成为性能问题,那么我稍后会重构其他内容,但由于例外,我从来不需要从业务应用程序中挤出性能。

答案 4 :(得分:0)

我也允许例外,但根据你的线程寻找替代方案。为什么不在PictureData对象中包含状态或错误信息。然后,您可以返回包含错误的对象,并将其他内容留空。只是一个建议,但你几乎正在做什么例外来解决:)

答案 5 :(得分:0)

首先,永远不应将异常用作控制流机制。例外是错误传播和处理机制,但绝不应用于控制程序流。控制流是条件语句和循环的领域。这通常是许多程序员犯下的一个严重误解,并且通常是在他们试图处理异常时导致这种噩梦的原因。

在像C#这样提供结构化异常处理的语言中,我们的想法是允许识别,传播代码中的“特殊”案例,并最终处理。处理通常留给应用程序的最高级别(即具有UI和错误对话框的Windows客户端,具有错误页面的网站,后台服务的消息循环中的日志记录工具等)与Java不同,后者使用检查异常处理,C#不要求您专门处理可能通过您的方法的每个异常。相反,尝试这样做无疑会导致一些严重的性能瓶颈,因为捕获,处理和可能重新抛出异常是昂贵的业务。

C#中有例外的一般想法是,如果它们发生......我强调如果,因为它们被称为例外,因为在正常操作期间,您不应该遇到任何 特殊条件 ,......如果它们发生,那么您可以使用工具安全,干净地恢复并呈现用户(如果有一个)通知应用程序故障和可能的解决方案选项。

大多数时候,编写良好的C#应用​​程序在核心业务逻辑中不会有那么多try / catch块,并且使用块会有更多尝试/最终,或者更好。对于大多数代码,响应异常的关注点是通过释放资源,锁等来恢复,并允许异常继续。在更高级别的代码中,通常在应用程序的外部消息处理循环中或在ASP.NET等系统的标准事件处理程序中,您最终将使用try / catch执行结构化处理,可能有多个catch子句来处理需要独特处理的特定错误。

如果您正确处理异常并构建以适当方式使用异常的代码,则不必担心大量的try / catch / finally块,返回代码或带有大量ref和out的复杂方法签名参数。您应该看到更像这样的代码:

public void ClientAppMessageLoop()
{
    bool running = true;
    while (running)
    {
        object inputData = GetInputFromUser();
        try
        {
            ServiceLevelMethod(inputData);
        }
        catch (Exception ex)
        {
            // Error occurred, notify user and let them recover
        }
    }
}

// ...

public void ServiceLevelMethod(object someinput)
{
    using (SomeComponentThatsDisposable blah = new SomeComponentThatsDisposable())
    {
        blah.PerformSomeActionThatMayFail(someinput);
    } // Dispose() method on SomeComponentThatsDisposable is called here, critical resource freed regardless of exception
}

// ...

public class SomeComponentThatsDisposable: IDosposable
{
    public void PErformSomeActionThatMayFail(object someinput)
    {
        // Get some critical resource here...

        // OOPS: We forgot to check if someinput is null below, NullReferenceException!
        int hash = someinput.GetHashCode();
        Debug.WriteLine(hash);
    }

    public void Dispose()
    {
        GC.SuppressFinalize(this);

        // Clean up critical resource if its not null here!
    }
}

通过遵循上面的范例,你不会有太多凌乱的try / catch代码,但是你仍然会受到“保护”的异常,否则会中断你的正常程序流程并冒泡到更高级别的异常处理代码。

编辑:

一篇很好的文章,涵盖了异常的预期用途,以及为什么不在C#中检查异常,下面是对C#语言的首席架构师Anders Heijlsberg的采访:

http://www.artima.com/intv/handcuffsP.html

编辑2:

为了提供一个更好的示例,使用您发布的代码,可能以下内容将更有用。我猜的是一些名字,并且做了一些我遇到过服务的方式......所以请原谅我的任何许可:

public PictureDataService: IPictureDataService
{
  public PictureDataService(RepositoryFactory repositoryFactory, LoaderFactory loaderFactory)
  {
     _repositoryFactory = repositoryFactory;
     _loaderFactory = loaderFactory;
  }

  private readonly RepositoryFactory _repositoryFactory;
  private readonly LoaderFactory _loaderFactory;
  private PictureDataRepository _repo;
  private PictureDataLoader _loader;

  public void Save(UserAccount account, UserSubmittedFile file)
  {
    #region Validation
    if (account == null) throw new ArgumentNullException("account");
    if (file == null) throw new ArgumentNullException("file");
    #endregion

    using (PictureDataRepository repo = getRepository())
    using (PictureDataLoader loader = getLoader())
    {
      PictureData pictureData = loader.GetPictureData(file);
      pictureData.For(account);
      repo.Save(pictureData);
    } // Any exceptions cause repo and loader .Dispose() methods 
      // to be called, cleaning up their resources...the exception
      // bubbles up to the client
  }

  private PictureDataRepository getRepository()
  {
    if (_repo == null)
    {
      _repo = _repositoryFactory.GetPictureDataRepository();
    }

    return _repo;
  }

  private PictureDataLoader getLoader()
  {
    if (_loader == null)
    {
        _loader = _loaderFactory.GetPictureDataLoader();
    }

    return _loader;
  }
}

public class PictureDataRepository: IDisposable
{
  public PictureDataRepository(ConnectionFactory connectionFactory)
  {
  }

  private readonly ConnectionFactory _connectionFactory;
  private Connection _connection;

  // ... repository implementation ...

  public void Dispose()
  {
    GC.SuppressFinalize(this);

    _connection.Close();
    _connection = null; // 'detatch' from this object so GC can clean it up faster
  }
}

public class PictureDataLoader: IDisposable
{
  // ... Similar implementation as PictureDataRepository ...
}