All streams
Search
Write a publication
Pull to refresh
13
0
Григорьев Антон @Vglk

User

Send message
Для этого нужно иметь экземпляр наследника. Если он не возникает естественным образом (а в моей задаче не возникает), его приходится создавать. Это приемлемо работает только при выполнении двух условий:
1. Создание экземпляра стоит не очень дорого.
2. У создания нет побочных эффектов.

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


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

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

Если вас интересует дальнейшая дискуссия, то прошу продолжить её в ветке ответа habr.com/ru/post/464141/#comment_20540683 — там я изложил новые аргументы.
Про фабрику как вариант я и сам написал в статье. И написал, почему этот вариант меня не устраивает. Главная претензия — проверки того, что плагин вместе с фабрикой выполняет все требования контракта приходится переносить с этапа компиляции на этап выполнения. Классовые методы и метаклассы могли бы формализовать контракт настолько, что его мог бы проверить компилятор.

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

Есть некоторый универсальный класс
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 нужные атрибуты, это вскроется только во время выполнения программы.

Вот такая задача из реальной жизни. Есть у вас вариант решения такой задачи?
Из-за полного незнания Питона я лишь поверхностно понял ваш пример. Но, вероятно, вы правы. Я тоже в целом предпочитаю более гибкие и универсальные конструкции, а не специализированные, просто в статье я воспроизвёл тот механизм, который хорошо знаю.

А так даже интересно получается: если метаклассы — это частный случай классов, можно ведь создавать метаклассы для метаклассов, правильно? Не знаю, зачем это может понадобиться, но иметь средства, достаточно гибкие для того, чтобы реализовать такое, мне бы понравилось.
Фабрики класса мне не нравятся по следующим причинам:

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

И вопрос к вам. UserControl в WinForms и WPF — это, по сути, тот же плагин. Но разработчики этих библиотек предпочли обойтись без фабрик. Как вы думаете, почему? И было ли бы лично вам удобнее, если бы это реализовали через фабрики?
Если бы всё было так просто! В вашем варианте прежде чем начать хоть как-то работать с плагином, надо его создать, получить какую-то информацию о плагине, не создавая его, не получится. Это не всегда удобно — процесс создания плагина может быть дорогим или проводить к изменениям в пользовательском интерфейсе. А ещё это лишние операции выделения и освобождения динамической памяти, чего я очень не люблю, так как они, особенно при многократном повторении, становятся достаточно затратными.
Зато вижу пару «проблем», которые кассовые методы легко могут создать.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Information

Rating
Does not participate
Registered
Activity