Попередня сторінка          Зміст           Наступна сторінка          Електронні посібники ВНТУ

 

ЛАБОРАТОРНАЯ РАБОТА № 7

ІНТЕРФЕЙСИ. КОЛЕКЦІЇ

 

 

Мета: навчитися створювати та реалізовувати інтерфейси

 

7.1 Теоретичні відомості

 

7.1.1 Створення інтерфейсів

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

Помітьте важливу різницю у використанні Інтерфейсів. Клас імплементує інтерфейс на відміну від успадковує базовий клас.

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

Синтаксис задання інтерфейсу схожий на синтаксис визначення класу. Ви використовуєте ключове слово interface для визначення інтерфейсу як це показано в прикладі нижче:

// Визначення інтерфейсу

public interface IBeverage

{

  // Methods, properties, events, and indexers go here.

}

Зауваження: У конвенції написання коду вказано, що імена інтерфейсів мають починатись з "I" хоча це і не обов'язково.

Визначення інтерфейсів можуть містити модифікатор доступу, що схоже на задання класів. Ви можете використовувати модифікатори доступу під час декларації вашого інтерфейсу, подані у таблиці Є.1 (Додаток Є):

Додання членів інтерфейсу

Інтерфейс визначає сигнатуру членів, але не задає імплементацію. Інтерфейс може включати методи, властивості, події та індексатори:

  • Для визначення методу ви задаєте ім'я методу, тип що повертається і параметри:
  • int GetServingTemperature(bool includesMilk);

  • Для визначення властивості, ви зазначаєте ім'я властивості, тип властивості, get та set:
  • bool IsFairTrade { get; set; }

  • Для визначення події, ви використовуєте ключове слово event, що супроводжується делегатом, що здійснює обробку події та ім'ям події:
  • event EventHandler OnSoldOut;

  • Для визначення індексатора, ви зазначаєте тип, що повертається та get і set:
  • string this[int index] { get; set; }

Члени інтерфейсу не включають модифікатори доступу. Усі члени є public. Інтерфейси не можуть включати членів, що відносяться лише до внутрішньої функціональності класу, такі як поля, константи, оператори або конструктори.

Давайте поглянемо на конкретний приклад. Припустимо, що ви хочете розробити програму лояльності для роботи програми компанії, що займається кавою. Ви можете створити інтерфейс, що називається ILoyaltyCardHolder та визначає:

  • Властивість лише для читання з назвою TotalPoints.
  • Метод з назвою AddPoints, що працює з десятковими значеннями.
  • Метод, що називається ResetPoints.

Наступний приклад демонструє інтерфейс, що визначає одну властивість, що лише зчитується, та два методи:

// визначення інтерфейсу

public interface ILoyaltyCardHolder

{

   int TotalPoints { get; }

   int AddPoints(decimal transactionValue);

   void ResetPoints();

}

Помітьте, що методи в інтерфейсі не включають тіло методу. Так само, у властивостях в інтерфейсі визначається чи можуть вони зчитуватись і чи можуть вони задаватись (get; set;), але не зазначено жодної реалізації.  Інтерфейс просто заявляє, що будь-який клас, що його реалізує, повинен включати в себе і забезпечити реалізацію для трьох членів. Розробник класу може вибрати як реалізуються методи. Наприклад, імплементація методу AddPoints буде приймати десяткові значення аргументів (сума готівки при здійсненні транзакції) і повертатиме ціле значення (число бонусів, що додається). Розробник класу може реалізувати ці методи багатьма способами. Наприклад, реалізація методу AddPoints може:

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

Наступний приклад демонструє клас, що реалізовує інтерфейс ILoyaltyCardHolder:

// Реалізація інтерфейсу

public class Customer : ILoyaltyCardHolder

{

   private int totalPoints;

   public int TotalPoints

   {

      get { return totalPoints; }

   }

   public int AddPoints(decimal transactionValue)

   {

      int points = Decimal.ToInt32(transactionValue);

      totalPoints += points;

   }

   public void ResetPoints()

   {

      totalPoints = 0;

   }

   // Інші члени класу Customer.

}

Здійснюючи імплементацію інтерфейсу ILoyaltyCardHolder, клас вказує користувачу, що у ньому здійснена реалізація операції AddPoints. Однією з ключових переваг інтерфейсу є те, що вони можуть поділити код на модулі. Ви можете змінити шлях реалізації інтерфейсу в класі в будь-який час без втручання в користувацькі класи, що оперують визначення інтерфейсів, а не їх реалізаціями.

Явна та неявна реалізації

Коли ви створюєте клас, що імплементує інтерфейс, ви обираєте чи явно чи не явно його задати. Для задання інтерфейсу неявно, ви реалізуєте кожен член інтерфейсу з сигнатурою, що відповідає визначенню в інтерфейсі. Для реалізації інтерфейсу в явному вигляді, ви можете вказати кожне ім'я члена так, щоб було ясно, що елемент належить до певного інтерфейсу.

Наступний приклад показує явну реалізацію інтерфейсу IBeverage:

// Реалізація інтерфейсу явно

public class Coffee : IBeverage

{

