【问题标题】:Is it necessary to check null values with constructor injection?是否有必要使用构造函数注入检查空值?
【发布时间】:2019-03-23 15:26:45
【问题描述】:

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

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

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

public class MyController
{
    private readonly IMyService _myService;

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

【问题讨论】:

  • 这也是我的怀疑,但你能链接到文档或其他验证这一点的来源吗?
  • @CamiloTerevinto 这并不意味着可以永远传入一个空实例,对吗?如果不是来自 IOC 容器,手动实例化可能为空。我一直只是检查 null 并抛出 ArgumentNullException 以确保安全
  • @CamiloTerevinto 它是反 DI,我从不手动实例化任何东西。 但是没有办法以编程方式强制执行。我不认为为了安全而进行检查会伤害或占用任何重要的处理能力
  • @CamiloTerevinto:让我们假设不是每个人都做得很完美。这只在协作多任务、裸指针、未检查的数组边界、期望每个分配后都有一个 dealloc 等方面失败了。;)
  • ... manual instantiation would never make it through code reviews and common sense 即使在单元测试中?

标签: c# dependency-injection .net-core constructor-injection null-check


【解决方案1】:

构造函数注入是否需要检查空值?

视情况而定。

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

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

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

那就去做吧。手动实例化会在某处导致 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));
}

【讨论】:

  • 另外,使用较新的 C# 语言选项,您可以这样做; (当然就在 ctor 里面)_httpContextAccessor = httpContextAccessor ?? throw new ArgumentNullException(nameof(httpContextAccessor)); 有点意思。
  • 感谢@ScottFraley - 添加到回答中
【解决方案2】:

为了避免碍事,在构造函数中进行空检查并没有什么坏处。


有两种方法可以从框架中获取 DI 服务。

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

第二个是GetRequiredService<T>()。如果找不到该服务,此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: '在尝试激活 'DITests.ServiceB' 时,无法解析类型 'DITests.IServiceA' 的服务。'

如果我改为使用services.GetService&lt;IServiceA&gt;(),我会得到一个空值。


您可以通过查看GitHub source code 亲自了解这一点。当调用任何一种方法时,它最终都会到达CreateConstructorCallSite 方法。如果无法解析您的类型的依赖关系,则会引发异常。


对于 ASP.Net Core MVC,它使用 GetRequiredService&lt;&gt;() 从 DI 图中获取您的控制器。


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

正如您在帖子中提到的,我没有看到微软书面文件明确表示您不需要

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

【讨论】:

    【解决方案3】:

    Camilo Terevintogunr2171 的这些答案中的任何一个都对您有好处。

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

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

    通过一种或另一种方式,您可以立即知道在您的特定情况下出现了什么问题以及问题出在哪里。控制器构造函数中的空检查只是开销。这就是来自Camilo Terevinto 的答案失败的地方。你永远不会自己发起它。 .NET Core 中的内置 DI 服务旨在永远不会将 null 注入到您的构造函数中,除非您指示它这样做。

    gunr2171 的回答是关于 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(...)
    

    你拥有的每一个控制器/视图模型。

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


    .NET 6 及更高版本

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

    如果您坚持进行空检查,此方法可以为您节省一些输入。

    【讨论】:

    • 注入存储库首先在控制器中做什么?
    • 嗯,这是很常见的做法。见this
    • “问问你自己,后者是否会给你带来任何新信息” - 我认为将ArgumentNullException 扔到NullReferenceException 上的好处是你' d 早期失败,抛出实例而不是稍后(可能很长一段时间),从而掩盖了真正的问题。如果这是一个库类或将看到除纯粹由 DI 之外的任何用途,我会签入,这样做的开销确实很小。
    • @pcdev 早期失败是一种有效的方法,但不是在这个问题的上下文中。 .NET Core/.NET 5 DI 不会将 null 注入到构造函数中。如果注入的依赖项变为null,它将发生在这些检查的范围之外,所以再一次 - 让它们在那里没有真正的价值。
    • 关键点是The build-in DI service in .NET Core is designed to never inject a null into your constructors.。如果依赖项注册未完成,我们会在 HostBuilder 的运行本身中收到错误,该错误通常在 Program 文件中声明。这意味着,与过去我们仅在使用依赖项时才会得到 NullReferenceException 不同,在 .NetCore 中,我们会在初始阶段本身得到错误。
    【解决方案4】:

    我想提出不同的观点。这……

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

    ...不仅是对执行环境的运行时检查,它还是未来开发人员的类不变量(参见“Design by Contract”)。

    由于_myServicereadonly 并且每个构造函数都包含一个空检查,因此将来修改/扩展该类的开发人员将确定字段_myService 永远不能为空, 即使不知道如何使用该类

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

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

    ...但是在构造函数中添加一个空检查是在 C# 中更惯用的方法。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2012-11-26
      • 1970-01-01
      • 1970-01-01
      • 2017-11-04
      • 2017-01-13
      • 2016-04-12
      相关资源
      最近更新 更多