C#. Обобщения (Generics). Основные понятия





Обобщения (Generics). Основные понятия. Обобщенные классы и структуры


Содержание


Поиск на других ресурсах:

1. Понятие обобщения (generics). Недостатки необобщенных агрегированных структур данных (коллекций). Необходимость применения обобщений

В первых версиях C# .NET при создании программ данные описывались строго определенными типами, такими как int, float, double и тому подобное. На базе этих типов можно было создавать базовые элементы программы — масивы, структуры, классы и тому подобное.

Необходимость создания одинаковых элементов для различных типов порождала избыточность кода. Так, если в программе нужно было объявить три массива данных типов int, float и char, то каждый массив должен был быть объявлен отдельно. Также отдельно нужно было разрабатывать коды обработки этих массивов. Если каждый массив должен быть обработан одинаковым способом (например, поиск элемента), то код обработки каждого массива повторялся. Отличие было только в описании типа данных массива (int, float, char и т.д.). В результате возникла потребность в создании механизма, который позволил бы обрабатывать каждый массив единственным унифицированным способом независимо от его базового типа данных.

Второй причиной стало улучшение типовой безопасности в случае использования необобщенных коллекций. В этом виде коллекций (CollectionBase, ArrayList, HashTable, SortedList и другие) данные хранятся в виде типа object, который является базовым для всех типов. При вытягивании данных из коллекции нужно явно указывать приведение типа (иначе компилятор выдаст ошибку на этапе компиляции). Если при явном приведении типа указать не тот тип, то ошибка возникнет на этапе выполнения. Во избежание этого нужно добавлять дополнительные блоки проверок с использованием операторов try-catch, is, as. Такие проверки добавят в программу лишний код, что усложнит ее.

Учитывая вышеизложенные недостатки, начиная с версии .NET 2.0 в язык C# была введена поддержка так называемых обобщений.

Обобщение — средство языка C#, позволяющее создавать программный код, содержащий единственное (типизированное) решение задачи для различных типов, с его последующим применением для любого конкретного типа (int, float, char и т.д.).

Термин обобщение ассоциируется с понятием параметризованный тип. С помощью параметризованных типов можно создавать некоторые элементы языка (классы, структуры и т.д.) таким образом, что в них обрабатываемые данные представлены параметром типа.

 

2. Преимущества применения обобщений (generics)

Использование обобщений дает следующие преимущества:

  • упрощение программного кода. Использование обобщений позволяет реализовывать алгоритм для любого типа элементов. Не нужно создавать похожие варианты алгоритма для различных типов (int, float, string и т.д.);
  • обеспечение типовой безопасности. В обобщенных элементах (классах, методах, коллекциях и т.п.) помещаются только объекты определенного типа, указываемого при их объявлении;
  • в обобщениях исключена необходимость явного приведения типа при преобразовании объекта или иного типа обрабатываемых данных;
  • повышение производительности. При использовании обобщений структурные типы передаются по значению. При этом не выполняется упаковка (boxing) и распаковка (unboxing), которые замедляют выполнение программы.

 

3. К каким элементам языка могут быть применены обобщения?

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

  • классам;
  • структурам;
  • интерфейсам;
  • методам;
  • делегатам.

Отдельно выделяют применение обобщений к коллекциям. На базе этого механизма создан ряд обобщенных коллекций: Collection<T>, List<T>, Dictionary<TKey, TValue>, SortedList<TKey, TValue>, Stack<T>, Queue<T>, LinkedList<T>.

Если элемент языка (класс, структура, интерфейс, метод, делегат) оперирует параметризованным типом данных (обобщением), то этот элемент называется обобщенным (обобщенный класс, обобщенный метод и т.д.).

 

4. Синтаксис объявления обобщенного класса для нескольких типов T1, T2, …, TN

В языке C# существует возможность объявлять обобщенные классы, которые работают с некоторыми обобщенными типами. Эти типы служат параметрами, которые могут передаваться в класс.

Общая форма объявления обобщенного класса следующая:

class ClassName<T1, T2, ..., TN>
{
  // Реализация методов и полей класса,
  // которые работают с обобщенными типами T1, T2, ..., TN
  // ...
}

здесь

  • ClassName – имя обобщенного класса;
  • T1, T2, …, TN – имена обобщенных типов, используемых для работы в классе ClassName. Имена типов могут быть любыми (например Type или TT). Имена типов берутся в скобки <>.

Например, упрощенный вид обобщенного класса, получающего параметрами типы Type1, Type2 имеет вид:

