Скомпилированный класс компилятора для ключевого слова delegate содержит виртуальные методы

Когда ключевое слово delegate используется в С#, компилятор С# автоматически генерирует класс, полученный из класса System.MulticastDelegate.

Этот сгенерированный компилятором класс содержит также 3 метода: Invoke, BeginInvoke and EndInvoke.

Все эти три метода отмечены public virtual extern но интересно, что сам класс отмечен sealed.

Виртуальные методы, определенные в закрытом классе, не только воспринимаются как интуитивно понятные, но и фактически являются незаконными в С#.

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

Изменить 1:

Может ли причина принудительно использовать код операции "callVirt" IL в отличие от "вызова", чтобы объект-делегат всегда проверялся значением null в среде CLR, прежде чем пытаться выполнить любой из трех методов? Хотя я не понимаю, почему delegate должен быть особым делом в этом отношении.

Кроме того, это не удар производительности, чтобы заставить использовать callvirt (хотя это может быть незначительное)

Изменить 2:

Добавлен тег CIL, так как выясняется, что способ определения делегатов С# на самом деле определяется стандартом CIL. Стандарт утверждает, что (следующий не полный текст)

Делегаты должны иметь базовый тип System.Delegate. Делегаты должны быть запечатаны, и единственными членами, которых имеет делегат, являются либо первые два, либо все четыре метода, как указано здесь. Эти методы должны быть объявлены временем выполнения и управляемыми. У них не должно быть тела, так как этот орган должен автоматически создаваться VES. Другие методы, доступные для делегатов, наследуются от класса System.Delegate в библиотеке базового класса. Методы делегата:

  1. Конструктор экземпляра
  2. Метод Invoke должен быть виртуальным
  3. Метод BeginInvoke, если он есть, должен быть виртуальным
  4. Метод EndInvoke должен быть виртуальным

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

Если стандарт подчеркивает что-то, это должно быть по какой-то веской причине и обоснованию.

Итак, теперь вопрос заключается в том, почему стандарт CIL для делегатов подчеркивает одновременно герметичность и виртуальность?

Есть ли уловка здесь?:

У них не должно быть тела, так как этот орган должен автоматически создаваться VES.

Являются ли они помечены как виртуальные, так что тело, созданное VES/CLR, может быть выполнено при вызове этих методов?

Ответ 1

Как я уже отмечал в своем вопросе, что эта запечатанная виртуальная аномалия фактически предусмотрена стандартом CIL. Остается неясным, почему в стандарте CIL конкретно упоминаются, что методы делегирования Invoke, BeginInvoke и EndInvoke должны быть виртуальными, в то же время требуя печати наследуемого класса Delegate.

Кроме того, после прохождения кода SSCLI я узнал, что внутренняя оптимизация JIT-компилятора автоматически переводит любой вызов callvirt по виртуальному методу закрытого класса на обычный вызов с дополнительной проверкой нуля. Это означает, что делегаты не пострадают от какого-либо повышения производительности при вызове метода Invoke (или любого другого) из команды callvirt несмотря на то, что она отмечена как виртуальная в IL.

Когда вызывается вызов делегата, CLR автоматически генерирует сильно оптимизированное тело для этого метода, а не компиляцию кода IL для генерации тела, которое он выполняет для "нормальных" методов. Это не имеет никакого отношения к тому, чтобы быть помеченным virtual в IL.

Я также проверил вручную, модифицируя IL-код и повторно собрав его, чтобы виртуальный объект можно было безопасно удалить из сформированного кода IL класса класса. Сгенерированная сборка, несмотря на нарушение стандарта CIL, отлично работает.

