Как общее ограничение предотвращает упаковку типа значения с неявно реализованным интерфейсом?
Мой вопрос несколько связан с этим: явно реализованный интерфейс и общие ограничения.
Мой вопрос, однако, заключается в том, каким образом компилятор позволяет использовать общее ограничение, чтобы исключить необходимость упаковки типа значения, который явно реализует интерфейс.
Я думаю, мой вопрос сводится к двум частям:
Что происходит с закулисной реализацией 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));
}
}
И результирующий ИЛ:
.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
в обоих случаях.
Так что я не знаю, что именно с боксом требуется для виртуального вызова, и я особенно не понимаю, как использование универсального ограничения, связанного с типом значения, устраняет необходимость операции бокса.
Я заранее всех благодарю...
5 ответов
Мой вопрос, однако, заключается в том, каким образом компилятор позволяет использовать общее ограничение, чтобы исключить необходимость упаковки типа значения, который явно реализует интерфейс.
Под "компилятором" неясно, имеете ли вы в виду джиттер или компилятор C#. Компилятор C# делает это, испуская ограниченный префикс для виртуального вызова. См. Документацию ограниченного префикса для деталей.
Что происходит с закулисной реализацией CLR, которая требует, чтобы тип значения был упакован при доступе к явно реализованному элементу интерфейса
Является ли вызываемый метод явно реализованным элементом интерфейса или нет, не имеет особого значения. Более общий вопрос состоит в том, почему любой виртуальный вызов требует, чтобы тип значения был помещен в коробку?
Традиционно считается, что виртуальный вызов является косвенным вызовом указателя метода в таблице виртуальных функций. Это не совсем то, как вызовы интерфейса работают в CLR, но это разумная ментальная модель для целей этого обсуждения.
Если именно так будет вызываться виртуальный метод, то откуда взялся vtable? Тип значения не содержит vtable. Тип значения просто имеет свое значение в своем хранилище. Бокс создает ссылку на объект, для которого настроена виртуальная таблица, указывающая на все виртуальные методы типа значения. (Опять же, я предупреждаю вас, что это не совсем то, как работают вызовы интерфейса, но это хороший способ подумать об этом.)
Что происходит с общим ограничением, которое снимает это требование?
Джиттер будет генерировать свежий код для каждой другой конструкции аргумента типа значения универсального метода. Если вы собираетесь генерировать свежий код для каждого отдельного типа значения, вы можете адаптировать этот код к этому конкретному типу значения. Это означает, что вам не нужно создавать виртуальную таблицу, а затем посмотреть, каково ее содержимое! Вы знаете, каким будет содержимое таблицы, поэтому просто сгенерируйте код, чтобы вызвать метод напрямую.
Конечная цель - получить указатель на таблицу методов класса, чтобы можно было вызвать правильный метод. Это не может произойти напрямую с типом значения, это просто блок байтов. Есть два способа добраться туда:
- Opcodes.Box реализует преобразование бокса и превращает значение типа значения в объект. Объект имеет указатель таблицы методов со смещением 0.
- Opcodes.Contrained передает джиттер указателю таблицы методов напрямую, без необходимости в боксе. Включено общим ограничением.
Последнее явно более эффективно.
Упаковка необходима, когда объект типа значения передается подпрограмме, которая ожидает получить объект типа класса. Объявление метода типа string ReadAndAdvanceEnumerator<T>(ref T thing) where T:IEnumerator<String>
фактически объявляет целое семейство функций, каждая из которых ожидает свой тип T
, Если T
бывает тип значения (например, List<String>.Enumerator
), компилятор Just-In-Time на самом деле будет генерировать машинный код исключительно для выполнения ReadAndAdvanceEnumerator<List<String>.Enumerator>()
, Кстати, обратите внимание на использование ref
; если T
Если тип класса (типы интерфейса используются в любом контексте, кроме ограничений, считаются типами классов), использование ref
было бы ненужным препятствием для эффективности. Однако, если есть вероятность, что T
может быть this
мутирующая структура (например, List<string>.Enumerator
), использование ref
будет необходимо обеспечить this
мутации, выполняемые структурой во время выполнения ReadAndAdvanceEnumerator
будет выполнен на копии звонящего.
Общее ограничение обеспечивает только проверку времени компиляции того, что в метод передается правильный тип. Конечным результатом всегда является то, что компилятор генерирует соответствующий метод, который принимает тип среды выполнения:
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.
}
Удаление общего ограничения только останавливает время компиляции, проверяя, что вы передаете правильный метод в метод.
Я думаю, что вам нужно использовать
- отражатель
- ildasm / monodis
чтобы действительно получить ответ, который вы хотите
Вы, конечно, можете посмотреть спецификации CLR (ECMA) и / или источника компилятора C# ( моно).