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

StackOverflow https://stackoverflow.com/questions/5812099

Вопрос

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

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

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.

Это было полезно?

Решение

Я думаю, что ответ в спецификации 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 Тогда вы ожидаете GetI() как мы звоним F() четыре раза. Однако фактический результат, который мы получаем, составляет 1 и 2. Причина в том, что бокс сделал копию.

Это показывает нам, что существует разница, если мы - это значение, и если мы не являемся.

Другие советы

Значение нет обязательно Получите коробку. Шаг перевода 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;
    }
}

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

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

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

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top