如何最好地扩展空值检查?

17

你们都可以这样做:

public void Proc(object parameter)
{
    if (parameter == null)
        throw new ArgumentNullException("parameter");

    // Main code.
}

Jon Skeet曾经提到他有时会使用这个扩展来进行检查,所以你可以这样做:

parameter.ThrowIfNull("parameter");

所以我提出了两种扩展实现方式,但我不知道哪一种是最好的。
第一种:
internal static void ThrowIfNull<T>(this T o, string paramName) where T : class
{
    if (o == null)
        throw new ArgumentNullException(paramName);
}

第二点:

internal static void ThrowIfNull(this object o, string paramName)
{
    if (o == null)
        throw new ArgumentNullException(paramName);
}

你认为怎样?


1
几乎从来都不是一个好主意去创建一个扩展方法来扩展object/一切。. https://dev59.com/tGsz5IYBdhLWcg3wy7HU#7652359 - Tim Schmelter
1
这样递归是安全的吗?不会无限递归吗? - Rup
Jon Skeet在这里提到:https://dev59.com/gnVC5IYBdhLWcg3wcwsm - dash
@Rup 什么条件会停止递归? - anouar.bagari
2
当然很清楚:函数的第一行在涉及任何条件逻辑之前再次调用函数。这不是无限的吗? - Simon Whitehead
显示剩余2条评论
7个回答

13

我倾向于使用广泛存在的Guard类来实现这个:

static class Guard
{
    public static void AgainstNulls(object parameter, string name = null)
    {
        if (parameter == null) 
            throw new ArgumentNullException(name ?? "guarded argument was null");

        Contract.EndContractBlock(); // If you use Code Contracts.
    }
}

Guard.AgainstNulls(parameter, "parameter");

避免扩展object,而对于一个null对象的方法调用在外人看来似乎没有意义(虽然我知道可以对扩展方法进行null方法调用而是完全有效的)。

至于哪种最好,我都不会选。 它们两个都有无限递归。我也不会费心保护信息参数,让它可选为null。你的第一个解决方案也不支持Nullable<T>类型,因为class约束阻止了它。

我们的Guard类还在之后调用Contract.EndContractBlock(),以便在我们决定启用代码合同时,符合所需的“if-then-throw”结构。

这也是PostSharp aspect的完美候选。


我对此有两种想法;我认为我更喜欢Jon Skeet的明确的NonNullable<T>合约,因为它在创建时明确表示我永远不希望T为空,而Guard.AgainstNulls可能会被遗忘,或者可能隐藏在另一个方法中。不过,我完全同意你的PostSharp方面,所以+1 + -0.5 + 1(加上Math.Floor)的结果是... +1 ;-) - dash
1
我认为@AgentFire的意思是NotNullable<T>。另一方面,有些人只是喜欢使用扩展语法而不是显式方法调用。 - dash
如果某些东西非常重要,以至于应该将其纳入合同中,那么很可能也应该为它编写测试——这意味着,如果您忘记了防止空值,测试也会提醒您。 - Adam Houldsworth
如果每个人都为每种可能的用法编写测试就好了 :-) 我实际上认为NotNullable<T>更明确 - 我已经决定,在创建此时它永远不会为空,而Guard.AgainstNulls是我创建了某些东西,但我需要记住我从不希望它为空;这在很大程度上是一种语义上的差异,我确实喜欢你的答案!当然,这里也有一定的编码风格元素。 - dash
@dash 我不这样做的唯一原因是有太多的东西在使用时不能为null。在我看来,主要的区别在于像这样的结构体完全防止它们为空,而测试只意味着在使用之前它们不能为null(但在其他地方仍然可以设置为null)。 - Adam Houldsworth

5
从.NET 6开始,System.ArgumentNullException 类中现在具有名为 ThrowIfNull 的静态方法,其签名如下:
ThrowIfNull(object? argument, string? paramName = null);

因此,不要写成:
if (value == null)
{
    throw new System.ArgumentNullException(nameof(value));
}

现在我们可以简单地写成:
System.ArgumentNullException.ThrowIfNull(value);

文档: https://learn.microsoft.com/zh-cn/dotnet/api/system.argumentnullexception.throwifnull?view=net-6.0


这个新方法的实现利用了System.Runtime.CompilerServices.CallerArgumentExpressionAttribute属性进一步简化,无需开发人员显式提供所保护的参数名称。

引入此新API的讨论可在此处找到: https://github.com/dotnet/runtime/issues/48573

引入它到.NET 6代码库的PR可在此处找到: https://github.com/dotnet/runtime/pull/55594


在可空上下文创建之后,所有的空值检查都应该停止存在。 - AgentFire
@AgentFire 我不认为情况是这样的。可空上下文的引入是一个受欢迎的补充,可以在构建时避免关于此主题(特别是在新代码库中)的常见错误,但您仍然可能在运行时被解引用所咬。您可以在此处阅读有关这些陷阱的更多信息:https://learn.microsoft.com/en-us/dotnet/csharp/nullable-references#known-pitfalls - yv989c
是的,您可能会受到旧的第三方库的影响,这些库由可空上下文处理,就像它们应该处理的那样。或者更好的是,您可能会因为忽略或误用可空上下文功能而受到影响。这并不意味着您应该重新开始编写所有的“ThrowIfNull”代码。 - AgentFire

