Как присвоить автосвойству C# значение по умолчанию с помощью пользовательского атрибута?

Как присвоить автосвойству C# значение по умолчанию, используя настраиваемый атрибут?

Это код, который я хочу видеть:

class Person 
{
    [MyDefault("William")]
    public string Name { get; set; }
}

Я знаю, что нет встроенного метода для инициализации значения по умолчанию с использованием атрибута. Могу ли я написать свой собственный класс, который использует мои пользовательские атрибуты для инициализации значения по умолчанию?


person Contango    schedule 20.06.2011    source источник
comment
Почему бы вам просто не использовать конструктор?   -  person svick    schedule 20.06.2011
comment
Это не так интересно, наверное :)   -  person Ivan Danilov    schedule 20.06.2011


Ответы (4)


Вы можете использовать такой вспомогательный класс:

public class DefaultValueHelper
{
    public static void InitializeDefaultValues<T>(T obj)
    {
        var properties =
            (from prop in obj.GetType().GetProperties()
             let attr = GetDefaultValueAttribute(prop)
             where attr != null
             select new
             {
                Property = prop,
                DefaultValue = attr.Value
             }).ToArray();
        foreach (var p in properties)
        {
            p.Property.SetValue(obj, p.DefaultValue, null);
        }

    }

    private static DefaultValueAttribute GetDefaultValueAttribute(PropertyInfo prop)
    {
        return prop.GetCustomAttributes(typeof(DefaultValueAttribute), true)
                   .Cast<DefaultValueAttribute>()
                   .FirstOrDefault();
    }
}

И вызовите InitializeDefaultValues в конструкторе вашего класса.

class Foo
{
    public Foo()
    {
        DefaultValueHelper.InitializeDefaultValues(this);
    }

    [DefaultValue("(no name)")]
    public string Name { get; set; }
}

РЕДАКТИРОВАТЬ: обновленная версия, которая генерирует и кэширует делегата для инициализации. Это делается для того, чтобы избежать использования отражения каждый раз, когда метод вызывается для данного типа.

public static class DefaultValueHelper
{
    private static readonly Dictionary<Type, Action<object>> _initializerCache;

    static DefaultValueHelper()
    {
        _initializerCache = new Dictionary<Type, Action<object>>();
    }

    public static void InitializeDefaultValues(object obj)
    {
        if (obj == null)
            return;

        var type = obj.GetType();
        Action<object> initializer;
        if (!_initializerCache.TryGetValue(type, out initializer))
        {
            initializer = MakeInitializer(type);
            _initializerCache[type] = initializer;
        }
        initializer(obj);
    }

    private static Action<object> MakeInitializer(Type type)
    {
        var arg = Expression.Parameter(typeof(object), "arg");
        var variable = Expression.Variable(type, "x");
        var cast = Expression.Assign(variable, Expression.Convert(arg, type));
        var assignments =
            from prop in type.GetProperties()
            let attr = GetDefaultValueAttribute(prop)
            where attr != null
            select Expression.Assign(Expression.Property(variable, prop), Expression.Constant(attr.Value));

        var body = Expression.Block(
            new ParameterExpression[] { variable },
            new Expression[] { cast }.Concat(assignments));
        var expr = Expression.Lambda<Action<object>>(body, arg);
        return expr.Compile();
    }

    private static DefaultValueAttribute GetDefaultValueAttribute(PropertyInfo prop)
    {
        return prop.GetCustomAttributes(typeof(DefaultValueAttribute), true)
                   .Cast<DefaultValueAttribute>()
                   .FirstOrDefault();
    }
}
person Thomas Levesque    schedule 20.06.2011
comment
Использование отражения дорого - person Dustin Davis; 20.06.2011
comment
@DustinDavis, да, но это не значит, что вы никогда не должны его использовать ... Конечно, это не очень хорошее решение, если вам нужно создать много экземпляров класса. - person Thomas Levesque; 20.06.2011
comment
Конечно, можно было бы сгенерировать и кэшировать делегат, который инициализирует свойства. Таким образом, отражение будет сделано только один раз. - person Thomas Levesque; 20.06.2011
comment
Конечно. Я использую его часто, но я предполагаю, что ему понадобится эта функциональность в больших масштабах. Было бы неплохо, если бы существовала какая-то структура атрибутов, с которой мы могли бы работать. - person Dustin Davis; 20.06.2011
comment
Выложи пример кеша делегатов и я дам тебе +1 - person Dustin Davis; 20.06.2011
comment
Я уже публиковал такой пример еще до того, как вы начали разговор :) - person Ivan Danilov; 20.06.2011
comment
@Thomas Только для получения списка свойств / атрибутов. CLR кэширует его сама, поэтому накладных расходов не так много. Самая медлительность связана с самим вызовом сеттера: CLR должна проверять безопасность типов, аргументы, права доступа к коду и т. д. Все это я выполняю только один раз. Но если это действительно критическая часть - вы, конечно, можете кэшировать список свойств и значений для каждого типа. Просто, но достаточно долго... Если кому-то это действительно нужно - покажу и это. В противном случае... я несколько ленив :) - person Ivan Danilov; 20.06.2011
comment
@DustinDavis, я только что опубликовал новую версию. Пришлось отказаться от общей подписи... - person Thomas Levesque; 20.06.2011
comment
@ Томас Я тоже это сделал. Кстати моя версия будет работать и на .NET 3.5)))) Но +1 ваш) - person Ivan Danilov; 20.06.2011