class ClassName<Type1, Type2>
{
  // ...
}

После объявления обобщенного класса можно создать ссылку на этот класс. В простейшем случае общая форма создания ссылки на такой класс имеет вид

ClassName<t1, t2, ..., tN> obj = new ClassName<t1, t2, ..., tN>(arguments);

здесь

  • t1, t2, tN – имена типов-заполнителей, которые выступают аргументами. Это могут быть типы-значения int, float, char и т.д., встроенные типы .NET или пользовательские типы, определенные в программе;
  • ClassName<t1, t2, …, tN>(arguments) – вызов конструктора класса, который получает аргументы arguments;
  • obj – имя экземпляра обобщенного класса.

 

5. Синтаксис объявления обобщенного класса и создания ссылки на него для одного типа T

Если в обобщённом классе параметром типа выступает только один тип T, то общая форма может быть проще

class ClassName<T>
{
  // ...
}

После этого объявить ссылку на обобщенный класс ClassName можно по следующей форме

ClassName<type> obj = new ClassName<type>(arguments);

здесь

  • type – тип-заполнитель, который устанавливается в качестве аргумента;
  • ClassName<type>(arguments) – вызов конструктора класса с перечислением аргументов;
  • obj – имя объявляемой ссылки.

 

6. Обобщенные структуры. Синтаксис объявления обобщенной структуры для типа T

Структуры как и классы могут использовать обобщенный тип T в качестве параметра. Общая форма объявления обобщенной структуры для нескольких типов следующая

struct StructName<T1, T2, ..., TN>
{
  // ...
}

здесь

  • StructName – имя структуры;
  • T1, T2, TN – имена параметризованных типов, используемых структурой.

Создание экземпляра структуры с именем StructName имеет следующий общий вид:

StructName<t1, t2, ..., tN> val = new StructName<t1, t2, ..., tN>(arguments);

здесь

  • t1, t2, …, tN – типы-заполнители, на основе которых формируется конкретный экземпляр с именем val;
  • arguments – аргументы конструктора структуры.

 

7. Примеры обобщенных классов получающих параметром тип T

Пример 1. Реализовать класс Point, который определяет точку на координатной плоскости. В классе реализовать:

  • внутренние поля x, y;
  • конструктор с 2 параметрами;
  • свойства доступа к внутренним полям класса;
  • метод, выводящий значения внутренних полей класса.

 

using System;

namespace ConsoleApplication3
{
  // Класс, описывающий точку.
  // Класс получает параметром тип T.
  class Point<T>
  {
    // Внутренние поля
    T x, y;

    // Конструктор
    public Point(T _x, T _y)
    {
      x = _x; y = _y;
    }

    // Свойства
    public T X
    {
      get { return x; }
      set { x = value; }
    }

    public T Y
    {
      get { return y; }
      set { y = value; }
    }

    // Метод, выводящий данные на экран
    public void Print(string message)
    {
      Console.Write(message + " => ");
      Console.WriteLine("x = {0}, y = {1}", x, y);
    }
  }

  class Program
  {
    static void Main(string[] args)
    {
      // Демонстрация использования обобщенного класса Point<T>
      // 1. Создать экземпляр с привязкою к типу int
      Point<int> objInt = new Point<int>(23, 15);
      objInt.Print("objInt");

      // 2. Создать экземпляр с привязкою к типу double
      Point<double> objDouble = new Point<double>(7.88, -3.22);
      objDouble.Print("objDouble = ");

      Console.ReadKey();
    }
  }
}

В вышеприведенном коде в функции Main() создается экземпляр Point класса для типа int

Point<int> objInt = new Point<int>(23, 15);

Соответственно, создается экземпляр класса Point для типа double

Point<double> objDouble = new Point<double>(7.88, -3.22);

Типы int и double, устанавливаемые для класса Point<T> называются закрыто сконструированными типами.

Пример 2. Объявить обобщенный класс, который описывает Трапецию. Класс должен быть обобщенным и оперировать некоторым типом T (класс получает параметром тип T).

В классе реализовать следующие элементы:

  • внутренние поля a, b, h, определяющие соответственно длины сторон и высоту трапеции;
  • конструкторы;
  • свойства A, B, H для доступа к внутренним полям;
  • метод Area(), возвращающий площадь трапеции.

Программный код класса следующий:

using System;

namespace ConsoleApp19
{
  // Обобщенный класс, реализующий трапецию
  class Trapezoid<T>
  {
    // Внутренние поля класса
    private T a, b, h;

