C#. Обмеження на конструктор new(), базовий клас та інтерфейс

Обмеження на конструктор, базовий клас та інтерфейс


Зміст


Пошук на інших ресурсах:




1. Обмеження на конструктор: where T : new(). Особливості використання

Обмеження типу new() дозволяє отримати екземпляр (об’єкт) узагальненого типу T в узагальненому класі. Для того, щоб отримати цей екземпляр, потрібно щоб тип (клас) T обов’язково містив конструктор без параметрів (конструктор за замовчуванням).
Як відомо, конструктор без параметрів у класі може бути в одному з двох випадків:

  • якщо в класі не оголошено жодного конструктора. У цьому випадку компілятор створює власний конструктор без параметрів, який ще називається конструктор за замовчуванням;
  • якщо в класі конструктор без параметрів оголошується явно. У цьому випадку крім цього конструктора клас може містити реалізації інших додаткових конструкторів з параметрами.

Можлива ситуація, коли в класі реалізовано один або декілька конструкторів з параметрами а конструктора без параметрів не оголошено. У цьому випадку такий клас (тип) не підійде для створення екземпляру (об’єкту) типу T.

 

2. Приклад, що пояснює застосування обмеження new()

Нехай задано два класи:

  • клас MyClass, що містить явно заданий конструктор без параметрів;
  • клас GenClass<T>, що містить обмеження new() для типу T.

Скорочений програмний код класів такий:

// Клас, що містить явно заданий конструктор без параметрів
class MyClass
{
  // явно заданий конструктор без параметрів
  public MyClass()
  {
    // ...
  }
}

// Узагальнений клас, який отримує параметром тип T.
// Тип T має обмеження, яке вимагає, щоб тип T мав у своїй реалізації
// конструктор без параметрів
class GenClass<T> where T : new()
{
  // Внутрішня змінна типу T
  T obj;

  // Конструктор класу GenClass<T>
  public GenClass()
  {
    // Створити екземпляр типу T
    obj = new T(); // працює, оскільки задано new()
  }
}

Як видно з вищенаведеного коду, в узагальненому класі створюється екземпляр узагальненого типу T. Відповідно екземпляр узагальненого класу створюється стандартним способом без проблем

...
// Створити екземпляр узагальненого класу
GenClass<MyClass> obj = new GenClass<MyClass>();
...

Якщо з класу MyClass прибрати конструктор без параметрів а замість нього створити інший конструктор з параметрами, то при створенні екземпляру класу GenClass<T> компілятор видасть помилку

// Клас, що не містить конструктор без параметрів
class MyClass
{
  // Деякий конструктор з параметрами,
  // конструктору без параметрів немає
  public MyClass(int a)
  {
    // ...
  }
}

// Узагальнений клас, який отримує параметром тип T.
class GenClass<T> where T : new()
{
  // ...
}

...

// Створити екземпляр узагальненого класу
GenClass<MyClass> obj = new GenClass<MyClass>(); // тут помилка компіляції
...

Якщо з оголошення класу GenClass<T> прибрати обмеження where T : new() як показано нижче

// Клас не містить обмеження new()
class GenClass<T>
{
  // Внутрішня змінна типу T
  T obj;

  // Конструктор класу GenClass<T>
  public GenClass()
  {
    // Створити екземпляр типу T неможливо!
    obj = new T(); // тут не працює, помилка на етапі компіляції
  }
}

то в конструкторі класу GenClass<T> створити екземпляр типу T буде неможливо.

 

3. Обмеження на базовий клас: where T : BaseClass

При оголошенні узагальненого класу з використанням обмеження на базовий клас в якості обмеження вказується ім’я класу. Цей клас може бути базовим для багатьох класів, що утворюють ієрархію. Таким чином, дається інформація компілятору про обмежену множину класів, які будуть виступати в якості аргументу типу.

Обмеження на базовий клас дає наступні переваги:

  • в узагальненому класі використовується обмежена кількість членів, які визначені в базовому класі та класами, що є похідними від нього. Всі інші класи та їх елементи не використовуються (“відфільтровуються”). Тепер компілятор знає про типи членів, які може мати аргумент типу;
  • забезпечується гарантоване використання потрібної множини класів в якості аргументу типу. Ця множина класів обмежується іменем базового класу. Якщо при створенні екземляру узагальненого класу спробувати вказати тип, що не входить в потрібну множину класів, то виникне помилка на етапі компіляції. Такий підхід є коректний і дозволяє уникнути важковловимих помилок, які можуть виникнути на етапі виконання програми.

 

