Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

ТЕМА КОНТЕЙНЕРЫ / Кубенский Итераторы

.pdf
Скачиваний:
18
Добавлен:
09.02.2015
Размер:
497.09 Кб
Скачать

Тема Итераторы

В этом разделе мы рассмотрим вопрос о том, как лучше всего организовать просмотр элементов сложной структуры данных. Пусть, например, некоторая программа использует определение списка целых чисел, приведенное нами в разд. 1.2. Программа может построить список, используя операции, определенные В классе IntList, такие как IntList::addFirst И IntList::addLast, ИЛИ удалять из него элементы с помощью операции intList::remove, однако, для того, чтобы просматривать содержимое элементов списка, этих операций недостаточно. Попробуем восполнить этот недостаток.

Если в программе требуется проверять, имеется ли в списке некоторое конкретное число, то удобнее всего было бы иметь среди операций класса intList метод boolean hasEiement (int). Конечно, такую операцию определить несложно, тем более что, по-видимому, она была бы полезна во многих случаях. Однако таких ситуаций может оказаться довольно много, и не всегда получится, что соответствующая операция будет достаточно общей для того, чтобы стоило включать ее в интерфейс класса. Например, если в программе требуется подсчитывать количество отрицательных элементов списка, то, конечно, тоже можно было бы определить операцию int IntList::negatives (), но на самом деле такая операция слишком специфична для того, чтобы включать ее непосредственно в описание класса.

Имеется и еще одно соображение. Основу обеих приведенных в качестве примеров операций составляет одно и то же действие — просмотр элементов списка. Повторять это действие в реализации каждой из операций не очень разумно. Если бы удалось выделить просмотр элементов в отдельную операцию, то это не только позволило бы легко определять все действия, подобные приведенным выше, но и обеспечило бы независимость реализации просмотра элементов от конкретных действий с элементами списка.

Одно из возможных решений этой задачи — это описать абстрактное действие над элементами списка в виде класса (конечно, это опять будет абстрактный класс) и параметризовать просмотр элементов списка объектом такого класса.

Остановимся на этом решении чуть более подробно. Абстрактный класс, представляющий операцию, выполняемую над элементами списка, будет содержать единственную интерфейсную функцию — функцию обработки целого числа— элемента списка. Определение такого класса может выглядеть следующим образом:

class Actor { public:

virtual void action(int & element) = 0;

};

Теперь просмотр элементов списка может быть организован независимо от содержания конкретной операции над элементами списка, в виде метода void intList:: traverse (Actor & а).

В листинге 2.24 представлена реализация операции traverse в контексте описания класса intList

class IntList {

/* класс Listitem представляет элемент списка, связанный со

 

следующим с помощью поля next

 

*/

 

 

 

struct Listitem {

 

 

int item;

// значение элемента списка

 

Listitem *next;

// указатель на следующий элемент списка

 

// Конструктор для создания нового элемента

 

Listitem(int i, Listitem *n = NULL)

{ item = i; next = n; }

 

};

 

 

int count;

// счетчик числа элементов

Listitem *first;

// первый элемент списка

Listitem *last;

// последний элемент списка

public :

 

 

// Операция прохождения списка

 

void traverse(Actor & а);

 

};

// Конец определения класса IntList

