#c_sharp #net #дизайн_api
Пытаюсь выбрать синтаксис для инициализации прикреплённых свойств в своей библиотеке CsConsoleFormat. Прикреплённые свойства выполняют ту же роль, что и в WPF. Вот, какие варианты нарисовались. В силу разных причин ни один не нравится. Индексатор (а.к.а. инициализатор словаря): var a = new Element { Oops = 1, I = 2, Did = 3, It = 4, Again = 5, [Element.FooProperty] = 2.1, [Element.BazProperty] = Guid.NewGuid() }; a.WritePropertyValues(); Плюсы: Выглядит симпатично, почти как инициализация обычных свойств. Сочетается с инициализатором объекта на одном уровне. Работает не только инициализация, но и чтение, и запись. В Avalonia UI используется похожий синтаксис для биндингов. Минусы: Один, но жирный: индексаторы не могут быть обобщёнными, поэтому никакой проверки типов, везде object. Причём теряется не только проверка, но и конвертация. Инициализатор коллекции с двумя аргументами: var b = new Element { Oops = 1, I = 2, Did = 3, It = 4, Again = 5, Values = { { Element.FooProperty, 2 }, { Element.BarProperty, "Hello!" } } }; b.WritePropertyValues(); Плюсы: Строго типизировано. Можно совместить с индексатором для чтения и записи. Минусы: Инициализаторы коллекций и объектов не совмещаются, приходится выделять на отдельный уровень. Выглядит сомнительно из-за леса из фигурных скобочек: в конце выражения их аж три штуки. Если добавлять индексатор для чтения и записи, то получается мешанина: в одном месте типизировано, в другом нет. Инициализатор коллекции с одним аргументом в сочетании с оператором: var c = new Element { Oops = 1, I = 2, Did = 3, It = 4, Again = 5, Values = { Element.FooProperty == 10, Element.BazProperty == Guid.NewGuid() } }; c.WritePropertyValues(); Плюсы: Строго типизировано. Синтаксис умеренно краткий и умеренно приятный. Можно совместить с индексатором для чтения и записи. Минусы: Оператор присваивания не перегружается, приходится перегружать самый низкоприоритетный бинарный оператор, а он уже достаточно высоко, чтобы портить некоторые выражения (Element.BoolProperty == a == b). Равенство для имитации присваивания — не самый логичный ход. Не хочется оказаться в роли "изобретателя" оператора >> в C++. Впрочем, в Avalonia UI позволяют играться с операторами, почему бы и мне не. Если добавлять индексатор для чтения и записи, то получается мешанина: в одном месте типизировано, в другом нет. Старый-добрый fluent: var d = new Element { Oops = 1, I = 2, Did = 3, It = 4, Again = 5 } .Set(Element.FooProperty, 1337).Set(Element.BarProperty, "World!"); d.WritePropertyValues(); Плюсы: Строго типизировано. Можно совместить с симметричным методом Get. Если совмещать с Get, то операции симметричные и однообразные. Минусы: Синтаксис кошмарный и ужасный, если нужно использовать и обычные, и прикреплённые свойства (перенести все свойства в fluent — не вариант). Код, реализующий все синтаксисы, описанные выше: internal class Program { private static void Main() { var a = new Element { Oops = 1, I = 2, Did = 3, It = 4, Again = 5, [Element.FooProperty] = 2.1, [Element.BazProperty] = Guid.NewGuid() }; a.WritePropertyValues(); var b = new Element { Oops = 1, I = 2, Did = 3, It = 4, Again = 5, Values = { { Element.FooProperty, 2 }, { Element.BarProperty, "Hello!" } } }; b.WritePropertyValues(); var c = new Element { Oops = 1, I = 2, Did = 3, It = 4, Again = 5, Values = { Element.FooProperty == 10, Element.BazProperty == Guid.NewGuid() } }; c.WritePropertyValues(); var d = new Element { Oops = 1, I = 2, Did = 3, It = 4, Again = 5 } .Set(Element.FooProperty, 1337).Set(Element.BarProperty, "World!"); d.WritePropertyValues(); Console.ReadKey(); } } internal class Element { public static readonly PropertyFooProperty = Property.Register("Foo", 1); public static readonly Property BarProperty = Property.Register("Bar", "a"); public static readonly Property BazProperty = Property.Register("Baz", Guid.Empty); private readonly Dictionary _properties = new Dictionary (); public int Oops { get; set; } public int I { get; set; } public int Did { get; set; } public int It { get; set; } public int Again { get; set; } public Values Values { get; } public Element() { Values = new Values(this); } public object this[Property property] { get => _properties.TryGetValue(property, out object value) ? value : property.DefaultValueUntyped; set => _properties[property] = value; } public Element Set (Property prop, T v) { _properties[prop] = v; return this; } public void WritePropertyValues() { foreach (KeyValuePair property in _properties) Console.WriteLine($"{property.Key.Type.Name} {property.Key.Name} = {property.Value} (default: {property.Key.DefaultValueUntyped})"); Console.WriteLine(); } } internal class Values : IEnumerable { private readonly Element _element; public Values(Element element) => _element = element; public void Add (Property prop, T v) => _element[prop] = v; public void Add (PropertyValue pv) => _element[pv.Property] = pv.Value; IEnumerator IEnumerable.GetEnumerator() => null; } internal abstract class Property { public string Name { get; } public object DefaultValueUntyped { get; } public abstract Type Type { get; } protected Property(string name, object defaultValueUntyped) { Name = name; DefaultValueUntyped = defaultValueUntyped; } public static Property Register (string name, T defaultValue) => new Property (name, defaultValue); } internal class Property : Property { public T DefaultValue => (T)DefaultValueUntyped; public override Type Type => typeof(T); internal Property(string name, T defaultValue) : base(name, defaultValue) { } public static PropertyValue operator ==(Property property, T value) => new PropertyValue (property, value); public static PropertyValue operator !=(Property property, T value) => default; } internal struct PropertyValue { public Property Property { get; set; } public T Value { get; set; } public PropertyValue(Property property, T value) { Property = property; Value = value; } } Возможно, я упускаю как-то более удобный вариант из виду? Есть ещё какие-нибудь альтернативы? Желательно строго типизированные и с кратким синтаксисом. Мнения по поводу описанных выше вариантов тоже приветствуются.
Ответы
Ответ 1
Лично мне нравится fluent вариант - то, что оно отделено от обычных свойств даже плюс (мухи отдельно, котлеты отдельно). Но если вы хотите получить код короче, то вам придется изобретать конвенкции. Я подумал о анонимных классах, как их используют в asp.net mvc например. var d = new Element { Oops = 1, I = 2, Did = 3, It = 4, Again = 5 } .AddProperties(new { FooProperty = 10, BarProperty = "alkdjalkds", BazProperty = Guid.NewGuid() }); Если уж сильно надо строгую типизацию, то можно как то так сделать public static class Ext { internal static T Value (this Property prop, T value) { return value; // тут можно придумать что то типа AttchedPropertyValue , который можно создать только отсюда } } Тогда .AddProperties (new { FooProperty = Element.FooProperty.Value(10), BarProperty = Element.BarProperty.Value("alkdjalkds"), BazProperty = Element.BazProperty.Value(Guid.NewGuid()) }); Ну, и считать это все как то так public Element AddProperties (object b) { var sourceType = typeof(T); foreach (var p in b.GetType().GetProperties()) { var fieldInfo = sourceType.GetField(p.Name, BindingFlags.Public | BindingFlags.Static); if (fieldInfo == null) throw new ArgumentException("bla bla"); var fieldValue = fieldInfo.GetValue(null) as Property; var value = p.GetValue(b); if (fieldInfo.FieldType.IsGenericType) if (value.GetType()!=fieldInfo.FieldType.GetGenericArguments()[0]) throw new ArgumentException("bla bla bla"); _properties[fieldValue] = value; } return this; } Ответ 2
У меня тоже вариант с Value: internal class Property: Property { // ... public PropertyValue Value(T t) => new PropertyValue (this, t); } и синтаксисом var e = new Element { Oops = 1, I = 2, Did = 3, It = 4, Again = 5, Values = { Element.FooProperty.Value(10), Element.BazProperty.Value(Guid.NewGuid()) } }; Сильная типизация в наличии. Синтаксис не очень. Ещё один вариант с сильной типизацией, за счёт более сложного определения attached property (которое, впрочем, можно вынести в snippet): var f = new Element { Oops = 1, I = 2, Did = 3, It = 4, Again = 5, Values = { (Element.Foo)10, (Element.Baz)Guid.NewGuid() } }; Достигается следующим изменением кода: interface IPropertyValue { Property Property { get; } T Value { get; } } internal struct PropertyValue : IPropertyValue { // остальное как было } internal class Values : IEnumerable { private readonly Element _element; public Values(Element element) => _element = element; public void Add (Property prop, T v) => _element[prop] = v; // заменили на интерфейс public void Add (IPropertyValue pv) => _element[pv.Property] = pv.Value; IEnumerator IEnumerable.GetEnumerator() => null; } И определение: internal class Element { public static readonly Property FooProperty = Property.Register("Foo", 1); public struct Foo : IPropertyValue { public Property Property => Element.FooProperty; public int Value { get; set; } public static explicit operator Foo(int value) => new Foo() { Value = value }; } public static readonly Property BarProperty = Property.Register("Bar", "a"); public struct Bar : IPropertyValue { public Property Property => Element.BarProperty; public string Value { get; set; } public static explicit operator Bar(string value) => new Bar() { Value = value }; } public static readonly Property BazProperty = Property.Register("Baz", Guid.Empty); public struct Baz : IPropertyValue { public Property Property => Element.BazProperty; public Guid Value { get; set; } public static explicit operator Baz(Guid value) => new Baz() { Value = value }; } private readonly Dictionary _properties = new Dictionary (); // дальше как было
Комментариев нет:
Отправить комментарий