Обзор универсальных типов

Каждый разработчик для .NET постоянно использует универсальные шаблоны, прямо или косвенно. Замечали ли вы, что используете IEnumerable<T> при работе с LINQ в C#? Или, может быть, вы замечали, что большинство методов возвращает IQueryable<T> в примере "универсального репозитория" для обращения к базам данных через Entity Framework? Возможно, вы задавались вопросом, что в этих примерах означает T и зачем это нужно.

Впервые появившись в платформа .NET Framework 2.0, универсальные шаблоны по сути являются "шаблоном кода", который позволяет разработчикам определять структуры данных, безопасные для типов, без фиксации фактического типа данных. Например, List<T> — это универсальная коллекция, которую можно объявить и использовать с любым типом, например List<int>, List<string>, List<Person> и т. д.

Чтобы понять, чем полезны универсальные шаблоны, давайте рассмотрим конкретный пример класса до и после добавления универсальных шаблонов: ArrayList. В .NET Framework 1.0 все элементы ArrayList имели тип Object. Любой добавленный в коллекцию элемент негласно преобразовывался в Object. То же самое происходило и при чтении элементов из списка. Эти процессы называются упаковка-преобразование и распаковка-преобразование, и их использование ухудшает производительность. Более того, отсутствует надежный способ определения типа данных в списке во время компиляции. Это приводит к созданию кода, который неудобно обслуживать. Универсальные шаблоны решают эту проблему, определяя тип данных для каждого экземпляра в списке. Например, вы можете указать, что List<int> всегда будет содержать целые числа, а List<Person> — объекты Person.

Универсальные шаблоны также используются во время выполнения. Среда выполнения знает, какой тип структуры вы используете, что позволяет эффективнее хранить ее в памяти.

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

  using System;
  using System.Collections;
  using System.Collections.Generic;
  using System.Diagnostics;

  namespace GenericsExample {
    class Program {
      static void Main(string[] args) {
        //generic list
        List<int> ListGeneric = new List<int> { 5, 9, 1, 4 };
        //non-generic list
        ArrayList ListNonGeneric = new ArrayList { 5, 9, 1, 4 };
        // timer for generic list sort
        Stopwatch s = Stopwatch.StartNew();
        ListGeneric.Sort();
        s.Stop();
        Console.WriteLine($"Generic Sort: {ListGeneric}  \n Time taken: {s.Elapsed.TotalMilliseconds}ms");

        //timer for non-generic list sort
        Stopwatch s2 = Stopwatch.StartNew();
        ListNonGeneric.Sort();
        s2.Stop();
        Console.WriteLine($"Non-Generic Sort: {ListNonGeneric}  \n Time taken: {s2.Elapsed.TotalMilliseconds}ms");
        Console.ReadLine();
      }
    }
  }

Эта программа возвращает приблизительно следующее:

Generic Sort: System.Collections.Generic.List`1[System.Int32]
 Time taken: 0.0034ms
Non-Generic Sort: System.Collections.ArrayList
 Time taken: 0.2592ms

Первое, что можно заметить, — сортировка списка с универсальным шаблоном осуществляется значительно быстрее, чем без него. Также видно, что для списка с универсальным шаблоном указан конкретный тип ([System.Int32]), а обычный список остается обобщенным. Так как среда выполнения знает, что List<int> с универсальным шаблоном имеет тип Int32, она может применить целочисленный массив для хранения элементов списка в памяти. В то же время для ArrayList без универсального списка нужно приводить все элементы к объекту. Как вы видите этом примере, дополнительные приведения занимают время и замедляют сортировку списка.

Еще одно преимущество понимания типа универсального шаблона заключается в упрощении отладки. При отладке универсального шаблона в C# вы заранее знаете тип каждого элемента в структуре данных. Без универсальных шаблонов вы бы не имели об этом никакого понятия.

См. также