void IntList::traverse(Actor & a) {

for (Listitem *current = first; current; current = current->next) { a.action(current->item) ;

}

Теперь для того, чтобы выполнить конкретную обработку элементов списка, надо отдельно описать действие по обработке одного элемента, затем создать объект соответствующего класса, применить к нему операцию traverse и получить результат обработки. Например, если требуется найти сумму всех элементов списка, то прежде всего опишем действие суммирования в виде класса — наследника абстрактного класса Actor:

class Summator : public Actor { int sum;

public: Summator():sum(0) {}

int getSum() { return sum; }

void action(int & element) { sum += element; } };

Основное действие, которое выполняют объекты этого класса над элементами списка, — это добавление значения элемента к хранящейся в объекте сумме. Теперь, если имеется список целых, представленный переменной list класса intList, то просуммировать его элементы можно с помощью специально созданного для этого объекта класса Summator:

Summator summator; list.traverse(summator);

Наконец, накопленную сумму можно извлечь из объекта summator с помощью вызова

summator. getSum ().

Похожим образом можно реализовать и другие упомянутые выше действия. Например, найти нужный элемент в списке можно определив класс, для которого действие над элементом списка заключается в сравнении его с некоторым заранее заданным значением:

class Comparator : public Actor {

int value;

// значение для сравнения

bool found;

// признак того, найдено ли значение value в списке

public :

 

Comparator(int val):value(val), found(false) {} bool hasFound() { return found; }

void action(int & element) { if (value == element) found = true; } };

Тогда функция, проверяющая, имеется ли в списке элемент с заданным значением, могла бы выглядеть следующим образом:

bool hasElement(IntList & l, int value) { Comparator comp(value);

l. traverse (comp) ; return comp.has Found();

}

Разумеется, в самом определении класса intList специфические функции, использующие просмотр элементов списка, могут использовать функцию traverse не только для того, чтобы уменьшить длину кода, но и для того, чтобы сохранить общий принцип хорошего программирования: одно и то же действие не должно описываться несколько раз. Так, функции поиска элемента в списке или суммирование элементов списка могут и должны определяться непосредственно в теле самого класса intList.

Однако и такие экзотические задачи, как подсчет числа отрицательных элементов списка, будут решаться точно так же, надо только определить подходящее действие над элементом списка в виде описания класса-наследника класса Actor:

class NegativeCounter : public Actor { int counter;

public : NegativeCounter():counter(0) {}

int getCounter() { return counter; }

void action(int & element) { counter += (element < 0); } };

int negatives(IntList & l) { NegativeCounter counter; l.traverse(counter);

return counter.getCounter();

}

Правда, функция подсчета количества отрицательных элементов списка вряд ли будет включена в список методов класса intList.

Недостатки

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

Во-первых, сразу бросается в глаза неудобство в определении новых операций. Для того чтобы описать новую операцию по просмотру элементов списка, надо определять новый класс, а для этого придется описать, по крайней мере, конструктор класса, члены класса и т. п. Очень длинно и неудобно.

Во-вторых, не любую операцию, требующую просмотра элементов списка, можно описать, определив лишь операцию обработки одного конкретного элемента. Это можно сделать лишь в том случае, когда обработка отдельных элементов не зависит (или почти не зависит) от значений других элементов. Некоторые трудности возникнут даже при определении такой простой операции, как проверка того, возрастают ли значения элементов списка от начала списка к его концу, т. е. упорядочен ли список. Трудность состоит в том, что значение каждого элемента списка надо сравнивать со значением предыдущего элемента, а значит, при обработке потребуется запоминать его значение для сравнения со значением следующего элемента.

Наконец, с помощью приведенного метода невозможно обрабатывать несколько списков одновременно, просматривая их элементы поочередно. Например, невозможно написать функцию, которая сравнивала бы поэлементно два списка. Действительно, метод traverse просматривает все элементы одного списка, и пока просмотр полностью не завершится, нет никакой возможности вмешаться в работу этого метода: приостановить его работу, завершить до того, как будут просмотрены все элементы и т. п.

Внутренние итераторы

Просмотр элементов списка или другой структуры, состоящей из однотипных элементов, часто называют итерацией этой структуры, а механизм такого просмотра называют, соответственно, итератором. В описанном выше способе просмотра элементов списка итератором является метод traverse класса intList. Именно он обеспечивает переход от элемента к элементу при итерации списка. Итератор, подобный только что описанному методу traverse, часто называют внутренним итератором, поскольку этот метод описан внутри класса intList, т. е. сам объект класса intList отвечает за просмотр своих элементов.

Конечно, один и тот же класс может предлагать несколько различных внутренних итераторов. Например, для просмотра элементов списка в обратном порядке может быть предложен другой итератор, скажем, traverseBack. Для представленной реализации класса intList в виде однонаправленного списка такой итератор не будет слишком естественным, однако все же представим такой итератор в виде пары функций. Описание этих функций дано в листинге 2.25. В этом решении просмотр в обратном порядке обеспечивается рекурсивной функцией traverseBackRec, которая для каждого элемента списка сначала с помощью рекурсивного вызова производит просмотр остатка списка, расположенного за этим элементом, а уже после этого обрабатывает сам этот элемент.

Представленная рекурсивная функция выглядит очень просто, однако при прохождении по списку ей потребуется количество памяти, пропорциональное размеру самого списка!

Листинг 2,25. Итератор для просмотра элементов списка в обратном порядке class IntList {

public:

/ / Функция, представляющая внутренний итератор списка

// для просмотра элементов списка в обратном порядке,

void traverseBack(Actor & а) { traverseBackRec(a, first); }

private:

//Вспомогательная рекурсивная функция, обеспечивающая

//обратную итерацию элементов списка, начиная с элемента list. void traverseBackRec(Actor & a, Listitem * list);

};

void IntList::traverseBackRec(Actor & a, Listitem * list) {

if (list) {

 

 

 

 

 

 

traverseBackRec(a,

list->next);

//

Итерация

последующих

элементов

a.action(list->item) ;

//-Обработка элемента list

 

}

}

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

Внешние итераторы

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

for (/* начать итерацию */;

/* пока есть еще элементы */; /* перейти к следующему элементу */) {

int item = /* взять очередной элемент */; /* обработать этот элемент */

}

Если считать, что все действия, представленные в этом фрагменте комментариями, реализованы в виде методов класса intList, то, например, цикл, суммирующий элементы списка l, будет выглядеть так:

int sum = 0;

for (l.startIterator(); l.hasMoreElements(); l.nextElement()) { int item = l.getCurrent();

sum += item;

Конечно, реализовать такие функции для класса intList нетрудно. Для этого надо лишь запоминать в объектах этого класса текущую позицию при итерации списка. Вот как, например, может выглядеть такая реализация:

class IntList {

private :

Listitem * curPos; // Текущая позиция итерации public :

void startIterator() {

curPos = first; }

bool hasMoreElements()

const { return curPos != NULL; }

void nextElement()

{

if (curPos) curPos = curPos->next; }

int

getCurrentO const { return curPos->item; }

int

& getCurrent()

{ return curPos->itern; }

};

На первый взгляд кажется, что приведенная реализация, несмотря на свою простоту, очень удобна для использования и позволяет реализовать не только такие функции, как суммирование или поиск элемента в списке, но и те задачи, которые не могли быть решены с помощью внутреннего итератора. Действительно, вот как теперь будет выглядеть функция поиска элемента с заданным значением:

bool hasElement(IntList & l, int value) {

for (l.startIterator(); l.hasMoreElements(); l.nextElement()) { if (value == l.getCurrent()) return true;

}

return false;

}

 

Так же легко можно реализовать и функцию поэлементного сравнения списков:

 

bool equalLists(IntList & l1, IntList & l2) { for

 

(l1.startIterator(), l2.startIterator();

 

l1.hasMoreElements() && l2.hasMoreElements();

 

l1.nextElement(), l2.nextElement()) { if (l1.getCurrent()

!=

l2.getCurrent()) return false;

 

}

 

return !(l1.hasMoreElements() || l2.hasMoreElements());

 

}

 

Больше не требуется описывать специальных классов для представления действия по обработке одного элемента списка. Вообще, работа со списком так, как это показано в вышеприведенных примерах, выглядит простой и привычной.

Недостатки

У приведенного решения имеются очень серьезные недостатки, так что использование описанного метода для итерации списков практически никогда не используется.

Один из этих недостатков — это требование точной согласованности вызовов всех методов, представляющих итератор списка. Действительно, для того чтобы функции имели хоть какой-то смысл, необходимо, чтобы прежде всего был бы вызван метод startIterator. Без такого вызова значение переменной curPos останется неопределенным, а значит, работа остальных функций также будет не определена. Далее, обязательно нужно проверять наличие следующих элементов в списке с помощью метода hasMoreElements, в противном случае обращение к текущему элементу с помощью метода getCurrent вызовет возбуждение исключительной ситуации.

Второй и главный недостаток состоит в том, что в каждый момент времени над списком должен быть запущен лишь один итератор, поскольку информация о текущем положении указателя итерации хранится непосредственно в самом объекте. Пусть, например, требуется определить, каково максимальное число повторений одного и того же значения имеется среди элементов списка. Простейшее решение состоит в том, чтобы брать по очереди каждый из элементов списка и проверять, сколько таких элементов есть в этом списке. Попробуем написать функцию, которая подсчитывает число элементов списка, равных заданному значению.

int countElements(IntList & Is, int value) {

int count = 0;

for (Is.startlterator(); Is.hasMoreElements(); Is.nextElement()) { if (value = Is.getCurrent()) count++;

}

return count;

}

Теперь, казалось бы, очень просто написать и требуемую функцию:

int maxEqualElements(IntList & Is) {

int max = 0;

for (Is.startlterator(); Is.hasMoreElements(); Is.nextElement()) { int current = countElements(Is, Is.getCurrent());

if (current > max) max = current;

}

return current;

Однако приведенная функция работать не будет! Дело в том, что в процессе ее работы во время итерации списка внутри цикла производится повторная итерация того же самого списка, которая, разумеется, нарушит работу внешнего цикла итерации.

Решение этой проблемы состоит в том, чтобы вынести указатель текущего положения итерации из списка в отдельный объект, который и будет полностью управлять итерацией этого списка. Тогда при необходимости начать повторную итерацию списка, не закончив предыдущую, нужно будет лишь создать новый объект для управления итерацией, называемый внешним итератором.

Естественно полагать, что инициализация итератора происходит в конструкторе, и тогда, если считать, что классом объекта итератора является IntListiterator, решение предыдущей задачи о подсчете максимального количества повторений элементов списка будет выглядеть следующим образом:

int countElements(IntList & ls, int value) {

int count = 0;

for (IntListiterator i(ls); i.hasMoreElements(); i.nextElement()) { if (value == i.getCurrent()) count++;

}

return count;

}

int maxEqualElements(IntList & ls) {

int max = 0;

for (IntListiterator i(ls); i.hasMoreElements() ; i.nextElement()) { int current = countElements(Is, i.getCurrent());

if (current > max) max = current;

}

return max;

}

Конечно же, определение класса IntListiterator теснейшим образом связано с классом IntList. Возможно, самым правильным решением будет поместить его определение внутрь определения класса IntList. Реализация методов этого класса останется практически такой же, как и в нашем предыдущем "наивном" определении внешнего итератора. Несмотря на то, что наше новое решение выглядит чуть более громоздким, оно имеет дополнительное преимущество: обойти инициализацию итератора теперь невозможно даже по ошибке, поскольку объект, представляющий итератор, не может быть создан помимо конструктора. В следующем примере (листинг 2.26) внешний итератор описан в виде класса, определение которого погружено в класс IntList. Обратите внимание также на описание дополнительного метода iterator класса IntList. Этот метод содержит вызов конструктора нашего внешнего итератора, так что создавать новые итераторы заданного списка list можно будет двумя способами: либо с помощью непосредственного обращения к конструктору класса iterator:

IntList::Iterator * mylterator = new IntList::Iterator(list);

Либо с помощью метода iterator:

IntList::Iterator * mylterator = list.iterator();

Пожалуй, второй способ выглядит более элегантно.

Листинг 2.26. Внешний итератор для класса IntList

class IntList {

public :

// Класс, представляющий внешний итератор списка class Iterator {

Listitem * curPos; // Текущая позиция в списке

public :

// Конструктор создает новый итератор для списка list. Iterator(IntList & list) { curPos = list.first; }

//Метод hasMoreElements проверяет, есть ли еще элементы для итерации bool hasMoreElements() const { return curPos != NULL; }

//Метод nextElement сдвигает текущую позицию на следующий элемент,

void nextElement()

{ if (curPos) curPos = curPos->next; }

//Функция getCurrent обеспечивает доступ к текущему элементу, int getCurrent() const { return curPos->item; }

//Этот вариант метода позволяет изменить значение текущего элемента,

int & getCurrent()

{ return curPos->item; }

};

 

friend class Iterator;

 

// Этот метод создает новый внешний итератор для итерации списка this. Iterator * iterator() { return new Iterator(*this); }

};

Простые функции останутся такими же простыми и для этого решения. Приведем в качестве примера функцию, вычисляющую среднее арифметическое значений элементов списка.

double average(IntList & list) { IntList::Iterator *i = list.iterator();

double sum = 0, count = 0;

for (; i->hasMoreElements(); i->nextElement()) { sum += i->getCurrent(); count++;

}

delete i;

Обратите внимание, как созданный итератор уничтожается оператором delete в конце работы. Это необходимо сделать, поскольку создавался он с помощью оператора new при вызове метода list. iterator ().

Конечно, как и в случае внутренних итераторов, в одном и том же классе может быть определено несколько внешних итераторов. Например, для просмотра списка элементов в обратном порядке может быть определен другой внешний итератор. Правда, такой внешний итератор для однонаправленного списка определить гораздо сложнее, чем внутренний итератор, поскольку решение с помощью рекурсивной функции в данном случае не подходит. Пожалуй, самым простым решением здесь было бы создание нового обращенного списка из указателей на элементы исходного списка.

До сих пор мы использовали итераторы только для того, чтобы исследовать элементы списка, но не для того, чтобы изменять структуру списка. Действительно, основное назначение итераторов состоит именно в том, чтобы просматривать элементы, а не для того, чтобы их менять. Тем не менее иногда все же удобно рассматривать внешний итератор не только как средство просмотра, но и как обобщение понятия указателя на элемент списка, с помощью которого можно добавлять новые элементы или удалять имеющиеся. Действительно, класс iterator в определении класса intList инкапсулирует указатель на некоторый элемент списка, поэтому имеет смысл определить для него не только операции перемещения указателя и доступа к элементам списка, но и операции изменения структуры списка. Наиболее распространенной является реализация операции удаления из списка текущего элемента, но можно определить также и операции вставки в список новых элементов в текущей позиции.

Рассмотрим еще одну реализацию внешнего итератора списка, в которой определим эти дополнительные операции. Метод remove () будет удалять текущий элемент списка, при этом позиция указателя итерации будет сдвигаться на следующий за удаляемым элемент. Два метода insertBefore(int item) и

insert After (int item) будут добавлять новый элемент в список перед и, соответственно, после текущего элемента. В обоих случаях текущая позиция будет устанавливаться на вновь созданный элемент.

Для изменения структуры списка недостаточно иметь в итераторе указатель только на текущий элемент списка, поскольку при вставках и удалениях элементов необходимо изменять также поле next предыдущего элемента списка. Поэтому в новом представлении итератора будут храниться два указателя — указатель на текущий

элемент и ссылка на переменную, в которой хранится указатель на этот элемент. Эта ссылка может указывать либо на поле next предыдущего элемента списка, либо на поле first самого списка, если текущим элементом является первый элемент. Заметим, что в тот момент, когда итератор добирается до конца списка (метод hasMoreElements в этот момент выдает значение false), есть возможность вставить элемент "перед текущим" с помощью метода insertBefore. При этом новый элемент становится последним элементом в списке, а текущим элементом становится новый, только что вставленный элемент.

В случае, когда какую-либо из операций выполнить невозможно (например, удалить текущий элемент в ситуации, когда итератор дошел до конца списка), будем возбуждать исключительную ситуацию NoListEiement. В листинге 2.27 представлено модифицированное определение внешнего итератора списка целых intList::iterator. Описание класса для представления исключительной ситуации NoListEiement в тексте книги опущено.

Листинг 2.27, Определение внешнего итератора с возможностью модификации списка

class IntList { public :

// Класс, представляющий внешний итератор списка class Iterator {

Listitem * curPos; // Текущая позиция в списке Listitem ** pred; // Ссылка на место, где хранится // указатель на этот элемент

public :

// Конструктор создает новый итератор для списка list. Iterator(IntList & list) { curPos = *(pred = &list.first); }

//Метод hasMoreElements проверяет, есть ли еще элементы для итерации bool hasMoreElements() const { return curPos != NULL; }

//Метод nextElement сдвигает текущую позицию на следующий элемент, void nextElement() { if (curPos) curPos = *(pred = &curPos->next); }

//Функция getCurrent обеспечивает доступ к текущему элементу,

int getCurrent() const { return curPos->item; }

// Этот вариант метода позволяет изменить значение текущего элемента, int & getCurrent() { return curPos->item; }

//Метод для удаления текущего элемента;

//текущим становится следующий элемент void remove () { if (!curPos)

throw NoListEiement("remove: отсутствует текущий элемент");

*pred = curPos->next; delete curPos; curPos = *pred;

}

//Следующие два метода вставляют новый элемент

//перед текущим и после текущего

void insertBefore(int newValue) {

Listitem * newltem = new Listitem(newValue, curPos); curPos = *pred = newltem;

}

void insertAfter(int newValue) { if (!curPos)

throw NoListEiement("insertAfter: отсутствует текущий элемент"); Listitem * newltem = new Listitem (newValue, curPos->next) ; curPos = *(pred = &newltem->next);

}

};

friend class Iterator;

// Этот метод создает новый внешний итератор для итерации списка this. Iterator * iterator() { return new Iterator(*this); }

} ;

Соседние файлы в папке ТЕМА КОНТЕЙНЕРЫ