C#空值合并运算符返回null

10

最近我的同事给我展示了一段代码,但它并没有正常工作:

public class SomeClass
{
    private IList<Category> _categories;

    public void SetCategories()
    {
        _categories = GetCategories() ?? new List<Category>();
        DoSomethingElse();
    }

    public IList<Category> GetCategories()
    {
        return RetrieveCategories().Select(Something).ToList();
    }
}

(我知道这个操作符是多余的,因为linq ToList将始终返回一个列表,但这就是代码的设置方式)。

问题在于_categories为空。在调试器中,在_categories = GetCategories() ?? new List<Category>()上设置断点,然后跳到DoSomethingElse()时,_categories仍将为空。

直接将_categories设置为GetCategories()可以正常工作。 将??拆分为完整的if语句也可以正常工作。空合并运算符不行。

这是一个ASP.NET应用程序,因此可能有不同的线程干扰,但它在他的计算机上,只有他连接在浏览器中。 _cateogories不是静态或其他任何东西。

我想知道的是,这怎么可能发生?

编辑:

只是为了增加奇异性,_categories从未在该函数之外的任何地方设置(除了初始化类)。

确切的代码如下:

public class CategoryListControl
{
    private ICategoryRepository _repo;
    private IList<Category> _categories;

    public override string Render(/* args */)
    {
        _repo = ServiceLocator.Get<ICategoryRepository>();
        Category category = _repo.FindByUrl(url);
        _categories = _repo.GetChildren(category) ?? new List<Category>();
        Render(/* Some other rendering stuff */);
    }
}

public class CategoryRepository : ICategoryRepository
{
    private static IList<Category> _categories;

    public IList<Category> GetChildren(Category parent)
    {
        return _categories.Where(c => c.Parent == parent).ToList<Category>();
    }
}

即使GetChildren返回null,CategoryListControl._categories也绝对不应该为null。GetChildren也永远不应该因为IEnumerable.ToList()返回null。

编辑2:

尝试@smartcaveman的代码后,我发现了这个问题:

Category category = _repo.FindByUrl(url);

_categories = _repo.GetChildren(category) ?? new List<Category>();

_skins = skin; // When the debugger is here, _categories is null

Renderer.Render(output, _skins.Content, WriteContent); // When the debugger is here, _categories is fine.

同时,当测试if(_categories == null) throw new Exception()时,如果在if语句中_categories为空,那么跳过异常不会被抛出。

所以,看起来这是一个调试器的bug。


2
这里有趣的部分是GetCategories()无论如何都不能返回null。因此,合并运算符也没有用处。 - Pieter van Ginkel
2
@Pieter:是的,他知道。 - BoltClock
你确定在调用 ServiceLocator 时获取到了具体实现吗?只是确认一下,因为这是有时候容易被忽略的事情之一(就像在 Release 模式下尝试设置断点 :))。 - Skurmedel
@Skurmedel,我们能够在调试器中进入它,所以我相信是这样的。 - Snea
既然这是两年前的事了,你可能不记得了,但是你是否碰巧是在使用 ReSharper 单元测试运行器来运行代码?我刚遇到了非常相似的情况,再次出现了空合并操作符的问题,甚至可以修复代码,然后再次崩溃。之后我退出了 VS 并在 StackOverflow 上发布了帖子,但是之后我就无法再现这个问题了。请参见 https://dev59.com/B2Ik5IYBdhLWcg3wWtBz - Tim Long
显示剩余5条评论
6个回答

2

这可能是调试器问题,而不是代码的问题。尝试在使用合并运算符的语句后打印值或进行空值检查。


我认为程序一开始应该是抛出了 NullReferenceException 异常,这导致他附加了调试器并看到了问题。虽然我认为这仍然是可能的。 - Snea
我猜我错认为抛出了NullReferenceException。检查null将返回该值不为null,即使调试器直接位于if语句上时,它显示_categories为空。 - Snea

2

空值合并运算符并没有出现问题。我经常使用类似的方式,并且非常成功。可能是其他问题导致了错误。


1
如果你确信这是线程问题,那么请使用 lock 关键字。我相信这样应该可以解决。
public class SomeClass
{
    private IList<Category> _categories;

    public void SetCategories()
    {
        lock(this) 
        {
          _categories = GetCategories() ?? new List<Category>();
          DoSomethingElse();
        }
    }

