Страницы

Поиск по вопросам

понедельник, 2 марта 2020 г.

Дизайн API: Синтаксис для инициализации прикреплённых свойств

#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(); // дальше как было

Комментариев нет:

Отправить комментарий