10. Блоки та процедурні об’єкти

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

То що це за нова крута можливість? Це здатність приймати код (тобто код між do і end ), обгортати його в об'єкт (званий або англійською), зберігати його в змінній або передавати його в метод, а потім виконувати код цього блоку, коли б ви не побажали (більше одного разу, якщо хочете). Таким чином, блок нагадує справжній метод за винятком того, що він не прив'язаний до жодного об'єкта (він сам є об'єктом), і ви можете зберігати його або передавати його як параметр подібно до того, як ви це робите з будь-яким іншим об'єктом. Думаю, настав час навести приклад:

Отже, я створив об'єкт proc (ця назва, гадаю, означає скорочення від "procedure", тобто "процедура", але набагато важливіше, що воно римується з "block"), який містить блок коду, потім я за допомогою call викликав proc-об'єкт тричі. Як бачите, це дуже нагадує метод.

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

Добре, ось ми дізналися, що собою представляють блоки і proc -і [читається: "проки" - Прим. перев. ], і як їх можна використовувати, але в чому тут справа? Чому просто не використовувати методи? Ну тому що деякі речі ви просто не зможете зробити за допомогою методів. Зокрема, ви не можете передавати методи в інші методи (але ви можете передавати методи процедурні об'єкти), і методи не можуть повертати інші методи (але вони можуть повертати proc-об'єкти). Це можливо просто тому, що proc -іє об'єктами, а методи – ні.

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

Методи приймання процедурних об'єктів

Коли ми передаємо процедурний об'єкт у метод, ми можемо керувати тим, як, у якому разі чи скільки разів ми викликаємо proc-об'єкт. Наприклад, є, скажімо, щось, що ми хочемо зробити перед та після виконання деякого коду:

Можливо, це не виглядає так особливо приголомшливим. але це і є. :-) У програмуванні занадто часто є суворі вимоги до того, що має бути зроблено і коли. Якщо ви хочете, наприклад, зберегти файл, потрібно відкрити файл, записати туди інформацію, яку ви хочете в ньому зберігати, а потім закрити файл. Якщо ви забудете закрити файл, можуть статися "Погані Речі" TM . Але кожного разу, коли ви хочете зберегти або завантажити файл, вам потрібно робити те саме: відкривати файл, виконувати те, що ви дійсно бажаєте зробити, потім закривати файл. Це втомлює і легко забувається. У Ruby збереження (або завантаження) файлів працює подібно до наведеного вище коду, тому вам не потрібно турбуватися ні про що, крім того, що ви дійсно хочете зберегти (або завантажити). (У наступному розділі я покажу вам, де дізнатися, як робити такі речі, як збереження та завантаження файлів.)

Ви також можете написати методи, які будуть визначати, скільки разів (або навіть за якоїсь умови) викликати процедурний об'єкт. Ось метод, який викликатиме переданий йому proc-об'єкт приблизно в половині випадків, і ще один метод, який викликатиме його двічі:

(Якщо ви перезавантажите цю сторінку кілька разів [мається на увазі сторінка оригінального підручника- Прим. перев. ], то ви побачите інші результати.) Це найпоширеніші застосування процедурних об'єктів, які дають нам можливість робити такі речі, які ми просто не могли б зробити, використовуючи лише методи. Звичайно, ви могли б написати метод, щоб підморгнути двічі, але ви не змогли б написати метод, щоб просто робити двічі щось!

Перш ніж ми продовжимо, давайте подивимося на останній приклад. Досі всі процедурні об'єкти, що передаються, були досить схожі один на одного. Цього разу вони будуть зовсім іншими, і ви побачите, наскільки подібний метод залежить від тих процедурних об'єктів, що були йому передані. Наш метод прийме звичайний об'єкт та процедурний об'єкт, і викличе процедурний об'єкт із звичайним об'єктом як параметр. Якщо процедурний об'єкт поверне false, ми закінчимо виконання, інакше ми викличемо процедурний об'єкт із повернутим об'єктом. Ми продовжуватимемо так робити, поки процедурний об'єкт не поверне false (що йому краще зробити врешті-решт, інакше програма "загнеться"). Цей метод поверне останнє значення, повернене процедурним об'єктом, що не дорівнює false .

Добре визнаю, що це був досить дивний приклад. Але він показує, наскільки по-різному поводиться наш метод, коли йому передають різні процедурні об'єкти.

Метод inspect багато в чому схожий на to_s за винятком того, що рядок, що їм повертається, — це спроба показати код на Ruby для створення об'єкта, який ви йому передали. Тут він показує нам весь масив, повернутий за нашого першого виклику методу doUntilFalse . Ви, мабуть, також помітили, що ми самі ніколи не зводили в квадрат цей 0 наприкінці масиву, але оскільки 0 у квадраті завжди дорівнює 0 нам це і не потрібно було робити. А так як завждиFalse , як визнаєте, повертає завжди false, метод doUntilFalse нічого не робив, коли ми викликали його вдруге; він просто повернув те, що йому було передано.

Методи, що повертають процедурні об'єкти

