Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Шаблони проектування.docx
Скачиваний:
3
Добавлен:
25.11.2019
Размер:
73.72 Кб
Скачать
  • Ітератор

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

Універсальні мови програмування (Паскаль, Ада, Java, C/C++) мають вбудовану підтримку одного найпростішого різновиду контейнерів — масивів.

Контейнери зовсім не вичерпуються масивами:

масив від моменту створення має фіксований розмір,

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

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

Однин з найтиповіших способів обробки контейнеру — це перебір всіх його елементів по одному.

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

Разом з тим, з концептуальної точки зору ця складність зовсім непотрібна, зайва — клієнта цікавить просто можливість перебрати один за одним всі елементи якогось контейнера і зовсім не цікавлять особливості його реалізації (фізичний спосіб зберігання елементів у пам’яті).

Саме для приховування цих деталей від клієнтського коду призначений шаблон «ітератор».

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

  • Розглянемо поелементну обробку масиву (на прикладі мови C++)

void ProcessArray(double *paf, int n) {

for( int i = 0; i < n; ++i ) {

обробити paf[i];

}

}

Тут аргумент paf є покажчиком на початок масиву значень, аргумент n містить кількість елементів, а змінна i є індексом поточного елементу масиву: ця змінна послідовно пробігає всі значення від 0 до n-1.

  • Попередній фрагмент, з використанням арифметики адрес

void ProcessArray(double *paf, int n) {

for( double *pf = paf; pf-paf < n; ++ pf ) {

обробити *pf;

}

}

Тут для доступу використано не номер (індекс) поточного елементу, а покажчик pf на нього.

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

Покажчик pf в наведеному вище прикладі і є найпростішим ітератором, бо він слугує каналом доступу до одного (поточного) елементу контейнеру та підтримує такі операції:

  • встановити на перший елемент контейнера;

  • перевірити, чи не переглянуто контейнер до кінця;

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

Ітератор — це допоміжний об’єкт, спеціально призначений для роботи з певним різновидом контейнерів, та який має наступні властивості:

  • встановити на перший елемент контейнера;

  • перевірити, чи не переглянуто контейнер до кінця;

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

В якості прикладу можна взяти класи контейнерів зі стандартної бібліотеки шаблонів STL (Standard Template Library), яка є невід’ємною складовою стандарту мови C++.

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

однаковим терміном «шаблон» перекладаються два різні англійські терміни:

pattern — заготовка задуму розв’язку типової задачі та template — спеціальний засіб мови програмування для підтримки узагальнених алгоритмів і структур даних, звільнених від жорсткої прив’язки до конкретних типів даних.

Таким чином, у реченні «в стандартній бібліотеці шаблонів використано шаблон Ітератор» два входження терміну «шаблон» мають різний сенс: перший — template, другий — pattern.

int print(std::list<double> const& l) {

 std::list<double>::const_iterator i;

 for( i = l.begin(); i != l.end(); ++i ) {

   std::cout << *i << std::endl;

 }

}

Функція print призначена для того, щоб роздрукувати список значень, по одному елементу в рядку.

Її аргумент l — константне посилання на список дійсних чисел: шаблон list з простору імен std реалізує згальний (тобто з довільним типом елементу) двозв’язний список, параметр у кутових дужках фіксує тип елементу, перетворюючи загальний шаблон списку на конкретний клас «список дійсних чисел»; нарешті, оскільки роздруківка елементів списку ніяк не змінює список, варто в явному вигляді оголосити посилання константним.

Змінна i, призначена для того, щоб пробігати по елементах списку, оголошена як константний ітератор списку дійсних чисел.

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

На початку циклу цій змінній присвоюється початкове значення, що повертає для списку l метод begin() — такий метод є в кожному контейнерному класі з бібліотеки STL, він повертає ітератор, що вказує на перший елемент контейнера.

Перед кожною ітерацією перевіряється, чи не зрівнявся ітератор i з ітератором, який повертає для даного списку метод end(), — цей метод існує в кожному контейнерному класі бібліотеки STL та повертає ітератор, що вказує на неіснуючий елемент після останнього елементу контейнера.

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

