Пример использования Обобщений (Generics) в C#

// Archived from my old blog bibobo.ru and kept as it was, in Russian. Some of the APIs here are outdated.

Давеча потребовалось реализовать инфраструктуру объектов-оберток для редактора роллет.

Исходная задача была следующей:

  • Есть построитель роллет, который имеет ряд коллекций с объектами разных типов, описывающими один из элементов роллеты (привод, направляющая, ламель и т. п.)
  • Необходимо реализовать логику расчета роллеты по уже заполненному списку параметров
  • Внутренние типы построителя наследуются от базового класса RollCalc.Model.CommonProperties

Для упрощения реализации было решено создать свой набор объектов, описывающих каждый из элементов роллеты, чтобы не иметь жесткую связь с внутренними объектами редактора (ибо работать с ними было весьма не удобно). В своих объектах можно реализовать любой набор методов и свойств, которые существенно облегчат расчет и избавят нас от хардкода.
Естественно создавать свои объекты и заполнять их базовые свойства нужно по внутренним коллекциям редактора и желательно парой строчек кода. Пожалуй в данной ситуации наилучший выход — использовать обобщения языка C#. Они позволят нам написать бизнес логику один раз и использовать ее для разных классов (логика-то сходная).

Построим инфраструктуру следующим образом:

  • Реализуем абстрактный базовый класс RollCalcElementBase<T>, который будет содержать всю логику по заполнению общих полей для всех типов-наследников. Почему именно базовый класс, а не интерфейс? Чтобы не париться с реализацией базовой логики в каждом классе-наследнике.
  • В базовом классе обязательно реализуем метод public virtual void InitFromPresentationObject(T presentationObject), который будет инициализировать экземпляр базового класса по его представлению из объекта редактора роллет
  • Реализуем произвольное количество классов-наследников от RollCalcElementBase<T>, в которых можно будет расширить функционал базового класса или переопределить его
  • В основном классе программы создадим коллекции наших объектов-наследников
  • Там же, в основном классе добавим метод, создающий наши объекты по коллекциям объектов редактора
  • Плюс нужен будет метод, получающий экземпляр одного из наших объектов по его типу и наименованию (так уж сложилось, что расчет спецификации роллеты сильно зависит от производителя, а значит и от артикула того или иного элемента)

Весь код был упрощен, что бы не раздувать статью.

Обязательно перед чтением кода необходимо понять как можно накладывать ограничения на использование типов в обобщенном классе или методе. Очень хорошо про это описано на MSDN в статье Ограничения параметров типа

Код базового класса и классов-наследников:

/// <summary>
    /// Базовый класс элемент роллет
    /// Обратите внимание на предложение where
    /// where T : RollCalc.Model.CommonProperties - накладываем ограничение на использование типа в обобщенном классе, только тип элемента построителя роллет
    /// </summary>
    /// <typeparam name="T">Элемент построителя роллет</typeparam>
    public abstract class RollCalcElementBase<T> where T : RollCalc.Model.CommonProperties
    {
        public string Name { get; set; }

        private bool _enabled;

        public bool Enabled
        {
            get { return _enabled; }
            set
            {
                _enabled = value;
                if (PresentationObject != null) PresentationObject.Visible = value;
            }
        }
        public T PresentationObject { get; set; }

        /// <summary>
        /// Инициализировать из объекта роллеты
        /// </summary>
        /// <param name="presentationObject">объект роллеты</param>
        public virtual void InitFromPresentationObject(T presentationObject)
        {
            Name = presentationObject.Description;
            Enabled = presentationObject.Visible;
            PresentationObject = presentationObject;

            presentationObject.Tag = this;
        }
    }

    /// <summary>
    /// Профиль ламели
    /// </summary>
    public class Profile : RollCalcElementBase<LamellaProfile>
    {
    }

    /// <summary>
    /// Тип короба
    /// </summary>
    public class Collar : RollCalcElementBase<ProtectiveCollarType>
    {
    }

    /// <summary>
    /// Привод роллеты
    /// </summary>
    public class Drive : RollCalcElementBase<DriveSystem>
    {
        public int MaxWeight { get; set; }

        public bool CalcEnabled(decimal rollWidth)
        {
            Enabled = rollWidth <= MaxWeight;
            return Enabled;
        }

        public override void InitFromPresentationObject(DriveSystem presentationObject)
        {
            object tag = presentationObject.Tag;

            base.InitFromPresentationObject(presentationObject);

            if (tag is int)
            {
                MaxWeight = Convert.ToInt32(tag);
            }
        }
    }

