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

 

ЛАБОРАТОРНА РОБОТА № 8

ПОДІЇ ДЕЛЕГАТИ

 

 

Мета роботи: навчитися викликати події та ознайомитись з делегатами.

 

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

 

8.1.1 Події та делегати

Події (events) це механізм, що дозволяє об'єктам повідомляти іншим об'єктам, коли щось відбувається. Наприклад, на веб сторінці генерується подія, коли користувач взаємодіє з елементом управління, натискаючи на кнопку. Ви можете створити код, що підписується на ці події і відповідає певними діями на них.

Без подій, вашій програмі знадобиться постійно переглядати значення в елементів управління і шукати зміни стану, що потребують дій у відповідь.

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

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

 

8.1.2 Створення подій та делегатів

Коли ви створюєте подію в структурі чи класі, вам потрібен буде спосіб, що надасть можливість іншому коду підписатись на подію. У C# ви можете це зробити створивши делегат. Повторимо, делегат – це спеціальний тип, що визначає сигнатуру методу, іншими словами тип і параметри методу. З імені зрозуміло, що делегат себе поводить як представник методу з відповідною сигнатурою.

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

  • Cтворити метод з сигнатурою, що відповідає делегату події. Цей метод відомий як обробник події.
  • Підписатись на подію, додавши ім’я обробника методу до публікації події, іншими словами, об'єкту, що буде формувуати подію.
  • Коли подія створена, делегат запускає всі методи обробки події, що підписані на цю подію.

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

Першим чином, у такому випадку вам потрібно визначити делегат. Для визначення делегата, ви використовуєте ключове слово – delegate. Делагат включає два параметри:

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

Далі вам потрібно оголосити подію. Для задання події, вам потрібно використати ключове словоevent. Перед заданням ім’я вашої події ви задаєте ім’я делегата, який ви хочете асоціювати з вашою подією.

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

//Задання Delegate і Event

public struct Coffee

{

   public EventArgs e;

   public delegate void OutOfBeansHandler(Coffee coffee, EventArgs args);

   public event OutOfBeansHandler OutOfBeans;

}

У цьому прикладі ви визначаєте подію з назвою OutOfBeans. Ви асоціюєте її з ім’ям делегатаOutOfBeansHandler. Делегат OutOfBeansHandler приймає два параметри, екземпляр Coffee, який є об'єктом, що створює подію, та екземпляр EventArgs, що може бути використаний, щоб отримати більше інформацію про подію.

 

8.1.3 Виникнення та підписка подій

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

Для того, щоб виникла подія, вам потрібно зробити наступні кроки:

1. Перевірити чи подія дорівнює null. Подія буде дорівнювати null, якщо ніякий код на цю подію не підписаний.

2. Викличіть подію і надайте аргументи делегату.

Наприклад, структура Coffee включає метод з назвою MakeCoffee. Кожен раз, коли ви викликаєте метод MakeCoffee, метод зменшує рівень запасу екземпляру Coffee. Якщо рівень запасу опускається певної межі, тоді MakeCoffee метод спричинить виникнення події OutOfBeans.

Наступний приклад демонструє як спричиняти подію:

// Спричинення події

public struct Coffee

{

   // Задання події та делегата.

   public EventArgs e = null;

   public delegate void OutOfBeansHandler(Coffee coffee, EventArgs args);

   public event OutOfBeansHandler OutOfBeans;

   int currentStockLevel;

   int minimumStockLevel;

   public void MakeCoffee()

   {

      // Зменшення рівня запасу.

      currentStockLevel--;

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

      if (currentStockLevel < minimumStockLevel)

      {

         // Перевірка події на null.

         if (OutOfBeans != null)

         {

                     // Спричинення події.

            OutOfBeans(this, e); 

         }

      }

   }

}

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

Якщо ви хочете обробляти подію, вам потрібно зробити наступне:

  • Створити метод з сигнатурою, що відповідає делегату події.
  • Використати додатковий оператор (+=) для додання методу обробки події до вашої події.

Припустимо, що ви створили екземпляр структури Coffee з назвою coffee1. У вашому класі Inventoryви хочете підписатись на OutOfBeans, що може бути спричинена coffee1.

Зауваження: У попередніх розділах було показано як структура Coffee, подія OutOfBeans і делегат OutOfBeansHandler задаються.

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

// Підписуємось на подію

public class Inventory

{

  public void HandleOutOfBeans(Coffee sender, EventArgs args)

   {

      string coffeeBean = sender.Bean;

      // Зміна порядку кави в зернах.

   }

   public void SubscribeToEvent()

   {

      coffee1.OutOfBeans += HandleOutOfBeans;

   }

}