4. Приклад, що пояснює обмеження на базовий клас. Рисунок

Нехай задано програмний код класів з іменами A, B, C, D, E, F, G, H, I а також програмний код узагальненого класу MyClass<T> в якому тип T обмежується базовим класом D

// Перелік класів, що утворюють ієрархію
class A
{
  // ...
}

class B : A
{
  // ...
}

class C : A
{
  // ...
}

class D : A
{
  // ...
}

class E : D
{
  // ...
}

class F : D
{
  // ...
}

class G : F
{
  // ...
}

class H : B
{
  // ...
}

class I : B
{
  // ...
}

// Узагальнений клас, що отримує параметром тип T, який обмежується класом D
class MyClass<T> where T : D
{
  // ...
}

Згідно з вищенаведеним програмним кодом, класи A, B, C, D, E, F, G, H, I утворюють ієрархію як показано на рисунку 1.

 

C#. Узагальнення. Обмеження на базовий клас в ієрархії

Рисунок 1. Ієрархія класів A, B, C, D, E, F, G, H, I. Обмеження на базовий клас D

Згідно з рисунком 1, при оголошенні екземпляру класу MyClass<T> допускається використовувати типи (класи) D, E, F, G в якості аргументу типу T

// Оголосити екземпляр класу MyClass<T> в якому
// в якості аргументу виступає тип D
MyClass<D> obj1 = new MyClass<D>(); // Ok!

// Аргументом виступає клас E, який є підкласом класу D
MyClass<E> obj2 = new MyClass<E>(); // Ok!

// Аргументом виступає клас F, який є підкласом класу D
MyClass<F> obj3 = new MyClass<F>(); // Ok!

// Підклас класу F також може бути аргументом типу T класу MyClass
MyClass<G> obj4 = new MyClass<G>(); // Ok!

При спробі оголосити екземпляр класу MyClass<T> з будь-яким іншим типом аргументу виникне помилка на етапі компіляції

// Спроба використати інший клас, що не входить в ієрархію де базовим
// класом виступає клас D
MyClass<int> obj = new MyClass<int>(); // помилка на етапі компіляції

// Клас A є базовим для класу D, однак він не входить в ієрархію,
// в якій базовим класом служить клас D. Тому це також помилка
MyClass<A> obj5 = new MyClass<A>(); // помилка на етапі компіляції

// Використання в якості аргументу типу класу з бібліотеки .NET Framework
// також є помилкою
MyClass<System.IO.TextWriter> obj6 = new MyClass<System.IO.TextWriter>(); // помилка компіляції

Таким чином обмеження на базовий клас дозволяє відфільтрувати класи, використання яких в якості аргументу типу для заданого узагальненого класу не має змісту.

 

5. Обмеження на інтерфейс: where T : Interface

Обмеження на інтерфейс потребує реалізації одного або декількох інтерфейсів в якості аргументу типу узагальненого класу.
Як відомо, в інтерфейсі оголошується перелік методів (елементів), які обов’язково потрібно реалізувати в класах, які успадковують цей інтерфейс. Якщо для деякого узагальненого класу в якості обмеження типу T задається ім’я інтерфейсу, то цей тип повинен містити реалізацію цього інтерфейсу.

Обмеження на інтерфейс застосовується у наступних випадках:

  • коли потрібно використовувати члени інтерфейсу в узагальненому класі;
  • коли потрібно гарантувати використання тільки тих аргументів типу, які реалізують вказаний інтерфейс.

Допускається після ключового слова where задавати перелік декількох інтерфейсів. Загальна форма такого оголошення приблизно наступна

class ClassName<T> where T : Interface1, Interface2, ... InterfaceN
{
  ...
}

тут

  • Interface1, Interface2, InterfaceN – перелік імен інтерфейсів, які повинен реалізувати тип T.

У випадку з декількома інтерфейсами тип T повинен містити реалізацію методів всіх інтерфейсів. Якщо не буде повністю реалізований хоча б один інтерфейс, компілятор буде видавати помилку.

 

6. Приклад, що пояснює обмеження на інтерфейс

Нехай задано наступні елементи:

  • інтерфейс IInterface, в якому оголошується один метод Print();
  • клас InformClass, який реалізує інтерфейс IInterface;
  • клас MyClass, який не реалізує інтерфейс IInterface;
  • узагальнений (generic) клас GenClass<T> в якому тип T обмежується іменем інтерфейсу IInterface.

