C# 哪种设计更可取:测试创建、尝试创建、创建捕获?

C# 哪种设计更可取:测试创建、尝试创建、创建捕获?,c#,C#,让我们假设有一个创建用户的操作。如果指定的电子邮件或用户名存在,此操作可能会失败。如果它失败了,它需要知道确切的原因。在我看来,有三种方法可以做到这一点,我想知道是否有一个明确的赢家 下面是一个类用户: class User { public string Email { get; set; } public string UserName { get; set; } } 实现创建操作的方法有三种: 测试创建 if (UserExists(user)) act on user

让我们假设有一个创建用户的操作。如果指定的电子邮件或用户名存在,此操作可能会失败。如果它失败了,它需要知道确切的原因。在我看来,有三种方法可以做到这一点,我想知道是否有一个明确的赢家

下面是一个类用户:

class User
{
    public string Email { get; set; }
    public string UserName { get; set; }
}
实现创建操作的方法有三种:

测试创建

if (UserExists(user)) act on user exists error;
if (UsernameExists(user)) act on username exists error;
CreateUser(user);
enum CreateUserResultCode
{
    Success,
    UserAlreadyExists,
    UsernameAlreadyExists
}

if (!TryCreate(user, out resultCode))
{
    switch(resultCode)
    {
        case UserAlreadyExists: act on user exists error;
        case UsernameAlreadyExists: act on username exists error;
    }
}
UserExists和UsernameExists请求db服务器进行验证。在CreateUser中再次重复这些调用,以确保正确使用API。若验证失败,我将在这两种情况下抛出ArgumentOutOfRangeException。因此,有一个性能的打击

尝试创建

if (UserExists(user)) act on user exists error;
if (UsernameExists(user)) act on username exists error;
CreateUser(user);
enum CreateUserResultCode
{
    Success,
    UserAlreadyExists,
    UsernameAlreadyExists
}

if (!TryCreate(user, out resultCode))
{
    switch(resultCode)
    {
        case UserAlreadyExists: act on user exists error;
        case UsernameAlreadyExists: act on username exists error;
    }
}
这种模式只进行一次验证,但我们求助于使用所谓的错误代码,这被认为是一种不好的做法

创建捕获

try
{
    CreateUser(user);
}
catch(UserExistsException)
{
    act on user exists error;
}
catch(UsernameExistsException)
{
    act on username exists error;
}
我在这里不使用错误代码,但我现在必须为每种情况创建一个单独的异常类。这或多或少是异常应该如何使用的,但我想知道创建一个单独的异常而不是enum条目是否值得


那么,我们是否有一个明确的赢家,或者这更多的是一个品味问题?

测试创建可能会导致比赛条件,所以这不是一个好主意。它也可能做额外的工作

如果您希望错误是正常代码流的一部分(例如在用户输入的情况下),那么Try Create很好

如果错误确实是异常的(所以您不必担心性能),那么createcatch很好

那么,我们是有明确的赢家,还是更多的是品味问题

第一个选项有一个基本缺陷-如果
CreateUser
依赖于外部资源,那么它永远不会是线程安全或安全的,其他实现可能会在测试之间创建。一般来说,我倾向于避免这种“模式”,因为这一点

至于其他两种选择——这实际上取决于是否预期会发生失败。如果
CreateUser
在某种正常的基础上会失败,那么Try*模式是我的首选,因为使用异常本质上变成了对控制流使用异常


如果失败真的是一个例外情况,那么例外情况就更容易理解。

这有点主观,但有一些具体的优点和缺点值得指出

测试创建方法的一个缺点是竞争条件。如果两个客户端尝试在几乎相同的时间创建同一用户,则它们可能都通过了测试,然后尝试创建同一用户

在TryCreate和CreateCatch之间,我更喜欢CreateCatch,但这是我个人的口味。有人可能会说createcatch使用异常进行流控制,这通常是不受欢迎的。另一方面,Try Create需要一个有点笨拙的
output
参数,这很容易被忽略


因此,我更喜欢创建Catch,但这里肯定有争论的余地。

您指定
UserExists
UsernameExists
都进行DB调用。我假设
CreateUser
也进行数据库调用

为什么不让数据库处理线程问题?进一步假设在您的表上设置了适当的约束,您可以让它在存储的proc调用中出错


因此,我将投票支持创建Catch

@Reed。我越来越考虑尝试*方法,但我对所有声称我不应该使用错误代码进行错误处理的书籍和文章感到气馁(例如)。他们指出最大的问题之一是如何将错误代码传递到调用堆栈中。有什么想法吗?@andriys这取决于他们是否需要放弃调用堆栈。如果您需要,异常可能会更好——但如果这真的只是控制流(如示例代码),那么只有一个结果通常更简单、更清晰。我肯定会避免单独的检查,当然,没有一个选项可以保证线程安全。只有当有一个原子操作用于创建(如果可能)返回有关成功或失败原因的信息时,后两个才是线程安全的。我知道你知道这一点,但我认为值得指出,以免有人产生错误的印象。@LarsH是的,当然。第一个选项只是保证API本身永远不会有线程安全性。其他的需要API(内部)处理潜在的竞争条件…我也更喜欢在这种情况下创建捕获。主键冲突当然是一个有效的异常。对于无效的用户输入,可能会有点含糊不清,但我可能仍然会选择抛出FormatException。您可能对Raymond Chen的博客文章感兴趣,他认为错误代码优于异常。还有其他外部参考吗?我很感兴趣。我想指出,在很多情况下,比如访问数据库,异常的性能成本并不是什么大问题。然而,处理这些例外情况的概念成本很低。如果有人复制了您的
Create Catch
逻辑,却忘记了
Catch
部分,则会发生奇怪的事情,但在
Try Create
案例中,返回值会使其更加明显,且失败是无声的。当然,每件事都有利弊。