   private int servingTempWithoutMilk { get; set; }

   private int servingTempWithMilk { get; set; }

   public int IBeverage.GetServingTemperature(bool includesMilk)

   {

      if(includesMilk)

      {

          return servingTempWithMilk;

      }

      else

      {

         return servingTempWithoutMilk;

      }

   }

   public bool IBeverage.IsFairTrade { get; set; }

   // Інші члени, що не належать до інтерфейсу.

}

У більшості випадків реалізовувати інтерфейс явно чи не явно є лише естетичним вибором. Деякі розробники полюбляють явну реалізацію інтерфейсу, оскільки це робить код більш зрозумілим. Єдиним сценарієм при якому ви мусите використовувати явну реалізацію інтерфейсу – це якщо ви використовуєте два інтерфейси, що використовують ті ж самі ім'я. Наприклад, якщо ви використовуєте інтерфейс з назвою IBeverage та IInventoryItem, і обидва вони декларують властивість Boolean з назвою IsAvailable, вам потрібно буде задати хоча б одну з цих властивостей явно IsAvailable. У цьому сценарії компілятор не зможе вирішити цю проблему без явної реалізації.

 

7.1.2 Поліморфізм інтерфейсів

Поліморфізм – це концепт, у якому стверджується, коли ви можете представляти собою екземпляр класу як екземпляр будь-якого інтерфейсу, який реалізує клас. Поліморфізм інтерфейсів може допомогтизбільшити гнучкість і модульність коду. Припустимо у вас є декілька класів, що реалізовують інтерфейс IBeverage, такі як Coffee, Tea, Juice і т.д. Ви можете писати код, що працює із будь-яким з цих класів, як з екземпляром IBeverage без знання будь-яких деталей реалізації класу. Наприклад, ви можете побудувати колекцію екзекмплярів IBeverage без потреби отримання інформації про кожен клас що імплементує IBeverage.

Наприклад, якщо клас Coffee реалізує IBeverage інтерфейс, ви можете представити новий об'єкт Coffee як екземпляр Coffee або екземпляр IBeverage:

// представлення об'єкту за типом інтерфейсу

Coffee coffee1 = new Coffee();

IBeverage coffee2 = new Coffee();

Ви можете використовувати неявне приведення для перетворення до типу інтерфейсу, тому що ви знаєте, що клас повинен включати в себе всі елементи інтерфейсу.

// Приведення до типу інтерфейсу

IBeverage beverage = coffee1;

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

// Приведення об'єкту типу інтерфейсу до похідного класу

Coffee coffee3 = beverage as Coffee;

// або

Coffee coffee4 = (Coffee)beverage;

Реалізація багатьох інтерфейсів

У багатьох випадках, ви будете хотіти створити класи, що реалізовують більше, ніж один інтерфейс.

Наприклад, ви можете хотіти:

  • Реалізувати IDisposable інтерфейс для того, щоб .NET runtime розпорядився вашим класом вірно.
  • Реалізувати IComparable інтерфейс, щоб дозволити колекціям класів сортувати екземпляри класів.
  • Реалізувати ваш власний інтерфейс для визначення функціональності вашого класу.

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

Наступний приклад демонструє, як створити клас, що реалізовує декілька інтерфейсів:

// Оголошення класу, що використовує декілька інтерфейсів

public class Coffee: IBeverage, IInventoryItem

{

}

 

7.1.Колекції

Коли ви створюєте багато елементів одного і того ж типу, незважаючи на те чи це цілі числа чи це рядки чи це об'єкти класу, наприклад Coffee, вам знадобиться шлях для обробки цих елементів ніби цемасив. Ви, можливо захочете рахувати кількість елементів в масиві, додавати елементи або видаляти елементи з масиву або переглядати один за одним ці елементи. Для цих цілей ви можете використовувати колекції.

Колекції – це важливий інструмент для обробки багатьох елементів. Вони також потрібні і для розробки графічних додатків. Такі елементи як випадаючий список чи меню – це як правило прив'язка до даних в колекції.

Обираємо колекції

Всі колекції мають спільні властивості. Для обробки колекції елементів ви повинні мати можливість:

  • Додавати елементи до колекції.
  • Видаляти елементи з колекції.
  • Отримувати конкретні елементи з колекції.
  • Підрахувати кількість елементів в колекції.
  • Перебрати елементи в колекції один за одним.

