优化代码会导致Singleton中出现空引用

时间:2019-06-01 17:11:36

标签: c# singleton nullreferenceexception

在我的应用程序中,我有一个静态对象,其中包含另一个对象的实例。我有一个单例,在其构造函数中称为实例持有者。实例所有者的实例是在调用Singleton之前设置的,因此可以正常工作。当我打开优化代码时,就会出现问题。突然,我得到对实例所有者的实例的空引用。

在此示例中,我将Visual Studio 2017与.NET 4.6.1中的控制台应用程序一起使用。我的主要应用是WPF应用,我正在App()构造函数中调用Singleton。

class Program
{
    static void Main(string[] args)
    {
        StringHolder.ImportantString = "Howdy";
        var x = Singleton.Current;
    }
}

public static class StringHolder
{
    public static string ImportantString { get; set; }
}

public class Singleton
{
    public static Singleton Current { get; } = new Singleton();
    private Singleton()
    {
        var x = StringHolder.ImportantString.ToLower(); // Null Reference occurs here when Optimize Code is on.
    }
}

这似乎是由于在Main的第一行被调用之前在 中创建了Singleton引起的。添加Console.WriteLine()调用即可显示这种情况。

一种解决方案是将Main函数分成多个部分:设置ImportantString并调用“ Part2”函数,该函数调用Singleton。

static void Main(string[] args)
{
    StringHolder.ImportantString = "Howdy";
    Part2();
}

public static void Part2()
{
    var x = Singleton.Current;
}

但是,此解决方案不适用于此处的示例代码:它仅适用于我的主项目。我不太确定为什么。

另一种解决方案是更改Singleton.Current的工作方式:

    public static Singleton _current;
    public static Singleton Current => _current ?? (_current = new Singleton());

(这显然可以解决此问题,因为静态属性直到被调用时才会实例化。)

第三个解决方案是添加一个静态构造函数:

    static Singleton() { }

但是,修复我的代码不是我关心的事情。我的担心如下:

  1. 为什么突然打开“优化代码”会使Singleton成员提前创建?
  2. 为什么Part2()技巧仅在我的主应用程序中起作用?
  3. 为什么添加静态构造函数可以解决该错误?

1 个答案:

答案 0 :(得分:1)

添加一些跟踪:

class Program
{
    static void Main(string[] args)
    {
        StringHolder.ImportantString = "Howdy";
        var x = Singleton.Current;
        Console.WriteLine("Done");
    }
}

public class Singleton
{
    public static Singleton Current { get; } = new Singleton();

    private Singleton()
    {
        try
        {

            Console.WriteLine("Calling ctor.");
            var x = StringHolder.ImportantString.ToLower(); // Null Reference occurs here when Optimize Code is on.
            Console.WriteLine("ctor called.");
        }
        catch(Exception e)
        {
            Console.WriteLine($"ctor failed with {e.GetType()}");
        }
    }
}

public static class StringHolder
{
    private static string importantString;

    public static string ImportantString
    {
        get
        {
            Console.WriteLine("Getting ImportantString");
            return importantString;
        }
        set
        {
            Console.WriteLine("Setting ImportantString");
            importantString = value;
            Console.WriteLine("ImportantString set");
        }
    }
}

并以Debug模式运行程序。

输出为:

Setting ImportantString
ImportantString set
Calling ctor.
Getting ImportantString
ctor called.
Done

Release模式运行程序。

输出为:

Calling ctor.
Getting ImportantString
ctor failed with System.NullReferenceException
Setting ImportantString
ImportantString set
Done