Якщо i — ітератор, що вказує на деякий елемент контейнера, то вираз *i дає значення цього елементу (більш точно — посилання на значення, а в даному випадку ще й константне посилання).

Наприкінці кожної ітерації до ітератору i застосовується операція інкременту ++, яка «переставляє» його на один елемент вперед.

Якщо порівняти функцію print з наведеним вище другим варіантом функції ProcessArray, можна помітити, що ітератор дозволяє працювати зі складним контейнером — двозв’язним списком — так само просто, як і зі звичайним масивом.

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

Оскільки ітератор суттєво пов’язаний з внутрішніми структурами даних контейнера, нам необхідно буде створити і свій власний контейнерний клас.

Щоб не ускладнювати задачу, візьмемо для прикладу однозв’язні списки (хоча ця задача і так вичерпно вирішена в бібліотеці STL та деяких інших подібних бібліотеках, важко знайти більш вдалий приклад для напрацювання самостійного досвіду).

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

  1. #include <iostream>

  2. #include <assert.h>

  3. template<typename T>

  4. class CList {

  5. private:

  6.  struct CListNode {

  7.    CListNode(T const& value, CListNode *pNext);

  8.    T m_value;

  9.    CListNode *m_pNext;

  10.  };

  11.  CListElenent *m_pHead;

  12. public:

  13.  CList();

  14.  ~CList();

  15.  CList<T>& prepend(T const& value);

  16.  class iterator {

  17.  friend class CList;

  18.  public:

  19.    iterator();

  20.    bool isFinished() const;

  21.    T& value();

  22.    void next();

  1.  private:

  2.    iterator(CListNode *p);

  3.    CListNode *m_p;

  4.  };

  5.  iterator begin();

  6. };

  7. template<typename T>

  8. CList<T>::CListNode::CListNode(

  9.  T const& value,

  10.  CList<T>::CListNode *pNext

  11. ):

  12.  m_value(value),

  13.  m_pNext(pNext)

  14. {}

  15. template<typename T>

  16. CList<T>::CList(): m_pHead(NULL)

  17. {}

  1. template<typename T>

  2. CList<T>::~CList() {

  3.  CListNode *p = m_pHead;

  4.  while( p != NULL ) {

  5.    CListNode *q = p->m_pNext;

  6.    delete p;

  7.    p = q;

  8.  }

  9. }

  10. template<typename T>

  11. CList<T>& CList<T>::prepend(T const& value) {

  12.  m_pHead = new CListNode(value, m_pHead);

  13.  return *this;

  14. }

  15. template<typename T>

  16. CList<T>::iterator::iterator():

  17.  m_p(NULL)

  18. {}

  1. template<typename T>

  2. CList<T>::iterator::iterator(CListNode *p):

  3.  m_p(p)

  4. {}

  5. template<typename T>

  6. typename Clist<T>::iterator CList<T>::begin() {

  7.  return CList<T>::iterator(m_pHead);

  8. }

  9. template<typename T>

  10. bool CList<T>::iterator::isFinished() const {

  11.  return (m_p == NULL);

  12. }

  13. template<typename T>

  14. T& CList<T>::iterator::value() {

  15.  return m_p->m_value;

  16. }

  1. template<typename T>

  2. void CList<T>::iterator::next() {

  3.  assert( m_p != NULL );

  4.  m_p = m_p->m_pNext;

  5. }

  6. int main() {

  7.  CList<double> l;

  8.  l.prepend(3.14159).prepend(2.7182818).prepend(1.61803);

  9.  CList<double>::iterator i;

  10.  for( i = l.begin(); !i.isFinished(); i.next() ) {

  11.    std::cout << i.value() << std::endl;

  12.  }

  13. }

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

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

Користувачу нашого класу CList непотрібні технічні деталі оперування ланками та покажчиками-зв’язками між ними, користувачу потрібне лише лінійно впорядковане сховище для елементів.

Для того, щоб захистити користувача від зайвої складності (а також і для того, щоб захистити цілісність структур даних від необережних дій користувача), структурний тип CListNode, що моделює ланку списку, оголошено приватною (private) в класі CList (рядки 6-11).