Кожен клас колекції в C # надає методи і властивості, які допомагають здійснити ці основні операції. Але крім цих операцій ви захочете керувати колекціями по-різному в залежності від конкретних вимог вашої програми. Колекції поділяються на категорії:

  • Клас List – клас списку як одновимірний масив, який динамічно розширюється в міру додавання елементів. Наприклад, використовувати клас список можна для підтримки списку доступних напоїв у вашому кафе.
  • Клас Dictionary (Словник) зберігає колекцію пар ключ/значення. Кожен елемент в колекції складається з двох об'єктів – ключа і значення. Значення є об'єкт, який ви хочете зберігати та переглядати, а ключ є об'єктом, який ви використовуєте для індексування та пошуку значення. У більшості класів словник, ключ повинен бути унікальним, в той час як повторювані значення цілком прийнятні. Наприклад, ви могли б використовувати клас словників для зберігання списку рецептів кави. Ключ буде містити унікальне ім'я кави, а значення буде містити інгредієнти та інструкції для приготування кави.
  • Класи Queue (Черга) являє собою колекції об'єктів, що умовно називається "перший прийшов – перший пішов", як і відбувається зазвичай (якщо відкинути випадки "я тільки запитати") у звичайних чергах. Ми отримуємо елементи з колекції в тому ж порядку, в якому вони були додані. Наприклад, ви можете використовувати клас чергу для обробки замовлень в кафе, щоб гарантувати, що клієнти отримують свої напої в порядку черги.
  • Класи Stack (Стек) представляє собою колекцію об'єктів, що умовно називається "останній прийшов – перший пішов". Елемент, який ви додали в колекцію останнім є першим елементом, який ви будете переглядати. Наприклад, ви можете використовувати клас стек, щоб визначити 10 останніх відвідувачів вашого кафе.

Коли ви вибираєте вбудований клас колекцій для певного сценарію, задайте собі наступні запитання:

  • Вам потрібен список, словник, стек або черга?
  • Чи буде вам потрібно впорядкувати колекцію?
  • Наскільки велику колекцію ви очікуєте отримати?
  • Якщо ви використовуєте клас словник, чи ви повинні будете отримувати елементи тільки за індексом, чи також за ключем?
  • Чи складається ваша колекція виключно з рядків?

Якщо ви зможете відповісти на всі ці питання, ви будете мати можливість вибрати у C# клас колекції, який найкращим чином відповідає вашим потребам.

 

7.1.Стандартні класи колекцій

Простір імен System.Collections надає ряд колекцій загального призначення, який включає в себе списки, словники, черги і стеки. У таблиці Є.2 (Додаток Є) наведено найбільш важливі класи колекцій в просторі імен System.Collections.

 

7.1.Спеціалізовані класи колекцій

Простір імен System.Collections.Specialized надає класи колекцій, що задовольняють особливі вимоги, такі як спеціалізовані колекції словники і строго типізовані колекції рядків. Таблиця Є.3 (Додаток Є) демонструє найбільш важливі класи колекцій в просторі імен System.Collections.Specialized.

 

7.1.Використання колекцій

Найбільш часто використовувана колекція – це клас ArrayList. ArrayList зберігає елементи як лінійну колекцію елементів. Ви можете додати об'єкти будь-якого типу до колекції ArrayList, але ArrayListпредставляє кожен елемент в колекції як екземпляр System.Object. Коли ви додасте елемент до колекції ArrayList, ArrayList неявно приведе до типу чи конвертує ваші елементи до типу Object. Коли ви проходите по елементах колекції, ви маєте явно перетворювати об'єкти назад в оригінальний тип.

Наступний приклад демонструє як додавати і проходитись по колекції ArrayList:

Додавання і проходження по елементах ArrayList

// Створення ArrayList колекції.

ArrayList beverages = new ArrayList();

 

// Створення елементів, які ми хочемо додати до колекції.

Coffee coffee1 = new Coffee(4, "Arabica", "Columbia");

Coffee coffee2 = new Coffee(3, "Arabica", "Vietnam");

Coffee coffee3 = new Coffee(4, "Robusta", "Indonesia");

 

// Додавання елементів до колекції.

// Елементи неявно приводяться до типу Object, коли ви їх додаєте.

beverages.Add(coffee1);

beverages.Add(coffee2);

beverages.Add(coffee3);

 

// Проходження по елементах у колекції.

// Елементи повинні бути явно перетворені назад до початкового типу.

Coffee firstCoffee = (Coffee)beverages[0];

Coffee secondCoffee = (Coffee)beverages[1];

Коли ви працюєте з колекціями, одним з найбільш звичних задач є ітерування по колекції. В основному це означає, що ви переглядаєте кожен елемент колекції по черзі, як правило, щоб оцінити кожен елемент по відношенню до деяких критеріїв або для знаходження певних значень для кожного елементу. Для того, щоб перебрати колекцію, ви можете використовувати цикл foreach. Цикл foreach відображає кожен елемент з колекції по черзі, використовуючи ім'я змінної, що ви задали під час декларації циклу.

Наступний приклад демонструє, як проходити по колекції ArrayList:

// Проходження по списку

foreach(Coffee coffee in beverages)

{

   Console.WriteLine("Bean type: {0}", coffee.Bean);

   Console.WriteLine("Country of origin: {0}", coffee.CountryOfOrigin);

   Console.WriteLine("Strength (1-5): {0}", coffee.Strength);

}

Використання колекції словник

Клас словник зберігає колекцію пар ключ/значення. Найбільш поширений клас словників – Hashtable. Коли ви додаєте елемент до колекції Hashtable, ви маєте визначити ключ та значення. Ключ та значення можуть бути екземплярами будь-якого класу, але Hashtable неявно переведе ключ і значення до типу об'єкт. Коли ви проходитесь по значеннях колекції, ви маєте явно перевести об'єкт в його оригінальний тип.

