【问题标题】:Why am I NOT getting a Null Reference Exception?为什么我没有收到空引用异常?
【发布时间】:2019-03-07 16:14:02
【问题描述】:

我正在使用 LINQ to Entities 从数据库中获取一些数据。以下是我的查询。

var location = from l in dbContext.Locations
join e in dbContext.Equipment on l.ID equals e.LocationID into rs1
from e in rs1.DefaultIfEmpty()
where ids.Contains(l.ID)
select new
{
    EquipmentClass = e,
    LocationID = l.ID,
    LocationName = l.Name,
    EquipmentName = e == null ? null : e.Name,
    Description = e == null ? null : e.Description,
    InServiceStatus = e == null ? false : e.InServiceStatus,
    EquipmentType = e.EquipmentType.Name
};

foreach (var item in location)
{
    // some logic
}

在上面的代码中,ids 是我传递的用于过滤结果的整数列表。当我得到结果时,我看到其中一条返回记录的EquipmentClass 为空。我执行了一些空值检查,但意识到我忘记对其中一个属性进行空值检查。现在我希望在EquipmentType = e.EquipmentType.Name 上得到一个空引用异常,但我没有。令我惊讶的是,它工作得很好,并且设置为 null。我的EquipmentClass 的属性类型为EquipmentType,这是另一个类。 EquipmentType 有一个 Name 属性,它是一个字符串。

  1. 为什么不抛出空引用异常?

作为测试,我从InServiceStatus = e == null ? false : e.InServiceStatus 中删除了空检查,并且在使用该查询运行 foreach 循环时它失败并出现无效操作异常。

  1. 这是否意味着我只需要对不可为空的值进行空检查?

更新

foreach (var item in location)
{
    var p = item.EquipmentClass.EquipmentType.Name;
}

在查询之后添加了这个。在分配 p 时,我得到一个空引用异常。我不确定它是怎么走那么远的,因为它应该在 foreach 循环的第一行失败。如果没有声明变量 p 的行,我没有得到空引用异常。如果有人能解释发生了什么,我将不胜感激。仅供参考,item.EquipmentClassitem.EquipmentType 的值在 foreach 循环开始时都为空。

更新 2: 我发现this link 似乎有人在使用 LINQ to SQL 时遇到了几乎相同的问题。我明白了答案的要点,但不完全理解它对我上面的两个问题的潜在影响。

【问题讨论】:

  • 我怀疑这是可能的。如果没有 null 检查代码EquipmentType = e.EquipmentType.Name 也将失败。你确定你的观察是正确的?我在最后尝试了一个快速代码示例,但它按预期失败了。
  • @RBT 实际上,运行我的代码不会产生任何错误,它会愉快地运行EquipmentType = e.EquipmentType.Name 并将所有值设置为 null(即,e = null,e.EquipmentType = null , 和 e.EquipmentType.Name = null)。
  • 你能用EquipmentClass的定义更新你的帖子吗?
  • 我已经更新了我的答案。如果您将foreach 枚举到最后,它将失败。当对应的匹配可用于连接时,它可能会通过几次初始迭代。

标签: c# linq linq-to-entities


【解决方案1】:

如果您针对IQueryable 编写一个LINQ 查询,那么您在幕后调用的方法(例如SelectWhere 等)除了记录之外没有任何作用您如何称呼它们,即它们记录谓词表达式并继承 LINQ 提供程序。一旦您开始迭代查询,会要求提供者执行查询模型。所以基本上,提供者使用表达式模型为您提供预期类型的​​结果。

提供程序绝不需要实际编译甚至执行您作为表达式提供的代码(模型)。事实上,LINQ to SQL 或 LINQ to Entities 的全部意义在于提供者不这样做,而是将代码表达式转换为 SQL。

因此,您的查询实际上被呈现为 SQL 查询,并且该查询的结果被翻译回来。因此,您在查询中看到的变量e 不一定是真正创建的,而只是用于 LINQ 提供程序编译 SQL 查询。但是,大多数数据库服务器都有空传播。

对 LINQ to Objects 运行相同的查询,您将得到丢失的 NullReferenceException

【讨论】:

  • 对可空值进行空值检查是否仍被认为是一种好习惯,或者在这种情况下会被认为是多余的?
  • @Sudsy1002 实际上,许多数据库允许配置是否传播空值,因此在上面,您的代码将取决于该数据库配置。因此,是的,空检查是一件好事,以防您更改 LINQ 提供程序或更改数据库配置。
【解决方案2】:

您的更新帮助我了解了您真正关心的问题。关于 LINQ 查询,您需要了解的概念是 deferred execution in LINQ

