ВУЗ: Не указан

Категория: Не указан

Дисциплина: Не указана

Добавлен: 11.12.2025

Просмотров: 3161

Скачиваний: 1

ВНИМАНИЕ! Если данный файл нарушает Ваши авторские права, то обязательно сообщите нам.

В т о р а я

с т р о к а

 

Между в т о р о й и

т р е т ь е й с т р о к о й

П о с л е д н я я с т р о к а

Нажмите

< E n t e r >

д л я з а в е р ш е н и я п р о г р а м м ы . . .

Обобщенную версию этого связанного списка можно найти в демонстраци­ онной программе G e n e r i c L i n k e d L i s t C o n t a i n e r на прилагаемом ком­ пакт-диске. Обратите внимание, что G e n e r i c L i n k e d L i s t C o n t a i n e r продолжает использовать интерфейс I E n u m e r a t o r , который будет рас­ смотрен немного позже, но в некоторых ситуациях следует применять вме­ сто него новую обобщенную версию I E n u m e r a t o r < T > . Однако пока не стоит начинать анализировать обобщенные классы. Кроме того, обратитесь к встроенному обобщенному классу L i n k e d L i s t , который, несомненно, превосходит написанный здесь.

Зачем нужен связанный список

Связанный список может показаться пустыми хлопотами. Его основное преимущест­ во заключается в большой скорости вставки и удаления узлов. "Ну хорошо, — можете сказать вы. — Но ведь добавление s t r i n g в массив из четырех или пяти строк не слож­ нее перемещения нескольких ссылок для освобождения места." А что вы скажете, если этот массив будет содержать несколько сотен тысяч строк, и вы должны выполнять мас­ су вставок и удалений из него?

Второе преимущество связанного списка в том, что он может расти и уменьшаться. Если вы думаете, что вам будут нужны 1000 объектов, то вы должны создать массив на 1000 элементов, независимо от того, будете вы их использовать или нет. Что еще хуже, если вы в действительности создадите 2000 объектов, то можете считать, что в этот раз вам крупно не повезло. (Да, конечно, можно создать второй массив с большей емкостью и скопировать в него содержимое первого, но что при этом можно сказать об эффектив­ ности и затратах памяти?)

На этом принципе основаны многие распространенные вирусы. Например, неко­ торый исполненный благих намерений программист решает, что 256 символов будет достаточно для любого имени файла, и объявляет массив c h a r [ 2 5 6 ] . Ес­ ли программист забудет убедиться, что имя на самом деле не длиннее, чем ожи­ дается, то у хакера появляется шанс сломать программу, передав ей неправдопо­ добно длинное имя, и переписать тем самым часть кода за массивом (это называ­ ется переполнением буфера). Впрочем, это проблема в первую очередь C/C++: С# автоматически проверяет выход за границы массива.

