В этой статье я попытаюсь всё-таки рассказать, что такое метаклассы, тем, кто с ними никогда не сталкивался. Дальше уже пусть каждый сам решает, хорошо ли было бы иметь такую вещь в языке, или рефлексии достаточно. Всё, что я тут пишу, это именно фантазии на тему того, как оно могло бы всё быть, если бы в C# действительно были метаклассы. Все примеры в статье написаны на этом гипотетическом варианте C#, ни один существующий на данный момент компилятор не сможет их откомпилировать.
Что такое метакласс
Итак, что же такое метакласс? Это специальный тип, который служит для описания других типов. В C# есть что-то очень похожее – тип Type. Но только похожее. Значением типа Type можно описать любой тип, метакласс же может описывать только наследников класса, указанного при объявлении метакласса.
Для этого наш гипотетический вариант C# обзаводится типом Type<T>, являющимся наследником Type. Но Type<T> пригоден только для описания типа T или его наследников.
Поясню это на таком примере:
class A { } class A2 : A { } class B { } static class Program { static void Main() { Type<A> ta; ta = typeof(A); // Это откомпилируется ta = typeof(A2); // Это тоже откомпилируется ta = typeof(B); // Ошибка компиляции – Type<B> несовместим с Type<A> ta = (Type<A>)typeof(B); // Исключение во время работы программы из-за невозможности приведения Type tx = typeof(A); ta = tx; // Ошибка компиляции – нет неявного приведения Type к Type<A> ta = (Type<A>)tx; // Здесь всё нормально Type<B> tb = (Type<B>)tx; // Исключение } }
Приведённый выше пример – это первый шаг к появлению метаклассов. Тип Type<T> позволяет ограничивать то, какие типы могут описываться соответствующим значениям. Эта возможность и сама по себе может оказаться полезной, но на этом возможности метаклассов не исчерпываются.
Метаклассы и статические члены классов
Если некоторый класс X имеет статические члены, то метакласс Type<X> получает аналогичные ему члены, уже не статические, через которые можно обращаться к статическим членам X. Поясним эту запутанную фразу примером.
class X { public static void DoSomething() { } } static class Program { static void Main() { Type<X> tx = typeof(X); tx.DoSomething(); // Тот же результат, что и при вызове X.DoSomething(); } }
Тут, вообще говоря, встаёт вопрос – а что если в классе X будет объявлен статический метод, имя и набор параметров которого совпадает с именем и набором параметров одного из методов класса Type, наследником которого является Type<X>? Есть несколько достаточно простых вариантов решения этой проблемы, но я не буду на них останавливаться – для простоты считаем, что в нашем фантазийном языке конфликтов имён волшебным образом не бывает.
Приведённый выше код у любого нормального человека должен вызывать недоумение – зачем нам нужна переменная для вызова метода, если мы этот метод можем вызвать напрямую? Действительно, в таком виде эта возможность является бесполезной. Но польза появляется, если добавить к ней классовые методы.
Классовые методы
Классовые методы – это ещё одна конструкция, которая есть в Delphi, но отсутствует в C#. Эти методы при объявлении помечаются словом class и являются чем-то средним между статическими методами и методами экземпляра. Как и статические методы, они не привязаны к конкретному экземпляру и могут быть вызваны через имя класса без создания экземпляра. Но, в отличие от статических методов, они имеют неявный параметр this. Только this в данном случае является не экземпляром класса, а метаклассом, т.е. если классовый метод описан в классе X, то его параметр this будет иметь тип Type<X>. И пользоваться им можно будет примерно так:
class X { public class void Report() { Console.WriteLine($”Метод вызван из класса {this.Name}”); } } class Y : X { } static class Program { static void Main() { X.Report() // Вывод: «Метод вызван из класса X» Y.Report() // Вывод: «Метод вызван из класса Y» } }
Пока эта возможность не сильно впечатляет. Но благодаря ей классовые методы, в отличие от статических, могут быть виртуальными. Точнее, статические методы тоже можно было бы сделать виртуальными, но не совсем понятно, что дальше с этой виртуальностью делать. А вот с классовыми методами таких проблем не возникает. Рассмотрим это на таком примере.
class X { protected static virtual DoReport() { Console.WriteLine(“Привет!”); } public static Report() { DoReport(); } } class Y : X { protected static override DoReport() { Consloe.WriteLine(“Пока!”); } } static class Program { static void Main() { X.Report() // Вывод: «Привет!» Y.Report() // Вывод: ??? } }
По логике вещей, при вызове Y.Report должно быть выведено «Пока!». Но метод X.Report не имеет никакой информации о том, из какого класса он был вызван, поэтому выбрать между X.DoReport и Y.DoReport динамически он не может. Как следствие, X.Report всегда будет вызывать X.DoReport, даже если Report был вызван через Y. Смысла делать метод DoReport виртуальным нет никакого. Поэтому C# и не разрешает делать статические методы виртуальными – сделать-то их виртуальными было бы можно, но извлечь пользу из их виртуальности не получится.
Другое дело – классовые методы. Если бы Report в предыдущем примере был не статическим, а классовым, он бы «знал», когда его вызывают через X, а когда через Y. Соответственно, компилятор мог бы сгенерировать код, который выбрал бы нужный DoReport, и вызов Y.Report привёл бы к выводу «Пока!».
Эта возможность сама по себе полезна, но становится ещё более полезной, если к ней добавить возможность вызова классовых переменных через метаклассы. Как-то вот так:
class X { public static virtual Report() { Console.WriteLine(“Привет!”); } } class Y : X { public static override Report() { Consloe.WriteLine(“Пока!”); } } static class Program { static void Main() { Type<X> tx = typeof(X); tx.Report() // Вывод: «Привет!» tx = typeof(Y); tx.Report() // Вывод: «Пока!» } }
Чтобы достичь подобного полиморфизма без метаклассов и виртуальных классовых методов, для класса X и каждого из его наследников пришлось бы писать вспомогательный класс с обычным виртуальным методом. Это требует существенно больших усилий, да и контроль со стороны компилятора будет не столь полным, что увеличивает вероятность где-нибудь ошибиться. Между тем ситуации, когда нужен полиморфизм на уровне типа, а не на уровне экземпляра, встречаются регулярно, и если язык поддерживает такой полиморфизм, это очень полезное свойство.
Виртуальные конструкторы
Если в языке появились метаклассы, то к ним нужно добавить и виртуальные конструкторы. Если в классе объявлен виртуальный конструктор, то все его наследники должны перекрывать его, т.е. иметь собственный конструктор с таким же набором параметров, например:
class A { public virtual A(int x, int y) { ... } } class B : A { public override B(int x, int y) : base(x, y) { } } class C : A { public C(int z) { ... } }
В этом коде класс C не должен откомпилироваться, потому что у него нет конструктора с параметрами int x, int y, а вот класс B откомпилируется без ошибок.
Возможен ещё один вариант: если в наследнике не перекрыт виртуальный конструктор предка, компилятор автоматически перекрывает его, примерно так же, как сейчас он автоматически создаёт конструктор по умолчанию. Оба подхода имеют очевидные плюсы и минусы, но для общей картины это не принципиально.
Виртуальный конструктор можно использовать везде, где можно использовать обычный конструктор. Кроме того, если класс имеет виртуальный конструктор, у его метакласса появляется метод CreateInstance с таким же набором параметров, как у конструктора, и этот метод будет создавать экземпляр класса, как это показано в примере ниже.
class A { public virtual A(int x, int y) { ... } } class B : A { public override B(int x, int y) : base(x, y) { } } static class Program { static void Main() { Type<A> ta = typeof(A); A a1 = ta.CreateInstance(10, 12); // Будет создан экземпляр A ta = typeof(B); A a2 = ta.CreateInstance(2, 7); // Будет создан экземпляр B } }
Другими словами, мы получаем возможность создавать объекты, тип которых определяется на этапе выполнения. Сейчас это тоже можно делать с помощью Activator.CreateInstance. Но этот метод работает через рефлексию, поэтому правильность набора параметров проверяется только на этапе выполнения. А вот если у нас будут метаклассы, то код с неправильными параметрами просто не откомпилируется. Кроме того, при использовании рефлексии скорость работы оставляет желать лучшего, а метаклассы позволяют свести издержки к минимуму.
Заключение
Меня всегда удивляло, почему Хейлсберг, который является главным разработчиком и Delphi, и C#, не стал делать метаклассы в C#, хотя они так хорошо зарекомендовали себя в Delphi. Может быть, тут дело в том, что в Delphi (в тех версиях, которые делал ещё Хейлсберг) практически полностью отсутствует рефлексия, и альтернативы метаклассам просто нет, чего нельзя сказать о C#. Действительно, все примеры из этой статьи не так сложно переделать, используя только те средства, которые есть в языке уже сейчас. Но всё это будет работать заметно медленнее, чем могло бы с метаклассами, и правильность вызовов будет проверяться во время выполнения, а не компиляции. Так что моё личное мнение — C# сильно выиграл бы, если бы в нём появились метаклассы.
ссылка на оригинал статьи https://habr.com/ru/post/464141/
Добавить комментарий