Обобщения

Последнее обновление: 29.12.2021

Кроме обычных типов фреймворк .NET также поддерживает обобщенные типы (generics), а также создание обобщенных методов. Чтобы разобраться в особенности данного явления, сначала посмотрим на проблему, которая могла возникнуть до появления обобщенных типов. Посмотрим на примере. Допустим, мы определяем класс для хранения данных пользователя:

class Person
{
	public int Id { get;}
	public string Name { get;}
    public Person(int id, string name)
    {
        Id = id; 
        Name = name;
    }
}

Класс Person определяет два свойства: Id - уникальный идентификатор пользователя и Name - имя пользователя.

Здесь идентификатор пользователя задан как числовое значение, то есть это будут значения 1, 2, 3, 4 и так далее.

Однако также нередко для идентификатора используются и строковые значения. И у числовых, и у строковых значений есть свои плюсы и минусы. И на момент написания класса мы можем точно не знать, что лучше выбрать для хранения идентификатора - строки или числа. Либо, возможно, этот класс будет использоваться другими разработчиками, которые могут иметь свое мнение по данной проблеме, например, они могут для представления идентификатора создать специальный класс.

И на первый взгляд, чтобы выйти из подобной ситуации, мы можем определить свойство Id как свойство типа object. Так как тип object является универсальным типом, от которого наследуется все типы, соответственно в свойствах подобного типа мы можем сохранить и строки, и числа:

class Person
{
	public object Id { get;}
	public string Name { get;}
    public Person(object id, string name)
    {
        Id = id; 
        Name = name;
    }
}

Затем этот класс можно было использовать для создания пользователей в программе:

Person tom = new Person(546, "Tom");
Person bob = new Person("abc123", "Bob");

int tomId = (int)tom.Id;
string bobId = (string) bob.Id;

Console.WriteLine(tomId);   // 546
Console.WriteLine(bobId);   // abc123

Все вроде замечательно работает, но такое решение является не очень оптимальным. Дело в том, что в данном случае мы сталкиваемся с такими явлениями как упаковка (boxing) и распаковка (unboxing).

Так, при передаче в конструктор значения типа int, происходит упаковка этого значения в тип Object:

Person tom = new Person(546, "Tom");	// упаковка в значения int в тип Object

Чтобы обратно получить данные в переменную типов int, необходимо выполнить распаковку:

int tomId = (int)tom.Id;		// Распаковка в тип int

Упаковка (boxing) предполагает преобразование объекта значимого типа (например, типа int) к типу object. При упаковке общеязыковая среда CLR обертывает значение в объект типа System.Object и сохраняет его в управляемой куче (хипе). Распаковка (unboxing), наоборот, предполагает преобразование объекта типа object к значимому типу. Упаковка и распаковка ведут к снижению производительности, так как системе надо осуществить необходимые преобразования.

Кроме того, существует другая проблема - проблема безопасности типов. Так, мы получим ошибку во время выполнения программы, если напишем следующим образом:

Person tom = new Person(546, "Tom");
string tomId = (string)tom.Id;  // !Ошибка  - Исключение InvalidCastException
Console.WriteLine(tomId);   // 546

Мы можем не знать, какой именно объект представляет Id, и при попытке получить число в данном случае мы столкнемся с исключением InvalidCastException. Причем с исключением мы столкнемся на этапе выполнения программы.

Для решения этих проблем в язык C# была добавлена поддержка обобщенных типов (также часто называют универсальными типами). Обобщенные типы позволяют указать конкретный тип, который будет использоваться. Поэтому определим класс Person как обощенный:

class Person<T>
{
	public T Id { get; set; }
    public string Name { get; set; }
    public Person(T id, string name)
    {
        Id = id; 
        Name = name;
    }
}

Угловые скобки в описании class Person<T> указывают, что класс является обобщенным, а тип T, заключенный в угловые скобки, будет использоваться этим классом. Необязательно использовать именно букву T, это может быть и любая другая буква или набор символов. Причем сейчас на этапе написания кода нам неизвестно, что это будет за тип, это может быть любой тип. Поэтому параметр T в угловых скобках еще называется универсальным параметром, так как вместо него можно подставить любой тип.

Например, вместо параметра T можно использовать объект int, то есть число, представляющее номер пользователя. Это также может быть объект string, либо или любой другой класс или структура:

Person<int> tom = new Person<int>(546, "Tom");  // упаковка не нужна
Person<string> bob = new Person<string>("abc123", "Bob");

int tomId = tom.Id;      // распаковка не нужна
string bobId = bob.Id;  // преобразование типов не нужно

Console.WriteLine(tomId);   // 546
Console.WriteLine(bobId);   // abc123

Поскольку класс Person является обобщенным, то при определении переменной после названия типа в угловых скобках необходимо указать тот тип, который будет использоваться вместо универсального параметра T. В данном случае объекты Person типизируются типами int и string:

Person<int> tom = new Person<int>(546, "Tom");  // упаковка не нужна
Person<string> bob = new Person<string>("abc123", "Bob");

Поэтому у первого объекта tom свойство Id будет иметь тип int, а у объекта bob - тип string. И в случае с типом int упаковки происходить не будет.

При попытке передать для параметра id значение другого типа мы получим ошибку компиляции:

Person<int> tom = new Person<int>("546", "Tom");  // ошибка компиляции

А при получении значения из Id нам больше не потребуется операция приведения типов и распаковка тоже применяться не будет:

int tomId = tom.Id;      // распаковка не нужна

Тем самым мы избежим проблем с типобезопасностью. Таким образом, используя обобщенный вариант класса, мы снижаем время на выполнение и количество потенциальных ошибок.

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

// класс компании
class Company<P>
{
    public P CEO { get; set; }  // президент компании
    public Company(P ceo)
    {
        CEO = ceo;
    }
}
class Person<T>
{
    public T Id { get;}
    public string Name { get;}
    public Person(T id, string name)
    {
        Id = id; 
        Name = name;
    }
}

Здесь класс компании определяет свойство CEO, которое хранит президента компании. И мы можем передать для этого свойства значение типа Person, типизированного каким-нибудь типом:

Person<int> tom = new Person<int>(546, "Tom");
Company<Person<int>> microsoft =  new Company<Person<int>>(tom);

Console.WriteLine(microsoft.CEO.Id);  // 546
Console.WriteLine(microsoft.CEO.Name);  // Tom

Статические поля обобщенных классов

При типизации обобщенного класса определенным типом будет создаваться свой набор статических членов. Например, в классе Person определено следующее статическое поле:

class Person<T>
{
    public static T? code;
    public T Id { get; set; }
    public string Name { get; set; }
    public Person(T id, string name)
    {
        Id = id; 
        Name = name;
    }
}

Теперь типизируем класс двумя типами int и string:

Person<int> tom = new Person<int>(546, "Tom");
Person<int>.code = 1234;

Person<string> bob = new Person<string>("abc", "Bob");
Person<string>.code = "meta";

Console.WriteLine(Person<int>.code);       // 1234
Console.WriteLine(Person<string>.code);   // meta

В итоге для Person<string> и для Person<int> будет создана своя переменная code.

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

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

class Person<T, K>
{
    public T Id { get;}
    public K Password { get; set; }
    public string Name { get;}
    public Person(T id, K password, string name)
    {
        Id = id; 
        Name = name;
        Password = password;
    }
}

Здесь класс Person использует два универсальных параметра: один параметр для идентификатора, другой параметр - для свойства-пароля. Применим данный класс:

Person<int, string> tom = new Person<int, string>(546, "qwerty", "Tom");
Console.WriteLine(tom.Id);  // 546
Console.WriteLine(tom.Password);// qwerty

Здесь объект Person типизируется типами int и string. То есть в качестве универсального параметра T используется тип int, а для параметра K - тип string.

Обобщенные методы

Кроме обобщенных классов можно также создавать обобщенные методы, которые точно также будут использовать универсальные параметры. Например:

int x = 7;
int y = 25;
Swap<int>(ref x, ref y); // или так Swap(ref x, ref y);
Console.WriteLine($"x={x}    y={y}");   // x=25   y=7

string s1 = "hello";
string s2 = "bye";
Swap<string>(ref s1, ref s2); // или так Swap(ref s1, ref s2);
Console.WriteLine($"s1={s1}    s2={s2}"); // s1=bye   s2=hello

void Swap<T>(ref T x, ref T y)
{
    T temp = x;
    x = y;
    y = temp;
}

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

При вызове метода Swap типизируем его определенным типом и передаем ему соответствующие этому типу значения.

Дополнительные материалы
Помощь сайту
Юмани:
410011174743222
Перевод на карту
Номер карты:
4048415020898850