Я хочу множественое наследование в 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
и аннигилировать их, т.к. для наших целей они не особенно полезны.
Магический алгоритм
Алгоритм замены реализации примерно такой:
-
Находим класс у которого есть аттрибут(ы)
InheritAttribute
. Удаляем их полностью т.к. они нам не особо нужны. (В последствии можно также удалить сам класс.) -
Поскольку каждый аттрибут предоставил нам мэппинг Интерфейс→Класс, для начала можно удалить все заглушки этих интерфейсов – они нам больше не нужны :)
-
Теперь можно на место этих заглушек поставить собственно реализацию. Естественно конструкторы вставлять не надо, и с конфликтами тоже надо быть поосторожней (я имею ввиду explicit interface implementation и те проблемы, который могут быть от diamond inheritance).
-
Вот собственно и все, можно перекомпилировать.
После подобный манипуляций, мы получим определение 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. Стоит ли это усилий? Конечно нет. ■
Оставить комментарий