Мой дядя самых честных правил программ исходники за так ...

28 февр. 2009 г.

Пара интересных трюков для C# 3.0

В заметке я хочу обсудить применение двух трюков, ставших возможными после введения новых синтаксических конструкций в C# 3.0, таких как анонимные типы и лямбда-выражения (анонимные функции).

Эти трюки многим, конечно же, уже известны.

Анонимные типы как словарь

Итак, у нас появилась возможность задавать в области действия функций локальные анонимные типы:

var obj = new { Name = "Object", Color = "Black" };

Анонимный тип, по вполне понятным причинам, можно передать другой функции только нетипизировано, в виде объекта типа System.Object. Что делает его довольно бесполезным вне функции.

Идея состоит в том, чтобы преобразовать анонимный объект в словарь типа ключ-значение. И использовать такие объекты как альтернативный вариант для функций принимающих параметры в виде такого словаря.

Реализуем небольшой метод-расширение, который принимает произвольный объект и возвращает словарь со свойствами и их значениями из объекта.

ObjectExtensions.cs

using System.Collections.Generic;

 

namespace Home.Andir.Examples

{

    static class ObjectExtensions

    {

        public static Dictionary<string, object> ToDictionary(this object obj)

        {

            return obj.ToDictionary<object>();

        }

 

        public static Dictionary<string, T> ToDictionary<T>(this object obj)

        {

            var dict = new Dictionary<string, T>();

 

            foreach (var prop in obj.GetType().GetProperties())

            {

                dict.Add(prop.Name, (T)prop.GetValue(obj, null));

            }

 

            return dict;

        }

    }

}

Применение этого “трюка” можно найти в ASP.Net MVC в виде экземпляра объекта HtmlHelper. Напишем примерную реализацию метода HtmlHelper.Link, который сгенерирует содержимое произвольного тега “a”.

Вначале применение:

HtmlHelper.Link("Ссылка",

    new { href = "http://example.org/", title = "Нажмите, чтобы перейти далее." }

    );

Неправда ли, немного элегантнее и чуть меньше символов, чем:

HtmlHelper.Link("Ссылка",

    new Dictionary<string, string>{

        { "href", "http://example.org/" },

        { "title", "Нажмите, чтобы перейти далее." } }

    );

А теперь реализация:

HtmlHelper.cs

using System;

using System.Collections.Generic;

using System.Linq;

 

namespace Home.Andir.Examples

{

    class HtmlHelper

    {

        public static string Tag(string name, string text, object attrs)

        {

            return Tag(name, text, attrs.ToDictionary<string>());

        }

 

        public static string Tag(string name, string text,

            Dictionary<string, string> attrs)

        {

            var attrString = String.Join(" ",

                attrs.Select(x => String.Format("{0}=\"{1}\"", x.Key, x.Value))

                     .ToArray()

                    );

 

            return String.Format(

                "<{0} {1}>{2}</{0}>", name, attrString, text);

        }

 

        public static string Link(string text, object attrs)

        {

            return Tag("a", text, attrs);

        }

 

        public static string Link(string text, Dictionary<string, string> attrs)

        {

            return Tag("a", text, attrs);

        }

    }

}

 

Статически-типизированный Reflection

Основная проблема, которая возникает при использовании механизма отражения (Reflection) – это использование строк в качестве аргументов при извлечении данных. Это сильно мешает использованию инструментов для автоматического рефакторинга.

Рассмотрим объект:

class NamedObject

{

    [DisplayName("Имя")]

    public string Name { get; set; }

}

Пусть, у нас имеется нетипизированный экземпляр этого объекта и требуется узнать значение атрибута “DisplayName”.

ComponentHelper.cs

using System;

using System.ComponentModel;

using System.Reflection;

 

namespace Home.Andir.Examples

{

    public static class ComponentHelper

    {

        public static string GetPropertyDisplayName(Type type, string propName)

        {

            return GetDisplayName(type.GetProperty(propName));

        }

 

