Обобщение | Generic

Обобщение | Generic

Обобщения - это переменный тип данных. Обобщения позволяют заранее не задавать тип используемых данных, а определить его во время выполнения программы. Поддержка: интерфейсы, классы, методы, события и делегаты.

Зачем это нужно?

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

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 ограничена интерфейсами и делегатами.

Каждый из параметров-типов обобщенного делегата или интерфейса должен быть помечен как ковариантный или контравариантный. Это не приводит ни к каким нежелательным последствиям, но позволит применять ваших делегатов в большем количестве сценариев и позволит вам осуществлять приведение типа переменной обобщенного делегата к тому же типу делегата с другим параметром-типом.

Параметры-типы могут быть:

  • Инвариантными. Параметр-тип не может изменяться.
  • Контравариантными. Параметр-тип может быть преобразован от класса к классу, производному от него. В языке C# контравариантный тип обозначается ключевым словом in. Контравариантный параметр-тип может появляться только во входной позиции, например, в качестве аргументов метода.
  • Ковариантными. Аргумент-тип может быть преобразован от класса к одному из его базовых классов. В языке С# ковариантный тип обозначается ключевым словом out. Ковариантный параметр обобщенного типа может появляться только в выходной позиции, например, в качестве возвращаемого значения метода.

Предположим, что существует следующий тип делегата:

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

results for ""

    No results matching ""