Жодна стороння функція «не бачить» цей структурний тип, з ним можуть безпосередньо працювати лише методи шаблону CList. Кожен об’єкт структурного типу CListNode містить значення елементу (член m_value) та покажчик на наступну ланку (член m_pNext).

Структуру списку утворюють саме об’єкти типу CListNode, а об’єкт класу CList слугує «оболонкою» для усього списку.

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

Об’єкт-список потребує лише покажчика на першу ланку (рядок 12).

Конструктор списку створює порожній список — його член «покажчик на першу ланку» отримує початкове значення «порожній покажчик» (рядок 41).

Задача деструктора (рядки 44-52) — звільнити динамічну пам’ять, яку займають всі ланки списку.

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

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

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

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

Ця операція (рядок 56) створює в динамічній пам’яті новий об’єкт типу CListNode (ланка), в якому в якості значення елемента зберігається значення value, передане в метод через аргумент, а покажчиком на наступну ланку для цього об’єкту стає покажчик на ту ланку, яка досі вважалася початком списку.

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

Варто звернути увагу, що метод prepend повертає посилання на об’єкт-список, в який він щойно вставив елемент.

Це дозволяє зіставляти ланцюги з кількох викликів даного методу, одним рядком коду вставляючи на початок списку кілька елементів - див. рядок 93.

Нарешті переходимо до центрального предмету даного прикладу.

Клас ітератору оголошено всередині шаблону класу списку.

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

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

iterator i;

а у вигляді

CList<якийсь_тип_елементу>::iterator i;

Іншими словами, існує не ип «ітератор», а «ітератор списку таких-то значень».

Конструктор по замовчуванню (рядок 62) створює ітератор, що не вказує на жодну ланку списку.

Конструктор з аргументом (рядок 67) створює ітератор, що вказує на задану ланку.

Звернімо увагу, що конструктор без аргументу оголошено загальнодоступним (public), а конструктор з аргументом — приватним.

Справді, оскільки саме поняття ланки списку приховане від користувацького коду, інкапсульоване всередині класу списку, то користувацький код і не може використовувати другий конструктор.

Щоб зробити його доступним для методів класу CList, клас списку оголошено другом класу ітератора, рядок 18.

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

Метод value повертає посилання на значення поточного елементу списку (тобто того, що зберігається в ланці, на яку вказує член m_p ітератору) — рядок 82.

Метод next переставляє ітератор на одну ланку вперед (рядок 88).

Для цього треба взяти ту ланку, на яку ітератор вказує покажчиком m_p зараз, взяти з тієї ланки член-покажчик m_pNext та присвоїти його в член m_p ітератора.

Ця операція має сенс лише тоді, коли ітератор вказує хоча б на якусь ланку — коли початковим значенням члена m_p не є порожній покажчик, див. рядок 87.

Метод begin списку повертає ітератор, що вказує на першу ланку списку.

Для цього потрібно створити такий ітератор, у якого член m_p вказує туди ж, куди і член m_pHead списку, рядок 72.

Використання ітератору показано в рядках 95 та 96 — видно, що ітератор дозволяє реалізувати ідіому «пробігти по всім елементам контейнера по одному».

Використання ітераторів для доступу до елементів складних структур даних має низку переваг.

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

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

Натомість при грамотному підході достатньо лише замінити ім’я типу даних, оскільки в різних контейнерах ітератори мають однаковий інтерфейс.

  • Порівняйте текст до і після заміни типів

int print(std::list<double> const& l) {

 std::list<double>::const_iterator i;

 for( i = l.begin(); i != l.end(); ++i ) {

   std::cout << *i << std::endl;

 }

}

int print(std::vector<double> const& l) {

 std::vector<double>::const_iterator i;

 for( i = l.begin(); i != l.end(); ++i ) {

   std::cout << *i << std::endl;

 }

}

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

Наприклад, за самою своєю сутністю однозв’язний список допускає лише однонаправлені ітератори: для кожної ланки можна знайти наступну, а попередню — ні; для двозв’язного списку природними є двонаправлені ітератори тощо.