Пытаюсь выбрать синтаксис для инициализации прикреплённых свойств в своей библиотеке
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();
// дальше как было