#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 Property FooProperty = 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 (); // дальше как было
Комментариев нет:
Отправить комментарий