Итератори в Delphi

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

И така, итератори. Започвайки, изглежда, с версия 2007, в допълнение към стандартното "за", Delphi поддържа следния синтаксис:

Това не само е неудобно, но би изисквало добавяне на нов ред към процедурата всеки път, когато се добавя нов флаг към набора.

Сега за по-интересните характеристики на итераторите. Можете да създавате итератори за вашите собствени класове. Например собствен списък или TStringList може да поддържа итерация върху елементите на списъка. Повечето стандартни класове го поддържат:

Очевиден въпрос: възможно ли е да не се създаде нито обект, нито запис за итерация? Не можем ли просто да върнем препратка към себе си вGetEnumerator, ако сме сигурни, че итерацията ще се използва само на свой ред?

Правилен отговор: не, не можете. Delphi автоматично унищожава итератора след употреба. Ако върнете основния обект вGetEnumerator, той ще бъде унищожен. Нищо не може да се направи по въпроса, не можете да отменитеDestroy.

Някой ще попита този трик със записите става ли. Записите не се унищожават, нали? Да, записите не се унищожават, но с тях подобни трикове нямат особен смисъл. Не забравяйте, че високите резултати се предават по стойност; това означава, че функцията GetEnumerator връща не препратка към запис, а цял блок от данни, цялото му съдържание. Можете, разбира се, да върнете самия извикващ:

Това ще означава само, че ще създадетеновзапишете TMyRecord и копирайте цялото съдържание на стария в него. Този подход, между другото, може да се окаже полезен един ден, например, ако във вашия TMyRecord има малко количество критична информация. Когато осъществявате достъп от множество нишки, понякога е полезно да не заключвате обекта за цялата итерация, а да копирате информацията му за по-късна итерация и да я освободите незабавно.

ЗаключванияТова бяха простите приложения на итераторите, сега нека да преминем към по-сложните. Най-хубавото на итераторите е, че те позволяват да се изпълнява произволен код по време на итерация. Това ще използваме. Например, нека направим итерацията нишково-безопасна. Когато работи с множество нишки, всеки програмист ще повтори по следния начин:

Понякога този подход, както казах, е много удобен - но не винаги. Често, за да запазите състоянието, трябва да копирате целия масив, дори и указателите, а това е изключително дълго. Жалко е, че не можем да знаем кога Delphi разрушава структура. или можем ли?

Това, което ще направим сега, е малко вълшебство. Delphi не извиква деструктори за записи, но финализира цялото им съдържание, включително извикване на _Release за интерфейси. Затова ще създадем интерфейс, който ще освободи заключването на _Release. Първо, имаме нужда от заменена реализация на IInterface:

Оставихме RefCnt само за да не нарушим случайно вътрешни оптимизации на Delphi, които използват тази стойност. Най-общо казано, човек винаги може да върне един. Нашият обект не се унищожава, когато RefCnt падне до нула; животът му се определя, както за обикновен делфийски предмет, чрез ръчно унищожаване. Когато улавя препратка към себе си, той заключва итерирания обект, когато го освободи, го отключва.

Сега самата колекция:

Обърнете внимание, че ние съхраняваме gatekeeper като обект. Ако трябваше да го съхраним като интерфейс, той ще бъде постоянно уловен. „Интелигентните“ указатели към интерфейси в Delphi са проектирани така, че те автоматично извикват _AddRef, когато присвояват стойност на променлива от тип интерфейс, и автоматично извикват _Release, когато изчистват тази стойност.

Когато бъдем попитани за TMyCollectionEnumerator, ние използваме това свойство: връщаме итератор, вътре в който поставяме променлива от тип IInterface. Когато поставим нашия Gatekeeper в него, delphi автоматично изпълнява _AddRef, заключвайки колекцията. Когато записът бъде унищожен, Delphi автоматично финализира записа, изчиства полето Gatekeeper и тъй като е тип интерфейс, извиква _Release върху него - и колекцията се отключва.

Това несъмнено е много удобен и бърз начин. Един обект от тип Gatekeeper е достатъчен за всяка колекция; можете да го използвате в множество итератори наведнъж. Създава се веднъж, когато се създава TMyCollection, и не добавя почти никакви допълнителни разходи. Тук обаче има някои подводни камъни. Въпреки че Delphi гарантира, че записът на итератора ще бъде унищожен и като го унищожи, гарантира, че интерфейсът ще бъде почистен, не се знае кога ще го направи. В прикачения код направих някои експерименти и открих например, че въпреки че в нормалните функции итераторът се унищожава незабавно при излизане от "for . in", в основното тяло на итераторите на запис на конзолното приложение изобщо не се унищожават. Така че тази техника трябва да се използва с повишено внимание.

ФилтриДруга интересна употреба на итератори са филтрите. Вместо да напишете:

Проблемът тук е, че синтаксисът на delphi стриктно изисква обектът от дясната страна на "for . in" да се приложиGetEnumerator. Филтърната функция, която пишем, трябва сама да създаде и върне някакъв обект, а този обект след това ще трябва да създаде друг - изброителя. Бих искал да мога да използвам същия този обект, създаден във FilterKeepalive като изброител (в края на краищата вече не е необходим!). Това обаче няма да работи със записи поради вече споменатата причина: ако просто върнем „Result := Self“ в GetEnumerator, ние всъщност ще копираме записа и няма да подобрим ситуацията по никакъв начин.

Друго нещо са класовете. Тук няма допълнителни разходи:

Класът просто връща препратка към себе си. Ако си спомняте, беше забранено да се прави това за колекция, тъй като delphi унищожава итератора след употреба. Тук обаче това не е само полезно за нас, но и жизненоважно: кой друг ще унищожи временния клас, създаден във FilterKeepalive?

ГенераториДруга интересна употреба на итераторите е, че не е нужно да итерираме върху съществуващи обекти. Итераторът може да обхожда елементи, които изчислява в движение. Според връзката в примерите има генериране на числа на фибоначи и ще решим по-практичен проблем - и по-бързо (разбира се, без фабрични класове и интерфейси, дай Боже).

Нека създадем итератор, който ще ни върне всички прозорци от по-високо ниво в системата:

Това е всичко. Функцията TopLevelWindows връща фабрика и изобщо не изисква никакви операции (върнатият запис се разпределя автоматично). Включената програма минава през всички прозорци и ги отпечатва на екрана (не се крие, не се страхувайте, още не съм толкова луд).

P.S.Общо казано, можеше да не се извращава с windows. Обикновените масиви работят също толкова добре:

И тукбеше доста лесно да се справим с масив, но ние се възползваме от факта, че ако искаме да прекъснем по средата на изброяването, няма да правим ненужни заявки към файловата система. Е, като цяло, не разпределяйте масиви отново. Записът за итерация се разпределя в стека, доколкото разбирам.

ЗаключениеКато приложение, давам кода за четири малки конзолни програми, илюстриращи някои от горните. Компилацията изисква Delphi 2010, който можете да изтеглите за 30 дни от уебсайта на embarcaderro. Може би ще се компилира на предишни версии, не съм го тествал.