Рубріки: Теория

Как работать с обобщениями (generics) в C#: краткий гайд

Ольга Змерзла

Обобщения — конструкции языка программирования C#, позволяющие писать код, который будет одинаково работать с различными типами данных с сохранением строгой типизации языка.

Преимущество использования обобщений — в возможности избежать упаковки и распаковки значимых типов. Это позволяет увеличить производительность кода. Также благодаря обобщениям код можно использовать повторно. Нет необходимости писать свой собственный код для каждого типа данных, не нужно использовать перегрузку методов и писать отдельный классы для типов данных. С generics в C# намного меньше шансов получить какие-либо ошибки, поскольку код лежит в одном конкретном месте программы. 

«О, нет! Обобщения»

Содержание статьи

1. Обобщенное значение по умолчанию

2. Статические поля generic-классов

2.1 Исключения

3. Использование нескольких универсальных параметров

4. Обобщенные методы

5. Пример кода Generics C#

6. Класс Generics

7. Наследование класса Generics

8. Generics Interface

9. Использование Generic с Exception

10. Метод generics

11. Инициализация объекта Generic

12. Массив Generic

13. Итоги

Обобщенное значение по умолчанию

Получить обобщенное значение по умолчанию для параметра типа можно благодаря использованию ключевого слова default. Случаются ситуации, когда нужно присвоить переменным универсальные параметры начальных значений и даже null. Поскольку напрямую присвоить значение нельзя, используется оператор default(T). С его помощью мы можем присвоить типам значений 0 (обнуление полей типа), а ссылочным типам — значение null.

class Account<T>
{
    T id = default(T);
}

Статические поля generic-классов

Статические поля практически всегда уникальны для отдельно взятого закрытого типа:

class Bob<T> { public static int Count; }
...
Console.WriteLine (++Bob<int>.Count); // 1
Console.WriteLine (++Bob<int>.Count); // 2
Console.WriteLine (++Bob<string>.Count); // 1
Console.WriteLine (++Bob<object>.Count); // 1

Создается свой набор статических членов при типизации класса. Не существует единого статического поля для объектов разных типов. В них создаются копии статического поля. 

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

Пример несовместимости кода:

class LengthLimitedSingletonCollection<T> where T : new()
{
  protected const int MaxAllowedLength = 5;
  protected static Dictionary<Type, object> instances = new Dictionary<Type, object>(); // Noncompliant

  public static T GetInstance()
  {
    object instance;

    if (!instances.TryGetValue(typeof(T), out instance))
{
      if (instances.Count >= MaxAllowedLength)
      {
        throw new Exception();
      }
      instance = new T();
      instances.Add(typeof(T), instance);
    }
    return (T)instance;
  }

Пример совместимости кода:

public class SingletonCollectionBase
{
  protected static Dictionary<Type, object> instances = new Dictionary<Type, object>();
}

public class LengthLimitedSingletonCollection<T> : SingletonCollectionBase where T : new()
{
  protected const int MaxAllowedLength = 5;

  public static T GetInstance()
  {
    object instance;

    if (!instances.TryGetValue(typeof(T), out instance))
    {
      if (instances.Count >= MaxAllowedLength)
      {
        throw new Exception();
      }
      instance = new T();
      instances.Add(typeof(T), instance);
    }
    return (T)instance;
  }
}

Исключения

Параметр типа статический член не используется с закрытыми типами:

public class Cache<T>
{
   private static Dictionary<string, T> CacheDictionary { get; set; } // Compliant
}

Использование нескольких универсальных параметров

Как и для методов, обобщения могут использовать более одного типа параметров. Чтобы это осуществить, в операторе <> универсальные типы нужно ввести через запятую:

public class Exercise<U, V>
{
}

Например:

class Transaction<U, V>
{
    public U FromAccount { get; set; }  // денежный перевод со счета
    public U ToAccount { get; set; } // денежный перевод на счет
    public V Code { get; set; }      // код транзакции
    public int Sum { get; set; }     // сумма
}

U и V — два универсальных параметра, используемые в классе Transaction.

Чтобы применить этот класс, необходимо объект Transaction типизировать с помощью string и <int>:

Account<int> acc1 = new Account<int> { Id = 1857, Sum = 4500 };
Account<int> acc2 = new Account<int> { Id = 3453, Sum = 5000 };
 
Transaction<Account<int>, string> transaction1 = new Transaction<Account<int>, string>
{
    FromAccount = acc1,
    ToAccount = acc2,
    Code = "45478758",
    Sum = 900
};

Другими словами, вместо U теперь используется класс Account <int>, а вместо параметра V — тип string.

Класс, типизируемый Transaction, является обобщенным.

Обобщенные методы

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

static void Swap<T> (ref T a, ref T b)
{
    T temp = a; a = b; b = temp;
}

Generic Methods или обобщенные методы меняют местами значения двух переменных:

int x = 5, y = 10;
Swap (ref x, ref y);

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

Swap<int> (ref x, ref y);

В обобщенном типе методы кастомно необобщенные. Чтобы сделать их таковыми, необходимо задать параметр типа. В данном примере это <int>.

Пример кода Generics C#

/// <summary>
    /// Класс для демонстрации шаблонов. 
    /// </summary>
    /// <typeparam name="T"> Тип данных.</typeparam>
    public class TemplateTest<T>
    {
        T[] arr = new T[10];
        int index = 0; 