    public IList<Category> GetCategories()
    {
        return RetrieveCategories().Select(Something).ToList();
    }
}

1

(1) 在错误出现之前,DoSomethingElse() 可能会将 _categories 字段设置为 null。测试的方法是将 _categories 字段设置为只读。如果这是错误原因,则会收到编译器错误,指出只读字段不能用作赋值目标。
(2) _categories 字段是通过不同线程中的某些其他函数进行设置的。无论哪种方式,以下操作应该可以解决您的问题,或者至少让问题变得清晰明了。

public class SomeClass
{
    private static readonly object CategoryListLock = new object();
    private readonly List<Category> _categories = new List<Category>();
    private bool _loaded = false;

    public void SetCategories()
    {
        if(!_loaded)
        {
            lock(CategoryListLock)
            {
                if(!_loaded)
                {
                    _categories.AddRange(GetCategories());
                    _loaded = true;
                }
            }
        }
        DoSomethingElse();
    }

    public IList<Category> GetCategories()
    {
        return RetrieveCategories().Select(Something).ToList();
    }
}

在看到您的编辑后,似乎您有两个不同的字段是 IList<Category> _categories。在 CategoryListControl 中的 _categories 字段为空并不合理,但根据您发布的内容,CategoryRepository 类中的静态 _categories 应该为空。也许您对引发错误的字段感到困惑。我知道该行代码是在 CategoryListControl 中调用的,因此您的错误将说它在 CategoryListControl 类中,但实际异常可能来自 GetChildren() 方法,该方法试图从空列表中创建子列表。由于这些字段具有相同的名称,很容易混淆。通过将 CategoryRepository 中的 _categories 字段设置为只读初始化字段来测试这一点。

即使 CategoryRepository 中的 _categories 字段并不总是为空,它也可能受到我解释如何修复 Control 类的任何线程问题的影响。

为确保您正在调试正确的 _categories 字段,请尝试以下操作。

    _categories = GetCategories() ?? new List<Category>();
    if(_categories == null){
          throw new Exception("WTF???");
     }
    DoSomethingElse();

如果你没有收到"WTF???"的异常,那么你就知道错误的源头在其他地方。

关于Linq扩展:无论是Where()还是ToList()都不会返回null。如果任何参数为null,这两种方法都会抛出ArgumentNullException异常。我用反射检查了一下。

请告诉我们你得到了什么结果。我现在也很好奇。


不幸的是,_categories 只能从该函数中设置。 - Snea
@smartcaveman,我非常确定是CategoryListControl中的_categories,并且由于此原因引发了异常。在调试器中,我们可以在没有异常的情况下跨越空合并运算符行,而在下一行CategoryListControl._categories为空。如果CategoryRepository._cateogries为空,那么Where()中的空合并运算符行将抛出异常,因为输入不能为空。无论如何,GetCategories中的ToList都不会返回null,对吧? - Snea
@smartcaveman,检查一下修改。尝试你的代码似乎表明存在调试器问题。我接受了Tim H的答案,因为从技术上讲,我认为这就是问题所在,但如果可以的话,我也会接受你的答案。谢谢。 - Snea
Visual Studio 2010 连接到 w3wp.exe (iis7)。 - Snea

1

尝试进行全新的构建。转到“Build”菜单,然后选择“Clean”,再重新进行调试。代码本身没有问题。


1
这可能是因为过期的二进制文件导致的吗?+1 - Pieter van Ginkel
可以实现,但我们已经将代码更改为手动if语句,这样做起作用了,然后再改回来,结果它又停止工作了。过时的二进制文件难道不会让调试器抱怨源代码不同吗?我今天会看看是否能够再次复现这个问题。 - Snea
那么这就不是过时的二进制文件了。我猜你发布的代码是一个简化版 - 还有其他我们需要看到的东西吗? - Chris Shain
@Christ Shain,已经使用实际代码进行了编辑 - 但我仍然不明白它如何影响_categories为空。 - Snea
尝试这样做...将_categories设置为属性,在setter中,使用console.writeline或其他日志记录方式,查找何时设置它,在哪个线程上等。如果您正确地进行调试,则唯一可能的答案是另一个线程正在更新其值。 - Chris Shain

1
这可能是因为您已经开启了优化 - 在这种情况下,赋值操作可能会被延迟,直到编译器可以证明这样做不会改变结果。当然,在调试器中看起来很奇怪,但这是完全正常的。

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