Множественное наследование в C# с использованием IL(D)ASM

Я хочу множественое наследование в C#. Да, в большинстве случаев оно не нужно, и можно обойтись интерфейсами и повторной реализацией, но я против: я не хочу постоянно делать реализацию паттерна Декоратор, потому что мне, если честно, лень. Чего я хочу так это иметь возможность определить интерфейсы и базовые классы, и чтобы наследование работало правильно. Пусть даже я никогда не смогу привести наследуемый класс к конкретному типу класса – мне в принципе интерфейса хватит. В этом посте – про то как это можно реализовать.

Here be dragons

Окей, давайте возьмем избитый пример с драконом:

public class Bird : IBird
  {
    public void Fly()
    {
      Console.WriteLine("Flying!");
    }
  }
  public class Lizard : ILizard
  {
    public void Walk()
    {
      Console.WriteLine("Walking");
    }
  }
 
  public class Dragon : Bird, Snake // так работать не будет : )
  {
    ...
  }

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

public interface IBird
  {
    void Fly();
  }
  public interface ILizard
  {
    void Walk();
  }

Про декораторы

На самом деле, большинство разработчиков в данном случае начнет строить декоратор. В свое время я занимался тем же самым, и даже написал add-in который позволял мне создавать декораторы (и тем самым реализовывать множественное наследие путем дупликации) с помощью статического анализа существующих файлов. В результате получилось бы нечто подобное:

public class Dragon : IBird, ILizard
{
  private IBird bird = new Bird();
  private ILizard lizard = new Lizard();
  public void Fly() { bird.Fly(); }
  public void Walk() { lizard.Walk(); }
}

На самом деле, тут есть вариант (да и в нашей реализации тоже) – всегда есть вариант отнаследовать хотя бы от одного класса. Это конечно запутает всех кто использует реализованный класс, но… можно написать и вот так:

public class Dragon : Bird, ILizard
{
  private IBird bird = new Bird();
  public void Fly() { bird.Fly(); }
}

На самом деле, этот паттерн можно обсуждать долго, особенно если мы будем рассматривать ситуации вроде реализации, скажем, INotifyPropertyChanged (навязчивый интерфейс!) в классах Bird, Lizard и Dragon одновременно – по секрету скажу что в сложных ситуациях любой паттерн валится на куски.

Как делать «правильное» множественное наследование

Сегодня реализовывать эти интерфейсы в классе Dragon я не буду т.к. они уже реализованы, а дупликация кода (по крайней мере в самом коде) — зло. А вот дупликация кода путем автоматизированных решений – самое то. Для начала давайте создадим некий маркировачный аттрибут, который будет указывать на то, что нам нужно именно наследование реализации.

[AttributeUsage(AttributeTargets.Class, AllowMultiple=true, Inherited=true)]
  public class InheritAttribute : Attribute
  {
    private readonly Type parentInterface;
    private readonly Type parentClass;
    public InheritAttribute(Type parentInterface, Type parentClass)
    {
      this.parentInterface = parentInterface;
      this.parentClass = parentClass;
    }
  }

Аттрибут сам по себе ничего не делает, но пользоваться им уже можно:

[Inherit(typeof(IBird), typeof(Bird))]
  [Inherit(typeof(ILizard), typeof(Lizard))]
  public class Dragon : IBird, ILizard
  {
    public void Fly()
    {
      throw new NotImplementedException();
    }
    public void Walk()
    {
      throw new NotImplementedException();
    }
  }

Теперь осталось самое главное – заполнить реализацию. Впрочем это не то чтобы сложно. Помните что у нас есть две полезные утилиты – ildasm и ilasm которые разбирают сборку на IL и потом собирают ее воедино? Вот-вот, самое время воспользоваться этим чудом.

