Предположим, мы пишем приложение, в котором необходимо работать с данными пользователя:
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# (списки, очереди, стеки и т. д). Знание этого инструмента программирование поможет вам создавать более универсальные структуры и алгоритмы. Удачи в обучении!
Комментарии