        /// <summary>
        /// Добавление элемента в массив.
        /// </summary>
        /// <param name="value"> Добавляемый элемент.</param>
        /// <returns> Был ли добавлен элемент.</returns>
        public bool Add(T value)
        {
            if (index >= 10)
            {
                return false;
            }

            arr[index++] = value;
            return true;
        }


        /// <summary>
        /// Получить элемент по индексу.
        /// </summary>
        /// <param name="index"> Индекс.</param>
        /// <returns> Элемент по индексу.</returns>
        public T get(int index)
        {
            if (index < this.index && index >= 0)
            {
                return arr[index];
            }
            else
            {
                return default(T);
            }
        }

        /// <summary>
        /// Получить количество элементов в массиве.
        /// </summary>
        /// <returns> Количество добавленных элементов.</returns>
        public int Count()
        {
            return index;
        }
    }

Шаблонный класс создан, и теперь у нас есть возможность использовать любой тип данных. Но поддержка всех типов данных не всегда нужна, потому при помощи ключевого слова where можно указать ограничения:

public class TemplateTest<T> where T:  YYYYY

где YYYYY может иметь значения:

  • класса;
  • struct;
  • new();
  • имени класса;
  • имени интерфейса.

Класс Generics

Класс Generic определяется знаком <T> после имени самого класса:

public class TestClass<T> { }

В TestClass <> может быть использована любая буква.

Основные типы классов

Класс Generic Описание
Collection<T> Базис для generic-коллекции Compare. Сравнение двух объектов на равенство.
List<T> Список элементов с динамически изменяемым размером.
Stack<T> Реализация списка по принципу LIFO (last in, first out).
Queue<T> Реализация списка по принципу FIFO (first in, first out).
Dictionary<TKey, TValue> Generic-коллекция пар имя/значение.

Наследование класса Generics

Если базовый класс — это класс, который наследуется, то класс, который наследует, называется производным.

То есть производный класс — это особый вариант базового класса, который наследует все методы, свойства и переменные.

Разрешается вводить дополнительный класс в объявление другого класса. Это и называется наследованием.

Generics Interface

Чтобы создать Generics Interface, нужно, прежде всего, следовать правилам создания интерфейса:

public interface ICounter<T>
{
}

Также нужно добавить элементы, которые необходимо переопределить. Рассмотрим пример получения generic-интерфейса из другого generic-интерфейса:

public interface ICounter<T>
{
    int Count { get; }
    T Get(int index);
}

Использование Generic с Exception

Определение Exception с параметрами Generics:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace GenericsTutorial
{
   class MyException<E> : ApplicationException
   {
   }
}

Действительный Generic Exception:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace GenericsTutorial
{
    class UsingGenericExceptionValid02<K>
    {
        public void SomeMethod()
        {
            try
            {
                // ... 
            }
            // Действительно
            catch (MyException<string> e)
            {
                // Сделать что-то здесь.
            }
            // Действительно
            catch (MyException<K> e)
            {
                // Сделать что-то здесь.
            }
            catch (Exception e)
            {
            }
        }
    } 
}

Метод Generics

Метод в классе может стать обобщенным.

Использование метода в Generics:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace GenericsTutorial
{
    public class MyUtilsDemo
    { 
        public static void Main(string[] args)
        { 
            // K = int: Phone
            // V = string: Name
            KeyValue<int, string> entry1 = new KeyValue<int, String>(12000111, "Tom");
            KeyValue<int, string> entry2 = new KeyValue<int, String>(12000112, "Jerry");

            // (K = int).
            int phone = MyUtils.GetKey(entry1);
            Console.WriteLine("Phone = " + phone);

            // Списко содержит элементы вида KeyValue<int,string>.
            List<KeyValue<int, string>> list = new List<KeyValue<int, string>>();

            // Добавить элемент в список.
            list.Add(entry1);
            list.Add(entry2);

            KeyValue<int, string> firstEntry = MyUtils.GetFirstElement(list, null);

            if (firstEntry != null)
            {
                Console.WriteLine("Value = " + firstEntry.GetValue());
            } 
            Console.Read();
        } 
    } 
}

Результат:

Phone = 12000111. Value = Tom

Инициализация объекта Generic

public void DoSomething<T>()
{ 
     // Инициализировать объект Generic
     T t = new T(); // Error 
}

Обычно ошибкой инициализации объекта Generic является отсутствие T() в параметре Т. В таком случае следует добавить when T : new()

Например:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace GenericsTutorial
{
    class GenericInitializationExample 
    {
        // Вид T должен быть видом, имеющим Constructor по умолчанию.
        public void DoSomeThing<T>()
            where T : new()
        {
            T t = new T();
        } 
        // Вид T должен быть видом, имеющим Constructor по умолчанию.
        // и расширенным из класса KeyValue.
        public void ToDoSomeThing<K>()
            where K: KeyValue<K,string>, new( )
        {
            K key = new K();
        }  
        public T DoDefault<T>()
        {
            // Возвращает null если T является ссылочным видом (reference type).
            // Или 0 если T является видом числа  (int, float,..)
            return default(T);
        }
    } 
}

Массив Generic

C# позволяет объявлять обобщенные массивы:

T[] myArray = newT[10];

Например:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace GenericsTutorial
{
   class GenericArrayExample
   {
       public static T[] FilledArray<T>(T value, int count)
       {
           T[] ret = new T[count];
           for (int i = 0; i < count; i++)
           {
               ret[i] = value;
           }
           return ret;
       }
       public static void Main(string[] args)
       {
           string value = "Hello"; 
           string[] filledArray = FilledArray<string>(value, 10);
           foreach (string s in filledArray)
           {
               Console.WriteLine(s);
           }
       }
   }
}

Итоги

Обобщения позволяют указывать обрабатываемые данные в виде параметров (в методах, классах, структурах, интерфейсах).

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

Основные преимущества обобщений:

  1. Безопасность. Исключают необходимость проверки соответствия типов в коде вручную.
  2. Повторное использование кода. Обобщенный класс определяется один раз, а дальше создаются копии.
  3. Производительность. Достигается за счет отсутствия упаковки и распаковки значимых типов.

Видео: обобщения в C#

Останні статті

Обучение Power BI – какие онлайн курсы аналитики выбрать

Сегодня мы поговорим о том, как выбрать лучшие курсы Power BI в Украине, особенно для…

13.01.2024

Work.ua назвал самые конкурентные вакансии в IТ за 2023 год

В 2023 году во всех крупнейших регионах конкуренция за вакансию выросла на 5–12%. Не исключением…

08.12.2023

Украинская IT-рекрутерка создала бесплатный трекер поиска работы

Unicorn Hunter/Talent Manager Лина Калиш создала бесплатный трекер поиска работы в Notion, систематизирующий все этапы…

07.12.2023

Mate academy отправит работников в 10-дневный оплачиваемый отпуск

Edtech-стартап Mate academy принял решение отправить своих работников в десятидневный отпуск – с 25 декабря…

07.12.2023

Переписки, фото, история браузера: киевский программист зарабатывал на шпионаже

Служба безопасности Украины задержала в Киеве 46-летнего программиста, который за деньги устанавливал шпионские программы и…

07.12.2023

Как вырасти до сеньйора? Девелопер создал популярную подборку на Github

IT-специалист Джордан Катлер создал и выложил на Github подборку разнообразных ресурсов, которые помогут достичь уровня…

07.12.2023