空值合并运算符的影响?

8
一段时间以前,我编译了两个版本的代码,一个使用(Nullable<T>)x.GetValueOrDefault(y),另一个使用(Nullable<T>)x ?? y)。在反编译成IL后,我注意到空合并运算符被转换为GetValueOrDefault调用。由于它是一个方法调用,可以传递表达式,在执行方法之前对其进行评估,因此y似乎总是会被执行。例如:
using System;

public static class TestClass
{
    private class SomeDisposable : IDisposable
    {
        public SomeDisposable()
        {
            // Allocate some native resources
        }

        private void finalize()
        {
            // Free those resources
        }

        ~SomeDisposable()
        {
            finalize();
        }

        public void Dispose()
        {
            finalize();
            GC.SuppressFinalize(this);
        }
    }

    private struct TestStruct
    {
        public readonly SomeDisposable _someDisposable;
        private readonly int _weirdNumber;

        public TestStruct(int weirdNumber)
        {
            _weirdNumber = weirdNumber;
            _someDisposable = new SomeDisposable();
        }
    }

    public static void Main()
    {
        TestStruct? local = new TestStruct(0);

        TestStruct local2 = local ?? new TestStruct(1);

        local2._someDisposable.Dispose();
    }
}

似乎会导致一个不适应的对象,可能还会影响性能。

首先,这是真的吗?或者JIT或类似的东西是否会改变实际执行的ASM代码?

其次,有人能解释一下为什么会出现这种行为吗?

注意:这只是一个例子,不是基于真实代码的,请不要发表“这是糟糕的代码”之类的评论。

IL DASM:
好的,当我用.Net Framework 2.0编译它时,调用null合并和GetValueOrDefault生成了相同的代码。在.Net Framework 4.0中,它生成了这两个代码:

GetValueOrDefault:

.method private hidebysig static void  Main() cil managed
{
  .entrypoint
  // Code size       19 (0x13)
  .maxstack  2
  .locals init ([0] valuetype [mscorlib]System.Nullable`1<int32> nullableInt,
           [1] int32 nonNullableInt)
  IL_0000:  nop
  IL_0001:  ldloca.s   nullableInt
  IL_0003:  initobj    valuetype [mscorlib]System.Nullable`1<int32>
  IL_0009:  ldloca.s   nullableInt
  IL_000b:  ldc.i4.1
  IL_000c:  call       instance !0 valuetype [mscorlib]System.Nullable`1<int32>::GetValueOrDefault(!0)
  IL_0011:  stloc.1
  IL_0012:  ret
} // end of method Program::Main

空值合并运算符:

.method private hidebysig static void  Main() cil managed
{
  .entrypoint
  // Code size       32 (0x20)
  .maxstack  2
  .locals init (valuetype [mscorlib]System.Nullable`1<int32> V_0,
           int32 V_1,
           valuetype [mscorlib]System.Nullable`1<int32> V_2)
  IL_0000:  nop
  IL_0001:  ldloca.s   V_0
  IL_0003:  initobj    valuetype [mscorlib]System.Nullable`1<int32>
  IL_0009:  ldloc.0
  IL_000a:  stloc.2
  IL_000b:  ldloca.s   V_2
  IL_000d:  call       instance bool valuetype [mscorlib]System.Nullable`1<int32>::get_HasValue()
  IL_0012:  brtrue.s   IL_0017
  IL_0014:  ldc.i4.1
  IL_0015:  br.s       IL_001e
  IL_0017:  ldloca.s   V_2
  IL_0019:  call       instance !0 valuetype [mscorlib]System.Nullable`1<int32>::GetValueOrDefault()
  IL_001e:  stloc.1
  IL_001f:  ret
} // end of method Program::Main

事实证明,这种情况不再存在,当HasValue返回false时,它会直接跳过GetValueOrDefault的调用。

如果y是调用一个返回需要被处理的对象的方法,则无论如何,如果x为空,则会存在泄漏问题。 - M.Babcock
@M.Babcock 不是真正的内存泄漏,只是延迟清理内存。看看修改后的示例,希望这能更好地解释问题。 - Aidiakapi
实际上我说错了,如果local不为null并且该方法确实被调用,那么就会出现泄漏,因为结果将从垃圾回收器(GC)中被收集而不被处理。要回答这个问题,即确认该方法是否每次都被调用,你可以在方法中设置断点并运行它。 - M.Babcock
1个回答

6
在反编译为IL后,我注意到空值合并运算符被转换为GetValueOrDefault调用。 x ?? y 被转换为 x.HasValue ? x.GetValueOrDefault() : y。它不会被转换为 x.GetValueOrDefault(y),如果这样做了就是编译器的错误。你是对的,如果x不为空,y不应该被评估,而实际上也没有被评估。 编辑: 如果可以证明y的评估是不带副作用的(其中“副作用”包括“抛出异常”),那么将其转换为x.GetValueOrDefault(y)不一定是错误的,但这仍然是一个我认为编译器不执行的转换:并不是所有情况下都有用的优化。

但是 default(T) 是所有位都为零,就像一个 Enum 一样,即使这可能不是 Enum 的正确值,其默认值始终为 0(请记住此定义:enum Something { Value1 = 1, Value2 = 2 } 默认值:0)。 - Aidiakapi
我不能百分之百确定,但我认为在C++/CLI中,default(T)返回nullptr,因为对于引用类型的default(T)(技术上是自动解引用的指针)返回null,我引用一下:“The nullptr keyword is equivalent to Nothing in Visual Basic and null in C#.” 所以我猜它确实会返回所有位都是零,包括在C++/CLI中。但我从未使用过C++/CLI(虽然我用过Win32 C++和一些不安全的C#),所以我不能保证这一点 :P。 - Aidiakapi
我已经检查过,这就是我知道的原因。对于常规指针而言,所有位都为零表示nullptr。指向成员的指针类型是一种单独的类型,在C#或VB.NET中不存在。它的默认值是所有位都为零,这意味着“在偏移量为零的位置的成员”,而nullptr则有不同的表示方式。 - user743382
实际上,在C++/CLI中,它表示为-1,据我所记,这也是大多数标准C++实现中的表示方式。 - user743382
-1 似乎是最明显的选择,因为在偏移量为 -1 的位置没有内存单元,并且在使用无符号数据类型时,-1 是最大值。对于“魔数”来说似乎是合理的选择。 - Aidiakapi
显示剩余6条评论

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