{{1}的Debug模式IL为:

Main

{{1}的.method private hidebysig static void Main(string[] args) cil managed { .entrypoint // Code size 30 (0x1e) .maxstack 1 .locals init ([0] class OptimizationIssue.Singleton x) IL_0000: nop IL_0001: ldstr "Howdy" IL_0006: call void OptimizationIssue.StringHolder::set_ImportantString(string) IL_000b: nop IL_000c: call class OptimizationIssue.Singleton OptimizationIssue.Singleton::get_Current() IL_0011: stloc.0 IL_0012: ldstr "Done" IL_0017: call void [mscorlib]System.Console::WriteLine(string) IL_001c: nop IL_001d: ret } // end of method Program::Main 模式Release为:

IL
两者的

Main看起来都差不多。

{{1}的.method private hidebysig static void Main(string[] args) cil managed { .entrypoint // Code size 27 (0x1b) .maxstack 8 IL_0000: ldstr "Howdy" IL_0005: call void OptimizationIssue.StringHolder::set_ImportantString(string) IL_000a: call class OptimizationIssue.Singleton OptimizationIssue.Singleton::get_Current() IL_000f: pop IL_0010: ldstr "Done" IL_0015: call void [mscorlib]System.Console::WriteLine(string) IL_001a: ret } // end of method Program::Main 模式IL为:

Debug

{{1}的disassembly模式Main为:

        {
01A3084A  in          al,dx  
01A3084B  push        edi  
01A3084C  push        esi  
01A3084D  push        ebx  
01A3084E  sub         esp,38h  
01A30851  mov         esi,ecx  
01A30853  lea         edi,[ebp-44h]  
01A30856  mov         ecx,0Eh  
01A3085B  xor         eax,eax  
01A3085D  rep stos    dword ptr es:[edi]  
01A3085F  mov         ecx,esi  
01A30861  mov         dword ptr [ebp-3Ch],ecx  
01A30864  cmp         dword ptr ds:[16042E8h],0  
01A3086B  je          01A30872  
01A3086D  call        7247F5A0  
01A30872  xor         edx,edx  
01A30874  mov         dword ptr [ebp-40h],edx  
01A30877  nop  
            StringHolder.ImportantString = "Howdy";
01A30878  mov         ecx,dword ptr ds:[4402334h]  
01A3087E  call        01A30458  
01A30883  nop  
            var x = Singleton.Current;
01A30884  call        01A30468  
01A30889  mov         dword ptr [ebp-44h],eax  
01A3088C  mov         eax,dword ptr [ebp-44h]  
01A3088F  mov         dword ptr [ebp-40h],eax  
            Console.WriteLine("Done");
01A30892  mov         ecx,dword ptr ds:[4402338h]  
01A30898  call        70DD3CD4  
01A3089D  nop  
        }
01A3089E  nop  
01A3089F  lea         esp,[ebp-0Ch]  
01A308A2  pop         ebx  
01A308A3  pop         esi  
01A308A4  pop         edi  
01A308A5  pop         ebp  
01A308A6  ret  

Release的差异很大。 disassembly有所作为。似乎Main删除了未使用的变量。但是它仍然会创建类型 StringHolder.ImportantString = "Howdy"; 00D51072 in al,dx 00D51073 mov ecx,dword ptr ds:[3A32344h] 00D51079 call dword ptr ds:[0BF4DF4h] Console.WriteLine("Done"); 00D5107F mov ecx,dword ptr ds:[3A32348h] 00D51085 call 70DD3CD4 00D5108A pop ebp 00D5108B ret ,在执行disassembly方法之前调用其静态构造函数。由于代码中的JIT-compilation,隐式创建了静态构造函数。尚未设置JIT-compilation时,它为null,因此尝试在其上调用OptimizationIssue.Singleton时会抛出Main

public static Singleton Current { get; } = new Singleton();移除StringHolder.ImportantString并查看NullReferenceException

ToLower()

它变化不大。我们手动删除了编译器自动删除的内容。但是不再提到var x = Singleton.Current;类型,因此不会调用静态构造函数,因此也不例外。

Addig Maindisassembly更改为:

            StringHolder.ImportantString = "Howdy";
00FE084A  in          al,dx  
00FE084B  mov         ecx,dword ptr ds:[3BF2334h]  
00FE0851  call        dword ptr ds:[0CC4DF4h]  
            Console.WriteLine("Done");
00FE0857  mov         ecx,dword ptr ds:[3BF2338h]  
00FE085D  call        70DD3CD4  
00FE0862  pop         ebp  
00FE0863  ret  

现在,由于某些原因,它不会删除Singleton,而是在设置static Singleton() { }之后,仅在执行该行之前调用disassembly静态构造函数,因此也不例外。

enter image description here

这是优化 StringHolder.ImportantString = "Howdy"; 0169084A in al,dx 0169084B mov ecx,dword ptr ds:[41F2334h] 01690851 call dword ptr ds:[1334DF4h] var x = Singleton.Current; 01690857 call dword ptr ds:[1334E60h] Console.WriteLine("Done"); 0169085D mov ecx,dword ptr ds:[41F2338h] 01690863 call 70DD3CD4 01690868 pop ebp 01690869 ret 的魔力。不要依赖它从var x = Singleton.Current;中删除Singleton,最好将StringHolder.ImportantString添加到JIT-compilation。 (或者更好不要创建您从不使用的对象。)

然后输出为:

static Singleton() { }

Singleton是:

[MethodImpl(MethodImplOptions.NoOptimization)]

一切正常。

故事的寓意:具有{strong>优化的Main充满了内联,删除和许多其他难以预料的东西,并且能够改变您的代码行为以一种奇怪的方式。