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). Після використання значенням посилального типу присвоюється null, а структурного 0.

Наприклад. Якщо в узагальненому класі MyGenericClass<T> оголошуються дві узагальнені змінні з іменами a, b, то значення за замовчуванням може бути присвоєне цим змінним в конструкторі класу за наступним зразком.

// Узагальнений клас
class MyGenericClass<T>
{
  // Узагальнені змінні
  T a, b;

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

 


Зв’язані теми