为什么我没有收到空引用异常?

3

我正在使用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循环开始时都是null。 更新2: 我在这个链接中找到了一个与我使用LINQ to SQL几乎完全相同的问题。我理解了答案的要点,但不完全理解它对我上面提出的两个问题可能产生的影响。

我怀疑这是不可能的。如果没有进行空值检查,代码“EquipmentType = e.EquipmentType.Name”也会失败。你确定你的观察是正确的吗?我在我的端上尝试了一个快速的代码示例,它如预期一样失败了。 - RBT
@RBT 实际上,运行我的代码并没有产生任何错误,它愉快地运行了 EquipmentType = e.EquipmentType.Name 并将所有值都设置为 null(即 e = null,e.EquipmentType = null 和 e.EquipmentType.Name = null)。 - Sudsy1002
我认为这是类似的问题:https://stackoverflow.com/a/13148126 - kapsiR
显示剩余3条评论
3个回答

5
如果您针对 IQueryable 编写 LINQ 查询,那么在幕后调用的方法(例如 SelectWhere 等)不会执行任何操作,只记录您的调用方式,即记录谓词表达式并携带 LINQ 提供程序。一旦开始迭代查询,将要求提供程序执行查询模型。因此,提供程序使用表达式模型为您提供预期类型的结果。 提供程序不一定需要实际编译或执行您作为表达式提供的代码(模型)。实际上,LINQ to SQL 或 LINQ to Entities 的整个目的就是使提供程序不这样做,而是将代码表达式转换为 SQL。 因此,您的查询实际上被呈现为 SQL 查询,并将该查询的结果转换回来。因此,您在查询中看到的变量 e 不一定真正创建,而仅用于 LINQ 提供程序编译 SQL 查询。但是,大多数数据库服务器都具有空值传播。 对 LINQ to Objects 运行相同的查询,您将得到缺少的 NullReferenceException。

1
在可空值上进行空值检查是否仍然被认为是良好的实践,还是在这种情况下被认为是多余的? - Sudsy1002
1
@Sudsy1002 实际上,许多数据库允许配置是否传播空值,因此在上述情况下,您的代码将取决于该数据库配置。因此,是的,进行空值检查是一个好习惯,以防您更改LINQ提供程序或更改数据库配置。 - Georg

1
你的更新帮助我了解了你真正关心的问题。与LINQ查询相关的概念是延迟执行
请查看以下链接以获取更多详细信息: LINQ中延迟执行的好处是什么? Linq - 查找延迟执行或非延迟执行的最快方法是什么? 现在,在你的情况下会发生什么?你已经将查询存储在location变量中。这一步只是初始化部分。它并没有通过ORM层在数据库上执行查询。你可以这样测试。
在你使用LINQ查询初始化location变量的代码行上设置一个断点。当调试器在Visual Studio中停止时,然后打开SQL Server Management Studio(SSMS)并启动SQL Server Profiler会话。
现在在 Visual Studio 中按下 F10 以跨过代码语句。此时,在分析器会话中您将看不到任何查询执行,如下所示:

enter image description here

那是因为直到此时,LINQ 查询并没有被执行。现在你已经到达了以下代码行:
foreach (var item in location)
{
    var p = item.EquipmentClass.EquipmentType.Name;
}

进入foreach循环后,LINQ查询将被触发并且您将在SQL Server分析器中的跟踪会话中看到相应的日志。因此,除非被枚举,否则不会触发LINQ查询。这被称为延迟执行,即运行时将执行推迟到枚举时。如果您从未在代码中枚举location变量,则根本不会发生查询执行。
因此,回答您的查询,只有在查询触发时才会出现异常。在此之前不会出现任何异常!
更新1:您说-我没有得到null引用异常。是的!直到您到达对应RHS加入记录缺失的记录时才会得到null引用异常。请查看下面的代码以获得更好的理解:
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变量时,它不会在第一次迭代中停止。它会在第二次迭代中停止。当它无法获取与mylist1中具有id 2的MyClass1对象对应的记录/对象时,它会停止。 mylist2中没有任何带有id 2的对象。希望这可以帮助!


是的,我知道查询在枚举之前不会执行,但这不是我的问题。即使循环被枚举,我也没有得到空引用错误。我特意放置了上面的代码以显示我在p的赋值上出现了错误,而不是在查询的枚举上。如果我从示例中删除变量p的那一行,我将根本不会得到异常。 - Sudsy1002
听起来对我来说有些不可能,但我会尝试解决您的问题。请分享“EquipmentClass”类的定义。 - RBT
我的代码在任何迭代中都不会出错,我可以顺利通过整个foreach循环。我知道你在说什么,我也理解。但我想说的是,在我的情况下发生了完全不同的事情。也许这与LINQ to SQL/Entities有关。我已经在我的帖子中更新了一个类似问题的链接。在那个链接的答案中,似乎表明LINQ正在操作数据如何在幕后分配,显然以一种不会导致空引用异常的方式进行,而这是使用对象时所期望的。 - Sudsy1002

-1

你的 e.EquipmentType.Namenull,并且被赋值给了 EquipmentType。将 null 赋值给一个可空类型是完全可以的。你正在使用 DefaultIfEmpty(),如果它无法匹配任何条件,则会使用默认值初始化元素,在这种情况下,你的 e.ElementType.Name 被设置为 null,这也没问题。使用 ToList() 将会抛出异常。

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


DefaultIfEmpty并不能使e.EquipmentType.Name成为null值。如果我使用LINQ to Objects,相同的语句会产生空引用异常。我的目标也不是要得到这个异常,我想知道为什么我没有得到这个异常。 - Sudsy1002

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