В оставшейся части главы будут проанализированы три разных подхода к общей за­ даче итерирования коллекции. В этом разделе будет продолжено обсуждение наиболее традиционного (как минимум, для программистов на С#) подхода с использованием ите­ раторов, которые реализуют интерфейс I E n u m e r a t o r . В качестве примера рассматри­ вается итератор для связанного списка из предыдущего раздела.

Глава 20. Работа с коллекциями

461


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

Доступ к коллекции: общая задача

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

/ / п е р е д а е т с я к о л л е к ц и я л ю б о г о в и д а

 

v o i d m y C l e a r F u n c t i o n ( C o l l e c t i o n a C o l l ,

i n t i n d e x )

{

 

 

 

a C o l l [ i n d e x ]

= 0 ; / / И н д е к с и р о в а н и е р а б о т а е т н е д л я в с е х

 

 

/ / т и п о в к о л л е к ц и й

 

/ /

. . . п р о д о л ж е н и е . . .

 

}

 

 

 

Коллекции каждого типа могут сами определять свои методы доступа (и делают это).

Например, связанный список может предоставить метод G e t N e x t ()

для выборки сле­

дующего

элемента из цепочки объектов; стек может предложить

методы Push()

и P o p ()

для добавления и удаления объектов и т.д.

 

Более общий подход состоит в предоставлении для каждого класса коллекции от­ дельного так называемого класса итератора, который знает, как работать с конкретной коллекцией. Каждая коллекция X определяет свой собственный класс I t e r a t o r X . В от­ личие от X, I t e r a t o r X представляет общий интерфейс I E n u m e r a t o r , золотой стан­ дарт итерирования. Этот метод использует второй объект, именуемый итератором, в ка­ честве указателя внутрь коллекции.

Итератор (перечислитель) обладают следующими преимуществами.

Каждый класс коллекции может определить свой собственный класс итератора. Поскольку итератор реализует стандартный интерфейс I E n u m e r a t o r , с ним обычно легко работать.

Прикладной код не должен знать о внутреннем устройстве коллекций. Пока про­ граммист работает с итератором, тот берет на себя все заботы о деталях. Это — хорошая инкапсуляция.

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

Чтобы сделать возможным наличие цикла f o r e a c h , интерфейс I E n u m e r a t o r должен поддерживать различные типы коллекций — от массивов до связанных списков. Следова­ тельно, его методы должны быть максимально обобщенными, насколько это возможно. Например, нельзя использовать итератор для произвольного доступа к элементам коллек­ ции, поскольку большинство коллекций не обеспечивают подобного доступа.

462

Часть VII. Дополнительные главы

 



I E n u m e r a t o r предоставляет три следующих метода.

R e s e t () — устанавливает итератор таким образом, чтобы он указывал на начало коллекции. Примечание: обобщенная версия I E n u m e r a t o r , I E n u m e r a t o r < T > , не предоставляет метод R e s e t ( ) . В случае обобщенного L i n k e d L i s t просто начинайте работу с вызова M o v e N e x t ( ) .

M o v e N e x t () — перемещает итератор от текущего объекта в контейнере к сле­ дующему.

C u r r e n t — свойство (не метод), которое дает объект данных, хранящийся в те­ кущей позиции итератора.

Описанный принцип продемонстрирован приведенной далее функцией. Про­ граммист класса M y C o l l e c t i o n (не показанного здесь) создает соответствующий класс итератора — скажем, I t e r a t o r M y C o n t a i n e r (применяя соглашение об именах I t e r a t o r X , упоминавшееся ранее). Прикладной программист ранее сохра­ нил ряд объектов C o n t a i n e d D a t a O b j e c t s в коллекции M y C o l l e c t i o n . Приве­ денный далее фрагмент исходного текста использует три стандартных метода I E n u m e r a t o r для чтения этих объектов:

/ / К л а с с M y C o l l e c t i o n х р а н и т о б ъ е к т ы т и п а

 

 

/ / C o n t a i n e d D a t a O b j e c t

d a t a

 

 

 

 

 

 

void

M y F u n c t i o n ( M y C o l l e c t i o n

m y C o l l )

 

 

 

{

 

 

 

 

 

 

 

 

 

 

/ / П р о г р а м м и с т ,

с о з д а в ш и й к л а с с M y C o l l e c t i o n ,

с о з д а л т а к ж е

/ / и к л а с с и т е р а т о р а

I t e r a t o r M y C o l l e c t i o n ;

п р и к л а д н о й

/ / п р о г р а м м и с т

с о з д а е т о б ъ е к т и т е р а т о р а д л я п р о х о д а п о

/ /

о б ъ е к т у m y C o l l

 

 

 

 

 

 

 

 

I E n u m e r a t o r i t e r a t o r

=

n e w

I t e r a t o r M y C o l l e c t i o n ( m y C o l l ) ;

/ / п е р е м е щ а е м и т е р а т о р в

" с л е д у ю щ у ю п о з и ц и ю " в н у т р и

/ / к о л л е к ц и и

 

 

 

 

 

 

 

 

 

w h i l e ( i t e r a t o r . M o v e N e x t ( ) )

 

 

 

 

 

 

{

 

 

 

 

 

 

 

 

 

 

 

/ / П о л у ч а е м с с ы л к у н а о б ъ е к т д а н н ы х в т е к у щ е й п о з и ц и и

 

/ / к о л л е к ц и и

 

 

 

 

 

 

 

 

 

 

C o n t a i n e d D a t a O b j e c t

c o n t a i n e d D a t a ;

/ / d a t a

 

 

c o n t a i n e d =

( C o n t a i n e d D a t a O b j e c t ) i t e r a t o r . C u r r e n t ;

 

/ / . . . и с п о л ь з у е м о б ъ е к т

д а н н ы х

c o n t a i n e d . . .

 

Функция M y F u n c t i o n ()

принимает

в

качестве аргумента

коллекцию C o n ­

t a i n e d D a t a O b j e c t s . Она

начинается с

создания

итератора

типа I t e r a t o r M y ­

C o l l e c t i o n . Функция начинает цикл с вызова M o v e N e x t ( ) . При первом вызове MoveNext () перемещает итератор к первому элементу коллекции. При каждом по­ следующем вызове M o v e N e x t () перемещает указатель "на одну позицию." Функ­ ция M o v e N e x t () возвращает f a l s e , когда коллекция исчерпана и итератор боль­ ше нельзя передвинуть.

Свойство C u r r e n t возвращает ссылку на объект данных в текущей позиции итера­ тора. Программа преобразует возвращаемый объект в C o n t a i n e d D a t a O b j e c t перед тем, как присвоить его переменной c o n t a i n e d . Вызов C u r r e n t некорректен, если предшествующий вызов метода M o v e N e x t () не вернул t r u e .

Глава 20. Работа с коллекциями

463


Использование foreach

Методы I E n u m e r a t o r достаточно стандартны для того, чтобы С# использовали их автоматически для реализации конструкции f o r e a c h .

Цикл f o r e a c h может обращаться к любому классу, реализующему интерфейс I E n u m e r a b l e , как показано в приведенной обобщенной функции, которая может работать с любым классом — от массивов и связанных списков до стеков и очередей:

v o i d M y F u n c t i o n ( I E n u m e r a b l e c o n t a i n e r O f S t r i n g s )

 

{

 

 

 

f o r e a c h ( s t r i n g s

i n c o n t a i n e r O f S t r i n g s )

 

 

{

 

 

 

C o n s o l e . W r i t e L i n e ( " С л е д у ю щ а я с т р о к а -

{ о } " ,

s ) ;

}

 

 

 

}

 

 

 

Класс реализует I E n u m e r a b l e путем определения метода G e t E n u m e r a t o r ( ) , который возвращает экземпляр I E n u m e r a t o r . Скрыто от посторонних глаз f o r e a c h вызывает метод G e t E n u m e r a t o r () для получения итератора. Цикл ис­ пользует этот итератор для обхода контейнера. Каждый выбираемый им элемент приводится к соответствующему типу перед тем, как продолжить выполнение тела

цикла. Обратите внимание, что

I E n u m e r a b l e и

I E n u m e r a t o r различные, но свя-

занные

интерфейсы. С#

2.0

предоставляет обобщенную версию обоих интерфей­

с о в —

см. информацию

о пространстве имен

S y s t e m . C o l l e c t i o n s . G e n e r i c

в справочной системе.

 

 

 

Итак, цикл f o r e a c h можно записать таким образом:

f o r e a c h ( i n t n V a l u e

i n m y C o n t a i n e r )

 

{

 

 

 

 

// ...

}

Это эквивалентно следующему циклу f o r : f o r ( I E n u m e r a t o r i =

m y C o n t a i n e r . G e t E n u m e r a t o r ( ) ; / /

i . M o v e N e x t ( ) ;

/ /

)

/ /

Ин и ц и а л и з а ц и я

Ус л о в и е

Пу с т о й и н к р е м е н т

{

i n t n V a l u e = ( i n t ) i . C u r r e n t

/ /

П о л у ч е н и е

т е к у щ е г о

//

/ /

э л е м е н т а

 

}

 

 

 

Раздел инициализации цикла f o r получает

итератор. Раздел

условия использует

M o v e N e x t () для определения конца контейнера. M o v e N e x t () сам увеличивает указа­ тель, т.е. раздел инкремента цикла пуст. (Тем не менее, вам все равно следует указать точку с запятой после раздела условия, после которой не следует никакой код. В приве­ денном фрагменте исходного текста это точка с запятой после i . M o v e N e x t ().) В пер­ вой строке цикла выбирается очередной объект и преобразуется к i n t ( C u r r e n t всегда возвращает тип O b j e c t ) . Если возвращенный объект не является i n t , С# генерирует исключение неверного преобразования типа.

464

Часть VII. Дополнительные главы