【问题标题】:Totally unexplainable NullReferenceException完全无法解释的 NullReferenceException
【发布时间】:2012-06-13 19:52:11
【问题描述】:

好的,所以我在这行代码中偶尔会收到 NullReferenceException:

if (!_oracleTenantSettings.OraclePlanSettings.ContainsKey(_key) || _oracleTenantSettings.OraclePlanSettings[_key] == null)

和/或这一行:

_oraclePlanSettings = _oracleTenantSettings.OraclePlanSettings[_key];

OraclePlanSettings 是一个 SortedList,它不能为空,因为有问题的代码被包围:

 if (_oracleTenantSettings.OraclePlanSettings != null && _oracleTenantSettings.OraclePlanSettings.Count > 0)

所以我得到了一个 NRE,但整个代码行中没有一个部分可能永远为空。时期。 (感觉到挫败感?)这包括密钥,但无论如何都不会抛出 NRE。我不明白。 VS可能只是放错了CLR异常吗?如果是这样,哪里是开始寻找的好地方?

堆栈跟踪只是一条线:

 at company.product.Mvc.OracleSettingsStoreCache.VerifyValueInCacheOrInsert[T](T& returnVal, SettingsType settingType, String tenantId, String planId, String pageMnemonic, String processId, String transcationType, String language, String country, String wapTransactionType, String wapCodeGroup, String wapLoanReasons, String palleteType, Boolean isInsert, Object _cacheValue) in blahblahblah.OracleSettingsStoreCache.cs:line 290

这是整个代码块:

if (!string.IsNullOrEmpty(tenantId) && (!IsWacMode() || (IsWacMode() && settingType == OracleSettingsType.SettingsType.FetchWAPInvestmentTransfer)) && _useCache != "false")
                {
                    tenantId = tenantId.ToUpper().Trim();

                    _oracleTenantSettings = null;

                    if (_oracleCacheManager.Contains(_cacheKey))
                        _oracleTenantSettings = _oracleCacheManager.Get<OracleTenantSetting>(_cacheKey);

                    if (_oracleTenantSettings != null)
                    {
                        if (_oracleTenantSettings.OraclePlanSettings != null && _oracleTenantSettings.OraclePlanSettings.Count > 0)
                        {
                            _key = language + "_" + country + "_" + tenantId;
           ***LINE 290***   if (!_oracleTenantSettings.OraclePlanSettings.ContainsKey(_key) || _oracleTenantSettings.OraclePlanSettings[_key] == null)
                            {
                                _objectMissing = TypeOfObjectMissing.TenantObjectDoesNotExist;
                            }
                        }

【问题讨论】:

  • 您是否在调试器中查看过究竟是什么引发了 NRE?异常可能发生在 OraclePlanSettings 属性 getter 内部,它只是在您的代码中浮出水面。
  • 你确定那是实际异常的代码行吗?还是堆栈跟踪中的第一行 user 代码?
  • key = language + "" + country + "_" + tenantId;其中所有三个都是包含所有这些代码的方法的输入参数。所以如果那是问题,它会在方法调用上爆炸
  • 非常偶尔?这是一个可以被多个线程访问的静态集合吗?像这样的莫名其妙的零星错误通常是线程问题,在测试和访问之间发生变化。
  • RE:“那是 VS 在调试期间捕获 CLR 异常的那一行。但当然堆栈跟踪会说同样的事情”——不,异常堆栈的顶部不一定与视觉工作室报告异常的行相同。检查堆栈。

标签: c# .net exception clr nullreferenceexception


【解决方案1】:

如果没有看到代码所在的上下文,就很难确定。但是根据您描述的症状,即非常零星的......莫名其妙的......不可能的东西是空的......我强烈怀疑是线程问题。例如,如果集合是静态的并且可能被多个线程访问,则可能会发生(尽管这是一种罕见的机会时机)第二个线程在第一个线程测试是否存在某些东西和它访问之间修改集合内容那东西。

如果是这种情况,您必须使您的代码更加线程安全。您可以使用锁定或并发集合来避免此问题。要使用锁,您需要使用同步对象(而不是动态创建的新对象)。您还希望搜索访问该集合的所有位置,并用锁围绕每个位置...仅查看该集合的代码必须使用锁以及修改该集合的代码。这是一个 SO 答案的大话题,所以我建议您使用这个非常棒的资源:

http://www.albahari.com/threading/

在这种情况下,您可以通过以下方式获得 NRE:

thread 1 checks if entry exists in SortedList myList for _key="hello" 
gets true
thread 1 checks if entry for _key="hello" is non-null
gets true
thread 2 sets myList["hello"] = null
thread 1 executes myList["hello"].Something() and gets NRE.

根据对您帖子的编辑,这些行似乎是

if (_oracleTenantSettings != null) {
    if (_oracleTenantSettings.OraclePlanSettings != null && _oracleTenantSettings.OraclePlanSettings.Count > 0) {
        _key = language + "_" + country + "_" + tenantId;
         if (!_oracleTenantSettings.OraclePlanSettings.ContainsKey(_key) || _oracleTenantSettings.OraclePlanSettings[_key] == null)

如果 NRE 发生在最后一行,那么在执行第一行或第二行之后,另一个线程可以将 _oracleTenantSettings_oracleTenantSettings.OraclePlanSettings 设置为 null。发生这些事情中的任何一个都会导致最后一行抛出 NRE。

以下代码不是使您的代码线程安全的正确方法,但可以作为一种快速方法来查看是否确实如此,因为它会降低这种情况(空引用异常)的可能性:

var oracleTS = _oracleTenantSettings;
if (oracleTS != null) {
    var planSettings = oracleTS.OraclePlanSettings;
    if ((planSettings != null) && (planSettings.Count > 0)) {
        _key = language + "_" + country + "_" + tenantId;
        if (!planSettings.ContainsKey(_key) || planSettings[_key] == null)

请注意,最后一行可能仍然存在与线程相关的其他问题,例如在条件的第一部分和第二部分之间被另一个线程删除的键,或者在测试后 planSettings 计数发生变化。但是,如果这段代码大大减少了 NRE,那么您就可以很好地了解发生了什么,并且您应该仔细检查并适当地使您的代码线程安全,并在需要的地方使用锁。进一步说,人们需要更多地了解其他代码在做什么,尤其是修改 _oracleTenantSettings 的代码。

【讨论】:

  • 但这会在myList["hello"].Something() 行上爆炸,不是吗?
  • @PhillipSchmidt - 对..当您尝试使用 myList 中的空值执行 Something() 时,您会得到 NRE。
  • 对,异常发生在检查ContainsKey 或检索实际值时,不是尝试使用结果。这就是为什么我认为这不是线程问题。
  • 仅当有人使用 dict[""].DoSomething() 时才不是 if obj = dict[""];obj!=null?obj.DoSomething():DoNothing();
  • @CodingGorilla 我认为这是一个线程问题,但不是他认为的那样。我的猜测是——考虑以下场景:线程 A 检查 _oracleTenantSettings 是否为空。它不是,所以它继续。然后线程 B 进入并点击 _oracleTenantSettings = null 行。然后线程 A 尝试访问 _oracleTenantSettings,认为它不可能为空,因为它刚刚检查过。但它是,因为线程 B 取消了它。似乎有可能,对吧?
【解决方案2】:

我的猜测是有另一个线程在访问该属性。

修复它的一种快速方法是锁定它每次访问它时,如下所示:

var oraclePlanSettings = _oracleTenantSettings.OraclePlanSettings;
lock (oraclePlanSettings)
{
    // from now on you can safely access your cached reference "oraclePlanSettings"
    if (oraclePlanSettings != null && oraclePlanSettings.Count > 0)
        _oraclePlanSettings = oraclePlanSettings[_key]; // ... blabla
}

小心死锁。

【讨论】:

    【解决方案3】:

    我同意前面的答案,这可能是线程问题,但我有一些要补充的。

    这是一个快速而肮脏的测试,以确定它是否是线程。

    设置一个重现错误的场景(比如在一夜之间以恒定循环的几个 (10) 线程中运行它)

    将此属性应用于您的班级

    [同步]

    您的类必须继承自 ContextBoundObject。

    这会强制类的所有实例在单个线程上运行(速度较慢)。

    重新运行测试。

    如果您的问题消失了,则说明您遇到了线程问题。如果速度是一个问题,您需要返回并围绕接触该对象的代码进行所有锁定。如果您将所有内容转换为使用相关对象的属性,则只需锁定 getter 和 setter。

    如果快速而肮脏的测试未能解决问题,则可能是其他问题。例如,如果您使用不安全的代码或不安全的 dll(即用非 .Net c++ 编写的东西),则可能是内存损坏问题。

    希望这会有所帮助。

    这里是关于属性的更多细节,包括从 ContextBoundObject 继承。

    Ms Docs

    代码示例:

    // Context-bound type with the Synchronization context attribute.
    [Synchronization()]
    public class SampleSynchronized : ContextBoundObject {
    
        // A method that does some work, and returns the square of the given number.
        public int Square(int i)  {
    
            Console.Write("The hash of the thread executing ");
            Console.WriteLine("SampleSynchronized.Square is: {0}", 
                                 Thread.CurrentThread.GetHashCode());
            return i*i;
        }
    }
    

    【讨论】:

    • 很好的答案 - 我会尽快研究一下
    • 请注意,如果您是新手,那么做所有的“锁定”工作并非易事。您必须锁定使用相关资源的每一段代码。如果您错过了一个,您仍然可以获得“线程竞争条件”。封装得越多,关注点分离得越好,这就越容易。另一个好的设计得到回报而糟糕的设计受到惩罚的地方:)
    • 值得注意的是,SynchronizationAttribute 涵盖了实例字段和实例方法,但不包括静态字段和静态方法。 msdn.microsoft.com/en-us/library/z8chs7ft%28v=vs.71%29.aspx
    • 不错!不知道那也不同步全局字段。好收获!
    【解决方案4】:

    更新

    我建议在代码中的某处,Equality 运算符或 == 已在相关对象之一上重载,并且当未正确检查 null (或失败)或返回相等时发生故障当它不是时。

    为在这种情况下使用的任何类对象检查 == 上的所有运算符重载并纠正..

    原创

    把逻辑改成这个,因为你想先检查没有键...然后..当有一个有效的键但()它的值为空时进行检查:

     if ((_oracleTenantSettings.OraclePlanSettings.ContainsKey(_key) == false) || 
         ((_oracleTenantSettings.OraclePlanSettings.ContainsKey(_key)) && 
           _oracleTenantSettings.OraclePlanSettings[_key] == null)))
    

    如果您仔细考虑原始语句的逻辑流程,它实际上会出现间歇性失败的原因。 :-)

    编辑: 让我解释一下,按照这个逻辑逐步进行

    1. 在原始 if 子句中,当它评估 (!ContainsKey(_key)) 时,意味着当密钥不存在 (true) 时,它会更改为 FALSE。
    2. 然后,由于#1 中的 False,Or 开始起作用。它评估 OraclePlanSettings[_key] BUT 关键不在那里吗?
    3. 因此它执行代码来检查 null 是否存在无效键并抛出异常。

    只有打破我展示的逻辑,才不会抛出异常。

    【讨论】:

    • 我也不确定。如果 if 的第一边通过(意味着它确实有密钥),那么检查 if 后半部分中的值将始终有效——无需验证密钥是否存在
    • @PhillipSchmidt 在两种情况下,代码需要报告缺少 TenantObjectDoesNotExist。第一种是计划设置没有键(ContainsKey == false)。第二种情况是当有一个有效的键(ContainsKey == true)并且包含的​​值为空时。通过将这两个单独的条件组合在一起,它为我们提供了在 if 的主体中报告 TenantObjectDoesNotExist 的逻辑。
    • 对,所以有两种情况:如果 ContainsKey==false,则不执行 if 的后半部分。如果不为假,则为真,即 (_oracleTenantSettings.OraclePlanSettings.ContainsKey(_key)) 为真,因此检查 AND _oracleTenantSettings.OraclePlanSettings[_key] == null 得到与仅检查 _oracleTenantSettings.OraclePlanSettings 相同的结果[_key] == null
    • @PhillipSchmidt 查看我的编辑并告诉我是否有意义。谢谢。
    • @PhillipSchmidt 不等...我想我一定是误解了它。 DOH!
    猜你喜欢
    • 1970-01-01
    • 2013-07-09
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2022-01-04
    • 1970-01-01
    • 2011-05-29
    • 1970-01-01
    相关资源
    最近更新 更多