Напомиалка: вот как работают эти утилиты.

  • ildasm MyAssembly.dll /out=MyAssembly.tmp разбирает сборку MyAssembly.dll в текстовый IL-файл с названием MyAssembly.tmp а также создает файл MyAssembly.res в котором содержатся ресурсы этой сборки.

  • ilasm MyAssembly.tmp /resource=MyAssembly.res /dll /output=MyAssembly.dll восстанавливает сборку из IL-файла. Естественно что с подписью и т.п. возникают некоторые проблемы, но меня сейчас это мало волнует.

Давайте посмотрим на то, какой IL нам выдаст тип Dragon (весь файл демонстрировать не буду) при его декомпиляции.

.class public auto ansi serializable beforefieldinit ConsoleApplication1.Dragon
       extends [mscorlib]System.Object
       implements ConsoleApplication1.IBird,
                  ConsoleApplication1.ILizard
{
  .custom instance void ConsoleApplication1.InheritAttribute::.ctor(class [mscorlib]System.Type,
                                                                    class [mscorlib]System.Type) = ( 01 00 1B 43 6F 6E 73 6F 6C 65 41 70 70 6C 69 63   // ...ConsoleApplic
                                                                                                     61 74 69 6F 6E 31 2E 49 4C 69 7A 61 72 64 1A 43   // ation1.ILizard.C
                                                                                                     6F 6E 73 6F 6C 65 41 70 70 6C 69 63 61 74 69 6F   // onsoleApplicatio
                                                                                                     6E 31 2E 4C 69 7A 61 72 64 00 00 )                // n1.Lizard..
  .custom instance void ConsoleApplication1.InheritAttribute::.ctor(class [mscorlib]System.Type,
                                                                    class [mscorlib]System.Type) = ( 01 00 19 43 6F 6E 73 6F 6C 65 41 70 70 6C 69 63   // ...ConsoleApplic
                                                                                                     61 74 69 6F 6E 31 2E 49 42 69 72 64 18 43 6F 6E   // ation1.IBird.Con
                                                                                                     73 6F 6C 65 41 70 70 6C 69 63 61 74 69 6F 6E 31   // soleApplication1
                                                                                                     2E 42 69 72 64 00 00 )                            // .Bird..
  .method public hidebysig newslot virtual final 
          instance void  Fly() 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 Dragon::Fly
  .method public hidebysig newslot virtual final 
          instance void  Walk() 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 Dragon::Walk
  .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 Dragon::.ctor
} // end of class ConsoleApplication1.Dragon

Чтобы воткнуть сюда реализацию из Bird и Lizard нужно всего лишь распарсить эти структуры и сделать текстовую замену. Для начала, конечно, нужно найти инстансы аттрибутов InheritAttribute и аннигилировать их, т.к. для наших целей они не особенно полезны.

Магический алгоритм

Алгоритм замены реализации примерно такой:

  1. Находим класс у которого есть аттрибут(ы) InheritAttribute. Удаляем их полностью т.к. они нам не особо нужны. (В последствии можно также удалить сам класс.)

  2. Поскольку каждый аттрибут предоставил нам мэппинг Интерфейс→Класс, для начала можно удалить все заглушки этих интерфейсов – они нам больше не нужны :)

  3. Теперь можно на место этих заглушек поставить собственно реализацию. Естественно конструкторы вставлять не надо, и с конфликтами тоже надо быть поосторожней (я имею ввиду explicit interface implementation и те проблемы, который могут быть от diamond inheritance).

  4. Вот собственно и все, можно перекомпилировать.

После подобный манипуляций, мы получим определение Dragon которое выглядит вот так:

