Почему вызов явной реализации интерфейса для типа значения приводит к его упаковке?
Мой вопрос в некоторой степени связан с этим вопросом: как общее ограничение предотвращает упаковку типа значения с неявно реализованным интерфейсом?, но отличается, потому что для этого не нужно ограничение, потому что оно не является общим.
У меня есть код
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 ответа
Я думаю, что ответ в спецификации 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. Причина в том, что бокс сделал копию.
Это показывает нам, что есть разница между тем, если мы упаковываем значение и если мы не ставим значение
Значение не обязательно помещается в коробку. Этап перевода с 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.Stackru]
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
Однако тип этой переменной будет Object
который реализует IFoo
", и, следовательно, бокс будет необходимо, чтобы этот момент.
Может быть полезно определить статический ExecF<T>(ref T thing) where T:I
метод, который затем может вызвать I.F()
метод на thing
, Такой метод не требует какого-либо бокса и учитывает любые мутации, выполняемые I.F()
,