❓ Как использовать обобщения в C# – краткая инструкция для новичков

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

Термины:
Обобщения, обобщённые типы, шаблоны, дженерики и generics обозначают одно и то же.

Предположим, мы пишем приложение, в котором необходимо работать с данными пользователя:

class User    
{
 public int  Id { get; set; }
 public string Name { get; set; }
}

У класса User есть два свойства: ID – уникальный идентификатор и Name – имя пользователя. В данном случае идентификатор имеет тип данных int (целочисленный). Однако ID может быть типом string или GUID. Чтобы это предусмотреть, можно пойти одним из путей:

  • Написать много реализаций класса под каждый тип данных.
  • Использовать тип Object как универсальный тип данных.
  • Использовать обобщённые типы.

Первый вариант не подходит из-за избыточности кода. Разберём второй:

class User
{
 public Object  Id { get; set; }
 public string Name { get; set; }
}

Теперь используем этот класс:

User userOne = new User{};
User userTwo = new User{};

userOne.Id = 1;
userTwo.Id = "2";

int OneID = (int)userOne.Id;
string TwoID = (string)userTwo.Id;

Console.WriteLine(OneID);
Console.WriteLine(TwoID);

Код отработает без ошибок, но в нем есть подводные камни. При присвоении свойству Id целочисленного значения происходит процесс упаковки (boxing):

userOne.Id = 1; //Упаковка типа int в тип Object
Упаковка – это неявное преобразование значения некоторого типа (например, int) к типу Object.

При получении данных обратно в переменную типа int выполняется процесс распаковки (unboxing).

int OneID = (int)userOne.Id; //Распаковка типа Object в тип int
Распаковка – это явное преобразование значения типа Object к некоторому другому типу. Подробнее про процессы упаковки и распаковки можно почитать в руководстве по программированию на C#.

Упаковка и распаковка ведут к снижению производительности, так как требуют затрат ресурсов системы на преобразование типов. Кроме того, такой способ не является безопасным (type safety). Если в переменную (свойство объекта) типа Object упакована строка (string), то при попытке ее распаковки в int мы получим исключение InvalidCastException.

userTwo.Id = "2";
int TwoID = (int)userTwo.Id; // Исключение InvalidCastException

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

Обобщение типа

Синтаксис:

class ClassName<T>
{
 public T PropertyName { get; set; }
}
Литера T (от англ. template — шаблон) в угловых скобках означает, что будет использоваться обобщение. Вместо символа T может быть выбрано любое наименование обобщения, однако такое написание считается общепринятым. Параметр в угловых скобках называется универсальным параметром.

Перепишем класс с использованием шаблона:

class User<T>
{
 private T session; //Шаблон применяется к полю
 public T  Id { get; set; } //Шаблон применяется к свойству
 public string Name { get; set; }
}

При создании экземпляра класса вместо буквы Т нужно указать тот тип данных, который нам необходим:

//Треугольные скобки после имени класса указывают на то,
//что внутри класса будет использоваться тип int 
User<int> userOne = new User<int> { }; 

//Треугольные скобки после имени класса указывают на то,
//что внутри класса будет использоваться тип string 
User<string> userTwo = new User<string> { };

userOne.Id = 1;
userTwo.Id = "2";

int OneID = userOne.Id;

//При попытке присвоить переменной типа int значение типа string 
//компилятор выдаст ошибку
int TwoID = userTwo.Id;

Console.WriteLine(OneID);
Console.WriteLine(TwoID);

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

class User<T, U>
{
 private T session; //Шаблон применяется к полю
 public T  Id { get; set; } //Шаблон применяется к свойству
 public U Name { get; set; }
}

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

public void DoAction<T>(T ValueOne, T ValueTwo) {};

Ограничения универсального параметра

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

class UserList<T> where T: User<int>
{
 T [] List = new T[10];
}

Выражение where T: User говорит о том, что универсальный параметр T должен быть представлен классом User, или его наследником. В качестве ограничения может применяться только один класс.

Ограничением могут быть следующие типы:

  • классы;
  • интерфейсы;
  • class – универсальный параметр должен быть классом;
  • struct – универсальный параметр должен быть структурой;
  • new() – универсальный параметр должен иметь общедоступный (public) конструктор без параметров.

При использовании нескольких ограничений, задать их можно в строгом порядке:

  1. Название класса, class или struct. Эти ограничения нельзя применять одновременно.
  2. Название интерфейса.
  3. new().
interface IUser { }

class User { }
 
class UserList<T> where T: User , IUser , new() { }

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

class UserList<T, U, V> 
 where T: User 
 where U: new()
{}

Таким же способом можно задать ограничение обобщённых методов:

public static void DoAction<T>(T ValueOne, T ValueTwo) where T : User {}

Универсальный тип метода DoAction ограничен классом User или его наследником.

***

Обобщения присутствует во многих языках программирования. Шаблонизация используется фактически во всех динамических структурах языка C# (списки, очереди, стеки и т. д). Знание этого инструмента программирование поможет вам создавать более универсальные структуры и алгоритмы. Удачи в обучении!

Источники

ЛУЧШИЕ СТАТЬИ ПО ТЕМЕ

admin
29 января 2017

Изучаем алгоритмы: полезные книги, веб-сайты, онлайн-курсы и видеоматериалы

В этой подборке представлен список книг, веб-сайтов и онлайн-курсов, дающих...