❓ Как использовать обобщения в C# – краткая инструкция для новичков
Обобщения (generics) необходимы в тех случаях, когда мы не можем заранее знать тип данных, который будем использовать. Они есть во многих языках программирования, и сегодня мы расскажем про обобщения в C# – материал будет полезен для начинающих разработчиков.
Предположим, мы пишем приложение, в котором необходимо работать с данными пользователя:
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 выполняется процесс распаковки (unboxing).
int OneID = (int)userOne.Id; //Распаковка типа Object в тип int
Упаковка и распаковка ведут к снижению производительности, так как требуют затрат ресурсов системы на преобразование типов. Кроме того, такой способ не является безопасным (type safety). Если в переменную (свойство объекта) типа Object упакована строка (string), то при попытке ее распаковки в int мы получим исключение InvalidCastException.
userTwo.Id = "2"; int TwoID = (int)userTwo.Id; // Исключение InvalidCastException
Программа скомпилируется, а исключение выскочит во время её выполнения. Подобных ошибок можно избежать, используя обобщённые типы.
Обобщение типа
Синтаксис:
class ClassName<T> { public T PropertyName { get; set; } }
Перепишем класс с использованием шаблона:
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) конструктор без параметров.
При использовании нескольких ограничений, задать их можно в строгом порядке:
- Название класса, class или struct. Эти ограничения нельзя применять одновременно.
- Название интерфейса.
- 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# (списки, очереди, стеки и т. д). Знание этого инструмента программирование поможет вам создавать более универсальные структуры и алгоритмы. Удачи в обучении!