Как общее ограничение предотвращает упаковку типа значения с неявно реализованным интерфейсом? - PullRequest
13 голосов
/ 03 апреля 2011

Мой вопрос в некоторой степени связан с этим: Явно реализованный интерфейс и универсальное ограничение .

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

Я предполагаю, что мой вопрос сводится к двум частям:

  1. Что происходит с последующим- реализация среды CLR, которая требует, чтобы тип значения был упакован при обращении к явно реализованному элементу интерфейса, и

  2. Что происходит с общим ограничением, которое снимает это требование?

Пример кода:

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 в обоих случаях.

Так что я не знаю, что такое с боксом, который требуется для виртуального вызова, и я особенно не понимаю, как использовать общее ограничениеРедактирование до значения типа устраняет необходимость в операции упаковки.

Заранее благодарю всех ...

Ответы [ 5 ]

22 голосов
/ 03 апреля 2011

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

Под "компилятором" не ясно, имеете ли вы в виду джиттер или компилятор C #. Компилятор C # делает это, испуская ограниченный префикс для виртуального вызова. Подробности смотрите в документации ограниченного префикса .

Что происходит с закулисной реализацией CLR, которая требует, чтобы тип значения был упакован при доступе к явно реализованному элементу интерфейса

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

Традиционно считается, что виртуальный вызов является косвенным вызовом указателя метода в таблице виртуальных функций. Это не совсем то, как вызовы интерфейса работают в CLR, но это разумная ментальная модель для целей этого обсуждения.

Если так будет вызываться виртуальный метод, тогда откуда берется vtable ? Тип значения не содержит vtable. Тип значения просто имеет свое значение в своем хранилище. Бокс создает ссылку на объект, для которого настроена виртуальная таблица, указывающая на все виртуальные методы типа значения. (Опять же, я предупреждаю вас, что это не точно , как работают вызовы интерфейса, но это хороший способ думать об этом.)

Что происходит с общим ограничением, которое снимает это требование?

Джиттер будет генерировать свежий код для каждой конструкции аргумента типа значения универсального метода. Если вы собираетесь генерировать свежий код для каждого отдельного типа значения, вы можете адаптировать этот код к этому конкретному типу значения. Это означает, что вам не нужно создавать виртуальную таблицу, а затем посмотреть, каково ее содержимое! Вы знаете, каким будет содержимое vtable, поэтому просто сгенерируйте код для непосредственного вызова метода.

6 голосов
/ 03 апреля 2011

Конечная цель - получить указатель на таблицу методов класса, чтобы можно было вызывать правильный метод. Это не может происходить напрямую с типом значения, это просто блок байтов. Есть два способа добраться туда:

  • Opcodes.Box, реализует преобразование бокса и превращает значение типа значения в объект. Объект имеет указатель таблицы методов со смещением 0.
  • Opcodes.Contrained, передает джиттер указателю таблицы методов напрямую без необходимости в боксе Включено общим ограничением.

Последнее явно более эффективно.

3 голосов
/ 06 февраля 2012

Упаковка необходима, когда объект типа значения передается подпрограмме, которая ожидает получить объект типа класса.Объявление метода типа 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 будет выполнено после копии звонящего.

0 голосов
/ 03 апреля 2011

Общее ограничение обеспечивает только проверку времени компиляции, что в метод передается правильный тип. Конечным результатом всегда является то, что компилятор генерирует соответствующий метод, который принимает тип среды выполнения:

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.
}

Удаление общего ограничения только останавливает время компиляции, проверяя, что вы передали правильный метод в метод.

0 голосов
/ 03 апреля 2011

Я думаю, вам нужно использовать

  • отражатель
  • ildasm / monodis

, чтобы действительно получить нужный ответ

Конечно, вы можете посмотреть спецификации CLR (ECMA) и / или источника компилятора C # ( mono )

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...