Наступний приклад демонструє як проходити по елементах з колекції Hashtable. У цьому випадку ключ і значення це рядки:

Додавання і проходження по елементах Hashtable

// Створення нової Hashtable колекції.

Hashtable ingredients = new Hashtable();

 

// Додавання пар ключ/значення до колекції.

ingredients.Add("Café au Lait", "Coffee, Milk");

ingredients.Add("Café Mocha", "Coffee, Milk, Chocolate");

ingredients.Add("Cappuccino", "Coffee, Milk, Foam");

ingredients.Add("Irish Coffee", "Coffee, Whiskey, Cream, Sugar");

ingredients.Add("Macchiato", "Coffee, Milk, Foam");

 

// Перевірка чи такий ключ існує.

if(ingredients.ContainsKey("Café Mocha"))

{

   // Отримання значення, що асоціюється зі значенням ключа.

   Console.WriteLine("The ingredients of a Café Mocha are: {0}", ingredients["Café Mocha"]);

}

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

У наступному прикладі демонструється як проходити по ключах в колекції Hashtable і отримувати значення, що асоціюється з кожним ключем:

// Ітерування по колекції

foreach(string key in ingredients.Keys)

{

   // Для кожного ключа по черзі знаходиться значення, що з ним асоціюється.

   Console.WriteLine("The ingredients of a {0} are {1}", key, ingredients[key]);

}

Запити по колекції за допомогою предикатів лямбда-виразів

Деякі колекції в .NET Framework не підтримують позначень масиву для доступу до елементу в колекції. Ці колекції надають метод Find для визначення місця розташування елементів в колекції. Метод Find вимагає задання предиката, який буде використаний як критерій пошуку. В цьому випадку предикат стає методом, який буде перевіряти кожен елемент в колекції, повертаючи логічне значення Boolean на основі результатів співпадінь. Пошук закінчується, як тільки елемент знайдений.

Предикати, як правило, записуються у вигляді лямбда-виразу. За аналогією з методами, з якими ви уже знайомі, лямбда-вираз містить список параметрів і тіло методу, але він не містить ім'я методу і не містить тип значення, що повертається. Тип, що повертається, отримується з контексту, в якому використовується лямбда-вирази.

Наступний приклад лямбда-виразу для запиту по колекції об'єктів Employee( колекція також продемонстрована ).

List<Employee> employees= new List<Employee>()

{

new Employee() { empID = 001, Name = "Tom", Department= "Sales"},

new Employee() { empID = 024, Name = "Joan", Department= "HR"},

new Employee() { empID = 023, Name = "Fred", Department= "Accounting" },

new Employee() { empID = 040, Name = "Mike", Department= "Sales" },

};

 

// Знайти елемент списку, де employee id дорівнює 023

Employee match = employees.Find((Employee p) => { return p.empID == 023; });

Console.WriteLine("empID: {0}\nName: {1}\nDepartment: {2}", match.empID, match.Name, match.Department);

 

Результат згенерований кодом:

empID: 023

Name: Fred

Department: Accounting

Лямбда-вираз у коді вище наступний (Employee p) => { return p.empID == 023; }.

 

7.1.7 Узагальнення (Generics)

У темі про колекції, ви побачили, що при використання колекції ArrayList дозволено зберігати різні типи даних. Це відрізняється від масиву, де типи даних в масиві повинні бути одного і того ж типу. Але у ArrayList також існує проблема. Ми обговорювали раніше, що усе, що ви зберігаєте в ArrayList автоматично переводиться в тип Object, оскільки він є кореневим в .NET. Пригадаємо, що поліморфізм дозволяє базовому класу представляти підклас. .NET використовує Object як базовий клас для усіх інших типів, що створюються в C#.

Той факт, що ArrayList зберігає усі елементи як Object також означає, що під час проходження вам потрібно приводити усі об'єкти назад в той тип, у якому вони були спочатку. Це може бути проблемою або навіть може викликати помилку. Для вирішення цієї помилки ви можете використовувати Узагальнення (Generics).

Узагальнення дозволяють вам створювати та використовувати строго типізовані колекції і при цьому не потрібно здійснювати приведення типів та здійснювати пакування та розпакування (box, unbox) типів значень.

Створення та використання класу Узагальнення

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

    Додати параметр типу T в трикутних дужках після ім'я класу.

    Використовуйте параметр типу T замість назви типу членів вашого класу.

Наступний приклад демонструє як створювати клас узагальнення:

// Створення класу Узагальнення

public class CustomList<T>

{

   public T this[int index] { get; set; }

   public void Add(T item)

   {

      // Логіка методу.

   }

   public void Remove(T item)

   {

      // Логіка методу.

   }

}

Коли ви створюєте екземпляр вашого класу Узагальнення, ви задаєте тип, що ви хочете використовувати як параметр типу. Наприклад, якщо ви хочете використовувати вами заданий список для зберігання об'єктів типу Coffee, ви будете задавати Coffee як параметр типу.

Наступний приклад показує як створити екземпляр класу узагальнення:

//створення екземпляру класу узагальнення

CustomList<Coffee> clc = new CustomList<Coffee>;

Coffee coffee1 = new Coffee();