    // Конструктор
    public Trapezoid(T _a, T _b, T _h)
    {
      a = _a;
      b = _b;
      h = _h;
    }

    // Свойства доступа к полям класса
    public T A
    {
      get { return a; }
      set { a = value; }
    }

    public T B
    {
      get { return b; }
      set { b = value; }
    }

    public T H
    {
      get { return h; }
      set { h = value; }
    }

    // Метод, возвращающий площадь трапеции
    public double Area()
    {
      double a = Convert.ToDouble(this.a);
      double b = Convert.ToDouble(this.b);
      double h = Convert.ToDouble(this.h);
      return (a + b) * h / 2;
    }
  }

  class Program
  {
    static void Main(string[] args)
    {
      // Продемонстрировать работу обобщенного класса Trapezoid<T>
      // 1. Создать ссылку с типом-заполнителем short
      Trapezoid<short> tr1 = new Trapezoid<short>(3, 5, 2);

      // 2. Вычислить площадь tr1 и вывести ее на экран
      Console.WriteLine("Area of tr1 = {0:f3}", tr1.Area());

      // 3. Создать ссылку tr2 с типом-заполнителем float
      Trapezoid<float> tr2 = new Trapezoid<float>(1.5f, 2.8f, 1.1f);

      // 4. Вычислить площадь tr2
      double area = tr2.Area();
      Console.WriteLine("Area of tr2 = {0:f3}", tr2.Area());

      Console.ReadKey();
    }
  }
}

Результат выполнения программы

Area of tr1 = 8.000
Area of tr2 = 2.365

 

8. Примеры объявления и использования обобщенных структур

Пример 1. Объявляется структура Line<T>, реализующая отрезок на координатной плоскости. Отрезок задан координатами точек (x1; y1), (x2; y2).

В функции main() демонстрируется создание экземпляра структуры с параметрами типов double и long.

using System;

namespace ConsoleApp19
{
  // Обобщенная структура, получающая параметром тип T.
  // Структура определяет линию с координатами точек.
  struct Line<T>
  {
    // Внутренние поля - координаты точек
    public T x1, y1, x2, y2;

    public Line(T x1, T y1, T x2, T y2)
    {
      this.x1 = x1; this.y1 = y1;
      this.x2 = x2; this.y2 = y2;
    }

    public void Print(string message)
    {
      Console.Write(message + "   ");
      Console.WriteLine("x1 = {0}, y1 = {1}, x2 = {2}, y2 = {3}",
        x1, y1, x2, y2);
    }
  }

  class Program
  {
    static void Main(string[] args)
    {
      // Объявить экземпляр структуры Line<T> с параметром типа double
      Line<double> line1 = new Line<double>(2.5, 3.8, -1.4, 8.2);
      line1.Print("line1: ");

      // Объявить экземпляр структуры Line<T> с параметром типа long
      Line<long> line2 = new Line<long>(2323L, -2332L, 5000L, 3000L);
      line2.Print("line2: ");

      Console.ReadKey();
    }
  }
}

Результат выполнения программы:

line1:   x1 = 2.5, y1 = 3.8, x2 = -1.4, y2 = 8.2
line2:   x1 = 2323, y1 = -2332, x2 = 5000, y2 = 3000

Пример 2. Реализовать структуру ComplexNumber<T>, которая описывает комплексное число. Структура получает параметром тип T. В теле структуры реализовать следующие элементы:

  • внутренние поля обобщенного типа T, которые определяют вещественную составляющую (re) и мнимую составляющую (im) комплексного числа;
  • конструктор с двумя параметрами обобщенного типа T;
  • свойства чтения значений полей структуры ComplexNumber<T>;
  • перегруженные операторы + (плюс) и — (минус), которые реализуют соответственно сложение и вычитание комплексных чисел типа ComplexNumber<T>;
  • метод Print() для вывода информации о комплексном числе.

В структуре ComplexNumber<T> реализовать проверку на допустимость типов: могут использоваться только числовые типы.

Текст решения задачи следующий

using System;

namespace ConsoleApp19
{
  // Обобщенная структура, описывающая комплексное число.
  // Структура получает параметром тип T.
  struct ComplexNumber<T>
  {
    // Внутренние поля
    private T re, im;