.class public auto ansi serializable beforefieldinit ConsoleApplication1.Dragon
       extends [mscorlib]System.Object
       implements ConsoleApplication1.IBird,
                  ConsoleApplication1.ILizard
{
  .method public hidebysig newslot virtual final 
          instance void  Fly() cil managed
  {
    // Code size       13 (0xd)
    .maxstack  8
    IL_0000:  nop
    IL_0001:  ldstr      "Flying!"
    IL_0006:  call       void [mscorlib]System.Console::WriteLine(string)
    IL_000b:  nop
    IL_000c:  ret
  } // end of method Bird::Fly
  .method public hidebysig newslot virtual final 
          instance void  Walk() cil managed
  {
    // Code size       13 (0xd)
    .maxstack  8
    IL_0000:  nop
    IL_0001:  ldstr      "Walking"
    IL_0006:  call       void [mscorlib]System.Console::WriteLine(string)
    IL_000b:  nop
    IL_000c:  ret
  } // end of method Lizard::Walk
  .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 Dragon::.ctor
} // end of class ConsoleApplication1.Dragon

На самом деле, я бы с удовольствием показал разбор IL на кусочки, но боюсь что это слишком скучно – для того чтобы разобрать кастомный IL на кусочки, я использую достаточно простую структуру.

internal class Section
{
  public string Name { get; set; }
  public int StartLine { get; set; }
  public int StartContentLine { get; set; }
  public int EndLine { get; set; }
}

Далее все просто – берем IL-файл, делаем File.ReadAllLines() и потом находим все нужные нам структуры. Что касается процесса замены, то это обычное манипулирование списка – стираем аттрибуты (кто бы мог подумать что они вот так странно описываются), двигаем начинку местами.

Важно заметить что в процессе копирования начинку, скажем, из Lizard в Dragon мы хоть и игнорируем конструктор, но оставляем все остальное – методы (= свойства), поля, да всё что только можно. Конечно, если наследуемый класс сам реализует какой-то интерфейс то все становится намного сложнее (если мы хотим это правильно сделать).

Заключение

В моем эксперименте я просто создал дополнительный проект который вызывал постпроцессинг через Process.Start(). В реальном использовании, наверное пришлось бы писать экшн для MSBuild, но скорее всего я не буду этим заниматься. Причиной является прежде всего то, что во-первых это решение нереально использовать в коммандной работе, т.к. оно реализует «неочевидное поведение» – я-то знаю что аттрибуты означают множественное наследование, но другие разработчики об этом вряд ли догадаются, даже если я все задокументрую. Второй проблемой являются коллизии и прочие проблемы которые появятся как только мы начнем серьезно использовать этот постпроцессинг. Например та же проблема diamond inheritance, для которой придется писать полноценный парсер для IL. Кстати о парсерах – поиск в интернетах навел меня на этот проект, но файлов для него я найти не смог, да и других аналогичных проектов тоже не нашлось.

В заключение хочу сказать, что это был очередной интересный эксперимент, который хоть и «сработал» в плане implementation inheritance, но потребовал бы месяц-другой кропотливой работы для того чтобы стать production-ready. Стоит ли это усилий? Конечно нет.  ■

5 responses to “Множественное наследование в C# с использованием IL(D)ASM”

  1. WOW :)
    Класс .. но тогда вопрос – для чего нужен C# ?
    А если будут изменения в логике …

    1. Ну C# нужен потому, что нужно в каком-то языке программировать. Для примера выше это в принципе не критично, можно было написать и на VB, например.

      Что касается изменений в логике, все ОК пока не натыкаешься на грабли вроде diamond inheritance. Конечно в сложных сценариях придется что-то дописать. Но для простах ситуаций типа “скопировать начинку класса А в класс Б” вполне себе пойдет.

  2. В итоге мы получили класс:

    Dragon: IBird, ILizard

    Если честно то множественного наследования тут не видно.

    Статью было бы корректнее назвать “Автоматизация наследования интерфейсов с помощью кодогенерации”. )))

    1. смотри Mono.Cecil для твоих целей как раз (типизированный анализ сброк, классов, типов, правка на уровне IL без загрузки средствами .Net)

  3. Всё это фигня, т.к. даже при использовании атрибутов, приходится писать NotImplemented заглушки – в чём тогда профит по ср. с декоратор??
    Мне кажется, уж если извращаться с такими вещами, так каким-нибудь T4! (ну, или Nemerle :) ).

Оставить комментарий