В цьому прикладі, сигнатура HandleOutOfBeans методу відповідає делегату для події OutOfBeans.Коли ви викликаєте метод SubscribeToEvent, метод HandleOutOfBeans додається до списку підписників події OutOfBeans об’єкту coffee1.

Для того, щоб відписатись від події, ви використовуєте оператор віднімання (-=) для видалення вашого методу обробки події від події.

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

//Відпистись від події

public void UnsubscribeFromEvent()

{

   coffee1.OutOfBeans -= HandleOutOfBeans;

}

Життєвий цикл об'єкта

Життєвий цикл об'єкта має декілька стадій, що починаються зі створення об'єкта, а закінчуються його зруйнуванням. Для створення об'єкта у вашому додатку, ви використовуєте ключове слово new. Коли  загальномовне виконуюче середовище (CLR) виконує код для створення нового об'єкта, проходять наступні кроки:

1.  Воно виділяє блок пам'яті, який достатньо великий для утримання об'єкта.

2.  Воно ініціалізовує блок пам'яті новим об'єктом.

CLR займається виділенням пам'яті для всіх керованих об'єктів. Проте при використанні некерованих об'єктів, вам може бути потрібно написати код для виділення пам'яті для некерованих об'єктів, що ви створили. Некеровані об'єкти - це ті об'єкти, що не є .NET компонентами такими як Microsoft Word об'єкт, підключення до бази даних, або файл ресурсів.

Коли ви завершили роботу з об'єктом, ви можете позбутись його для звільнення ресурсів таких як підключення до бази даних чи дескриптори файлів, що він споживає. При утилізації об'єкта, CLR використовується механізм, що надивається збирач сміття ( garbage collector - GC ) для виконання наступних кроків:

1.  GC вивільняє ресурси.

2.  Пам'ять, що була виділена на об'єкт буде утилізована.

Збирач сміття (GC) - це окремий процес, що запускається у власному потоці щоразу, коли керований код додатка працює.

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

Коли .NET програма виконана, GC ініціалізується CLR.  GC виділяє сегмент пам'яті, який буде використовуватися для зберігання і управління об'єктами для кожної .NET програми, що запущена.  Ця область пам'яті називається керована купа (managed heap) , яка відрізняється від нативної купи, використовуваної в контексті операційної системи.

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

Зауваження: Об'єм сегмента, що алокується GC залежить від імплементації і може бути змінений у будь-який час під час оновлення. Коли ви пишете вашу програму, ви ніколи не повинні робити припущення про сегмент виділеної пам'яті, що буде використовуватись GC.

GC виникає, якщо виконуються наступні умови:

  • Не вистачає фізичної пам'яті.
  • Пам'ять, яка на разі виділена під об'єкти, перевищує допустимий поріг. Поріг може змінюватись в процесі виконання програми.
  • Метод GC.Collect викликаний. Зазвичай, вам не потрібно викликати цей метод, оскільки GC працює постійно. Навіть якщо ви викличете цей метод, це не гарантує того, що цей метод почне свою роботу тоді, коли ви його викличите.

 

8.1.4 Реалізація шаблону видалення

Шаблон видалення – це шаблон, що призначений для вивільнення ресурсів, що використовували об'єкти. .NET Framework надає IDisposable інтерфейс в просторі імен System, що дозволяє реалізувати шаблон видалення у вашому додатку.

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

Виклик методу Dispose не руйнує об'єкт. Об'єкт залишається в пам'яті до того часу, поки останнє посилання на об'єкт не буде видалено і GC не вивільнить ресурси.

Багато класів в .NET Framework, що охоплюють некеровані ресурси( такі як StreamWriter) клас реалізовують IDisposable інтерфейс. Клас StreamWriter реалізує об'єкт TextWriter для запису текстової інформації в потік. Ви також повинні реалізувати інтерфейс IDisposable, коли ви створюєте свої власні класи, що посилаються на не керований тип.

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

