【问题标题】:What is the best way to extend null check?扩展空检查的最佳方法是什么?
【发布时间】:2012-07-17 12:05:18
【问题描述】:

你们都这样做:

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);
}

你怎么看?

【问题讨论】:

  • 创建一个扩展对象/一切的扩展方法几乎从来都不是一个好主意。 . stackoverflow.com/a/7652359/284240
  • 这样递归安全吗?这不会无限递归吗?
  • Jon Skeet 在这里提到它:stackoverflow.com/questions/291340/…
  • @Rup 什么条件会停止递归?
  • 当然很清楚:函数的第一行在涉及任何条件逻辑之前再次调用该函数..那不是无限的吗?

标签: c# null extension-methods isnull


【解决方案1】:

为此,我倾向于坚持使用无处不在的 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 方法是完全有效的)。

至于哪个最好,我都不会使用。 它们都有无限递归。我也不会费心保护 message 参数,将其设为 null 即可。您的第一个解决方案也不支持 Nullable&lt;T&gt; 类型,因为 class 约束会阻止它。

当我们决定启用代码协定时,我们的 Guard 类后面还有 Contract.EndContractBlock() 调用,因为它符合所需的“if-then-throw”结构。

这也是PostSharp aspect 的完美候选人。

【讨论】:

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

我会使用internal static void ThrowIfNull&lt;T&gt;(this T o, string paramName) where T : class。我不会使用internal static void ThrowIfNull(this object o, string paramName),因为它可能会进行拳击。

【讨论】:

  • 该选择的唯一缺点是您将无法测试 Nullable&lt;T&gt; 项目。
  • @AgentFire 来自编译器:“类型'int?'必须是引用类型才能在 VS2010 中将其用作泛型类型或方法中的参数“T”,因此发生了一些变化。
  • 为什么要在这种情况下测试 Nullable 是否为 null?如果有这样的需求,那么功能需要重新设计
  • @AgentFire Framework 4.0 也是,但这将是一个编译器。 Vasile 是的,但我只是说出来,因为它们在技术上可以包含 null。
  • 拳击的影响也可以忽略不计,人们忘记了它实际上是从 .NET 前泛型时代开始进行了相当优化的。虽然它可以增加 GC 压力。如今,我倾向于发现“拳击”并不是一个有效的包罗万象的借口,拳击在某些情况下是完全可以接受的,而在其他情况下则不行。
【解决方案3】:

我会这样做以避免硬编码参数名称。明天它可能会改变,然后你有更多的工作:

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);

一个这样的here。但是如果你负担不起那么大的打击,肯定不要使用这个..

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

new { myClass.MyProperty1 }.ThrowIfNull();

您可以缓存属性值以进一步提高性能,因为属性名称在运行时不会更改。

另见此问题:Resolving a parameter name at runtime

【讨论】:

  • 从您的代码示例中,如果对象为空,它会返回,您确定它正在工作吗?
  • 我不确定你的意思。哪个对象?匿名对象?是的,它确实从函数返回 null,这是一个糟糕的设计选择 imo。最好也扔在那里。
  • 我认为文森特的意思是在第一个代码示例中实现“ThrowIfNull”方法,在第一个 if 语句中,如果对象为 null 并且不抛出错误,它将返回。
【解决方案4】:

从 .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://docs.microsoft.com/en-us/dotnet/api/system.argumentnullexception.throwifnull?view=net-6.0


implementation of this new method 利用System.Runtime.CompilerServices.CallerArgumentExpressionAttribute 属性进一步简化了这一点,不需要开发人员明确提供要保护的参数的名称。

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

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

【讨论】:

  • 在创建可空上下文时,所有空检查应该不复存在。
  • @AgentFire 我不相信是这样的。可空上下文的引入是一个受欢迎的补充,它将避免在构建时围绕该主题(特别是在新代码库中)的常见错误,但您仍然可以在运行时被取消引用所困扰。您可以在此处阅读有关这些陷阱的更多信息:docs.microsoft.com/en-us/dotnet/csharp/…
  • 是的,您可能会被旧的 3rd 方库所困扰,这些库应该由可为空的上下文处理。或者更好的是,您可能会因忽略或滥用可为空的上下文功能而受到影响。这并不意味着您应该重新开始编写那些“ThrowIfNull”。
【解决方案5】:

如何使用表达式树(来自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.
}

【讨论】:

  • 为时已晚。您现在可以使用nameof() 运算符作为成员名称。与您的解决方案不同,它是编译时处理的。
  • @AgentFire 您错过了解决方案的重点。在这里,他不必多次传入parameter1。通过将参数作为表达式传递给助手类,他得到了值和参数名称:)
【解决方案6】:

基于 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();
    }
}

【讨论】:

    【解决方案7】:

    第二种处理方式似乎更优雅。在这种情况下,您可以对每个托管对象进行限制。

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

    【讨论】:

    • 我也可以使用类型化参数 来做到这一点。
    猜你喜欢
    • 1970-01-01
    • 2020-12-08
    • 2013-06-22
    • 2012-03-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-10-05
    • 2018-03-19
    相关资源
    最近更新 更多