Почему вызов явной реализации интерфейса для типа значения делает его упакованным? - PullRequest
15 голосов
/ 28 апреля 2011

Мой вопрос несколько связан с этим: Как универсальное ограничение предотвращает упаковку типа значения с неявно реализованным интерфейсом? , но отличается, потому что для этого не нужно ограничение, потому что это вообще не универсально.

У меня есть код

interface I { void F(); }
struct C : I { void I.F() {} }
static class P {
    static void Main()
    {    
        C x;
        ((I)x).F();
    }
}

Основной метод компилируется в это:

IL_0000:  ldloc.0
IL_0001:  box        C
IL_0006:  callvirt   instance void I::F()
IL_000b:  ret

Почему это не компилируется?

IL_0000:  ldloca.s   V_0
IL_0002:  call       instance void C::I.F()
IL_0007:  ret

Я понимаю, почему вам нужна таблица методов для виртуального вызова, но вам не нужно делать виртуальный вызов в этом случае. Если интерфейс реализован нормально, он не выполняет виртуальный вызов.

Также связано: Почему явные реализации интерфейса являются частными? - существующие ответы на этот вопрос неадекватно объясняют, почему методы помечены как метаданные как частные (а не просто имеют непригодные имена). Но даже это не полностью объясняет, почему оно упаковано, так как оно по-прежнему упаковывается при вызове изнутри C.

Ответы [ 3 ]

8 голосов
/ 28 апреля 2011

Я думаю, что ответ в спецификации C # того, как интерфейсы могут быть обработаны. Из спецификации:

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

В приведенной ниже таблице говорится об интерфейсе

Пустая ссылка, ссылка на экземпляр типа класса, который реализует этот тип интерфейса, или ссылка на упакованное значение значения тип, который реализует этот интерфейс тип

В нем явно сказано, что это будет коробочное значение типа значения. Компилятор просто подчиняется спецификации

** Редактировать **

Для добавления дополнительной информации на основе комментария. Компилятор может переписать его, если он имеет тот же эффект, но поскольку происходит боксирование, вы создаете копию типа значения, не имеющего того же типа значения. Из спецификации снова:

Преобразование бокса подразумевает создание копия значения в штучной упаковке. Это отличается от преобразования ссылка-тип для типа объекта, в значение которого продолжает ссылаться тот же экземпляр и просто рассматривается как менее производный тип объект.

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

public interface I { void F(); }
public struct C : I {
    public int i;
    public void F() { i++; } 
    public int GetI() { return i; }
}

    class P
    {
    static void Main(string[] args)
    {
        C x = new C();
        I ix = (I)x;
        ix.F();
        ix.F();
        x.F();
        ((I)x).F();
        Console.WriteLine(x.GetI());
        Console.WriteLine(((C)ix).GetI());
        Console.ReadLine();
    }
}

Я добавил внутренний элемент в структуру C, который увеличивается на 1 каждый раз, когда F() вызывается для этого объекта. Это позволяет нам увидеть, что происходит с данными нашего типа значения. Если на x не был выполнен бокс, вы ожидаете, что программа выписает 4 для обоих вызовов на GetI(), поскольку мы вызываем F() четыре раза. Однако фактический результат, который мы получаем, равен 1 и 2. Причина в том, что бокс сделал копию.

Это показывает нам, что есть разница между тем, если мы поместим значение в коробку, и если мы не будем указывать значение

2 голосов
/ 14 июля 2018

Это значение не обязательно в штучной упаковке. Этап перевода с C # на MSIL обычно не выполняет большую часть классных оптимизаций (по нескольким причинам, по крайней мере, некоторые из которых действительно хороши), поэтому вы, вероятно, все равно увидите инструкцию box, если посмотрите в MSIL, но иногда JIT может по закону исключить фактическое распределение, если обнаружит, что может сойти с рук. Начиная с .NET Fat 4.7.1, похоже, что разработчики никогда не вкладывали средства в обучение JIT, как выяснить, когда это было законно. JIT .NET Core 2.1 делает это (не уверен, когда он был добавлен, я просто знаю, что он работает в 2.1).

