Почему вызов явной реализации интерфейса по типу значения вызывает его коробку?

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

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

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.

Ответы

Ответ 1

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

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

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

Нулевая ссылка, ссылка на экземпляр типа класса, который реализует этот тип интерфейса или ссылка на значение в штучной упаковке значения тип, реализующий этот интерфейс Тип

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

** Изменить **

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

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

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

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();
    }
}

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

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

Ответ 2

Значение не обязательно necessarily упаковывается. Этап перевода с С# в MSIL обычно не выполняет большую часть классных оптимизаций (по нескольким причинам, по крайней мере, некоторые из которых действительно хороши), поэтому вы, вероятно, все равно увидите инструкцию box, если вы посмотрите на MSIL, но JIT иногда может на законных основаниях исключить фактическое распределение, если обнаружит, что ему это сойдет с рук. Начиная с .NET Fat 4.7.1, похоже, что разработчики никогда не вкладывали средства в обучение JIT, как выяснить, когда это было законно..NET Core 2.1 JIT делает это (не уверен, когда он был добавлен, я просто знаю, что он работает в 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.Qaru]
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;
    }
}

Ответ 3

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

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

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