Как работать с обобщениями (generics) в C#: краткий гайд
Обобщения — конструкции языка программирования C#, позволяющие писать код, который будет одинаково работать с различными типами данных с сохранением строгой типизации языка.
Преимущество использования обобщений — в возможности избежать упаковки и распаковки значимых типов. Это позволяет увеличить производительность кода. Также благодаря обобщениям код можно использовать повторно. Нет необходимости писать свой собственный код для каждого типа данных, не нужно использовать перегрузку методов и писать отдельный классы для типов данных. С generics в C# намного меньше шансов получить какие-либо ошибки, поскольку код лежит в одном конкретном месте программы.
«О, нет! Обобщения»
Содержание статьи
1. Обобщенное значение по умолчанию
2. Статические поля generic-классов
3. Использование нескольких универсальных параметров
7. Наследование класса Generics
9. Использование Generic с Exception
11. Инициализация объекта Generic
Получить обобщенное значение по умолчанию для параметра типа можно благодаря использованию ключевого слова default
. Случаются ситуации, когда нужно присвоить переменным универсальные параметры начальных значений и даже null
. Поскольку напрямую присвоить значение нельзя, используется оператор default(T)
. С его помощью мы можем присвоить типам значений 0
(обнуление полей типа), а ссылочным типам — значение null
.
class Account<T> { T id = default(T); }
Статические поля практически всегда уникальны для отдельно взятого закрытого типа:
class Bob<T> { public static int Count; } ... Console.WriteLine (++Bob<int>.Count); // 1 Console.WriteLine (++Bob<int>.Count); // 2 Console.WriteLine (++Bob<string>.Count); // 1 Console.WriteLine (++Bob<object>.Count); // 1
Создается свой набор статических членов при типизации класса. Не существует единого статического поля для объектов разных типов. В них создаются копии статического поля.
Если вы хотите, чтобы статическое поле было общим для всех типов, необходимо определить базовый класс, где будут храниться все ваши статические члены, а после установить свой generic-тип, чтобы он наследовался от базового класса.
Пример несовместимости кода:
class LengthLimitedSingletonCollection<T> where T : new() { protected const int MaxAllowedLength = 5; protected static Dictionary<Type, object> instances = new Dictionary<Type, object>(); // Noncompliant public static T GetInstance() { object instance; if (!instances.TryGetValue(typeof(T), out instance)) { if (instances.Count >= MaxAllowedLength) { throw new Exception(); } instance = new T(); instances.Add(typeof(T), instance); } return (T)instance; }
Пример совместимости кода:
public class SingletonCollectionBase { protected static Dictionary<Type, object> instances = new Dictionary<Type, object>(); } public class LengthLimitedSingletonCollection<T> : SingletonCollectionBase where T : new() { protected const int MaxAllowedLength = 5; public static T GetInstance() { object instance; if (!instances.TryGetValue(typeof(T), out instance)) { if (instances.Count >= MaxAllowedLength) { throw new Exception(); } instance = new T(); instances.Add(typeof(T), instance); } return (T)instance; } }
Параметр типа статический член не используется с закрытыми типами:
public class Cache<T> { private static Dictionary<string, T> CacheDictionary { get; set; } // Compliant }
Как и для методов, обобщения могут использовать более одного типа параметров. Чтобы это осуществить, в операторе <>
универсальные типы нужно ввести через запятую:
public class Exercise<U, V> { }
Например:
class Transaction<U, V> { public U FromAccount { get; set; } // денежный перевод со счета public U ToAccount { get; set; } // денежный перевод на счет public V Code { get; set; } // код транзакции public int Sum { get; set; } // сумма }
U
и V
— два универсальных параметра, используемые в классе Transaction
.
Чтобы применить этот класс, необходимо объект Transaction
типизировать с помощью string
и <int>
:
Account<int> acc1 = new Account<int> { Id = 1857, Sum = 4500 }; Account<int> acc2 = new Account<int> { Id = 3453, Sum = 5000 }; Transaction<Account<int>, string> transaction1 = new Transaction<Account<int>, string> { FromAccount = acc1, ToAccount = acc2, Code = "45478758", Sum = 900 };
Другими словами, вместо U
теперь используется класс Account <int>
, а вместо параметра V
— тип string
.
Класс, типизируемый Transaction
, является обобщенным.
Обобщенные методы также используют универсальные параметры. Они могут быть объявлены в классах, интерфейсах и структурах:
static void Swap<T> (ref T a, ref T b) { T temp = a; a = b; b = temp; }
Generic Methods или обобщенные методы меняют местами значения двух переменных:
int x = 5, y = 10; Swap (ref x, ref y);
Чаще всего компилятор сам определяет тип аргументов, поэтому нет никакой необходимости передавать обобщенный метод. Но в отдельных случаях обобщенный метод можно вызвать со следующими аргументами:
Swap<int> (ref x, ref y);
В обобщенном типе методы кастомно необобщенные. Чтобы сделать их таковыми, необходимо задать параметр типа. В данном примере это <int>
.
/// <summary> /// Класс для демонстрации шаблонов. /// </summary> /// <typeparam name="T"> Тип данных.</typeparam> public class TemplateTest<T> { T[] arr = new T[10]; int index = 0; /// <summary> /// Добавление элемента в массив. /// </summary> /// <param name="value"> Добавляемый элемент.</param> /// <returns> Был ли добавлен элемент.</returns> public bool Add(T value) { if (index >= 10) { return false; } arr[index++] = value; return true; } /// <summary> /// Получить элемент по индексу. /// </summary> /// <param name="index"> Индекс.</param> /// <returns> Элемент по индексу.</returns> public T get(int index) { if (index < this.index && index >= 0) { return arr[index]; } else { return default(T); } } /// <summary> /// Получить количество элементов в массиве. /// </summary> /// <returns> Количество добавленных элементов.</returns> public int Count() { return index; } }
Шаблонный класс создан, и теперь у нас есть возможность использовать любой тип данных. Но поддержка всех типов данных не всегда нужна, потому при помощи ключевого слова where
можно указать ограничения:
public class TemplateTest<T> where T: YYYYY
где YYYYY
может иметь значения:
struct
;new()
;Класс Generic определяется знаком <T>
после имени самого класса:
public class TestClass<T> { }
В TestClass <>
может быть использована любая буква.
Основные типы классов
Класс Generic | Описание |
Collection<T> | Базис для generic-коллекции Compare. Сравнение двух объектов на равенство. |
List<T> | Список элементов с динамически изменяемым размером. |
Stack<T> | Реализация списка по принципу LIFO (last in, first out). |
Queue<T> | Реализация списка по принципу FIFO (first in, first out). |
Dictionary<TKey, TValue> | Generic-коллекция пар имя/значение. |
Если базовый класс — это класс, который наследуется, то класс, который наследует, называется производным.
То есть производный класс — это особый вариант базового класса, который наследует все методы, свойства и переменные.
Разрешается вводить дополнительный класс в объявление другого класса. Это и называется наследованием.
Чтобы создать Generics Interface, нужно, прежде всего, следовать правилам создания интерфейса:
public interface ICounter<T> { }
Также нужно добавить элементы, которые необходимо переопределить. Рассмотрим пример получения generic-интерфейса из другого generic-интерфейса:
public interface ICounter<T> { int Count { get; } T Get(int index); }
Определение Exception
с параметрами Generics
:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace GenericsTutorial { class MyException<E> : ApplicationException { } }
Действительный Generic Exception
:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace GenericsTutorial { class UsingGenericExceptionValid02<K> { public void SomeMethod() { try { // ... } // Действительно catch (MyException<string> e) { // Сделать что-то здесь. } // Действительно catch (MyException<K> e) { // Сделать что-то здесь. } catch (Exception e) { } } } }
Метод в классе может стать обобщенным.
Использование метода в Generics
:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace GenericsTutorial { public class MyUtilsDemo { public static void Main(string[] args) { // K = int: Phone // V = string: Name KeyValue<int, string> entry1 = new KeyValue<int, String>(12000111, "Tom"); KeyValue<int, string> entry2 = new KeyValue<int, String>(12000112, "Jerry"); // (K = int). int phone = MyUtils.GetKey(entry1); Console.WriteLine("Phone = " + phone); // Списко содержит элементы вида KeyValue<int,string>. List<KeyValue<int, string>> list = new List<KeyValue<int, string>>(); // Добавить элемент в список. list.Add(entry1); list.Add(entry2); KeyValue<int, string> firstEntry = MyUtils.GetFirstElement(list, null); if (firstEntry != null) { Console.WriteLine("Value = " + firstEntry.GetValue()); } Console.Read(); } } }
Результат:
Phone = 12000111. Value = Tom
public void DoSomething<T>() { // Инициализировать объект Generic T t = new T(); // Error }
Обычно ошибкой инициализации объекта Generic
является отсутствие T()
в параметре Т
. В таком случае следует добавить when T : new()
Например:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace GenericsTutorial { class GenericInitializationExample { // Вид T должен быть видом, имеющим Constructor по умолчанию. public void DoSomeThing<T>() where T : new() { T t = new T(); } // Вид T должен быть видом, имеющим Constructor по умолчанию. // и расширенным из класса KeyValue. public void ToDoSomeThing<K>() where K: KeyValue<K,string>, new( ) { K key = new K(); } public T DoDefault<T>() { // Возвращает null если T является ссылочным видом (reference type). // Или 0 если T является видом числа (int, float,..) return default(T); } } }
C# позволяет объявлять обобщенные массивы:
T[] myArray = newT[10];
Например:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace GenericsTutorial { class GenericArrayExample { public static T[] FilledArray<T>(T value, int count) { T[] ret = new T[count]; for (int i = 0; i < count; i++) { ret[i] = value; } return ret; } public static void Main(string[] args) { string value = "Hello"; string[] filledArray = FilledArray<string>(value, 10); foreach (string s in filledArray) { Console.WriteLine(s); } } } }
Обобщения позволяют указывать обрабатываемые данные в виде параметров (в методах, классах, структурах, интерфейсах).
Например, с помощью дженериков можно создать общий класс для обработки самых разных данных.
Основные преимущества обобщений:
Видео: обобщения в C#
Прокси (proxy), или прокси-сервер — это программа-посредник, которая обеспечивает соединение между пользователем и интернет-ресурсом. Принцип…
Согласитесь, было бы неплохо соединить в одно сайт и приложение для смартфона. Если вы еще…
Повсеместное распространение смартфонов привело к огромному спросу на мобильные игры и приложения. Миллиарды пользователей гаджетов…
В перечне популярных чат-ботов с искусственным интеллектом Google Bard (Gemini) еще не пользуется такой популярностью…
Скрипт (англ. — сценарий), — это небольшая программа, как правило, для веб-интерфейса, выполняющая определенную задачу.…
Дедлайн (от англ. deadline — «крайний срок») — это конечная дата стачи проекта или задачи…