Если вы хотите сделать это с помощью PostSharp (как предлагают ваши теги), используйте аспект отложенной загрузки. Вы можете увидеть тот, который я создал здесь http://programmersunlimited.wordpress.com/2011/03/23/postsharp-weaving-community-vs-professional-reasons-to-get-a-professional-license/

С аспектом вы можете применить значение по умолчанию к одному свойству или применить его к нескольким свойствам с помощью одного объявления на уровне класса.

Аспект ленивой загрузки будет использовать базовый класс LocationInterceptionAspect.

[Serializable]
    [LazyLoadingAspect(AttributeExclude=true)]
    [MulticastAttributeUsage(MulticastTargets.Property)]
    public class LazyLoadingAspectAttribute : LocationInterceptionAspect
    {
        public object DefaultValue {get; set;}

        public override void OnGetValue(LocationInterceptionArgs args)
        {
           args.ProceedGetValue();
            if (args.Value != null)
            {
              return;
            }

            args.Value = DefaultValue;
            args.ProceedSetValue();
        }

    }

затем примените аспект так

[LazyLoadingAspect(DefaultValue="SomeValue")]
public string MyProp { get; set; }
person Dustin Davis    schedule 20.06.2011

Если спекулировать с выражениями, вы можете сделать инициализирующие делегаты и кэшировать их. Это сделает код намного быстрее по сравнению с простым отражением.

internal static class Initializer
{
    private class InitCacheEntry
    {
        private Action<object, object>[] _setters;
        private object[] _values;

        public InitCacheEntry(IEnumerable<Action<object, object>> setters, IEnumerable<object> values)
        {
            _setters = setters.ToArray();
            _values = values.ToArray();

            if (_setters.Length != _values.Length)
                throw new ArgumentException();
        }

        public void Init(object obj)
        {
            for (int i = 0; i < _setters.Length; i++)
            {
                _setters[i](obj, _values[i]);
            }
        }
    }

    private static Dictionary<Type, InitCacheEntry> _cache = new Dictionary<Type, InitCacheEntry>();

    private static InitCacheEntry MakeCacheEntry(Type targetType)
    {
        var setters = new List<Action<object, object>>();
        var values = new List<object>();
        foreach (var propertyInfo in targetType.GetProperties())
        {
            var attr = (DefaultAttribute) propertyInfo.GetCustomAttributes(typeof (DefaultAttribute), true).FirstOrDefault();
            if (attr == null) continue;
            var setter = propertyInfo.GetSetMethod();
            if (setter == null) continue;

            // we have to create expression like (target, value) => ((TObj)target).setter((T)value)
            // where T is the type of property and obj is instance being initialized
            var targetParam = Expression.Parameter(typeof (object), "target");
            var valueParam = Expression.Parameter(typeof (object), "value");
            var expr = Expression.Lambda<Action<object, object>>(
                Expression.Call(Expression.Convert(targetParam, targetType),
                                setter,
                                Expression.Convert(valueParam, propertyInfo.PropertyType)),
                targetParam, valueParam);
            var set = expr.Compile();

            setters.Add(set);
            values.Add(attr.DefaultValue);
        }
        return new InitCacheEntry(setters, values);
    }

    public static void Init(object obj)
    {
        Type targetType = obj.GetType();
        InitCacheEntry init;
        if (!_cache.TryGetValue(targetType, out init))
        {
            init = MakeCacheEntry(targetType);
            _cache[targetType] = init;
        }
        init.Init(obj);
    }
}
person Ivan Danilov    schedule 20.06.2011

Вы можете создать такой метод:

public static void FillProperties<T>(T obj)
{
    foreach (var property in typeof(T).GetProperties())
    {
        var attribute = property
            .GetCustomAttributes(typeof(DefaultValueAttribute), true)
            .Cast<DefaultValueAttribute>()
            .SingleOrDefault();
        if (attribute != null)
            property.SetValue(obj, attribute.Value, null);
    }
}

Затем вы можете либо использовать фабричный метод, который вызывает этот метод, либо вызывать его непосредственно из конструктора. Обратите внимание, что такое использование отражения, вероятно, не является хорошей идеей, если вы создаете много объектов таким образом, а производительность важна.

person svick    schedule 20.06.2011
comment
Спасибо за ответ. Чтобы вызвать это, вызовите FillProperties(this) из конструктора вашего класса (при условии, что класс не является статическим). - person Contango; 20.06.2011
comment
Это работает на 100%. Я также установил новый атрибут MyDefault и использовал его для установки значения по умолчанию для каждого свойства. Таким образом, если код переносится в другой проект, он будет ломаться с шумом, а не молча. - person Contango; 20.06.2011