5

我会使用internal static void ThrowIfNull<T>(this T o, string paramName) where T : class。我不会使用internal static void ThrowIfNull(this object o, string paramName),因为它可能会进行装箱操作。


1
那个选择的唯一缺点是你将无法测试 Nullable<T> 项。 - Adam Houldsworth
来自编译器的信息:“在使用泛型类型或方法中的参数'T'时,'int?'类型必须是引用类型”,这在VS2010中表示有所改变。 - Adam Houldsworth
1
在这种情况下,为什么要测试 Nullable 是否为 null?如果确实有这样的需要,那么函数就需要重新设计。 - Vasile Mare
@AgentFire 框架4.0也是一个编译器。Vasile没错,但我只是说一下,因为它们在技术上可以包含null。 - Adam Houldsworth
2
装箱的影响也是微不足道的,人们忘记了它实际上从.NET泛型之前就已经被优化了。虽然它可能会增加GC压力。我倾向于发现“装箱”这个词如今不再是一个有效的通用借口,因为在某些情况下,装箱是完全可以接受的,而在其他情况下则不是。 - Adam Houldsworth

4
我会这样做来避免硬编码参数名称。明天可能会改变,那时你就需要做更多的工作:
public static void ThrowIfNull<T>(this T item) where T : class
{
    var param = typeof(T).GetProperties()[0];
    if (param.GetValue(item, null) == null)
        throw new ArgumentNullException(param.Name);
}

并将其命名为:

public void Proc(object parameter)
{
    new { parameter }.ThrowIfNull(); //you have to call it this way.

    // Main code.
}

性能影响微不足道(在我的普通电脑上,它运行了100000次,只用了不到25毫秒),比通常看到的基于表达式的方法要快得多。
ThrowIfNull(() => resource);

这样一个链接。但如果你承受不起那么大的打击,一定不要使用它。

您还可以将其扩展到对象的属性。

new { myClass.MyProperty1 }.ThrowIfNull();

您可以缓存属性值以进一步提高性能,因为属性名称在运行时不会更改。
另请参阅此问题:在运行时解决参数名称

从你的代码示例中,它返回对象是否为空,你确定它能正常工作吗? - Vincent
我不确定你在这里指的是什么对象。匿名对象吗?是的,它确实从函数返回null,但在我看来这是一个糟糕的设计选择。最好也在那里抛出异常。 - nawfal
我认为文森特的意思是在第一个代码示例中的“ThrowIfNull”方法的实现中,在第一个if语句中,如果对象为空,则会返回而不是抛出错误。 - Mayoweezy

0

基于 C# 10,我使用 ThrowIfNull 扩展方法:

public static class CheckNullArgument
{
    public static T ThrowIfNull<T>(this T argument)
    {
        ArgumentNullException.ThrowIfNull(argument);

        return argument;
    }
}

使用方法:

public class UsersController
{
    private readonly IUserService _userService;

    public UsersController(IUserService userService)
    {
        _userService = userService.ThrowIfNull();
    }
}

0

使用表达式树怎么样(来自Visual Studio Magazine):

using System;
using System.Linq.Expressions;
namespace Validation
{
   public static class Validator
   {
     public static void ThrowIfNull(Expression<Func<object>> expression)
     {
       var body = expression.Body as MemberExpression;
       if( body == null)
       {
         throw new ArgumentException(
           "expected property or field expression.");
       }
       var compiled = expression.Compile();
       var value = compiled();
       if( value == null)
       {
         throw new ArgumentNullException(body.Member.Name);
       }
     }
     public static void ThrowIfNullOrEmpty(Expression<Func<String>> expression)  
     {
        var body = expression.Body as MemberExpression;
        if (body == null)
        {
          throw new ArgumentException(
            "expected property or field expression.");
        }
        var compiled = expression.Compile();
        var value = compiled();
        if (String.IsNullOrEmpty(value))
        {
          throw new ArgumentException(
            "String is null or empty", body.Member.Name);
        }
      }
   }

}

使用方式如下:

public void Proc(object parameter1, object parameter2, string string1)
{
    Validator.ThrowIfNull(() => parameter1);
    Validator.ThrowIfNull(() => parameter2);
    Validator.ThrowIfNullOrEmpty(() => string1);
    // Main code.
}

1
现在为时已晚。您现在可以使用 nameof() 运算符来获取成员名称。而且它是编译时处理的,不像您的解决方案。 - AgentFire
@AgentFire,你误解了这个解决方案的重点。他不必多次传递 parameter1。通过将参数作为表达式传递给辅助类,他既得到了值,又得到了参数名称 :) - nawfal

-1

第二种方法似乎是处理相同问题更优雅的方式。在这种情况下,您可以对每个受控对象设置限制。

internal static void ThrowIfNull(this object o, string paramName)
{
       if (o == null)
        throw new ArgumentNullException(paramName);
}

我也可以用带有类型参数来做那件事。 - AgentFire

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接