Почему вызов явной реализации интерфейса по типу значения вызывает его коробку?
Мой вопрос несколько связан с этим: Как общее ограничение предотвращает бокс типа значения с неявно реализованным интерфейсом?, но отличается тем, что он должен" 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()
.