Comments 67
Для тех единичный случаев, когда нужны мета классы, можно задействовать рефлексия. Скорее всего, поэтому шарп не реализует все прикольные штуки, которые можно придумать.
Но идея интересная. Предлагаю вам еще добавить динамическое множественное наследование и написать транспайлер ака TypeSharp.
«динамическое множественное наследование» добавить, что это привнесёт, на ваш взгляд?
В этом коде класс C не должен откомпилироваться, потому что у него нет конструктора с параметрами int x, int y, а вот класс B откомпилируется без ошибок.
В C# класс С тоже не откомпилируется, он тоже обязан вызвать базовый конструктор через : base(..)
.
Возможен ещё один вариант: если в наследнике не перекрыт виртуальный конструктор предка, компилятор автоматически перекрывает его
Я не вижу ни одного плюса такой непонятной реализации.
А вот если у нас будут метаклассы, то код с неправильными параметрами просто не откомпилируется.
Все, что от C# требуется, это добавить Method<T>() where T : class. new (int, string)
, но мелкомягкие уже что-то вроде "сложно и не нужно" писали на этот счет.
Ну и стоит заметить что рефлексия это лишь инструмент для обучения, если нужно именно быстро и динамически создавать строго-типизированные классы, можно воспользоваться скомпилированными ExpressionTrees или даже прямой IL генерацией.
new T()
компилируется в Activator.CreateInstance(typeof(T))
, так что особых преимуществ ограничение new
с параметрами не даст, можно вызвать Activator.CreateInstance
руками. Проверку наличия конструктора на этапе компиляции несложно реализовать с помощью Roslyn analyzer.
Я не вижу ни одного плюса такой непонятной реализации.
Ну, собственно, плюс не относится непосредственно к теме метаклассов. Меня очень задалбывает одна штука в C#. Представьте, что у вас есть класс, в котором определены десять конструкторов. Вам надо написать наследник этого класса. В наследнике только перекрывается один виртуальный метод. Новые поля и свойства не добавляются, поэтому код инициализации писать не нужно. Но все десять конструкторов вам придётся вручную перекрыть, и код каждого конструктора будет состоять только из вызова аналогичного унаследованного конструктора. Мне очень не нравится такая рутина, тем более что она резко контрастирует с тем, к чему я привык в Delphi, где производный класс автоматически наследовал все конструкторы предка. Например, чтобы объявить свой класс исключения, в Delphi достаточно написать:
type TMyException = class(TException);
И всё, все конструкторы TException будут доступны TMyException. Сравните это с тем, как аналогичное объявление будет выглядеть в C#. Поэтому хотелось бы, чтобы хотя бы для классов без новых полей и свойств конструкторы наследовались автоматически, чтобы наоборот, надо было явно указывать, если какой-то унаследованный конструктор не нужен.
Все, что от C# требуется, это добавить Method<T>() where T: class. new (int, string)
Согласен с тем, что ограничения дженериков недостаточно гибкие. Я бы ещё добавил, например, ограничения по тому, какие определены операторы для типа, разделил бы классы и интерфейсы. Но предложенный вами метод не решает всех проблем, хотя существенно облегчает жизнь.
Ну и стоит заметить что рефлексия это лишь инструмент для обучения, если нужно именно быстро и динамически создавать строго-типизированные классы, можно воспользоваться скомпилированными ExpressionTrees или даже прямой IL генерацией.
А вот с этим не согласен. Мне приходится писать не только готовые приложения, но и библиотеки, которыми пользуются потом другие люди. И нередко возникает необходимость сделать что-то как в WinForms или WPF, где пользователь может написать свой собственный UserControl, а библиотека будет работать с этим классом так же легко, как со своими внутренними классами. Вот здесь и нужна или рефлексия, или метаклассы, или ещё какой-то способ динамически получать метаинформацию о классе и уметь его создавать.
Обычно делают так. В библиотеке есть абстрактный базовый класс или интерфейс, который должен реализовать клиент. Клиент сам создаёт наследников этого класса и передаёт в функции библиотеке. Почему такое решение вас не устраивает?
Классовые методы – это ещё одна конструкция, которая есть в Delphi, но отсутствует в C#. Эти методы при объявлении помечаются словом class и являются чем-то средним между статическими методами и методами экземпляра.
Я может что-то неправильно понял, но что мешает использовать extension methods?
Ок, разница понятна. Но если честно у меня всё ещё есть сомнения в необходимости такого в С#. Ну с той точки зрения что не могу вспомнить ни один "use case", где такое могло пригодится и который не решался уже доступными в С# фичами.
Зато вижу пару "проблем", которые кассовые методы легко могут создать.
П. С. Хотя могу себе конечно представить что есть какие-то сценарии, где без классовых методов ну совсем не обойтись, но они просто лежат совсем в другой плоскости разработки.
Зато вижу пару «проблем», которые кассовые методы легко могут создать.
Это какие же?
И да, я согласен что если какая-то фича кажется тебе не особенно безопасной, то можно её просто не использовать. И если бы в C# уже были классовые методы и/или были проблемы/ситуации, которые без них ну вообще не решить, то никаких «претензий» к ним у меня бы не было.
Но пока же я вижу просто желание добавить что-то в C# чтобы определённой(и на мой взгляд относительно небольшой) группе людей просто было удобнее/привычнее работать с новым языком.
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());
Полезно для реализации системы плагинов. Часто требуется получить мета-информацию о плагине, не создавая его экземпляр. Сейчас это обычно реализуют атрибутами (что ограничивает мета-информацию примитивными типами), а можно будет получать её непосредственно из статических свойств.
Вместо:
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
, этот вариант позволит вернуть мета-данные любого типа с проверкой на этапе компиляции, но несколько неочевидно в применении.
upd. Прошу прощения, неправильно прочитал пример. Тут действительно наверное придётся через рефлекию в том или ином виде.
можно будет получать её непосредственно из статических свойств.
Ничего хорошего в этом нет. Метаинформация, нужная только для движка плагинов, смешивается с обычными полями и методами.
Ничего хорошего в этом нет. Метаинформация, нужная только для движка плагинов, смешивается с обычными полями и методами.
Но если её не смешивать, тоже ничего хорошего не получается. Каждому плагину нужно написать свою фабрику. Так как перечень возможных плагинов и их фабрик нельзя захардкодить, нужно предусмотреть какой-то реестр и механизм сопоставления (хотя бы через атрибуты). Слишком много действий, не контролируемых компилятором, а это повышает вероятность трудноуловимых ошибок.
Каждому плагину нужно написать свою фабрику. Так как перечень возможных плагинов и их фабрик нельзя захардкодить, нужно предусмотреть какой-то реестр и механизм сопоставления (хотя бы через атрибуты).
Не понял вашу мысль. Все плагины должны реализовывать какой-нибудь контракт IPlugin. А как реализовывать — дело плагина. Вот и всё.
IPlugin
{
void Init();
}
В методе инит вся тяжелая логика, она может быть в подкапотной фабрике, может не быть — детали реализации. Что касается цены, метакласс тоже не бесплатный, и неизвестно, что в итоге будет дешевле.
Вот, например, ситуация, когда фабрики бесполезны. Задача, кстати, вполне реальная, если вы вдруг знаете, как её можно решить изящнее, чем это сделал я, буду весьма признателен.
Есть некоторый универсальный класс
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. Библиотека не сможет использовать полиморфизм при работе с этими типами. Имея описание одного из типов T в виде Type, библиотека не доберётся до нужного статического члена без использования рефлексии.
В общем-то, оба этих недостатка присущи и моему решению с атрибутами, так что выбор между атрибутами и статикой делается из чисто эстетических соображений. К сожалению, ничего лучшего C# не предлагает.
И да рефлексии и статика не то чтобы удачное решение. И не только с точки зрения эстетики. Но опять же возвращаемся к тому что добавление метаклассов само по себе может быть не особо удачным решением в контексте всего языка :)
П.С. И если забыть про метаклассы и просто обсуждать систему плагинов в С#, то не проще будет взять какой-нибудь готовый фреймворк вроде того же MEF? :)
в случае с метаклассами и пунктом «1» можно быть на 100% уверенным что пользователь всё сделает правильно?Да, если в базовом классе объявить статический метод как virtual; abstract; то в наследниках программист обязан будет его определить. Довольно забавно выглядит модификатор virtual у static-метода, но тем не менее, в дельфи это работает.
Статики вообще надо использовать по минимуму.
Гражданин Мартин, который Роберт Мартин, описывая смысл архитектуры, исходит из того, что в хорошей архитектуре основная идея — отложить решения о деталях. Статики — они про раннее связывание в любом случае.
1. Создание экземпляра стоит не очень дорого.
2. У создания нет побочных эффектов.
Если хотя бы одно из этих условий не выполняется, вариант с созданием не проходит. Плюс мы должны гарантировать, что у любого наследника BaseT должен быть конструктор с определённым фиксированным набором параметров, что тоже не всегда удобно.
Есть у вас вариант решения такой задачи?Есть довольно прямолинейный способ: без статиков, без контейнера, с полной проверкой в compile-time
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();
}
}
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();
}
}
Тут лучше всего подойдёт C++ — у него в шаблонах тупо синтаксическая подстановка, и у класса-параметра T можно хоть статический метод вызывать, хоть к переменной обращаться, и всё без оверхеда в runtime
template<class T>
class Container {
public:
std::string getMetaInfo() { return T::metaInfo(); }
};
class SimpleT {
public:
static std::string metaInfo() { return "simple"; }
};
Статический конструктор можно вызвать методом RuntimeHelpers.RunClassConstructor для typeof(T).TypeHandle
Программа работает только с фабрикой, и напрямую класс плагина не знает, и он ей не нужен. Вся метаинформация о классе плагине, которая должна быть доступна до создания экземпляра плагина, должна быть доступна через фабрику.
1. Это дополнительная нагрузка на того, кто будет писать плагины: надо написать не только плагин, но и фабрику к нему.
2. Встаёт вопрос о том, как создавать сами фабрики. Они создаются либо через рефлексию со всеми вытекающими отсюда последствиями, либо ответственность за их создание переносится на разработчиков плагинов, что ещё больше усложняет работу с библиотекой.
3. Экземпляры фабрик — это затраты на их хранение в памяти, перемещение при компрессии и т.п. Если этого можно избежать, почему бы этого не сделать (правда, этот пункт спорный — он связан с тем, что я ещё помню, как программировать на ZX Spectrum с 48 кБ памяти, поэтому стараюсь её если не экономить, то хотя бы не использовать совсем уж бездумно).
4. Ну и, наконец, главное — у нас появляются две сущности — класс и его фабрика, которые тесно связаны между собой. В общем случае фабрика должна уметь сообщить, объекты какого класса она создаёт, а класс — какая фабрика ему нужна. И то, что они о себе сообщают, должно соответствовать тому, как они реализованы. Это заставляет разработчика плагина каждый раз выполнять рутинную работу, в которой легко сделать ошибку, не отлавливаемую компилятором. Я же предпочитаю избегать рутинной работы, пусть такие вещи делает сам компьютер. Или хотя бы пусть он проверяет, не сделал ли я какую-нибудь глупую ошибку. Метаклассы — это шаг как раз в таком направлении.
И вопрос к вам. UserControl в WinForms и WPF — это, по сути, тот же плагин. Но разработчики этих библиотек предпочли обойтись без фабрик. Как вы думаете, почему? И было ли бы лично вам удобнее, если бы это реализовали через фабрики?
- Затраты одинаковы. Писать статические методы в классе плагина, либо методы в классе фабрики.
- Хоть один класс придется искать.
- Фабрика не обязана содержать огромное состояние. Экземпляр будет занимать всего 12 байт (24 на х64)
- В общем случае у фабрики будет всего один обязательный метод: IPlugin Create(); А плагин о фабрике не будет знать ничего, это не его обязанность.
UserControl — просто базовый класс, используемые наследники известны на этапе компиляции.
Если вас интересует дальнейшая дискуссия, то прошу продолжить её в ветке ответа habr.com/ru/post/464141/#comment_20540683 — там я изложил новые аргументы.
И вопрос к вам. UserControl в WinForms и WPF — это, по сути, тот же плагин.Это плагин для IDE, но не плагин для программы, куда включен UserControl.
Но разработчики этих библиотек предпочли обойтись без фабрикИ без метаклассов.
Как вы думаете, почему?Потому что нет требований к производительности. В IDE можно пользоваться рефлексией.
И было ли бы лично вам удобнее, если бы это реализовали через фабрики?Тут вопрос требований. Если нужно создавать миллион объектов в секунду, без фабрики не обойтись. Если плагин загружается однократно при старте, тут открытый вопрос — ускорить загрузку, используя фабрики, или добавить удобства разработчикам плагинов.
Если вас интересует дальнейшая дискуссия, то прошу продолжить её в ветке ответа habr.com/ru/post/464141/#comment_20540683 — там я изложил новые аргументы.
Перегрузка статических методов? Если есть разные реализации какой-либо абстракции, то про статику нужно забыть.
Виртуальный конструктор? Паттерн фабрика.
Кажется, правильно не стали затаскивать.
Может быть, тут дело в том, что в Delphi (в тех версиях, которые делал ещё Хейлсберг) практически полностью отсутствует рефлексия, и альтернативы метаклассам просто нет, чего нельзя сказать о C#.
Скорее всего.
А в целом интересно, спасибо)
В результате получаем существенное усложнение языка, так как кучу понятий приходится дублировать.
Если уж в C# и затаскивать метаклассы, то было бы разумно сделать их так, как в языках, где они таки являются классами. И используются, в частности, при создании, собственно, классов. Это же, собственно, стандарт — в большинстве языков, где есть метаклассы классы — это обычные объекты, а метаклассы, соотвественно, обычные классы. Delphi и Java/.NET — два ущербных варианта (но ущербных по разному).
Из распространённых — можете посмотреть, хотя бы, на Python…
P.S. Вообще метаклассы в Delphi (как и многое другое в современном Delphi) оставляют «неприятный привкус во рту». Зачем вообще понятие «конструктор»? Почему это не просто функция класса (возможно виртуальная)? Да, я знаю ответ (потому что так исторически сложилось)… и тем не менее — это некрасиво и запутанно.
Почему у вас метакласс — не класс?
Как же не класс? Он у меня наследник типа Type, а Type — это класс. Или вы что-то другое имели ввиду?
Ну вот смотрите как это в 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 (и у вас) метакласс — это что-то такое странное, создаваемое компилятором только для того, чтобы сделать наследование конструкторов и виртуальные конструкторы (без явных фабрик), фактически.
А так даже интересно получается: если метаклассы — это частный случай классов, можно ведь создавать метаклассы для метаклассов, правильно? Не знаю, зачем это может понадобиться, но иметь средства, достаточно гибкие для того, чтобы реализовать такое, мне бы понравилось.
А так даже интересно получается: если метаклассы — это частный случай классов, можно ведь создавать метаклассы для метаклассов, правильно?Да, конечно. Цепочка не уходит в бесконечность, впрочем, так как у стандартного класса Metaclass его metaclass — это тоже Metaclass.
Не знаю, зачем это может понадобиться, но иметь средства, достаточно гибкие для того, чтобы реализовать такое, мне бы понравилось.На LISP посмотрите. Собственно там метаклассы и появились много лет назад. И да — это имеет смысл. Например для AST вам потребуются десятки типов — и при этом у них могут быть разные метаклассы. Если из окажется много, то можно создать и метакласс для них… на практике я нечто подобное видел только в FORTH: классов и метаклассов там нет, зато есть «компилирующие слова»… и иногда там бывают трёх-четырёхступенчатые иерархии…
P.S. Но вообще у этих технологий есть проблема: примерно у 5-10% при взгляде на них «загораются глаза», они говорят «ух как клёво» и начинают пользовать. Но 90%-95% ничего не готовы изучать и так далее. А так как решает-таки большинство, то… имеем то, что имеем.
Вообще говоря в язык можно понапихать огромное количество фич, с «Дом Советов». Язык распухнет до невозможности, его будет трудно читать и использовать, так что каждая фича должна быть обоснована и выверена. Кроме метаклассов ведь можно еще огромное количество фич включить в С#, они и включаются потому что они более очевидно полезные чем метаклассы, так что C# уже давно не тот компактный язык.
Непонятно зачем всё это нужно. Тема для чего это нужно не раскрыта совсем.
Я думал, что это более-менее очевидно. Но, похоже, просчитался — не все сталкиваются с такими задачами, где это может быть полезно. Собственно, на этот вопрос уже хорошо ответили в этом комментарии.
Чтобы достичь подобного полиморфизма без метаклассов и виртуальных классовых методов, для класса X и каждого из его наследников пришлось бы писать вспомогательный класс с обычным виртуальным методом
Не могли бы вы поподробней разъяснить, почему необходимо избежать использования виртуальных классовых методов?
Правильно ли я понял, что решаемая проблема крайне похожа на double-dispatching, реализуемый, в частности, паттерном Visitor?
Не могли бы вы поподробней разъяснить, почему необходимо избежать использования виртуальных классовых методов?
Я так понял, что под виртуальными классовыми методами вы имели ввиду виртуальные методы экземпляра. Ответ такой: потому что их использование требует создания экземпляра класса, и если этот класс неизвестен на момент компиляции использующего его кода, получаем все те же проблемы.
Правильно ли я понял, что решаемая проблема крайне похожа на double-dispatching, реализуемый, в частности, паттерном Visitor?
Только частично. Паттерн Visitor, насколько я его знаю, ориентирован на работу, во-первых, с уже созданными объектами, а во-вторых, с объектами из фиксированного перечня классов. Вопросы создания объектов и динамического расширения перечня классов он не покрывает.
Как я уже писал в другом комментарии, все эти штуки предназначены, в первую очередь, для написания библиотек типа WinForms, при использовании которого можно создать свой UserControl, а библиотека будет с ним работать как с родным. В частности, на метаклассах построена библиотека VCL — дельфийский аналог WinForms (точнее, метаклассы придуманы в Delphi для того, чтобы можно было написать VCL).
Фантазии на тему метаклассов в C#