Для реалізації інтерфейсу IDisposable у вашому додатку, потрібно виконати наступні кроки:

  1. Переконатись, що простір імен System в переліку, додавши наступний рядок на початку вашого файлу з кодом.
  2. using System;

  3. Здійснити реалізацію IDisposable інтерфейу при визначенні класу.
  4. ...

    public class ManagedWord : IDisposable

    {

       public void Dispose()

       {

          throw new NotImplementedException();

       }

    }

  5. Додати private поле до класу(наприклад bool _isDisposed), яке ви зможете використовувати для отримання інформації про статус видалення об'єкту і перевірки того, чи метод Dispose уже був викликаний і ресурси вивільнились.
  6. public class ManagedWord : IDisposable

    {

       bool _isDisposed;

       ...

    }

  7. Додати до коду перевірки до ваших загальнодоступних методів перевірку чи об'єкт метод Dispose ще не був викликаний.
  8. public void OpenWordDocument(string filePath)

    {

       if (this._isDisposed)

          throw new ObjectDisposedException("ManagedWord");

           ...

    }

  9. Додати перевантаження метода Dispose, що приймає Boolean парамент. Перевантажений метод Dispose має розпоряджатись як керованими так і не керованими ресурсами, якщо він був викликаний безпосередньо, в цьому випадку ви передаєте логічний параметр із значенням істини. Якщо ви передаєте істинний параметр зі значенням хиби метод, Dispose повинен тільки намагатися звільнити некеровані ресурси. Ви можете зробити це, якщо об'єкт вже утилізований або буде утилізоваий за допомогою GC.
  10. public class ManagedWord : IDisposable

    {

       ...

        protected virtual void Dispose(bool isDisposing)

        {

            if (this._isDisposed)

                return;

            if (isDisposing)

            {

               // Вивільнення лише керованих ресурсів.

               ...

            }

            // Завжди вивільняє некеровані ресурси.

            ...

            // Вказує, що об'єкт був видалений.

            this._isDisposed = true;

        }

    }

  11. Додайте код до метода без параметрів Dispose, щоб викликати перевантажений метод Dispose, а після цього викликати метод GC.SuppressFinalize. Метод GC.SuppressFinalize вказує GC, що не потрібно викликати деструктор.

public void Dispose()

{

   Dispose(true);

   GC.SuppressFinalize(this);

}

Після того, як ви реалізували інтерфейс IDisposable при визначенні вашого класу, ви можете викликати метод Dispose у вашого об'єкта для вивільнення будь-яких ресурсів, що об'єкт може споживати. Ви можете викликати метод Dispose з деструктора, що ви визначили в класі.

Реалізація деструктора

Деструктор – це метод, який буде викликаний, коли GC буде "збирати" цей об'єкт. Для визначення деструктора, вам потрібно додати тільду (~) перед іменем класу. Потім потрібно додати логіку деструктора в дужках.

Наступний приклад демонструє синтаксис додання деструктора.

// Визначення деструктора

class ManagedWord

{

    ...

    // Деструктор

    ~ManagedWord

    {

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

    }

}

Коли ви визначите деструктор, компілятор автоматично перевизначає метод Finalize класа об'єкта. Тим не менш, ви не можете явно перевизначити метод Finalize; ви повинні оголосити деструктор і дати можливість компілятору виконати перетворення.

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

Наступний приклад демонструє, як викликати метод Dispose з деструктора.

// Виклик метода Dispose з деструктора

class ManagedWord

{

    ...

    // Деструктор

    ~ManagedWord

    {

        Dispose(false);

    }

}

Управління життєвим циклом об'єкта

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

Наступний приклад коду показує як викликати метод Dispose для об'єкту, що реалізує інтерфейс IDisposable.

// Виклик метода Dispose

var word = new ManagedWord();

 // Код, що використовує об'єкт ManagedWord.

word.Dispose();

Викликати метод Dispose явно після того, як код використав об'єкт можна, але якщо код видасть помилку перед викликом метода Dispose, метод Dispose ніколи не буде викликаний. Більш надійним підходом є виклик метода Dispose у блоці finally у try/catch/finally або в try/finally. Будь-який код, що знаходиться в блоці finally завжди виконується, не зважаючи на те, які помилки можуть виникнути. Саме тому цей підхід може завжди гарантувати те, що ваш код викличе метод Dispose.

Наступний приклад коду демонструє як ви можете викликати метод Dispose у блоці finally.

// Виклик методу Dispose у блоці finally

var word = default(ManagedWord);

try

{

   word = new ManagedWord();

   // Код працює з об'єктом ManagedWord.

}

catch

 

{

    // Код, що здійснює обробку помилок.

}

finally

 

{

   if(word!=null)

      word.Dispose();

}

Зауваження: Коли ви явно викликаєте метод Dispose, це хороша практика перевірити чи об'єкт не є null перед цим, тому що ви не можете гарантувати стан об'єкта.

Альтернативно(і майже завджи краще), ви можете використовувати  using для неявного виклику методу Dispose.

Наступний приклад коду демонструє використання using

using (var word = default(ManagedWord))

{

   // Код використовує об'єкт ManagedWord.

}

Якщо ваш об'єкт не реалізовує з якихось причин інтерфейс IDisposable,  try/finally блок – це єдиний безпечний спосіб виконати код для вивільнення ресурсів.

 

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

 

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

 

  1. Що таке події?
  2. Виклик події.
  3. Що таке делегати?
  4. Делегати з іменованими методами
  5. Делегати з анонімними методами