В заметке я хочу обсудить применение двух трюков, ставших возможными после введения новых синтаксических конструкций в 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 и многих других фреймворках. Уверен, что можно найти применение им и в своём коде ;-).
Я очень давно уже искал второй трюк, ни на секунду не сомневаясь, что это возможно и внутри .net разработчикам без этого не обойтись. Спасибо !
ОтветитьУдалитьИнтересно, как на Яве повторить подобное, там то язык более консервативный... А порой очень надо такое...
I think these tricks are really important and definitely help me while Custom .NET Development.
ОтветитьУдалить