        private static string GetDisplayName(PropertyInfo propInfo)

        {

            var displayName = propInfo.Name;

 

            foreach (var attr in propInfo.GetCustomAttributes(true))

                if (attr is DisplayNameAttribute)

                    displayName = (attr as DisplayNameAttribute).DisplayName;

 

            return displayName;

        }

    }

}

Теперь вызываем:

Program.cs

using System;

 

namespace Home.Andir.Examples

{

    class Program

    {

        static void Main(string[] args)

        {

            Console.WriteLine("DisplayName = \"{0}\"",

                ComponentHelper.GetPropertyDisplayName(typeof(NamedObject), "Name")

                );

 

            Console.ReadKey();

        }

    }

}

Вот тут мы и обнаруживаем проблему. Если использовать автоматический рефакторинг и переименовать свойство NamedObject.Name, например, в NamedObject.Name1, придётся ещё вручную переименовывать и имя свойства в вызове метода GetPropertyDisplayName.

Вот тут нам приходят на помощь, как это не странно, лямбда выражения, с их помощью предыдущий вызов можно переписать так:

Program.cs

using System;

 

namespace Home.Andir.Examples

{

    class Program

    {

        static void Main(string[] args)

        {

            Console.WriteLine("DisplayName = \"{0}\"",

                ComponentHelper.GetPropertyDisplayName<NamedObject>(x => x.Name)

                );

 

            Console.ReadKey();

        }

    }

}

Как видно, теперь у нас нет использования строки с именем свойства, и автоматический рефакторинг будет работать безопасно.

Реализация такого варианта следующая:

ComponentHelper.cs

using System;

using System.ComponentModel;

using System.Reflection;

using System.Linq.Expressions;

 

namespace Home.Andir.Examples

{

    public static class ComponentHelper

    {

        public static string GetPropertyDisplayName<T>(

            Expression<Func<T, object>> propertyExpression

            )

        {

            return GetDisplayName(

                StaticReflection.GetPropertyInfo<T>(propertyExpression));

        }

 

        public static string GetPropertyDisplayName(Type type, string propName)

        {

            return GetDisplayName(type.GetProperty(propName));

        }

 

        private static string GetDisplayName(PropertyInfo propInfo)

        {

            var displayName = propInfo.Name;

 

            foreach (var attr in propInfo.GetCustomAttributes(true))

                if (attr is DisplayNameAttribute)

                    displayName = (attr as DisplayNameAttribute).DisplayName;

 

            return displayName;

        }

    }

}

StaticReflection.cs

using System;

using System.Linq.Expressions;

using System.Reflection;

 

namespace Home.Andir.Examples

{

    public static class StaticReflection

    {

        public static PropertyInfo GetPropertyInfo<T>(

            Expression<Func<T, object>> propertyExpression)

        {

            var memberExpression = propertyExpression.Body as MemberExpression;

            if (memberExpression == null)

                throw new InvalidOperationException(

                    "Only member access allowed in lambda expression.");

 

            var propInfo = memberExpression.Member as PropertyInfo;

            if (propInfo == null)

                throw new InvalidOperationException(

                    "Only property access allowed in lambda expression.");

 

            return propInfo;

        }

    }

}

Эпилог

Применение этих трюков можно найти в ASP.Net MVC, Rhino Mocks, Autofac, AutoMapper и многих других фреймворках. Уверен, что можно найти применение им и в своём коде ;-).

2 комментария:

  1. Я очень давно уже искал второй трюк, ни на секунду не сомневаясь, что это возможно и внутри .net разработчикам без этого не обойтись. Спасибо !

    Интересно, как на Яве повторить подобное, там то язык более консервативный... А порой очень надо такое...

    ОтветитьУдалить
  2. I think these tricks are really important and definitely help me while Custom .NET Development.

    ОтветитьУдалить

Публикуются только комментарии, которые показались автору блога заслуживающими внимания.