依赖注入中检查 null 的更好方法

7
使用构造函数进行依赖注入时,我总是需要在将实例传递给内部属性之前检查是否为null。例如:
public UserManager(User user, IStateManager stateManager)
{
    if(user == null) throw new ArgumentNullException;
    if(statemanager == null) throw new ArgumentNullException("stateManager");

    _user = user;
    _stateManager = statemanager;
} 

在每个控制器/类上反复使用这种模式似乎很繁琐。是否有更好的方法来处理这个问题?顺便说一下,不同的控制器将有不同的构造函数初始化器。我正在使用Simple Injector进行DI。


只是顺便问一下,你在这里传递的是具体对象还是接口?我印象中DI会在构造函数中注入接口的具体实现。也许我漏掉了什么。 - mihai
嗨,我已经更正了这个问题。StateManager将传递一个接口。大多数构造函数将使用接口,但在这种小场景中,User将是一个部分。 - James Andrew Smith
4个回答

20
这是重复代码,但几乎不会成为问题,因为这可能会在代码库中引起全面更改吗?你是否需要更改许多这些检查?几乎不需要。阅读这篇博客文章,它详细介绍了这一点。
老实说,当涉及到我的注入构造函数时,我几乎不再添加这些空值检查,因为我知道我的DI容器不会在自动装配这些类型时将空引用注入到我的构造函数中。这样可以避免我写所有这些空值检查。
有人可能会争论我现在编写代码时考虑了我的DI容器,但我会反驳。我只是编写解决问题所需的最小量代码。在我的情况下,添加这些空值检查对我没有帮助。
但请注意,在我为可重用库编写代码的情况下,我绝对会写这些空值检查,因为我不知道谁在调用该代码。对于不作为注入构造函数(消息、实体、值类型、DTO)使用的构造函数,我确实添加了这些检查。但是,以下是一些如何使此过程更加美好的想法:
您可以添加一个漂亮的帮助程序方法,如下所示:
public UserManager(User user, IStateManager stateManager)
{
    Requires.IsNotNull(user, "user");
    Requires.IsNotNull(statemanager, "statemanager");

    _user = user;
    _stateManager = statemanager;
}

然而,这并不能真正帮助减少重复代码,尽管它确实减少了生成的机器代码的实际大小(但这很少是一个问题)。因此,你可以像这样使这个方法返回一个值:

public UserManager(User user, IStateManager stateManager)
{
    _user = Requires.IsNotNull(user, "user");
    _stateManager = Requires.IsNotNull(statemanager, "statemanager");
}

或者…使用C# 6.0:

public UserManager(User user, IStateManager stateManager)
{
    _user = Requires.IsNotNull(user, nameof(user));
    _stateManager = Requires.IsNotNull(statemanager, nameof(statemanager));
}

您可以按照以下方式实现此方法:
public static class Requires {
    public static T IsNotNull<T>(T instance, string paramName) where T : class {
        // Use ReferenceEquals in case T overrides equals.
        if (object.ReferenceEquals(null, instance)) {
            // Call a method that throws instead of throwing directly. This allows
            // this IsNotNull method to be inlined.
            ThrowArgumentNullException(paramName);
        }

        return instance;
    }

    private static void ThrowArgumentNullException(paramName) {
        throw new ArgumentNullException(paramName);
    }
}

在 C# 8 中,非空引用类型 可以默认为非空:

public UserManager(User user, IStateManager stateManager)
{
    _user = user;
    _stateManager = statemanager;
}

请注意,这仅仅是在编译时执行的强制措施,而不是运行时执行的强制措施。因此不会抛出任何异常。
C# 9 可能会改变这一点。有一个使用感叹号符号 ! 来添加运行时检查的提案:
public UserManager(User user!, IStateManager stateManager!)
{
    _user = user;
    _stateManager = statemanager;
}

4
非常干净利落的方式。谢谢。 - James Andrew Smith

4
我使用一个静态方法.ThrowIfNull,如果为空,则会抛出带有正确参数名称的ArgumentNullException。
public MyClass 
{
  public MyClass(DependencyType1 firstDependency, DependencyType2 secondDependency)
  {
     Arguments.ThrowIfNull(firstDependency, secondDependency);

     _firstDependency = firstDependency;
     _secondDependency = secondDependency;
  }
}

public static class Arguments
    {
        public static void ThrowIfNull(params object[] args)
        {
            for (var i = 0; i < args.Length; i++)
            {
                if (args[i] != null
                    && args[i].GetType() == typeof(ArgumentAction)
                    && (ArgumentAction)args[i] == ArgumentAction.Skip) continue;
                if (args[i] == null) throw GetArgumentNullException(i);
            }
        }

        private static ArgumentNullException GetArgumentNullException(int argIndex)
        {
            var frame = new StackFrame(2, false);
            var method = frame.GetMethod();
            var args = method.GetParameters();
            var name = args[argIndex].Name;
            return new ArgumentNullException(name);
        }

        public enum ArgumentAction
        {
            Undefined = 0,
            Skip
        }
    }

3
实际上,这些检查不产生任何商业价值,但会使你的代码更加混乱。其他开发人员可能会感到困惑,需要额外的努力理解为什么你要进行这些检查。 通常情况下,当依赖项无法解析时,IoC容器会抛出错误。即使您的容器没有抛出异常,在执行过程中,您也会遇到空引用异常。最好添加Debug.Assert语句,这种语句表示你不确定在这里发生了什么,但有时会得到null。

0

如果您非常坚持注入依赖项的检查,在 .NET 6 中有一个new method在 .NET API 中:ArgumentNullException.ThrowIfNull(someParameter)

我的建议是要么正确配置您的 DI,要么使用 PostSharp。


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