是否有必要使用构造函数注入检查空值?

Is it necessary to check null values with constructor injection?

我正在使用 .NET Core 构造函数注入。 在一位同事的代码审查中,他提出了一个问题,即我是否应该检查控制器中注入依赖项的空值。

由于框架负责创建服务的实例,在我看来它会处理任何错误并且永远不会将空值依赖项传递给构造函数。不过我没有任何事实证据,所以我想知道是否有必要进行空检查。

例如,我是否应该在下面的代码中检查 'myService' 是否为空? (假设代码配置为使用 DI)

public class MyController
{
    private readonly IMyService _myService;

    public MyController(IMyService myService) 
    {
        _myService = myService;
    }
}

Is it necessary to check null values with constructor injection?

视情况而定。

  • 这个内部代码是否被您和(可能)一些队友在(幸运的)代码审查环境中使用?

不要。这不是必需的。框架不允许这样做。

  • 此代码是否在 public 库中,由多人使用或实际上没有遵循依赖注入?

那就去做吧。手动实例化会在某处导致 NullReferenceException 并且很难追踪。

就是说,使用这样的东西:

public MyController(IMyService myService) 
{
    if (myService == null)
    {
        throw new ArgumentNullException(nameof(myService));
    }

    _myService = myService;
}

是一种非常便宜的检查,如果有人出于某种原因通过 null,追踪起来会容易得多。


更好的是,正如@ScottFraley 提到的那样,使用更新的 C# 版本,上面的代码可读性更高:

public MyController(IMyService myService) 
{
    _myService = myService ?? throw new ArgumentNullException(nameof(myService));
}

为了避免明显的影响,在您的构造函数中进行空检查没有任何坏处。


从框架获取DI服务的方法有两种。

第一个是GetService<T>()。如果尚未注册此类服务,则此 return 为空值。

第二个是GetRequiredService<T>(). This will throw an exception如果找不到服务。

如果这两种方法无法完全实例化您请求的服务,它们都会抛出异常。

class Program
{
    static void Main(string[] args)
    {
        var services = new ServiceCollection()
            .AddTransient<IServiceB, ServiceB>()
            .BuildServiceProvider();

        var servB = services.GetService<IServiceB>();
    }
}

public interface IServiceA { }
public interface IServiceB { }

public class ServiceA : IServiceA { }
public class ServiceB : IServiceB { public ServiceB(IServiceA a) { } }

在此示例中,ServiceB 需要 IServiceA,但 A 未添加到依赖关系图中。最后一个方法会抛出异常:

System.InvalidOperationException: 'Unable to resolve service for type 'DITests.IServiceA' while attempting to activate 'DITests.ServiceB'.'

如果我改为 services.GetService<IServiceA>(),我会得到一个空值。


您可以通过查看 GitHub source code. When calling either method, it will eventually make it's way to the CreateConstructorCallSite 方法来亲自了解这一点。如果无法解析您的类型的依赖项,则会抛出异常。


至于 ASP.Net 核心 MVC,它使用 GetRequiredService<>() 从 DI 图中获取控制器。


总之,不,如果您使用的是纯 Microsoft DI 框架,则您 不需要 在构造函数中对 DI 对象执行 null 检查。正如Camilo Terevinto所说,框架不允许。

如您在 post 中所述,我还没有看到明确说明您不需要

的书面 Microsoft 文档

如果您将 IServiceProvider 作为构造函数参数传入,并且您正在从构造函数中解析服务, 那么 您将需要进行空值检查.

or 中的任何一个答案都会对您有所帮助。

一般来说,我们可以讨论将一个例外交易到另一个例外(NullReferenceExceptionArgumentNullException)是一笔好交易,还是根本就是一笔交易。问问自己,后者是否给您带来了任何新信息。在这种特殊情况下,我的拙见是

如果您的控制器的任何依赖项是 null,您会立即知道问题出在哪里。

无论哪种方式,您都会立即知道在您的特定情况下问题出在哪里。控制器的构造函数中的空检查只是开销。这就是 的答案失败的地方。你永远不会自己发起它。 .NET Core 中的内置 DI 服务旨在永远不会将 null 注入构造函数,除非您指示它这样做。

的回答是在谈论 DI 的完全不同的方面。这是一个有效的答案,但针对不同的问题。


超过 80% 的 DI 使用是构造函数注入。大多数这些注入都与 Razor 页面中的 MVC 控制器和视图模型有关。 MVC 是 ASP.NET Core 3.x 和 replaced by Razor Pages 中已弃用的设计模式,但逻辑几乎相同。

任何可用的控制器至少需要三个依赖项:

  1. 记录器
  2. 映射器
  3. 存储库

把我推到这里,你不想打字:

_logger = logger ?? throw new ArgumentNullException(...)
_mapper = mapper ?? throw new ArgumentNullException(...)
_repository = repository ?? throw new ArgumentNullException(...)

您拥有的每个 controller/view 型号。

肯定有更好的方法来做到这一点。我个人的建议是 this 在重复的问题中回答。


.NET 6 及更高版本

.NET中有一个new method API ArgumentNullException.ThrowIfNull(someParameter).

如果您坚持进行空值检查,此方法可以节省您的输入时间。

我想提出一个不同的观点。这...

private readonly IMyService _myService;

public MyController(IMyService myService) 
{
    _myService = myService ?? throw new ArgumentNullException(nameof(myService));
}

...不仅是对执行环境的运行时间检查,它也是对未来开发者class不变的(cf. "Design by Contract").

由于 _myServicereadonly 并且每个构造函数都包含空检查,开发人员 modifying/extending 这个 class 将来会 肯定知道 字段 _myService 永远不能为空,即使不知道 class 的使用方式

指定此不变量的另一种方法是向 _myService 添加注释...

// will always be provided by DI and, thus, will never be null
private readonly IMyService _myService;

...但是在 C# 中,向构造函数添加 null 检查是更惯用的方法。