Нижче наводиться скорочений програмний код вищеописаних елементів

...

// Обмеження на інтерфейс
// 1. Задано інтерфейс з деяким методом
interface IInformation
{
  // Деякий метод в інтерфейсі, що виводить деяке повідомлення
  void PrintInfo(string message);
}

// 2. Клас, що реалізує інтерфейс IInfomation
class InformClass : IInformation
{
  // Реалізація методу PrintInfo()
  public void PrintInfo(string message)
  {
    Console.WriteLine("InformClass: " + message);
  }
}

// 3. Клас, який НЕ РЕАЛІЗУЄ інтерфейс IInformation
class MyClass
{
  // Цей клас також містить власний метод PrintInfo()
  public void PrintInfo(string message)
  {
    Console.WriteLine("MyClass: " + message);
  }
}

// 4. Узагальнений клас, який містить обмеження на інтерфейс IInformation
class GenClass<T> where T : IInformation
{
  // ...
}

...

Якщо спробувати оголосити екземпляр (об’єкт) класу GenClass<T>, в якому аргументом типу T виступає клас InformClass

// 1. Спроба оголошення екземпляру узагальненого класу,
// в якому параметром типу виступає клас InformClass
GenClass<InformClass> obj1 = new GenClass<InformClass>(); // працює

то такий код буде працювати правильно. Це зв’язано з тим, що клас InformClass реалізує інтерфейс IInformation

// 2. Клас, що реалізує інтерфейс IInfomation
class InformClass : IInformation
{
  ...
}

Якщо спробувати оголосити екземпляр класу GenClass<T>, в якому аргументом типу виступає тип MyClass, то виникне помилка на етапі компіляції

// 2. Спроба використати параметром типу клас MyClass
GenClass<MyClass> obj2 = new GenClass<MyClass>(); // помилка компіляції

Це пояснюється тим, що клас MyClass не реалізує інтерфейс IInformation

// Клас не реалізує інтерфейс IInformation
class MyClass
{
  ...
}

У цьому випадку спрацьовує обмеження

where T : IInformation

в оголошенні класу GenClass<T>.

 

7. Використання обмежень для встановлення зв’язку між двома параметрами типу. Неприкрите обмеження типу в обмеженнях where T:BaseClass. Приклад

В узагальнених класах при використанні обмежень на базовий клас

where T : BaseClass

можна встановлювати зв’язок між двома параметрами типу.
У найбільш загальному випадку оголошення такого узагальненого класу наступне

class MyClass<T1, T2> where T1 : T2
{
  // ...
}

При такому оголошенні вказується, що:

  • узагальнений клас MyClass отримує параметрами два типи T1, T2;
  • тип T1 обмежений типом T2.

Обмеження на параметр типу T1 називається неприкритим обмеженням типу.

Приклад. Нехай задано наступні класи:

  • клас A, який є базовим для класів B, C;
  • класи B, C, які успадковані від класу A;
  • узагальнений клас GenClass<T1, T2>, який використовує неприкрите обмеження типу T1 типом T2.

Програмний код скороченої реалізації вищевказаних класів наступний

// Деяка ієрархія класів
class A
{
  // ...
}

class B : A
{
  // ...
}

class C : A
{
  // ...
}

// Використання обмежень для встановлення зв'язку між двома параметрами типу.
// У класі GenClass тип T1 обмежується типом T2.
class GenClass<T1, T2> where T1 : T2
{
  // ...
}

Тепер можна створювати екземпляр класу MyClass<T1, T2> наступним чином

// Створення екземпляру класу: GenClass<T1, T2> where T1:T2
GenClass<B, A> obj = new GenClass<B, A>(); // Ok!

Вищенаведений фрагмент є правильним, оскільки клас B є підкласом класу A.

Якщо спробувати створити екземпляр класу іншим способом

// тут помилка, клас A не є підкласом класу B
GenClass<A, B> obj = new GenClass<A, B>(); // помилка компіляції

// тут також помилка, клас C не є підкласом класу B
GenClass<C, B> obj2 = new GenClass<C, B>(); // помилка компіляції

// і це помилка, клас B не є підкласом класу C
GenClass<B, C> obj3 = new GenClass<B, C>(); // помилка

то виникне помилка на етапі компіляції.

 


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