Обзор универсальных типов
Каждый разработчик для .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# вы заранее знаете тип каждого элемента в структуре данных. Без универсальных шаблонов вы бы не имели об этом никакого понятия.