Продолжая тему эквивалентности делегатов, давайте рассмотри ещё один хитрый и интересный случай, о котором мне любезно рассказал Владимир Решетников. Дело в том, что случай с base-вызовами - не единственный случай, когда компилятор C# генерирует wrapper-методы, что может нарушать эквивалентность делегатов. Давайте рассмотрим такую ситуацию:

using System;

interface IFoo {
  void Bar();
}

class Foo : IFoo {
  public void Bar() {
    Console.WriteLine("Foo.Bar()");
  }
}

static class Boo {
  private static event Action E = delegate { };
  private static void Main() {
    var foo = new Foo();
    IFoo ifoo = foo;

    E += foo.Bar;  // подписываем метод класса
    E -= ifoo.Bar; // отписываем метод интерфейса

    E(); // ???
  }
}

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

Давайте теперь выделим из класса Foo базовый класс и поместим его в другую сборку FooLibrary.dll:

using System;

namespace FooLibrary {
  public class Foo {
    public void Bar() {
      Console.WriteLine("Foo.Bar()");
    }
  }
}

Интерфейс реализуем неявно, но методом базового класса:

using System;

interface IFoo {
  void Bar();
}

class DerivedFoo : FooLibrary.Foo, IFoo {
  // IFoo.Bar реализуется методом базового класса
}

static class Boo {
  private static event Action E = delegate { };
  private static void Main() {
    var foo = new DerivedFoo();
    IFoo ifoo = foo;

    E += foo.Bar;
    E -= ifoo.Bar;

    E(); // ???
  }
}

Запустив этот код, можно обнаружит, что на экран выводится “Foo.Bar()”, то есть отписки не происходит! Давайте перенесём базовый класс и наследника в единую сборку:

using System;

interface IFoo {
  void Bar();
}

public class Foo {
  public void Bar() {
    Console.WriteLine("Foo.Bar()");
  }
}

class DerivedFoo : Foo, IFoo {
  // IFoo.Bar реализуется методом базового класса
}

static class Boo {
  private static event Action E = delegate { };
  private static void Main() {
    var foo = new DerivedFoo();
    IFoo ifoo = foo;

    E += foo.Bar;
    E -= ifoo.Bar;

    E(); // ???
  }
}

На экране снова пусто! Что за чертовщина!?

Дело в том, что реализация методов интерфейсов требует от реализующих методов виртуальности - аннотации virtual на уровне MSIL и слота в таблице виртуальных методов. Такую аннотацию C# добавляет на методы, если они объявлены как virtual в самом C#, однако это не единственный случай. При реализации интерфейса не виртуальным методом, C# на самом деле вынужден добавить ему на уровне MSIL аннотации virtual и sealed одновременно (это вовсе не означает, что вызовы всем методов, неявно реализующих интерфейсы становятся виртуальными), что заставляет среду исполнения выделить для этого метода слот в таблице виртуальных методов.

Однако не всегда C# может провернуть эту хитрость, так как реализовать интерфейс можно и подходящим методом из базового класса, который может быть скомпилированным без аннотации virtual, так как не реализует никакие интерфейсы - именно это и происходит в двух последних примерах. Для того, чтобы всё же реализовать такими методами интерфейс, компилятор C# пользуется двумя методами - он может либо тихо подкорректировать метаданные базового класса, сделав метод virtual sealed на уровне MSIL, либо сгенерировать в производном классе метод-обёртку с аннотациями virtual sealed, вызывающий базовый метод. Первый способ предпочтительнее и используется в случае, если базовый класс находится непосредственно в той же единице компиляции - сборке.

Надеюсь, теперь становится понятно, что в предпоследнем примере отписка от события производится не методом Foo.Bar(), а методом-обёрткой из класса FooDerived, так как компилятор C# не может изменить метаданные во внешней сборке. Для исправления данной ситуации достаточно сделать метод базового класса виртуальным и никаких методов-обёрток генерироваться не будет.