.class private auto ansi beforefieldinit MainApp
       extends [mscorlib]System.Object
{
  .class auto ansi sealed nested private Echo
         extends [mscorlib]System.MulticastDelegate
  {
    .method public hidebysig specialname rtspecialname 
            instance void  .ctor(object 'object',
                                 native int 'method') runtime managed
    {
    } // end of method Echo::.ctor

    .method public hidebysig instance int32  Invoke(int32 i) runtime managed
    {
    } // end of method Echo::Invoke

    .method public hidebysig instance class [mscorlib]System.IAsyncResult 
            BeginInvoke(int32 i,
                        class [mscorlib]System.AsyncCallback callback,
                        object 'object') runtime managed
    {
    } // end of method Echo::BeginInvoke

    .method public hidebysig instance int32  EndInvoke(class [mscorlib]System.IAsyncResult result) runtime managed
    {
    } // end of method Echo::EndInvoke

  } // end of class Echo

  .method public hidebysig static void  Main() cil managed
  {
    .entrypoint
    // Code size       34 (0x22)
    .maxstack  3
    .locals init ([0] class MainApp app,
             [1] class MainApp/Echo dele)
    IL_0000:  nop
    IL_0001:  newobj     instance void MainApp::.ctor()
    IL_0006:  stloc.0
    IL_0007:  ldloc.0
    IL_0008:  ldftn      instance int32 MainApp::DoEcho(int32)
    IL_000e:  newobj     instance void MainApp/Echo::.ctor(object,
                                                           native int)
    IL_0013:  stloc.1
    IL_0014:  ldloc.1
    IL_0015:  ldc.i4.5
    //callvirt can also be replaced by call without affecting functionality
    // since delegate object is essentially not null here
    IL_0016:  callvirt   instance int32 MainApp/Echo::Invoke(int32)
    IL_001b:  call       void [mscorlib]System.Console::WriteLine(int32)
    IL_0020:  nop
    IL_0021:  ret
  } // end of method MainApp::Main

  .method private hidebysig instance int32 
          DoEcho(int32 i) cil managed
  {
    // Code size       7 (0x7)
    .maxstack  1
    .locals init ([0] int32 CS$1$0000)
    IL_0000:  nop
    IL_0001:  ldarg.1
    IL_0002:  stloc.0
    IL_0003:  br.s       IL_0005

    IL_0005:  ldloc.0
    IL_0006:  ret
  } // end of method MainApp::DoEcho

  .method public hidebysig specialname rtspecialname 
          instance void  .ctor() cil managed
  {
    // Code size       7 (0x7)
    .maxstack  8
    IL_0000:  ldarg.0
    IL_0001:  call       instance void [mscorlib]System.Object::.ctor()
    IL_0006:  ret
  } // end of method MainApp::.ctor

} // end of class MainApp

Обратите внимание, что я преобразовал виртуальные методы в обычные методы экземпляра.

Поскольку этот измененный IL работает отлично, это доказывает, что стандартные обязательные виртуальные методы в запечатанном классе делегатов не нужны. Они также могут быть обычными методами.

Поэтому, по всей вероятности, эта аномалия заключается в том, чтобы подчеркнуть, что вызов этих трех методов делегирования фактически приведет к вызову какого-либо другого метода (например, полиморфизма во время выполнения, как и "обычные" виртуальные методы), или это было так, чтобы учесть некоторые будущие гипотетическое улучшение, связанное с делегатами.

Ответ 2

Вы разбираетесь с дизассемблером, который вы использовали для определения типа. Который должен перевести IL обратно на распознаваемый язык, например С#. Это, в общем, невозможно сделать с полной верностью, правила для IL не совпадают с правилами языка С#. Это происходит не только для делегатов, но и метод реализации интерфейса виртуальный, даже если вы не объявляете его виртуальным в своем коде С#.

Для дальнейшей мутной воды IL фактически позволяет компилятору испускать не виртуальный вызов для виртуального метода, если он может определить целевой объект из анализа кода. Но этого не произойдет для делегата или интерфейса. И IL разрешает делать виртуальный вызов не виртуальному методу, что компилятор С# делает с удовольствием, чтобы реализовать гарантию того, что метод экземпляра никогда не может быть вызван с нулевым значением.

Но это использование С# - умный трюк, обнаруженный только после разработки CLR. Первоначальное намерение виртуального, конечно же, заключалось в том, чтобы аннотировать, что метод следует вызывать с помощью Callvirt. В конечном счете это не имеет значения, потому что компилятор знает о делегировании и интерфейсе поведения и всегда будет излучать Callvirt. И фактический вызов метода реализован в коде CLR, который предполагает активацию Callvirt.

Ответ 3

Это побочный эффект процесса компиляции. Я не знаю точной причины этого, есть примеры такого поведения. Например, скомпилированный статический класс становится абстрактным закрытым классом (поэтому вы не можете создать его экземпляр, и вы не сможете наследовать его).

Ответ 4

Кажется, это не относится к делегатам. Я попробовал этот пример:

 public abstract class Base
    {
        public abstract void Test();
    }

    public sealed class Derived : Base
    {
        public override  void Test()
        {
            throw new NotImplementedException();
        }
    }

и в ILDasm я получаю это для реализации Test():

.method public hidebysig virtual instance void 
        Test() cil managed
{
  // Code size       7 (0x7)
  .maxstack  8
  IL_0000:  nop
  IL_0001:  newobj     instance void [mscorlib]System.NotImplementedException::.ctor()
  IL_0006:  throw
} // end of method Derived::Test

Возможно, ключевое слово override не является ключевым словом CLR.