Как общее ограничение предотвращает бокс типа значения с неявно реализованным интерфейсом?
Мой вопрос несколько связан с этим: Явно реализованный интерфейс и общие ограничения.
Мой вопрос, однако, заключается в том, как компилятор позволяет общему ограничению устранять необходимость в боксировании типа значения, который явно реализует интерфейс.
Я думаю, мой вопрос сводится к двум частям:
-
Что происходит с реализацией CLR за кадром, которая требует ввода типа значения при доступе к явно реализованному члену интерфейса и
-
Что происходит с общим ограничением, которое устраняет это требование?
Пример кода:
internal struct TestStruct : IEquatable<TestStruct>
{
bool IEquatable<TestStruct>.Equals(TestStruct other)
{
return true;
}
}
internal class TesterClass
{
// Methods
public static bool AreEqual<T>(T arg1, T arg2) where T: IEquatable<T>
{
return arg1.Equals(arg2);
}
public static void Run()
{
TestStruct t1 = new TestStruct();
TestStruct t2 = new TestStruct();
Debug.Assert(((IEquatable<TestStruct>) t1).Equals(t2));
Debug.Assert(AreEqual<TestStruct>(t1, t2));
}
}
И результат IL:
.class private sequential ansi sealed beforefieldinit TestStruct
extends [mscorlib]System.ValueType
implements [mscorlib]System.IEquatable`1<valuetype TestStruct>
{
.method private hidebysig newslot virtual final instance bool System.IEquatable<TestStruct>.Equals(valuetype TestStruct other) cil managed
{
.override [mscorlib]System.IEquatable`1<valuetype TestStruct>::Equals
.maxstack 1
.locals init (
[0] bool CS$1$0000)
L_0000: nop
L_0001: ldc.i4.1
L_0002: stloc.0
L_0003: br.s L_0005
L_0005: ldloc.0
L_0006: ret
}
}
.class private auto ansi beforefieldinit TesterClass
extends [mscorlib]System.Object
{
.method public hidebysig specialname rtspecialname instance void .ctor() cil managed
{
.maxstack 8
L_0000: ldarg.0
L_0001: call instance void [mscorlib]System.Object::.ctor()
L_0006: ret
}
.method public hidebysig static bool AreEqual<([mscorlib]System.IEquatable`1<!!T>) T>(!!T arg1, !!T arg2) cil managed
{
.maxstack 2
.locals init (
[0] bool CS$1$0000)
L_0000: nop
L_0001: ldarga.s arg1
L_0003: ldarg.1
L_0004: constrained !!T
L_000a: callvirt instance bool [mscorlib]System.IEquatable`1<!!T>::Equals(!0)
L_000f: stloc.0
L_0010: br.s L_0012
L_0012: ldloc.0
L_0013: ret
}
.method public hidebysig static void Run() cil managed
{
.maxstack 2
.locals init (
[0] valuetype TestStruct t1,
[1] valuetype TestStruct t2,
[2] bool areEqual)
L_0000: nop
L_0001: ldloca.s t1
L_0003: initobj TestStruct
L_0009: ldloca.s t2
L_000b: initobj TestStruct
L_0011: ldloc.0
L_0012: box TestStruct
L_0017: ldloc.1
L_0018: callvirt instance bool [mscorlib]System.IEquatable`1<valuetype TestStruct>::Equals(!0)
L_001d: stloc.2
L_001e: ldloc.2
L_001f: call void [System]System.Diagnostics.Debug::Assert(bool)
L_0024: nop
L_0025: ldloc.0
L_0026: ldloc.1
L_0027: call bool TesterClass::AreEqual<valuetype TestStruct>(!!0, !!0)
L_002c: stloc.2
L_002d: ldloc.2
L_002e: call void [System]System.Diagnostics.Debug::Assert(bool)
L_0033: nop
L_0034: ret
}
}
Ключевой вызов constrained !!T
вместо box TestStruct
, но последующий вызов по-прежнему callvirt
в обоих случаях.
Поэтому я не знаю, что это такое с боксом, который требуется для создания виртуального вызова, и я особенно не понимаю, как использование общего ограничения, связанного с типом значения, устраняет необходимость в операции бокса.
Я благодарю всех заблаговременно...
Ответы
Ответ 1
Мой вопрос, однако, заключается в том, как компилятор позволяет общему ограничению устранять необходимость в боксировании типа значения, который явно реализует интерфейс.
Под "компилятором" неясно, имеете ли вы в виду дрожание или компилятор С#. Компилятор С# делает это, испуская префикс с ограничениями виртуального вызова. Подробнее см. документацию префикса с ограничениями.
Что происходит с реализацией CLR за кадром, требующей ввода типа значения при доступе к явно реализованному члену интерфейса
Является ли вызываемый метод явно введенным членом интерфейса или нет, не имеет особого значения. Более общий вопрос заключается в том, почему любой виртуальный вызов требует ввода типа значения?
Традиционно считается, что виртуальный вызов является косвенным вызовом указателя метода в таблице виртуальных функций. Это не совсем то, как работают интерфейсные вызовы в среде CLR, но это разумная ментальная модель для целей этого обсуждения.
Если это произойдет, как будет вызван виртуальный метод, то откуда это происходит? Тип значения не содержит в нем vtable. Тип значения просто имеет значение в своем хранилище. Бокс создает ссылку на объект, у которого есть vtable, настроенная так, чтобы указывать на все виртуальные методы типа значения. (Опять же, я предупреждаю вас, что это не совсем так, как работают интерфейсные вызовы, но это хороший способ подумать об этом.)
Что происходит с общим ограничением, которое устраняет это требование?
Дрожание будет генерировать свежий код для каждой конструкции аргумента типа значения общего метода. Если вы собираетесь генерировать новый код для каждого типа значений, вы можете настроить этот код на этот тип значения. Это означает, что вам не нужно создавать vtable, а затем искать содержимое vtable! Вы знаете, каково будет содержимое vtable, поэтому просто создайте код для непосредственного вызова метода.
Ответ 2
Конечная цель - получить указатель на таблицу методов класса, чтобы можно было вызвать правильный метод. Это не может произойти непосредственно по типу значения, это просто капля байтов. Есть два способа добраться туда:
- Opcodes.Box реализует преобразование бокса и превращает значение типа значения в объект. Объект имеет указатель таблицы методов со смещением 0.
- Opcodes.Contrained, направляет дрожание указателя таблицы методов непосредственно без необходимости в боксе. Включено общим ограничением.
Последнее явно более эффективно.
Ответ 3
Бокс необходим, когда объект типа значения передается подпрограмме, которая ожидает получить объект класса. Объявление метода, подобное string ReadAndAdvanceEnumerator<T>(ref T thing) where T:IEnumerator<String>
, фактически объявляет целое семейство функций, каждый из которых ожидает другого типа T
. Если T
является типом значения (например, List<String>.Enumerator
), компилятор Just-In-Time фактически генерирует машинный код исключительно для выполнения ReadAndAdvanceEnumerator<List<String>.Enumerator>()
. BTW, обратите внимание на использование ref
; если T
были типом класса (типы интерфейсов, используемые в любом контексте , кроме ограничений count как типы классов), использование ref
было бы лишним препятствием для эффективности. Если, однако, существует вероятность того, что T
может быть this
-mutating struct (например, List<String>.Enumerator
), использование ref
будет необходимо, чтобы гарантировать, что мутации this
, выполняемые структурой во время выполнения из ReadAndAdvanceEnumerator
будет выполняться по копии вызывающего абонента.
Ответ 4
Я думаю, вам нужно использовать
- отражатель
- ildasm/monodis
чтобы действительно получить ответ, который вы хотите
Вы можете, конечно, изучить спецификации CLR (ECMA) и источник компилятора С# (mono)
Ответ 5
Общее ограничение предоставляет только проверку времени компиляции, что в этот метод передается правильный тип. Конечным результатом всегда является то, что компилятор генерирует соответствующий метод, который принимает тип среды выполнения:
public struct Foo : IFoo { }
public void DoSomething<TFoo>(TFoo foo) where TFoo : IFoo
{
// No boxing will occur here because the compiler has generated a
// statically typed DoSomething(Foo foo) method.
}
В этом смысле он обходит необходимость боксирования типов значений, поскольку создается явный экземпляр метода, который принимает этот тип значения напрямую.
В то время как когда тип значения передается в реализованный интерфейс, экземпляр является ссылочным типом, который находится в куче. Поскольку мы не используем дженерики в этом смысле, мы вынуждаем приведение к интерфейсу (и последующему боксу), если тип среды выполнения является типом значения.
public void DoSomething(IFoo foo)
{
// Boxing occurs here as Foo is cast to a reference type of IFoo.
}
Удаление общего ограничения останавливает время компиляции, проверяя, что вы передаете правильный тип в метод.