Coffee coffee2 = new Coffee();

clc.Add(coffee1);

clc.Add(coffee2);

Coffee firstCoffee = clc[0];

Коли ви створюєте екземпляр класу, то кожен об'єкт T буде замінений на об'єкт типу, що ви задали. Наприклад, Якщо ви задаєте CustomList клас з параметрами типу Coffee:

  • Метод Add буде приймати лише аргументи типу Coffee.
  • Метод Remove буде приймати лише аргументи типу Coffee.
  • Індексатор повертатиме значення типу Coffee.

Переваги Generics

Використання класів узагальнення, особливо для колекцій, має три великі переваги порівняно з не узагальненими підходами: безпека типів, немає приведення типів, немає пакування та розпакування (boxing, unboxing).

 

7.1.8 Безпека типів

Розглянемо приклад, де ви використовуєте ArrayList для зберігання колекції об'єктів типу Coffee. Ви можете додати об'єкти будь-якого типу до ArrayList. Уявимо, що розробник додав об'єкт типу Tea до колекції. Код працюватиме без посилок, однак виникне помилка під час виконання при виклику методу Sort, оскільки колекції не можуть порівнювати об'єкти різних типів (у загальному випадку). Більш того, коли ви переглядаєте об'єкт колекції, ви маєте приводити об'єкт до коректного типу. Якщо ви намагатиметесь привести об'єкт до не правильного типу, то виникне помилка під час виконання.

Наступний приклад демонструє обмеження, що стосуються обмежень безпеки типів при використанні підходу ArrayList:

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

var coffee1 = new Coffee();

var coffee2 = new Coffee();

var tea1 = new Tea();

var arrayList1 = new ArrayList();

arrayList1.Add(coffee1);

arrayList1.Add(coffee2);

arrayList1.Add(tea1);

// Метод Sort видає помилку часу виконання, тому що колекція не є однорідною.

arrayList1.Sort();

// Приведення типів видає помилку часу виконання, бо ви не можете привести екземпляр типу Tea до Coffee.

Coffee coffee3 = (Coffee)arrayList1[2];

Альтернативою ArrayList може бути узагальнення List<T> для збереження колекції об'єктів Coffee. Коли ви створюєте список, ви надаєте елемент типу Coffee. У цьому випадку ваш список гарантовано буде однорідним, тому що вам не буде дозволено під час запуску коду додати об'єкт іншого типу. Метод Sort буде працювати, оскільки колекція буде однорідна. В решті решт, індексатор поверне об'єкт типу Coffee, а не System.Object, отже немає ризиків отримання помилки приведення типів.

Наступний приклад показує альтернативне використання замість ArrayList узагальнення List<T>:

// Використання узагальнень для безпечного використання типів

var coffee1 = new Coffee();

var coffee2 = new Coffee();

var tea1 = new Tea();

var genericList1 = new List<Coffee>();

genericList1.Add(coffee1);

genericList1.Add(coffee2);

// Цей рядок викличе помилку під час побудови програми, оскільки аргумент не типу Coffee.

genericList1.Add(tea1);

// Метод Sort буде працювати, оскільки гарантовано, що колекція буде однорідна.

genericList1.Sort();

// Індексатор повертає об'єкт типу Coffee, тому немає потреби робити перетворення значення, що повертається.

Coffee coffee3 = genericList[1];

Приведення типів

Приведення типів – це досить дороге задоволення в плані використання обчислювальних ресурсів. Коли ви додаєте елемент в ArrayList, ваші елементи неявно приводяться до типу System.Object. Коли ви проходитесь по елементах з ArrayList, ви маєте явно привести їх назад до оригінального типу. Використовуючи узагальнення для додавання і проходження по елементах без приведення типів, ви можете покращити продуктивність вашого додатку.

Пакування та розпакування

Якщо ви хочете зберігати значення(наприклад int,float,double,...) в ArrayList, ви повинні здійснити пакування (boxing) елементів при додаванні до колекції і розпакування (unboxing), коли ви їх хочете переглянути. Boxing та unboxing вимагає значних обчислювальних ресурсів і може значно уповільнити програму, особливо якщо ви проходитесь по великій колекції. На противагу, ви можете додати значення до узагальнення без пакування та розпакування.

Наступний приклад показує різницю між узагальненим і не узагальнемим підходом щодо використання колекцій:

// Boxing та Unboxing: Узагальнення на противагу не узагальненому підходу

int number1 = 1;

var arrayList1 = new ArrayList();

// Пакування Int32 як System.Object.

arrayList1.Add(number1);

// Розпакування Int32.

int number2 = (int)arrayList1[0];

var genericList1 = new List<Int32>();

// Додавання Int32 без пакування.

genericList1.Add(number1);

//Отримання Int32 без розпакування.

int number3 = genericList1[0];

Обмеження для узагальнень

У деяких випадках, ви можете обмежити типи, що розробник може використовувати, коли він створює екземпляр узагальненого класу. Природа цих обмежень залежить від логіки, що ви плануєте реалізувати. Наприклад, якщо колекція класів використовує властивість з назвою AverageRating для сортування елементів в колекції, вам потрібне буде обмеження на тип параметру для класу, що включає властивість AverageRating. Уявимо, що AverageRating властивість визначена IBeverage в інтерфейсі. Для реалізації ви будете обмежувати тип параметра для класів, що реалізують інтерфейс IBeverage, використовуючи ключове слово where.

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

