Как стать автором
Обновить

Фантазии на тему метаклассов в C#

Время на прочтение6 мин
Количество просмотров7.5K
Программистам вроде меня, которые пришли в C# с большим опытом работы в Delphi, часто не хватает того, что в Delphi принято называть ссылками на класс (class reference), а в теоретических работах – метаклассами. Я несколько раз натыкался в разных форумах на обсуждение, проходящее по одной схеме. Начинается оно с вопроса кого-то из бывших дельфистов на тему того, как сделать метакласс в C#. Шарписты просто не понимают вопроса, пытаются уточнить, что это за зверь такой – метакласс, дельфисты как могут объясняют, но объяснения краткие и неполные, и в итоге шарписты остаются в полном недоумении, зачем всё это нужно. Ведь то же самое можно сделать с помощью рефлексии и фабрик класса.

В этой статье я попытаюсь всё-таки рассказать, что такое метаклассы, тем, кто с ними никогда не сталкивался. Дальше уже пусть каждый сам решает, хорошо ли было бы иметь такую вещь в языке, или рефлексии достаточно. Всё, что я тут пишу, это именно фантазии на тему того, как оно могло бы всё быть, если бы в 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# сильно выиграл бы, если бы в нём появились метаклассы.
Теги:
Хабы:
Всего голосов 15: ↑14 и ↓1+13
Комментарии67

Публикации

Истории

Работа

Ближайшие события