Ще одна з крутих можливостей, які можна робити з процедурними об'єктами, це те, що їх можна створювати методами, а потім повертати їх. Це уможливлює різноманітні божевільні, але потужні програмістські штучки (з вражаючими назвами на кшталт , і ). Але річ у тому, що я майже ніколи не використовував це на практиці, а також не пригадаю, щоб бачив, як хтось застосовував це у своєму коді. Думаю, це не такі речі, які зазвичай потрібно робити на Ruby, а, можливо, Ruby просто підштовхує вас знаходити інші рішення — не знаю. У будь-якому випадку, я тільки коротко торкнуся цього.

У цьому прикладі метод compose приймає два процедурні об'єкти і повертає новий процедурний об'єкт, який викликаний викликає перший процедурний об'єкт і передає його результат в другий.

Зверніть увагу, що виклик proc1 повинен бути всередині дужок при виклику proc2, щоб він був здійснений першим.

Передача блоків (не proc-об'єктів) у методи

Ну, добре, цей підхід представляє суто академічний інтерес, до того ж застосовувати його дещо важко. В основному складність полягає в тому, що тут вам доводиться виконати три кроки (визначити метод, створити процедурний об'єкт та викликати метод із процедурним об'єктом); тоді як є відчуття, що має бути лише два (визначити метод і передати блок безпосередньо в цей метод, зовсім не використовуючи процедурний об'єкт), оскільки в більшості випадків ви не хочете використовувати процедурний об'єкт/блок після того, як ви передали його методу. Що ж, хай буде вам відомо,що у Ruby все це вже зроблено за нас! Фактично, ви вже робили це щоразу, коли використовували ітератори.

Спочатку я швидко покажу вам приклад, а потім ми його обговоримо.

Отже, все, що ми повинні зробити, щоб передати блок у метод eachEven , це "приліпити" блок після методу. Подібним чином ви можете передати блок у будь-який метод, хоча багато методів просто проігнорують блок. Щоб змусити ваш метод не ігнорувати блок, а взяти його і перетворити його на процедурний об'єкт, потрібно помістити ім'я процедурного об'єкта в кінці списку параметрів вашого методу і поставити перед ним амперсанд (&). Звичайно, це трохи мудро, але не занадто, і вам доведеться зробити це лише один раз (коли ви описуєте метод). А потім ви можете використовувати цей метод знову і знову так само, як і вбудовані методи, що приймають блоки такі, як each і times. (Пам'ятайте, 5 .times do . )

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

Пам'ятаю, одного разу я захотів зробити, щоб можна було виміряти, скільки часу виконуються різні секції програми. (Це також відомо як програмного коду.) І я написав метод, який засікає час перед виконанням коду, потім виконує його, в кінці знову засікає час і обчислює різницю. Зараз я не можу знайти цей метод, але мені він і не потрібний; він, можливо, виглядав приблизнотак:

Як просто! Як елегантно! Тепер за допомогою цього крихітного методу я легко можу виміряти час роботи будь-якої секції в будь-якій програмі, як тільки захочу: я просто закину код в блок і відправлю його методу profile . Що може бути простішим? У більшості мов мені знадобилося б явно додавати код для вимірювання часу (той, що написаний у profile ) до та після кожної секції, яку я хотів би захронометрувати. В той час, як у Ruby я все тримаю в одному-єдиному місці і (що більш важливо) окремо від решти!

Спробуйте ще дещо

• Дідусь годинник. Напишіть метод, який приймає блок і викликає його один раз для кожної години, яка пройшла сьогодні. Таким чином, якщо я би передав йому блок do puts ' БОМ! ' end , він би відбивав час (майже) як дідусиний годинник. Перевірте ваш метод з кількома різними блоками (включаючи те, що я вам дав).Підказка: Ви можете використовувати Time.now.hour для отримання поточної години. Однак, він повертає число між 0 і 23, тому вам доведеться змінити ці числа, щоб отримати звичайні числа, як на циферблаті (від 1 до 12).

• Протоколування програм. Напишіть метод під назвою log, який приймає рядок опису блоку і, звичайно, сам блок. Подібно до методу doSelfImportantly , він повинен виводити за допомогою puts рядок, що повідомляє, що він почав виконання блоку, і ще один рядок в кінці, що повідомляє, що він закінчив виконання блоку, а також повідомляє вам, що повернув блок. Перевірте метод, відправивши йому блок коду. Всередині цього блоку розмістіть інший виклик методу log, передавши йому інший блок. (Це називається .) Іншими словами, ваш висновок має виглядати приблизно так:

Ну ось майже все, що ви мали намір дізнатися з цього підручника. Мої вітання! Вививчили дуже багато! Можливо, вам не здається, що ви пам'ятаєте все, або ви пропустили деякі частини. Ну і добре, це нормально. Програмування це не те, що ви знаєте; це те, що ви можете обчислити. Поки ви знаєте, де знайти те, що ви забули, у вас буде все гаразд. Сподіваюся, ви не думаєте, що я написав усе це, не заглядаючи кудись час від часу? Я саме так і робив. Мені також багато допомагали з кодом, який виконує всі приклади в цьому підручнику. Але куди ж я заглядав і кого просив про допомогу? Давайте, я вам покажу.