Ітератори в delphi - void

Ітератори в Delphi

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

Це приклад типового використання ітерації за розділами. Без ітерації довелося б надходити ось так:

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

Тепер про більш цікаві можливості ітераторів. Допускається створювати ітератори власним класах. Наприклад, власний список або TStringList можуть підтримувати ітерацію за елементами списку. Більшість стандартних класів її і підтримують:

Підтримку ітерації можна додати і власним класу. Для цього нам буде потрібно допоміжний клас, итератор (або енумератор, кому як більше подобається). Основний клас повинен мати функцію GetEnumerator. яка створює і повертає екземпляр допоміжного класу:

Сам итератор повинен містити в собі функцію MoveNext, що повертає False, якщо більше елементів немає, і властивість Current, яке повертає поточний елемент:

Ось і готовий найпростіший итератор. Чи не можна його як-небудь поліпшити? Наприклад, будь-який досвідчений дельфіст тут же помітить, що кожне використання ітератора вимагає створення об'єкта, а створення об'єктів, ми пам'ятаємо, досить повільна операція. Слава богу, можна зробити итератор рекордом. Все, що потрібно змінити в нашому коді - прибрати виклик до успадкованого Create:

Таким чином отримуємо досить швидку ітерацію за довільним контейнеру. Можна і сам TMyCollection зробити рекордом. Особливої ​​вигоди в продуктивності це не дасть, оскільки він створюється лише один раз, але якщо це потрібно для інших цілей - завжди будь ласка. Про всяк випадок нагадую, як в Дельфи реалізовані посилання хрест-навхрест. З класами:

Очевидний питання: а чи не можна взагалі не створювати на ітерацію ні об'єкта, ні рекорду? Чи не можна просто повертати в GetEnumerator посилання на самого себе, якщо ми впевнені, що итерацией будуть користуватися тільки по черзі?

Правильна відповідь: ні, не можна. Дельфі автоматично знищує итератор після використання. Якщо в GetEnumerator ви повернете основний об'єкт, він і буде знищений. Зробити з цим нічого не можна, перевизначити Destroy не можна.

Хтось запитає, чи можна провернути цей фокус з рекордами. Рекорди адже не знищуються? Так, рекорди не знищуються, але з ними такі трюки і не мають особливого сенсу. Не забувайте, що рекорди передаються за значенням; це означає, що функція GetEnumerator повертатися не посилання на рекорд, а цілий блок даних, весь його вміст. Ви можете, звичайно, повернути і сам викликається об'єкт:

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

блокування
Це були прості застосування ітераторів, а тепер перейдемо до більш складним. Найзручніше в Ітератор то, що вони дозволяють виконувати довільний код у момент перебору. Цим ми і скористаємося. Наприклад, зробимо перебір потоко-безпечним. При роботі з декількома потоками будь-який програміст виконує перебір приблизно так:

Спростимо цю конструкцію!

Іноді такий підхід, як я вже говорив, дуже зручний - але не завжди. Часто для збереження стану потрібно копіювати весь масив, нехай навіть покажчиків, а це вкрай довго. Як шкода, що ми не можемо дізнатися, коли Delphi знищує структуру ... або можемо?

Те, що ми зараз зробимо - це невелике чаклунство. Дельфі не викликає деструкторов для рекордів, але вона остаточно оформлюються все їх вміст, в тому числі, викликає _Release для інтерфейсів. Тому ми створимо інтерфейс, який буде звільняти блокування по _Release.
Для початку нам потрібно перевизначених реалізація IInterface:

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

Тепер сама колекція:

Зверніть увагу, що ми зберігаємо Gatekeeper як об'єкт. Якби ми зберігали його, як інтерфейс, він постійно перебував би захопленим. "Розумні" покажчики на інтерфейси в Дельфи влаштовані так, що автоматично викликають _AddRef при присвоєнні значення змінної интерфейсного типу, і автоматично викликають _Release при очищенні цього значення.

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

Це, безсумнівно, дуже зручний і швидкий спосіб. Досить одного об'єкта типу Gatekeeper на будь-яку колекцію; можна використовувати його в безлічі ітераторів відразу. Він створюється один раз, при створенні TMyCollection, і майже не додає накладних витрат. Однак тут є свої підводні камені. Хоча Delphi гарантує знищення рекорду-ітератора, а знищуючи його, гарантує очищення інтерфейсу, невідомо, коли вона це зробить. У доданому коді я виконував деякі експерименти, і з'ясував, наприклад, що хоча в звичайних функціях итератор знищується відразу ж по виходу з "for ... in", в основному тілі консольного застосування ітератори-рекорди не знищуються взагалі. Так що цей прийом слід використовувати з обережністю.

фільтри
Ще одне цікаве застосування ітераторів - фільтри. Замість того, щоб писати:

Хотілося б щось таке:

З ітераторами це легко зробити, правда, за додаткову ціну - якщо ви користуєтеся рекордами. Ця додаткова ціна - створення ще одного тимчасового рекорду. демонструю:

Проблема тут в тому, що синтаксис дельфи жорстко вимагає від об'єкта, що стоїть в правій частині "for ... in" реалізовувати GetEnumerator. Функція-фільтр, яку ми пишемо, повинна вже сама по собі створити і повернути якийсь об'єкт, а цей об'єкт потім повинен буде створити ще один - енумератор. Хотілося б, щоб можна було в якості енумератора використовувати цей самий, створений в FilterKeepalive об'єкт (в кінці кінців, він більше нізачем не потрібен!). Однак з рекордами це не пройде по вже згаданої причини: якщо ми просто повернемо в GetEnumerator "Result: = Self", ми насправді скопіюємо рекорд, і нічим не покращимо стан.

Інша справа - класи. Тут ніякої додаткової вартості не накладається:

Клас просто повертає посилання на самого себе. Якщо пам'ятаєте, для колекції це робити заборонялося, оскільки дельфи знищує итератор після використання. Однак тут нам це не просто на руку, а життєво необхідно: хто ще знищить створений в FilterKeepalive тимчасовий клас?

Генератори
Ще одне цікаве застосування ітераторів пов'язано з тим, що нам зовсім не обов'язково перебирати вже існуючі об'єкти. Итератор може перебирати елементи, що обчислюються їм же на ходу. За посиланням в прикладах є генерація чисел Фібоначчі, а ми вирішимо більш практичну задачу - і більш швидко (вже зрозуміло, без класів-фабрик і інтерфейсів, боронь мене господи).

Створимо итератор, який буде повертати нам всі вікна старшого рівня в системі:

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

Очевидно, нам потрібно фабрика, яка його створює:

От і все. Функція TopLevelWindows повертає фабрику, причому для цього не потрібно робити взагалі ніяких операцій (повертається рекорд виділяється автоматично). Додається програма перебирає всі вікна і друкує їх на екрані (не приховує, не бійтеся, я ще не такий псих).

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

Ну добре, добре, хочете - файли?

Тут теж абсолютно спокійно можна було обійтися масивом, але ми вигадували на те, що якщо в середині енумераціі нам захочеться зробити break - ми не зробимо зайвих запитів до файлової системи. Ну і взагалі, масиви зайвий раз не виділяти. Рекорд для ітерації-то виділяється в стеці, наскільки я розумію.