请通过以下链接了解更多详情:

What are the benefits of a Deferred Execution in LINQ?

Linq - What is the quickest way to find out deferred execution or not?

现在你的情况会怎样?您已将查询存储在 location 变量中。该特定步骤只是初始化部分。它并没有真正通过您的 ORM 层对您的数据库执行查询。这就是您可以测试的方式。

在使用 LINQ 查询初始化 location 变量的代码行上放置一个断点。当调试器在 Visual Studio 中停止时,转到 SQL Server Management Studio (SSMS) 并启动 SQL Server Profiler 会话。

现在在 Visual Studio 中按 F10 跳过代码语句。此时,您将在分析器会话中看到绝对没有查询执行,如下所示:

这都是因为 LINQ 查询直到此时才被执行。

现在你到达下面的代码行:

foreach (var item in location)
{
    var p = item.EquipmentClass.EquipmentType.Name;
}

在您进入 foreach 循环的那一刻,LINQ 查询被触发,您将在 SQL Server 探查器中看到相应的登录跟踪会话。因此,除非枚举 LINQ 查询,否则它不会被触发。这称为延迟执行,即运行时延迟执行直到枚举。如果您从未在代码中枚举 location 变量,那么查询执行将永远不会发生。

所以要回答您的查询,只有在查询被触发时才会出现异常。不是在那之前!

更新 1:您是说 - 我没有收到空引用异常。是的!您将不会获得空引用异常,直到您到达缺少相应 RHS 连接记录的记录。请查看以下代码以更好地理解:

class Program
{
    static void Main(string[] args)
    {
        var mylist1 = new List<MyClass1>();
        mylist1.Add(new MyClass1 { id = 1, Name1 = "1" });
        mylist1.Add(new MyClass1 { id = 2, Name1 = "2" });

        var mylist2 = new List<MyClass2>();
        mylist2.Add(new MyClass2 { id = 1, Name2 = "1" });

        var location = from l in mylist1
                       join e in mylist2 on l.id equals e.id into rs1
                       from e in rs1.DefaultIfEmpty()
                       //where ids.Contains(l.ID)
                       select new
                       {
                           EquipmentClass = e,
                           InServiceStatus = e == null ? 1 : e.id,
                           EquipmentType = e.id
                       };

        foreach (var item in location)
        {

        }
    }
}

class MyClass1
{
    public int id { get; set; }
    public string Name1 { get; set; }
}

class MyClass2
{
    public int id { get; set; }

    public string Name2 { get; set; }
}

所以,现在当我开始迭代 location 变量时,它不会在第一次迭代中中断。它在第二次迭代中中断。当它无法获取对应于MyClass1 对象的记录/对象时,它会中断,而id 2 存在于mylist1 中。 mylist2 没有任何带有id 的对象 2. 希望对您有所帮助!

【讨论】:

  • 是的,我知道查询在被枚举之前不会执行,这不是我关心的问题。我没有收到空引用错误,即使枚举了循环。我专门在上面的代码中输入了我在分配 p 时得到的错误,不是查询的枚举。如果我在示例中删除带有变量 p 的行,我根本不会遇到异常。
  • 对我来说听起来不可能,但我会尽力解决您的问题。请分享EquipmentClass类的定义。
  • 我的代码不会在任何迭代中中断,我通过整个 foreach 循环没有问题。我知道你在说什么,我明白。我是说在我的情况下发生了完全不同的事情。也许它特定于 LINQ to SQL/Entities。我已经用指向类似问题的链接更新了我的帖子。在该链接的答案中,它似乎表明 LINQ 正在操纵数据在后台的分配方式,显然不会导致您在使用对象时所期望的空引用异常。
【解决方案3】:

您的e.EquipmentType.Namenull,它被分配给EquipmentType,将null 分配给nullable 类型非常好,您正在使用DefaultIfEmpty(),它将使用它们的元素初始化元素如果它无法匹配任何条件,则默认值,在这种情况下,您的 e.ElementType.Name 被设置为 null 我猜这很好。使用ToList() 会抛出异常。

我希望我说的有些道理,你们可能已经讨论过这个问题。

【讨论】:

  • DefaultIfEmpty 不允许 e.EquipmentType.Name 为空值。如果我使用 LINQ to Objects,相同的语句将产生空引用异常。此外,我的目标不是获得异常,我想知道为什么我没有获得异常。
猜你喜欢
  • 1970-01-01
  • 2019-02-09
  • 1970-01-01
  • 2019-06-11
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-03-12
相关资源
最近更新 更多