    // Конструктор
    public ComplexNumber(T _re, T _im)
    {
      // Проверка в конструкторе, есть ли тип T - числовой
      if ((_re is String)||(_re is Char)||(_re is Boolean)||
          (_im is String)||(_im is Char)||(_im is Boolean))
      {
        // Если тип не числовой, сгенерировать исключение
        throw new Exception();
      }

      re = _re; im = _im;
    }

    // Свойства для чтения полей комплексного числа
    public T Re
    {
      get { return re; }
    }

    public T Im
    {
      get { return im; }
    }

    // Метод, выводящий информацию о текущем комплексном числе
    public void Print(string message)
    {
      Console.Write(message + " ");
      Console.WriteLine("re = {0}, im = {1}", re, im);
    }

    // Перегруженный оператор сложения +.
    // Все конвертируется в тип double.
    // Оператор возвращает конкретный тип ComplexNumber<double>.
    public static ComplexNumber<double> operator + (ComplexNumber<T> c1,
      ComplexNumber<T> c2)
    {
      // 1. Конвертировать любой числовой тип T в тип double
      double re = Convert.ToDouble(c1.re) + Convert.ToDouble(c2.re);
      double im = Convert.ToDouble(c1.im) + Convert.ToDouble(c2.im);

      // 2. Создать новый экземпляр (ссылку) с привязкой к типу double
      ComplexNumber<double> res = new ComplexNumber<double>(re, im);

      // 3. Вернуть результат
      return res;
    }

    // Оператор вычитания комплексных чисел.
    // Все конвертируется в тип float.
    // Оператор возвращает конкретный тип ComplexNumber<float>.
    public static ComplexNumber<float> operator - (ComplexNumber<T> c1,
      ComplexNumber<T> c2)
    {
      // Конвертировать любой числовой тип в тип float
      float re = Convert.ToSingle(c1.re) - Convert.ToSingle(c2.re);
      float im = Convert.ToSingle(c1.im) - Convert.ToSingle(c2.im);

      // Объявить ссылку на экземпляр с прив'язкой к типу float
      ComplexNumber<float> res = new ComplexNumber<float>(re, im);

      // Вернуть результат
      return res;
    }
  }

  class Program
  {
    static void Main(string[] args)
    {
      // 1. Объявить 2 комплексных числа для типа double
      //    2.8 + 0.5*j
      ComplexNumber<double> cm1 = new ComplexNumber<double>(2.8, 0.5);

      // 1.7 - 2.2*j
      ComplexNumber<double> cm2 = new ComplexNumber<double>(1.7, -2.2);

      // Сложить комплексные числа
      // Использовать перегруженный оператор +
      ComplexNumber<double> cm3 = cm1 + cm2;
      cm3.Print("cm3: ");

      // ------------------------------------
      // 2. Для типа int - целочисленный тип
      ComplexNumber<int> cm4 = new ComplexNumber<int>(2, 8);
      ComplexNumber<int> cm5 = new ComplexNumber<int>(4, -3);

      // вычесть числа - получить соответствующий экземпляр с типом float
      ComplexNumber<float> cm6 = cm4 - cm5;
      cm6.Print("cm6: ");

      Console.ReadKey();
    }
  }
}

Результат выполнения программы

cm3: re = 4.5, im = -1.7
cm6: re = -2, im = 11

В классе операторные функции operator+() и operator-() возвращают результат для типа-заполнителя double. Это логично, поскольку тип double является наиболее широким числовым типом который может корректно принимать значения других «узких» типов таких как int, float, short, long и тому подобное.

Проверка на корректность типа (определение нечислового типа) осуществляется сразу в конструкторе класса ComplexNumber<T>. Комплексные числа — это данные числовых типов (float, double, int, short, long и т.д.). Если в функции main() попробовать добавить код класса ComplexNumber<string>, который оперирует строками, например

ComplexNumber<string> cmS = new ComplexNumber<string>("a2", "aa3");
ComplexNumber<string> cmS2 = new ComplexNumber<string>("5", "-4");
ComplexNumber<double> cmS3 = cmS + cmS2;

то компилятор сгенерирует исключительную ситуацию в момент вызова конструктора.

 

9. Ограничения, накладываемые на обобщения