// Обмеження параметра типу

public class CustomList<T> where T : IBeverage

{

}

Ви можете застосувати шість типів обмежень для параметру типу, який наведений у таблиці Є.  (Додаток Є).

Ви можете застосувати наступні шість типів обмежень для параметру типу:

// Застосування багатьох обмежень

public class CustomList<T> where T : IBeverage, IComparable<T>, new()

{

}

Використання узагальнення для колекцій

Одним з найбільш загальних і важливих застосувань узагальнень є створення колекцій. Узагальнення для колекції поділяються на дві широкі категорії: узагальнення списку і узагальнення словників. Узагальнення списку зберігає колекцію об'єктів типу T.

Клас List<T>

Клас List<T> надає строго типізовані альтернативи для класу ArrayList. Як клас ArrayList, клас List<T> включає методи для:

  • Додавання елементу.
  • Видалення елементу.
  • Додавання елементу за певним індексом.
  • Сортування елементів в колекції з використанням порівняння за замовчуванням або створення власного способу порівняння.
  • Зміна порядку частини чи усієї колекції.

Наступний приклад демонструє як використовувати клас List<T>.

// Використання класу List<T>

string s1 = "Latte";

string s2 = "Espresso";

string s3 = "Americano";

string s4 = "Cappuccino";

string s5 = "Mocha";

// Додавання елементів до строго типізованої колекції.

var coffeeBeverages = new List<String>();

coffeeBeverages.Add(s1);

coffeeBeverages.Add(s2);

coffeeBeverages.Add(s3);

coffeeBeverages.Add(s4);

coffeeBeverages.Add(s5);

// Сортування елементів з використанням порівняння за замовчуванням.

// Для об'єкта типу String, порівняння за замовчуванням в алфавітному порядку.

coffeeBeverages.Sort();

// Колекція у консольному вікні.

foreach(String coffeeBeverage in coffeeBeverages)

{

   Console.WriteLine(coffeeBeverage);

}

Інші узагальнення для списків

Простір імен System.Collections.Generic також включає різні узагальнення колекцій, що надають більш спеціалізовану функціональність:

  • Клас LinkedList<T> надає узагальнення, в якому кожен елемент пов'язаний з попереднім і наступним елементом колекції. Кожен елемент в колекції представлений об'єктом LinkedListNode<T>, що містить значення типу T, посилання на екземпляр батька LinkedList<T> , посилання на попередній елемент в колекції, і посилання на наступний елемент в колекції.
  • Клас Queue<T> представляє строго типізовану колекцію типу "перший зайшов - перший вийшов".
  • Клас Stack<T> представляє строго типізовану колекцію типу "останній зайшов - перший пішов".

Використання узагальнення словників

Клас словників зберігає колекцію пар ключ/значення. Значення - це об'єкт, що ви хочете зберігати, а ключ - це об'єкт, що ви використовуєте як індекс для отримання значення. Наприклад, ви можее використовувати клас словник для зберігання рецептів кави, де ключ - це назва кави і значення - це спосіб приготування. У випадку узагальнення словників і ключ і значення є строго типізованими.

Клас Dictionary<TKey, TValue>

Dictionary<TKey, TValue> - строго типізований клас словників загального призначення. Ви можете додати однакові значення, але ключі мають бути унікальні. Клас видасть помилку ArgumentException, якщо ви будете намагатись додати ключ, що уже існує в словнику.

Наступний клас демонструє як використовувати словник <TKey, TValue>:

// Використання словника <TKey, TValue>

// Створення нового словника рядків з ключами, що теж містять рядок.

var coffeeCodes = new Dictionary<String, String>();

// Додавання елементів в словник.

coffeeCodes.Add("CAL", "Café Au Lait");

coffeeCodes.Add("CSM", "Cinammon Spice Mocha");

coffeeCodes.Add("ER", "Espresso Romano");

coffeeCodes.Add("RM", "Raspberry Mocha");

coffeeCodes.Add("IC", "Iced Coffee");

// Наступний рядок викличе помилку ArgumentException, оскільки ключ уже існує.

// coffeeCodes.Add("IC", "Instant Coffee");

// Для отримання значення, що асоціюється з ключем, ви можете використовувати індексатор.

// Виникне KeyNotFoundException помилка, якщо такий ключ не існує.

Console.WriteLine("The value associated with the key \"CAL\" is {0}", coffeeCodes["CAL"]);

// Як альтернатива ви можете використовувати метод TryGetValue.

// Буде повернено true, якщо ключ існує і false, якщо ключ не існує.

string csmValue = "";

if(coffeeCodes.TryGetValue("CSM", out csmValue))

{

   Console.WriteLine("The value associated with the key \"CSM\" is {0}", csmValue);

}

else

 

{

   Console.WriteLine("The key \"CSM\" was not found");

}

