Привязываем к объекту любые данные с помощью словаря свойств-расширений

9 ноября 2009 г.

Наверняка, многие из вас любят писать разные методы-расширения. Хочу с вами поделиться одним из таких методов, возможно кому-то он покажется удобным.

Проблема

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

Ниже я приведу свое решение данной проблемы.
Вкратце, его суть в том, что к объекту прикрепляется словарь свойств расширений.
Для начала посмотрите, как решение можно использовать.

Решение

//создаем самый простой объект
var obj = new object();

//расширяем объект свойством haha типа String
obj.Extension("haha", "хехе");

//расширяем объект свойством Haha типа DateTime (имена регистрозависимы)
obj.Extension("Haha", DateTime.Now);

//теперь получаем наши значения из свойств расширений
Console.WriteLine(obj.Extension("haha"));
Console.WriteLine(obj.Extension<DateTime>("Haha").Year);

//результат:
//хехе
//2010

По-моему, довольно удобно. Описание работы и исходники ниже.

Как оно работает

Свойства-расширения хранятся во вспомогательном словаре, ключом в котором является слабая ссылка на объект, а значением является как раз словарь свойств-расширений этого объекта (имя свойства-значение). Благодаря слабым ссылкам, мы уверены в том, что наш объект не зависнет в памяти и будет нормально уничтожен сборщиком мусора. Однако, после уничтожения самого объекта, нам нужно удалить его свойства-расширения из вспомогательного словаря. За это будет отвечать специальный таймер.

//вспомогательный словарь свойств-расширений
private static readonly Dictionary<WeakReference, Dictionary<string, object>> _extensionMembers;
//таймер, убивающий словари-расширения мертвых объектов
private static readonly Timer _cleaner;

static ObjectExtensions()
{
  _extensionMembers = new Dictionary<WeakReference, Dictionary<string, object>>();

  _cleaner = new Timer((o) =>
  {
    //будем убирать мусор каждые 10 секунд
    foreach (var key in _extensionMembers.Keys.Where(k => !k.IsAlive).ToArray())
    {
      _extensionMembers.Remove(key);
    }
  }, null, 0, 10000);
}

Далее привожу методы создания и удаления словаря свойств-расширений для объекта:

/// <summary>
/// метод создания/получения существующего словаря свойств-расширений объекта
/// </summary>
private static Dictionary<string, object> Extensions(this object targetObject, bool createIfNotExist)
{
  //нет объекта - расширять нечего
  if (targetObject == null)
    return null;

  lock (targetObject)
  {
    //получаем ключ из вспомогательного словаря
    var weakKey = _extensionMembers.Keys.FirstOrDefault(w => object.ReferenceEquals(w.Target, targetObject));

    if (weakKey != null)
    {
      //ключ найден, возвращаем словарь
      return _extensionMembers[weakKey];
    }
    else if (createIfNotExist)
    {
      //создаем новый ключ и словарь для объекта
      weakKey = new WeakReference(targetObject, false);
      var members = new Dictionary<string, object>(StringComparer.Ordinal);
      _extensionMembers[weakKey] = members;
      return members;
    }
  }
  return null;
}

/// <summary>
/// метод удаления словаря свойств-расширений объекта
/// </summary>
public static void ClearExtensions(this object targetObject)
{
  //нет объекта - удалять нечего
  if (targetObject == null)
    return;

  lock (targetObject)
  {
    //получаем ключ из вспомогательного словаря
    var weakKey = _extensionMembers.Keys.FirstOrDefault(w => object.ReferenceEquals(w.Target, targetObject));

    if (weakKey != null)
    {
      //ключ найден, удаляем словарь
      _extensionMembers.Remove(weakKey);
    }
  }
}

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

/// <summary>
/// Установить значение свойства-расширения
/// </summary>
public static void Extension(this object targetObject, string key, object value)
{
  if (targetObject == null || String.IsNullOrEmpty(key))
    return;

  //получаем словарь свойств-расширений, либо создаем новый
  var extensions = targetObject.Extensions(true);

  //устанавливаем значение, либо удаляем его, если value == null
  if (value != null)
  {
    extensions[key] = value;
  }
  else
  {
    lock(targetObject)
    {
      extensions.Remove(key);
    }
  }
}

/// <summary>
/// Получить значение свойства-расширения
/// </summary>
public static T Extension<T>(this object targetObject, string key)
{
  if (targetObject != null && !String.IsNullOrEmpty(key))
  {
    //получаем словарь свойств-расширений, новый не создаем
    var extensions = targetObject.Extensions(false);

    //если есть словарь
    if (extensions != null)
    {
      //если есть свойство нужное в словаре
      if (extensions.ContainsKey(key))
      {
        lock(targetObject)
        {
          if (extensions.ContainsKey(key))
          {
            //и это свойство того типа, который мы ожидаем
            var value = extensions[key];
            if (value is T)
              //возвращаем значение
              return (T)extensions[key];
          }
        }
      }
    }
  }

  return default(T);
}

Применение

Надеюсь, что данный пример борьбы со строгой типизацией будет понят правильно и не использован во зло. Стоит учитывать, что производительность решения целиком зависит от класса Dictionary и количества элементов в нем.
Я применяю его, например, для написания других методов-расширений, когда необходимо закешировать результат их выполнения. Возможно, вы найдете другие разумные сценарии применения данного подхода.

Исходный код можно скачать здесь

Теги:
рубрика C#