Код из основного класса (коллекции наших объектов, а так же методы загрузки и получения экземпляра нашего объекта:

private List<Drive> _drives = new List<Drive>();
        public List<Drive> Drives {get { return _drives; }}

        private List<Profile> _profiles = new List<Profile>();
        public List<Profile> Profiles{get { return _profiles; }}

        private List<Collar> _collars = new List<Collar>();
        public List<Collar> Collars{get { return _collars; }}

        /// <summary>
        /// Загрузка и связывание элемента из списка элементов построителя роллет
        /// Обратите внимание на предложения where в заголовке метода
        /// where T : RollCalc.Model.CommonProperties - ограничивает нас по исходному типу объекта роллеты
        /// where T2 : RollCalcElementBase<T>, new() - вводит следующее ограничение: наследник базового класса RollCalcElementBase<T>, имеющий конструктор без параметров (т.е. именно НАСЛЕДНИК, подставить сюда базовый класс уже не получится, поскольку он абстрактный и сконструировать его мы не сможем
        /// </summary>
        /// <typeparam name="T">элемент построителя</typeparam>
        /// <typeparam name="T2">наш связанный элемент</typeparam>
        /// <param name="list"></param>        
        public void LoadFromElementList<T, T2>(IList<T> list)
            where T : RollCalc.Model.CommonProperties
            where T2 : RollCalcElementBase<T>, new()
        {
            List<T2> elements = new List<T2>();

            //создаем наши объекты по объектам построителя, складываем их в список
            foreach (T sourceElement in list)
            {
                T2 element = new T2();
                element.InitFromPresentationObject(sourceElement);
                elements.Add(element);
            }

            //та самая магия, выбираем по типу нашего объекта соответствующую переменную коллекции и присваиваем нашу коллекцию ей
            if (typeof (T2) == typeof (Drive)) _drives = elements.Cast<Drive>().ToList();
            if (typeof(T2) == typeof(Profile)) _profiles = elements.Cast<Profile>().ToList();
            if (typeof (T2) == typeof (Collar)) _collars = elements.Cast<Collar>().ToList();
        }

        /// <summary>
        /// Возвращает элемент по имени
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="name"></param>
        /// <returns></returns>
        public T GetElementByName<T>(string name)
            where T : class
        {
            //объявляем переменную коллекции нашего базового типа
            List<RollCalcElementBase<RollCalc.Model.CommonProperties>> list = null;

            //делаем обратное действие - по переданному типу Т ищем правильную коллекцию наших объектов-наследников и кастуем их к базовому типу
            if (typeof (T) == typeof (Drive))
            {
                list = _drives.Cast<RollCalcElementBase<RollCalc.Model.CommonProperties>>().ToList();
            }
            if (typeof (T) == typeof (Profile))
            {
                list = _profiles.Cast<RollCalcElementBase<RollCalc.Model.CommonProperties>>().ToList();
            }
            if (typeof (T) == typeof (Collar))
            {
                list = _collars.Cast<RollCalcElementBase<RollCalc.Model.CommonProperties>>().ToList();
            }

            //ищем экземпляр в коллекции по наименованию и возвращаем его (при этом кастуем его к типу-наследнику)
            if (list != null)
            {
                foreach (RollCalcElementBase<CommonProperties> elementBase in list)
                {
                    if (elementBase.Name.ToLower() == name.ToLower())
                    {
                        return elementBase as T;
                    }
                }
            }

            return null;
        }

Вот пожалуй и все пироги, на последок пример кода формирования коллекции наших объектов по базовой коллекции построителя:

RollCalcSettings.Settings.LoadFromElementList<DriveSystem, Drive>(settings.DriveSystems);
RollCalcSettings.Settings.LoadFromElementList<LamellaProfile, Profile>(settings.LamellaProfiles);
RollCalcSettings.Settings.LoadFromElementList<ProtectiveCollarType, Collar>(settings.ProtectiveCollarTypes);

Используя такой подход мы добились следующего:

  • Избавили себя от мороки в работе с исходными объектами построителя
  • Сохранили связь с исходными объектами (свойство public T PresentationObject { get; set; } в базовом классе)
  • Добавили гибкости всей системе за счет возможности расширять функционал наследников наших объектов, например тот или иной элемент роллеты может или не может иметь цвет
  • Реализовали один метод инициализации и один метод получения экземпляра для любого типа объекта в системе
  • Узнали тонкости в накладывании ограничений на использование обобщенного типа в классе или методе