// Ви також можете використовувати індексатор для зміни значення, що асоціюється з ключем.

coffeeCodes["IC"] = "Instant Coffee";

Інші класи словників

SortedList<TKey, TValue> і SortedDictionary<TKey, TValue> класи надають узагальнення словників, у яких елементи сортовані за ключем. Різниця між цими класами в реалізації:

  • Клас узагальнення SortedList використовує менше пам'яті, ніж SortedDictionary.
  • Клас SortedDictionary швидший і більш ефективний у додаванні і видалені несортованих даних.

Використання колекцій інтерфейсів

Простір імен System.Collections.Generic надає ряд загальних колекцій, щоб задовольнити потреби різних сценаріїв використання. Проте будуть обставини, при яких ви захочете створити свої власні класи узагальнень для того, щоб забезпечити більш спеціалізовану функціональність. Наприклад, ви зможете зберігати дані у вигляді дерева.

Що потрібно почати робити, якщо ви хочете створити окремий клас колекції? Всі колекції мають деякі спільні риси. Наприклад, ви захочете отримати можливість перераховувати елементи колекції за допомогою циклу foreach, і ви будете потребувати методи для додавання елементів, видалення елементів і очищення списку.

Інтерфейси IEnumerable та IEnumerable<T>

Якщо ви хочете мати змогу використовувати foreach для перерахування елементів у вашій власній колекції узагальнення, ви маєте реалізувати інтерфейс IEnumerable<T>. Інтерфейс IEnumerable<T> визначає єдиний метод з назвою GetEnumerator(). Цей метод має повертати об'єкт типу IEnumerator<T>. foreach опирається на об'єкти перерахування і здійснює перерахування по колекції.

Інтерфейс IEnumerable<T> наслідується від інтерфейсу IEnumerable, що також визначає єдиний метод GetEnumerator(). Коли інтерфейс успадковується від іншого інтерфейсу, він надає усі елементи батьківського інтерфейсу. Іншими словами, якщо ви реалізуєте IEnumerable<T>, вам також потрібно реалізувати IEnumerable.

Інтерфейс ICollection<T>

ICollection<T> інтерфейс визначає базову функціональність, що притаманна усім колекціям узагальнень. Інтерфейс успадковує від IEnumerable<T>, що означає, якщо ви хочете реалізувати ICollection<T>, ви повинні також реалізувати члени, що визначені IEnumerable<T> і IEnumerable.

Методи інтерфейса ICollection<T> наведені в таблиці Є.5 (Додаток Є).

Властивості інтерфейса ICollection<T> наведені в таблиці Є.6 (Додаток Є).

Інтерфейс IList<T>

Інтерфейс IList<T> визначає ключову функціональність для списків узагальнень. Ви повинні реалізувати цей інтерфейс, якщо ви визначаєте лінійну колекцію одиничних об'єктів. На додаток до членів, що успадковуються від ICollection<T>, IList<T> інтерфейс визаначає методи і властивості, що дозволяє використовувати індексатори для роботи з елементами в колекції. Наприклад, ящо ви створите список з назвою myList, ви можете використовувати myList[0] для досутуп до перших елементів в колекції.

Методи інтерфейса IList<T> наведені в таблиці Є.7 (Додаток Є).

Властивості інтерфейса IList<T> наведені в таблиці Є.8 (Додаток Є).

Інтерфейс IDictionary<TKey, TValue>

Інтерфейс IDictionary<TKey, TValue> визначає ключову функціональність для узагальнених словників. Ви повинні реалізувати інтерфейс, якщо ви визначаєте колекцію пар значення/ ключ. На додаток до елементів, що наслідуються від ICollection<T>, IDictionary<T> інтерфейси визначають методи і властивості, що є специфічними для роботи з парами ключ-значення.

Методи інтерфейса IDictionary<TKey, TValue> наведені в таблиці Є.9 (Додаток Є).

Властивості інтерфейса IDictionary<TKey, TValue> наведені в таблиці Є.10 (Додаток Є).

Створення перелічуваних колекцій

Для перелічення по колекції, ви зазвичай використовуєте цикл foreach. Цикл foreach проходиться по кожному елементу по черзі в порядку, що підходить до колекції. foreach маскує певні ускладнення перерахувань. Для роботи foreach клас колекцій узагальнення має реалізувати інтерфейс IEnumerable<T>. Цей інтерфейс містить метод GetEnumerator, що повертає тип IEnumerator<T>.

Інтерфейс IEnumerator<T>

Інтерфейс IEnumerator<T> визначає функціональність, що усі перерахування мають реалізуватись.

Методи інтерфейса IEnumerator<T> наведені в таблиці Є.11 (Додаток Є).

Властивості інтерфейса IEnumerator<T> наведені в таблиці Є.12 (Додаток Є).

Завжди при перерахуванні є покажчик на елемент колекції. Стартова точка - це вказівник перед першим елементом. Коли ви викликаєте метод MoveNext, покажчик переміщується на наступний елемент в колекції. Метод MoveNext повертає істину (true), якщо можна було перевести покажчик на одну позицію вперед, або хиба (false), якщо було досягнуто кінця колекції. В кожний момент часу під час перерахування властивість Current повертає елемент, на який покажчик вказує.

