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

Программистам вроде меня, которые пришли в 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# сильно выиграл бы, если бы в нём появились метаклассы.
Поделиться публикацией

Похожие публикации

AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама

Комментарии 67

    0
    Возможно пригодилось бы. Только непонятно если это typeof(A2) типа Type<A2>, то привести к Type<A> не получится из за не вариативности классов.
      0

      Для тех единичный случаев, когда нужны мета классы, можно задействовать рефлексия. Скорее всего, поэтому шарп не реализует все прикольные штуки, которые можно придумать.


      Но идея интересная. Предлагаю вам еще добавить динамическое множественное наследование и написать транспайлер ака TypeSharp.

        0
        я всегда думала, что c# сам по себе — прикольная штука)
        «динамическое множественное наследование» добавить, что это привнесёт, на ваш взгляд?
          0
          Меня всегда удивляло, почему Хейлсберг, который является главным разработчиком и Delphi, и C#, не стал делать динамическое множественное наследование в C#, хотя оно так хорошо зарекомендовало себя в других языках. Все примеры его использования не так сложно переделать, используя только те средства, которые есть в языке уже сейчас. Но всё это будет работать заметно медленнее, чем могло бы с динамическим множественным наследованием, и правильность вызовов будет проверяться во время выполнения, а не компиляции. Так что моё личное мнение — C# сильно выиграл бы, если бы в нём появилось динамическое множественное наследование.
        0
        В этом коде класс C не должен откомпилироваться, потому что у него нет конструктора с параметрами int x, int y, а вот класс B откомпилируется без ошибок.

        В C# класс С тоже не откомпилируется, он тоже обязан вызвать базовый конструктор через : base(..).


        Возможен ещё один вариант: если в наследнике не перекрыт виртуальный конструктор предка, компилятор автоматически перекрывает его

        Я не вижу ни одного плюса такой непонятной реализации.


        А вот если у нас будут метаклассы, то код с неправильными параметрами просто не откомпилируется.

        Все, что от C# требуется, это добавить Method<T>() where T : class. new (int, string), но мелкомягкие уже что-то вроде "сложно и не нужно" писали на этот счет.


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

          0

          new T() компилируется в Activator.CreateInstance(typeof(T)), так что особых преимуществ ограничение new с параметрами не даст, можно вызвать Activator.CreateInstance руками. Проверку наличия конструктора на этапе компиляции несложно реализовать с помощью Roslyn analyzer.

            0
            Я не вижу ни одного плюса такой непонятной реализации.

            Ну, собственно, плюс не относится непосредственно к теме метаклассов. Меня очень задалбывает одна штука в C#. Представьте, что у вас есть класс, в котором определены десять конструкторов. Вам надо написать наследник этого класса. В наследнике только перекрывается один виртуальный метод. Новые поля и свойства не добавляются, поэтому код инициализации писать не нужно. Но все десять конструкторов вам придётся вручную перекрыть, и код каждого конструктора будет состоять только из вызова аналогичного унаследованного конструктора. Мне очень не нравится такая рутина, тем более что она резко контрастирует с тем, к чему я привык в Delphi, где производный класс автоматически наследовал все конструкторы предка. Например, чтобы объявить свой класс исключения, в Delphi достаточно написать:
            type TMyException = class(TException);

            И всё, все конструкторы TException будут доступны TMyException. Сравните это с тем, как аналогичное объявление будет выглядеть в C#. Поэтому хотелось бы, чтобы хотя бы для классов без новых полей и свойств конструкторы наследовались автоматически, чтобы наоборот, надо было явно указывать, если какой-то унаследованный конструктор не нужен.

            Все, что от C# требуется, это добавить Method<T>() where T: class. new (int, string)

            Согласен с тем, что ограничения дженериков недостаточно гибкие. Я бы ещё добавил, например, ограничения по тому, какие определены операторы для типа, разделил бы классы и интерфейсы. Но предложенный вами метод не решает всех проблем, хотя существенно облегчает жизнь.

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

            А вот с этим не согласен. Мне приходится писать не только готовые приложения, но и библиотеки, которыми пользуются потом другие люди. И нередко возникает необходимость сделать что-то как в WinForms или WPF, где пользователь может написать свой собственный UserControl, а библиотека будет работать с этим классом так же легко, как со своими внутренними классами. Вот здесь и нужна или рефлексия, или метаклассы, или ещё какой-то способ динамически получать метаинформацию о классе и уметь его создавать.
              0
              Я не очень понимаю подход, когда библиотека должна инспектировать вызывающее её приложение и создавать оттуда классы. Хотя бы потому, что классы в приложении могут создаваться хитрым способом с выделением сырой памяти.

              Обычно делают так. В библиотеке есть абстрактный базовый класс или интерфейс, который должен реализовать клиент. Клиент сам создаёт наследников этого класса и передаёт в функции библиотеке. Почему такое решение вас не устраивает?
            +3
            Классовые методы – это ещё одна конструкция, которая есть в Delphi, но отсутствует в C#. Эти методы при объявлении помечаются словом class и являются чем-то средним между статическими методами и методами экземпляра.

            Я может что-то неправильно понял, но что мешает использовать extension methods?
              0
              Методы расширения не могут обеспечить настоящий полиморфизм. Чтобы код мог вызвать такой метод, он должен быть доступен на момент компиляции этого кода. А классовые методы могут быть виртуальными в полном смысле слова, т.е. код умеет работать только с некоторым общим предком, но этого достаточно, чтобы вызывать переопределённые методы, которые будут написаны уже после компиляции этого кода.
                0

                Ок, разница понятна. Но если честно у меня всё ещё есть сомнения в необходимости такого в С#. Ну с той точки зрения что не могу вспомнить ни один "use case", где такое могло пригодится и который не решался уже доступными в С# фичами.
                Зато вижу пару "проблем", которые кассовые методы легко могут создать.


                П. С. Хотя могу себе конечно представить что есть какие-то сценарии, где без классовых методов ну совсем не обойтись, но они просто лежат совсем в другой плоскости разработки.

                  0
                  Зато вижу пару «проблем», которые кассовые методы легко могут создать.

                  Это какие же?
                    0
                    Ну например то же самое изменение поведения класса после компиляции :) Это обоюдоострый клинок и с таким всегда нужно быть аккуратным. И не уверен что не будет проблем с юнит-тестингом.

                    И да, я согласен что если какая-то фича кажется тебе не особенно безопасной, то можно её просто не использовать. И если бы в C# уже были классовые методы и/или были проблемы/ситуации, которые без них ну вообще не решить, то никаких «претензий» к ним у меня бы не было.

                    Но пока же я вижу просто желание добавить что-то в C# чтобы определённой(и на мой взгляд относительно небольшой) группе людей просто было удобнее/привычнее работать с новым языком.
              +2
              Что-то не представляю когда бы мне эта возможность могла пригодится…
                –1
                В Delphi нет дженериков, и поэтому можно было передавать тип как параметр.
                type
                  TAnimal = class
                  public
                    constructor Create();
                  end;
                  TCat = class(TAnimal)
                  end;
                  TDog = class(TAnimal)
                  end;
                  TAnimalClass = class of TAnimal;
                
                function CreatePet(t: TAnimalClass): TAnimal;
                begin
                  result := t.Create();
                end;
                
                cat := CreatePet(TCat);
                dog := CreatePet(TDog);


                В C#, я думаю, достаточно средств, чтобы записать это другими способами
                public interface IAnimalTraits {
                    IAnimal Create();
                };
                
                public class DogTraits : IAnimalTraits {
                    public IAnimal Create() { return new Dog(); }
                };
                
                
                IAnimal CreatePet(IAnimalTraits t) {
                    return t.Create();
                }
                
                var cat = CreatePet(new CatTraits());
                var dog = CreatePet(new DogTraits());
                  0
                  В Delphi нет дженериков,

                  Э? Вы про какую версию Delphi? Они даже в FPC есть

                    –1
                    Классическую ))) 3,5,7…
                      0
                      Хмм, забудьте уже про мамонтов :) Дженерики есть и уже очень давно.
                  0

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


                  Вместо:


                  interface IPlugin
                  {
                      void DoWork();
                  }
                  
                  class PluginAttribute : Attribute
                  {
                      public string Name { get; }
                      public PluginAttribute(string name)
                      {
                          Name = name;
                      }
                  }
                  
                  [Plugin("My plugin")]
                  class MyPlugin : IPlugin
                  {
                      void DoWork() {};
                  }
                  
                  string GetPluginName(Type type)
                  {
                      return type.GetCustomAttribute<PluginAttribute>().Name;
                  }

                  можно будет написать:


                  interface IPlugin
                  {
                      static string Name { get; }
                      void DoWork();
                  }
                  
                  class MyPlugin : IPlugin
                  {
                      static string Name => "My plugin";
                      void DoWork() {};
                  }
                  
                  string GetPluginName(Type<IPlugin> type)
                  {
                      return type.Name;
                  }

                  Впрочем, есть ещё вариант с вызовом статических методов через рефлексию и реализацией проверки их наличия с Roslyn analyzer.
                  Также встречал вариант с указанием класса-дескриптора в атрибуте, экземпляр которого создаётся для получения мета-информации. В этом варианте тоже нужен Roslyn analyzer для проверки, что дескриптор реализует интерфейс.
                  Ещё можно сделать виртуальные методы в PluginAttribute, этот вариант позволит вернуть мета-данные любого типа с проверкой на этапе компиляции, но несколько неочевидно в применении.

                    0
                    Хм, а что мешает использовать генерики и реализовать «второй вариант» через них?

                    upd. Прошу прощения, неправильно прочитал пример. Тут действительно наверное придётся через рефлекию в том или ином виде.
                      0
                      можно будет получать её непосредственно из статических свойств.

                      Ничего хорошего в этом нет. Метаинформация, нужная только для движка плагинов, смешивается с обычными полями и методами.
                        0
                        Ничего хорошего в этом нет. Метаинформация, нужная только для движка плагинов, смешивается с обычными полями и методами.

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

                          Не понял вашу мысль. Все плагины должны реализовывать какой-нибудь контракт IPlugin. А как реализовывать — дело плагина. Вот и всё.
                            0
                            Если бы всё было так просто! В вашем варианте прежде чем начать хоть как-то работать с плагином, надо его создать, получить какую-то информацию о плагине, не создавая его, не получится. Это не всегда удобно — процесс создания плагина может быть дорогим или проводить к изменениям в пользовательском интерфейсе. А ещё это лишние операции выделения и освобождения динамической памяти, чего я очень не люблю, так как они, особенно при многократном повторении, становятся достаточно затратными.
                              0
                              И не надо усложнять. Вам уже ответили про фабрику, как вариант. Она может быть скрыта в реализации плагина. Например
                              IPlugin
                              {
                              void Init();
                              }
                              В методе инит вся тяжелая логика, она может быть в подкапотной фабрике, может не быть — детали реализации. Что касается цены, метакласс тоже не бесплатный, и неизвестно, что в итоге будет дешевле.
                                0
                                Про фабрику как вариант я и сам написал в статье. И написал, почему этот вариант меня не устраивает. Главная претензия — проверки того, что плагин вместе с фабрикой выполняет все требования контракта приходится переносить с этапа компиляции на этап выполнения. Классовые методы и метаклассы могли бы формализовать контракт настолько, что его мог бы проверить компилятор.

                                Вот, например, ситуация, когда фабрики бесполезны. Задача, кстати, вполне реальная, если вы вдруг знаете, как её можно решить изящнее, чем это сделал я, буду весьма признателен.

                                Есть некоторый универсальный класс
                                public class Container<T> where T : BaseT
                                где BaseT — некоторый абстрактный тип. Типы Container и BaseT описаны в библиотеке, наследников BaseT и производные от универсального Container будет создавать пользователь библиотеки, на этапе компиляции библиотеки они неизвестны. Есть задачи, требующие метаописания класса Container<T>, причём это метаописание зависит от того, как реализован класс T, т.е. для него тоже нужно метаописание (например, это нужно, чтобы правильно распарсить строку, в которой хранится значение Container<T>; реализовать парсинг статическим методом Container<T> не очень удобно, потому что такой метод потом без рефлексии не вызовешь, так как вызывающий код в общем случае не знает, с какой именно производной от Container<T> ему придётся работать, а рефлексия нежелательна из-за медленной скорости).

                                Имея классовые методы, я бы сделал очень просто. В классе BaseT объявил бы абстрактный классовый метод для получения метаинформации. Соответственно, в любом потомке BaseT обязательно надо было бы перекрывать его. Далее, я сделал бы абстрактного неуниверсального предка ContainerBase для Container<T>, в котором тоже объявил бы абстрактный классовый метод для метаинформации, а в Container<T> реализовал бы этот метод с учётом метаинформации о типе T. И когда возникала бы необходимость получить информацию о конкретной производной от Container<T>, я бы получил её, вызвав этот классовый метод через метакласс для ContainerBase, и за счёт полиморфизма получил бы информацию о нужном классе. Бинго!

                                Как пришлось реализовывать это имеющими средствами. Во-первых, для наследников BaseT придуманы атрибуты, с помощью которых разработчик, создающий этих наследников, описывает метаинформацию для своего класса. Container<T> реализован так (ContainerInfo — это некоторый тип, содержащий информацию о нём):
                                
                                public abstract ContainerBase
                                {
                                    protected static Dictionary<Type, ContainerInfo> meta
                                        = new Dictionary<Type, ContainerInfo>();
                                
                                    public static IReadOnlyDictionary<Type, ContainerInfo> Meta
                                        { get => meta; }
                                }
                                
                                public class Container<T> : ContainerBase
                                {
                                    static Container()
                                    {
                                         ContainerInfo info = new ContainerInfo();
                                         // Здесь анализируются атрибуты класса T
                                         // и заполняются свойства info
                                         meta[typeof(Container<T>)] = info;
                                    }
                                }
                                

                                Чтобы получить метаинформацию о некотором контейнере, тип которого содержит переменная Type t, нужно выполнить конструкцию
                                ContainerBase.Meta[t]

                                Это вполне работоспособно, хотя выглядит несколько коряво. Но главное, что мне не нравится, это то, что компилятор даже не чихнёт, если забыть назначить наследнику BaseT нужные атрибуты, это вскроется только во время выполнения программы.

                                Вот такая задача из реальной жизни. Есть у вас вариант решения такой задачи?
                                  0

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

                                    0
                                    Всё упирается в ваше нежелание создавать экземпляр класса.


                                    Да, именно так. А вам не кажется неестественным создавать экземпляр только для того, чтобы получить данные, которые по сути не привязаны к этому экземпляру и никак не зависят от его существования или не существования? Вы находите такое положение вещей естественным и простым?
                                      0
                                      Если эта информация общая для всех объектов одного класса и не зависит от их состояния, то запихайте эту информацию в static и ничего создавать не надо будет.

                                      Или я опять не уловил какие-то ньюансы?
                                        0
                                        В общем-то, я уже отвечал на этот вопрос, опишу подробнее. Итак, типы T создаёт пользователь, в типичном случае применения библиотеки их придётся создавать 15-20 штук, а то и больше. Две проблемы:
                                        1. В каждом типе пользователь должен не забыть описать статический член определённого формата. Заставить компилятор проверить, что пользователь не забыл и не перепутал, невозможно.
                                        2. Библиотека не сможет использовать полиморфизм при работе с этими типами. Имея описание одного из типов T в виде Type, библиотека не доберётся до нужного статического члена без использования рефлексии.

                                        В общем-то, оба этих недостатка присущи и моему решению с атрибутами, так что выбор между атрибутами и статикой делается из чисто эстетических соображений. К сожалению, ничего лучшего C# не предлагает.
                                          +1
                                          А в случае с метаклассами и пунктом «1» можно быть на 100% уверенным что пользователь всё сделает правильно?

                                          И да рефлексии и статика не то чтобы удачное решение. И не только с точки зрения эстетики. Но опять же возвращаемся к тому что добавление метаклассов само по себе может быть не особо удачным решением в контексте всего языка :)

                                          П.С. И если забыть про метаклассы и просто обсуждать систему плагинов в С#, то не проще будет взять какой-нибудь готовый фреймворк вроде того же MEF? :)
                                            0
                                            в случае с метаклассами и пунктом «1» можно быть на 100% уверенным что пользователь всё сделает правильно?
                                            Да, если в базовом классе объявить статический метод как virtual; abstract; то в наследниках программист обязан будет его определить. Довольно забавно выглядит модификатор virtual у static-метода, но тем не менее, в дельфи это работает.
                                              0
                                              Ок, согласен, проблема с «забыл» действительно решается, но «перепутать» он всё ещё может :)
                                                0
                                                От «перепутал» вообще средств нет. Надо, допустим, к числу прибавить 2, а программист промахнулся и напечатал 3. Ни один компилятор от такого не спасёт.
                                                0
                                                Виртуальными в Delphi можно делать не статические, а классовые методы. Статические методы в Delphi тоже есть (появились в поздних версиях, возможно, под влиянием C#), но их виртуальными делать, естественно, нельзя.
                                                  +1
                                                  А зачем вообще статические методы, если их полностью перекрывает функционал классовых?
                                                    0
                                                    Не знаю. Особенно если учесть, что в Delphi есть ещё простые процедуры и функции, вообще не привязанные ни к какому классу.

                                                    Если я правильно помню, статические методы появились, когда Delphi пытались переделать под .NET. Видимо, для совместимости с другими .NET-языками.

                                      0
                                      Возможно, не до конца вник в вашу задачу, но почему вы зациклились именно на мета? Вот решение без статиков и атрибутов. Добавляем в BaseT абстрактный метод(свойство), который предоставляет нужную для работы с ним информацию, и дёргаем его в контейнере, когда нам эта информация понадобилась.
                                      Статики вообще надо использовать по минимуму.
                                        0
                                        Поддержу предыдущего оратора.
                                        Гражданин Мартин, который Роберт Мартин, описывая смысл архитектуры, исходит из того, что в хорошей архитектуре основная идея — отложить решения о деталях. Статики — они про раннее связывание в любом случае.
                                          0
                                          Для этого нужно иметь экземпляр наследника. Если он не возникает естественным образом (а в моей задаче не возникает), его приходится создавать. Это приемлемо работает только при выполнении двух условий:
                                          1. Создание экземпляра стоит не очень дорого.
                                          2. У создания нет побочных эффектов.

                                          Если хотя бы одно из этих условий не выполняется, вариант с созданием не проходит. Плюс мы должны гарантировать, что у любого наследника BaseT должен быть конструктор с определённым фиксированным набором параметров, что тоже не всегда удобно.
                                          0
                                          Есть у вас вариант решения такой задачи?
                                          Есть довольно прямолинейный способ: без статиков, без контейнера, с полной проверкой в compile-time

                                          Spoiler header
                                              public abstract class BaseT
                                              {
                                              }
                                          
                                              public abstract class BaseMeta<T>
                                                  where T : BaseT
                                              {
                                                  public abstract string GetInfo();
                                              }
                                          
                                              public class SimpleT : BaseT
                                              {
                                              }
                                          
                                              public class SimpleMeta : BaseMeta<SimpleT>
                                              {
                                                  public override string GetInfo()
                                                  {
                                                      return "It's simple";
                                                  }
                                              }
                                          
                                              public class Container<T, TMeta>
                                                  where T : BaseT
                                                  where TMeta : BaseMeta<T>, new()
                                              {
                                                  public static string GetMeta()
                                                  {
                                                      return new TMeta().GetInfo();
                                                  }
                                              }
                                          

                                            0
                                            Метакласс можно и закешировать, это ещё уменьшит оверхед, и наверное, сделает его сопоставимым с дельфийским метаклассом
                                            Spoiler header
                                                public class Container<T, TMeta>
                                                    where T : BaseT
                                                    where TMeta : BaseMeta<T>, new()
                                                {
                                                    static readonly TMeta meta = new TMeta();
                                            
                                                    public static string GetMeta()
                                                    {
                                                        return meta.GetInfo();
                                                    }
                                                }
                                            

                                              +2
                                              А ещё можно сделать BaseT интерфейсом, SimpleT структурой, а на контейнер наложить соответствующие ограничения, и тогда memory traffic ещё снизится.
                                                0
                                                Если говорить о моей реальной задаче, то в ней вообще нет никакого BaseT, это я его тут написал для того, чтобы было удобнее обсуждать. В моём же случае Container<T> никогда не создаёт экземпляры T и не использует созданные кем-то другим. Единственное, что ему нужно от T — прочитать его атрибуты. Поэтому T может быть классом (даже абстрактным), интерфейсом, структурой или перечислением — это ни на что не повлияет.
                                                0
                                                И пользователю библиотеки вместо одного типа придётся описывать два, да ещё и Container приобретает второй параметр, бессмысленный для пользователя, нужный только для поддержания инфраструктуры. Мне бы не понравилось пользоваться такой библиотекой. Я уж не говорю о том, что для одного T можно породить двух наследников BaseMeta и породить два разных Container с одним типом, но разной метаинформацией о нём. В каких-то случаях такая возможность, может быть, даже окажется полезной, но в моём случае может привести к неприятным ошибкам.
                                                  0
                                                  Так-то в C# можно служебный класс вкладывать внутрь класса T, чтобы он не торчал в общем неймспейсе, но у контейнера всё равно будет два параметра.

                                                  Тут лучше всего подойдёт C++ — у него в шаблонах тупо синтаксическая подстановка, и у класса-параметра T можно хоть статический метод вызывать, хоть к переменной обращаться, и всё без оверхеда в runtime
                                                  template<class T>
                                                  class Container {
                                                  public:
                                                      std::string getMetaInfo() { return T::metaInfo(); }
                                                  };
                                                  
                                                  class SimpleT {
                                                  public:
                                                      static std::string metaInfo() { return "simple"; }
                                                  };
                                                0
                                                Не сработал этот способ. Статический конструктор вызывается при первом вызове какого-то метода, оператор typeof его не вызывает, поэтому оказалось нельзя получить метаинформацию о Container сразу после создания типа. Пришлось выкручиваться: из Container статический конструктор выкинул, в ContainerBase вместо свойства Meta сделал статический метод GetMeta(Type t), который проверяет наличие информации о типе t в словаре meta, и если её там нет, выполняет анализ, который раньше был в конструкторе Container, и помещает информацию в словарь. В общем, выкрутился, но сколько же лишних телодвижений из-за того, что C# не обеспечивает полиморфизм на уровне типа!
                                        0
                                        Зачем сопоставлять плагины и фабрики?

                                        Программа работает только с фабрикой, и напрямую класс плагина не знает, и он ей не нужен. Вся метаинформация о классе плагине, которая должна быть доступна до создания экземпляра плагина, должна быть доступна через фабрику.
                                          0
                                          Фабрики класса мне не нравятся по следующим причинам:

                                          1. Это дополнительная нагрузка на того, кто будет писать плагины: надо написать не только плагин, но и фабрику к нему.
                                          2. Встаёт вопрос о том, как создавать сами фабрики. Они создаются либо через рефлексию со всеми вытекающими отсюда последствиями, либо ответственность за их создание переносится на разработчиков плагинов, что ещё больше усложняет работу с библиотекой.
                                          3. Экземпляры фабрик — это затраты на их хранение в памяти, перемещение при компрессии и т.п. Если этого можно избежать, почему бы этого не сделать (правда, этот пункт спорный — он связан с тем, что я ещё помню, как программировать на ZX Spectrum с 48 кБ памяти, поэтому стараюсь её если не экономить, то хотя бы не использовать совсем уж бездумно).
                                          4. Ну и, наконец, главное — у нас появляются две сущности — класс и его фабрика, которые тесно связаны между собой. В общем случае фабрика должна уметь сообщить, объекты какого класса она создаёт, а класс — какая фабрика ему нужна. И то, что они о себе сообщают, должно соответствовать тому, как они реализованы. Это заставляет разработчика плагина каждый раз выполнять рутинную работу, в которой легко сделать ошибку, не отлавливаемую компилятором. Я же предпочитаю избегать рутинной работы, пусть такие вещи делает сам компьютер. Или хотя бы пусть он проверяет, не сделал ли я какую-нибудь глупую ошибку. Метаклассы — это шаг как раз в таком направлении.

                                          И вопрос к вам. UserControl в WinForms и WPF — это, по сути, тот же плагин. Но разработчики этих библиотек предпочли обойтись без фабрик. Как вы думаете, почему? И было ли бы лично вам удобнее, если бы это реализовали через фабрики?
                                            +1
                                            1. Затраты одинаковы. Писать статические методы в классе плагина, либо методы в классе фабрики.
                                            2. Хоть один класс придется искать.
                                            3. Фабрика не обязана содержать огромное состояние. Экземпляр будет занимать всего 12 байт (24 на х64)
                                            4. В общем случае у фабрики будет всего один обязательный метод: IPlugin Create(); А плагин о фабрике не будет знать ничего, это не его обязанность.

                                            UserControl — просто базовый класс, используемые наследники известны на этапе компиляции.

                                              0
                                              Прошу прощения, с WinForms я лоханулся — вылетело из головы, что эта библиотека устроена не так, как дельфийская VCL, и при программировании на WinForms дизайнер сразу пишет код, где создаются нужные объекты. В VCL дизайнер пишет информацию о размещённых контролах в ресурсы, а при запуске программа их разбирает, там действительно без метаинформации никак.

                                              Если вас интересует дальнейшая дискуссия, то прошу продолжить её в ветке ответа habr.com/ru/post/464141/#comment_20540683 — там я изложил новые аргументы.
                                              +1
                                              И вопрос к вам. UserControl в WinForms и WPF — это, по сути, тот же плагин.
                                              Это плагин для IDE, но не плагин для программы, куда включен UserControl.
                                              Но разработчики этих библиотек предпочли обойтись без фабрик
                                              И без метаклассов.
                                              Как вы думаете, почему?
                                              Потому что нет требований к производительности. В IDE можно пользоваться рефлексией.
                                              И было ли бы лично вам удобнее, если бы это реализовали через фабрики?
                                              Тут вопрос требований. Если нужно создавать миллион объектов в секунду, без фабрики не обойтись. Если плагин загружается однократно при старте, тут открытый вопрос — ускорить загрузку, используя фабрики, или добавить удобства разработчикам плагинов.
                                                0
                                                Прошу прощения, с WinForms я лоханулся — вылетело из головы, что эта библиотека устроена не так, как дельфийская VCL, и при программировании на WinForms дизайнер сразу пишет код, где создаются нужные объекты. В VCL дизайнер пишет информацию о размещённых контролах в ресурсы, а при запуске программа их разбирает, там действительно без метаинформации никак.

                                                Если вас интересует дальнейшая дискуссия, то прошу продолжить её в ветке ответа habr.com/ru/post/464141/#comment_20540683 — там я изложил новые аргументы.
                                    0
                                    не там ответил
                                      0
                                      Для меня осталось нераскрытым, таки зачем это может пригодиться в C#?
                                      Перегрузка статических методов? Если есть разные реализации какой-либо абстракции, то про статику нужно забыть.
                                      Виртуальный конструктор? Паттерн фабрика.
                                      Кажется, правильно не стали затаскивать.
                                      Может быть, тут дело в том, что в Delphi (в тех версиях, которые делал ещё Хейлсберг) практически полностью отсутствует рефлексия, и альтернативы метаклассам просто нет, чего нельзя сказать о C#.

                                      Скорее всего.
                                      А в целом интересно, спасибо)
                                        +1
                                        Класс как объект высшего порядка… Есть наверное во всех динамических языках, где есть классы. Потому как можно творить любую дичь на этапе выполнения. Забавно, что в Delphi она была во время компиляции. Хотя в Pascal были и другие плюшки в системе типов, по которым я иногда скучаю, например enumerated types, subranges.
                                          0
                                          Не знаю, всё это выглядит больше как пародия на метаклассы. Почему у вас метакласс — не класс?

                                          В результате получаем существенное усложнение языка, так как кучу понятий приходится дублировать.

                                          Если уж в C# и затаскивать метаклассы, то было бы разумно сделать их так, как в языках, где они таки являются классами. И используются, в частности, при создании, собственно, классов. Это же, собственно, стандарт — в большинстве языков, где есть метаклассы классы — это обычные объекты, а метаклассы, соотвественно, обычные классы. Delphi и Java/.NET — два ущербных варианта (но ущербных по разному).

                                          Из распространённых — можете посмотреть, хотя бы, на Python

                                          P.S. Вообще метаклассы в Delphi (как и многое другое в современном Delphi) оставляют «неприятный привкус во рту». Зачем вообще понятие «конструктор»? Почему это не просто функция класса (возможно виртуальная)? Да, я знаю ответ (потому что так исторически сложилось)… и тем не менее — это некрасиво и запутанно.
                                            0
                                            Почему у вас метакласс — не класс?

                                            Как же не класс? Он у меня наследник типа Type, а Type — это класс. Или вы что-то другое имели ввиду?
                                              0
                                              Я имею в виду, что его методы не описываются как методы класса, что у него не может быть своего состояния и так далее. То есть это — не обычный класс, который управляет другим классом, а некоторая странная конструкция, которую компилятор собирает «в кучку» из разрозненных кусков.

                                              Ну вот смотрите как это в Python сделано:
                                              class Meta(type):
                                                  def __new__(cls, name, bases, dct):
                                                      x = super().__new__(cls, name, bases, dct)
                                                      x.attr = 100
                                                      x.foo = lambda a : a * a
                                                      return x
                                              
                                              class Foo(metaclass=Meta):
                                                  pass
                                              
                                              print(Foo.attr)
                                              # 100
                                              print(Foo.foo(2))
                                              # 4
                                              То есть метакласс — это просто класс, ну вот совершенно обычный класс — с одном исключением: он используется тогда, когда другой класс создаётся. И может там понаделать конструкторов, деструкторов и массу чего ещё — в принципе все возможности языка могут быть использованы.

                                              В C++ метаклассов (пока?) нету — но пропозал следует той же идее.

                                              А в Delphi (и у вас) метакласс — это что-то такое странное, создаваемое компилятором только для того, чтобы сделать наследование конструкторов и виртуальные конструкторы (без явных фабрик), фактически.
                                                0
                                                Из-за полного незнания Питона я лишь поверхностно понял ваш пример. Но, вероятно, вы правы. Я тоже в целом предпочитаю более гибкие и универсальные конструкции, а не специализированные, просто в статье я воспроизвёл тот механизм, который хорошо знаю.

                                                А так даже интересно получается: если метаклассы — это частный случай классов, можно ведь создавать метаклассы для метаклассов, правильно? Не знаю, зачем это может понадобиться, но иметь средства, достаточно гибкие для того, чтобы реализовать такое, мне бы понравилось.
                                                  +1
                                                  А так даже интересно получается: если метаклассы — это частный случай классов, можно ведь создавать метаклассы для метаклассов, правильно?
                                                  Да, конечно. Цепочка не уходит в бесконечность, впрочем, так как у стандартного класса Metaclass его metaclass — это тоже Metaclass.

                                                  Не знаю, зачем это может понадобиться, но иметь средства, достаточно гибкие для того, чтобы реализовать такое, мне бы понравилось.
                                                  На LISP посмотрите. Собственно там метаклассы и появились много лет назад. И да — это имеет смысл. Например для AST вам потребуются десятки типов — и при этом у них могут быть разные метаклассы. Если из окажется много, то можно создать и метакласс для них… на практике я нечто подобное видел только в FORTH: классов и метаклассов там нет, зато есть «компилирующие слова»… и иногда там бывают трёх-четырёхступенчатые иерархии…

                                                  P.S. Но вообще у этих технологий есть проблема: примерно у 5-10% при взгляде на них «загораются глаза», они говорят «ух как клёво» и начинают пользовать. Но 90%-95% ничего не готовы изучать и так далее. А так как решает-таки большинство, то… имеем то, что имеем.
                                            0
                                            Непонятно зачем всё это нужно. Тема для чего это нужно не раскрыта совсем.

                                            Вообще говоря в язык можно понапихать огромное количество фич, с «Дом Советов». Язык распухнет до невозможности, его будет трудно читать и использовать, так что каждая фича должна быть обоснована и выверена. Кроме метаклассов ведь можно еще огромное количество фич включить в С#, они и включаются потому что они более очевидно полезные чем метаклассы, так что C# уже давно не тот компактный язык.
                                              0
                                              Непонятно зачем всё это нужно. Тема для чего это нужно не раскрыта совсем.

                                              Я думал, что это более-менее очевидно. Но, похоже, просчитался — не все сталкиваются с такими задачами, где это может быть полезно. Собственно, на этот вопрос уже хорошо ответили в этом комментарии.
                                              0
                                              Чтобы достичь подобного полиморфизма без метаклассов и виртуальных классовых методов, для класса X и каждого из его наследников пришлось бы писать вспомогательный класс с обычным виртуальным методом


                                              Не могли бы вы поподробней разъяснить, почему необходимо избежать использования виртуальных классовых методов?

                                              Правильно ли я понял, что решаемая проблема крайне похожа на double-dispatching, реализуемый, в частности, паттерном Visitor?
                                                0
                                                Не могли бы вы поподробней разъяснить, почему необходимо избежать использования виртуальных классовых методов?

                                                Я так понял, что под виртуальными классовыми методами вы имели ввиду виртуальные методы экземпляра. Ответ такой: потому что их использование требует создания экземпляра класса, и если этот класс неизвестен на момент компиляции использующего его кода, получаем все те же проблемы.

                                                Правильно ли я понял, что решаемая проблема крайне похожа на double-dispatching, реализуемый, в частности, паттерном Visitor?

                                                Только частично. Паттерн Visitor, насколько я его знаю, ориентирован на работу, во-первых, с уже созданными объектами, а во-вторых, с объектами из фиксированного перечня классов. Вопросы создания объектов и динамического расширения перечня классов он не покрывает.

                                                Как я уже писал в другом комментарии, все эти штуки предназначены, в первую очередь, для написания библиотек типа WinForms, при использовании которого можно создать свой UserControl, а библиотека будет с ним работать как с родным. В частности, на метаклассах построена библиотека VCL — дельфийский аналог WinForms (точнее, метаклассы придуманы в Delphi для того, чтобы можно было написать VCL).
                                                  0
                                                  VCL — дельфийский аналог WinForms

                                                  Если точнее, то дельфийский праобраз, учитывая кто дизайнил C# и WinForms.

                                              Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                                              Самое читаемое