Вот результаты теста, который я провел, чтобы доказать это:

BenchmarkDotNet=v0.10.14, OS=Windows 10.0.17134
Intel Core i7-6850K CPU 3.60GHz (Skylake), 1 CPU, 12 logical and 6 physical cores
Frequency=3515626 Hz, Resolution=284.4444 ns, Timer=TSC
.NET Core SDK=2.1.302
  [Host] : .NET Core 2.1.2 (CoreCLR 4.6.26628.05, CoreFX 4.6.26629.01), 64bit RyuJIT
  Clr    : .NET Framework 4.7.1 (CLR 4.0.30319.42000), 64bit RyuJIT-v4.7.3131.0
  Core   : .NET Core 2.1.2 (CoreCLR 4.6.26628.05, CoreFX 4.6.26629.01), 64bit RyuJIT


                Method |  Job | Runtime |     Mean |     Error |    StdDev |  Gen 0 | Allocated |
---------------------- |----- |-------- |---------:|----------:|----------:|-------:|----------:|
       ViaExplicitCast |  Clr |     Clr | 5.139 us | 0.0116 us | 0.0109 us | 3.8071 |   24000 B |
 ViaConstrainedGeneric |  Clr |     Clr | 2.635 us | 0.0034 us | 0.0028 us |      - |       0 B |
       ViaExplicitCast | Core |    Core | 1.681 us | 0.0095 us | 0.0084 us |      - |       0 B |
 ViaConstrainedGeneric | Core |    Core | 2.635 us | 0.0034 us | 0.0027 us |      - |       0 B |

Исходный код теста:

using System.Runtime.CompilerServices;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Attributes.Exporters;
using BenchmarkDotNet.Attributes.Jobs;
using BenchmarkDotNet.Running;

[MemoryDiagnoser, ClrJob, CoreJob, MarkdownExporterAttribute.StackOverflow]
public class Program
{
    public static void Main() => BenchmarkRunner.Run<Program>();

    [Benchmark]
    public int ViaExplicitCast()
    {
        int sum = 0;
        for (int i = 0; i < 1000; i++)
        {
            sum += ((IValGetter)new ValGetter(i)).GetVal();
        }

        return sum;
    }

    [Benchmark]
    public int ViaConstrainedGeneric()
    {
        int sum = 0;
        for (int i = 0; i < 1000; i++)
        {
            sum += GetVal(new ValGetter(i));
        }

        return sum;
    }

    [MethodImpl(MethodImplOptions.NoInlining)]
    private static int GetVal<T>(T val) where T : IValGetter => val.GetVal();

    public interface IValGetter { int GetVal(); }

    public struct ValGetter : IValGetter
    {
        public int _val;

        public ValGetter(int val) => _val = val;

        [MethodImpl(MethodImplOptions.NoInlining)]
        int IValGetter.GetVal() => _val;
    }
}
2 голосов
/ 23 декабря 2011

Проблема в том, что не существует такой вещи, как значение или переменная, которая является «просто» типом интерфейса; вместо этого, когда делается попытка определить такую ​​переменную или привести к такому значению, реальным типом, который фактически используется, является «Object, который реализует интерфейс».

Это различие вступает в игру с дженериками. Предположим, что подпрограмма принимает параметр типа T, где T:IFoo. Если передать такую ​​подпрограмму в структуру, которая реализует IFoo, переданный параметр не будет типом класса, который наследуется от Object, но вместо этого будет подходящим типом структуры. Если бы подпрограмма должна была передать переданный параметр локальной переменной типа T, параметр был бы скопирован по значению, без упаковки. Однако, если бы она была назначена локальной переменной типа IFoo, тип этой переменной был бы «Object, который реализует IFoo», и, следовательно, для этой точки потребуется бокс.

Может быть полезно определить статический метод ExecF<T>(ref T thing) where T:I, который затем может вызвать метод I.F() для thing. Такой метод не требует какого-либо бокса и учитывает любые мутации, выполняемые I.F().

...