Коли ви створили перерахування, вам потрібно визначити:

  • Що є першим елементом перерахування в колекції.
  • В якому порядку покажчик повинен рухатись по перерахуванню.

Інтерфейс IEnumerable<T>

Інтерфейс IEnumerable<T> визначає єдиний метод з назвою GetEnumerator. Він повертає екземпляр IEnumerator<T>.

Метод GetEnumerator повертає покажчик для перерахування для вашого класу колекції. Цей покажчик буде використовуватись для foreach циклу, якщо ви не зазначите альтернативу. Однак, ви можете створити додаткові методи для визначення альтернативного покажчика перерахування.

Наступний приклад демонструє колекцію, що реалізує покажчик перерахування за замовчуванням. А також альтернативний покажчик, що здійснює перерахунок колекції у зворотньому порядку:

// Визначення альтернативного методу перерахунку

class CustomCollection<T> : IEnumerable<T>

{
   public IEnumerator<T> Backwards()

   {

      // Цей метод повертає альтернативний перерахунок

      // Реалізація не демонструється

   }
   #region IEnumerable<T> Members

   public IEnumerator<T> GetEnumerator()

   {

      // Цей метод поветрає перерахування за замовчуванням

      // Реалізація не демонструється

   }

   #endregion

   #region IEnumerable Members

   IEnumerator IEnumerable.GetEnumerator()

   {

      // Цей метод вимагається, оскільки IEnumerable<T> успадковується від IEnumerable

      throw new NotImplementedException();

   }

   #endregion

}

Наступний приклад демонструє як покажчик за замовчуванням або альтернативний покажчик здійснює ітерацію по колекції:

// Проходження по колекції

CustomCollection<Int32> numbers = new CustomCollection<Int32>();

// Додавання елементів до колекцій.

// Використовується перерахування за замовчуванням по колекції:

foreach (int number in numbers)

{

   // …

}

// Використання альтернативного перерахування для ітерації по колекції:

foreach(int number in numbers.Backwards())

{

   // …

}

Реалізація перерахування

Ви можете створити перерахування, створивши власний клас, що реалізує інтерфейс IEnumerator<T>. Однак, якщо ваш клас використовує за основу тип перерахування для зберігання даних, ви можете використовувати ітератор для реалізації інтерфейсу IEnumerable<T> без надання реалізації IEnumerator<T>. Найкращий спосіб зрозуміти ітератори - це написати простий приклад.

Наступний приклад показує як ви можете використовувати ітератори для реалізації перерахування:

// Використання перерахувань за допомогою ітераторів

using System;

using System.Collections;

using System.Collections.Generic;

class BasicCollection<T> : IEnumerable<T>

{

    private List<T> data = new List<T>();

    public void FillList(params T [] items)

    {

        foreach (var datum in items)

          data.Add(datum);

    }

    IEnumerator<T> IEnumerable<T>.GetEnumerator()

    {

        foreach (var datum in data)

        {

            yield return datum;

        }

    }

    IEnumerator IEnumerable.GetEnumerator()

    {

        throw new NotImplementedException();

    }

}

Наступний приклад демонструє екземпляр колекції узагальнення List<T> для зберігання даних. Екземпляр List<T> містить метод FillList. Коли метод GetEnumerator викликається, цикл foreach проходиться по колекції. У циклі foreach, yield повертає кожен елемент колекції. Цей yield повертає вираз, що визначає ітератор—по суті, yield призупиняє виконання до повернення поточного елемента і тільки тоді переходить до наступного елемента в послідовності. Таким чином, хоча метод GetEnumerator, не повертає тип IEnumerator, компілятор може побудувати перелічення, виходячи від логіки ітерації, що ви передбачили.

 

7.2 Завдання до лабораторної роботи (Додаток Є)

 

7.3 Контрольні запитання:

 

  1. Для чого використовуються інтерфейси?
  2. Яка відмінність між інтерфейсами та абстрактними класами?
  3. Скільки класів можуть мати реалізацію методів інтерфейсу?
  4. Скільки інтерфейсів може бути реалізовано в одному класі?
  5. Який загальний вигляд опису інтерфейсу?
  6. Які елементи мови програмування можна вказувати в інтерфейсах?
  7. Як виглядає загальна форма реалізації інтерфейсу в класі?
  8. Яка загальна форма класу, що реалізує декілька інтерфейсів?
  9. Яким чином в інтерфейсі описується властивість?
  10. Які елементи програмування мови C# не можна описувати в інтерфейсах?
  11. Що таке явна реалізація члену інтерфейсу?
  12. В яких випадках краще використовувати інтерфейс, а в яких абстрактний клас?
  13. Які є види колекцій?
  14. Якими типами даних оперують неузагальнені колекції?
  15. Якими типами даних оперують спеціальні колекції?
  16. В якому просторі імен оголошуються спеціальні колекції?
  17. Які стандартні структури даних реалізують узагальнені колекції?
  18. Які особливості використання паралельних (багатопотокових) колекцій?
  19. В якому просторі імен визначені паралельні (багатопотокові) колекції?