В программах на обобщения накладываются следующие ограничения:

  • свойства не могут быть обобщенными но могут использоваться в обобщенном классе (структуре);
  • индексаторы не могут быть обобщенными но могут использоваться в обобщенном классе (структуре);
  • перегруженные операторы (operator) не могут быть обобщенными. Однако использование параметра типа T здесь допускается;
  • события (event) не могут быть обобщенными но могут использоваться в обобщенных классах (структурах);
  • к обобщенному классу нельзя применять модификатор extern;
  • типы указателей нельзя использовать в аргументах типа;
  • если в обобщенном классе (структуре) используется статическое поле (static), то в объекте каждого конкретного типа (int, double и т.д.) создается уникальная копия этого поля. То есть, нет единого статического поля для всех объектов различных типов, которые конструируются;
  • к обобщенному типу T не могут быть применены арифметические операции (+, -, * и другие), а также операции сравнения. Это связано с тем, что при создании экземпляра с типом-заполнителем (инстанциировании) вместо параметра типа может быть использован тип данных, не поддерживающий эти операции.

 

10. Понятие закрыто-сконструированного и открыто-сконструированного типа

Если объявляется класс (структура и т.п.) с некоторым типом T по образцу

class ClassName<T>
{
  ...
}

то этот класс относится к открыто-сконструированному типу. Фактически, данный класс является абстракцией.

Если на основе объявленного класса ClassName<T> объявляется экземпляр класса с некоторым типом-заполнителем, то такой тип класса относится к закрыто-сконструированному типу. Например, для типа-заполнителя double будет сформирован тип

ClassName<double>

который есть закрыто-сконструированным.

Если аргумент типа является параметром типа, то такой тип считается открытым типом. Любой другой тип, не относящийся к открытому типу, считается закрытым типом.

Если для обобщенного типа заданы все аргументы типа, то этот тип считается сконструированным типом. Если все аргументы типа относятся к закрытым типам, то такой тип считается закрыто-сконструированным типом. Если часть аргументов типа относятся к открытым типам (T), то такой тип считается открыто-сконструированным.

 

11. Пример обобщенного класса, получающего параметрами два типа T1, T2

Объявляется класс DoubleTypes<T1, T2>, который получает параметрами два типа с именами T1, T2. В классе объявляются следующие элементы:

  • внутренние поля var1, var2 соответственно типов T1, T2;
  • конструктор;
  • метод Print(), выводящий значения полей var1, var2.

Текст консольной программы следующий

using System;

namespace ConsoleApp19
{
  // Класс, получающий два параметра типа T1, T2
  class DoubleTypes<T1, T2>
  {
    // Внутренние поля класса
    T1 var1;
    T2 var2;

    // Конструктор
    public DoubleTypes(T1 v1, T2 v2)
    {
      var1 = v1;
      var2 = v2;
    }

    // Метод, выводящий информацию о внутренних полях
    public void Print()
    {
      Console.WriteLine("var1 = {0}", var1);
      Console.WriteLine("var2 = {0}", var2);
    }
  }

  class Program
  {
    static void Main(string[] args)
    {
      // 1. Создание ссылки для типов-заполнителей int, string
      DoubleTypes<int, string> dt = new DoubleTypes<int, string>(23, "abc");
      dt.Print();

      // 2. Создание ссылки для char, bool
      DoubleTypes<char, bool> dt2 = new DoubleTypes<char, bool>('z', true);
      dt2.Print();

      // 3. Создание ссылки для float, short
      DoubleTypes<float, short> dt3 = new DoubleTypes<float, short>(25.5F, 11);
      dt3.Print();

      // 4. Создание ссылки для одного типа double
      DoubleTypes<double, double> dt4 = new DoubleTypes<double, double>(23.3, 1.8);
      dt4.Print();

      Console.ReadKey();
    }
  }
}

Результат выполнения программы

var1 = 23
var2 = abc
var1 = z
var2 = True
var1 = 25.5
var2 = 11
var1 = 23.3
var2 = 1.8

 

12. Задание значений по умолчанию для переменных обобщенного типа. Ключевое слово default. Пример

Поскольку, при объявлении обобщенного класса, параметр типа заранее неизвестен, то невозможно определить значение по умолчанию для переменных обобщенного типа. В этом случае применяется ключевое слово default(T). После использования default значению ссылочного типа присваивается null, а структурного 0.

Например. Если в обобщённом классе MyGenericClass<T> объявляются две обобщенные переменные с именами a, b, то значение по умолчанию может быть присвоено этим переменным в конструкторе класса по следующему образцу.

// Обобщенный класс
class MyGenericClass<T>
{
  // Обобщенные переменные
  T a, b;

  // Конструктор
  public MyGenericClass()
  {
    // Присвоение значения по умолчанию переменным a, b
    a = default(T);
    b = default(T);
  }
}

 


Связанные темы