Добавлен , опубликован
Раздел:
C#
Данная статья призвана рассказать начинающим пользователям про Generic-типы и их использование в языке C#.
Вряд ли я смогу охватить весь тот объем, который позволяет реализовать нам данная фича, все рассказать не хватит и статьи - однако здесь содержится основная информация по Generic-типам, которой может быть достаточно, чтобы начать программировать с их использованием.
Самая соль generic-типов кроется в универсальности - это такие типы, на месте которых может находиться... Да почти любой другой тип. Если мы, конечно, укажем любой.
Если вы уже использовали язык для написания хоть каких-нибудь задач, вам наверняка знаком тип List<T>, позволяющий хранить значения. Так, например, тип List<int> хранит в себе числа, а тип List<string> хранит строки. Фактически, список очень похож на массив, за единственной разницей - мы не указываем для него точное количество хранимых данных, его размер является динамичным.
Тип List<T> - классический пример использования Generic-типа.
Следующий код демонстрирует некоторые возможности класса, на случай если вы слышите про это впервые:
var strs = new List<string>(); //Создаем список строк
strs.Add("первая строка"); //Добавляем строку 'первая строка'
strs.Add("вторая строка"); //Добавляем строку 'вторая строка'
var c = strs[1]; //Запишет в 'c' значение "вторая строка"

var ints = new List<int>(); //Создаем список чисел
ints.Add(10); //Записываем в список значение 10
ints.Add(15); //Записываем в список значение 15
var i = strs[1]; //Запишет в i значение "15"
Этот пример уже может рассказать кое-что о классе List - в нем мы можем хранить данные разных типов (о чем я уже писал ранее). Как раз эта возможность и обеспечивается generic-типом, делая класс List универсальней.
Generic-типы объявляются в треугольных скобках, туда же и подставляется значение типа, например:
public class MyClass<T> //Описан класс MyClass с generic-типом T
{
    //Пока что пустое тело класса
}
var myClass = new MyClass<int>(); //Объявляем экземпляр класса MyClass подставляя тип int
Логично, что сам по себе generic-тип в заголовке классов нам погоды не делает. В случае с нашим классом MyClass<T> мы можем подставлять тип T в теле класса, чтобы произвести необходимые операции с этим типом:
public class MyClass<T> 
{
    private T value; //Мы можем подставлять его в качестве типа в любое место внутри класса. в котором мы его объявили

    public T GetValue() 
    {
        return value;
    }
}
Однако generic-типы можно объявить не только для всего класса, но и для отдельного метода. Например:
public class MyClass
{
    private object value;

