Почему универсальный метод с ограничением класса T: приводит к боксу?

Почему универсальный метод, который ограничивает T для класса, будет иметь инструкции по боксу в генерирующем коде MSIL?

Я был весьма удивлен этим, так как, конечно, поскольку T ограничен ссылочным типом, сгенерированный код не должен выполнять какую-либо упаковку.

Вот код С#:

protected void SetRefProperty<T>(ref T propertyBackingField, T newValue) where T : class
{
    bool isDifferent = false;

    // for reference types, we use a simple reference equality check to determine
    // whether the values are 'equal'.  We do not use an equality comparer as these are often
    // unreliable indicators of equality, AND because value equivalence does NOT indicate
    // that we should share a reference type since it may be a mutable.

    if (propertyBackingField != newValue)
    {
        isDifferent = true;
    }
}

Вот сгенерированный IL:

.method family hidebysig instance void SetRefProperty<class T>(!!T& propertyBackingField, !!T newValue) cil managed
{
    .maxstack 2
    .locals init (
        [0] bool isDifferent,
        [1] bool CS$4$0000)
    L_0000: nop 
    L_0001: ldc.i4.0 
    L_0002: stloc.0 
    L_0003: ldarg.1 
    L_0004: ldobj !!T
    L_0009: box !!T
    L_000e: ldarg.2 
    L_000f: box !!T
    L_0014: ceq 
    L_0016: stloc.1 
    L_0017: ldloc.1 
    L_0018: brtrue.s L_001e
    L_001a: nop 
    L_001b: ldc.i4.1 
    L_001c: stloc.0 
    L_001d: nop 
    L_001e: ret 
}

Обратите внимание на коробку! T инструкции.

Почему это генерируется?

Как этого избежать?

Ответ 1

Вам не нужно беспокоиться о каких-либо ухудшениях производительности от команды box, потому что если его аргумент является ссылочным типом, команда box ничего не делает. Хотя все еще странно, что команда box даже была создана (возможно, ленивость/более простой дизайн при генерации кода?).

Ответ 2

Я не уверен, почему какой-то бокс идет. Один из возможных способов избежать бокса - не использовать его. Просто перекомпилируйте без бокса. Пример:

.assembly recomp_srp
{
    .ver 1:0:0:0
}

.class public auto ansi FixedPBF
{

.method public instance void .ctor() cil managed
{

}

.method hidebysig public instance void SetRefProperty<class T>(!!T& propertyBackingField, !!T newValue) cil managed
{
    .maxstack 2    
        .locals init ( bool isDifferent, bool CS$4$0000)

        ldc.i4.0
        stloc.0
        ldarg.1
        ldobj !!T
        ldarg.2
        ceq
        stloc.1
        ldloc.1
        brtrue.s L_0001
        ldc.i4.1
        stloc.0
        L_0001: ret

}

}

... если вы сохраняете файл recomp_srp.msil, вы можете просто перекомпилировать его как таковую:

ildasm/dll recomp_srp.msil

И он работает нормально без бокса на моем конце:

        FixedPBF TestFixedPBF = new FixedPBF();

        TestFixedPBF.SetRefProperty<string>(ref TestField, "test2");

... конечно, я изменил его с защищенного на публичный, вам нужно будет снова внести изменения и предоставить остальную часть вашей реализации.

Ответ 3

Я считаю, что это предусмотрено дизайном. Вы не ограничиваете T конкретным классом, чтобы он, скорее всего, сбрасывал его на объект. Следовательно, почему вы видите, что IL включает бокс.

Я бы попробовал этот код, где T: ActualClass

Ответ 4

Вслед за парой пунктов. Прежде всего, эта ошибка возникает для обоих методов в универсальном классе с ограничением, where T: class а также для универсальных методов с тем же ограничением (в универсальном или неуниверсальном классе). Это не происходит для (иначе идентичного) неуниверсального метода, который использует Object вместо T:

// static T XchgNullCur<T>(ref T addr, T value) where T : class =>
//              Interlocked.CompareExchange(ref addr, val, null) ?? value;
    .locals init (!T tmp)
    ldarg addr
    ldarg val
    ldloca tmp
    initobj !T
    ldloc tmp
    call !!0 Interlocked::CompareExchange<!T>(!!0&, !!0, !!0)
    dup 
    box !T
    brtrue L_001a
    pop 
    ldarg val
L_001a:
    ret 


// static Object XchgNullCur(ref Object addr, Object val) =>
//                   Interlocked.CompareExchange(ref addr, val, null) ?? value;
    ldarg addr
    ldarg val
    ldnull
    call object Interlocked::CompareExchange(object&, object, object)
    dup
    brtrue L_000d
    pop
    ldarg val
L_000d:
    ret

Обратите внимание на некоторые дополнительные проблемы с первым примером. Вместо простого ldnull у нас есть посторонний вызов initobj бессмысленно нацеленный на избыточную локальную переменную tmp.

Хорошая новость, намекаемая здесь, заключается в том, что все это не имеет значения. Несмотря на различия в коде IL, сгенерированном для двух приведенных выше примеров, JIT x64 генерирует для них практически идентичный код. Следующий результат относится к режиму выпуска.NET Framework 4.7.2 с оптимизацией "не подавлено".

enter image description here