复杂类型的方法参数验证

Method argument validation of complex types

我很喜欢方法中的参数验证。像这样的东西在我的代码中很常见:

public void Foo(string someString)
{
    #region parameter validation
    if(string.IsNullOrWhiteSpace(someString))
        throw new ArgumentNullException("someString");
        // Let's ignore the .NET 4.6 nameof-feature for the sake of this question.
    #endregion

    // do things
}

现在假设该方法不接受字符串,而是接受复杂对象。假设我希望该方法验证是否设置了此复杂对象的 属性。这样做的正确方法是什么?到目前为止,我是这样做的:

public void Foo(Person person)
{
    #region parameter validation
    if(person == null)
        throw new ArgumentNullException("person");
    if(string.IsNullOrWhiteSpace(person.Name))
        throw new ArgumentNullException("person.Name");
    #endregion

    // do things
}

对此有一些最佳实践吗? ArgumentNullException 是正确的方法吗?甚至推荐检查这样的属性吗?

在我的例子中,我所有的模型都有一个 Validate 方法,这样我就调用它,如果它 returns false(这意味着我有错误)我调用另一个方法来从中获取错误并且向用户显示,如下所示:

if (person == null)
    throw new ArgumentNullException (nameof (person));

// Validates only 1 property.
if (!person.Validate (nameof (person.MyProperty)))
    DisplayErrors (person);

// Validates all properties.
if (!person.Validate ())
    DisplayErrors (person);

背后的原因是在大多数情况下我必须允许模型在用户填写信息时无效。我将这些方法与 WPF 的 INotifyDataErrorInfo 接口一起使用,以启用 UI 在发生错误时刷新。

当一个方法或操作只需要对象的一部分时,外观模型是您创建一个新的 class 来包裹更复杂的模型的解决方案,并且外观 class Validate 方法也进行部分验证。

对我来说,听起来 99% 的情况都是设计问题。没有名字的人有效吗?我怀疑不是。如果不是,你为什么允许一个没有一个?如果您(出于某种原因)确实允许它,那么对象是否有责任维护它自己的 "valid-ness"?我会依靠数据注释之类的东西来标记所需状态的元数据,并简单地检查传入的人是 "valid"

您可以考虑 code contracts。当然,如您所示进行检查是验证参数的一种非常好的方法……假设您只验证您正在使用的字段。验证您未引用的字段是自找麻烦(除非这是该方法的明确目的)。

最新版本的 C# 提供了一个 '?.'运算符可以让你做这样的事情:

int? length = customers?.Length;

string name = person?.Name;

这使您可以保护自己免受空引用异常的影响,并且在某些情况下可能比预验证更可取。

哦,还有 运行 代码分析工具通常会在验证参数之前标记使用参数,因此至少该工具认为这是最佳做法。

更新(回复评论): 这在很大程度上取决于你想做什么。如果您正在创建一个 API 并希望 return 尽可能最好的错误消息,那么您所建议的可能是您能做的最好的。这假设您主要关心与 API 的消费者沟通。

如果您的主要兴趣是确保提供给您的方法的数据有效并且不会导致您的方法表现不佳 (CYA),您应该查看合同。合同的优点是提供 post 条件(确保您的方法没有 return 意外)和不变条件(确保没有更改您不打算更改的内容)。契约可以提供静态(编译时)保证和运行时保证。有一种相当细粒度的方式来管理合约选项。运行时契约违规会产生人们可能不太熟悉的契约异常。用户可能也不熟悉编译时投诉。 这假定您主要关心确保正确操作或不因滥用 API 而受到指责。

您可以同时使用两者,例如使用 Contracts 进行编译时检查,使用更简单的 'if' 检查运行时。

官方好像没有正确的做法,所以我还是坚持自己的做法,就是

if(myComplexObject.SomeProperty == null)
    throw new ArgumentNullException("myComplexObject.SomeProperty");

来电者会立即知道出了什么问题。

请注意,我没有使用 nameof-operator,因为那样只会给我 SomeProperty 而不是 myComplexObject.SomeProperty。我可以做到 nameof(myComplexObject) + "." + nameof(myComplexObject.SomeProperty,但我需要再考虑一下,乍一看并不正确。

以下是我认为其他方法比我的方法更糟糕的原因:

有人说我应该在模型中验证,但我不会这样做,因为模型不知道如何针对它不知道的方法验证对象。如果它是整体对象实例有效性,当然,将其粘贴到模型中,或者如果你问我什至是业务层来进行业务验证,但情况并非如此。

其他人说我可以只使用空合并运算符(C# 中的 ?.),我也不会这样做,因为它不是 "if null then just work with that" 场景,而是 "I will not do a thing if this is null, and you should feel bad for giving me null, fix your code"场景

然后是代码契约,它提供了一种验证参数的好方法,并且有一些非常好的好处,但最终出现了同样的问题:我如何告诉调用者一个复杂对象的确切值是无效的?他需要知道参数 Y 的 属性 X 是错误的,仅此而已。虽然代码合同不是我问题的答案,但我认为如果出现相同情况,我的答案也可以用在那里。

然后有人建议我不应该在那个地方验证对象的属性,而是在需要它的函数中。问题是, 属性 几乎不用于作为单个分隔值传递给另一个函数。另外,它会与接口的想法作斗争。