    public T GetValue<T>() 
    {
        return (T)value;
    }
}
Бывают случаи, когда нам нужно указывать больше одно generic-типа. В этом случае мы перечисляем их через запятую:
public TOutput MyFunction<TInput,TOutput>(TInput input) //Используем больше одного generic-типа.
{
    //Тело функции
}
Ну что ж, возьмем простой пример и посмотрим, как вызываются методы с Generic типом. А вот и пример:
public void MyFunction<T>(T data) 
{
    //Тело функции
}
Для данного примера вызов функции (предполагается что вызываем в том же классе) будет вот таким:
MyFunction<MyType>(value);
При этом наши данные в переменной value должны наследоваться от MyType.
В таких тривиальных примерах, однако, вовсе не обязательно указывать Generic-тип, если компилятор видит что value имеет тип, который можно подставить в качестве T - он подставит его автоматически (да и по функции в принципе логично, что Generic-тип это тип, присущий параметру data.
Иначе говоря, такой пример можно записать еще проще:
MyFunction(value);
В простонародье это называется авторесолв. Но он работает только на простых примерах, когда тип можно легко предположить. Скажем, если вы указали данный тип в качестве возвращаемого для этой самой функции или какого-то параметра функтора - тип вычислить проблематично, но если это входящий параметр, значение которого однозначно определено в вызове - вероятность гораздо выше.
Так же Generic-тип может использоваться для делегата, однако я упущу этот момент в данной статье и оставлю его для статьи про делегаты, но вы можете самостоятельно ознакомиться с этим случаем вот здесь

Ограничение Generic-типа

Что ж, вероятно я уже успел показать все возможные варианты для подстановки generic-типа. Но зачастую наш generic-тип должен представлять какой-то более-менее конкретный тип.
Что это подразумевает?
Наш generic-тип изначально подразумевает любой тип. Но если нам, например, нужно разрешить для подстановки только классы Apple и Banana, наследуемые от класса Fruct - мы должны произвести дополнительные манипуляции.
Generic-типы ограничиваются при помощи ключевого слова where.
На следующих примерах показано место нахождения данного оператора:
public class MyClass<T> where T : struct
{
    //Тело класса
}
public void MyFunction<T1, T2>(T1 arg1, T2 arg2) 
    where T1 : Apple
    where T2 : Banana, new() 
//where пишется для каждого параметра отдельно, разделяясь разве что пробелами
//Однако несколько ограничений для одного параметра пишутся через запятую
{
    //Тело функции
}
Всего имеется 6 типов ограничений:
  • where T: struct
Тип должен быть значением. Такими являются например типы int, bool, float и все прочие типы, не имеющие значения null (все структуры).
  • where T : class
Тип должен быть классом. Такие типы могут иметь значение null, и имеют ряд своих вкусностей. Например, только ограничив тип до класса мы можем использовать оператор as для нашего Generic-типа. Так же, помимо самих классов под этим подразумеваются все остальные ссылочные типы - интерфейсы, массивы, делегаты.
  • where T : new()
Тип должен иметь открытый конструктор без параметров. При использовании с другими ограничениями ограничение new() должно устанавливаться последним.
  • where T : <любой наш класс>
Тип должен наследоваться от указанного класса или быть этим классом.
  • where T : <любой интерфейс>
Тип должен наследоваться от указанного интерфейса или быть им. Таких ограничений можно поставить несколько для одного параметра (так как мы можем наследоваться от нескольких интерфейсов). Интерфейсы так же могут содержать как ограничиваемый Generic-тип, так или любой другой.
  • where T1 :T2
Generic-тип T1 должен быть представлен generic-типом T2 или наследоваться от него.
Как несложно заметить - все случаи кроме :new() - это самые обычные случаи наследования. Единственное что в этих вариантах разное - показаны разные случаи применения этого наследования.

Значение по умолчанию

Бывают случаи, когда нам нужно вывести значение по умолчанию для нашего типа.
Что такое значение по умолчанию?
Для обычных классов это null, для int - 0, для bool - false (и далее по аналогии).
Для таких действий используется ключевое слово default(T):
Например:
public T GetDefaulForType<T>() 
{
    return default(T);
}
Данный пример возвращает значение по умолчанию для типа T.

Заключение

Вот такая вот мини-статья вышла. Здесь разобраны далеко не все случаи применения Generic-типов, однако, если вам хочется углубиться в данную тему - вы можете зайти на MSDN и прочитать о Generic-типах более развернутую информацию.
Мы разобрали что такое Generic-типы, куда они пишутся, на что заменяются. Узнали как эти типы можно ограничить и как вывести значение по умолчанию.
Я оставляю данную статью без конкретных примеров - их вы сможете придумать сами.
Весь код писал внутри ресурса статьи, и, возможно, где-то что-то пропустил. Если вы найдете ошибки в коде - пишите в комментарии, поправлю.
Спасибо за прочтение.
0
18
10 лет назад
0
Познавательная статья, поставлю два жирных плюса.
2
4
10 лет назад
2
а в с++ это называется шаблон
0
29
10 лет назад
0
icedragoxx, да, но сделаны они совершенному по разному механизму. Поэтому тут они Generic-типы, а в тех же плюсах - шаблоны.
Но смысл в них один и тот же
Чтобы оставить комментарий, пожалуйста, войдите на сайт.