Обобщения - это переменный тип данных. Обобщения позволяют заранее не задавать тип используемых данных, а определить его во время выполнения программы. Поддержка: интерфейсы, классы, методы, события и делегаты.
Зачем это нужно?
Посмотрим на проблему, которая могла возникнуть до появления обобщенных типов. Посмотрим на примере:
System.Collections.ArrayList array = new System.Collections.ArrayList(); int x = 44; // Упаковка значения x в тип Object. array.Add(x); // Распаковка в значение типа int первого элемента коллекции. int y = (int)array[0]; Console.WriteLine(y);
ArrayList содержит коллекцию значений типа
Object
, а это значит, что в вызовеarray.Add(x)
значение переменной x сначала будет "упаковано" в значение типаObject
, а потом при получении элементов из коллекции - наоборот, "распаковано" в нужный тип (Подробнее: Упаковка\Распаковка | Boxing\Unboxing).Но существует другая проблема - проблема безопасности типов. Так, мы получим ошибку во время выполнения программы, если напишем следующим образом:
System.Collections.ArrayList array = new System.Collections.ArrayList(); int x = 44; // Упаковка значения x в тип Object. array.Add(x); strings = "hello"; // Добавление s в коллекцию ArrayList. array.Add(s); // по индексу 1 в коллекции array строка s. int y = (int)array[1];
Обобщенные типы позволяют указать конкретный тип, который будет использоваться. Например, используем обобщенный класс List:
List<int> array = newList<int>();
int x = 44;
array.Add(x);
// Распаковка уже не нужна.
int y = array[0];
string s = "hello";
// здесь будет ошибка компиляции, так как можно добавлять только объекты int.
array.Add(s);
Поскольку класс List
является обобщенным, то нам нужно задать в выражении<тип>
тип данных, для которого этот класс будет применяться. Коллекция в примере типизирована типомint
. Из этого следует, что число будет добавлено в коллекцию array , то строка нет,мы получим ошибку во время компиляции на этой строчке array.Add(s);
.
Таким образом, используя обобщенный вариант класса, мы снижаем время на выполнение и количество потенциальных ошибок.
Создадим теперь свой обобщенный класс Bank:
class Bank<T>
{
T[] clients;
public Bank() { }
public Bank(T[] _clients)
{
this.clients = _clients;
}
}
Используя букву T
в описанииclass Bank<T>
, мы указываем, что данный тип будет использоваться этим классом. В классе мы создаем массив объектов этого типа. Причем сейчас нам неизвестно, что это будет за тип. И это может быть любой тип. А параметр T
в угловых скобках называется "универсальным параметром" или "указателем места заполнения типом Т", так как вместо него можно подставить любой тип.
Например, вместо параметра T можно использовать объект int
. Это также может быть объект string
, либо или любой другой класс или структура:
Bank<int> bank = newBank<int>(new int[] { 1, 2, 4, 5, 6 });
Bank<string> bank2 = newBank<string>(new string[] {"13433","342454","21432"});
Иногда возникает необходимость присвоить переменным универсальных параметров некоторое начальное значение, в том числе и null
. Но напрямую мы его присвоить не можем.
В этом случае нам надо использовать операторdefault(T). Он присваивает ссылочным типам в качестве значения null, а типам значений - значение 0:
classBank<T>
{
T id = default(T);
}
Например, необходимо чтобы класс Bank
хранил набор счетов, представленных объектами класса Account
.
class Account
{
public int Id { get; set;}
}
Но у этого класса может быть много наследников: DepositAccount
, DemandAccount
и т.д. Однако мы не можем знать, какой вид счета в банке в данном случае будет использоваться. В этом случае в качестве универсального параметра можно установить тип Account
:
class Bank<T>
where T : Account
{
T[] accounts;
}
С помощью выраженияwhere T : Account
мы указываем, что используемый тип T
обязательно должен быть классом Account
или его наследником.
При этом мы можем задать множество ограничений через запятую:
class Client { }
interface IAccount { }
interface ITransfer { }
class Bank<T>
where T : Client, IAccount, ITransfer
{
}
Следует учитывать, что только один класс может использоваться в качестве ограничения, в то время как интерфейсов может применяться несколько. При этом класс должен указываться первым.
Кроме того, можно указать ограничение, чтобы использовались только структуры:
class Bank<T>
where T : struct
{
}
или классы:
class Bank<T>
where T : class
{
}
А также можно задать в качестве ограничения на класс или структуру new()
, что позволит реализовать конструктор по умолчанию с помощью слова new:
class Bank<T>
where T : new()
{
}
Мы можем также задать сразу несколько универсальных параметров и ограничения к каждому из них:
class Operation<T, U>
where U : class
where T : Account, new()
{
}
Кроме обобщенных классов можно также создавать обобщенные методы, которые точно также будут использовать универсальные параметры. Например:
abstract class Person
{
public string FirstName { get; set; }
public string LastName { get; set; }
public Person(string fName, string lName)
{
FirstName = fName;
LastName = lName;
}
public void Display();
}
class Client : Person
{
public Client(string lName, string fName) : base(fName, lName)
{
}
public override void Display()
{
Console.WriteLine(FirstName + " " + LastName);
}
}
class Program
{
private static void Display<T>(T person) where T : Person
{
person.Display();
}
static void Main(string[] args)
{
Client client42 = new Client("Том", "Симпсон");
Display<Client>(client42);
}
}
Один обобщенный класс может быть унаследован от другого обобщенного:
class Transaction<T>
{
T inAccount;
T outAccount;
public Transaction(T inAcc, T outAcc)
{
inAccount = inAcc;
outAccount = outAcc;
}
}
class UniversalTransaction<T> : Transaction<T>
{
public UniversalTransaction(T inAcc, T outAcc) : base(inAcc, outAcc)
{
}
}
При этом производный класс необязательно должен быть обобщенным. Например:
class StringTransaction : Transaction<string>
{
public StringTransaction(string inAcc, string outAcc) : base(inAcc, outAcc)
{
}
}
Теперь в производном классе в качестве типа будет использоваться типstring
.
Также производный класс может быть типизирован параметром совсем другого типа, отличного от типа базового класса:
class IntTransaction<T> : Transaction<int>
{
T operationCode = default(T);
public IntTransaction(int inAcc, int outAcc, T operCode) : base(inAcc, outAcc)
{
this.operationCode = operCode;
}
}
Пример с интерфейсом:
Допустим, имеется generic-интерфейс IMyInterface
с одним параметром-обобщения T
. Интерфейс имеет один член - это свойство MyProperty
типа T
:
interface IMyInterface<T>
{
T MyProperty { get; set; }
}
Например, нам поставили задачу спроектировать обобщенный класс A
, который будет реализовывать интерфейс IMyInterface
. Мы сделаем так:
class A <T> : IMyInterface <T>
{
public T MyProperty { get ; set ; }
// остальной код
}
Далее нам ставят задачу спроектировать не обобщенный класс B
, имеющий свойство MyProperty
типа int
, и чтобы он реализовывал интерфейс IMyInterface<T>
. Мы делаем так:
/ / ошибка
class B : IMyInterface <T>
{
public int MyProperty { get ; set ; }
// остальной код
}
И у нас будет ошибка, т.к. класс B
не обобщенный, про типTон ничего не знает, и в тоже время он должен реализовать свойство MyProperty
типа T
.
Чтобы выполнить задачу, мы должны "закрыть" IMyInterface
конкретным типом-аргументом, в нашем случае это int
:
class B : IMyInterface<int>
{
public int MyProperty { get ; set ; }
// остальной код
}
Какой вывод можно сделать?
Когда мы создаем generic-класс, реализующий generic-интерфейс, то, конечно, в интерфейс мы передаем тип-аргумента класса, который может быть любым типом (если, конечно, не имеются ограничения).
Если мы создаем уже не обобщенный класс и должны реализовать generic-интерфейс, то в этот интерфейс мы должны передать конкретный тип-аргумента, грубо говоря из generic'а-интерфейса сделать обычный.
Важно! Контр-вариантность обобщений в C# 4.0 ограничена интерфейсами и делегатами.
Каждый из параметров-типов обобщенного делегата или интерфейса должен быть помечен как ковариантный или контравариантный. Это не приводит ни к каким нежелательным последствиям, но позволит применять ваших делегатов в большем количестве сценариев и позволит вам осуществлять приведение типа переменной обобщенного делегата к тому же типу делегата с другим параметром-типом.
Параметры-типы могут быть:
Предположим, что существует следующий тип делегата:
public delegate TResult MyDelegate <in T , out TResult> (T arg);
Здесь параметр-типT
помечен словом in, делающим его контравариантным, а параметр-типTResult
помечен словом out, делающим его ковариантным. Пусть объявлена следующая переменная:
MyDelegate <Object, ArgumentException> fn1 = null ;
Ее можно привести к типуMyDelegate
с другими параметрами-типами:
MyDelegate <String, Exception>fn2 = fn1 ; // Явного приведения типа не требуется
Exception e = fn2 ( "" );
Это говорит о том, чтоfn1
ссылается на функцию, которая получаетObject
и возвращаетArgumentException
. Переменнаяfn2
пытается сослаться на метод, который получаетString
и возвращаетException
. Так как мы можем передатьString
методу, которому требуется типObject
(типString
является производным отObject
), а результат метода, возвращающегоArgumentException
, может интерпретироваться какException
(типArgumentException
является производным отException
), представленный здесь программный код cкомпилируется, а на этапе компиляции будет сохранена безопасность типов.
Еще пример:
public abstract class Shape { }
public class Circle : Shape { }
public interface IContainer<out T>
{
T Figure { get; }
}
public class Container<T> : IContainer<T>
{
private T figure;
public Container(T figure)
{
this.figure = figure;
}
public T Figure
{
get { return figure; }
}
}
class Program
{
static void Main()
{
Circle circle = new Circle();
IContainer<Shape> container = new Container<Circle>(circle);
Console.WriteLine(container.Figure.ToString());
// Delay.
Console.ReadKey();
}
}
Можно заметить, что переменная container
имеет тип IContainer
параметр которого закрыт типом <Shape>
, а создаваемый экземпляр имеет типContainer
, параметр которого закрыт типом <Circle>
. При приведении типа Container
к IContainer
проблем не будет, стандартный upcast. А для того чтобы привести тип закрывающего параметра <Circle>
к <Shape>
, необходимо при объявлении интерфейса пометить указатель места заполнения типом T
ключевым словом out
=> <out T>
.
Примечание: Вариантность действует только в том случае, если компилятор сможет установить возможность преобразования ссылок между типами. Другими словами, вариантность неприменима для значимых типов из-за необходимости упаковки (boxing).
Частичные (partial) методы не могут иметь out параметров.
Обобщения поддерживают перегрузку параметров типов:
MyClass<T>{ }
MyClass<T,R>{ }
Ресурс с примерами тут - https://metanit.com/sharp/tutorial/3.27.php