Как работать с обобщениями (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#
На фоне роста спроса на ликвидность в бычьем рынке 2025 года, криптозаймы снова выходят на…
Прокси (proxy), или прокси-сервер — это программа-посредник, которая обеспечивает соединение между пользователем и интернет-ресурсом. Принцип…
Согласитесь, было бы неплохо соединить в одно сайт и приложение для смартфона. Если вы еще…
Повсеместное распространение смартфонов привело к огромному спросу на мобильные игры и приложения. Миллиарды пользователей гаджетов…
В перечне популярных чат-ботов с искусственным интеллектом Google Bard (Gemini) еще не пользуется такой популярностью…
Скрипт (англ. — сценарий), — это небольшая программа, как правило, для веб-интерфейса, выполняющая определенную задачу.…