Кроме обычных типов фреймворк .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 типизируем его определенным типом и передаем ему соответствующие этому типу значения.