Текст
                    Официальное руководство от создателя Ruby
Д. Флэнаган
Ю. Мацумото
O’REILLY®
С^ППТЕР

David Flanagan and Yukihiro Matsumoto The Ruby Programming Language O’REILLY’ Beijing • Cambridge • Farnham • Koln • Paris • Sebastopol • Taipei • Tokyo
Д. Флэнаган, Ю. Мацумото ЯЗЫК ПРОГРАММИРОВАНИЯ Ruby ^ЛЛТЕР* Москва Санкт-Петербург Нижний Новгород Воронеж Ростов-на-Дону • Екатеринбург Самара • Новосибирск Киев • Харьков • Минск 2011
ББК 32.973.2-018 УДК 004.43 М36 Флэнаган Д., Мацумото Ю. М36 Язык программирования Ruby. — СПб.: Питер, 2011. — 496 с.: ил. — (Серия «Бестселлеры O'Reilly»). ISBN 978-5-459-00562-2 Эта книга — официальное руководство по динамическому языку программирования Ruby. Ав- торский состав воистину звездный: Дэвид Флэнаган — известнейший специалист в области про- граммирования, автор ряда бестселлеров по JavaScript и Java; Юкихиро «Matz» Мацумото — со- здатель и ведущий разработчик Ruby. В книге приведено детальное описание всех аспектов языка: лексической и синтаксической структуры Ruby, разновидностей данных и элементарных выражений, определений методов, клас- сов и модулей. Кроме того, книга содержит информацию об API-фуикциях платформы Ruby. Издание будет интересно опытным программистам, знакомящимся с новым для себя языком Ruby, а также тем, кто уже программирует на Ruby и хочет достичь более высокого уровня пони- мания и мастерства работы. ББК 32.973.2-018 УДК 004.43 Права на издание получены по соглашению с O'Reilly. Все права защищены. Никакая часть данной книги не может быть воспроизведена в какой бы то ни было фор- ме без письменного разрешения владельцев авторских прав. Информация, содержащаяся в данной книге, получена из источников, рассматриваемых издательством как надежные. Тем не менее, имея в виду возможные человеческие или технические ошибки, издательство не может гарантировать абсолютную точность и полноту приводимых сведений и не несет ответственности за возможные ошибки, связанные с использованием книги. ISBN 978-0-596-51617-8 (англ.) ISBN 978-5-459-00562-2 © O'Reilly,2008 © Перевод на русский язык ООО Издательство «Питер», 2011 © Издание на русском языке, оформление ООО Издательство «Питер», 2011
Краткое оглавление Предисловие.....................................................15 Глава 1. Введение..............................................17 Глава 2. Структура и выполнение Ruby-программ..................45 Глава 3. Типы данных и объекты.................................63 Глава 4. Выражения и операторы................................115 Глава 5. Инструкции и управляющие структуры...................151 Глава 6. Методы, proc- и lambda-объекты и замкнутые выражения........................................217 Глава 7. Классы и модули.......................................261 Глава 8. Отражение и метапрограммирование......................319 Глава 9. Платформа Ruby........................................363 Глава 10. Среда окружения Ruby.................................465
Оглавление Предисловие.................................................... 15 Благодарности....................................................15 Дэвид Фланаган (David Flanagan).................................15 Юкихиро Мацумото (Yukihiro Matsumoto)...........................16 Способы оформления, принятые в этой книге........................16 Глава 1. Введение................................................17 1.1. Экскурсия по Ruby...........................................18 1.1.1. Объектная ориентированность Ruby.........................19 1.1.2. Блоки и итераторы........................................19 1.1.3. Выражения и операторы Ruby...............................21 1.1.4. Методы...................................................22 1.1.5. Присваивания.............................................23 1.1.6. Суффиксы и префиксы, состоящие из знаков пунктуации......24 1.1.7. Регулярные выражения и числовые диапазоны................25 1.1.8. Классы и модули..........................................26 1.1.9. Сюрпризы Ruby............................................28 1.2. Опробование Ruby............................................29 1.2.1. Интерпретатор Ruby.......................................29 1.2.2. Отображение вывода.......................................30 1.2.3. Интерактивный Ruby с irb.................................31 1.2.4. Просмотр документации по Ruby с помощью ri...............32 1.2.5. Управление пакетом программ Ruby с помощью gem...........32 1.2.6. Дополнительные учебные пособия по Ruby...................34 1.2.7. Источники Ruby...........................................34 1.3. О книге.....................................................34 1.3.1. Как читать эту книгу.....................................36 1.4. Решение головоломки Судоку на Ruby..........................36 Глава 2. Структура и выполнение Ruby-программ....................45 2.1. Лексическая структура.......................................46 2.1.1. Комментарии..............................................46 2.1.2. Литералы.................................................48 2.1.3. Знаки пунктуации.........................................49 2.1.4. Идентификаторы...........................................49
Оглавление 7 2.1.5. Ключевые слова............................................51 2.1.6. Разделители...............................................52 2.2. Синтаксическая структура.....................................55 2.2.1. Блочные структуры языка Ruby..............................56 2.3. Структура файла..............................................57 2.4. Кодировка программы..........................................58 2.4.1. Объявление кодировки программы............................59 2.4.2. Кодировка исходного кода и установленная по умолчанию внешняя кодировка..............................................60 2.5. Выполнение программы.........................................61 Глава 3. Типы данных и объекты....................................63 3.1. Числа........................................................64 3.1.1. Целочисленные литералы...................................65 3.1.2. Литералы чисел с плавающей точкой........................66 3.1.3. Арифметика, используемая в Ruby..........................66 3.1.4. Двоичное представление чисел с плавающей точкой и ошибки округления............................................68 3.2. Текст........................................................69 3.2.1. Строковые литералы........................................69 3.2.2. Символьные литералы.......................................78 3.2.3. Строковые операторы.......................................79 3.2.4. Получение доступа к символам и подстрокам.................81 3.2.5. Выполнение итераций в отношении строк.....................83 3.2.6. Кодировка строк и многобайтовые символы...................84 3.3. Массивы......................................................90 3.4. Хэши.........................................................93 3.4.1. Хэш-литералы..............................................94 3.4.2. Хэш-коды, равенство и изменяющиеся ключи..................94 3.5. Диапазоны....................................................95 3.5.1. Проверка принадлежности к диапазону.......................97 3.6. Обозначения..................................................98 3.7. True, False и Nil............................................99 3.8. Объекты.....................................................100 3.8.1. Ссылки на объекты........................................100 3.8.2. Продолжительность существования объекта..................101 3.8.3. Идентичность объекта.....................................102 3.8.4. Класс объекта и тип объекта..............................102 3.8.5. Равенство объектов.......................................104 3.8.6. Объект Order.............................................107 3.8.7. Преобразование объектов..................................108 3.8.8. Копирование объектов.....................................112 3.8.9. Маршализация (Marshaling) объектов.......................113
8 Оглавление 3.8.10. Замораживание объектов..................................113 3.8.11. Пометка объектов........................................114 Глава 4. Выражения и операторы....................................115 4.1. Простые литералы и литералы ключевых слов....................116 4.2. Ссылки на переменные.........................................117 4.2.1. Неинициализированные переменные..........................117 4.3. Ссылки на константы..........................................118 4.4. Вызовы методов...............................................120 4.5. Присваивания.................................................123 4.5.1. Присваивание значений переменным.........................124 4.5.2. Присваивание значений константам.........................125 4.5.3. Присваивание значений атрибутам и элементам массива......126 4.5.4. Сокращенная запись присваивания..........................127 4.5.5. Параллельное присваивание................................129 4.6. Операторы....................................................133 4.6.1. Унарные операторы + и -..................................135 4.6.2. Возведение в степень: **.................................136 4.6.3. Арифметические операторы: и %........................136 4.6.4. Сдвиг и добавление: « и »................................136 4.6.5. Дополнение, объединение, пересечение: ~, &, | и Л........137 4.6.6. Сравнение: <, <=, >, >= и <=>............................138 4.6.7. Равенство: ==, !=, =-,!~ и ===...........................139 4.6.8. Булевы операторы: &&, ||,!, and, or, not.................140 4.6.9. Диапазоны и триггеры:.. и................................143 4.6.10. Условный оператор: ?:...................................145 4.6.11. Операторы присваивания..................................147 4.6.12. Оператор defined?.......................................147 4.6.13. Операторы-модификаторы..................................149 4.6.14. Что не относится к операторам...........................149 Глава 5. Инструкции и управляющие структуры.......................151 5.1. Условия......................................................152 5.1.1. If.......................................................152 5.1.2. Работа if в качестве модификатора........................155 5.1.3. Unless...................................................157 5.1.4. Case.....................................................158 5.1.5. Оператор ?:..............................................162 5.2. Циклы........................................................162 5.2.1. While и until............................................162 5.2.2. While и until в качестве модификаторов................... 163 5.2.3. Цикл for-in..............................................165 5.3. Итераторы и перечисляемые объекты............................166 5.3.1. Числовые итераторы.......................................168 5.3.2. Перечисляемые объекты....................................168
Оглавление 9 5.3.3. Создание собственных итераторов..........................170 5.3.4. Нумераторы...............................................172 5.3.5. Внешние итераторы........................................174 5.3.6. Итерация и параллельное изменение........................177 5.4. Блоки.........................................................178 5.4.1. Синтаксис блока..........................................178 5.4.2. Значение блока...........................................179 5.4.3. Блоки и область видимости переменных.....................180 5.4.4. Передача аргументов блоку................................181 5.5. Изменение хода работы программы...............................184 5.5.1. Return...................................................184 5.5.2. Break....................................................186 5.5.3. Next.....................................................188 5.5.4. Redo.....................................................190 5.5.5. Retry....................................................191 5.5.6. Throw и catch............................................192 5.6. Исключения и их обработка....................................194 5.6.1. Классы и объекты исключений..............................194 5.6.2. Выдача исключений с помощью raise........................197 5.6.3. Обработка исключений с помощью rescue....................198 5.6.4. Предложение else.........................................203 5.6.5. Предложение ensure.......................................204 5.6.6. Rescue в определениях метода, класса и модуля............206 5.6.7. Rescue в качестве модификатора инструкции................206 5.7. BEGIN и END..................................................207 5.8. Потоки, нити (fibers) и продолжения (continuations)..........208 5.8.1. Потоки для параллельного выполнения......................209 5.8.2. Нити для сопрограмм......................................209 5.8.3. Продолжения..............................................215 Глава 6. Методы, proc- и lambda-объекты и замкнутые выражения.............................................217 6.1. Определение простых методов..................................219 6.1.1. Значение, возвращаемое методом...........................219 6.1.2. Методы и обработка исключений............................220 6.1.3. Вызов метода для объекта.................................221 6.1.4. Определение синглтон-методов.............................221 6.1.5. Отмена определения методов...............................222 6.2. Имена методов................................................223 6.2.1. Методы-операторы.........................................224 6.2.2. Псевдонимы методов.......................................224 6.3. Методы и круглые скобки......................................226 6.3.1. Необязательные скобки....................................226 6.3.2. Обязательные скобки......................................227
10 Оглавление 6.4. Аргументы метода............................................228 6.4.1. Параметры по умолчанию...................................229 6.4.2. Список аргументов переменной длины и массивы.............230 6.4.3. Отображение аргументов на параметры......................232 6.4.4. Использование хэшей для поименованных аргументов.........232 6.4.5. Блоки-аргументы..........................................233 6.5. Ргос и lambda...............................................237 6.5.1. Создание Ргос-объектов...................................237 6.5.2. Вызов объектов Ргос и Lambda.............................240 6.5.3. Арность Ргос.............................................241 6.5.4. Идентичность Ргос-объектов...............................242 6.5.5. Чем lambda-объекты отличаютсяот ргос-объектов............242 6.6. Замкнутые выражения.........................................246 6.6.1. Замкнутые выражения и совместно используемые переменные..247 6.6.2. Замкнутые выражения и связывания.........................249 6.7. Объекты класса Method.......................................250 6.7.1. Несвязанные объекты метода...............................251 6.8. Функциональное программирование.............................252 6.8.1. Применение функции к перечисляемым объектам..............252 6.8.2. Составление функций......................................254 6.8.3. Частично применяемые функции.............................255 6.8.4. Функции, обладающие мемоизацией..........................256 6.8.5. Классы Symbol, Method и Ргос.............................257 Глава 7. Классы и модули.........................................261 7.1. Определение элементарного класса............................262 7.1.1. Создание класса..........................................263 7.1.2. Создание экземпляра класса Point.........................263 7.1.3. Инициализация класса Point...............................264 7.1.4. Определение метода to_s..................................265 7.1.5. Методы доступа и атрибуты................................266 7.1.6. Определение операторов...................................268 7.1.7. Доступ к массивам и хэшам с помощью метода [ ]...........271 7.1.8. Перечисление координат...................................271 7.1.9. Равенство точек..........................................272 7.1.10. Упорядочение Point-объектов.............................274 7.1.11. Изменяющийся Point-объект...............................275 7.1.12. Быстрое и простое создание изменяющихся классов.........276 7.1.13. Метод класса............................................278 7.1.14. Константы...............................................280 7.1.15. Переменные класса.......................................280 7.1.16. Переменные экземпляра класса............................281 7.2. Область видимости методов: открытые, защищенные и закрытые методы..283 7.3. Подклассы и наследование....................................286
Оглавление 11 7.3.1. Наследуемые методы........................................287 7.3.2. Переопределение методов...................................288 7.3.3. Дополнение поведения путем выстраивания цепочки...........290 7.3.4. Наследование методов класса...............................291 7.3.5. Наследование и переменные экземпляра......................291 7.3.6. Наследование и переменные класса..........................292 7.3.7. Наследование констант.....................................293 7.4. Создание и инициализация объектов.............................294 7.4.1. New, allocate и initialize................................294 7.4.2. Фабричные методы..........................................295 7.4.3. Dup, clone и initialize copy..............................296 7.4.4. Marshal dump и marshal load...............................298 7.4.5. Шаблон Синглтон (Singleton)...............................299 7.5. Модули........................................................301 7.5.1. Модули как пространства имен..............................301 7.5.2. Использование модулей в качестве миксинов.................304 7.5.3. Включаемые модули пространства имен.......................306 7.6. Загрузка и востребование модулей..............................307 7.6.1. Путь загрузки.............................................308 7.6.2. Выполнение загруженного кода..............................310 7.6.3. Автоматически загружаемые модули..........................311 7.7. Синглтон-методы и обособленные классы (eigenclass)............312 7.8. Поиск метода..................................................314 7.8.1. Поиск метода класса.......................................315 7.9. Поиск констант................................................317 Глава 8. Отражение и метапрограммирование........................ 319 8.1. Типы, классы и модули.........................................321 8.1.1. Предки и модули...........................................321 8.1.2. Определение классов и модулей.............................323 8.2. Вычисление строк и блоков.....................................323 8.2.1. Связывания и метод eval...................................323 8.2.2. Instance eval и class eval................................324 8.2.3. Instance exec и class exec................................325 8.3. Переменные и константы........................................325 8.3.1. Запрос, установка и проверка переменных...................326 8.4. Методы........................................................328 8.4.1. Вывод имен и проверка наличия методов.....................328 8.4.2. Получение объектов метода.................................329 8.4.3. Вызов Method-объектов.....................................329 8.4.4. Методы определения, отмены определения и присвоения псевдонимов.........................................330 8.4.5. Обработка неопределенных методов..........................332 8.4.6. Установка видимости метода................................333
12 Оглавление 8.5. Перехватчики................................................333 8.6. Трассировка.................................................335 8.7. Модули ObjectSpace и GC.....................................337 8.8. Создание своих собственных управляющих структур.............338 8.8.1. Отложенное и повторяющееся выполнение: after и every....338 8.8.2. Безопасное применение потоков путем синхронизации блоков.....340 8.9. Ненайденные методы и константы..............................341 8.9.1. Работа const_missing с кодовыми константами Юникода.....341 8.9.2. Отслеживание вызовов методов с помощью метода method_missing.342 8.9.3. Объекты, синхронизированные благодаря делегированию..........344 8.10. Динамически создаваемые методы..................................345 8.10.1. Определение методов с помощью class_eval...............346 8.10.2. Определение методов с помощью define method............347 8.11. Выстраивание цепочки псевдонимов...........................348 8.11.1. Отслеживание загрузки файлов и определение новых классов....349 8.11.2. Выстраивание цепочки методов для обеспечения безопасности работы потоков.................................................351 8.11.3. Выстраивание цепочки методов для осуществления отслеживания.354 8.12. Предметно-ориентированные языки............................356 8.12.1. Простой вывод XML с помощью метода method missing......356 8.12.2. Проверяемый вывод XML-кода с помощью генерации метода..358 Глава 9. Платформа Ruby..........................................363 9.1. Строки......................................................364 9.1.1. Форматирование текста...................................369 9.1.2. Упаковка и распаковка двоичных строк....................370 9.1.3. Строки и кодировки......................................371 9.2. Регулярные выражения........................................371 9.2.1. Литералы регулярных выражений...........................371 9.2.2. Фабричные методы регулярных выражений...................373 9.2.3. Синтаксис регулярных выражений..........................374 9.2.4. Определение соответствия шаблону с использованием регулярных выражений......................................................378 9.3. Числа и математические операции.............................385 9.3.1. Числовые методы.........................................385 9.3.2. Модуль Math.............................................386 9.3.3. Десятичная арифметика...................................387 9.3.4. Комплексные числа.......................................388 9.3.5. Рациональные числа......................................388 9.3.6. Векторы и матрицы.......................................389 9.3.7. Случайные числа.........................................390 9.4. Работа с датой и временем...................................390 9.5. Коллекции...................................................393 9.5.1. Перечисляемые объекты...................................393 9.5.2. Массивы.................................................401
Оглавление 13 9.5.3. Хэши.....................................................408 9.5.4. Наборы...................................................414 9.6. Файлы и каталоги.............................................419 9.6.1. Имена файлов и каталогов.................................420 9.6.2. Вывод содержимого каталогов..............................422 9.6.3. Проверка файлов..........................................423 9.6.4. Создание, удаление и переименование файлов...............425 9.7. Ввод и вывод данных..........................................426 9.7.1. Открытие потоков.........................................426 9.7.2. Потоки и кодировки.......................................429 9.7.3. Чтение из потока.........................................431 9.7.4. Запись в поток...........................................435 9.7.5. Методы произвольного доступа.............................437 9.7.6. Закрытие, сброс и тестирование потоков...................437 9.8. Работа в сети................................................439 9.8.1. Самый простой клиент.....................................439 9.8.2. Самый простой сервер.....................................440 9.8.3. Датаграммы...............................................441 9.8.4. Более сложный пример клиента.............................442 9.8.5. Мультиплексированный сервер..............................443 9.8.6. Извлечение веб-страниц...................................445 9.9. Потоки и параллельные вычисления.............................446 9.9.1. Время существования потоков..............................448 9.9.2. Потоки и переменные......................................449 9.9.3. Диспетчеризация потоков..................................451 9.9.4. Состояния потоков........................................452 9.9.5. Составление списка потоков и их группы...................455 9.9.6. Примеры организации потоков..............................455 9.9.7. Исключение потоков и взаимная блокировка.................458 9.9.8. Очередь, и очередь определенного размера.................461 9.9.9. Переменные условий и очереди.............................462 Глава 10. Среда окружения Ruby....................................465 10.1. Вызов Ruby-интерпретатора...................................466 10.1.1. Наиболее востребованные ключи...........................467 10.1.2. Ключи, связанные с предупреждениями и выдачей информации.468 10.1.3. Ключи, относящиеся к кодировке..........................468 10.1.4. Ключи, связанные с обработкой текста....................469 10.1.5. Ключи разного назначения................................470 10.2. Высокоуровневое окружение...................................471 10.2.1. Предопределенные модули и классы........................471 10.2.2. Высокоуровневые константы...............................472 10.2.3. Глобальные переменные...................................474
14 Оглавление 10.2.4. Предопределенные глобальные функции.....................478 10.2.5. Глобальные функции, определенные пользователями.........480 10.3. Сокращения для удобства извлечения данных и составления отчетов.481 10.3.1. Функции ввода...........................................481 10.3.2. Нерекомендуемые функции извлечения данных...............482 10.3.3. Функции составления отчетов.............................482 10.3.4. Сокращения, используемые в однострочных сценариях.......483 10.4. Обращение к операционной системе................................483 10.4.1. Вызов команд операционной системы.......................484 10.4.2. Процессы и ветвления....................................485 10.4.3. Отлавливание сигналов...................................487 10.4.4. Прерывание работы программ..............................487 10.5. Безопасность....................................................488 10.5.1. Помеченные данные.......................................489 10.5.2. Ограниченное выполнение и уровни безопасности...........489
Предисловие Эта книга является обновленной и расширенной версией книги «Ruby in a Nut- shell» (O’Reilly), написанной Юкихиро Мацумото (Yukihiro Matsumoto), более известного как Мац (Matz). Она в чем-то повторяет модель классической книги «The С Programming Language» (Prentice Hall), написанной Брайаном Кернига- ном (Brian Kernighan) и Деннисом Ричи (Dennis Ritchie), и выражает стремление предоставить всестороннюю документацию по языку Ruby, не вдаваясь при этом в формальности спецификации языка. Эта книга написана для опытных програм- мистов, знакомящихся с новым для себя языком Ruby, а также для тех, кто уже программирует на Ruby и хочет достичь следующего уровня понимания и мастер- ства работы на этом языке. Путеводитель по структуре этой книги вы найдете в главе 1. Благодарности Дэвид Фланаган (David Flanagan) Прежде всего я должен поблагодарить Маца за тот прекрасный язык, который он создал, за его помощь в понимании этого языка и за предыдущую книгу- справочник, из которой выросла эта книга. Я также благодарен: О человеку, использующему псевдоним why the lucky stiff за очаровательные ри- сунки, украшающие эти страницы (вы встретитесь с ними на титульных листах глав), и, разумеется, за его собственную книгу по Ruby, «why’s (poignant) guide to Ruby», которую можно найти в Интернете по адресу http://poignantguide.org; О моим техническим рецензентам: Дэвиду Блэку (David A. Black), директо- ру компании Ruby Power and Light, LLC (http://www.rubypal.com); Чарльзу Оливеру Наттеру (Charles Oliver Nutter), представителю команды разработ- чиков JRuby (http://www.jruby .org) из компании Sun Microsystems; Шиохею Юраби (Shyouhei Urabe), специалисту по поддержке ветви Ruby 1.8.6; и Кену Куперу (Ken Cooper). Их отзывы помогли улучшить качество и доходчивость материала книги. Разумеется, любые оставшиеся в тексте ошибки следует от- нести на мой счет;
16 Предисловие О моему редактору, Майку Лоукидзу (Mike Loukides), за то, что он попросил на- писать эту книгу и постоянно вдохновлял меня на работу над ней, а также за его терпение, проявленное в этот период. И в заключение, конечно же, я хочу выразить свою любовь и благодарность своей семье. Дэвид Фланаган http://www.davidflanagan.com Январь 2008 года. Юкихиро Мацумото (Yukihiro Matsumoto) В дополнение к тем людям, о которых упомянул Дэвид (за исключением самого себя), я признателен за помощь всем представителям сообщества по всему миру, особенно представителям Японии: Кончи Сасада (Koichi Sasada), Нобиоши Нака- да (Nobuyoshi Nakada), Акира Танака (Akira Tanaka), Шаго Мэида (Shugo Maeda), Юсаку Накамура (Usaku Nakamura) и Шиохею Юраби (Shyouhei Urabe), при этом я назвал только некоторых из них (не придерживаясь какого-либо определенного порядка). И в заключение я выражаю благодарность своей семье, которая, как я надеюсь, простила своего мужа и отца за то, что он посвятил столько времени разработке языка Ruby. Юкихиро Мацумото Январь 2008 года. Способы оформления, принятые в этой книге В книге используется следующее оформление: О курсив — служит для выделения новых терминов, небольших фрагментов тек- ста и слов с определенной «интонацией»; О шрифт без засечек — применяется для имен файлов и папок, адресов электрон- ной почты и Интернета, имен программных и инструментальных средств; О моноширинный шрифт — предназначен для оформления примеров программного кода, элементов командной строки, типов данных, предложений, операторов, ключевых слов, а также имен классов, объектов, методов, переменных и кон- стант.
Глава 1 Введение
18 Глава 1. Введение Ruby — это язык динамического программирования со сложной, но выразитель- ной грамматикой и базовой библиотекой классов с богатым и мощным API. Ruby вобрал в себя черты таких языков, как Lisp, Smalltalk и Perl, но использует грам- матику, которой без особого труда смогут овладеть программисты, работающие на языках С и Java™. Ruby является абсолютным объектно-ориентированным языком, но в нем также неплохо уживаются процедурные и функциональные сти- ли программирования. Он включает мощные потенциальные возможности для метапрограммирования, позволяющие использовать Ruby для создания языков, предназначенных для работы в конкретных предметных областях (domain-specific languages — DSL). MATZ HA RUBY Юкихиро Мацумото (Yukihiro Matsumoto), известный англоязычному Ruby- сообществу как Мац (Matz), является создателем Ruby и автором справочника по этому языку — «Ruby in a Nutshell» (O'Reilly) (который с обновлениями и дополнениями и превратился в эту книгу). Он говорит: «До создания Ruby я изучил множество языков, но никогда не испытывал от них полного удовлетворения. Они были уродливее, труднее, сложнее или проще, чем я ожидал. И мне захотелось создать свой собственный язык, который смог бы удовлетворить мои программистские запросы. О целевой аудитории, для которой предназначался язык, я знал вполне достаточно, поскольку сам был ее представителем. К моему удивлению, множество программистов по всему миру испытали чувства, сходные с моими. Открывая для себя Ruby и программируя на нем, они получают удовольствие. Разрабатывая язык Ruby, я направил всю свою энергию на ускорение и упрощение процесса программирования. Все свойства Ruby, включая объектную ориентацию, сконструированы так, чтобы при своей работе они оправдывали ожидания средних по классу программистов (например, мои собственные). Большинство программистов считают этот язык элегантным, легкодоступным и приятным для программирования». Основная философия, заложенная Мацумото в конструкцию Ruby, сводится к его часто цитируемому высказыванию: «Ruby предназначен для того, чтобы сделать программистов счаст- ливыми». 1.1. Экскурсия по Ruby Этот раздел служит несколько бессистемным путеводителем по наиболее ин- тересным свойствам Ruby. Все, что в нем обсуждается, будет чуть позже под-
1.1. Экскурсия по Ruby 19 робно рассмотрено в этой книге, но этот начальный обзор даст возможность по- чувствовать красоту этого языка. 1.1.1. Объектная ориентированность Ruby Начнем с того, что Ruby является полностью объектно-ориентированным язы- ком. Каждое значение является объектом, даже простые числовые литералы и значения true, false и nil (ni 1 — это специальное значение, свидетельствующее собственно об отсутствии какого-либо значения; это Ruby-версия null). Давайте применим к этим значениям метод под названием class. В Ruby комментарии на- чинаются с символа #, а стрелки вида => показывают в комментариях значения, возвращенные комментируемым кодом (это соглашение используется по всей книге): 1.class # => Fixnum: число 1 относится к классу Fixnum 0.0.class # => Float: числа с плавающей точкой относятся к классу Float true.class # => TrueClass: true - единственный экземпляр класса TrueClass false.class # => FalseClass nil.class # => Ni1 Cl ass Во многих языках программирования вызовы функций и методов требуют ис- пользования круглых скобок, но ни в одном из вышеприведенных примеров кода их нет. Обычно для Ruby круглые скобки являются необязательными, и зачастую они опускаются, особенно если вызываемый метод не требует аргументов. Отсут- ствие круглых скобок при вызовах методов делает эти вызовы похожими на ссыл- ки на поименованные поля или поименованные переменные объекта. Это сделано намеренно, все дело в том, что Ruby очень строг в отношении инкапсуляции своих объектов — доступ к внутреннему состоянию объекта за его пределами отсутству- ет. Любой подобный доступ должен иметь посредника в виде метода доступа, та- кого как показанный выше метод cl ass. 1.1.2. Блоки и итераторы Возможность вызова методов в отношении целых чисел — это не просто какой- то аспект Ruby, известный лишь посвященным. Им довольно часто пользуются Ruby-программисты: 3.times { print "Ruby! " } # Выводит "Ruby! Ruby! Ruby! " l.upto(9) {|x| print x } # Выводит "123456789" times и upto — это методы, выполняемые в отношении целочисленных объектов. Они представляют собой особую разновидность методов, известную как итерато- ры, и ведут себя как циклы. Код, помещенный в фигурные скобки, — известный как блок — связан с вызовом метода и служит в качестве тела цикла. Использо- вание итераторов и блоков — еще одно примечательное свойство языка Ruby; хотя язык поддерживает обычный цикл while, большее распространение получила
20 Глава 1. Введение реализация циклов с использованием структурных компонентов, являющихся по сути вызовами методов. Целые числа — это не только значения, имеющие методы-итераторы. В массивах (и им подобных «перечисляемых» объектах) определен итератор по имени each, который однократно вызывает связанный с ним блок для каждого элемента мас- сива. Каждому вызову блока передается отдельный элемент массива: а = [3. 2. 1] а[3] = а[2] - 1 a.each do |elt| print elt+1 end # Это массив литералов # Квадратные скобки используются для запроса # и установки значений # элемента массива # each является итератором. Блок имеет параметр elt # и выводит "4321" # Вместо скобок {} в качестве ограничителей # блока использована # пара do-end В дополнение к each определены многие другие полезные итераторы: а = [1.2.3.4] b = а.тар {|х| х*х } с = a.select {|х| х£2==0 } a.inject do |sum,x| sum + x end # Сначала задается массив # Возведение элементов в квадрат: Ь - это [1,4,9.16] # Выбор четных элементов: с - это [2.4] # Вычисление суммы всех элементов => 10 Хэши, подобно массивам, являются в Ruby основной структурой данных. В со- ответствии со своим названием, они построены на структуре данных в виде хэш- таблицы и служат для отображения произвольных объектов-ключей на объ- екты-значения. (Иначе говоря, хэш связывает произвольные объекты-значения с объектами-ключами.) В хэшах, как и в массивах, при работе с элементами для запроса и присваивания им значений используются квадратные скобки. При этом ожидается, что вместо целочисленных индексов в квадратных скобках будут на- ходиться объекты-ключи. Подобно классу Array, в классе Hash так же определяется метод-итератор each. Этот метод однократно вызывает связанный с ним блок кода для каждой имеющейся в хэше пары ключ-значение и (в отличие от Array) пере- дает блоку в качестве параметров и ключ и значение: h = { :one => 1. :two => 2 } h[:one] h[:three] = 3 h.each do |key,value| print "#{value}:#{key}: end # Хэш, отображающий названия чисел на цифры # "стрелки" показывают отображения: ключ=>значение # двоеточие указывает, что это литералы обозначений # => 1. Доступ к значению по ключу # Добавление к хэшу новой пары ключ-значение # Перебор всех пар ключ-значение # Обратите внимание, что значения подставляются # в строку # Выводит "1:опе: 2:two: 3:three: "
1.1. Экскурсия по Ruby 21 В качестве ключа имеющиеся в Ruby хэши могут использовать любой объект, но чаще всего используются объекты типа обозначение — Symbol. Обозначения — это неизменяемые, изолированные строки. Они могут сравниваться не по текстовому наполнению, а по идентичности (поскольку два различных объекта-обозначения никогда не будут иметь одинаковое содержимое). Возможность связывать блок кода с вызовом метода — основное и очень мощное свойство Ruby. Хотя его применение наиболее очевидно для циклических кон- струкций, оно не менее полезно и для методов с однократным вызовом блока. На- пример: File.openCdata.txt") do [f| # Открытие указанного файла # и передача потока данных в блок line = f.readline end # Поток используется для чтения из файла # и автоматически закрывается, # когда заканчивается блок t = Thread.new do File.read("data.txt") End # Этот блок запускается в новом потоке # Чтение файла в фоновом режиме # Содержимое файла доступно как значение потока Отдельно следует заметить, что приведенный ранее пример использования мето- да Hash.each содержал следующую, довольно интересную строку кода: print "#{va1ue}:#{key}; " # Обратите внимание, что значения подставляются # в строку Строка, заключенная в двойные кавычки, может включать в себя произвольные Ruby-выражения, ограниченные группой символов #{ и символом }. Значение вы- ражения, помещенного в эти ограничители, преобразуется в строку (путем вызо- ва метода to_s, поддерживаемого всеми объектами). Получившаяся в результате этого строка используется для замены текста выражения и его ограничителей на строковый литерал. Эта подстановка значения выражения в строку обычно назы- вается вставкой строки. 1.1.3. Выражения и операторы Ruby Синтаксис Ruby ориентирован на использование выражений. Такие управляю- щие структуры, как i f, которые в других языках программирования назывались бы операторами, в Ruby представляют собой выражения. У них, как и у других, более простых выражений, есть значения, позволяющие написать следующий код: minimum = if х < у then х else у end Хотя все «операторы» в Ruby по своей сути являются выражениями, не все из них возвращают содержательные значения. К примеру, циклы while и определе- ния методов являются выражениями, которые, как правило, возвращают значе- ние nil.
22 Глава 1. Введение Как и во многих других языках, выражения в Ruby обычно выстраиваются из значений и операторов. Большинство использующихся в Ruby знаков операций знакомы всем, кто знает Си, Java, JavaScript или подобные им языки програм- мирования. Посмотрите на примеры самых обычных и самых необычных Ruby- операторов: 1 + 2 1 * 2 1 + 2 == 3 2 ** 1024 "Ruby" + " rocks!" "Ruby! ” * 3 "W Ss" % [3. "rubies"] max = x > у ? x : у # => 3: сложение # => 2: умножение # => true: == проверка равенства # 2 в степени 1024: в Ruby произвольная размерность # целых чисел # => "Ruby rocks!": объединение строк # => "Ruby! Ruby! Ruby!": повторение строки # => "3 Rubies": Форматирование в стиле имеющегося # в Python оператора printf # Условный оператор Многие Ruby-операторы реализованы в виде методов, и классы могут определять (или переопределять) эти методы как угодно. (Но они не могут определять со- вершенно новые операторы; существует лишь фиксированный набор общепри- нятых операторов.) Обратите, к примеру, внимание, что знаки операций + и * для целых чисел и строк ведут себя по-разному. Но в своих собственных классах вы можете определить эти операторы как угодно. Другим подходящим примером может послужить оператор «. Целочисленные классы Fixnum и Bignum, следуя правилам, принятым в языке программирования Си, используют этот оператор для операции поразрядного сдвига влево. В то же время (следуя C++) другие классы — строки, массивы и потоки — используют этот оператор для операции добавления. Если вы создаете новый класс, способный иметь значения, которые каким-то образом к нему добавляются, то неплохо было бы определить для него оператор «. Одним из самых мощных переопределяемых операторов является []. Классы Array и Hash используют этот оператор для доступа к элементам массива по индек- су и значениям хэша по ключу. Но в своих классах вы можете определить [ ] для чего угодно. Его можно даже определить в качестве метода, ожидающего исполь- зование нескольких аргументов, которые заключены в квадратные скобки и разде- лены запятыми. (Для указания подмассива или «вырезки» из массива класс Array воспринимает индекс и длину, заключенные в квадратные скобки.) И если нуж- но, чтобы квадратные скобки использовались в левой части выражения присваи- вания, то можно определить соответствующий оператор []=. Значение в правой части присваивания будет передано в качестве конечного аргумента для метода, являющегося реализацией этого оператора. 1.1.4. Методы Методы определяются с помощью ключевого слова def. Возвращаемым значени- ем метода является то значение, которое вычисляется в его теле последним:
1.1. Экскурсия по Ruby 23 def square(x) # Определение метода по имени square # с единственным параметром х х*х # Возвращение х, возведенного в квадрат end # Завершение метода Когда метод, подобный этому, определен за пределами класса или модуля, он фактически является глобальной функцией, а не методом, вызываемым для объ- екта. (Но с технической точки зрения такой метод становится закрытым методом класса Object.) Методы также могут быть определены для единичных объектов за счет указания перед именем метода имени объекта, для которого он определяет- ся. Подобные методы известны как синглтон (singleton) методы (методы, опреде- ленные в единственном экземпляре), и именно так в Ruby определяются методы класса: def Math.square(x) # Определение метода класса для модуля Math х*х end Модуль Math является частью базовой библиотеки Ruby, и этот код добавляет к ней новый метод. Здесь проявляется ключевое свойство Ruby — классы и мо- дули являются «открытыми» и могут быть модифицированы и расширены в про- цессе работы. Параметры метода могут иметь определенные значения по умолчанию, и методы могут воспринимать произвольное количество параметров. 1.1.5. Присваивания Имеющийся в Ruby оператор = (не подлежащий переопределению) присваивает значение переменной: х = 1 Присваивание может сочетаться с другими операторами, такими как + и -: х += 1 # Приращение х: учтите, что в Ruby нет оператора ++. у -= 1 # Уменьшение у: также нет и оператора --. Ruby поддерживает параллельные присваивания, позволяя использовать в выра- жении присваивания более одного значения и более одной переменной: х, у = 1, 2 # То же самое, что и х = 1; у = 2 а, b = Ь, а # Две переменные обмениваются значениями x.y.z = [1,2,3] # Значения элементов массива автоматически # присваиваются переменным Методам в Ruby позволено возвращать более одного значения, и в таких методах можно с пользой применить параллельное присваивание. Например: # Определение метода для преобразования декартовых координат (х.у) в полярные def polar(x.y) theta = Math.atan2(y,x) # Вычисление угла , продолжение
24 Глава 1. Введение г = Math.hypot(х,у) # Вычисление расстояния [г, theta] # Последнее выражение является возвращаемым значением end # А так мы используем этот метод с параллельным присваиванием, то distance, angle = polar(2,2) Методы, заканчивающиеся знаком равенства (=), являются специализированны- ми, поскольку Ruby позволяет им быть вызванными с использованием синтак- сиса присваивания. Если у объекта о есть метод по имени х=, то следующие две строки программного кода делают одно и то же: о,х=(1) # Обычный синтаксис вызова метода о.х =1 # Вызов метода через присваивание 1.1.6. Суффиксы и префиксы, состоящие из знаков пунктуации Мы уже видели, что методы, чьи имена заканчиваются на =, могут быть вызваны в качестве выражения присваивания. Имена Ruby-методов могут также закан- чиваться вопросительным или восклицательным знаком. Вопросительный знак используется для обозначения предикатов — методов, которые возвращают бу- лево значение. Например, в классах Array и Hash определены методы по имени empty?, которые проверяют наличие в структуре данных каких-нибудь элементов. Восклицательный знак в окончании имени метода служит признаком того, что при использовании метода следует соблюдать особую осторожность. В некото- рых базовых классах Ruby определены пары методов с одинаковыми именами с той лишь разницей, что у одного имени стоит в окончании восклицательный знак, а у другого — нет. Обычно метод, не имеющий восклицательного знака, возвращает модифицированную копию объекта, для которого он вызван, а ме- тод с восклицательным знаком является методом-мутатором, который изменяет непосредственно сам объект. К примеру, у класса Array определены методы sort и sort!. В дополнение к символам пунктуации в конце имен методов такие же символы будут встречаться и в начале имен переменных Ruby: глобальные переменные имеют префикс $, переменные экземпляров — префикс @, а переменные класса — префикс Возможно, к этим префиксам нужно будет привыкать, но через неко- торое время вы сможете по достоинству оценить тот факт, что префикс сообщает об области действия переменной. Префиксы нужны для устранения неоднознач- ностей очень гибкой грамматики Ruby. Различные префиксы могут рассматри- ваться в качестве платы за возможность избавиться от круглых скобок при вы- зовах методов.
1.1. Экскурсия по Ruby 25 1.1.7. Регулярные выражения и числовые диапазоны Мы уже упоминали, что массивы и хэши являются в Ruby основными структу- рами данных. Мы также продемонстрировали использование чисел и строк. Но нашего внимания заслуживают еще два типа данных. Объекты Regexp (регулярные выражения) описывают текстовые шаблоны и располагают методами для опреде- ления, соответствует данная строка этому шаблону или нет. А объекты Range пред- ставляют значения (обычно целые числа), располагающиеся между двумя ко- нечными точками. В Ruby регулярные выражения и числовые диапазоны имеют литеральный синтаксис: /[Rr]uby/ # Соответствует "Ruby" или "ruby" /\d{5}/ # Соответствует пяти последовательным цифрам 1. .3 # Все х, где 1 <= х <= 3 1.. .3 # Все х. где 1 <= х < 3 В объектах Regexp и Range определен обычный оператор ==, предназначенный для проверки равенства. В дополнение к нему в этих объектах также определен оператор === для проверки соответствия и принадлежности к группе элементов. Имеющийся в Ruby оператор case (похожий на оператор switch в Си или Java), сравнивает свои выражения со всеми возможными случаями, используя оператор ===, поэтому этот знак операции часто называют оператором case-равенства. С его помощью можно проверить выполнения следующих условий: # Определение названия поколения жителей США на основе их года рождения # Выражение case проверяет числовые диапазоны с помощью === generation = case birthyear when 1946..1963: "Поколение всплеска рождаемости" when 1964..1976: "Поколение X" when 1978..2000: "Поколение Y" el se nil end # Метод, запрашивающий у пользователя подтверждение def are_you_sure? # # while true # print "Вы уверены? [y/n]: " # response = gets # case response # when /A[yY]/ # return true # when /A[nN]/, /А$/ # return false # end Определение метода. Обратите внимание на вопросительный знак! Цикл до явного возврата Вопрос пользователю Получение его ответа Начало условного оператора case Если ответ начинается с у или Y, метод возвращает true Если ответ начинается с n, N или он пустой, метод возвращает false end end
26 Глава 1. Введение 1.1.8. Классы и модули Класс является коллекцией родственных методов, которые оперируют состоя- нием объекта. Состояние объекта содержится в его переменных экземпляра, тех самых переменных, чье имя начинается с символа О и чьи значения характерны для данного конкретного объекта. В следующем программном коде определяется пример класса по имени Sequence (последовательность) и демонстрируется, как написать методы итерации и определить операторы: # # Этот класс представляет последовательность чисел, характеризующуюся тремя # параметрами: from, to и by (от, до и через). Числа х в этой последовательности # удовлетворяют следующим двум ограничениям: # # from <= х <= to # х = from + n*by, где n - целое число # class Sequence # Это класс относится к перечисляемым - enumerable: это определяется # приведенным ниже итератором each. include Enumerable # Включение в класс методов этого модуля # Метод initialize играет особую роль: он вызывается автоматически для # инициализации заново создаваемых экземпляров класса def initialize(from, to, by) # Наши параметры просто сохраняются в переменных экземпляра для их # дальнейшего использования Ofrom, Oto, Oby = from, to. by # Обратите внимание на параллельное # присваивание и на префикс О end # Этот итератор востребован модулем Enumerable def each х = Ofrom # Старт в начальной точке while х <= Oto # Пока мы не достигли конечной точки yield х # Передача х блоку, связанному с итератором х += Oby # Приращение х end end # Определение метода length (следующего за массивами) для возвращения # количества значений в последовательности def length return 0 if Ofrom > Oto # Заметьте, что if используется в качестве # оператора-модификатора Integer((Oto-Ofrom)/Oby) +1 # Вычисление и возвращение длины # последовательности end
1.1. Экскурсия по Ruby 27 # Определение другого имени для одного и того же метода. # Наличие нескольких имен у Ruby-методов - вполне обычная практика alias size length # Теперь size является синонимом length # Переопределение оператора доступа к элементам массива, чтобы предоставить # произвольный доступ к элементам sequence def[](index) return nil if index < 0 # Возвращение nil для отрицательных индексов v = @from + index*@by # Вычисление значения if v <= @to # Если оно является частью последовательности v # Вернуть его else # Если нет... nil # Вернуть nil end end # Переопределение арифметических операторов для возвращения новых объектов # Sequence def *(factor) Sequence.new(@from*factor, @to*factor, @by*factor) end def +(offset) Sequence.new(@from+offset, @to+offset, @by) end end Примеры программного кода, в которых используется класс Sequence: s = Sequence.new(l. 10. 2) # От 1 до 10 через 2 s.each {|х| print х } # Выводит "13579" print s[s.size-1] # Выводит 9 t = (s+l)*2 # От 4 до 22 через 4 Основным свойством нашего класса Sequence является его итератор each. Если нас интересует только метод-итератор, то определять целый класс не нужно. Вместо этого мы можем просто написать метод-итератор, воспринимающий параметры from, to и by. Вместо создания глобальной функции, давайте определим его в каче- стве самостоятельного модуля: module Sequences # Начинаем создавать новый модуль def seif.fromtoby(from, to. by) # синглтон-метод модуля x = from while x <= to yield x x += by end end end
28 Глава 1. Введение С определенным таким образом итератором мы можем написать следующий код: Sequences, fromtobyd, 10, 2) {|х| print х } # Выводит "13579" Подобный итератор избавляет от необходимости создания объекта Sequence для осуществления перебора числовой последовательности. Но имя метода получает- ся слишком длинным, и синтаксис его вызова нас тоже мало устраивает. Нам ну- жен был всего лишь способ перебора числовых объектов Range с шагом, отличным от единицы. В Ruby есть одно очень любопытное свойство — даже его встроенные, базовые классы являются открытыми: любая программа может добавить к ним свои методы. Поэтому у нас есть реальная возможность определить новый метод- итератор для числовых диапазонов: class Range def by(step) x = self.begin if exclude_end? while x < self.end yield x x += step end else while x <= self.end yield x x += step end end end end # Открываем существующий класс для добавлений # Определяем итератор по имени by # Начинаем с одного конца диапазона # Для диапазонов типа .... которые исключают # конечную точку, проверяем с помощью оператора < # А для диапазонов ... включающих конечную точку, # проверяем с помощью оператора <= # Завершение определения метода # Завершение модификации класса # Выводит "0246810" # Выводит "02468" # Примеры (0..10).by(2) {|х| print х} (0...10).by(2) {|х| print х} Созданный нами метод by удобен, но не нужен; в классе Range уже определяет- ся итератор по имени step, который служит для этой же цели. В базовом Ruby API заложены довольно большие возможности, поэтому на изучение основ (из- ложенных в главе 9) стоит потратить время, чтобы не пришлось заново изобре- тать велосипед, понапрасну тратя время на создание уже реализованных для вас методов! 1.1.9. Сюрпризы Ruby В каждом языке есть свойства, обескураживающие неискушенных в нем програм- мистов. Рассмотрим два удивительных свойства Ruby. Строки в Ruby обладают изменчивостью, что может, в частности, удивить Java- программистов. Оператор []= позволяет изменять символы строки или вставлять,
1.2. Опробование Ruby 29 удалять или заменять подстроки. Оператор « позволяет дополнять строку, а в классе String определяется ряд других методов, которые осуществляют непо- средственное изменение самой строки. Поскольку строки изменчивы, строковые литералы в программе не являются уникальными объектами. Если вы поставите строковый литерал внутри цикла, он будет вычисляться в но- вый объект при каждой итерации этого цикла. Чтобы предотвратить любые буду- щие изменения строки или любого другого объекта, в отношении этого объекта нужно вызвать метод freeze (заморозить). В условиях и циклах Ruby (к которым относятся 1 f и whi1 е), чтобы определить, ка- кую ветвь вычислять, или же определить, продолжать или нет цикл, вычисляют- ся условные выражения. Чаще всего они вычисляются в значения true или false, но необязательно именно в них. Значение nj 1 рассматривается как fal se, а любое другое значение — как true. Это, скорее всего, удивит программистов, работаю- щих на Си, ожидающих, что 0 будет рассматриваться как false, и программистов JavaScript, ожидающих, что пустая строка "" будет рассматриваться как false. 1.2. Опробование Ruby Мы надеемся, что ознакомительный тур по свойствам Ruby возбудил ваш интерес и вызвал желание испытать Ruby в деле. Для этого понадобится интерпретатор Ruby и вдобавок к нему знания о том, как пользоваться тремя инструментальны- ми средствами: irb, ri и gem, которые поставляются в одном пакете с интерпретато- ром. В этом разделе объясняется, как их получить и использовать. 1.2.1. Интерпретатор Ruby Официальный веб-сайт Ruby — http://www.ruby-lang.org. Если Ruby еще не уста- новлен на вашем компьютере, то с главной страницы ruby-lang.org можно просле- довать по ссылке загрузки (download) и получить инструкции по загрузке и уста- новке стандартной, созданной на Си эталонной реализации Ruby. После установки Ruby интерпретатор можно вызвать командой ruby: > ruby -е 'puts "hello world!"' hello world! Параметр командной строки -e заставляет интерпретатор выполнить указанную одиночную строку Ruby-кода. Но в большинстве случаев вам придется размещать свою Ruby-программу в файле и давать указание интерпретатору вызвать этот файл: > ruby hello.rb hello world!
30 Глава 1. Введение ДРУГИЕ РЕАЛИЗАЦИИ RUBY В отсутствие формальной спецификации языка Ruby интерпретатор Ruby с веб-сайта ruby-lang.org является эталонной реализацией, которая определяет сам язык. Иногда ее называют реализацией Ruby от самого Маца — MRI, или «Matz Ruby Implementation». В Ruby 1.9 исходный MRI-интерпретатор был объединен с YARV («Yet Another Ruby Virtual machine» — новой виртуальной машиной Ruby), чтобы создать новую эталонную реализацию, выполняющую внутреннюю компиляцию в байт-код, а затем выполняющую этот байт-код на виртуальной машине. Но доступна не только эталонная реализация. Когда писались эти строки, существовала как альтернативная реализация, дове- денная до уровня выпуска 1.0 (JRuby), так и несколько других реализаций, находящихся в процессе разработки: JRuby JRuby является созданной на Java реализацией Ruby, которая доступна на веб- сайте http://jruby.org. На момент работы над книгой была выпущена версия JRuby 1.0, совместимая с Ruby 1.8. А версия JRuby совместимая с Ruby 1.9 вполне может быть доступна на момент чтения этих строк. JRuby является программным обеспечением с открытым кодом, первоначально разработанным в Sun Microsystems. IronRuby IronRuby — это реализация Ruby, созданная компанией Microsoft для .NET framework и DLR (Dynamic Language Runtime). Исходный код доступен по разрешительной лицензии Microsoft Permissive License. На момент работы над книгой IronRuby еще не был доведен до версии 1.0. Домашняя веб-страница проекта — http://www.ironruby.net. Rubinius Rubinius является проектом с открытым кодом, в описании которого ска- зано: «альтернативная реализация Ruby, написанная преимущественно на самом Ruby. Виртуальная машина Rubinius, названная shotgun, в общих чертах основана на архитектуре Smalltalk-80 VM». На момент работы над книгой Rubinius еще не был доведен до версии 1.0. Домашняя веб-страница проекта Rubinius — http://rubini.us. Cardinal Cardinal является реализацией Ruby, предназначенной для запуска на вирту- альной машине Parrot VM (целью которой было усиление мощности Perl 6 и ряда других динамических языков). На момент работы над книгой ни Parrot, ни Cardinal так и не были доведены до версии 1.0. У Cardinal нет собственной веб-страницы; сведения об этой реализации размещаются в виде части проекта с открытым кодом Parrot по адресу http://www.parrotcode.org. 1.2.2. Отображение вывода Для испытания свойств Ruby нужен способ для отображения вывода, чтобы ваши тестовые программы смогли вывести результаты своей работы. Функ- ция puts, которая ранее была задействована в коде "hello world" — это один из
1.2. Опробование Ruby 31 способов вывода результатов. Если не вдаваться в подробности, то puts выводит строку текста на консоль и добавляет к ней разделитель строк (если строка уже не оканчивалась одним из таких разделителей). Если функции передан объект, не являющийся строкой, puts вызывает метод to_s, определенный в этом объекте, и выводит строку, возвращенную этим методом. Функция print делает прибли- зительно то же самое, но разделителя строки не добавляет. Наберем, к примеру, в текстовом редакторе следующую программу из двух строк и сохраним ее в фай- ле по имени count.rb: 9.downto(l) {|n| print n } # Разделитель строк между числами отсутствует puts ” blastoff!" # Заканчивается разделителем строк Теперь запустим программу в нашем Ruby-интерпретаторе: > ruby count.rb У программы должен быть следующий вывод: 987654321 blastoff! Функция р может показаться вам весьма полезной альтернативой функции puts. Ее не только проще набирать, но она также преобразует объекты в строки при по- мощи метода inspect, который иногда возвращает более подходящее для програм- миста представление, чем функция to_s. К примеру, при отображении массива функция р выводит его в нотации литерала массива, в то время как функция puts просто выводит каждый элемент массива в отдельной строке. 1.2.3. Интерактивный Ruby с irb irb (сокращение для интерактивного Ruby — «interactive Ruby») — это командный процессор Ruby. Наберите в его командной строке любое Ruby-выражение, и он его вычислит и выведет для вас полученное значение. Чаще всего это самый про- стой способ испытания свойств языка, о которых вы прочтете в этой книге. При- мер сеанса работы с i rb с дополнительными примечаниями выглядит следующим образом: > irb --simple-prompt » 2**3 => 8 » "Ruby! " * 3 => "Ruby! Ruby! Ruby! " » l.upto(3){|x| puts x } 1 2 3 => 1 » quit # Запуск irb из терминала # Попытка возведения в степень # Получившийся результат # Попытка повтора вывода строки # Получившийся результат # Попытка использования итератора # Три строки вывода, # поскольку мы вызываем puts три раза # Возвращаемое значение l.upto(3) # Выход из irb # Возвращение к командной строке
32 Глава 1. Введение Этот пример рабочего сеанса показывает все, что нужно знать о irb, чтобы вос- пользоваться этим средством для исследования Ruby. Но в irb есть и другие по- лезные свойства, включая дочерний командный процессор (для запуска которого в командной строке процессора следует набрать 1 rb) и возможности изменения конфигурации. 1.2.4. Просмотр документации по Ruby с помощью ri Другим ценным инструментальным средством Ruby является средство просмо- тра документации ri1. Если в командной строке вызвать ri, указав имя Ruby-класса, модуля или метода, то будет выведена соответствующая документация. Можно указать имя метода, не уточняя имени класса или модуля, тогда в результате будет показан лишь список всех методов с этим именем (если только этот метод не явля- ется уникальным). Обычно имя класса или модуля можно отделить от имени ме- тода точкой. Если в классе определяется метод класса и метод экземпляра с одним и тем же именем, то вместо точки нужно использовать двойное двоеточие (::) для ссылки на метод класса или знак решетки (#) для ссылки на метод экземпляра. Посмотрите на несколько примеров вызова ri: ri Array ri Array.sort ri Hash#each ri Math::sqrt Отображаемая ri документация извлекается из специально отформатированных комментариев исходного кода Ruby. Подробности изложены в разделе 2.1.1.2. 1.2.5. Управление пакетом программ Ruby с помощью gem Система управления пакетами программ Ruby известна как RubyGems, а пакеты или модули, распространяемые с помощью RubyGems, называются gem-пакетами, или геммами. RubyGems упрощает процесс установки программного обеспечения Ruby и может автоматически управлять сложными взаимосвязями пакетов. Сценарий внешнего интерфейса RubyGems называется gem и распространяется с Ruby 1.9, так же как irb и ri. В Ruby 1.8 его нужно устанавливать отдельно — см. http://rubygems.org. После установки программы gem ею можно воспользовать- ся следующим образом: # gem install rails Successfully installed activesupport-1.4.4 1 По поводу того, что означает «ri», мнения разделяются. Это средство называют «Ruby In- dex», «Ruby Information» и «Ruby Interactive».
1.2. Опробование Ruby 33 Successfully installed activerecord-1.15.5 Successfully installed actionpack-1.13.5 Successfully Installed actionmailer-1.3.5 Successfully Installed actionwebservice-1.2.5 Successfully installed rails-1.2.5 6 gems Installed Installing ri documentation for activesupport-1.4.4... Installing ri documentation for activerecord-1.15.5... ...и t. д.... Как видно из примера, команда gem i nstal 1 устанавливает самые последние версии запрошенных вами gem-пакетов, а также устанавливает ряд gem-пакетов, необхо- димых для работы востребованного gem-пакета. У дет есть ряд полезных подко- манд. Посмотрим несколько примеров: gem list дет enviroment дет update rails дет update дет update --system дет uninstall rails # Выводит список установленных дет-пакетов # Отображает конфигурационную информацию RubyGems # Обновляет указанный дет-пакет # Обновляет все установленные дет-пакеты # Обновляет саму систему RubyGems # Удаляет все установленные дет-пакеты В Ruby 1.8 устанавливаемые вами gem-пакеты не могут быть автоматически за- гружены с помощью Ruby-метода require. (Чтобы узнать о загрузке модулей с кодом Ruby при помощи метода require, нужно обратиться к разделу 7.6.) Если вы пишите программу, которая будет использовать модули, установленные как gem-пакеты, сначала нужно затребовать модуль rubygems. Некоторые пакеты рас- пространения Ruby 1.8 уже содержат в своей конфигурации библиотеку RubyGems, но вам может потребоваться самостоятельно провести его загрузку и установку. Загрузка модуля rubygems изменяет сам метод requi re, и он перед поиском в стан- дартной библиотеке проводит поиск в наборе установленных gem-пакетов. Мож- но также автоматически разрешить поддержку RubyGems, запустив Ruby в команд- ной строке с параметром - rubygems. А если добавить - rubygems к переменной среды RUBYOPT, то библиотека RubyGems будет загружаться при каждом вызове Ruby. В Ruby 1.9 модуль rubygems является частью стандартной библиотеки, и эта версия уже не требует загрузки gem-пакетов. Версия Ruby 1.9 сама знает, как найти уста- новленные gem-пакеты, и для использования gem-пакетов уже не нужно встав- лять в программу requi re ' rubygems'. Когда для загрузки gem-пакета используется requi re (как в версии 1.8, так и в вер- сии 1.9), то загружается самая последняя установленная версия указанного gem- пакета. Если есть более конкретные требования к версии, то перед вызовом require можно воспользоваться методом дет. Будет найдена и «активирована» версия gem-пакета, соответствующая указанным ограничениям, и последующий вызов метода requi re приведет к загрузке этой версии: require ’rubygems' # В Ruby 1.9 эта строка не обязательна gem 'RedCloth'. > 2.0', '< 4.0’ # Активация RedCloth версии 2.x или 3.x require 'RedCloth' # И загрузка этой версии
34 Глава 1. Введение Дополнительные сведения о методе requi ге и gem-пакетах можно найти в разде- ле 7.6.1. Полное описание RubyGems, программы дет и модуля rubygems не вписыва- ется в формат этой книги. Команда gem имеет собственную документацию, которая выводится командой gem help. Для более подробного описания метода gem можно воспользоваться командой ri gem. А для полной детализации следует обратиться к документации на веб-сайте http://rubygems.org. 1.2.6. Дополнительные учебные пособия по Ruby Эта глава началась с учебного введения в язык Ruby. На работу примеров про- граммного кода из этого введения можно посмотреть с помощью irb. Если перед более глубоким погружением в язык вам нужны дополнительные вводные руко- водства, т. е. два неплохих источника, доступных по следующей ссылке на глав- ную страницу: http://www.ruby-lang.org. Одно из учебных пособий, основанное на применении irb, называется «Ruby in Twenty Minutes»1. Другое учебное пособие, названное «Try Ruby!», интересно тем, что оно работает на вашем веб-браузере и не требует установки Ruby или irb на вашей системе1 2. 1.2.7. Источники Ruby На веб-сайте Ruby (http://www.ruby-lang.org) можно найти ссылки на другие ре- сурсы Ruby — интерактивную документацию, библиотеки, рассылки, блоги, интернет-чаты, группы пользователей и обмен мнениями. С главной страницы можно перейти по ссылкам Documentation (Документация) , Libraries (Библиотеки) и Community (Сообщество). 1.3.О книге Как следует из названия, эта книга призвана охватить аспекты языка программи- рования Ruby и претендует на роль всестороннего и доступного описания. Это издание книги касается версий языка 1.8 и 1.9. Ruby размывает границы между языком и платформой, и поэтому наша сфера рассмотрения языка включает под- робный обзор базового Ruby API. Но эта книга не является справочным руко- водством по API и не охватывает базовые классы в полном объеме. Она также не является ни пособием по рабочим средам Ruby (наподобие Rails), ни описанием Ruby-инструментария (наподобие rake и gem). 1 Когда шла работа над книгой, прямым URL для этого учебного пособия был http://www. ruby-lang.org/en/documentation/quickstart/ 2 Ссылку на «Try Ruby!» можно найти на главной веб-странице Ruby по URL: http://tryruby. hobix.com
1.3. О книге 35 Эта глава завершается богатым на комментарии пространным примером, демон- стрирующим не вполне обычную Ruby-программу. Следующие главы охватывают все аспекты Ruby по возрастающей. О Глава 2 раскрывает лексическую и синтаксическую структуру Ruby, включая такие основные вопросы, как набор знаков, чувствительность к регистрам и за- резервированные слова. О Глава 3 охватывает разновидности данных — числа, строки, диапазоны, массивы и т. д., — с которыми могут работать Ruby-программы, и раскрывает основные свойства всех Ruby-объектов. О Глава 4 охватывает элементарные Ruby-выражения — литералы, ссылки на пере- менные, вызовы методов и присваивания, — и в ней объясняются операторы, используемые для объединения элементарных выражений в составные. О Глава 5 объясняет условия, циклы (включая блоки и методы-итераторы), ис- ключения и другие Ruby-выражения, которые в других языках будут называть- ся операторами или управляющими структурами. О Глава 6 формально документирует определение Ruby-метода и синтаксис вы- зова, а также охватывает вызываемые объекты, известные как Ргос-объекты и лямбды. В этой главе объясняются замкнутые выражения и исследуется при- менение в Ruby технологии функционального программирования. О Глава 7 объясняет, как в Ruby определяются классы и модули. Классы являются основой объектно-ориентированного программирования, и в этой главе также охватываются такие темы, как наследование, видимость метода, подмешиваемые миксин-модули и алгоритм разрешения имени метода. О Глава 8 охватывает составляющие Ruby API, позволяющие программе прово- дить самоанализ и самоуправление, а затем в ней демонстрируется техноло- гия метапрограммирования, которая использует эти составляющие API для упрощения процесса программирования. Глава содержит пример предметно- ориентированного языка (domain-specific language, DSL) . О Глава 9 охватывает наиболее важные классы и методы базовой Ruby-платформы с демонстрацией простых фрагментов кода. Это не справочник, а скорее под- робный обзор базовых классов. Темы включают в себя обработку текста, вы- числение числовых выражений, использование коллекций (к которым отно- сятся массивы и хэши), использование операций ввода-вывода, работу в сети и потоки. После прочтения этой главы вы почувствуете всю широту Ruby-платформы и сможете использовать для более глубоких ее исследований инструменталь- ное средство ri, или интернет-справочник. О Глава 10 охватывает среду Ruby-программирования верхнего уровня, включая глобальные переменные и глобальные функции, аргументы командной строки, поддерживаемые Ruby-интерпретатором, и механизм безопасности Ruby.
36 Глава 1. Введение 1.3.1. Как читать эту книгу Программировать на Ruby довольно легко, но сам Ruby — язык непростой. По- скольку в этой книге приводится подробная документация по Ruby, то книга тоже непростая (хотя мы надеемся, что ее содержимое будет легко читаться и воспри- ниматься). Она предназначена для опытных программистов, которые хотят овла- деть Ruby и готовы во имя достижения этой цели к внимательному и вдумчивому чтению. Как и все подобные книги по программированию, эта книга содержит по всему тексту ссылки на предыдущий и последующий материал. Языки программиро- вания не являются линейными системами, и составить по ним линейную доку- ментацию просто невозможно. Из краткого содержания глав можно понять, что в этой книге принят подход к изучению Ruby от простого к сложному: книга начинается с простейших элементов Ruby-грамматики и последовательно доби- рается до документирования высокоуровневых синтаксических структур — от лексем к значениям, затем к выражениям и управляющим структурам, к методам и классам. Этот классический подход к документированию языков программи- рования все же не позволяет избежать проблем, связанных со ссылками на по- следующий материал. Книга предназначена для чтения в том порядке, в котором она написана, но с не- которыми забеганиями вперед лучше ознакомиться поверхностно или пропустить при первом чтении; они обретут куда больший смысл, когда вы к ним вернетесь после прочтения следующих глав. С другой стороны, не стоит бояться ссылок на предстоящий материал. Многие из них указаны с сугубо ознакомительной целью, давая понять, что подробности будут представлены чуть позже. Наличие ссылок еще не означает, что рассматриваемые далее подробности вопроса необходимы для понимания текущего материала. 1.4. Решение головоломки Судоку на Ruby Чтобы дать вам более наглядное представление о том, как обычно выглядит программа на языке Ruby, эта глава завершается довольно непростым Ruby- приложением. В качестве сравнительно небольшой по размеру программы, с по- мощью которой можно продемонстрировать ряд свойств Ruby мы выбрали реше- ние головоломки Судоку1. Не ждите, что в примере 1.1 все сразу станет понятным, 1 Судоку представляет собой логический пазл размером 9x9 клеток, часть из которых пуста, а часть заполнена цифрами. Нужно заполнить все пустые клетки цифрами от 1 до 9, чтобы ни одна из строк или столбцов или внутренний квадрат 3x3 не включали одну и ту же циф- ру дважды. Какое-то время головоломка Судоку была популярна в Японии, но неожидан- но, в 2004 и 2005 годах, приобрела популярность в англоязычном мире. Если вы не знакомы с Судоку, почитайте статью в Википедии (http://en.wikipedia.org/wiki/Sudoku) и попробуйте составить интернет-пазл (http://websudoku.com/).
1.4. Решение головоломки Судоку на Ruby 37 просто просмотрите весь код, он очень тщательно прокомментирован, поэтому слишком больших трудностей в понимании возникнуть не должно. Пример 1.1. Решение головоломки Судоку на Ruby # # В этом модуле определяется класс Sudoku::Puzzle, отображающий # пазл Судоку 9*9. а также определяющий классы исключений, выдаваемых при # неверном вводе данных и излишне ограниченных пазлов. В этом модуле для # решения пазла также определяется метод Sudoku.solve. Метод решения использует # метод Sudoku.scan, который также здесь определен. # # Этот модуль можно использовать для решения пазлов Судоку, # применив следующий код: # # require 'sudoku' # puts Sudoku.solve(Sudoku::Puzzle.new(ARGF.readlines)) # module Sudoku # # Класс Sudoku::Puzzle представляет состояние пазла Судоку 9x9. # # Некоторые определения и термины, используемые в этой реализации: # # - Каждый элемент пазла называется "cell" (ячейка). # - Строки и столбцы нумеруются от 0 до В, а координаты [0,0] # относятся к ячейке в верхнем левом углу пазла. # - Девять квадратов 3x3, известные как "boxes", также пронумерованы # от 0 до 8, счет ведется слева направо и сверху вниз. Нулевым является # левый верхний квадрат. Квадрат сверху справа имеет номер 2. Квадрат, # расположенный посредине, имеет номер 4. А квадрат в левом нижнем углу - В. # # Создайте новый пазл, используя Sudoku::Puzzle.new, определите начальное # состояние в виде строки или массива строк. В строке (строках) нужно # использовать символы от 1 до 9 для заданных значений, и '.' для ячеек, # значение которых не указано. Неотображаемые символы во вводе игнорируются. # # Чтение и запись в отдельные ячейки пазла осуществляется посредством # операторов # [] и []=, которые ожидают наличие двумерной [строка, столбец] индексации. # В этих методах для содержимого ячеек используются числа (а не символы) # от 0 до 9. 0 представляет неизвестное значение. # # Предикат has_duplicates? возвращает true, если пазл составлен неверно из-за # того, что любая строка, столбец или квадрат содержат одну # и ту же цифру дважды. # продолжение &
38 Глава 1. Введение Пример 1.1 (продолжение) # Метод each_unknown является итератором, перебирающим ячейки # пазла и вызывающим связанный с ним блок для каждой ячейки, чье значение # неизвестно. # # Метод possible возвращает целочисленный массив, имеющий диапазон 1..9. # Элементы массива состоят только из тех значений, которые разрешены в # конкретной ячейке. Если этот массив пуст, то пазл уже полностью определен # и не может быть решен. Если массив содержит всего один элемент, значит этот # элемент должен быть значением для этой ячейки пазла. # class Puzzle # Эти константы используются для переводов между внешним # представлением строки # пазла и ее внутренним представлением. ASCII = ".1234567В9" BIN = "\000\001\002\003\004\005\006\007\010\011" # Это инициализационный метод класса. Он вызывается автоматически для нового # экземпляра Puzzle, создаваемого при помощь Puzzle.new. Вводимый пазл # передается в виде массива строк или в виде отдельной строки. При этом # используются цифры в кодировке ASCII от 1 до 9. а для неизвестных ячеек # используется символ Неотображаемые символы, включая символы # новой строки, будут удалены. def initialize(lines) if (lines.respond_to? :join) # Если аргумент похож на массив строк, s = lines.join # то объединение их в одну строку else # Иначе предположение, что это строка, s = 1ines.dup # и создание ее закрытой копии end # Удаление из данных неотображаемых символов (включая символы новой строки) # Знак '!' в gsub! Показывает, что это метод-мутатор, непосредственно # изменяющий саму строку, а не создающий ее копию. s.gsub!(/\s/, "") # /\s/ - регулярное выражение, соответствующее любому # неотображаемому символу # Выдача исключения, если вводимые данные имеют неверный размер. # Заметьте, что мы используем unless вместо if и используем его в форме # модификатора. raise Invalid. "Пазл имеет неверный размер" unless s.size == Bl # Проверка на недопустимые символы, и сохранение положения первого из них. # Заметьте, что присваивание и проверка значения, на основе которого оно # осуществляется, проводятся одновременно. if 1 = s.index(/[A1234567B9\.]/) # Включение недопустимого символа в сообщение об ошибке. # Обратите внимание на Ruby-выражение внутри #{} в строковом литерале, raise Invalid. "Недопустимый символ #{s[i.1]} в пазле " end
1.4. Решение головоломки Судоку на Ruby 39 # Следующие две строчки превращают нашу строку ASCII-символов # в целочисленный массив, используя два мощных метода объекта String. # Получившийся массив сохраняется в переменной экземпляра @grid # Число 0 используется для представления неизвестного значения. s.tr!(ASCII. BIN) # Перевод ASCII-символов в байты @grid = s.unpack!'с*') # Распаковка байтов в массив чисел # Проверка строк, столбцов и квадратов на отсутствие дубликатов. raise Invalid, "В исходном пазле имеются дубликаты" if has_duplicates? end # Возвращение состояния пазла в виде строкового значения из 9 строк по # 9 символов (плюс символ новой строки) в каждой. def to_s # Этот метод реализован в одной магической строке Ruby, # которая повторяет в обратном порядке действия метода initialize(). # Возможно, написание подобного лаконичного кода и не является лучшим # стилем программирования, но зато с его помощью демонстрируется мощность # и выразительность языка. # # Если провести анализ, то приведенная ниже строка работает следующим # образом: # (0. .8).collect вызывает код в фигурных скобках девять раз - по одному # разу для каждой строки - и собирает значения, возвращаемые этим кодом # в массив. Код в фигурных скобках берет подмассив пазла, # представляющий отдельную строку, и запаковывает его числа в строку. # Метод join() объединяет элементы массива в единую строку, перемежая их # символами новой строки. И наконец, метод tr() преобразовывает # представление строки двоичных чисел в ASCII-цифры. (0..8),collect{|r| @grid[r*9,9].pack!’с9')}.join("\n").tr(BIN.ASCII) end # Возвращает дубликат этого объекта Puzzle. # Этот метод переопределяет Object.dup для копирования массива @grid. def dup copy = super # Создание поверхностной копии за счет вызова Object.dup @grid = @grid.dup # Создание новой копии внутренних данных сору # Возвращение скопированного объекта end # Мы переопределяем оператор доступа к массиву, чтобы получить возможность # доступа к отдельной ячейке пазла. Пазл двумерный # и должен быть индексирован координатами строки и столбца. def [](row. col) # Преобразование двумерных (строка,столбец) координат в индекс одномерного # массива, а также получение и возвращение значения ячейки по этому индексу @grid[row*9 + col] end продолжение &
40 Глава 1. Введение Пример 1.1 (продолжение) # Этот метод дает возможность оператору доступа к массиву быть использованным # в левой части операции присваивания. Он устанавливает новое значение # (newvalue) ячейки, имеющей координаты (row, col). def []=(row, col, newvalue) # Выдача исключения, если новое значение не относится к диапазону от 0 до 9. unless (0..9).include? newvalue raise Invalid. "Недопустимое значение ячейки" end # Установка значения для соответствующего элемента внутреннего массива. @grid[row*9 + col] = newvalue end # Этот массив является отображением одномерного индекса пазла на номер # квадрата. # Он используется в определенном ниже методе. Имя BoxOfIndex начинается с # заглавной буквы, значит это - константа. К тому же массив был заморожен, # поэтому он не может быть изменен. BoxOflndex = [ О,0.0.1,1,1,2,2,2.0.0.0.1,1,1,2,2,2,0,0,0,1,1,1,2,2,2, 3,3,3.4,4,4,5,5,5,3,3,3,4.4,4,5,5,5,3,3,3,4,4,4,5,5.5. 6.6,6,7,7,7,В,В,В.6.6.6,7,7,7,В,В,В,6,6,6,7,7,7,В,В,В ].freeze # Этот метод определяет собственную конструкцию цикла ("итератора") для # пазлов Судоку. Для каждой ячейки, чье значение неизвестно, этот метод # передает ("выдает") номер строки, номер столбца и номер квадрата, # связанного с этим итератором. def each_unknown O.upto В do |row| # Для каждой строки O.upto В do |col| # Для каждого столбца index = row*9+col # индекс ячейки для (строки, столбца) next if @grid[index] != 0 # Идем дальше, если значение ячейки известно, box = Box0fIndex[index] # вычисление квадрата для этой ячейки yield row, col, box # Вызов связанного блока end end end # Возвращение true, если любая строка, столбец или квадрат имеют дубликаты. # Иначе возвращение false. В Судоку дубликаты в строках, столбцах или # квадратах недопустимы, поэтому возвращение значения true означает # неправильный пазл, def has_duplicates? # uniq! возвращает nil, если все элементы массива уникальны. # Поэтому если uniq! возвращает что-либо другое, значит имеются дубликаты. O.upto(B) {|row| return true if rowdigits(row).uniq! } O.upto(B) {|col| return true if coldigits(col).uniq! } O.upto(B) {jboxj return true if boxdigits(box).uniq! }
1.4. Решение головоломки Судоку на Ruby 41 false # если все тесты пройдены, значит на игровом поле нет дубликатов end # Этот массив содержит набор всех цифр Судоку. Используется кодом, следующим # далее. AllDigits = [1. 2, 3. 4, 5. 6. 7. 8. 9].freeze # Возвращение массива всех значений, которые могут быть помещены в ячейку # с координатами (строка, столбец) без создания дубликатов в строке, столбце # или квадрате. # Учтите, что оператор плюс (+), примененный к массиву, проводит объединение. # а оператор минус (-) выполняет операцию по созданию набора различий. def possible(row, col. box) AllDigits - (rowdigits(row) + col digits(col) + boxdigits(box)) end private # Все методы после этой строки являются закрытыми методами класса # Возвращение массива всех известных значений в указанной строке, def rowdigits(row) # Извлечение подмассива, представляющего строку, и удаление всех нулей. # Вычитание массивов устанавливает различие с удалением дубликатов. @grid[row*9,9] - [0] end # Возвращение массива всех известных значений в указанном столбце, def coldigits(col) result = [] # Начинаем с пустого массива col.step(B0, 9) {|i| # Перебор столбцов по девять до ВО v = @grid[i] # Получение значения ячейки по этому индексу result « v if (v != 0) # Добавление его к массиву, если оно не нулевое } result # Возвращение массива end # Отображение номера квадрата на индекс его верхнего левого угла. BoxToIndex = [0. 3, б, 27, 30, 33, 54, 57, 60].freeze # Возвращение массива всех известных значений в указанном квадрате, def boxdigits(b) # Перевод номера квадрата в индекс его верхнего левого угла. i = BoxToIndex[b] # Возвращение массива значений с удаленными нулевыми элементами. [ @grid[i], @grid[i+l], @grid[i+2], @grid[i+9], @grid[i+10], @grid[i+ll], @grid[i+18]. @grid[i+19], @grid[i+20] ] - [0] end end # Завершение класса Puzzle , продолжение &
42 Глава 1. Введение Пример 1.1 (продолжение) # Исключение этого класса, указывающее на неверный ввод class Invalid < StandardError end # Исключение этого класса, указывающее, что пазл излишне ограничен # и решения нет. class Impossible < StandardError end # # Этот метод сканирует пазл, выискивая ячейки с неизвестными значениями, # для которых есть только одно возможное значение. # Если он обнаруживает такую ячейку, то устанавливает ее значение. Так как # установка ячейки изменяет возможные значения для других ячеек, метод # продолжает сканирование до тех пор. пока не просканирует весь пазл и # не найдет ни одной ячейки, чье значение он может установить. # # Этот метод возвращает три значения. Если он решает пазл, все три # значения - nil. В противном случае в первых двух значениях возвращаются # строка и столбец ячейки, чье значение все еще неизвестно. Третье значение # является набором значений, допустимых для этой строки и столбца. Это # минимальный набор возможных значений: в пазле нет ячейки с неизвестным # значением, чей набор возможных значений еще меньше. Это составное возвращаемое # значение допускает применение в методе solved полезной эвристики: этот метод # может выстроить догадку насчет значений для ячеек, которая скорее всего # будет верной. # Этот метод выдает исключение Impossible, если находит ячейку, для которой не # существует возможных значений. Такое может произойти, если пазл излишне # ограничен или если расположенный ниже метод solved выдал # неверную догадку. # # Этот метод производит непосредственное изменение объекта Puzzle. # Если предикат has_duplicates? выдал false на входе, то он выдаст false и на # выходе. # def Sudoku.scan(puzzle) unchanged = false # Это наша переменная цикла # Выполнение цикла до тех пор, пока все игровое поле не будет просканировано # без внесения изменений. until unchanged unchanged = true # Предположение, что на сей раз никакие ячейки # изменяться не будут rmin.cmin.pmin = nil # Отслеживание ячейки с минимальным возможным # набором min = 10 # Число, превышающее максимальное количество # возможностей
1.4. Решение головоломки Судоку на Ruby 43 # Циклический перебор ячеек с неизвестными значениями. puzzle.each_unknown do |row, col, box] # Определение набора значений, подходящих для этой ячейки р = puzzle.possiЫе(row. col, box) # Ветвление на основе размера набора р. # Нас интересуют три случая: p.size==0, p.size==l и p.size > 1. case p.size when 0 # Отсутствие возможных значений означает, что пазл излишне # ограничен raise Impossible when 1 # Мы нашли уникальное значение, и вставляет его в пазл puzzle[row,col] = р[0] # Установка значения для позиции пазла unchanged = false # Отметка о внесенном изменении else # Для любого другого количества возможных значений # Отслеживание наименьшего набора возможных значений. # И исключение беспокойств по поводу намерений повторить этот цикл. if unchanged && p.size < min min = p.size # Текущий наименьший размер rmin, cmin, pmin = row, col, p # Заметьте, параллельное присваивание end end end end # Возвращение ячейки с минимальным набором возможных значений. # Обратите внимание на то. что возвращается сразу несколько значений, return rmin, cmin, pmin end # Решение пазла Судоку с применением, по возможности, простой логики, но # возвращение, по необходимости, к решению "в лоб". Это рекурсивный метод. # Он либо возвращает решение, либо выдает исключение. Решение возвращается # в виде нового объекта Puzzle, не имеющего ячеек с неизвестным значением. # Этот метод не изменяет переданный ему Puzzle. Учтите, что этот метод # не в состоянии определить пазл с недостаточной степенью ограничений. def Sudoku.solve(puzzle) # Создание независимой копии пазла, которую можно изменять. puzzle = puzzle.dup # Использование логики для максимально возможного заполнения пространства # пазла. # Этот метод приводит к необратимым изменениям передаваемого ему пазла, но # всегда оставляет его в приемлемом состоянии. # Он возвращает строку, столбец и набор возможных значений для конкретной # ячейки. # Обратите внимание на параллельное присваивание этих возвращаемых значений # трем переменным. r.c.p = scan(puzzle) продолжение
44 Глава 1. Введение Пример 1.1 (продолжение) # Если пазл решен с помощью логического подхода, возвращение решенного пазла, return puzzle if г == nil # В противном случае попытка применения каждого значения в р для ячейки [г,с]. # Поскольку мы берем значение из набора возможных значений, догадка оставляет # пазл в приемлемом состоянии. Догадка либо приведет к решению, либо к # недопустимому пазлу. Мы узнаем о недопустимости пазла, если рекурсивный вызов # сканирования выдаст исключение. Если это произойдет, нужно будет выдвинуть # другую догадку или снова выдать исключение, если перепробованы все # полученные варианты. p.each do |guess| # Для каждого значения из набора возможных puzzleEr.cJ = guess # Выбираем догадку begin # Теперь пытаемся (рекурсивно) решить модифицированный пазл. # Эта активизация рекурсии будет опять вызывать scan(), чтобы применить # логику к модифицированному игровому полю, и затем будет выстраивать # догадку по поводу другой ячейки, # если это потребуется. # Следует помнить, что solved либо вернет правильное решение, либо # выдаст исключение. return solve(puzzle) # Если будет возврат, то мы просто вернем решение rescue Impossible next # Если будет выдано исключение, попытаемся # выстроить следующую догадку end end # Если мы добрались до этого места, значит ни одна из наших догадок не # сработала, стало быть, где-то раньше наша догадка была неправильной, raise Impossible end end Пример 1.1 в общем состоит из более чем 350 строк. Поскольку пример был на- писан для вступительной главы, он содержит слишком подробный комментарий. Если этот комментарий убрать, останется всего лишь 129 строк программного кода, что является довольно хорошим показателем для объектно-ориентирован- ного решения головоломки Судоку, не полагающегося на простой алгоритм ее ре- шения «в лоб». Мы надеемся, что этот пример в достаточной степени демонстри- рует мощь и выразительность языка Ruby.
Глава 2 Структура и выполнение Ruby-программ
46 Глава 2. Структура и выполнениейиЬу-программ В этой главе объясняется структура Ruby-программ. Сначала рассматривается лексическая структура, при этом охватываются лексемы и символы, из которых они состоят. Затем рассматривается синтаксическая структура Ruby-программ, объясняется, как выражения, управляющие структуры, методы, классы и т. д. за- писываются в виде последовательностей лексем. В завершение описываются фай- лы Ruby-кода, поясняется, как Ruby-программы могут быть разбиты на несколько файлов и как Ruby-интерпретатор выполняет файл Ruby-кода. 2.1. Лексическая структура Интерпретатор Ruby проводит синтаксический анализ программы как последо- вательности лексем. Лексемы состоят из комментариев, литералов, знаков пун- ктуации, идентификаторов и ключевых слов. В этом разделе вы познакомитесь с этими типам лексем, а также получите важные сведения о символах, из которых состоят лексемы, и о неотображаемых символах, разделяющих лексемы. 2.1.1. Комментарии Комментарии в Ruby начинаются с символа # и продолжаются до конца строки. Интерпретатор Ruby игнорирует символ # и любой текст, который за ним следует (но не игнорирует символ новой строки, который является значащим неотобража- емым символом и может служить признаком конца предложения). Если символ # находится внутри строки или литерала регулярного выражения (как показано в главе 3), то он является лишь частью этой строки или регулярного выражения и не представляет комментария: # Вся эта строка является комментарием х = "#Это строка" # А это комментарий у = /#Это регулярное выражение/ # Это еще один комментарий Обычно при написании многострочных комментариев в начале каждой строки ставится отдельный символ #: # # Этот класс представляет комплексное число (Complex) # Несмотря на его название (другое значение слова Complex - сложное), сам по # себе он особой сложности не представляет. # Следует заметить, что в Ruby нет ничего подобного Си-комментарию вида /*... */. Способ вставки комментария в середину строки программного кода в нем также не предусмотрен. 2.1.1.1. Встроенные документы В Ruby поддерживается другой стиль многострочного комментария, извест- ный как встроенный документ. Он берет свое начало со строки, начинающейся
2.1. Лексическая структура 47 с =begi п, и продолжается вплоть до строки, начинающейся с =end (включая в себя и эту строку). Любой текст, появляющийся после =begi п или =end, является частью комментария, который также игнорируется, но дополнительный текст должен быть отделен от =begi п и =end хотя бы одним пробелом. Встроенные документы — удобный способ закомментировать длинные блоки кода, избавляющий от необходимости вставлять в начале каждой строки сим- вол #: =begin Нужно выключить следующий дефектный код! Любой код, расположенный здесь, будет закомментирован =end Учтите, что встроенный документ будет работать лишь в том случае, если первым в строке будет символ =: # =begin Так начинается комментарий. Теперь эта строка сама себя закомментировала! Теперь расположенный здесь код уже не будет закомментирован # =end Как следует из их названия, встроенные документы могут использоваться для включения в текст программы длинных блоков документации или для встраива- ния в Ruby-программу исходного кода на другом языке (к примеру, на HTML или SQL). Обычно встроенные документы предназначены для применения некоторы- ми инструментальными средствами постобработки, использующими исходный код Ruby, и для них характерным продолжением =begin является идентификатор, указывающий на тот инструмент, для которого написан комментарий. 2.1.1.2. Комментарии, используемые для документирования Ruby-программы могут включать встроенную API-документацию в виде специ- ально отформатированных комментариев, которые предшествуют определени- ям методов, классов и модулей. Эту документацию можно просматривать с по- мощью ri, инструментального средства, рассмотренного ранее в разделе 1.2.4. Инструментальное средство rdoc извлекает комментарии, используемые для до- кументирования из исходного кода Ruby, придавая им формат HTML или подго- тавливая их для просмотра в ri. Описание rdoc выходит за рамки тематики нашей книги; более подробную информацию об этом средстве можно получить в файле lib/rdoc/README исходного кода Ruby. Комментарии, используемые для документирования, должны располагаться не- посредственно перед тем модулем, классом или методом, чей API они докумен- тируют. Обычно они оформляются в виде многострочного комментария, каждая строка которого начинается с символа#, но они также могут быть созданы в форме встроенного документа, начинающегося с =begin rdoc. (Если опустить «rdoc», то инструментальное средство rdoc не станет обрабатывать этот комментарий.) В следующем примере комментариев показаны наиболее важные элементы фор- матирования, входящие в основы разметки комментариев Ruby, используемых
48 Глава 2. Структура и выполнениеКиЬу-программ для документирования; подробное описание этой грамматики доступно в ранее упоминавшемся файле README: # # Комментарии rdoc используют простую грамматику разметки, подобную той, что # используется в википедиях. # # Разделение абзацев производится пустой строкой. # # = Заголовки # # Заголовки начинаются со знака равенства # # == Подзаголовки # Показанная выше строка создает подзаголовок. # === Более глубокий подзаголовок # И т. д. # # = Примеры # # Строки с отступом используются, чтобы точно отобразить начертание кода. # Но для заголовков и списков отступы лучше не использовать. # # = Списки и шрифты # # Список элементов начинается с * или -. Шрифты задаются знаками пунктуации # или HTML: # * _курсив_ или <1>несколько слов, отмеченных курсивом</1> # * *полужирный* или <Ь>несколько слов, отмеченных полужирным шрифтом</Ь> # * +код+ или Онесколько слов, в формате кода</И> # # 1. Пронумерованные списки начинаются с номера. # 99. Можно использовать любой номер: последовательность соблюдать необязательно. # 1. Способ вложения списков отсутствует. # # Пункты списка описаний заключаются в квадратные скобки: # [Пункт 1] Описание пункта 1 # [Пункт 2] Описание пункта 2 # 2.1.2. Литералы Литералы — это значения, непосредственно появляющиеся в исходном коде Ruby Это могут быть числа, текстовые строки и регулярные выражения. (Другие ли- тералы, такие как значения массивов и хэшей, не представляют собой отдельные лексемы, являясь более сложными выражениями.) В действительности Ruby об- ладает довольно непростым синтаксисом числовых и строковых литералов, ко- торый подробно рассмотрен в главе 3. А сейчас вполне достаточно показать, как выглядят Ruby-литералы:
2.1. Лексическая структура 49 1 # Целочисленный литерал 1.0 # Литерал в виде числа с плавающей точкой 'one' # Строковый литерал "two" # Еще один строковый литерал /three/ # Литерал в виде регулярного выражения 2.1.3. Знаки пунктуации В Ruby знаки пунктуации используются для разных целей. Многие Ruby-опе- раторы пишутся с использованием знаков пунктуации, таких как + — для сложе- ния, * — для умножения и 11 — для булевой операции OR (или). Полный список Ruby-операторов приведен в разделе 4.6. Знаки пунктуации также служат для разделения строк, регулярных выражений, литералов массивов и хэшей, а также для группировки и разделения выражений, параметров методов и индексов мас- сивов. Мы еще увидим многие другие примеры использования знаков пунктуа- ции, встречающиеся повсюду в синтаксисе языка Ruby. 2.1.4. Идентификаторы Идентификатор — это обычное имя. Ruby использует идентификаторы для при- сваивания имен переменным, методам, классам и т. д. Идентификаторы Ruby состоят из букв, цифр и символов подчеркивания, но они не могут начинаться с цифры. Идентификаторы не могут включать пробелы или неотображаемые сим- волы, и они не могут включать знаки пунктуации, за исключением случаев, уже упомянутых в этом описании. Идентификаторы, начинающиеся с заглавной буквы в диапазоне A-Z, являются константами, и интерпретатор Ruby выдаст предупреждение (но не ошибку), если вы станете изменять значение такого идентификатора. Первые буквы в именах классов и модулей должны быть заглавными. Все, показанное в следующем при- мере, является идентификаторами: 1 х2 old_value Jnternal # Идентификаторы могут начинаться со знаков подчеркивания PI # Константа По соглашению, многословные идентификаторы, не являющиеся константами, пишутся со знаками подчеркивания — вот так, а многословные константы пишут- ся Вот_Так или ВОТ_ТАК. 2.1.4.1. Чувствительность к регистру Ruby является языком, чувствительным к регистру символов. Заглавные и строч- ные буквы отличаются друг от друга. К примеру, ключевое слово end отличается от ключевого слова END.
50 Глава 2. Структура и выполнениекиЬу-программ 2.1.4.2. Символы Юникода в идентификаторах Правила языка Ruby для образования идентификаторов определены в понятиях недопустимых символов ASCII. А вообще-то в идентификаторах могут приме- няться все символы за пределами набора ASCII, включая символы, рассматри- ваемые как знаки пунктуации. К примеру, в файле с кодировкой UTF-8 допустим следующий код Ruby: def х(х,у) # Имя этого метода является знаком умножения в Юникоде х*у # Тело этого метода осуществляет перемножение его аргументов end Точно так же японские программисты, пишущие программы в кодировке SJIS или EUC, могут включать в свои идентификаторы символы Кандзи. Подробности о написании Ruby-программ с использованием кодировок, отличных от ASCII, описаны в разделе 2.4.1. Специальные правила образования идентификаторов касаются использования символов ASCII и не распространяются на символы, находящиеся за пределами этого набора. К примеру, идентификатор не должен начинаться с цифры, но он может начинаться с той цифры, которая не входит в кодировку латиницы. По тем же правилам, если идентификатор должен представлять константу, его следует начинать с заглавной буквы набора символов ASCII. К примеру, идентификатор А константу представлять не может. Два идентификатора считаются одинаковыми, если они представлены одина- ковой последовательностью байтов. Некоторые наборы символов, такие как Юникод, для одного и того же символа имеют несколько кодов. Нормализация Юникода в Ruby отсутствует, и два разных кода символов рассматриваются как разные символы, даже если имеют одинаковое назначение или представлены од- ним и тем же шрифтовым образом. 2.1.4.З. Использование в идентификаторах знаков пунктуации Знаки пунктуации могут появляться в начале и в конце Ruby-идентификаторов и предназначения для определенных целей, что отражено в таблице. Посмотрите на примеры идентификаторов, имеющих в начале или в конце знаки пунктуации: Sfiles # Глобальная переменная ©data # Переменная экземпляра @@counter # Переменная класса empty? # Метод или предикат, возвращающий булево значение sort! # Альтернатива обычному методу сортировки, изменяющая сам объект timeoiit= # Метод, вызываемый присваиванием Многие Ruby-операторы реализованы в виде методов, с тем чтобы классы мог- ли их переопределить для своих собственных нужд. Тем самым предоставляется
2.1. Лексическая структура 51 возможность использования некоторых операторов и в качестве имен методов. В этом контексте знаки пунктуации или символы операторов рассматриваются в качестве идентификаторов, а не операторов. Более подробные сведения о Ruby- операторах изложены в разделе 4.6. $ В качестве префикса в глобальных переменных используется знак доллара. По примеру языка Perl, в Ruby определяется ряд глобальных переменных, включающих другие знаки пунктуации, в числе которых $_ и $-К. Перечень таких глобальных переменных приведен в главе 10 @ В переменных экземпляра в качестве префикса используется одинарный знак at, а в пере- менных класса — двойной знак. Переменные экземпляра и класса рассмотрены в главе 7 ? Для удобства методы, возвращающие булевы значения, зачастую имеют имена, заканчиваю- щиеся вопросительным знаком ! Имена методов могут заканчиваться восклицательным знаком, чтобы предупредить, что при их использовании следует проявлять осторожность. Это соглашение об именах зачастую служит отличительным признаком методов-мутаторов, изменяющих тот объект, для которого они были вызваны, от тех методов, которые возвращают модифицированную копию ис- ходного объекта = Методы, чьи имена заканчиваются знаком равенства, могут быть вызваны указанием имени метода без использования в выражении знака равенства в левой части оператора присваи- вания. (Более подробно эта особенность рассмотрена в разделе 4.5.3 и в разделе 7.1.5) 2.1.5. Ключевые слова Следующие ключевые слова имеют в Ruby специальное назначение и рассматри- ваются Ruby-парсером особым образом: _LINE_ case ensure not then _ENCODING_ class false or true _FILE— def for redo undef BEGIN defined? if rescue unless END do in retry unti 1 alias else module return when and el si f next self while begin end nil super yield break В дополнение к этим ключевым словам есть три похожие на ключевые слова лек- семы, которые тоже рассматриваются Ruby-парсером особым образом, когда по- являются в начале строки: =begin =end _______END__ Мы уже знаем, что =begi п и =end в начале строки устанавливают границы много- строчных комментариев. А лексема__END_помечает конец программы (и начало раздела данных), если появляется в строке сама по себе, без начальных или конеч- ных пробелов. В большинстве языков программирования эти слова называются «зарезервиро- ванными», и их использование в качестве идентификаторов категорически запре- щается. Ruby-парсер работает более гибко и не возражает, если эти слова получат
52 Глава 2. Структура и выполнениеЯиЬу-программ префикс @, @@ или $ и будут использованы в качестве имен переменных экземпляра, класса или глобальных переменных. Вы также можете использовать эти ключе- вые слова в качестве имен методов с тем лишь предостережением, что метод дол- жен всегда явным образом вызываться в отношении объекта. Тем не менее нужно учесть, что использование этих ключевых слов в идентификаторах приведет к за- путыванию программного кода. Лучше все же рассматривать эти ключевые слова в качестве зарезервированных. Многие важные свойства языка Ruby фактически реализованы как методы клас- сов Kernel, Module, Class и Object. Поэтому неплохо было бы взять за правило также рассматривать в качестве зарезервированных следующие идентификаторы: # Это методы, которые представляются операторами или ключевыми словами at_exit attr attr_accessor attr_reader attr_writer catch Include lambda load loop private proc protected public raise require throw # Это широко используемые глобальные функции Array chomp! gsub! select Float chop Iterator? sleep Integer chop! load split String eval open sprintf URI exec P srand abort exit print sub autoload exit! printf sub! autoload? fall putc syscall binding fork puts system block_g1ven? format rand test cal lee getc readline trap caller gets readlines warn chomp gsub scan # Это широко используемые методы объекта allocate freeze kind_of? superclass clone frozen? method taint display hash methods tainted? dup Id new to_a enum for Inherited nil? to_enum eql? Inspect objectjd to_s equal? 1nstance_of? respond_to? untaint extend Is a? send 2.1.6. Разделители Пробелы, знаки табуляции и символы новой строки сами по себе лексемами не яв- ляются, а используются для разделения лексем, которые без них объединились бы
2.1. Лексическая структура 53 в одну большую лексему. Кроме этой основной функции по разделению лексем, большинство разделителей игнорируются интерпретатором Ruby и используют- ся лишь для форматирования программ, облегчая их чтение и понимание. Тем не менее игнорируются не все разделители. Некоторые из них бывают востребованы, а некоторые — запрещены. Грамматика Ruby выразительна, но сложна, и суще- ствует ряд обстоятельств, при которых вставка или удаление разделителей может изменить смысл программы. И хотя такие случаи встречаются довольно редко, вам важно знать об их существовании. 2.1.6.1. Символы новой строки как признаки конца оператора Чаще всего зависимость от разделителей просматривается в отношении символа новой строки в роли признака конца оператора. В таких языках, как Си и Java, каждый оператор должен завершаться точкой с запятой. В Ruby тоже можно за- вершать строку этим символом, но надобность в этом возникает только при нали- чии в одной строке более одного оператора. По соглашению во всех других случа- ях точка с запятой опускается. В отсутствие завершающих точек с запятой Ruby-интерпретатор вынужден сам определять, где заканчивается оператор. Если присутствующий в строке код Ruby является синтаксически завершенным оператором, Ruby использует в качестве признака конца строки символ новой строки. (В Ruby 1.9 на этот счет имеется одно исключение, которое будет рассмотрено далее в этом разделе.) Когда все ваши операторы умещаются на отдельной строке, проблем не возникает. Но если так не получается, то следует разбить строку таким образом, чтобы Ruby- интерпретатор не смог толковать первую строку в качестве самостоятельного опе- ратора. Именно здесь и проявляется зависимость от разделителя: ваша программа может вести себя по-разному в зависимости от того, куда вставлен символ новой строки. К примеру, следующий код складывает значения х и у и присваивает сум- му переменной total: total = х + # Незавершенное выражение, синтаксический анализ продолжается У Но вот этот код присваивает значение х переменной total, а затем вычисляет пере- менную у, не внося в нее никаких изменений: total = х # Завершенное выражение + у # Бесполезное, но завершенное выражение В качестве еще одного примера рассмотрим использование операторов return и break. Ко всему прочему они могут сопровождаться выражением, предостав- ляющим возвращаемое значение. Символ новой строки между ключевым словом и выражением завершит оператор перед выражением. Без опасений преждевременного завершения оператора можно вставлять символ новой строки после знака операции или после точки или запятой в вызове метода, литерала массива или литерала хэша.
54 Глава 2. Структура и выполнениеЯиЬу-программ Также можно нейтрализовать обрыв строки с помощью обратного слэша, предот- вращающего автоматическое завершение оператора, выполняемое Ruby-интер- претатором: var total = первое_длинное_имя_переменной + второе_длинное_имя_переменной \ + третье_длинное_имя_переменной # Учтите, что в верхней строке оператор # не завершается В Ruby 1.9 правила завершения оператора немного изменились. Если первый не являющийся пробелом символ строки будет точкой, то строка рассматривается как строка-продолжение, и предшествующий символ новой строки не является признаком завершения оператора. Строки, начинающиеся с точек, удобны для длинных цепочек методов, которые иногда используются с «многословными (fluent) API», в которых каждый вызов метода возвращает объект, в отношении которого могут быть произведены дополнительные вызовы. К примеру: animals = Array.new .pushC’dog") # В Ruby l.B этот код не работает .pushC'cow") .pushCcat”) .sort 2.1.6.2. Пробелы и вызовы методов Грамматика Ruby позволяет при определенных обстоятельствах опускать скобки в вызовах методов. Это дает возможность использовать методы Ruby в качестве операторов, что является важной составляющей элегантности Ruby. Но, к сожале- нию, тем самым открывается опасная зависимость от пробелов. Рассмотрим сле- дующие две строки, отличающиеся одним-единственным пробелом: f(3+21+1 f (3+21+1 В первой строке значение 5 передается функции f, а затем к результату прибав- ляется 1. Поскольку во второй строке после имени функции стоит пробел, Ruby предполагает, что скобки в вызове функции были опущены. Скобки, появляющи- еся после пробела, используются для группировки подвыражения, но все выраже- ние (3+21+1 используется в качестве аргумента метода. Если разрешены предупре- ждения (использован ключ -w), Ruby, как только ему встретится неоднозначный код, подобный этому, выдает предупреждение. Правила, позволяющие преодолеть зависимость от разделителей, просты. О Никогда не ставьте пробел между именем метода и открывающей скобкой. О Если первый аргумент метода начинается с открывающей скобки, всегда ис- пользуйте скобки при вызове метода. Например, следует писать f((3+21+1). О Всегда запускайте интерпретатор Ruby с ключом -w, чтобы получать предупре- ждения в том случае, если вы забыли применить любое из правил, изложенных выше!
2.2. Синтаксическая структура 55 2.2. Синтаксическая структура До сих пор мы рассматривали лексемы Ruby-программ и символы, из которых они составлены. Теперь мы кратко рассмотрим, как эти лексические составляю- щие объединяются в более крупные синтаксические структуры Ruby-программ. В этом разделе дается описание синтаксиса программ на Ruby, от простейших вы- ражений до самых крупных модулей. Материал этого раздела станет, по сути, до- рожной картой к следующим главам. Основным элементом синтаксиса Ruby является выражение. Интерпретатор Ru- by вычисляет выражения, выдавая значения. Простейшими выражениями явля- ются первичные выражения, которые представляют собой сами значения. Число- вые и строковые литералы, рассмотренные ранее в этой главе, — это первичные выражения. Другие первичные выражения включают определенные ключевые слова, такие как true, false, nil и self. Ссылки на переменные также являются первичными выражениями; они вычисляются в значение переменной. Более сложные значения могут быть написаны в виде в виде составных выраже- ний: [1.2,3] {1=>"опе", 2=>"two"} 1..3 # Литерал массива # Литерал хэша # Литерал диапазона Знаки операций используются для выполнения вычислений над значениями, а составные выражения строятся путем объединения более простых подвыраже- ний с использованием знаков операций: 1 # Первичное выражение х # Еще одно первичное выражение х = 1 # Выражение присваивания х = х + 1 # Выражение с двумя знаками операций Знаки операций и выражения, включая переменные и выражения присваивания, рассматриваются в главе 4. Для составления операторов, таких как оператор 1 f, который предназначен для условного выполнения кода, и оператор while, который предназначен для повтор- ного выполнения кода, выражения могут быть объединены с ключевыми словами Ruby: if х < 10 then # Если это выражение истинно. х = х + 1 # то выполняет этот оператор end # Признак завершения условия while х < 10 do # Пока это выражение истинно... print х # выполняет этот оператор, х = х + 1 # а затем этот оператор end # Признак завершения цикла
56 Глава 2. Структура и выполнениеИиЬу-программ Формально в Ruby эти операторы являются выражениями, но существуют все- таки полезные различия между теми выражениями, которые влияют на управля- ющую логику программы, и теми, которые не оказывают такого влияния. Управ- ляющие структуры языка Ruby рассматриваются в главе 5. Обычно во всех программах, исключая простейшие, приходится группировать выражения и операторы в параметризированные компоненты программы, что- бы они могли выполняться многократно и оперировать различными входными данными. Эти параметризированные компоненты могут быть вам известны как функции, процедуры или подпрограммы. Поскольку Ruby является объектно- ориентированным языком, в нем они называются методами. Методы, наряду с родственными им структурами, называемыми ргос и lambda, являются темой главы 6. Наконец, группы методов, спроектированные для взаимодействия, могут быть объединены в классы, а группы родственных классов и методов, независимых от этих классов, могут быть сведены в модули. Классы и модули являются темой главы 7. 2.2.1. Блочные структуры языка Ruby Ruby-программы имеют блочную структуру. Модули, классы, определения мето- дов и большинство операторов Ruby включают блоки вложенного программного кода. В качестве границ блоков выступают ключевые слова или знаки пунктуа- ции, и по соглашению блоки имеют отступ от своих разделителей на два пробела. В Ruby-программах имеются две разновидности блоков. Одна из них формально называется «блоком». Эти блоки представляют собой фрагменты программного кода, связанные с методами-итераторами или передаваемые этим методам: 3.times { print "Ruby! " } В этом примере фигурные скобки и код внутри них являются блоком, связанным с методом-итератором, вызываемым три раза (3.times). Формальные блоки этого типа могут быть ограничены фигурными скобками или ключевыми словами do и end: l.uptoClO) do |х| print X end Ограничители do и end обычно используются для многострочного блока. Обратите внимание, что программный код внутри блока имеет отступ в два символа. Блоки рассматриваются в разделе 5.4. Чтобы не возникло путаницы с этими настоящими блоками, другую разновид- ность блоков мы можем назвать телом (тем не менее на практике термин «блок» употребляется в отношении обеих разновидностей). Тело — это просто перечень операторов, составляющих тело определения класса, метода, цикла while или чего- нибудь другого. Тела в Ruby никогда не заключаются в фигурные скобки — обычно
2.3. Структура файла 57 вместо них в качестве ограничителей выступают ключевые слова. Специальный синтаксис для тел операторов, тел методов и тел классов и модулей рассмотрен в главах 5, 6 и 7. Тела и блоки могут быть вложены в пределах друг друга и Ruby-программ, как правило, есть несколько уровней вложенности кода, которые читаются благодаря отступам относительно друг друга. Вот как выглядит пример подобной схемы: module Stats class Dataset def initialize(filename) 10.foreachtfilename) do if line[0,l] == "#" next end end end end end # Модуль # Класс в модуле # Метод в классе |11пе| # Блок в методе # Оператор if в блоке # Простой оператор в if # Завершение тела if # Завершение блока # Завершение тела метода # Завершение тела класса # Завершение тела модуля 2.3. Структура файла В отношении структуры файла с кодом Ruby существует всего лишь несколько правил. Эти правила имеют отношение к размещению Ruby-программ и не имеют прямого отношения к самому языку. Во-первых, если Ruby-программа содержит «shebangs-комментарии1, чтобы сооб- щить Юникс-подобной операционной системе, как ее следует выполнять, то этот комментарий должен быть размещен в первой строке. Во-вторых, если Ruby-программа содержит «codings-комментарий, имеющий от- ношение к кодировке (как описано в разделе 2.4.1), этот комментарий должен быть размещен в первой строке или во второй строке, если в первой строке раз- мещен «shebangs-комментарий. В-третьих, если файл содержит строку, состоящую из единственной лексемы _END__без пробелов ни до, ни после нее, Ruby-интерпретатор остановит обработ- ку файла в этой точке. Остальная часть файла может содержать произвольные данные, которые программа может прочитать, используя потоковый 10-объект DATA. (Подробности, касающиеся этой глобальной константы, рассмотрены в гла- ве 10 и в разделе 9.7.) Ruby-программы не обязательно размещать в одном файле. К примеру, многие программы загружают дополнительный Ruby-код из внешних библиотек. Для загрузки кода из другого файла программы используется оператор require, кото- рый ищет указанные модули кода в пути поиска и предотвращает загрузку любого 1 Shebang — от названия символов — sharp и bang #!, с которых начинается комментарий. — Прим, перев.
58 Глава 2. Структура и выполнениеЯиЬу-программ заданного модуля более одного раза. Подробности его работы рассматриваются в разделе 7.6. Каждая из этих особенностей структуры файла Ruby-кода иллюстрируется сле- дующим кодом: #!/usr/bin/ruby -w shebang-комментарий # -*- coding: utf-8 -*- coding-комментарий require 'socket' загрузка сетевой библиотеки здесь размещается код программы __END__ маркировка завершения кода здесь размещаются данные программы 2.4. Кодировка программы На самом низшем уровне Ruby-программа — это простая последовательность символов. Правила лексики Ruby определены с использованием символов ASCII- набора. Комментарии, к примеру, начинаются с символа # (ASCII-код 35), а раз- решенные символы-разделители — это горизонтальная табуляция (ASCII-код 9), новая строка (10), вертикальная табуляция (И), перевод страницы (12), возврат каретки (13) и пробел (32). Все ключевые слова Ruby написаны с использованием ASCII-символов, и все знаки операций и пунктуации получены из набора симво- лов ASCII. По умолчанию Ruby-интерпретатор предполагает, что исходный код представлен в кодировке ASCII. Но это не является обязательным требованием; интерпрета- тор также может обрабатывать файлы, использующие другие кодировки, пока они представляют полный набор ASCII-символов. Чтобы Ruby-интерпретатор мог воспринимать байты исходного файла как символы, он должен знать, какая коди- ровка используется. Ruby-файлы могут сами указывать на свою кодировку, кроме того, интерпретатору можно сообщить, какая кодировка в них использована. Как это сделать, мы скоро узнаем. Вообще-то Ruby-интерпретатор проявляет достаточную гибкость по отношению к символам, появляющимся в Ruby-программе. Определенные ASCII-символы имеют особое значение, а другие, тоже вполне определенные ASCII-символы, нельзя применять в идентификаторах, но кроме этого Ruby-программа может состоять из любых символов, допускаемых используемой кодировкой. Ранее мы уже объясняли, что идентификатор может содержать любые символы за преде- лами ASCII-набора. То же самое справедливо для комментариев и строк, а так- же для литералов регулярных выражений: они могут содержать любые символы, отличающиеся от символов-разделителей, которые отмечают завершение ком- ментария или литерала. В файлах в ASCII-кодировке строки могут состоять из любых байтов, включая те, которые представляют неотображаемые управляющие символы. (Но использовать эти байты в необработанном виде не рекомендуется;
2.4. Кодировка программы 59 в строковых литералах Ruby поддерживаются escape-последовательности, и по- добные произвольные символы можно вместо этого включить путем указания их цифрового кода.) Если файл записан в кодировке UTF-8, то комментарии, строки и регулярные выражения могут включать любые символы Юникода. Если в коди- ровке файла используются японские кодировки SJIS или EUC, то строка может включать Кандзи-символы. 2.4.1. Объявление кодировки программы По умолчанию Ruby-интерпретатор предполагает, что программы имеют коди- ровку ASCII. В Ruby 1.8 можно указать другую кодировку, воспользовавшись ключом командной строки -К. Чтобы запустить Ruby-программу, включающую символы Юникода в кодировке UTF-8, нужно вызвать интерпретатор с ключом -Ku. Программы, включающие японские символы в кодировке EUC-JP или SJIS, должны быть запущены с ключами -Ке и -Ks. В Ruby 1.9 также поддерживается ключ -К, но он больше не является предпочти- тельным способом задания кодировки программного файла. Вместо того чтобы заставлять пользователя сценария указывать кодировку при вызове Ruby, автор сценария может указать кодировку сценария, поместив в начале файла специаль- ный «codings-комментарий1. Например: # coding: utf-8 Весь комментарий должен быть написан в кодировке ASCII и должен включать строку coding, за которой следует знак двоеточия или равенства и название нуж- ной кодировки (которое не может включать пробелы или знаки пунктуации, за ис- ключением дефиса и знака подчеркивания). По обе стороны двоеточия или знака равенства допускается наличие пробелов, а строка codi ng может иметь любой пре- фикс, в том числе еп, чтобы образовать слово encoding. Весь комментарий, включая codi ng и название кодировки, нечувствителен к регистру символов и может быть написан с использованием букв в верхнем или нижнем регистре. Обычно комментарии, относящиеся к кодировке, пишутся так, чтобы информи- ровать об использованной кодировке файла и текстовый редактор. Пользователи Emacs могут написать так: # -*- coding: utf-8 -*- А пользователи vi могут написать так: # vi: set fi 1 eencodi ng=utf-8 : Обычно подобные комментарии о кодировке допускаются только в первой строке файла. Но они могут появляться и во второй строке, если первая строка занята «shebangs-комментарием (который подготавливает сценарий для выполнения под управлением операционных систем семейства Юникс): 1 В этом вопросе Ruby следует соглашениям, принятым для языка Python; см. http://www. python.org/dev/peps/pep-0263/
60 Глава 2. Структура и выполнениеЯиЬу-программ # !/usr/bin/ruby -w # coding: utf-8 Названия кодировок нечувствительны к регистру и могут быть написаны в верх- нем, в нижнем или в смешанном регистре. Ruby 1.9 поддерживает как минимум следующие кодировки исходного кода: ASCII-8BIT (которая также известна как BINARY), US-ASCII (7-битный ASCII), европейские кодировки от ISO-8859-1 до ISO-8859-15, кодировку Юникод UTF-8 и японские кодировки SHIFTJIS (так- же известную как SJIS) и EUC-JP. Но ваша версия или установка Ruby может также поддерживать и дополнительные кодировки. Как частный случай, файлы, имеющие кодировку UTF-8, указывают на нее, если первые три байта файла имеют следующее содержание: OxEF ОхВВ OxBF. Эти байты известны как ВОМ, или «Byte Order Mark» (маркер последовательности байтов) и являются для файлов в кодировке UTF-8 необязательным элементом. (Некото- рые Windows-программы добавляют эти байты при сохранении файлов в коди- ровке Юникод.) В Ruby 1.9 имеющееся в языке ключевое слово_ENCODING_(в начале и в конце ко- торого стоят сдвоенные знаки подчеркивания) вычисляется в значение исходной кодировки текущего исполняемого кода. Получающееся в результате этого значе- ние является объектом Encoding. (Дополнительная информация о классе Encoding изложена в разделе 3.2.6.2.) 2.4.2. Кодировка исходного кода и установленная по умолчанию внешняя кодировка В Ruby 1.9 очень важно понимать разницу между кодировкой исходного кода в Ruby-файле и установленной по умолчанию внешней кодировкой Ruby- процесса. Кодировка исходного кода — это то, что мы рассматривали ранее: она сообщает Ruby-интерпретатору, как считывать символы сценария. Как правило, кодировка исходного кода устанавливается с помощью «coding»-KOMMeHTapHeB. Ruby-программа может состоять из более чем одного файла, и разные файлы мо- гут иметь различную кодировку исходного кода. Эта исходная кодировка файла влияет на кодировку находящихся в этом файле строковых литералов. Дополни- тельные сведения о кодировке строк можно найти в разделе 3.2.6. Установленная по умолчанию внешняя кодировка — это нечто иное: она представ- ляет собой кодировку, которую Ruby использует по умолчанию, когда производит чтение из файлов и потоков. Эта кодировка является для Ruby-процесса глобаль- ной и от файла к файлу не изменяется. Обычно по умолчанию устанавливается та внешняя кодировка, на которую сконфигурирован ваш компьютер. Но вы также можете указать внешнюю кодировку по умолчанию в явном виде при помощи па- раметров командной строки в соответствии с приведенным ранее кратким описа- нием. Установленная по умолчанию внешняя кодировка не оказывает влияния на кодировку строковых литералов, но она имеет существенное значение для опера- ций ввода-вывода, рассматриваемых в разделе 9.7.2.
2.5. Выполнение программы 61 Ранее мы рассматривали ключ интерпретатора -К в качестве способа установки кодировки исходного кода. На самом деле этот ключ устанавливает используемую по умолчанию внешнюю кодировку процесса, а затем использует эту кодировку в качестве установленной по умолчанию кодировки исходного кода. В Ruby 1.9 ключ -К присутствует в целях поддержки совместимости с Ruby 1.8, но не является предпочтительным способом установки используемой по умолчанию внешней кодировки. В этой версии существуют два новых ключа, -Е и - -encoding, которые позволяют указать кодировку, используя ее полное название, а не одно- буквенное сокращение. Например: ruby -Е utf-8 # Название кодировки следует за -Е ruby -Eutf-8 # Ставить пробел необязательно ruby --encoding utf-8 # Название кодировки следует за --encoding с пробелом ruby --encoding=utf-8 # Или с -encoding используется знак равенства Подробное описание этих ключей приведено в разделе 10.1. Установленную по умолчанию внешнюю кодировку можно запросить с помощью Encoding.default_external. Этот метод класса возвращает объект Encoding. Чтобы получить название кодировки символов (в виде строки), полученной из настро- ек локальной машины, нужно воспользоваться методом Encoding.locale_charmap. Работа этого метода всегда основана на локальных установках и игнорирует клю- чи командной строки, с помощью которых переопределяется установленная по умолчанию внешняя кодировка. 2.5. Выполнение программы Ruby является языком сценариев. Это означает, что Ruby-программы — это про- сто списки, или сценарии, состоящие из операторов, предназначенных для выпол- нения. По умолчанию эти операторы выполняются последовательно, в порядке их обнаружения. Управляющие структуры Ruby (рассмотренные в главе 5) изменя- ют этот исходный порядок выполнения и дают, к примеру, возможность условного или повторного выполнения операторов. Программисты, привыкшие к традиционным статическим языкам с компиляцией кода вроде Си или Java, могут удивиться отсутствию в Ruby специального метода main, с которого начинается выполнение программы. Ruby-интерпретатор полу- чает на выполнение операторы сценария и приступает к их выполнению с первой и до последней строки. (На самом деле последнее утверждение не вполне корректно. Сначала Ruby- интерпретатор сканирует файл на наличие операторов BEGIN и выполняет код, вхо- дящий в их тела. Затем он возвращается к строке 1 и приступает к последователь- ному выполнению. Дополнительно об операторе BEGIN можно узнать в разделе 5.7.) Другие отличия Ruby от компилируемых языков касаются определений моду- лей, классов и методов. В компилируемых языках они являются структурами
62 Глава 2. Структура и выполнениейиЬу-программ синтаксиса, обрабатываемыми компилятором. В Ruby они такие же операторы, как и все остальное. Когда Ruby-интерпретатор встречается с определением клас- са, он выполняет этот оператор, вызывая появление нового класса. Точно так же, встретившись с определением метода, выполняет оператор, определяя тем са- мым новый метод. Позже, при выполнении программы, интерпретатор, наверное, встретит и выполнит выражение, вызывающее этот метод, и этот вызов приведет к выполнению операторов, входящих в тело метода. Ruby-интерпретатор вызывается из командной строки, и ему указывается сце- нарий, предназначенный для выполнения. Простейшие однострочные сценарии иногда пишутся прямо в командной строке. Но чаще всего в ней указывается имя файла, содержащего сценарий. Ruby-интерпретатор считывает файл и выполняет сценарий. Сначала он выполняет любые блоки BEGIN. Затем приступает к выпол- нению первой строки файла и продолжает свою работу, пока не случится следую- щее: О будет выполнен оператор, вызывающий завершение Ruby-программы; О будет достигнут конец файла; О будет считана строка, которой с помощью лексемы_END_помечено логическое завершение файла. Перед завершением своей работы Ruby-интерпретатор обычно (если только не был вызван метод exit!) выполняет код тела любого встреченного ему операто- ра END и любого другого кода «перехвата остановки», приведенного в функции at exit.
Глава 3 Типы ДАННЫХ И ОБЪЕКТЫ
64 Глава 3. Типы данных и объекты Чтобы разобраться в языке программирования, нужно знать, с какими типами данных в нем можно работать и что он может делать с этими данными. Эта глава посвящена значениям, с которыми работают Ruby-программы. Она начинается со всестороннего рассмотрения числовых и текстовых значений. Затем рассматри- ваются массивы и хэши — две важные структуры данных, являющиеся основной частью Ruby. После этого мы перейдем к рассмотрению диапазонов, обозначений и специальных значений true, false и nil. Все Ruby-значения являются объекта- ми, и эта глава завершается детальным рассмотрением свойств, присущих всем этим объектам. Классы, описываемые в этой главе, являются основными типами данных языка Ruby. В этой главе объясняется поведение этих типов: как литералы записывают- ся в программах, как работает арифметика, целочисленная и с плавающей точкой, как кодируются текстовые данные, как значения могут служить в качестве ключей хэша и т. д. Хотя здесь и рассматриваются числа, строки, массивы и хэши, в этой главе не предпринимается никаких попыток объяснить интерфейсы прикладного программирования — API, определенные этими типами. API-интерфейсы демон- стрируются на примерах в главе 9, в ней также описывается множество других важных (но не основных) классов. 3.1. Числа В Ruby имеется пять встроенных классов для представления чисел, а стандарт- ная библиотека включает еще три числовых класса, которые в отдельных случаях тоже могут оказаться полезными. На рис. 3.1 показана иерархия классов. Рис. 3.1. Иерархия класса Numeric Все числовые объекты в Ruby являются экземплярами класса Numeric. Все цело- численные объекты — экземплярами класса Integer. Если целочисленное значе- ние умещается в 31 бит (в большинстве случаев применения), объект является эк- земпляром класса Fi xnum. В ином случае этот объект является экземпляром класса Bignum. Объекты Bignum представляют целые числа произвольного размера, и если результат операции над Fi xnum-операндами слишком большой, чтобы поместиться
3.1. Числа 65 в Fixnum, этот результат неизменно преобразуется в Bignum. Аналогично этому, если результат операции над объектами Bignum снижается до пределов Fixnum, то резуль- тат становится объектом F i xnum. Вещественные числа в Ruby приблизительно со- ответствуют классу Float, который использует присущее используемой платфор- ме представление чисел с плавающей точкой. Классы Complex, BigDecimal и Rational не встроены в Ruby, но распространяются вместе с ним как часть стандартной библиотеки. Класс Complex несомненно пред- ставляет комплексные числа. Класс BigDecimal представляет вещественные чис- ла с произвольной точностью, при этом используется не двоичное, а десятичное представление. И класс Rational представляет рациональные числа: результаты деления одного целого числа на другое. Все числовые объекты являются неизменяемыми; методов, позволяющих изме- нять значения, сохраненные в объектах, не существует. Если передать методу ссылку на числовой объект, то испытывать беспокойство о том, что метод внесет в объект изменения, не стоит. Чаще всего находят применение объекты Fixnum, и, как правило, реализации языка Ruby рассматривают их в качестве непосред- ственных значений, а не в качестве ссылок. Но поскольку числа не изменяются, почувствовать разницу невозможно. 3.1.1. Целочисленные литералы Целочисленные литералы — это простая последовательность цифр: О 123 1234567В901234567В90 Если целочисленное значение находится в пределах класса Fixnum, значение явля- ется объектом Fixnum. В противном случае оно является объектом Bignum, поддер- живающим целочисленные значения любого размера. В целочисленные литералы могут быть вставлены знаки подчеркивания (но только не в начале и не в конце), И это свойство иногда используется в качестве разделителя класса тысяч: 1_000_000_000 # Один миллиард Если целочисленный литерал начинается с нуля и содержит более одной циф- ры, он рассматривается как число, имеющее основание, отличное от 10. Числа, начинающиеся с Ох или ОХ, являются шестнадцатеричными (числами по основа- нию 16), и в них в качестве цифр от 10 до 15 используются буквы от а до f (или от А до F). Числа, начинающиеся с ОЬ или ОВ, являются двоичными (числами по основа- нию 2) и могут включать в себя только цифры 0 и 1. Числа, начинающиеся с 0 и не имеющие последующей буквы, являются восьмеричными (числа по основанию 8) и должны состоять из цифр от 0 до 7. Примеры: 0377 # Восьмеричное представление числа 255 0Ы111_1111 # Двоичное представление числа 255 OxFF # Шестнадцатеричное представление числа 255
66 Глава 3. Типы данных и объекты Для представления отрицательных чисел нужно просто начинать целочисленный литерал со знака минус. Литералы могут также начинаться со знака плюс, но это не меняет значения литерала. 3.1.2. Литералы чисел с плавающей точкой Литералы чисел с плавающей точкой представляют собой необязательный знак числа, за которым следуют одна или несколько десятичных цифр, десятичная точка (символ .), одна или несколько дополнительных цифр и необязатель- ный показатель степени. Этот показатель начинается с буквы е или Е, за ко- торой следуют необязательные знак и одна или несколько десятичных цифр. Как и в целочисленных литералах, в числах с плавающей точкой могут быть использованы знаки подчеркивания. В отличие от целочисленных литералов, выразить числа с плавающей точкой в какой-нибудь другой, не десятичной си- стеме счисления, невозможно. Посмотрите на примеры литералов чисел с пла- вающей точкой: 0.0 -3.14 6.02е23 # Это означает 6.02 х 1023 1_000_000.01 # Число чуть больше миллиона Язык Ruby требует, чтобы цифры были до и после десятичной точки. К примеру, вы не можете просто написать . 1; запись должна быть конкретной — 0.1. Такие требования связаны с устранением неоднозначности в сложной грамматике Ruby. Этим Ruby отличается от многих других языков программирования. 3.1.3. Арифметика, используемая в Ruby Для всех числовых типов в Ruby определены стандартные операторы +, -, * и / — соответственно для сложения, вычитания, умножения и деления. Когда цело- численные результаты слишком велики для Fixnum, Ruby автоматически превра- щает их в Bignum, благодаря чему целочисленная арифметика в Ruby никогда не испытывает переполнений, как это бывает во многих других языках. В числах с плавающей точкой (по крайней мере на платформе, использующей стандартное представления таких чисел — IEEE-754) переполнение превращается в особые положительные или отрицательные бесконечные значения, а исчезновение пре- вращается в нуль. Работа оператора деления зависит от класса операндов. Если оба операнда цело- численные, операция выполняет деление с усечением результата до целого числа. Если оба операнда являются объектами Float, выполняется деление чисел с пла- вающей точкой: х = 5/2 # результат - 2 у = 5.0/2 # результат - 2.5 z = 5/2.0 # результат - 2.5
3.1. Числа 67 Целочисленное деление на нуль приводит к возникновению ошибки ZeroDi Vision- Error. Деление чисел с плавающей точкой на нуль не вызывает ошибки; оно просто возвращает значение Infinity (бесконечность). Случай деления 0.0/0.0 — особен- ный; в большинстве образцов современного аппаратного обеспечения и в боль- шинстве операционных систем это деление вычисляется в другое особое значе- ние числа с плавающей точкой, известное как NaN или Not-a-Number (не число). Оператор деления по модулю (Я) вычисляет остаток от целочисленного деления: х = 5X2 # результат - 1 Оператор X также может быть использован с Float-операндами, хотя это встреча- ется довольно редко: х = 1.5X0.4 # результат - 0.3 ДЕЛЕНИЕ, ДЕЛЕНИЕ ПО МОДУЛЮ И ОТРИЦАТЕЛЬНЫЕ ЧИСЛА Если один (но не оба) операнда являются отрицательными числами, Ruby вы- полняет операции целочисленного деления и деления по модулю по-другому, нежели такие языки, как Си, C++ и Java (но точно так же, как языки Python и Тс1). Рассмотрим частное от деления -7/3. Результат в виде числа с плаваю- щей точкой будет равен -2.33. Но результат целочисленного деления должен быть целым числом, поэтому это число должно быть округлено. Ruby выпол- няет округление в сторону минус бесконечности и возвращает -3. Си и род- ственные ему языки вместо этого округляют в сторону нуля и возвращают -2. (Это всего лишь один из способов охарактеризовать результаты; разумеется, никакого деления чисел с плавающей точкой на самом деле не происходит.) Важным следствием определения целочисленного деления в Ruby является то, что в нем -а/b равняется а/-Ь, но может не равняться -(а/Ь). Имеющееся в Ruby определение деления по модулю также отличается от того, что определено в Си и Java. В Ruby -7%3 равно 2. А в Си и Java результат будет равен -1. Значение результата, конечно, отличается, поскольку отли- чается значение частного. Но знак результата тоже отличается. В Ruby знак результата всегда такой же, как знак второго операнда. А в Си и Java знак результата всегда такой же, как знак первого операнда. (В Ruby есть также метод remainder, который ведет себя по знаку и результату деления по модулю так же, как оператор деления по модулю языка Си.) Ruby также позаимствовал у Фортрана оператор ** для возведения в степень. По- казатели степени должны быть целыми числами: х**4 # То же самое, что и х*х*х*х х**-1 # То же самое, что и 1/х х**(1/3.0) # То же самое, что и х х**(1/4) # Ничего не получится! Целочисленное деление означает, что мы имеем # дело с выражением, эквивалентным х**0, которое всегда равно 1 х**(1.0/4.0) # Это корень четвертой степени из х
68 Глава 3. Типы данных и объекты Когда операции возведения в степень объединены в одном выражении, они вы- числяются справа налево. Поэтому 4**3**2 равно 4**9, а не 64**2. Возведения в степень могут приводить к очень большим значениям. Следует пом- нить, что целые числа могут иметь любой размер, но объекты Fl oat не могут пред- ставлять числа, большие, чем Float: :МАХ. Поэтому выражение 10**1000 приведет к точному целочисленному результату, но выражение 9.9**1000 вызовет перепол- нение, которое выразится во Float-значении Infinity (бесконечность). Значения Fixnum и Bignum поддерживают стандартные операции с битами —, &, |, А,» и « — которые являются общепринятыми в Си, Java и многих других языках. (Подробности рассмотрены в разделе 4.6.) В дополнение к этому целочисленные значения могут также быть проиндексированы наподобие массивов, чтобы можно было запросить (но не установить) значения отдельных битов. Индекс 0 возвра- щает значение наименее значимого бита: even = (х[0] == 0) # число является четным, если наименее значимый бит равен 0 3.1.4. Двоичное представление чисел с плавающей точкой и ошибки округления Большая часть компьютерного оборудования и большинство языков программи- рования (включая Ruby) выражают вещественные числа приближенно, исполь- зуя представление с плавающей точкой, как это делается в Ruby-классе Fl oat. Для эффективной работы аппаратной части большинство представлений чисел с пла- вающей точкой являются двоичными, способными дать точное представление дробей вроде 1/2, 1/4 и 1/1024. Но, к сожалению, дроби, которыми мы пользуемся наиболее часто (особенно при осуществлении финансовых расчетов) — это 1/10, 1/100, 1/1000 и т. д. Двоичные представления чисел с плавающей точкой не могут создать простое представление чисел вроде 0.1. Объекты Float обладают достаточной точностью и могут составить вполне прием- лемое приближенное представление числа 0.1, но тот факт, что это число не может быть представлено абсолютно точно, приводит к некоторым проблемам. Рассмо- трим следующее простое Ruby-выражение: 0.4 - 0.3 == 0.1 # Во многих реализация вычисляется в false Из-за ошибок округления разница между приближенным представлением чисел 0.4 и 0.3 не является абсолютно равной приближенному представлению числа 0.1. Эта проблема характерна не только для Ruby, от нее также страдают языки Си, Java, JavaScript и все другие языки, использующие числа с плавающей точкой стандарта IEEE-754. Одним из решений этой проблемы является использование десятичного, а не двоичного представления вещественных чисел. Класс BigDecimal из стандартной библиотеки Ruby является одним из таких представлений. Арифметические дей- ствия над объектами BigDecimal выполняются в несколько раз медленнее, чем та- кие же действия над Float-значениями. Их скорость вполне приемлема для типо- вых финансовых вычислений, но недостаточна для научной обработки числовой
3.2. Текст 69 информации. Раздел 9.3.3 включает небольшой пример использования библиоте- ки BigDecimal. 3.2. Текст Текст представлен в Ruby объектами класса St ri ng. Строки являются изменяемыми объектами, и в классе String определяется мощный набор операторов и методов для извлечения подстрок, вставки и удаления текста, поиска, замены и т. д. Ruby предо- ставляет ряд способов для выражения строковых литералов в ваших программах, и часть из них поддерживает мощный синтаксис вставки строк, с помощью которо- го значения произвольных Ruby-выражений могут подставляться в строковые ли- тералы. В следующих разделах рассматриваются строковые и символьные литера- лы и строковые операторы. Полноценный строковый API рассмотрен в разделе 9.1. Текстовые шаблоны в Ruby представлены как объекты Regexp, и в Ruby определен синтаксис для включения регулярных выражений в ваши программы в букваль- ном виде. К примеру, код /[a-z]\d+/ представляет одну букву в нижнем регистре, за которой следует одна или несколько цифр. Регулярные выражения — это широ- ко используемое средство языка, но regexp не является таким же основным типом данных, как числа, строки и массивы. Описание синтаксиса регулярных выраже- ний и Regexp API изложено в разделе 9.2. ТЕКСТ В RUBY 1.8 И RUBY 1.9 Самая существенная разница между Ruby 1.8 и Ruby 1.9 состоит в том, что версия 1.9 предлагает полноценную встроенную поддержку Юникода и других многобайтовых текстовых представлений. Последствия этих изменений имеют разносторонний характер и будут упоминаться на всем протяжении раздела, особенно в подразделе 3.2.6. 3.2.1. Строковые литералы Ruby предоставляет довольно много способов буквальной вставки строковых ли- тералов в ваши программы. 3.2.1.1. Строковые литералы в одинарных кавычках Простейшие строковые литералы заключаются в одинарные кавычки (символы апострофа). Текст внутри кавычек является значением строки: 'Это простейший строковый литерал Ruby' Если знак апострофа нужно поместить внутри строкового литерала, заключенно- го в одинарные кавычки, перед ним нужно поставить символ обратного слэша, чтобы Ruby-интерпретатор не принял его за символ завершения строки: 'Won\'t you read 0\'Reilly\'s book?'
70 Глава 3. Типы данных и объекты Обратный слэш также используется для нейтрализации другого обратного слэ- ша, поэтому второй обратный слэш сам по себе не рассматривается как эскейп- символ. Посмотрите на некоторые случаи, когда возникает потребность в сдвоен- ных символах обратного слэша: 'Этот строчный литерал завершается одиночным обратным слэшем: W 'Это кавычка, перед которой стоит обратный слэш: \\\'' 'Два символа обратного слэша: \\\\' В строках, заключенных в одинарные кавычки, обратный слэш не играет особой роли, если следующий за ним символ отличается от кавычки или от обратного слэша. Поэтому в большинстве случаев обратные слэши в строковых литералах не нуждаются в удваивании (хотя и могут быть удвоены). К примеру, следующие два строковых литерала абсолютно равны: 'а\Ь' == 'а\\Ь' Строки в одинарных кавычках могут распространяться на несколько строк про- граммы, а получающийся в результате строковый литерал включает символы но- вой строки. Нейтрализовать символы новой строки символами обратного слэша невозможно: 'Это длинный строковый литерал. \ Включающий символы обратного слэша и новой строки' Если нужно разбить длинную строку в одинарных кавычках на несколько строк кода без вставки в нее символов новой строки, разбейте ее на несколько смежных строковых литералов, Ruby-интерпретатор объединит их в процессе проведения синтаксического анализа. Но при этом нужно помнить, что вы должны нейтрали- зовать символы новой строки (как было показано в главе 2) между литералами, чтобы Ruby не рассматривал символ новой строки в качестве завершения опера- тора: message = 'Эти три литерала '\ 'объединяются интерпретатором в один. '\ 'В получившейся строке не содержится символов новой строки.' 3.2.1.2. Строковые литералы в двойных кавычках Строковые литералы, заключенные в двойные кавычки, ведут себя более гибко, чем литералы в одинарных кавычках. Литералы в двойных кавычках поддержива- ют довольно большое количество эскейп-последовательностей на основе символа обратного слэша, в том числе \п для символа новой строки, \t для символа табуля- ции и \ ” для кавычки, не завершающей строку: "\Ь\"Этат цитата, начинаясь с табуляции, заканчивается символом новой строки \"\п" "\\" # Одиночный обратный слэш В Ruby 1.9 эскейп-последовательность \и вставляет произвольный Юникод- символ, заданный его кодом, в строку, заключенную в двойные кавычки.
3.2. Текст 71 Эта эскейп-последовательность достаточно сложна, поэтому мы отвели для ее описания отдельный раздел (см. раздел 3.2.1.3). Многие другие эскейп- последовательности на основе обратного слэша ничем особенным не отличаются и используются для кодировки внутри строк двоичных данных. Полный список эскейп-последовательностей показан в табл. 3.1. Более мощные строковые литералы, заключенные в двойные кавычки, могут так- же включать произвольные Ruby-выражения. При создании строки выражение вычисляется и превращается в строку в том месте, где находился сам текст вы- ражения. Эта подстановка вместо выражения его значения известна в Ruby как «вставка в строку». Выражение внутри строки, ограниченной двойными кавычка- ми, начинается с символа # и заключается в фигурные скобки: "360 rpaflycoe=#{2*Math::Р1} радианов" # "360 градусов=6.28318530717959 радианов" Когда выражение, вставляемое в строковый литерал, является простой ссылкой на глобальную переменную, переменную экземпляра или класса, фигурные скоб- ки могут быть опущены: Ssalutation = 'hello' # Определение глобальной переменной ”#$salcitation world" # Ее использование в строке в двойных кавычках Чтобы нейтрализовать специальное действие символа#, нужно использовать сим- вол обратного слэша. Учтите, что это может пригодиться лишь в том случае, если символ, следующий за #, представляет собой {, $ или @: "Мой телефонный #: 555-1234" # Нейтрализация не требуется "Для вставки выражения используйте \#{ " # Нейтрализация #{ символом обратного # слэша ВСТАВКА В СТРОКУ С ИСПОЛЬЗОВАНИЕМ SPRINTF Программисты, работающие на Си, наверное, обрадуются, узнав, что Ruby также поддерживает функции printf и sprintf1 для вставки в строку отформа- тированного значения: sprintff"Значение pi примерно равно X.4f", Math::PI) # Возвращает "Значение pi # примерно равно 3.1416" Преимущества такого способа вставки состоят в том, что отформатирован- ная строка может определять такие параметры, как количество десятичных знакомест для отображения значения объекта Float. В присущем Ruby стиле метод sprintf существует даже в форме оператора: нужно просто использовать оператор % между форматом строки и вставляемыми в нее аргументами: "pi примерно равно X.4f” % Math::PI # Аналогично приведенному выше примеру "£s: И" % ["pi". Math::PI] # Массив в правой части содержит несколько # аргументов Чтобы получить более подробную информацию, воспользуйтесь инструментальным сред- ством ri: ri Kernel. spri ntf
72 Глава 3. Типы данных и объекты Строковые литералы, заключенные в двойные кавычки, могут распространяться на несколько строк, и символы завершения строк станут частью строкового лите- рала, если только перед ними не будет стоять символ обратного слэша: "Этот строковый литерал содержит две строки, \ хотя записан в трех " Возможно, вы предпочтете ввести в свои строки признаки их завершения в яв- ном виде, к примеру для того, чтобы ввести в действие сетевые коды заверше- ния строки CRLF (Carriage Return Line Feed, то есть сочетание возврата каретки и перевода строки), применяющиеся, к примеру, в HTTP-протоколе. Для этого запишите все ваши строковые литералы в одной строке и включите явные призна- ки завершения, воспользовавшись эскейп-последовательностями \г и \п. Учтите, что смежные строковые литералы автоматически объединяются, но если они за- писаны в отдельных строках, символы новой строки между ними должны быть нейтрализованы: "Этот строковый объект содержит три строки.\г\п” \ "Он записан в виде трех смежных литералов,\г\п" \ "разделенных нейтрализованными символами новой строки \г\п" Таблица 3.1. Эскейп-проследовательности на основе обратного слэша в строках, заключенных в двойные кавычки Эскейп- последова- тельность Назначение \ X Обратный слэш перед любым символом х является эквивалентом самого сим- вола х, если только х не является символом завершения строки или одним из специальных символов abcefnrstuvxCM01234567. Этот синтаксис полезен при нейтрализации специального значения таких символов, как \, # и " \а Символ оповещения BEL (ASCII-код 7). Вызывает звуковой сигнал консоли. Эквивалентен \С-д или \007 \Ь \е \f \п \г \s \t \u nnnn Символ забоя (Backspace, ASCII-код 8). Эквивалентен \С-h или \010 Символ ESC (ASCII-код 27). Эквивалентен \033 Символ подачи страницы (ASCII-код 12). Эквивалентен \С-1 и \014 Символ новой строки (ASCII-код 10). Эквивалентен \С-J и \012 Символ возврата каретки (ASCII-код 13). Эквивалентен \C-rn и \015 Символ пробела (ASCII-код 32) Символ TAB (ASCII-код 9). Эквивалентен \С-1 и \011 Значения кодов (codepoint) nnnn Юникода, где каждый символ п является одной из шестнадцатеричных цифр. Ведущие нули не сбрасываются; для этой формы эскейп-последовательности \и требуются все четыре цифры. Поддерживается, начиная с версии Ruby 1.9 \u{ шестнадцате- ричные цифры} \v Значение (или значения) Юникода заданы шестнадцатеричными цифрами. См. описание этой эскейп-последовательности в основном тексте. Поддерживается, начиная с версии Ruby 1.9 Символ вертикальной табуляции (ASCII-код 11). Эквивалентен \С-к и \013
3.2. Текст 73 Эскейп- последова- тельность Назначение \ ппп Байт ппп, где ппп — три восьмеричные цифры, составляющие диапазон между ООО и 377 \ пп Аналогично \ 0 пп, где пп — две восьмеричные цифры, составляющие диапазон между 00 и 77 \ п Аналогично \00п, где п — восьмеричная цифра, составляющая диапазон между 0 и 7 \х пп Байт пп, где пп — две шестнадцатеричные цифры, составляющие диапазон между 00 и FF. (В качестве шестнадцатеричных цифр разрешено использовать буквы как в нижнем, так и в верхнем регистрах) \х п Аналогично \хОп, где п — шестнадцатеричная цифра, составляющая диапазон между 0 и F (или f) \с X \С- х Сокращенный вариант \С-х Символ, код которого формируется путем обнуления шестого и седьмого битов х, сохранения старшего бита и пяти младших битов, х может быть любым символом, но эта последовательность обычно используется для представле- ния управляющих символов от Control-A до Control-Z (ASCII-коды от 1 до 26). Исходя из раскладки таблицы ASCII, вы можете использовать для х буквы как нижнего, так и верхнего регистров. Учтите, что \сх является сокра- щенным вариантом, х может быть любым одиночным символом или эскейп- последовательностью, кроме \С \и, \х, или \ппп \М- х Символ, код которого формируется путем установки старшего бита кода х. Используется для представления метасимволов, которые технически не явля- ются частью набора символов ASCII, х может быть любым одиночным симво- лом или эскейп-последовательностью, кроме \М \и, \х или \ппп. \М может быть скомбинирован с \С, как в \M-\C-A \ eol (конец строки) Обратный слэш перед символом конца строки нейтрализует действие этого символа. Ни обратный слэш, ни символ конца строки в самой строке не по- являются З.2.1.З. Эскейп-последовательности Юникода В Ruby 1.9 строки в двойных кавычках при использовании эскейп-последо- вательности \ц могут включать любые символы Юникода. В ее наипростейшей форме за \и следуют строго четыре шестнадцатеричные цифры (изображающие их буквы могут быть в верхнем или нижнем регистре), которые представляют зна- чение Юникода между 0000 и FFFF. Например: "\u00D7" # => ”х”: лидирующие нули не могут быть отброшены "\u20ac" # => можно использовать буквы в нижнем регистре Во второй форме эскейп-последовательности за \и следует открывающая фигур- ная скобка, от одной до шести шестнадцатеричных цифр и закрывающая фигур- ная скобка. Цифры между скобками могут представлять любое значение Юнико- да между 0 и 10FFFF, и лидирующие нули в этой форме могут быть отброшены: ”\ы{А5}" # => то же самое, что и "\u00A5" "\и{ЗС0}" # Греческая буква пи в нижнем регистре: то же самое, что и "\uO3CO" ”\u{10ffff}” # Самое большое значение Юникода
74 Глава 3. Типы данных и объекты И наконец, форма \и{} этой эскейп-последовательности позволяет вставить в одну такую последовательность несколько значений Юникода. Для этого нужно про- сто поместить внутри фигурных скобок несколько непрерывных серий от одной до шести шестнадцатеричных цифр. После открывающей или перед закрывающей фигурной скобкой пробелов быть не должно: money = "\u{20AC АЗ А5}" # => "€£¥" Учтите, что пробелы внутри фигурных скобок не являются кодами пробелов в самой строке. Но вы можете закодировать ASCII-символ пробела, воспользо- вавшись Юникодом со значением 20: money = "\u{20AC 20 АЗ 20 А5}" # => "€ £ ¥" Строки, использующие эскейп-последовательность \и, кодируются с использова- нием кодовой таблицы Юникода UTF-8. (Более подробная информация о коди- ровании строк изложена в разделе 3.2.6.) Обычно эскейп-последовательности \и вполне допустимы в строках, но так быва- ет не всегда. Если исходные файлы используют кодировку, отличную от UTF-8, и строка содержит многобайтовые символы в этой кодировке (буквенные симво- лы, а не те, которые созданы при помощи эскейп-последовательности), то в такой строке использование \и недопустимо — невозможно в одной строке кодировать символы в двух разных кодировках. Если в качестве исходной используется коди- ровка UTF-8, то использовать \и можно всегда (см. раздел 2.4.1). Эту же эскейп- последовательность всегда можно использовать в строке, которая состоит из од- них ASCII-символов. Эскейп-последовательности \и могут появляться в строках, заключенных в двой- ные кавычки, а также в других формах цитируемого текста (которые скоро будут рассмотрены), таких как регулярные выражения, символьные литералы, строки с %-и ЭД-ограничителями, массивы с ЯШ-ограничителями, here-документы и команд- ные строки с обратными кавычками в качестве ограничителей. Java-программисты должны отметить, что имеющиеся в Ruby эскейп-последовательности \и могут появляться лишь в цитируемом тексте, а не в идентификаторах программы. 3.2.1.4. Произвольные ограничители для строковых литералов При работе с текстом, содержащим апострофы и кавычки, его трудно использо- вать в виде строкового литерала, заключенного в одинарные или двойные кавыч- ки. Для строковых литералов в Ruby поддерживается универсальный синтаксис цитирования (и, как мы увидим чуть позже, он же используется для регулярных выражений и литералов массива). Последовательность служит началом для строки, которая следует правилам строки, заключенной в одинарные кавычки, а последовательность ЗД (или просто X) представляет литерал, который следует правилам строки, заключенной в двойные кавычки. Первый символ, следующий за q или Q, является символом-ограничителем, и строковый литерал продолжа- ется, пока не встретится соответствующий (не подвергшийся нейтрализации)
3.2. Текст 75 ограничитель. Если открывающий ограничитель представляет собой символ (, [, { или <, то соответствующим ему ограничителем будет ), ], } или >. (Учтите, что обратная кавычка ' и апостроф ’ не составляют соответствующую пару.) Во всех остальных случаях закрывающий ограничитель будет точно таким же, как и от- крывающий. Посмотрите на несколько примеров: £q(Don’t worry about escaping ’ characters!) # Нейтрализация символа ’ здесь # не требуется XQ|Oh спросил: "Как дела?"| fc-Этот строковый литерал завершается символом новой строки \п- # здесь Q опущен Если понадобится нейтрализовать действие символа-ограничителя, можно вос- пользоваться символом обратного слэша (даже в более строгой ^q-форме) или просто выбрать другой символ-ограничитель: &}_Этот строковый литерал содержит \_знаки подчеркиваниях_ ЭД!Можно просто воспользоваться _другим_ ограничителем\!! Если используются парные ограничители, то до тех пор, пока они будут появлять- ся в литералах правильно вложенными парами, их нейтрализация не понадобит- ся: # В XML используются парные угловые скобки: X«book><titlе>Слравочник Ruby </title></book>> # Это работающий пример # Выражения, в которых используются парные, вложенные круглые скобки: Щ1+(2*3)) = #{(1+(2*3))}) # Это тоже работающий пример Я(А mismatched paren \( must be escaped) # Здесь нужен символ- # нейтрализатор З.2.1.5. Неге-документы Когда нужно воспользоваться длинными строковыми литералами, для них труд- но подобрать какой-нибудь символ-ограничитель, который можно было бы ис- пользовать, не опасаясь забыть о его нейтрализации где-нибудь внутри литерала. Имеющееся в Ruby решение этой проблемы позволяет задать в качестве ограни- чителей строки произвольную последовательность символов. Эта разновидность литерала позаимствована из синтаксиса оболочки операционной системы Юникс, и за ним исторически закрепилось название here-документ (то есть здесь нахо- дится документ). (Поскольку документ размещен прямо в исходном коде, а не во внешнем файле.) Неге-документы начинаются с последовательности « или «-. Непосредственно за этой последовательностью (без пробелов, чтобы не перепутать с оператором сдвига влево) следует идентификатор или строка, задающая завершающий ограничитель. Текст строкового литерала начинается на следующей строке и продолжается до тех пор, пока в отдельной строке не встретится текст-ограничитель. Например: document = «HERE # Начало here-документа Это строковый литерал. Он состоит из двух строк и неожиданно обрывается... HERE
76 Глава 3. Типы данных и объекты Ruby-интерпретатор получает содержимое строкового литерала, осуществляя по- строчное считывание из своего входящего потока. Но это еще не означает, что по- следовательность « должна быть последним элементом своей строки. В действи- тельности, после считывания содержимого here-документа, Ruby-интерпретатор возвращается назад, к той строке, где была эта последовательность, и продолжает ее синтаксический анализ. К примеру, следующий Ruby-код создает строку путем объединения двух here-документов и обычной строки, заключенной в кавычки: greeting = «HERE + «THERE + "World" Hello HERE There THERE Последовательность «HERE в строке 1 заставляет интерпретатор прочитать строки 2 и 3. А последовательность «THERE заставляет его прочитать строки 4 и 5. После считывания этих строк все три строковых литерала объединяются в один. Завершающий ограничитель here-документа обязательно должен появится в сво- ей собственной строке: за ним не должно быть никаких комментариев. Если here- документ начинается с «, то ограничитель должен находиться в начале строки. Если вместо этого литерал начинается с «-, то ограничитель может иметь перед собой свободное пространство. Символ новой строки в начале here-документа не является частью литерала, но символ новой строки в конце документа входит в его содержимое. Поэтому каждый here-документ заканчивается символом завер- шения строки, за исключением пустого here-документа, который аналогичен empty = «END END Если в качестве признака завершения используется, как и в предыдущем при- мере, идентификатор, не заключенный в кавычки, то here-документ в отношении эскейп-последовательностей с символом обратного слэша и действий символа # ведет себя как строка, заключенная в двойные кавычки. Если нужно, чтобы все воспринималось абсолютно буквально и никакие эскейп-последовательности не действовали, признак завершения документа нужно поместить в одинарные ка- вычки. Тогда в признаке можно будет использовать и пробелы: document = «'THIS IS THE END, MY DNLY FRIEND. THE END' . далее следует большой объем текста. в котором не действуют эскейп-последовательности. THIS IS THE END. MY ONLY FRIEND, THE END Одинарные кавычки вокруг признака завершения дают понять, что этот строко- вый литерал ведет себя как строка, заключенная в такие же кавычки. Но на самом деле эта разновидность here-документа ведет себя еще строже. Поскольку одинар- ная кавычка не является признаком его завершения, ее действие не нужно выклю- чать с помощью символа обратного слэша, также не нужно выключать действие
3.2. Текст 77 самого обратного слэша. Следовательно, в этой разновидности here-документа об- ратные слэши являются обыкновенной составляющей строкового литерала. В качестве ограничителя here-документа можно также использовать и строковый литерал, заключенный в двойные кавычки. Он ничем не будет отличаться от оди- ночного идентификатора, за исключением того, что в нем можно будет использо- вать пробелы: document = «-"# # #’’ # Это единственное место, куда можно вставить комментарий <html ><head><t i 11 e>#{1111e}</1111e></he a d> <body> <hl>#{tit1e}</hl> #{body} </body> </html> # # # Учтите, что способов включения комментариев в here-документ не существует. Исключение составляет место в первой строке, которое находится за лексемой «, и перед началом литерала. Если говорить обо всех символах # этого кода, один из них представляет комментарий, другие три символа позволяют вставить в лите- рал выражения, а остальные служат в качестве ограничителя. З.2.1.6. Выполнение команды, заключенной в обратную кавычку В Ruby поддерживается еще один синтаксис, в котором задействованы цитаты и строки. Когда текст заключен в обратные кавычки (символы ', известные также как обратные апострофы), он рассматривается как строковый литерал, заключен- ный в двойные кавычки. Значение этого литерала передается в специально назван- ный метод Kernel.'. Этот метод исполняет текст как команду оболочки операци- онной системы и возвращает в виде строки все, что эта команда выдает в ответ. Рассмотрим следующий код Ruby: 'Is' В операционной системе Юникс эти четыре символа выдают строку, в которой перечислены имена файлов текущего каталога. Разумеется, что эта команда цели- ком зависит от используемой платформы. Ее примерным эквивалентом в Windows может стать команда 'di г'. В Ruby поддерживается универсальный синтаксис цитирования, которым можно воспользоваться вместо обратных кавычек. Он похож на представленный ранее ЭД-синтаксис, но в нем используется другая символьная последовательность — (для выполнения — eXecute): Xx[ls] Учтите, что текст внутри обратных кавычек (или следующий за &х) обрабатыва- ется как литерал, заключенный в двойные кавычки, а это означает, что в строку могут быть вставлены произвольные Ruby-выражения. Например:
78 Глава 3. Типы данных и объекты if windows listcmd = 'dir' else listcmd = 'Is' end listing = '#{1istcmd}' Но в подобных случаях проще непосредственно вызвать метод обратной кавыч- ки: listing = Kernel(listcmd) З.2.1.7. Строковые литералы и изменчивость Строки в Ruby могут подвергаться изменениям. Поэтому Ruby-интерпретатор не может использовать один и тот же объект для представления двух одинаковых строковых литералов. (У Java-программистов это, скорее всего, вызовет удивле- ние.) Когда Ruby-интерпретатору встречается строковый литерал, он создает но- вый объект. Если вы включаете литерал в тело цикла, то Ruby будет создавать новый объект для каждой итерации. Вы сами можете в этом убедиться, запустив следующий код: 10.times { puts "test".objectjd } Чтобы не снижать эффективность работы программы, следует избегать использо- вания литералов внутри циклов. З.2.1.8. Метод String.new В дополнение ко всем рассмотренным ранее возможностям работы со строковыми литералами, можно создавать строки, используя метод String. new. При отсутствии аргументов этот метод возвращает вновь созданную строку, не имеющую симво- лов. А если указан единственный строковый аргумент, этот метод создает и воз- вращает новый String-объект, который представляет тот же текст, что и объект аргумента. 3.2.2. Символьные литералы Отдельные символы могут включаться в Ruby-программы буквально, если перед ними поставить знак вопроса. При этом отпадает необходимость использования каких-либо кавычек: ?А # Символьный литерал для ASCII-символа А ?” # Символьный литерал для символа двойной кавычки ?? # символьный литерал для знака вопроса Несмотря на то что в Ruby имеется синтаксис символьного литерала, специаль- ный класс для представления отдельных символов отсутствует. К тому же интер- претация символьных литералов при переходе от Ruby 1.8 к Ruby 1.9 претерпела
3.2. Текст 79 изменения. В Ruby 1.8 символьные литералы вычисляются в целочисленную ко- дировку указанного символа. К примеру, ?А — то же самое, что и число 65, посколь- ку ASCII-код для заглавной буквы А — это целое число 65. В Ruby 1.8 синтаксис символьного литерала работает только с ASCII и с символами, имеющими одно- байтовую кодировку В Ruby 1.9 и последующих версиях, символы — это простые строки единичной длины. Поэтому литерал ?А — это по сути то же самое, что литерал 'А', и нет ни- какой насущной потребности в новом коде для этого синтаксиса символьного ли- терала. В Ruby 1.9 синтаксис символьного литерала работает с многобайтовыми символами и может использоваться также с эскейп-последовательностью Юнико- да \и (но только не с его многокодовой формой \u{а b с}): ?\и20АС == ?€ # => true: Только в Ruby 1.9 ?€ == "\u20AC" # => true Теперь синтаксис символьного литерала может быть использован с любыми сим- вольными эскейп-последовательностями, перечисленными ранее в табл. 3.1: ?\t # Символьный литерал для символа табуляции ?\С-х # Символьный литерал для Ctrl-X ?\111 # Литерал для символа с кодом 0111 (восьмеричное число) 3.2.3. Строковые операторы В классе String определяется ряд полезных операторов для работы с текстовыми строками. Оператор + объединяет две строки и возвращает результат в качестве нового Stri ng-объекта: planet = "Земля" "Привет," + " " + planet # Получается "Привет, Земля " Java-программисты могут заметить, что оператор + не преобразует свой правый операнд в строку, это нужно сделать самостоятельно: "Привет, планета N» " + planet_number.to_s # to_s выполняет преобразование в строку Разумеется, в Ruby куда проще воспользоваться вставкой в строку, чем объедине- нием строки с помощью оператора +. При вставке вызов to_s происходит автома- тически: "Привет, планета N“#{planet_number}" Оператор « добавляет свой второй операнд к первому, что должно быть знакомо программистам, работающим с C++. Этот оператор существенно отличается от оператора +; вместо создания и возвращения нового объекта, он изменяет левый операнд: greeting = "Hello" greeting « " " « "World" puts greeting # Выводится "Hello World"
80 Глава 3. Типы данных и объекты Как и оператор +, оператор « не осуществляет преобразование типов в отноше- нии правого операнда. Но если правый операнд представляет собой целое число, то он воспринимается как код символа, и к строке добавляется соответствую- щий символ. В Ruby 1.8 в этом качестве разрешено использование только целых чисел в диапазоне от 0 до 255. В Ruby 1.9 может быть использовано любое целое число, которое представляет действующий код, применяемый для кодирования строк: alphabet = "А" alphabet « ?В # Теперь переменная alphabet имеет значение "АВ" alphabet «67 # А теперь она имеет значение "АВС" alphabet « 256 # Ruby 1.В выдаст ошибку: коды должны быть >=0 и < 256 Оператор * рассчитывает, что правый операнд будет целым числом. Он возвраща- ет String-объект, в котором текст, заданный в левом операнде, повторяется столь- ко раз, сколько задано правым операндом: ellipsis = '.'*3 # Вычисляется в ’...' Если слева от оператора будет стоять строковый литерал, то перед выполнением повторения будут произведены все вставки. Стало быть, следующий замыслова- тый код не будет отвечать вашему возможному замыслу: а = 0: ”#{а=а+1} " * 3 # Возвращает "1 1 1 ”. а не "1 2 3 ” В классе String определяются все стандартные операторы сравнения. Операторы == и ! = сравнивают строки на равенство и неравенство. Две строки будут равны лишь в том случае, когда у них одинаковая длина и все символы равны. Опера- торы <, <=, > и >= сравнивают относительный порядок построения строк путем со- поставления кодов символов, составляющих эти строки. Если одна строка явля- ется префиксом другой, то более короткая строка считается меньшей, чем более длинная. Сравнение основано строго на кодах символов. Никакой нормализации не проводится, и порядок следования символов естественного языка (если он от- личается от числовой последовательности кодов символов) игнорируется. Операции сравнения строк чувствительны к регистру1. Следует помнить, что в ASCII все буквы верхнего регистра имеют меньшие значения кодов, чем буквы нижнего регистра. Это, к примеру, означает, что "Z" < "а". Для сравнения ASCII- символов без учета регистра следует воспользоваться методом casecmp (рассмо- тренным в разделе 9.1) или перед сравнением привести строки к единому регистру с помощью методов downcase или upcase. (При этом нужно помнить, что «познания» Ruby в буквах верхнего или нижнего регистра ограничиваются набором символов ASCII.) 1 В Ruby 1.8 установка значения не рекомендуемой к использованию глобальной перемен- ной $= в t rue, делает ==, < и родственные им операторы сравнения нечувствительными к ре- гистру. Тем не менее этого делать не стоит; установка этой переменной вызывает появле- ние предупреждающего сообщения, даже если Ruby-интерпретатор вызван без ключа -w. В Ruby 1.9 действие переменной $= уже не поддерживается.
3.2. Текст 81 3.2.4. Получение доступа к символам и подстрокам Возможно, самым важным оператором, поддерживаемым в классе St г 1 ng, является оператор индекса массива в виде квадратных скобок — [], который используется для извлечения или изменения части строки. Этот оператор весьма гибок и может быть использован с целым рядом различных типов операндов. Он также может быть использован в левой части оператора присваивания в качестве способа из- менения содержимого строки. В Ruby 1.8 строка подобна массиву байтов или восьмибитных кодов символов. Длина этого массива выдается методом length или size, и получение или уста- новка значения элементов массива происходит обыкновенным указанием номера символа, помещенного в квадратные скобки: s = 'hello': # Ruby 1.8 s[0] # 104: код ASCII-символа первой буквы 'h' s[s.length-1] # 111: код последнего символа 'o' sE-1] # 111: еще один способ доступа к последнему символу s[-2] # 108: второй с конца символ s[-s.length] # 104: еще один способ доступа к первому символу s[s.length] # nil: символов с таким индексом не существует Заметьте, что отрицательные индексы массива указывают на количество позиций, начиная с 1, отсчитываемых с конца строки. Обратите также внимание и на то, что Ruby не выдает исключения, если предпринимается попытка получить доступ к символу за пределами строки, вместо этого он просто возвращает значение nil. Ruby 1.9 при указании индекса возвращает вместо кодов символов односимволь- ную строку. Следует учесть, что при работе со строками, состоящими из символов, закодированных с использованием различного числа байтов, произвольный до- ступ к символам менее эффективен, чем доступ к исходным байтам: s = 'hello': # Ruby 1.9 s[0] # 'h' первый символ строки, возвращенный в виде строки s[s.length-1] # 'o' последний символ 'о' s[-l] # 'o’ еще один способ доступа к последнему символу s[-2] # второй символ от конца s[-s.length] # 'h' еще один способ доступа к первому символу s[s.length] # nil символов с таким индексом не существует Чтобы изменить отдельные символы строки, нужно использовать квадратные скобки в левой части выражения присваивания. В Ruby 1.8 правая часть этого вы- ражения должна быть ASCII-кодом символа или строкой. В Ruby 1.9 правая часть выражения должна быть строкой. В обеих версиях языка можно использовать символьные литералы: s[0] = ?Н # Замена первого символа на заглавную Н s[-l] = ?0 # Замена последнего символа на заглавную О s[s.length] = ?! # ОШИБКА! Присваивание за пределами строки невозможно
82 Глава 3. Типы данных и объекты Правая часть такого оператора присваивания не обязательно должна быть кодом символа: это может быть любая строка, включая многосимвольную или пустую строку Кроме того, это работает как в Ruby 1.8, так и в Ruby 1.9: s = "hello" # Пример начинается с приветствия s[-l] = # Удаление последнего символа, теперь s - это "hell" s[-l] = ”р! ” # Изменение последнего обновленного символа и добавление # еще одного, теперь s - это "help!" Чаще всего из строки нужно извлечь не отдельные коды символов, а подстроки. Для этого нужно использовать два разделенных запятой операнда, заключенные в квадратные скобки. Первый операнд устанавливает индекс (который может иметь и отрицательное значение), а второй — длину (которая должна иметь по- ложительное значение). В результате получается подстрока, которая начинается с символа, соответствующего указанному индексу, и продолжается на указанное количество символов: s = "hello" s[0,2] s[-l,l] s[0,0] s[0,10] s[s.length, 1] s[s.length+1,1] s[0.-1] # "he" # "о": возвращает строку, а не код символа ?о # подстрока нулевой длины всегда пустая # "hello": возвращает все доступные символы # ””: пустая строка сразу за последним символом # nil: за пределами длины читать уже нечего # nil: отрицательное значение длины смысла не имеет Если присвоить проиндексированной таким образом строке какую-нибудь дру- гую строку, то указанная подстрока будет заменена на новую строку. Если в пра- вой части будет пустая строка, то произойдет удаление, а если в левой части будет указана нулевая длина, произойдет вставка: s = "hello" s[0,1] = ”Н" # Замена первой буквы заглавной буквой s[s.length.0] = ” world" # Добавление путем присваивания за концом строки s[5,0] = "," # Вставка запятой без удаления прежнего содержимого s[5,6] = "" # Удаление без вставки; s == "Hellod" Другой способ извлечения, вставки, удаления или замены подстроки связан с ин- дексированием строки при помощи объекта Range (диапазон). Чуть позже, в разде- ле 3.5, мы рассмотрим диапазоны более подробно. А сейчас нам достаточно знать, что Range — это целые числа, разделенные точками. Когда Range используется для индексирования строки, возвращаемое значение является подстрокой, символы которой являются частью диапазона: s = "hello" s[2..3] s[-3..-1] SCO..0] s[0...0] s[2..1] s[7..10] # "11": символы 2 и 3 # "По": отрицательные индексы тоже работают # "h": этот диапазон включает индекс одного символа # "": этот диапазон пуст # "": этот диапазон тоже пуст # nil: этот диапазон лежит за пределами строки
3.2. Текст 83 s[-2..-l] = "р!" # Замена: s приобретает значение "help!" s[0...0] = "Please " # Вставка: s приобретает значение "Please help!" s[6..1O] = ”" # Удаление: s приобретает значение "Please!" С этой формой, использующей единственный Range-объект, не следует путать ин- дексацию с использованием двух целых чисел, разделенных запятой. Хотя в обеих формах используется по два целых числа, между ними есть существенное разли- чие: в форме, где используется запятая, задается индекс и длина, а в форме, где используется Range-объект, задаются два индекса. Строку можно также проиндексировать с помощью другой строки. При этом воз- вращаемое значение является первой подстрокой целевой строки, которая соот- ветствует строке, используемой для индексирования, или nil, если соответствий найдено не было. Польза от применения этой формы индексирования строки ре- ально проявляется только в левой части оператора присваивания, когда нужно заменить соответствующую строку какой-нибудь другой строкой: s = "hello" # Начинаем со слова "hello" whi1e(s["1"]) # Пока строка содержит подстроку "1" s["l"] = "L"; # Заменить первый попавшийся символ "1" на символ "L" end # Теперь у нас есть слово "heLLo" И наконец, строку можно индексировать с использованием регулярного выраже- ния. (Объекты регулярных выражений будут рассматриваться в разделе 9.2.) В ре- зультате будет получена первая соответствующая шаблону подстрока, и опять же эта форма индексации строки наиболее полезна в левой части оператора присваи- вания: s[/[aelou]/] = # Замена первой гласной буквы звездочкой 3.2.5. Выполнение итераций в отношении строк В Ruby 1.8, в классе String определен метод each, который осуществляет по- строчный перебор строкового объекта. В класс String включены методы модуля Enumerable, и они могут использоваться для обработки имеющихся в строковом объекте отдельных строк. Чтобы осуществить последовательный перебор байтов строки, в Ruby 1.8 можно использовать итератор each byte, но его преимущества по сравнению с использованием оператора [] незначительны, поскольку в вер- сии 1.8 произвольный доступ к байтам осуществляется так же быстро, как и по- следовательный. В Ruby 1.9 ситуация в корне отличается. В этой версии метод each отсутству- ет, а класс String больше не содержит методы модуля Enumerable. Вместо each в Ruby 1.9 определены три строковых итератора с конкретными именами: each_ byte осуществляет последовательный перебор отдельных байтов, составляющих строку; each_char осуществляет последовательный перебор символов; a each_11ne осуществляет последовательный перебор строк. Если нужно провести посимволь- ную обработку строки, то, скорее всего, использование метода each_char станет
84 Глава 3. Типы данных и объекты более эффективным способом выполнения этой задачи, чем применение операто- ра [ ] и индексов символов: s = "¥1000" s.each_char {|х| print ”#{х} " } # Выводит "¥100 0". Ruby 1.9 0.upto(s.size-1) {|i| print "#{s[i]} "} #C многобайтовыми символами этот # вариант работает неэффективно 3.2.6. Кодировка строк и многобайтовые символы Строки в Ruby 1.8 и Ruby 1.9 имеют существенные отличия. О В Ruby 1.8 строки являются последовательностью байтов. Когда строки исполь- зуются для представления текста (а не двоичных данных), предполагается, что каждый байт строки представляет собой отдельный ASCII-символ. В версии 1.8 отдельными элементами строки являются не символы, а числа — фактическое значение байта или код символа. О В Ruby 1.9 строки являются настоящими последовательностями символов, и эти символы не должны ограничиваться набором ASCII. В версии 1.9 отдель- ными элементами строки являются символы, представляющие собой строку единичной длины, а не целочисленные коды символов. У каждой строки есть кодировка, определяющая соответствие между байтами строки и символами, которые эти байты представляют. В такой кодировке, как UTF-8, относящейся к Юникоду, для каждого символа используется различное количество байтов, и соотношение между байтами и символами один к одному (и даже два к одно- му) уже не действует. В следующем подразделе объясняются свойства строки в Ruby 1.9, связанные с кодировкой, а также демонстрируется элементарная поддержка многобайтовых символов в Ruby 1.8 с использованием библиотеки jcode. З.2.6.1. Многобайтовые символы в Ruby 1.9 Класс String в Ruby 1.9 был переписан с целью получения возможности понимать многобайтовые символы и уметь их правильно обрабатывать. Хотя поддержка многобайтовых символов является самым существенным изменением в Ruby 1.9, эти изменения не столь очевидны: просто появился код, нормально работающий с многобайтовыми символами. Тем не менее в механизме его работы стоит разо- браться. Если строка содержит многобайтовые символы, то количество байтов не соответ- ствует количеству символов. В Ruby 1.9 методы length и size возвращают количе- ство символов в строке, а новый метод bytesize возвращает количество байтов: # -*- coding: utf-B -*- # Используются символы Юникода в кодировке UTF-B # Этот строковый литерал содержит многобайтовый символ умножения s = "2x2=4”
3.2. Текст 85 # Строка содержит б байтов, являющихся кодировкой 5 символов s.length # => 5: Символы: '2' ’х' ’2’ ' = ' '4' s.bytesize # => 6: Байты (шестнадцатеричные): 32 сЗ 97 32 3d 34 Обратите внимание на то, что первая строка является coding-комментарием, устанавливающим, что исходный код имеет кодировку UTF-8 (см. раздел 2.4.1). Без этого комментария Ruby-интерпретатор не будет знать, как декодировать последовательность байтов в строковом литерале в последовательность симво- лов. Когда строка содержит символы, закодированные разным количеством байтов, непосредственное отображение индекса символа на смещение байта в строке уже невозможно. К примеру, в приведенной выше строке второй символ начи- нается во втором байте. Но третий символ начинается в четвертом байте. Поэто- му на быстрый произвольный доступ к любому символу строки рассчитывать не приходится. Когда в строке, имеющей многобайтовые символы, для доступа к символу или подстроке используется оператор [], Ruby должен провести вну- тренний последовательный перебор строки, чтобы найти нужный символ по ин- дексу. Поэтому в большинстве случаев нужно везде, где только можно, пытаться использовать для обработки строки последовательные алгоритмы. То есть по возможности использовать итератор each char вместо повторных вызовов опе- ратора []. С другой стороны, вряд ли стоит уделять этому слишком пристальное внимание. Ruby оптимизирует все, что только можно, и если строка содержит исключи- тельно однобайтовые символы, то произвольный доступ к таким символам будет вполне эффективен. Если хотите осуществить оптимизацию своими силами, то можете воспользоваться методом экземпляра asci i only? и определить, состоит ли вся строка из семибитных ASCII-символов. В классе Stri ng версии Ruby 1.9 определяется метод encodi ng, который возвращает кодировку строки (возвращаемое значение является объектом Encoding, который будет рассмотрен чуть позже): # coding: utf-8 s = "2x2=4" # Обратите внимание на многобайтовый знак умножения s.encoding # => <Encoding: UTF-8> t = ”2+2=4" # Все символы принадлежат поднабору ASCII кодировки UTF-8 t.encoding # => <Encoding: ASCII-8BIT> Кодировка строкового литерала основана на исходной кодировке файла, в кото- ром он появляется. Но его кодировка не всегда совпадает с исходной. Если, к при- меру, строковый литерал содержит только семибитные ASCII-символы, метод encoding вернет ASCII, даже если исходная кодировка — UTF-8 (набор, в который входит и ASCII). Такая оптимизация позволит строковым методам узнать, что все символы в строке однобайтовые. К тому же если строковый литерал содержит эскейп-последовательности \и, то его кодировка будет UTF-8, даже если исходная кодировка от нее отличается.
86 Глава 3. Типы данных и объекты ASCII И КОДИРОВКА BINARY Ранее упомянутое название кодировки «ASCII-8BIT», является используемым в Ruby 1.9 именем для унаследованной кодировки, которая использовалась в Ruby 1.8; это набор ASCII-символов, не имеющий ограничений на исполь- зование не выводимых на печать и управляющих символов. В этой кодировке один байт всегда равен одному символу и строка может содержать как двоич- ные, так и символьные данные. Некоторые методы Ruby 1.9 требуют указания названия кодировки (или им нужен объект Encoding — см. далее). Эту ASCII-кодировку можно указать как «ASCII-8BIT» или воспользоваться ее псевдонимом «BINARY». Как ни странно, но это так: исходя из интересов Ruby, последовательность байтов, рассматриваемая как не имеющая кодировку («BINARY»), — это то же са- мое, что и последовательность восьмибитных ASCII-символов. Поскольку кодировка «BINARY» на самом деле означает «незакодированные байты», эту же кодировку можно указать, передав nil вместо названия кодировки или объекта Encoding. В Ruby 1.9 также поддерживается кодировка под названием «US-ASCII», которая является истинной семибитной ASCII-кодировкой; она отличается от ASCII-8BIT тем, что не допускает никаких байтов с установленным вось- мым битом. Некоторые строковые операции, такие как объединение строк и их проверка на соответствие шаблону, требуют, чтобы две строки (или строка и регулярное выра- жение) имели совместимые кодировки. К примеру, если объединять ASCII-строку со строкой UTF-8, будет получена строка UTF-8. Но объединить строку UTF-8 со строкой SJIS невозможно; эти кодировки несовместимы, и в результате будет вы- дано исключение. Две строки (или строку и регулярное выражение) можно про- верить на совместимость кодировок путем использования метода класса Encoding, compatible?. Если кодировки двух аргументов совместимы, он возвращает назва- ние той из них, в набор которой входит другая тестируемая кодировка. Если коди- ровки несовместимы, метод возвращает ni 1. Воспользовавшись методом force encoding, можно установить кодировку строки явным образом. Этот метод будет полезен, если имеется строка байтов (возможно, считанная из потока ввода-вывода), и нужно сообщить Ruby, как именно байты должны быть интерпретированы в качестве символов. Или если имеется строка многобайтовых символов, но нужно индексировать отдельные байты с помощью оператора []: text = stream.readline.force_encoding("utf-8") bytes = text.dup.force_encoding(ni1) # кодировка nil означает binary Метод force_encoding не создает копии своего получателя; он модифицирует ко- дировку и возвращает строку. Этот метод не производит никаких изменений сим- волов, исходные байты строки не меняются, изменяется лишь их интерпретация языком Ruby. Как показано выше, аргумент метода force encoding может быть на- званием кодировки или значением ni 1, определяющим двоичную (binary) коди- ровку. Чтобы указать кодировку, можно также передать объект Encoding.
3.2. Текст 87 Метод force encodi ng не выполняет проверок; он не проверяет, представляют ли исходные байты строки допустимую последовательность символов в определен- ной кодировке. Для проверки следует воспользоваться методом val id_encoding?. Этот метод экземпляра не требует аргументов и проверяет, используя кодировку строки, могут ли ее байты интерпретироваться в качестве допустимой последова- тельности символов: s = "\ха4",force_encoding("utf-8") # Для кодировки UTF-8 это строка недопустима s.valid_encoding? # => false Применяемый к строке метод encode (и его вариант-мутатор encode!) существенно отличается от метода force_encoding. Он возвращает строку, которая представляет ту же последовательность символов, что и у его получателя, но использует другую кодировку. Чтобы изменить кодировку (или перекодировать) подобной строки, метод encode должен изменить исходные байты, из которых эта строка состоит. Приведем пример: # coding: utf-8 eurol = "\u20AC" puts eurol eurol.encoding eurol.bytesize # Начнем с символа Евро в кодировке Юникод # Выводится "€" # => <Encoding:UTF-8> # => 3 euro2 = eurol.encodeCiso-8859-15") # Перекодировка в Latin-15 puts euro2.inspect # Выводится "\xA4" euro2.encoding # => <Encoding:iso-8859-15> euro2.bytesize # => 1 еигоЗ = euro2.encode("utf-8") eurol == еигоЗ # Обратная перекодировка в UTF-8 # => true Следует учесть, что потребности в использовании метода encode возникают неча- сто. Скорее всего, перекодирование строк потребуется перед их записью в файл или отправкой через сетевое подключение. Но в разделе 9.7.2 мы сможем убедить- ся, что имеющиеся в Ruby классы потоков ввода-вывода поддерживают автомати- ческую перекодировку текста при осуществлении его внешней записи. Если строка, для которой вызывается encode, состоит из байтов с неуказанной кодировкой, нужно указать эту кодировку для интерпретирования этих байтов перед их перекодировкой. Это делается путем передачи методу encode двух аргу- ментов. Первый аргумент — требуемая кодировка, а второй — текущая кодировка строки. Например: # Интерпретация байта в кодировке iso-8859-15, и перекодировка его в UTF-8 byte = "\хА4" char = byte.encode("utf-8", "iso-8859-15") То есть следующие две строки имеют одинаковый эффект: text = bytes.encode(HyiKHaB_KOflHpoBKa. исходная_кодировка) text = bytes.dup.force_encodi ng(исходная_кодировка).encode(нужная_кодировка)
88 Глава 3. Типы данных и объекты Кодировки символов различаются не только по их преобразованию байтов в символы, но и по тому набору символов, который может быть ими представлен. В Юникоде (известном также как UCS — Universal Character Set, то есть универ- сальный набор символов), предпринята попытка предоставить все существующие символы, но кодировки символов, не основанные на Юникоде, могут представлять только поднабор символов. Поэтому, к примеру, не представляется возможным перекодировать все UTF-8-строки в EUC-JP; символы Юникода, не относящиеся к латинским или японским, не могут быть преобразованы в другую систему. Если метод encode или метод encode! встретит символ, не поддающийся перекоди- ровке, он выдаст исключение. # В кодировке iso-B859-l отсутствует знак Евро, поэтому выдается исключение "\u20AC".encode!"iso-8859-1") 3.2.6.2. Класс Encoding Имеющийся в Ruby 1.9 класс Encoding представляет кодировку символов. Объ- екты Encoding служат в качестве неявных идентификаторов кодировки и не отли- чаются широким набором собственных методов. Метод паше возвращает название кодировки. Метод to_s является синонимом метода пате, а метод i nspect превра- щает объект Encoding в более содержательную строку, чем та, что выдается мето- дом пате. Для каждой поддерживаемой языком встроенной кодировки, в Ruby есть констан- та, предоставляющая самый простой способ жесткого задания кодировки в вашей программе. Предопределенные константы имеют как минимум следующий со- став: Encoding: Encoding: Encoding: Encoding: ASCII_8BIT UTF_8 EUC_JP SHIFT JIS # To же, что и ::В INARY # Юникод-символы в кодировке UTF-8 # Японские символы в кодировке EUC # Японский набор символов: то же, что и ::SJIS, # ::WINDOWS_31J, ::СР932 Следует заметить, что поскольку речь идет о константах, они должны быть записа- ны символами верхнего регистра, и дефисы в названиях кодировок должны быть превращены в символы подчеркивания. Ruby 1.9 также поддерживает кодировку US-ASCII, европейские кодировки от ISO-8859-1 до ISO-8859-15, и кодировки Юникода UTF-16 и UTF-32 в вариантах обратного и прямого порядка следова- ния байтов, но они являются не встроенными, а динамически подгружаемыми по мере необходимости, и до тех пор пока они не загружены, константы для них не существуют. Если есть название кодировки в виде строки и нужно получить соответствующий объект Encoding, используется фабричный метод Encoding.find: encoding = Encoding.find! "utf-8") Использование Encodi ng. fi nd приводит, если это необходимо, к динамической за- грузке указанной кодировки. Метод Encoding.find воспринимает названия коди-
3.2. Текст 89 ровок как в верхнем, так и в нижнем регистре. Для получения названия кодировки в виде строки нужно вызвать метод name, принадлежащий объекту Encoding. Если нужен список доступных кодировок, вызывается метод Encodi ng. 11 st, кото- рый возвращает массив объектов Encodi ng. Метод Encodi ng. 1 i st выдает лишь спи- сок встроенных кодировок и любых кодировок, которые уже были динамически загружены. Вызов Encoding. find может привести к загрузке новых кодировок, ко- торые будут включены в список при следующих вызовах Encodi ng. 1 i st. Для получения объекта Encoding, представляющего внешнюю кодировку, ис- пользуемую по умолчанию (рассмотренную в разделе 2.4.2), вызывается метод Encoding.default external. Для получения кодировки для текущего языка нужно вызвать метод Encoding, locale_charmap и передать полученную от него строку ме- тоду Encodi ng. f i nd. Большинство методов, ожидающих использование объекта Encodi ng, смогут также воспринять ni 1 в качестве синонима для Encoding::BINARY (то есть незакодирован- ных байтов). Большинство методов также будут воспринимать вместо объекта Encoding названия кодировок (такие как ascii, binary, utf-B, euc-jp или sjis). Для осуществления перекодировки кодировки, поддерживаемые методом encode, могут быть расширенным набором кодировок, поддерживаемых классом Encoding. То есть (в зависимости от того, какие кодировки поддерживаются вашей верси- ей и установленным пакетом Ruby) вполне возможно использовать encode для перекодировки строки символов в строку байтов, которую Ruby не может интер- претировать в качестве символов. К примеру, это может понадобиться при связи с устаревшим сервером, для которого требуется применение необычной кодиров- ки символов. Объекты Encoding можно передавать методу encode, но при работе с кодировками, не поддерживаемыми классом Encodi ng, потребуется указать на- звание кодировки в виде строки. 3.2.6.3. Многобайтовые символы в Ruby 1.8 Обычно Ruby 1.8 рассматривает все строки в качестве последовательностей вось- мибитных байтов. Но в стандартной библиотеке, в модуле jcode есть элементарная поддержка многобайтовых символов (использующая кодировки UTF-8, EUC или SJIS). Для использования этой библиотеки нужно востребовать модуль jcode и устано- вить глобальную переменную $KCODE на ту кодировку, которая используется ваши- ми многобайтовыми символами. (В качестве альтернативы можно использовать при запуске Ruby-интерпретатора ключ командной строки -К.) В библиотеке jcode для объектов String определен новый метод jlength: он возвращает длину строки в символах, а не в байтах. Существующие в версии 1.8 методы length и size, изме- нениям не подвергаются — они возвращают длину строки в байтах. Библиотека jcode не изменяет оператор индексирования массива при работе со строками и не позволяет получить произвольный доступ к символам, составляю- щим строку с многобайтовыми символами. Но в ней определен новый итератор под названием each_char, который работает как стандартный метод each byte, но
90 Глава 3. Типы данных и объекты передает каждый символ строки (в виде строки, а не в виде кода символа) предо- ставленному вами блоку кода: $KCODE = "и" require "jcode" mb = "2\303\2272=4" mb.length mb.jlength mb.mbchar? mb.each_byte do |c| print c, " " end mb.each_char do |c| print c. " " End # Указание на Юникод UTF-8 или запуск Ruby с ключом -Ku # Загрузка поддержки многобайтовых символов # Это "2x2=4" co знаком умножения из набора Юникода # => 6: в этой строке б байт # => 5: но только 5 символов # => 1: позиция первого многобайтового символа, или nil # Перебор байтов строки. # "с" содержит объект Fixnum # Выводится "50 195 151 50 61 52 " # Перебор символов строки # "с" - String-объект, у которого jlength равен 1, # a length может изменяеться от символа к символу # Выводится "2x2=4" Библиотека jcode также модифицирует несколько существующих методов, при- надлежащих Stri ng, в числе которых chop, del ete и tr, приспосабливая их для рабо- ты со строками, имеющими многобайтовые символы. 3.3. Массивы Массивы представляют собой последовательность значений, позволяющую иметь к этим значениям доступ по их позициям, по индексам, выстроенным в последова- тельность. В Ruby первое значение массива имеет индекс, равный нулю. Методы size и length возвращают количество элементов массива. У последнего элемента массива индекс равен size-1. Отрицательные значения индексов отсчитываются с конца массива, поэтому к последнему элементу массива можно получить доступ по индексу -1. Второй с конца элемент имеет индекс -2 и т. д. При попытке считать значение элемента за концом массива (когда указанный индекс >= size) или перед его началом (когда указанный индекс < -size) Ruby просто возвращает nil и не выдает никакого исключения. Массивы в Ruby не типизированы и изменяемы. Элементы массива не обязаны быть одного и того же класса и в любое время могут подвергаться изменениям. Более того, размеры массивов могут динамически изменяться; к ним можно до- бавлять элементы, и они по необходимости будут расти в размерах. Если присво- ить значение элементу за концом массива, массив автоматически расширится на элементы со значением nil. (Но присвоение значения элементу перед началом массива является ошибкой.) Литерал массива является разделенным запятыми перечнем значений, заключен- ным в квадратные скобки: [1, 2. 3] [-10...О, 0..10,] # Массив, содержащий три объекта Fixnum # Массив из двух диапазонов: допускается конечная запятая
3.3. Массивы 91 [[1.2],[3,4],[5]] # Массив из вложенных массивов [х+у, х-у, х*у] # Элементы массива могут быть произвольными выражениями [] # Пустой массив имеет нулевой размер Для выражения литералов массивов, элементы которых являются короткими строками без пробелов, в Ruby имеется особый синтаксис: words = £w[this Is a test] open = ЭД| ( [ { < | white = ЭД(\з \t \r \n) # To же самое, что и: ['this'. 'is', 'a', 'test'] # To же самое, что и: [' (', '[’, ’{', '<’] # То же самое, что и: ["\s”, ”\t", "\r", "\n”] Литерал массива вводится с помощью последовательностей и ЭД, которые во многом схожи с!ри ЭД, с помощью которых вводился литерал String. В частно- сти, правила, относящиеся к ограничителям для ЭД и ЭД, совпадают с правилами, применяемыми к ЭД и ЭД. Внутри ограничителей действуют следующие правила: вокруг строк, относящихся к элементам массива, не требуется никаких кавычек, а между элементами не требуется никаких запятых. Элементы массива разделя- ются пробелами. Массив можно также создать с помощью конструктора Array. new, что дает возмож- ность программной инициализации элементов массива: empty = Array.new # []: возвращает новый пустой массив nils = Array.new(3) # [nil, nil. nil]: новый массив из трех элементов nil zeros = Array.new(4, 0) # [0, 0, 0, 0]: новый массив из четырех элементов 0 сору = Array.new(niIs) # Создание новой копии сущестующего массива count = Array.new(3) {|i| i+1} # [1,2,3]: Три элемента, вычисленные из индекса Для получения значения элемента массива следует использовать простое целое число в квадратных скобках: а = [0, 1, 4. 9. 16] # Массив содержит квадраты индексов элементов а[0] # Значение первого элемента равно 0 а[-1] # Значение последнего элемента равно 16 а[-2] # Значение второго с конца элемента равно 9 а[а.size-1] # Другой способ запроса значения последнего элемента a[-a.size] # Другой способ запроса значения первого элемента а[8] # Запрос значения элемента за концом массива возвращает nil а[-8] # Запрос значения элемента перед началом массива также # возвращает nil Все вышеперечисленные выражения, за исключением последнего, могут быть так- же использованы в левой части оператора присваивания: а[0] = "zero" а[-1] = 1..16 а[8] = 64 а[-9] = 81 # а содержит ["zero", 1, 4. 9. 16] # а содержит ["zero", 1, 4, 9, 1..16] # а содержит ["zero”, 1, 4, 9, 1..16, nil, nil, nil, 64] # Ошибка: присваивание перед началом массива невозможно Как и строки, массивы могут также быть индексированы с помощью двух целых чисел, которые представляют начальный индекс и количество элементов, или Range-объект. В любом случае выражение возвращает указанный подмассив:
92 Глава 3. Типы данных и объекты а = ('а'..'е').to а а[0,0] а[1.1] аЕ-2,2] а[0..2] аЕ-2..-1] а[0...-1] # Диапазон, превращаемый в ['а', 'Ь'. 'с', 'd'. 'е'] # []: Этот подмассив содержит нуль элементов # Е'Ь']: одноэлементный массив # ['d'.'e']: последние два элемента массива # ['а'. 'Ь', 'с']: первые три элемента массива # ['d'.’e']: последние два элемента массива # Е’а’, 'b'. 'с', 'd']: все элементы, кроме последнего При использовании в левой части оператора присваивания подмассив может быть заменен элементами массива из правой части. Эта базовая операция также работа- ет для осуществления вставки и удаления элементов: а[0,2] = Е’А’, 'В'] а[2...5]=Е’С'. 'О', а[0,0] = [1,2,3] а[0..2] = [] аЕ-1,1] = E’Z’] аЕ-1.1] = 'Z' аЕ-2,2] = nil # а становится ['А', ’В’, ’с', 'd', 'е'] 'Е'] # а становится ['А'. 'В', 'С, 'D'. 'Е'] # Вставка элементов в начало массива а # Удаление этих элементов # Замена последнего элемента другим # Для одного элемента массив можно не использовать # Удаление последних 2 элементов в версии 1.8: # замена значениями nil в версии 1.9 В дополнение к оператору квадратных скобок, используемому для индексирова- ния массива, в классе Array определяется ряд других полезных операторов. Для объединения двух массивов используется оператор +: а = [1, 2, 3] + [4. 5] а = а + [[6. 7, В]] а = а + 9 # [1. 2. 3. 4. 5] # [1, 2. 3, 4, 5, [6. 7, 8]] # Ошибка: правая часть должна быть массивом Оператор - вычитает один массив из другого. Сначала создается копия массива, указанного в левой части, а затем из этой копии удаляются любые элементы, если они появляются где-нибудь в массиве, указанном в правой части: Е’а', 'Ь'. 'с'. 'Ь'. 'а'] - Е'Ь'. 'с', ’d’] # Е’а'. 'а'] Оператор + создает новый массив, содержащий элементы обоих операндов. Для добавления элементов к концу существующего массива следует воспользоваться оператором «: а = [] а « 1 а « 2 « 3 а « [4,5,6] # Начнем с пустого массива # а содержит [1] # а содержит [1, 2, 3] # а содержит [1, 2, 3, [4. 5. 6]] Так же как и в классе St г 1 ng, в классе Array для повторений используется оператор умножения: а = [0] * 8 # [0, 0, 0, 0. 0, 0, 0, 0] Класс Array позаимствовал булевы операторы | и & и использует их для объеди- нения и логического произведения. Оператор | объединяет свои аргументы в це- почку, а затем удаляет из результата все продублированные элементы. Опера- тор & возвращает массив, который содержит элементы, появляющиеся в обоих
3.4. Хэши 93 массивах, используемых в качестве операндов. Возвращаемый массив не содер- жит продублированных элементов: а = [1. 1. 2, 2, 3, 3, 4] b = [5. 5, 4. 4. 3, 3, 2] а | b # [1, 2, 3. 4. 5]: дубликаты удаляются Ь | а # [5, 4. 3. 2. 1]: Элементы те же. но порядок их следования другой а & b # Е2, 3, 4] Ь & а # [4. 3, 2] Заметьте, что эти операторы не транзитивны: к примеру, а | Ь — это не одно и то же, что Ь| а. Но если вы не обращаете внимания на порядок следования элементов и рассматриваете массивы в качестве неупорядоченных наборов элементов, эти операторы приобретают более весомое значение. Следует также отметить, что алгоритм, с помощью которого осуществляется объединение и логическое про- изведение, не определен, и нет никаких гарантий порядка следования элементов в возвращаемых массивах. В классе Array определено довольно много полезных методов. Но здесь мы рассмо- трим только итератор each, используемый для организации циклического перебо- ра всех элементов массива: а = ('А'..'Z'),to_a # Используется массив букв a.each {|х| print х } # Побуквенный вывод алфавита Также достойны внимания и другие методы, определяемые в классе Array, среди которых clear, compact!, delete_1f, each_index, empty?, fill, flatten!, include?, index, join, pop, push, reverse, reverse_each, rindex, shift, sort, sort!, uniq! и unshift. Мы еще вернемся к массивам при рассмотрении параллельного присваивания в разделе 4.5.5 и вызова метода в главе 6. А детальное исследование Array-API еще предстоит в разделе 9.5.2. 3.4. Хэши Хэш является структурой данных, поддерживающей набор объектов, известных как ключи, и связывающей с каждым ключом какое-нибудь значение. Хэши из- вестны также как отображения, поскольку они отображают ключи на значения. Иногда их называют ассоциативными массивами, поскольку они связывают (ас- социируют) значения с каждым из ключей и могут рассматриваться как массивы, в которых в качестве индекса может использоваться не целое число, а любой объ- ект. Поясним все это на примерах: # Этот хэш отобразит названия цифр на сами цифры numbers = Hash.new # Создание нового пустого хэш-объекта numbersE"one"] =1 # Отображение строки "единица" на Fixnum 1 numbersE"two"] =2 # Заметьте, что здесь мы пользуемся нотацией массива numbersE"three"] = 3 sum = numbersE"one"j + numbersE"two"] # Извлечение этих значений
94 Глава 3. Типы данных и объекты В нашем введении в хэши описывается используемый в Ruby синтаксис хэш- литералов и объясняются требования к объектам, используемым в качестве хэш- ключей. Более подробная информация об API, определенном для класса Hash, предоставлена в разделе 9.5.3. 3.4.1. Хэш-литералы Хэш-литералы записываются в виде списка заключенных в фигурные скобки пар ключ-значение, которые разделяюся запятыми. Ключи и значение отделены друг от друга двухсимвольной «стрелкой»: =>. Рассмотренный ранее Hash-объект мож- но создать и при помощи следующего литерала: numbers = { "one" => 1, " two" => 2, "three" => 3 } В большинстве случаев в качестве хэш-ключей лучше работают не строки, a Symbol -объекты (обозначения): numbers = { :опе => 1. :two => 2, :three => 3 } Обозначения — это неизменяемые изолированные строки, записываемые как идентификаторы и начинающиеся с двоеточия; более подробно они будут рассмо- трены далее, в разделе 3.6 этой главы. В Ruby 1.8 вместо стрелок допускается применение запятых, но в Ruby 1.9 этот непопулярный синтаксис больше не поддерживается: numbers = { :one, 1, :two, 2, :three. 3 } # То же самое, но читается труднее В обеих версиях, Ruby 1.8 и Ruby 1.9, допускается одиночная завершающая запя- тая в конце списка ключ-значение: numbers = { :one => 1, :two => 2, } # Лишняя запятая игнорируется В Ruby 1.9 поддерживается очень удобный и сжатый синтаксис хэш-литерала для ключей и значений. Двоеточие перемещается в конец хэш-ключа и подменяет со- бой стрелку1: numbers = { one: 1, two: 2, three: 3 } Следует заметить, что между идентификатором ключа и двоеточием не допуска- ется никаких пробелов. 3.4.2. Хэш-коды, равенство и изменяющиеся ключи Хэши в Ruby реализованы с помощью структуры данных, называемой хэш-табли- цей, что вряд ли вызовет удивление. У объектов, используемых в хэше в качестве 1 В результате получается синтаксис, очень похожий на тот, что используется в объектах JavaScript.
3.5. Диапазоны 95 ключей, имеется метод под названием hash, который возвращает для ключа хэш- код, являющийся F1 xnum-объектом. Если два ключа равны друг другу, у них должен быть один и тот же хэш-код. Отличающиеся друг от друга ключи также могут иметь одни и те же хэш-коды, но работа хэш-таблиц наиболее эффективна, когда дубликаты хэш-кодов встречают- ся редко. Класс Hash проверяет ключи на равенство с помощью метода eql?. Для большин- ства имеющихся в Ruby классов, eql? работает как оператор == (более подробно этот вопрос рассмотрен в разделе 3.8.5). При определении нового класса, который переопределяет метод eql?, нужно также переопределить и метод hash, в против- ном случае экземпляры вашего класса не станут работать в качестве хэш-ключей. (Примеры написания метода hash приведены в главе 7.) Если определить класс и не переопределить eql?, то экземпляры класса при ис- пользовании в качестве хэш-ключей сравниваются по идентичности объектов. Два разных экземпляра вашего класса являются разными хэш-ключами, даже если они представляют одно и то же содержимое. В таком случае исходный метод hash действует соответствующим образом: он возвращает уникальный идентифи- катор объекта — objected. Учтите, что изменяемые объекты при использовании в качестве хэш-ключей мо- гут вызвать проблемы. Изменение содержимого объекта приводит, как правило, к изменению его хэш-кода. Если объект использован в качестве ключа, а затем подвержен изменениям, внутренняя хэш-таблица повреждается и хэш больше не сможет работать правильно. Поскольку строки могут изменяться, но часто используются в качестве хэш- ключей, Ruby рассматривает их в особом порядке и создает закрытые копии всех строк, используемых в качестве ключей. Но это единственный в своем роде особый случай; и при использовании в качестве хэш-ключей изменяемых объектов нужно проявлять крайнюю осторожность. Следует рассмотреть варианты создания за- крытой копии или вызова метода заморозки — freeze. Если нужно использовать изменяемые хэш-ключи, то при каждом изменении ключа следует вызывать метод rehash, определенный в классе Hash. 3.5. Диапазоны Объект Range представляет значения, расположенные между начальным и конеч- ным значениями. Литералы диапазона записываются путем помещения между начальным и конечным значениями двух или трех точек. Если используются две точки, диапазон является включающим и конечное значение является частью диа- пазона. Если используются три точки, то диапазон является исключающим и ко- нечное значение не является частью диапазона: 1..10 # Целые числа от 1 до 10, включая 10 1.0...10.0 # Числа между 1.0 и 10.0, исключая само значение 10.0
96 Глава 3. Типы данных и объекты Проверка принадлежности значения диапазону выполняется с помощью метода 1 пс 1 ude? (но чуть позже будут рассмотрены и другие способы проверки): cold_war = 1945..1989 cold_waг.include? birthdate.yea г При определении диапазона подразумевается принцип упорядоченности. Если диапазон — это ряд значений между двумя крайними точками, то вполне очевидна необходимость в каком-нибудь способе сравнения значений с крайними точками диапазона. В Ruby эта задача решается применением оператора сравнения <=>, ко- торый сравнивает свои два операнда и вычисляется в -1, 0 или 1, в зависимости от относительного порядка (или равенства). Оператор <=> определяется в классах, имеющих отношение к числам и строкам, для которых существует понятие упо- рядоченности. Значение только тогда может быть применено в качестве крайней точки диапазона, когда в отношении него может быть применен этот оператор. Как правило, крайние точки диапазона и значения «внутри» диапазона принад- лежат одному и тому же классу. Но технически рассматриваться на принадлежность к диапазону может любое значение, совместимое с оператором <=> крайних точек диапазона. Главное предназначение диапазона — сопоставление: возможность определения, где находится значение, внутри или за его пределами. Второе важное предназна- чение — итерация: если в классе крайних точек диапазона определен метод succ (для получения следующего элемента), значит есть дискретный набор элементов диапазона, которые могут подвергаться итерации путем использования методов each, step и методов модуля Enumerabl е. Рассмотрим, к примеру, диапазон ' а'..' с': г = 'а’.. 'С г.each {|1| print "[#{1}]"} # Выводит "Еа]ЕЬ]Ес]’’ r.step(2) { |1| print "[#{1}]"} # Выводит "Еа]Ес]" r.to_a #=> E'a'.'b'.'c']: Перечисляемое™ определяет # возможность использования метода to_a Причина, по которой все это работает, заключается в том, что в классе String определен метод succ, и 'а ' .succ — это ’b', а ’b’ .succ — это 'с'. Диапазоны, кото- рые могут подвергаться подобной итерации, являются дискретными. Диапазоны, у классов крайних точек которых не определен метод succ, не могут подвергаться итерации, и поэтому их можно назвать сплошными. Следует заметить, что диапа- зоны с целочисленными крайними точками являются дискретными, а с крайними точками, относящимися к числам с плавающей точкой, — сплошными. В обычных Ruby-программах чаще всего используются диапазоны с целочислен- ными крайними точками. В силу своей дискретности целочисленные диапазоны могут быть использованы для индексации строк и массивов. Их также удобно применять для представления перечисляемой коллекции возрастающих значений. Заметьте, что код присваивает литерал диапазона переменной, а затем вызывает методы, применяемые к диапазону, через эту переменную. Если нужно вызвать
3.5. Диапазоны 97 метод напрямую к литералу диапазона, то литерал нужно взять в круглые скобки, иначе метод будет вызван в отношении крайней точки диапазона, а не в отноше- нии самого Range-объекта: 1..3.to_a (1..3).to а # Попытка вызвать to_a для числа 3 # => [1.2.3] 3.5.1. Проверка принадлежности к диапазону В классе Range определяются методы для установки принадлежности произволь- ного значения к диапазону (то есть включено это значение в диапазон или нет). Перед тем как вдаваться в подробности работы этих методов, необходимо объяс- нить, что принадлежность может быть определена двумя различными способами. Это связано с различиями между сплошными и дискретными диапазонами. По первому определению значение х принадлежит диапазону begin, .end (начало..ко- нец), если: begin <= х <= end И х принадлежит диапазону begi п... end (с тремя точками), если: begin <= х < end Все значения крайних точек диапазона должны обеспечивать выполнение опера- тора <=>, поэтому это определение принадлежности справедливо для любого объ- екта Range и не требует, чтобы объекты крайних точек имели реализацию метода succ. Мы называем это проверкой на принадлежность к сплошному диапазону. Второе определение принадлежности, относящееся к дискретному диапазону, за- висит от наличия метода succ. В нем диапазон begin, .end трактуется как набор, включающий begin, begin.succ, begin.succ . succ и т. д. По этому определению принадлежность к диапазону явля- ется набором элементов и значение х включается в диапазон только в том случае, если оно представляет собой значение, возвращенное одним из вызовов метода succ. Следует учесть, что проверка на принадлежность к дискретному диапазону потенциально намного более затратная операция, чем проверка на принадлеж- ность к сплошному диапазону. Памятуя об этом, мы можем описать Range-методы для проверки принадлежно- сти. В Ruby 1.8 поддерживаются два метода, include? и member?. Они являются синони- мами и оба используют сплошной тест на принадлежность: r = 0...100 # Целочисленный диапазон от 0 до 99 г.member? 50 # => true: 50 принадлежит диапазону г.include? 100 # => false: 100 лежит за пределами диапазона г.include? 99.9 # => true: 99.9 меньше, чем 100 В Ruby 1.9 ситуация иная. В этой версии представлен новый метод, cover?, рабо- та которого напоминает работу методов include? и member? в Ruby 1.8: он всегда
98 Глава 3. Типы данных и объекты использует сплошную проверку на принадлежность. Методы include? и member? в Ruby 1.9 продолжают оставаться синонимами. Если крайние точки диапазона являются числами, в этих методах используется сплошная проверка на принадлежность, точно так же, как это делалось в Ruby 1.8. Но если крайние точки не являются числами, вместо этого в них используется дискретная проверка на принадлежность. Эти изменения можно проиллюстриро- вать с помощью дискретного строкового диапазона (чтобы понять, как работает String.succ, можно воспользоваться инструментальным средством ri): triples = "AAA".."ZZZ" triples.include? "ABC" triples.include? "ABCD" tri pies.cover? "ABCD" triples.to_a.include? "ABCD” # true: работает быстро в l.B и медленно в 1.9 # true в 1.8. false в 1.9 # Выдает true и работает быстро в 1.9 # Выдает false и работает медленно в 1.8 и 1.9 На практике большинство диапазонов имеют числовые крайние точки, и измене- ния в Range-API между Ruby 1.8 и 1.9 не имеют особого значения. 3.6. Обозначения Стандартная реализация Ruby-интерпретатора содержит таблицу обозначений, в которой хранятся имена всех классов, методов и переменных, о которых что- либо известно. Это позволяет такому интерпретатору избежать большинства случаев сравнения строк: он ссылается, к примеру, на имена методов по их по- зиции в таблице обозначений. Таким образом, относительно затратная по ресур- сам и времени строковая операция превращается в относительно малозатратную целочисленную операцию. Такие обозначения не являются всецело внутренними средствами интерпрета- тора; они также могут быть использованы в Ruby-программах. В Symbol-объекте содержится ссылка на обозначение. Литерал обозначения пишется путем при- соединения к идентификатору или строке в качестве префикса символа двоето- чия: :symbol :"symbol" /еще одно длинное обозначение' s = "string" sym = :”#{s}" # Литерал обозначения # Тот же самый литерал # Для обозначений с пробелами # можно использовать цитаты # Обозначение :string У обозначений также имеется синтаксис литерала £s, позволяющий использовать произвольные ограничители, который работает аналогично последовательностям и ад, используемым для строковых литералов: SsC"] # То же самое, что и : ""
3.7. True, False и Nil 99 Обозначения часто используются для ссылок на имена методов в рефлексивном коде. Предположим, например, что мы хотим узнать, имеется ли у какого-нибудь объекта метод each: о.respond_to? :each А вот еще один пример. В нем проверяется, отзывается ли данный объект на ука- занный метод, и если это так, то этот метод вызывается: name = :size if o.respond_to? name o.send(name) end Объект String можно превратить в объект Symbol путем использования метода intern или метода to_sym. А объект Symbol можно превратить в объект String, вос- пользовавшись методом to_s или его псевдонимом id2name: str = "string" # Начнем с объекта string sym = str.intern # Превращение в обозначение sym = str.to_sym # Еще один способ такого превращения str = sym.to_s # Обратное превращение в строку str = sym.id2name # Еще один способ такого превращения Две строки могут иметь одно и то же содержание, но быть абсолютно разными объектами. Но с обозначениями такого не происходит. Две строки с одинаковым содержимым будут превращены в один и тот же Symbol -объект. Два отличающихся друг от друга Symbol -объекта всегда будут иметь различное содержимое. При написании кода, использующего строки не ради их текстового наполнения, а в качестве разновидности уникального идентификатора, лучше отдать предпо- чтение обозначениям. К примеру, вместо того чтобы создавать метод, ожидающий аргумент, который может быть либо строкой «АМ», либо строкой «РМ», можно написать его так, чтобы он ожидал обозначение : АМ или обозначение : РМ. Срав- нение на равенство двух Symbol -объектов выполняется намного быстрее, чем та- кое же сравнение двух строк. По этой причине в качестве хэш-ключей в большин- стве случаев обозначения предпочтительнее строк. В Ruby 1.9 в классе Symbol определяется ряд строковых методов, среди которых length, size, операторы сравнения и даже операторы [] и =-. Это делает обозначе- ния в чем-то взаимозаменяемыми со строками и позволяет их использовать в ка- честве своеобразной неизменяемой (и не удаляемой при сборке мусора) строки. 3.7. True, False и Nil При чтении раздела 2.1.5, мы уже поняли, что true, false и nil — это ключевые слова Ruby. Ключевые слова true и false являются двумя булевыми значениями, которые представляют истину и ложь, да и нет, включено и выключено. Ключе-
100 Глава 3. Типы данных и объекты вое слово ni 1 является специальным значением, зарезервированным для указания отсутствия какого-либо значения. Каждое из этих ключевых слов вычисляется в специальный объект. Ключевое сло- во true вычисляется в объект, который является единственным экземпляром клас- са TrueClass. По аналогии с этим false и ni 1 являются единственными экземпля- рами классов FalseClass и NIIClass. Учтите, что класс Boolean в Ruby отсутствует. Для обоих классов, TrueClass и Fal seClass, надклассом является класс Object. Если нужно проверить, не равно ли значение ni 1, можно просто сравнить его с ni 1 или воспользоваться методом nil?: о == nil # Имеет ли "о" значение nil? o.nil? # Еще один способ проверки Следует заметить, что true, fal se и ni 1 ссылаются на объекты, а не на числа. Объ- екты fal se и ni 1 — это не является аналогом числа 0, a true — не является аналогом числа 1. Когда Ruby требуется булево значение, ni 1 ведет себя как false, а любое значение, отличное от ni 1 или false, ведет себя как true. 3.8. Объекты Ruby является чистейшим объектно-ориентированным языком: все значения яв- ляются объектами, и нет никакой разницы между элементарными типами и типа- ми объектов, как это бывает во многих других языках. В Ruby все объекты наследуются из класса по имени Object и совместно использу- ют методы, определяемые для этого класса. В этом разделе рассматриваются об- щие свойства всех имеющихся в Ruby объектов. Местами информация подается весьма насыщенно, но фундаментальность этих сведений предполагает обязатель- ность их усвоения. 3.8.1. Ссылки на объекты При работе с Ruby-объектами мы фактически имеем дело со ссылками на объек- ты. Работа ведется не с самим объектом, а со ссылкой на него1. Когда переменной присваивается какое-то значение, то объект не копируется «в» эту переменную; в ней просто сохраняется ссылка на объект. Поясним это на примерах программ- ного кода: s = "Ruby" # Создание строкового объекта. Сохранение ссылки на него в s. t = s # Копирование ссылки в t. И s. и t ссылаются на один и тот же объект. 1 Если вы знакомы с Си или C++, то можете воспринимать ссылку как указатель, т. е. адрес объекта в памяти. Но в Ruby указатели не используются. Ссылки в Ruby имеют скрытый характер и являются внутренним компонентом реализации языка. Способов получения адреса значения, обращения к значению не по имени или выполнения арифметических действий с указателем не существует.
3.8. Объекты 101 t[-l] = "" # Изменение объекта через ссылку, хранящуюся в t. print s # Доступ к измененному объекту через ссылку s. Выводится "Rub". t = "Java" # Теперь t ссылается на другой объект. print s.t # Выводится "RubJava". Когда в Ruby объект передается методу, то на самом деле ему передается ссылка. Это не сам объект, и это не ссылка на ссылку на объект. Иными словами, аргумен- ты метода передаются по значению, а не по ссылке, но в этих значениях передаются ссылки на объект. Поскольку ссылки на объект передаются методам, те могут ис- пользовать эти ссылки для внесения изменений в исходный объект. Затем, когда метод возвращает управление, эти изменения становятся видимыми. 3.8.1.1. Непосредственные значения Мы говорили, что все значения в Ruby — объекты, а работа со всеми объектами ведется через ссылки. Но в реализации ссылок объекты Fixnum и Symbol фактиче- ски являются «непосредственными значениями», а не ссылками. Ни один из этих объектов не имеет методов-мутаторов, поэтому объекты Fixnum и Symbol являются неизменяемыми, значит, фактически невозможно понять, что работа с ними ве- дется как со значениями, а не как со ссылками. Факт существования непосредственных значений должен рассматриваться как особенность реализации. Единственная практически значимая разница между не- посредственными значениями и значениями ссылок заключается в том, что непо- средственные значения не могут иметь определенных для них синглтон-методов. (Синглтон-методы рассмотрены в разделе 6.1.4.) 3.8.2. Продолжительность существования объекта Синтаксис встроенных классов Ruby, рассмотренных в этой главе, основан на ли- тералах, и экземпляры этих классов создаются простым включением их букваль- ных значений в ваш код. Объекты других классов должны быть созданы явным образом, и чаще всего это делается с помощью метода по имени new: myObject = myClass.new Метод new принадлежит классу Class. Его вызов приводит к выделению памяти для хранения нового объекта, затем путем вызова метода initialize инициализи- руется состояние этого «пустого» объекта, для которого только что была выделена память. Аргументы, переданные методу new, передаются непосредственно методу initialize. Метод initialize определен во многих классах для выполнения любой инициализации, необходимой для экземпляров. Методы new и initialize предоставляют исходную технологию для создания но- вых объектов, но в классах могут определяться другие методы, известные как «фа- бричные», которые возвращают экземпляры. Более подробные сведения о методах new, 1 ni ti а 1 i ze и фабричных методах будут изложены в разделе 7.4.
102 Глава 3. Типы данных и объекты Для Ruby-объектов явного высвобождения ресурсов, осуществляемого в языках наподобие Си и C++, не требуется. В Ruby используется технология под названи- ем сборка мусора, предназначенная для автоматического уничтожения объектов, надобность в которых уже миновала. Объекты становятся кандидатами на приме- нение к ним сборки мусора, когда они недоступны — когда на объект уже не оста- лось ссылок за исключением тех, что исходят от других недоступных объектов. Использование в Ruby сборки мусора означает, что Ruby-программы менее под- вержены утечкам памяти, чем программы, написанные на языках, в которых от объектов и памяти требуется явное высвобождение ресурсов. Но наличие сборки мусора еще не означает, что утечки памяти становятся невозможными: любой код, создающий долговременные ссылки на объекты, которые могли бы быть и крат- ковременными, может стать источником таких утечек. Рассмотрим хэш, который используется в качестве кэша. Если кэш не будет урезаться за счет использования какого-нибудь алгоритма удаления наименее используемых в последнее время элементов, то кэшируемые объекты будут оставаться доступными на все время доступности самого кэша. Если ссылка на хэш осуществляется через глобальную переменную, то он будет доступен на все время работы Ruby-интерпретатора. 3.8.3. Идентичность объекта У каждого объекта есть идентификатор — значение типа Fi xnum, которое можно по- лучить при помощи метода object ed. Значение, возвращаемое этим методом, яв- ляется константой и носит уникальный характер на весь период существования объекта. Пока объект является доступным, он всегда будет иметь один и тот же идентификатор, который совместно с ним не сможет использовать ни один другой объект. Метод id является не рекомендуемым к использованию синонимом object_id. Ruby 1.8 выдает при его использовании предупреждение, а из Ruby 1.9 он и вовсе удален. Разрешенным синонимом для object ! d является_1 d_. Он служит резервным ва- риантом, позволяющим иметь доступ к идентификатору объекта, даже если метод object_id был не определен или переопределен. В классе Object для упрощенного возвращения идентификатора объекта реализо- ван метод hash. 3.8.4. Класс объекта и тип объекта Для определения класса Ruby-объекта существует несколько способов. Самый простой из них — запросить этот класс: о = "test" # Это значение о.class # Возвращает объект, представляющий класс String Если вас интересует иерархия классов объекта, то любой класс можно запросить на предмет его надкласса:
3.8. Объекты 103 о.class # String: о - это объект класса String о.class.superclass # Object: надкласс для String - это Object o.class.superclass.superclass # nil: У объекта нет надкласса В Ruby 1.9 Object не является настоящим корневым элементом иерархии классов: # Только в Ruby 1.9 Object.superclass # BasicObject: В Ruby 1.9 у класса Object уже # есть надкласс BasicObject.supercl ass # nil: У BasicObject нет надкласса Более подробно BasicObject рассмотрен в разделе 7.3. Итак, самым простым способом проверки класса объекта является непосредствен- ное сравнение: o.class == String # true, если "о" является экземпляром класса String Метод instance_of? делает то же самое, но более элегантно: o.instance_of? String # true, если "о" является экземпляром класса String Обычно при проверке принадлежности объекта к какому-нибудь классу так- же хочется узнать, является ли объект экземпляром какого-нибудь подкласса этого класса. Для такой проверки используется метод is_a? или его синоним kind_of?: х = 1 x.instance_of? Fixnum x.instance_of? Numeric x.is_a? Fixnum x.is_a? Integer x.is_a? Numeric x.is_a? Comparable x.is_a? Object # Значение, с которым мы будем работать # true: является экземпляром класса # false: instance_of? не проверяет наследственность # true: х относится к Fixnum # true: х относится к Integer # true: х относится к Numeric # true: работает также с миксин-модулями # истина для любого значения х В классе Class оператор === определен таким образом, что его можно использовать вместо is_a?: Numeric === х # true: х относится к Numeric Этот способ уникален для Ruby, но его код читается, наверное, несколько хуже, чем код, использующий более традиционный метод 1 s_a?. В Ruby у каждого объекта есть однозначно определенный класс, и этот класс ни- когда не изменяется на всем протяжении времени существования объекта. С дру- гой стороны, тип объекта имеет более изменчивый характер. Тип объекта зависит от его класса, но класс объекта — это всего лишь составляющая типа объекта. Если речь идет о типе объекта, то подразумевается совокупность признаков его поведе- ния, характеризующих этот объект. Если выразить эту мысль по-другому, то тип объекта — это набор методов, на ко- торые он может реагировать. (Это определение приобретает рекурсивный харак- тер, поскольку здесь имеют значение не только имена методов, но также и типы аргументов, которые эти методы могут воспринимать.)
104 Глава 3. Типы данных и объекты В Ruby-программировании обращать внимание на класс объекта зачастую не приходится, нужно лишь знать, может ли в отношении него быть вызван какой- нибудь метод. Рассмотрим, к примеру, оператор «. Для массивов, строк, фай- лов и других классов, связанных с операциями ввода-вывода, он определен как оператор добавления. Если создавать метод, выполняющий текстовый вывод, можно написать его с учетом широкого применения этого оператора. Тогда соз- данный метод может быть вызван с любым аргументом, для которого реализован оператор «. Тут важен не класс аргумента, а то, что к нему можно применить операцию добавления. Метод respond_to? позволяет это проверить: о.respond_to? # результат true, если "о" обладает оператором « Недостаток такого подхода состоит в том, что он проверяет только имя метода, а не используемые им аргументы. Например, для Fixnum и Bignum оператор « реа- лизован как оператор сдвига влево, и в качестве аргумента он ожидает число, а не строку. Целочисленные объекты при проверке с помощью respond to? представ- ляются «добавляемыми», но выдают ошибку, когда программный код добавляет строку. Общего решения этой проблемы не существует, но для данного случая по- дошло бы явное исключение объектов Numeric с помощью метода i s_a?: o.respond_to? and not o.is_a? Numeric Другим примером отличия типа от класса служит класс St г 1 ng 10 (принадлежащий стандартной библиотеке Ruby). Stri nglO позволяет читать из строковых объектов и записывать в них, как будто они являются объектами ввода-вывода — 10. В клас- се StringlO имитируется 10 АР I — в StringIO-объектах определены такие же ме- тоды, как и в 10-объектах. Но StringlO не является подклассом 10. Если создается метод, ожидающий потоковый аргумент, и проводится проверка класса аргумента с помощью 1 s_a? 10, то этот метод не будет работать с Stri ng10-аргументами. Фокусировка внимания на типах, а не на классах, ведет к стилю программиро- вания, использующему типизацию по общим признакам, известную в Ruby как «duck-типизация» (утиная типизация). Приметы duck-типизации будут рассмо- трены в главе 7. 3.8.5. Равенство объектов В Ruby имеется невероятное количество методов сопоставления объектов для выяснения их равенства, и, чтобы понять, когда нужно использовать каждый из имеющихся методов, важно понять, как они работают. 3.8.5.1. Метод equal? Метод equal ? определяется в классе Object для проверки, ссылаются ли два значе- ния на один тот же объект. Для любых двух отличных друг от друга объектов этот метод всегда возвращает fа 1 se: а = "Ruby" # Одна ссылка на один String-объект b = с = "Ruby" # Две ссылки на другой String-объект
3.8. Объекты 105 a.equal?(b) # false: а и b относятся к разным объектам b.equal?(c) # true: b и с ссылаются на один и тот же объект По соглашению подклассы никогда не переопределяют метод equal ?. Другим способом определения являются ли два объекта по сути одним и тем же объектом, является проверка их идентификаторов — object_i d: a.objectjd == b.objectjd # Работает так же, как и а.equal?(b) 3.8.5.2. Оператор == Оператор == является наиболее распространенным способом проверки равенства. В классе Object — это просто синоним метода equal?, и он проверяет две ссылки на объект на идентичность. Во многих классах этот оператор переопределяется, чтобы дать возможность от- дельным экземплярам быть проверенным на равенство: а = "Ruby" # Объект String b = "Ruby" # Еще один объект String с тем же содержимым a.equal?(b) # false: а и b не ссылаются на один и тот же объект а == b # true: но эти два различных объекта имеют равные значения Заметьте, что в этом коде одиночный знак равенства является оператором при- сваивания. Для проверки на равенство в Ruby используются двойной знак равен- ства (в соответствии с соглашением, которое Ruby разделяет со многими другими языками программирования). Оператор == определен в большинстве стандартных классах Ruby для реализации приемлемого определения равенства. В том числе он определен и в классах Array и Hash. При использовании оператора == два массива равны, если у них одинако- вое количество элементов и если имеющиеся в них соответствующие элементы при применении оператора == будут равны друг другу. Равенство двух хэшей определяется оператором ==, если они содержат одинаковое количество пар ключ- значение и если их соответствующие ключи и значения сами равны друг другу. (Значения сравниваются с помощью оператора ==, а вот хэш-ключи сравниваются с помощью метода eql?, который будет рассмотрен далее в этой главе.) РАВЕНСТВО С ТОЧКИ ЗРЕНИЯ JAVA-ПРОГРАММИСТОВ У Java-программистов уже выработалась привычка использовать оператор = = для проверки, являются ли два объекта одним и тем же объектом, а метод equals они обычно используют для проверки, имеют ли два различных объекта одно и то же значение. Принятое в Ruby соглашение является едва ли не прямой противоположностью тому, что принято в Java. В операторах ==, определенных в классах Numeric, выполняется простое преобра- зование типов, поэтому (к примеру) Fixnum 1 и Float 1.0 при сравнении равны
106 Глава 3. Типы данных и объекты друг другу. Оператор ==, определенный в таких классах, как String и Array, обыч- но требует, чтобы оба оператора были одного и того же класса. Если для право- стороннего операнда определена функция преобразования to_str или to_ary (см. раздел 3.8.7), то тогда эти операторы вызывают оператор ==, определенный для операнда правой части, и дают возможность этому объекту решать, равен он или нет левосторонней строке или массиву. Поэтому есть возможность (которой не всегда пользуются) определять классы, которые ведут себя при сравнении как строки или массивы. Оператор != («не равно») используется в Ruby для проверки на неравенство. Когда Ruby встречает оператор ! =, то поступает просто — использует оператор == и инвертирует результат. Это означает, что в классе должен быть определен толь- ко оператор ==, определяющий его собственные представления о равенстве. Ruby предоставляет вам оператор != совершенно бесплатно. Тем не менее в Ruby 1.9 классы должны иметь свои собственные операторы !=, определенные явным об- разом. 3.8.5.3. Метод eql? Метод eql? определен в классе Object как синоним метода equal?. Классы, в кото- рых он переопределен, используют его, как правило, в качестве строгого варианта оператора ==, не осуществляющего преобразования типов. Например: 1 == 1.0 # true: Fixnum и Float объекты могут быть == l.eql?(1.0) # false: но они никогда не будут eql! Класс Hash использует eql? для проверки равенства двух ключей. Если для двух объектов eql? возвращает true, то их hash-методы должны также возвращать оди- наковые значения. Обычно если нужно создать класс и определить оператор ==, можно просто написать hash-метод и определить eql?, чтобы использовать ==. 3.8.5.4. Оператор == = Оператор === часто называют case-равенством, и он используется для проверки, совпадает ли заданное значение оператора case какому-нибудь предложению when этого оператора. (Оператор case осуществляет ветвление по нескольким направ- лениям и рассматривается в главе 5.) В классе Object исходный оператор === определен таким образом, что он вызыва- ет оператор ==. Поэтому для многих классов case-равенство — это одно и то же, что и равенство, определяемое оператором ==. Но в некоторых ключевых классах оператор === определен иначе, и в этих случаях является не только оператором принадлежности или соответствия. Для Range оператор === определен, чтобы про- верять, попадает значение в диапазон или нет. Для Regexp оператор === определен, чтобы проверять, соответствует строка регулярному выражению или нет. А для Class оператор === определен, чтобы проверять, является объект экземпляром клас- са или нет. В Ruby 1.9 для Symbol оператор === определен, чтобы вернуть true, если правосторонний операнд является тем же обозначением, которое представлено
3.8. Объекты 107 в качестве левостороннего операнда или если он является строкой, содержащей тот же текст. Примеры: (1. .10) — 5 /\d+/ === "123" String === "s" :s === "s" # true: 5 лежит в диапазоне 1..10 # true: строка соответствует регулярному выражению # true: "s" является экземпляром класса String # true в Ruby 1.9 Подобное применение оператора === смотрится довольно необычно. Чаще всего он используется в качестве неявной составляющей оператора case. I.8.5.5. Оператор =~ Оператор =- определен для классов String и Regexp (и Symbol в Ruby 1.9) для вы- полнения сопоставления с шаблоном, но на самом деле он не является операто- ром равенства. Поскольку в нем присутствует знак равенства, то он для полноты картины тоже отнесен к данной категории. Для класса Object определена «холо- стая» версия =-, которая всегда возвращает false. Этот оператор можно опреде- лить и в своем собственном классе, если в нем определены какие-нибудь операции сопоставления с шаблоном или имеется, к примеру, понятие приближенного ра- венства. Оператор ! - служит в качестве инверсии оператора =-. Его можно опреде- лить в Ruby 1.9, а в Ruby 1.8 он не имеет определений. (.8.6. Объект Order Практически в каждом классе может быть определен столь полезный метод, как ==, предназначенный для проверки экземпляров на равенство. А в некоторых клас- сах может быть определена также и упорядоченность. То есть если взять любые два экземпляра такого класса, то эти экземпляры должны быть равны или один экземпляр должен быть «меньше, чем» другой. Наиболее очевидными примерами определения такой упорядоченности служат классы чисел. Строки также облада- ют упорядоченностью в соответствии с числовым порядком кодов тех символов, из которых они составлены. (Если взять ASCII-текст, то это примерный алфавит- ный порядок с учетом регистра символов.) Если в классе определен некий поря- док, экземпляры класса могут подвергаться сравнению и сортировке. В классах Ruby упорядоченность определяется путем реализации оператора <=>. Этот оператор должен возвращать -1, если его левый операнд меньше, чем его правый операнд, 0, если оба его операнда равны, и 1, если левый операнд больше, чем правый операнд. Если два операнда не могут пройти сравнение на основании какого-либо общего признака (если, к примеру, правый оператор относится к дру- гому классу), оператор должен вернуть nil: 1 <=> 5 5 <=> 5 9 <=> 5 "1" <=> 5 # -1 # О # 1 # nil: целые числа и строки друг с другом не сравниваются
108 Глава 3. Типы данных и объекты Для сравнения значений вполне достаточно оператора <=>. Но его использование не всегда понятно на интуитивном уровне. Поэтому классы, в которых определен этот оператор, как правило, также вклю- чают модуль Comparable в качестве подмешиваемого миксин-модуля. (Модули и подмешиваемые миксин-модули рассматриваются в разделе 7.5.2.) В понятиях оператора <=>, в миксин-модуле Comparable определены следующие операторы: Меньше. чем Меньше, чем или равно Равно Больше, чем или равно Больше, чем В модуле Comparable не определен оператор !=; Ruby автоматически определяет этот оператор как отрицание оператора ==. В дополнение к этим операторам срав- нения в модуле Comparable также определен полезный метод сравнения под назва- нием between?: 1.between?(0.10) # true: 0 <= 1 <= 10 Если оператор <=> возвращает nil, все операторы сравнения, полученные на его основе, возвращают false. Примером может послужить специальное значение NaN, относящееся к классу Float: пап = 0.0/0.0: пап < 0 # Нуль # false деленный на нуль не является числом : значение не меньше нуля пап > 0 # false : значение не больше нуля пап == 0 # false : значение не равно нулю пап == пап # false : оно даже не равно самому себе! nan.equal?(nan) # Ну a это истина, не подвергаемая сомнению Заметьте, что определение оператора <=> и включение модуля Comparable приводит к определению оператора == для вашего класса. Обычно, когда некоторые классы могут реализовать эту операцию более эффективно, чем проверка на равенство, основанная на использовании оператора <=>, в них определяется их собственный оператор ==. Можно определить классы, в которых реализуются другие понятия равенства в используемых ими операторах == и <=>. К примеру, класс мог бы осу- ществлять строковые сравнения, зависящие от регистра, с использованием опе- ратора ==, но в то же время осуществлять сравнения, независящие от регистра, с использованием оператора <=>, поэтому экземпляры этого класса проходят со- ртировку более естественным образом. Вообще-то лучше, если оператор <=> будет возвращать 0 только в том случае, если оператор == будет возвращать true. 3.8.7. Преобразование объектов Во многих Ruby-классах определены методы, возвращающие представление объ- екта в виде значения другого класса. Наверное, самым распространенным и хоро- шо известным из них является метод to_s, предназначенный для представления
3.8. Объекты 109 объекта в виде строки, то есть в виде String-объекта. В следующих подразделах описаны различные категории преобразований. З.8.7.1. Явные преобразования В классах определяются явные методы преобразования для использования в коде приложения, когда требуется преобразовать значение в другое представление. Наиболее распространенными методами, относящимися к этой категории, явля- ются to_s, to_i, to_f и to_a, предназначенные соответственно для преобразования в строку — String, в целое число — Integer, в число с плавающей точкой — Float и в массив — Array. Обычно встроенные методы не вызывают для вас эти методы преобразования. Если вызывается метод, ожидающий строку — String, а ему передается объект какого-нибудь другого класса, то этот метод не предполагает преобразования ар- гумента с помощью метода to_s. (Тем не менее значения, вставленные в строку в двойных кавычках, автоматически конвертируются в строку с помощью метода to_s.) Метод to_s — несомненно, самый известный из методов преобразования, посколь- ку строковые представления объектов очень часто используются в пользователь- ских интерфейсах. Важной альтернативой to_s служит метод inspect. Метод to_s обычно предназначен для возвращения объекта в представлении, которое легко читается человеком, то есть в удобном для конечных пользователей виде. А метод inspect предназначен для проведения отладки и возвращает представление, кото- рое больше подходит для Ruby-разработчиков. Исходный метод inspect наследу- ется от класса Object и просто вызывает метод to_s. 3.8.7.2. Неявные преобразования Иногда класс имеет строгие признаки какого-нибудь другого класса. Имеющий- ся в Ruby класс исключений — Exception представляет ошибку или неожиданные условия, возникшие в программе и формирует сообщение об ошибке. В Ruby 1.8 объекты Exception не просто конвертируются в строки, они являются объектами, подобными строкам, и по многим аспектам могут рассматриваться как строки1. Например: # только в Ruby 1.8 е = Except!on.new("это не настоящее исключение") msg = "Ошибка: " + е # Объединение строк с использованием Exception-объекта Поскольку Exception-объекты подобны строкам, их можно использовать с опера- торами объединения строк, чего не скажешь о многих других классах Ruby. При- чина того, что Exception-объекты могут вести себя подобно String-объектам за- ключается в том, что в Ruby 1.8, в классе Exception реализуется метод неявного 1 Но подобное поведение не приветствуется, и в Ruby 1.9 неявное преобразование Exception в St г i ng уже не допускается.
110 Глава 3. Типы данных и объекты преобразования to_str, и оператор +, определенный в классе St ri ng, вызывает этот метод для своего правого операнда. Другими методами неявного преобразования являются to_1 nt, применяемый к объ- ектам, которые нужно сделать подобными целым числам, to_ary, применяемый к объектам, которые нужно сделать похожими на массивы, и to_hash, применяе- мый к объектам, которые нужно сделать подобными хэшам. К сожалению, те об- стоятельства, при которых вызываются эти методы неявного преобразования, не слишком хорошо документированы. К тому же во встроенных классах эти методы неявного преобразования не слишком часто реализуются. Ранее мы уже мимоходом отмечали, что оператор == при проверке равенства может выполнять слабую разновидность преобразования типов. Операторы ==, определенные в классах String, Array и Hash, осуществляют проверку на принад- лежность правого операнда к тому же классу, которым представлен левый опе- ранд. Если они одного класса, то проводится проверка равенства, а если нет, осу- ществляется проверка, определены ли для правого операнда методы to_str, to_ary или to hash. Они не вызывают один из этих методов, но если он существует, они вызывают метод ==, определенный для правого операнда, и дают ему возможность решать, равен ли правый операнд левому. В Ruby 1.9 во всех встроенных классах String, Array, Hash, Regexp и 10, определя- ются методы класса под названием try_convert. Эти методы преобразуют свои аргументы, если в них определен метод явного преобразования, или возвращают ni 1 в противном случае. Array try_convert(o) возвращает о. to_ary, если в о опреде- лен этот метод; в противном случае возвращается nil. Методы try_convert хорошо подходят для написания методов, допускающих неявные преобразования своих аргументов. 3.8.7.3. Функции преобразования В модуле Kernel определены четыре метода преобразования, которые ведут себя как глобальные функции. Эти функции — Array, Float, Integer и String — имеют такие же имена, как и классы, в которые они осуществляют преобразования, и не- привычны тем, что их имена начинаются с больших букв. Функция Array предпринимает попытку преобразования своего аргумента в мас- сив путем вызова метода to_ary. Если этот метод не определен или возвращает ni 1, она пробует применить метод to_a. Если to_a не определен или возвращает nil, функция Array просто возвращает новый массив, содержащий аргумент в качестве своего единственного элемента. Функция Float осуществляет непосредственное преобразование Numeric-аргу- менты в объекты Float. Для любого не-Numeric значения она вызывает метод to_f. Функция Integer преобразует свои аргументы в Fixnum или Bignum. Если аргу- мент является Numeric-значением, то осуществляется его непосредственное пре- образование. Значения чисел с плавающей точкой не округляются, а усекаются. Если аргумент является строкой, производится поиск показателя основания системы счисления (лидирующий 0 для восьмеричного основания, Ох — для
3.8. Объекты 111 шестнадцатеричного или 0b — для двоичного) и осуществляется соответствую- щее преобразование строки. В отличие от String.to i, эта функция не допускает наличия нечисловых завершающих символов. Для всех других типов аргументов функция Integer сначала предпринимает попытку осуществить преобразование с помощью метода to_i nt, и только после этого — с помощью метода to_i. И наконец, функция St г 1 ng преобразует свой аргумент в строку простым вызовом для него метода to_s. 3.8.7.4. Арифметические приведения типов данных оператора Для числовых типов определен метод преобразования под названием coerce (при- ведение типа данных). Этот метод предназначен для преобразования аргумента к тому же типу данных, что и числовой объект, для которого вызван метод, или для преобразования обоих объектов к какому-то более общему совместимому типу. Метод coerce всегда возвращает массив, который содержит два числовых значения одного и того же типа. Первый элемент массива является преобразованным значением аргумента coerce. Вторым элементом возвращенного массива является значение (при необходимо- сти преобразованное), для которого был вызван coerce: 1.1.coerced) require "rational" г = Rational(1.3) r. coerced) # [1.0, 1.1]: приведение Fixnum к Float # Использование рациональных чисел # Одна треть является рациональным числом # [Rational(2,1), Rational(1,3)]: Fixnum к Rational Метод coerce используется арифметическими операторами. К примеру, опера- тор +, определенный для класса Fi xnum, «не разбирается» в числах класса Rati опа 1, и если в качестве правого операнда используется значение Rati опа 1, он «не знает», как выполнять сложение. Решение предоставляется методом coerce. Числовые операторы написаны таким образом, что они не знают о типе правого операнда, они вызывают для правого операнда метод coerce, передавая ему в качестве аргу- мента левый операнд. Возвращаясь к нашему примеру сложения Fixnumn Rational, coerce, вызванный в отношении значения Rational, возвращает массив из двух ра- циональных значений. Теперь оператор +, определенный для Fixnum, может просто вызвать оператор + для значений массива. 3.8.7.5. Булевы преобразования типов В контексте преобразования типов булевы значения заслуживают специального упоминания. Ruby весьма строг в обращении со своими булевыми значениями: для true и false определены лишь методы to_s, возвращающие «true» и «false», и больше не существует никаких других методов преобразования. В нем также от- сутствует метод to_b для конвертации других значений в булевы. В некоторых языках значение false аналогично числу 0 или может быть пре- образовано в нуль и обратно. В Ruby значения true и false являются своими
112 Глава 3. Типы данных и объекты собственными отдельными объектами, и не существует никаких неявных преоб- разований, превращающих другие значения в true или fal se. Но это лишь полови- на истории. Имеющиеся в Ruby булевы операторы и их условные и циклические конструкции, использующие булевы выражения, могут работать с выражениями, отличными от true и false. Правило простое: в булевых выражениях любое зна- чение, отличное от false или ni 1, ведет себя подобно true (но не преобразуется в него). С другой стороны ni 1 ведет себя как false. Предположим, нужно узнать, равна переменная х значению ni 1 или нет. В некото- рых языках нужно построить выражение сравнения, которое вычисляется в true или false в явном виде: if х != nil # Выражение "х != nil" возвращает if значение true или false puts х # Выводит х. если эта переменная определена end В Ruby этот код работает, но в этом языке принято просто пользоваться тем фак- том, что все значения, отличные от ni 1 и false, ведут себя как true: if х # Если х не nil. puts х # то выводится его значение end Важно помнить, что в Ruby значения типа 0, 0.0 и пустая строка — "" ведут себя как true, что может вызвать удивление у тех, кто привык к работе с такими языка- ми, как Си или JavaScript. 3.8.8. Копирование объектов В классе Object определены два тесно связанных друг с другом метода, предна- значенные для копирования объектов. Оба они, и clone и dup, возвращают поверх- ностную копию объекта, для которого они вызываются. Если копируемый объект включает в себя какую-то внутреннюю структуру, ссылающуюся на другие объ- екты, то копируются только ссылки на объекты, а не сами объекты, на которые сделаны ссылки. Если для копируемого объекта определен метод initialize copy, то clone и dup просто выделяют новый, пустой экземпляр класса и вызывают для этого пусто- го экземпляра метод initialize_copy. Копируемый объект передается в качестве аргумента, и этот «конструктор копии» может инициализировать копию, как ей того требуется. Например, метод initialize copy может рекурсивно копировать внутренние данные объекта, чтобы получающийся в результате объект не был простой поверхностной копией оригинала. Классы могут также полностью переопределять методы clone и dup для создания любой требуемой разновидности копии. У методов clone и dup, определенных для Object, есть два важных различия. Первое состоит в том, что clone копирует как состояние замороженности, так и помечен- ное™ объекта (этим состояниям скоро будет дано определение), в то время как
3.8. Объекты 113 dup копирует лишь состояние помеченности; вызов dup в отношении заморожен- ных объектов приводит к возвращению незамороженной копии. А второе состоит в том, что clone копирует любые синглтон-методы объекта, a dup — нет. 3.8.9. Маршализация (Marshaling) объектов Состояние объекта можно сохранить, передав его методу класса Marshal .dump1. Если в качестве второго аргумента передать объект потока ввода-вывода, Marshal. dump записывает состояние объекта (и рекурсивно всех объектов, на которые он ссылается) в этот поток. В отсутствие этого аргумента он просто возвращает за- кодированное состояние в виде двоичной строки. Чтобы восстановить маршализованный объект, нужно передать содержащую его строку или поток ввода-вывода методу Marshal. 1 oad. Маршализация объекта — весьма простой способ сохранить его состояние для дальнейшего использования, и эти методы могут быть использованы для предо- ставления автоматического формата файла для Ruby-программ. Но следует иметь в виду, что двоичный формат, используемый методами Marshal .dump и Marshal .load зависит от используемой версии, и более новые версии Ruby не гарантируют воз- можности чтения маршализированных объектов, записанных более старыми вер- сиями Ruby. Методы Marshal dump и Marshal .load используются также для создания детальных копий объектов: def deepcopy(о) Marshal.load(Marshal.dump(o)) end Следует учесть, что файлы и потоки ввода-вывода, а также объекты Method и Binding, слишком динамичны для маршализации; у этого процесса отсутствует надежный способ восстановления их состояния. Структура YAML («YAML Ain’t Markup Language», то есть YAML — это не язык разметки) часто используется в качестве альтернативы модулю Marshal и осущест- вляет вывод объектов в текстовый формат, имеющий вид, вполне пригодный для чтения (и их обратную загрузку из этого формата). Эта структура представлена стандартной библиотекой, для использования которой нужно включить в код строку: require 'yaml ’. « 3.8.10. Замораживание объектов Любой объект может быть заморожен путем вызова определенного для него ме- тода freeze. Замороженные объекты становятся неизменяемыми — ничто из их 1 Иногда слово «marshal» и его варианты пишутся с двумя буквами «1»: marshall, marshalled и т. д. Если вы именно так его и пишите, то нужно запомнить, что имя Ruby-класса содер- жит только одну букву «1».
114 Глава 3. Типы данных и объекты внутреннего состояния не может быть изменено, а попытки вызвать любые опре- деленные для них методы-мутаторы терпят неудачу: s = "ice" s.freeze s.frozen? s.upcase! s[0] = "ni" # Строки являются изменяемыми объектами # превращение этой строки в неизменяемую # true: он был заморожен # ТуреЕггог: изменение замороженной строки невозможно # ТуреЕггог: изменение замороженной строки невозможно Замораживание объекта класса предотвращает добавление какого-нибудь метода к этому классу. Проверить, является ли объект замороженным, можно с помощью метода frozen?. Однажды замороженный объект уже не может быть «разморожен». Если копиро- вать замороженный объект с помощью метода cl one, то копия также будет заморо- женной. А если копировать замороженный объект с помощью метода dup, то копия заморожена не будет. 3.8.11. Пометка объектов Веб-приложениям часто приходится отслеживать данные, полученные от нена- дежного пользовательского источника, чтобы избежать инъекционных атак SQL и подобных им угроз безопасности. Ruby предоставляет простое решение этой проблемы: любой объект может быть помечен как ненадежный путем вызова ме- тода taint. Как только объект станет ненадежным, любые объекты, являющиеся производными от него, также будут ненадежными. Ненадежность объекта может быть проверена с помощью метода tainted?: s = "ненадежный" s.taint s.tainted? s.upcase.tainted? s[3,4].tainted? # Обычно объекты не помечены как ненадежные # Установка для объекта метки ненадежности # true: объект помечен как ненадежный # true: производные объекта также помечены как ненадежные # true: подстрока также несет метку ненадежности Пользовательский ввод — аргументы командной строки, переменные окружения и строки, считанные методом gets — автоматически помечаются как ненадеж- ные. Копии помеченных объектов, сделанные с помощью методов cl one и dup, остают- ся помеченными. Помеченный объект может стать непомеченным при использо- вании метода untai nt. Разумеется, его применение допустимо только после того, как объект проверен и есть уверенность, что он не представляет угроз безопас- ности. Применяемый в Ruby механизм пометки объекта приобретает наибольшую силу при использовании глобальной переменной $SAFE. Когда для этой переменной установлено значение больше нуля, Ruby устанавливает ограничения для различ- ных встроенных методов, и они не могут работать с ненадежными данными. Под- робности, касающиеся переменной SSAFE, изложены в главе 10.
Глава 4 Выражения и операторы
116 Глава 4. Выражения и операторы Выражение представляет собой фрагмент Ruby-кода, который Ruby-интерпре- татор может вычислить, чтобы выдать какое-нибудь значение. Посмотрим на не- сколько примеров выражений: 2 X Math.sqrt(2) х = Math.sqrt(2) х*х # Числовой литерал # Ссылка на локальную переменную # Вызов метода # Присваивание # Умножение с использованием оператора * Как видно из примеров, первичные выражения, такие как литералы, ссылки на пе- ременные и вызовы методов, могут быть объединены в более крупные выражения за счет использования операторов, таких как оператор присваивания и оператор умножения. Во многих языках программирования различаются выражения низкого уровня и высокоуровневые операторы, такие как условные операторы и циклы. В этих языках операторы управляют ходом программы, но они не имеют значений. Они выполняются, а не вычисляются. В Ruby нет четкого различия между операто- рами и выражениями; все в Ruby, включая определения класса и метода, может быть вычислено как выражение и будет возвращать значение. Но все же полезно отличать синтаксис, который обычно используется в качестве выражений, от син- таксиса, который обычно используется в качестве операторов. Выражения Ruby, влияющие на ход программы, рассмотрены в главе 5. Выражения Ruby, опреде- ляющие методы и классы, рассмотрены в главах 6 и 7. В этой главе рассматриваются наиболее простые и часто встречающиеся виды вы- ражений. Простейшие выражения — это литералы, которые уже рассматривались в главе 3. В данной главе объясняются переменные и ссылки на константы, вызо- вы методов, присваивания и составные выражения, созданные путем объединения более простых выражений с помощью операторов. 4.1. Простые литералы и литералы ключевых слов Литералы — это выражения вроде 1.0, ' he 11 о worl d' и [], которые непосредственно вставляются в текст программы. Они были представлены в главе 2 и подробно рассмотрены в главе 3. Стоит заметить, что многие литералы, такие как числа, являются первичными вы- ражениями — простейшими из всех возможных выражений, не составленных из более простых выражений. Другие литералы, такие как литералы массивов и хэ- шей, и строк, взятых в двойные кавычки, в которых используются вставки, вклю- чают в себя подвыражения, и поэтому не относятся к первичным выражениям. К первичным выражениям относятся и определенные ключевые слова Ruby, кото- рые могут рассматриваться как литералы ключевых слов или особые формы ссы- лок на переменные:
4.2. Ссылки на переменные 117 Nil True Вычисляется в значение ni 1 класса Ni 1 Cl ass Вычисляется в единственный экземпляр класса TrueClass, объект, который представляет булево значение true False Вычисляется в единственный экземпляр класса FalseClass, объект, который представляет булево значение fal se Self _FILE_ Вычисляется в текущий объект (более подробно sei f рассмотрен в главе 7) Вычисляется в строку, содержащее имя файла, выполняемого Ruby- интерпретатором. Может использоваться в сообщениях об ошибках _LINE_ Вычисляется в целое число, определяющего номер текущей строки кода в файле FILE _ENCODING_ Вычисляется в объект Encoding, определяющее кодировку текущего файла (работает только в Ruby 1.9) 4.2. Ссылки на переменные Переменная — это всего лишь имя для значения. Создание переменных и присваи- вание им значений производится с помощью выражений присваивания, которые будут рассмотрены в этой главе чуть позже. Когда имя переменной появляется в программе не в левой части присваивания, а где-нибудь в другом месте, то это считается ссылкой на переменную, которая вычисляется в значение переменной: one = 1.0 # Это выражение присваивания one # Эта ссылка на переменную вычисляется в 1.0 В главе 2 мы выяснили, что в Ruby имеется четыре типа переменных и лексиче- ские правила, определяющие их имена. Переменные, начинающиеся с символа $, являются глобальными, имеющими область видимости по всей Ruby-программе. Переменные, начинающиеся с символов @ и @@, являются переменными экземпля- ра и переменными класса, которые используются в объектно-ориентированном программировании и рассматриваются в главе 7. А переменные, имена которых начинаются со знака подчеркивания или буквы в нижнем регистре, являются локальными переменными, определяемыми только внутри текущего метода или блока. (Более подробно область видимости локальных переменных рассмотрена в разделе 5.4.3.) Переменные имеют простые, не составные имена. Если в выражении появляются символ . или символы ::, значит это выражение либо ссылка на константу, либо вызов метода. Например, Math: :Р1 — это ссылка на константу, a item.price — это вызов метода по имени price для значения, хранящегося в переменной item. Во время запуска Ruby-интерпретатор предопределяет ряд глобальных переменных. Их перечень приведен в главе 10. 4.2.1. Неинициализированные переменные Как правило, перед тем как использовать переменные в выражениях, им всег- да нужно присваивать значения, или проводить инициализацию. Но при
118 Глава 4. Выражения и операторы определенных обстоятельствах Ruby позволяет использовать переменные, кото- рые еще не были инициализированы. Для различных типов переменных работа- ют разные правила. Переменные класса Переменные класса перед своим использованием всегда должны иметь присво- енные им значения. Если сослаться на переменную класса, которой не было присвоено значение, Ruby выдаст ошибку имени — NameError. Переменные экземпляра Если сослаться не неинициализированную переменную экземпляра, Ruby вер- нет nil. Но в программировании довольствоваться таким его поведением счи- тается дурным тоном. Если запустить Ruby с ключом командной строки -w, он выдаст предупреждение об использовании неинициализированной перемен- ной. Глобальные переменные Неинициализированные глобальные переменные подобны тем же переменным экземпляра: они вычисляются в ni 1, но это приводит к предупреждению, если Ruby запущен с ключом -ы. Локальные переменные С ними дело обстоит сложнее, чем с предыдущими переменными, поскольку у локальных переменных нет акцентирующего внимание символа, используе- мого в качестве префикса. Значит, ссылки на локальные переменные выгля- дят как выражения вызова методов. Если Ruby-интерпретатор уже присваивал значение локальной переменной, то он знает, что это не метод, а переменная, и может вернуть значение переменой. Если присваивания не было, то тогда Ruby рассматривает выражение как вызов метода. Если метода с таким именем не существует, Ruby выдает ошибку имени — NameError. Таким образом, попытка использовать локальную переменную до того, как она была инициализирована, как правило, приводит к ошибке. Но тут есть одна особенность — переменная начинает свое существование, когда Ruby- интерпретатор видит для этой переменной выражение присваивания значения. Это происходит, даже если присваивание еще не было выполнено. Уже суще- ствующей переменной, которой еще не присвоено значение, дается исходное значение nil. Например: а = 0.0 if false # Это присваивание никогда не выполняется print а # Выводится nil: переменная существует, но значение ей не # присвоено print b # NameError: переменной или метода по имени b не существует 4.3. Ссылки на константы Константы в Ruby похожи на переменные, за исключением того, что их значе- ния, как предполагается, остаются постоянными на всем протяжении работы
4.3. Ссылки на константы 119 программы. На самом деле Ruby-интерпретатор не навязывает неизменное со- стояние констант, но выдает предупреждение, если программа изменяет значение константы. Лексически имена констант выглядят как имена локальных перемен- ных, за исключением того, что они должны начинаться с заглавной буквы. По со- глашению большинство констант пишутся заглавными буквами, а для разделения слов используется знак подчеркивания, ВОТ ТАК. Имена классов и модулей Ruby также являются константами, но по соглашению они пишутся с использованием начальной заглавной буквы и смешанного регистра, ВотТак. Хотя константы похожи на локальные переменные, начинающиеся с заглавных букв, они обладают областью видимости глобальных переменных и могут быть использованы в любом месте Ruby-программы, без учета области видимости. Но в отличие от глобальных переменных, константы могут быть определены класса- ми и модулями, и поэтому могут иметь составные имена. Ссылки на константы являются выражениями, которые вычисляются в значения указанных констант. Наипростейшие ссылки на константы являются первичны- ми выражениями — они состоят только из имен констант: CM_PER_INCH =2.54 # Определение константы CM_PER_INCH # Ссылка на константу. Вычисляется в 2.54. В дополнение к простым ссылкам, подобным этой, ссылки на константы могут быть и составными выражениями. В таком случае символы :: используются для отделения имени константы от имен классов или модулей, в которых она опреде- лена. Слева от символов :: может быть произвольное выражение, вычисляемое в объект класса или модуля. (Но обычно эти выражения являются простой ссыл- кой на константу, в которой приводится лишь имя класса или модуля.) Справа от символов :: располагается имя константы, определенное в классе или модуле. Например: Conversions::CM_PER_INCH # Константа, определенная в модуле Conversions moduTesEO]::NAME # Константа, определенная в элементе массива Модули могут быть вложенными, а это означает возможность определения кон- стант во вложенном пространстве имен: Conversions::Area::HECTARES_PER_ACRE Часть ссылки слева от символов :: может быть опущена, тогда константа разыски- вается в глобальной области видимости: ; :ARGV # Глобальная константа ARGV Учтите, что на самом деле это не означает для констант «глобальной области ви- димости». Подобно глобальным функциям, глобальные константы определены (и разыскиваются) в пределах класса Object. Поэтому :: ARGV является простым со- кращением выражения Object:: ARGV. Когда выражение ссылки на константу составлено с использованием символов ::, Ruby точно знает, где искать указанную константу. Но если символы составной ссылки, ::, отсутствуют, Ruby-интерпретатор должен искать соответствующее
120 Глава 4. Выражения и операторы определение константы. Поиск осуществляется в лексически ограниченном про- странстве видимости, а также в унаследованной иерархии включенного класса или модуля. Все подробности такого поиска изложены в разделе 7.9. Когда Ruby вычисляет выражение ссылки на константу, он возвращает значение константы или выдает исключение NameError, если константа с таким именем не может быть найдена. Учтите, что константа не будет существовать до тех пор, пока ей не будет присвое- но значение. Этим константы отличаются от переменных, которые начинают свое существование, когда интерпретатор видит, но не выполняет присваивание. Некоторые константы предопределяются Ruby-интерпретатором при его запуске. Их список приведен в главе 10. 4.4. Вызовы методов Выражение вызова метода состоит из четырех частей. О Произвольного выражения, чьим значением является объект, в отношении ко- торого вызывается метод. Это выражение продолжается символом . или симво- лами :: для отделения его от имени метода, которое следует далее. Выражение и разделитель необязательны; если они опущены, метод вызывается для теку- щего объекта — sei f. О Имени вызываемого метода. Это единственная обязательная часть выражения вызова метода. О Значений аргументов, переданных методу. Перечень аргументов может быть заключен в круглые скобки, но обычно их применение носит необязательный характер. (Необязательные и обязательные скобки подробно рассмотрены в раз- деле 6.3.) Если передается более одного аргумента, они отделяются друг от друга запятыми. Количество и типы требуемых аргументов зависят от определения метода. Некоторые методы вообще не требуют аргументов. О Необязательного блока кода, ограниченного фигурными скобками или парой ключевых слов do-end. Метод может вызвать этот код, используя ключевое сло- во yi el d. Такая возможность связывания произвольного кода с вызовом метода является основой работы имеющихся в Ruby мощных методов-итераторов. Более подробное изучение блоков, связанных с вызовами методов, предстоит в разделах 5.3 и 5.4. Имя метода обычно отделяется от имени объекта, для которого он вызывается, символом .. Также допустимы символы ::, но они используются редко, поскольку в этом случает вызов метода может выглядеть очень похожим на выражение ссыл- ки на константу. Когда Ruby-интерпретатор располагает именами метода и объекта, для которого он вызывается, он находит соответствующее определение указанного метода, ис- пользуя процесс, известный как «поиск метода», или «разрешение имени метода». Сейчас подробности не имеют значения, но более основательно они будут рассмо- трены в разделе 7.8.
4.4. Вызовы методов 121 Значением выражения вызова метода является значение последнего вычислен- ного выражения в теле этого метода. Дополнительные сведения об определении метода, его вызове и возвращаемых значениях будут изложены в главе 6. А здесь мы приведем некоторые примеры вызовов методов: puts "hello world” Math.sqrt(2) message.length a.each {|x| p x } # "puts" вызывается для self с одним строковым аргументом # "sqrt" вызывается для объекта Math с одним аргументом # "length" вызывается для объекта message без аргументов # "each" вызывается для объекта а. имея связанный с методом # блок ВЫЗОВ ГЛОБАЛЬНЫХ ФУНКЦИЙ Рассмотрим еще раз ранее показанный вызов метода: puts "hello world" Он представляет собой вызов определяемого в классе Kernel метода puts. Глобальные функции, определенные в Kernel, как и любые другие методы, определенные на верхнем уровне, находятся вне любых классов. Глобальные функции определены в качестве закрытых методов класса Object. Закрытые методы будут рассмотрены в главе 7. Но сейчас следует усвоить лишь то, что закрытые методы нельзя явным образом вызвать в отношении объекта- получателя — они всегда вызываются неявным образом в отношении объекта self, который всегда определен, и не важно, какое у него значение, для метода это значение равно Object. Поскольку глобальные функции являются метода- ми класса Object, эти методы всегда могут быть вызваны (неявным образом) в любом контексте, независимо от значения self. Один из ранее показанных примеров вызова метода касался message.length. Вы можете принять его за выражение ссылки на переменную, вычисляемое в значе- ние переменной 1 ength объекта message. Но это совсем не так. В Ruby используется довольно четкая объектно-ориентированная модель программирования: Ruby- объекты могут заключать в себе любое количество внутренних переменных экзем- пляра, но внешнему миру они показывают только методы. Поскольку метод 1 ength не требует никаких аргументов и вызывается без необязательных скобок, он вы- глядит как ссылка на переменную. На самом деле это сделано с особым умыслом. Подобные методы называются методами доступа к атрибутам, и мы понимаем, что объект message обладает атрибутом length1. Как будет показано далее, для объекта message можно определить метод по имени lengths Если этот метод ожидает пере- дачи ему единственного аргумента, то он относится к методу установки значения атрибута, и Ruby вызывает его, чтобы выполнить присваивание. Если такой ме- Но это еще не говорит о том, что каждый метод, не имеющий аргументов является методом доступа к атрибутам. К примеру, метод sort, определенный для массива, не имеет аргумен- тов, но его нельзя отнести к методам, возвращающим значение атрибута.
122 Глава 4. Выражения и операторы тод будет определен, то обе из следующих двух строк кода будут вызывать один и тот же метод: message.length=(3) # Обычный вызов метода message.length =3 # Вызов метода, замаскированный под присваивание Теперь рассмотрим следующую строку кода, предполагающую, что перемен- ная а содержит массив: а[0] Ее опять можно посчитать за особую разновидность выражения ссылки на пере- менную, где рассматриваемая переменная является элементом массива. Но и в этом случае мы имеет дело с вызовом метода. Ruby-интерпретатор превращает до- ступ к элементу массива в следующее выражение: а.[](0) Доступ к массиву превращается в вызов метода по имени [], который применяет- ся в отношении массива, и использует в качестве аргумента индекс массива. Та- кой синтаксис доступа к массиву не ограничивается одними массивами. Метод по имени [] можно определить для любого объекта. Когда объект «индексирован» с помощью квадратных скобок, любые значения внутри скобок будут переданы методу. Если метод [] написан в расчете на использование трех аргументов, то внутри квадратных скобок можно поместить три разделенных запятыми выраже- ния. Присваивания значений элементам массива также выполнены посредством вызова метода. Если для объекта о определяется метод по имени []=, то выра- жение о[х]=у превращается в о.[]=(х,у), а выражение o[x,y]=z превращается BO.[]=(x,y,z). Чуть позже в этой главе будет показано, что многие Ruby-операторы определены в виде методов, и выражения вроде х+у, превращаются в х. +(у), где имя метода — +. Тот факт, что многие Ruby-операторы определены в виде методов, означает, что эти операторы в тех классах, которые вами создаются, можно переопределить. Теперь рассмотрим очень простое выражение: х Если существует переменная по имени х (то есть Ruby-интерпретатор заметил присваивание х какого-нибудь значения), то это выражение ссылки на перемен- ную. Если такой переменной не существует, то это вызов метода х, не использую- щего аргументов, в отношении текущего объекта — sei f. Особой разновидностью выражения вызова метода служит слово super, которое в Ruby является зарезервированным словом. Это ключевое слово используется при создании подкласса для какого-нибудь другого класса. Сам по себе вызов super передает аргументы текущего метода тому методу, который имеет такое же имя, и определяется в надклассе.
4.5. Присваивания 123 Это ключевое слово может быть использовано так, как будто оно на самом деле является именем метода, и за ним может следовать произвольный перечень аргу- ментов. Ключевое слово super подробно рассмотрено в разделе 7.3.3. 4.5. Присваивания В выражении присваивания определяется одно или более одного значения для одного или более одного 1-значения. L-значение — это термин, применяемый для всего, что может появиться в левой части оператора присваивания. (В противовес, значения, расположенные в правой части оператора присваивания, иногда назы- вают r-значениями.) В Ruby 1-значения — это переменные, константы, атрибуты и массивы. Правила для различных видов 1-значений и смысл выражений при- сваивания несколько различаются, и каждый из этих видов будет подробно рас- смотрен в этом разделе. В Ruby есть три разные формы выражений присваивания. Простое присваивание использует одно 1-значение, оператор = и одно г-значение. Например: х = 1 # Присваивание 1-значению х значения 1 Сокращенная запись присваивания является усеченным вариантом выражения, обновляющего значение переменной путем выполнения некоторых других опера- ций (таких как сложение) по отношению к текущему значению переменной. В сокращенной записи присваивания используются операторы присваивания вро- де += и *=, в которых сочетается оператор, предназначенный для двух операндов, со знаком равенства: х += 1 # Присваивание 1-значению х значения х + 1 И наконец, параллельное присваивание представляет собой любое выражение при- сваивания, которое имеет более одного 1-значения или более одного г-значения. Приведем простой пример: x.y.z = 1,2,3 # Присваивание х значения 1. у - значения 2, a z - значения 3 Параллельное присваивание усложняется, когда количество 1-значений не совпа- дает с количеством г-значений или когда в правой части расположен массив. До- полнительные подробности изложены далее. Значением выражения присваивания служит присваиваемое значение (или мас- сив значений). К тому же оператор присваивания является «правоассоциатив- ным» — если в одном выражении появляется несколько присваиваний, они вы- числяются справа налево. Это означает, что присваивание может быть выстроено в цепочку, чтобы присвоить одно и то же значение нескольким переменным: х = у = О # Присваивание х и у значения О Заметим, что это не параллельное присваивание, а связанные в цепочку два про- стых присваивания: переменной у присваивается значение 0, а затем значение это- го первого присваивания (тоже 0) присваивается переменной х.
124 Глава 4. Выражения и операторы ПРИСВАИВАНИЕ И ПОБОЧНЫЕ ЭФФЕКТЫ Более важным, чем значение выражения присваивания, является тот факт, что присваивание устанавливает значение переменной (или другого 1-значения), и в силу этого оказывает влияние на состояние программы. Этот влияние на- зывается побочным эффектом присваивания. Многие выражения не имеют побочных эффектов и не влияют на состояние программы. Они относятся к идемпотентным (то есть приводящим к одним и тем же последствиям). Это означает, что выражение может вычисляться снова и снова и всякий раз будет возвращать одно и то же значение. Это также означает, что вычисление значения не влияет на значение других выражений. Вот несколько выражений, не имеющих побочных эффектов: х + у Math.sqrt(2) Важно усвоить, что присваивание не идемпотентно: х = 1 # Влияет на значение других выражений, использующих х х += 1 # При каждом новом вычислении возвращает другое значение Некоторые методы, к которым относится Math.sqrt, являются идемпотентны- ми: они могут быть вызваны без возникновения побочных эффектов. А другие методы таковыми не являются, и это в значительной степени зависит от того, осуществляют эти методы присваивания значений нелокальным переменным или нет. 4.5.1. Присваивание значений переменным Рассуждая о присваивании, мы обычно думаем о переменных, и они действи- тельно наиболее часто встречающиеся представители 1-значений в выражениях присваивания. Напомним, что в Ruby имеется четыре вида переменных: локаль- ные переменные, глобальные переменные, переменные экземпляра и переменные класса. Они отличаются друг от друга по первому символу в имени переменной. Присваивание одинаково работает со всеми видами переменных, поэтому здесь нам не нужно разбирать различия между разными видами переменных. Следует иметь в виду, что переменные экземпляра никогда не бывают видимы за пределами объекта, и в состав имен переменных никогда не входит название объ- екта. Рассмотрим следующее присваивание: point.x, point.у = 1. 2 L-значения этого выражения не являются переменными; они представляют собой атрибуты, которые вскоре будут рассмотрены. Присваивание значения переменной работает в соответствии с нашими ожида- ниями: переменной просто устанавливается указанное значений. Единственная трудность касается объявления переменной и неопределенности между именами локальных переменных и именами методов. В Ruby нет синтаксиса для явного объявления переменной: переменные начинают свое существование, когда им
4.5. Присваивания 125 присваивается какое-нибудь значение. К тому же имена локальных переменных и имена методов выглядят одинаково — у них нет префикса вроде символа $, по которому их можно было бы отличить друг от друга. Таким образом, простое выра- жение, такое как х, может ссылаться на локальную переменную с именем х или на метод, вызванный для текущего объекта — sei f, имеющий имя х. Чтобы разрешить эту неопределенность, Ruby рассматривает идентификатор в качестве локальной переменной, если он уже увидел любое предшествующее присваивание значений этой переменной. Он поступает подобным образом, даже если это присваивание никогда не выполнялось. Примером этому может послужить следующий код: class Ambiguous def х; 1; end # метод по имени "х". Всегда возвращает 1 def test puts х # Такие переменные не встречались: ссылка на метод, # определенный выше: выводится 1 # Строка, расположенная ниже, никогда не вычисляется из-за наличия условия # "if false". Но парсер видит ее. и рассматривает х в качестве переменной # во всей оставшейся части метода. х = 0 if false puts х # х - переменная, которой никогда не присваивалось # значение: выводится nil х = 2 # Это присваивание вычисляется puts х # Поэтому теперь эта строка выводит 2 end end 4.5.2. Присваивание значений константам Константы имеют явное отличие от переменных: их значения должны оставаться постоянными в течение всего времени работы программы. Поэтому при присваи- вании значений константам действует ряд особых правил. О Присваивание значений уже существующим константам заставляет Ruby вы- давать предупреждение. Тем не менее Ruby выполняет присваивание, что фак- тически означает непостоянство констант. О Присваивание значений константам не разрешено внутри тела метода. Ruby предполагает, что методы предназначены для многократного вызова; если можно было бы присваивать значение константе в методе, этот метод после первого же вызова выдавал бы предупреждение при каждом следующем вызове. Поэтому такое присваивание запрещено. В отличие от переменных, константы не начинают своего существования, пока Ruby-интерпретатор не выполнит выражение присваивания. Невычисляемые вы- ражения, похожие на следующее далее, не приводят к созданию константы: N = 100 if false
126 Глава 4. Выражения и операторы Значит, константа никогда не бывает в неинициализированном состоянии. Если константа существует, значит у нее есть присвоенное ей значение. Константа будет иметь значение ni 1 только в том случае, если именно оно и было ей при- своено. 4.5.3. Присваивание значений атрибутам и элементам массива В Ruby присваивание значений атрибутам или элементам массива фактически является сокращенной записью вызова метода. Предположим, у объекта о есть метод по имени ш=: последним символом в имени метода служит знак равенства. Тогда о. m можно использовать как 1-значение в выражении присваивания. Пред- положим также, что присваивается значение v: o.m = v Ruby-интерпретатор превратит это присваивание в следующий вызов метода: o.m=(v) # Если опустить скобки и добавить пробел, это будет выглядеть # как присваивание! То есть этот метод передает значение v методу т=. И этот метод может сделать с этим значением все что угодно. Как правило, будет проведена проверка, что значение имеет требуемый тип данных и находится в нужном диапазоне, а затем оно будет сохранено в переменной экземпляра объекта. Метод вроде ш= обычно сопровождается методом ш, который просто возвращает последнее переданное ш= значение. Принято говорить, что ш= — это метод-установщик (setter method), am — метод-получатель (getter method). Когда у объекта есть пара таких мето- дов, говорится, что он обладает атрибутом ш. В других языках программирования атрибуты иногда называют «свойствами». Более подробно Ruby-атрибуты будут рассмотрены в разделе 7.1.5. Присваивание значений элементам массива осуществляется также путем вызова метода. Если для объекта о определен метод по имени []= (именем метода служат именно эти три знака пунктуации), который ожидает передачи двух аргументов, то выражение о[х] = у фактически выполняется следующим образом: о.[]=(х,у) Если у объекта есть метод []=, который ожидает передачи трех аргументов, то он может быть индексирован с помощью двух значений, расположенных в квадрат- ных скобках. В таком случае следующие два выражения полностью равнозначны: о[х.у] = z o.[]=(x,y,z)
4.5. Присваивания 127 4.5.4. Сокращенная запись присваивания Сокращенная запись присваивания — это такая форма записи, в которой присваи- вание сочетается с какой-нибудь другой операцией. Чаще всего она используется для приращения значений переменных: х += 1 На самом деле += не является Ruby-оператором, и показанное выше выражение является сокращенной записью следующего выражения: X = X + 1 Сокращенная запись присваивания не может сочетаться с параллельным присва- иванием: она работает только с единственным 1-значением слева и единственным значением справа. Оно не может быть использовано, когда 1-выражение представ- ляет собой константу, поскольку ее использование приведет к присваиванию кон- станте нового значения, что станет причиной предупреждения. Но сокращенная запись присваивания может быть использована в том случае, когда 1-значение яв- ляется атрибутом объекта. Следующие два выражения полностью равнозначны: о.ш += 1 o.m=(o.m()+1) Сокращенная запись присваивания работает даже в том случае, когда 1-значением служит элемент массива. Эти два выражения тоже полностью равнозначны: о[х] -= 2 о.[]=(х. о.[](х) - 2) Заметьте, что в этом коде используется сочетание -=, а не сочетание +=. Как можно было предположить, псевдооператор -= вычитает свое г-значение из 1-значения. В дополнение к += и -= существует еще 11 других псевдооператоров, которые могут быть использованы для сокращения записи присваивания. Все они перечислены в таблице 4.1. Учтите, что сами по себе они не являются настоящими оператора- ми, они представляют собой простое сокращение для выражений, использующих другие операторы. Предназначение этих других операторов будет подробно рассмотрено далее в этой главе. К тому же, как мы увидим чуть позже, многие из этих других операторов определены в качестве методов. К примеру, если в классе определен метод по име- ни +, то тем самым изменяется и предназначение сокращенной записи присваива- ния += для всех экземпляров этого класса. Таблица 4.1. Псевдооператоры, использующиеся в сокращенной записи присваивания Присваивание Расширенный вариант X += у X = X + у X -= у X = X - у X *= у X = X * у продолжение &
128 Глава 4. Выражения и операторы Таблица 4.1 {продолжение) Присваивание Расширенный вариант X /= у X %= у X **= у X &&= у X 11= у X &= у X 1= у х А= у X «= у X »= у X = X / у X = X % у X = X ** у X = X && у X = X 11 у X = X & у X = X I у X = х А у X = X « у X = X » у ИДИОМА | | = Как замечено в начале этого раздела, наиболее часто сокращенная запись при- сваивания используется для приращения значения переменной с помощью псевдооператора +=. Для уменьшения значений переменных также часто используется псевдооператор -=. Другие псевдооператоры используются зна- чительно реже. Но одна идиома все же стоит упоминания. Представьте, что создается метод, вычисляющий некие значения, добавляющий их к массиву и возвращающий этот массив. Нужно предоставить пользователю возмож- ность определить массив, к которому будут добавляться результаты. Но если пользователь не определил массив, нужно создать новый, пустой массив. Можно воспользоваться следующей строкой кода: results ||= [] Тут есть над чем призадуматься. Это выражение расширяется в следующее: results = results || [] Если оператор || уже знаком по другим языкам программирования или если забежать чуть вперед и прочитать, что собой представляет Ruby-оператор ||, то можно понять, что правая часть этого присваивания вычисляется в значение results, если она не равна nil или false. В таком случае выражение приводит к созданию нового, пустого массива. Это означает, что показанная здесь сокра- щенная запись присваивания оставляет results без изменений, до тех пор пока значение results равно nil или false, в случае чего создается новый массив. Псевдооператор сокращенной записи присваивания ||= фактически ведет себя немного иначе, чем показанное здесь расширение. Если 1-значение псевдоо- ператора ||= не равно nil или false, то никакого присваивания вообще не вы- полняется. Если 1-значение является атрибутом или элементом массива, то метод-установщик, осуществляющий присваивание, не вызывается.
4.5. Присваивания 129 4.5.5. Параллельное присваивание Параллельным присваиванием считается любое выражение присваивания, в кото- ром более одного 1-значения, более одного г-значения или выполняются оба этих условия. Несколько 1-значений и несколько г-значений отделяются друг от друга запятыми. L-значения и г-значения могут предваряться символом *, который ино- гда называют оператором-звездочкой (splat operator), хотя на самом деле это не на- стоящий оператор. Предназначение * будет рассмотрено в этой главе чуть позже. Большинство выражений параллельного присваивания достаточно просты и име- ют вполне очевидное предназначение. Но есть и ряд сложных случаев, все вариан- ты которых будут рассмотрены в следующих подразделах. 4.5.5.1. Одинаковое количество I- и г-значений Самый простой случай параллельного присваивания возникает при одинаковом количестве 1- и г-значений: х. у. z = 1, 2. 3 # х=1; у=2; z=3 В данном случае первое г-значение присваивается первому 1-значению; второе r-значение присваивается второму 1-значению и т. д. Фактически эти присваивания выполняются параллельно, а не последовательно. К примеру, следующие две строки кода отличаются друг от друга: х.у = у.х # Выполняется параллельно: две переменные обмениваются значениями х = у: у = х # Выполняется последовательно: обе переменные имеют одно и то же # значение 4.5.5.Z. Одно 1-значение и несколько г-значений Когда имеется одно 1-значение и более одного г-значения, Ruby создает массив для хранения г-значений и присваивает этот массив 1-значению: х = 1. 2. 3 # х = [1,2.3] Перед 1-значением можно поставить символ *, при этом смысл или значение, воз- вращаемое этим присваиванием, не изменятся. Если нужно воспрепятствовать объединению нескольких г-значений в единый массив, нужно после 1-значения поставить запятую. Даже если после запятой от- сутствуют 1-значения, Ruby вынужден действовать, как будто он работает с не- сколькими 1-значениями: х, = 1, 2. 3 # х = 1: остальные значения отбрасываются 4.5.5.3. Несколько 1-значений и одно г-значение Когда имеется несколько 1-значений и только одно г-значение, Ruby предприни- мает попытку расширить г-значение в список значений для присваивания. Если г-значение является массивом, Ruby раскрывает массив, чтобы каждый элемент
130 Глава 4. Выражения и операторы стал отдельным г-значением. Если г-значение не является массивом, но для него реализован метод to_ary, Ruby вызывает этот метод, а затем раскрывает возвра- щенный им массив: х, у, z = [1, 2, 3] # То же самое, что и x.y.z = 1,2,3 Параллельное присваивание было переработано так, чтобы было несколько 1-зна- чений и нуль (если раскрываемый массив был пустым) или более г-значений. Если количество 1- и г-значений одинаково, то присваивание происходит так, как описано ранее в разделе 4.5.5.1. Если количество разное, то присваивание осу- ществляется так, как описано далее, в разделе 4.5.5.4. Чтобы превратить обычное непараллельное присваивание в параллельное, ко- торое автоматически распаковывает массив, расположенный справа, можно вос- пользоваться рассмотренным ранее трюком с замыкающей запятой: х = [1.2] # х получает значение [1.2]: это не параллельное присваивание х, = [1,2] # х получает значение 1: замыкающая запятая делает его # параллельным 4.5.5.4. Различное количество I- и г-значений Если 1-значений больше, чем г-значений и не использовано никаких операторов- звездочек, то первое г-значение присваивается первому 1-значению, второе г-зна- чение присваивается второму 1-значению и т. д., пока все г-значения не будут при- своены. Затем каждому из оставшихся 1-значений присваивается nil, при этом любое существующее значение для этого 1-значения переписывается: х. у. z = 1, 2 # х=1; у=2: z=ni 1 Если г-значений больше чем 1-значений и не использовано никаких операторов- звездочек, то г-значения присваиваются по порядку каждому 1-значению, а остав- шиеся г-значения отбрасываются: х. у = 1, 2. 3 # х=1; у=2: 3 ничему не присваивается 4.5.5.5. Оператор-звездочка Если перед г-значением поставить звездочку, это будет означать, что это значение является массивом (или похожим на массив объектом) и все его элементы долж- ны быть г-значениями. Элементы массива заменяют массив в исходном перечне г-значений, и присваивание происходит так, как описано выше: х, у, z = 1, *[2,3] # То же самое, что и x.y.z = 1,2,3 В Ruby 1.8 звездочка может появляться только перед последним г-значением при- сваивания. В Ruby 1.9 перечень г-значений в параллельном присваивании может иметь любое количество звездочек, и они могут появляться в любой позиции спи- ска. Но в обеих версиях языка не допускается попытка использования «двойной звездочки» для вложенных массивов: х.у = **[[1.2]] # Синтаксическая ошибка - SyntaxError!
4.5. Присваивания 131 В Ruby 1.8 звездочкой могут быть помечены массив, диапазон и хэш, используе- мые в качестве г-значений. В Ruby 1.9 звездочкой могут быть помечены массив, диапазон и нумератор (рассмотренный в разделе 5.3.4), используемые в качестве г-значений. Если применять звездочку к значению какого-нибудь другого клас- са, то это значение будет просто развернуто в само себя. Можно определить свои собственные развертываемые с помощью звездочки классы. В Ruby 1.8 определен метод to ary, который возвращает массив значений. В Ruby 1.9 вместо этого ис- пользуется имя tospl at. Когда перед 1-значением стоит звездочка, это означает, что все дополнительные г-значения должны быть помещены в массив и присвоены этому 1-значению. Зна- чение, присвоенное 1-значению, всегда является массивом, который может иметь нуль, один или более элементов: х.*у = 1.2.3 # х=1: у=[2.3] х,*у = 1, 2 # х=1: у=[2] х.*у = 1 # х=1: у=[] В Ruby 1.8 звездочка может предшествовать лишь последнему 1-значению переч- ня. В Ruby 1.9 левая часть параллельного присваивания может включать один оператор-звездочку, но он может появляться в любой позиции перечня: f Только для Ruby 1.9 *х.у = 1,2.3 #х=[1.2];у=3 *х.у = 1. 2 #х=[1]:у=2 *х.у = 1 # х=[]: у=1 Учтите, что звездочки могут появляться по обе стороны выражения параллельно- го присваивания: х, у, *z = 1. *[2,3.4] # X=l: у=2: z=[3,4]. И наконец, следует напомнить, что ранее мы рассмотрели два простых случая па- раллельного присваивания, в которых есть одно 1- или одно г-значение. Учтите, что в обоих этих случаях все происходит так, если бы перед единственным 1- или r-значением стояла звездочка. Явное включение звездочки в этом случае не возы- меет никакого дополнительного эффекта. 4.5.5.6. Скобки в параллельном присваивании Одним из самых непонятных свойств параллельного присваивания является ис- пользование в левой части скобок для «подприсваивания». Если группа из двух или более 1-значений взята в круглые скобки, то она изначально считается еди- ным 1-значением. При определении соответствующего г-значения в выражении рекурсивно применяются правила параллельного присваивания — это г-значение присваивается группе 1-значений, помещенных в скобки. Рассмотрим следующее присваивание: x.(y.z) = а, b
132 Глава 4. Выражения и операторы На самом деле это два присваивания, выполняемые одновременно: х = а y.z = b Но следует заметить, что второе присваивание само по себе является параллель- ным. Поскольку мы используем скобки в левой части выражения, выполняется рекурсивное параллельное присваивание. Чтобы все это работало, b должен быть развертываемым объектом, таким как массив или нумератор. Приведем несколько конкретных примеров, чтобы прояснить все рассмотренное ранее. Заметьте, что скобки в левой части действуют как «распаковщик» одного из уровней вложенного массива из правой части: x.y.z = 1,[2,3] x.(y.z) = 1,[2.3] a.b.c.d = [1.[2,[3.4]]] a.(b.(c.d)) = [1.[2.[3.4]]] # Скобок нет: x=l:y=[2,3];z=rril # Скобки: x=l;y=2;z=3 # Скобок нет: а=1;Ь=[2.[3.4]]:c=d=nil # Скобки: а=1:b=2:с=3:d=4 4.5.5.7. Значение параллельного присваивания Возвращаемое значение параллельного присваивания является массивом, со- стоящим из г-значений (после развертывания с помощью любого оператора- звездочки). ПАРАЛЛЕЛЬНОЕ ПРИСВАИВАНИЕ И ВЫЗОВ МЕТОДА В качестве отступления следует заметить, что если параллельное присваи- вание предваряется именем метода, Ruby-интерпретатор истолкует запя- тые в качестве разделителей аргумента метода, а не в качестве разделителей 1- и г-значений. Если нужно проверить возвращаемое значение параллельного присваивания, можно написать следующий код для его вывода: puts х,у=1,2 Но он не выдаст желаемого; Ruby посчитает, что вызывается метод puts с тремя аргументами: х, у= 1 и 2. Затем можно попытаться сгруппировать параллельное присваивание, поместив его в скобки: puts (х.у=1,2) Но этот код также не будет работать; скобки будут истолкованы как часть вы- зова метода (хотя в Ruby не приветствуется пробел между именем метода и открывающей скобкой). Чтобы на самом деле выполнить задуманное, нужно использовать вложенные скобки: puts((x,y=l,2)) Это один из тех странных и неприглядных случаев в грамматике Ruby, из которых складываются особенности этого языка. К счастью, необходимость использования подобного синтаксиса возникает довольно редко.
4.6. Операторы 133 4.6. Операторы Оператор — это лексема языка Ruby, которая представляет операцию (такую как сложение или сравнение), выполняемую над двумя или более операндами. Операнды являются выражениями, и операторы позволяют нам объединять эти выражения-операнды в более крупные выражения. Числовой литерал 2 и опера- тор +, к примеру, могут быть объединены в выражение 2+2. А в следующем вы- ражении объединены числовой литерал, выражение вызова метода и выражение ссылки на переменную с оператором умножения и оператором «меньше, чем»: 2 * Math.sqrt(2) < limit Далее в этом разделе, в таблице 4.2 приводятся все Ruby-операторы, а в следую- щем разделе приводится подробное описание каждого из них. Но чтобы получить полное представление об операторах, сначала нужно разобраться с числом опе- рандов, уровнем приоритета и взаимосвязанностью операторов. Число операндов (или арность) оператора определяет поле его деятельности. Унар- ные операторы предполагают использование единственного операнда. Бинарные операторы предполагают использование двух операндов. Тернарные операторы (представленные единственным экземпляром) предполагают использование трех операндов. Число операндов каждого оператора приведено в столбце N табли- цы 4.2. Учтите, что операторы + и - имеют как унарную, так и бинарную форму. Приоритетность операторов определяется тем, насколько «сильно» оператор привязан к своим операндам, и влияет на порядок вычисления выражения. Рас- смотрим, к примеру, следующее выражение: 1 + 2*3 # => 7 Оператор умножения по приоритетности стоит выше, чем оператор сложения, поэтому сначала выполняется умножение, и выражение вычисляется в 7. Табли- ца 4.2 выстроена сверху вниз по приоритетности операторов. Заметьте, что для булевых операторов AND, OR и NOT (И, ИЛИ и НЕ) существуют операторы как с высокой, так и с низкой степенью приоритета. Приоритетность операторов определяет лишь исходный порядок вычисления вы- ражения. Но для группировки подвыражений всегда можно воспользоваться круглыми скобками и определить свой собственный порядок вычисления. Например: (1 + 2) * 3 # => 9 Взаимосвязанность операторов определяет порядок вычислений, когда в вы- ражении последовательно появляется один и тот же оператор (или операторы с одинаковой степенью приоритета). Столбец «А» таблицы 4.2 определяет по- рядок совместной работы каждого оператора. «Ь» означает, что выражение вы- числяется слева направо. «R» означает, что выражение вычисляется справа на- лево. И «N» означает, что оператор не обладает взаимосвязанностью и не может
134 Глава 4. Выражения и операторы использоваться в выражении по нескольку раз без скобок, определяющих по- рядок вычисления. Большинство арифметических операторов имеют взаимосвязанность слева напра- во, что означает, что 10-5-2 вычисляется как (10-5)-2 вместо того, чтобы вычис- ляться 10- (5-2). С другой стороны, возведение в степень имеет взаимосвязанность справа налево, поэтому 2**3**4 вычисляется как 2**(3**4). Примером другого опе- ратора, имеющего взаимосвязанность справа налево, является оператор присваи- вания. В выражении а=Ь=0 значение 0 сначала присваивается переменной Ь. Затем значе- ние выражения (которое также равно нулю) присваивается переменной а. Многие операторы в Ruby реализованы в виде методов, позволяя классам опреде- лять новый смысл для этих операторов. В столбце «М» таблицы 4.2 указано, какие операторы являются методами. Операторы, помеченные буквой «Y» реализова- ны с помощью методов и могут быть переопределены, а операторы, помеченные буквой «N», переопределению не подлежат. Как правило, в классах могут быть определены свои собственные операторы арифметических действий, упорядоче- ния и равенства, но различные булевы операторы не могут быть переопределе- ны. В этой главе мы сгруппировали операторы по общности их предназначения для стандартных Ruby-классов. В других классах могут быть определены другие предназначения для операторов. К примеру, оператор + выполняет сложение чи- сел и относится к арифметическим операторам. Но он также используется для объединения строк и массивов. Оператор на основе метода вызывается как метод в отношении своего левого операнда (или своего единственного операнда в случае использования унарных операторов). Правый операнд передается методу в каче- стве аргумента. Имеющиеся в классах определения любых операторов на осно- ве методов можно найти точно так же, как и определения любых других методов класса. Например, чтобы найти определение оператора * для строк, можно вос- пользоваться инструментальным средством ri: ri 'String.*' Для определения унарных операторов + и - используются методы с именами -Ч? и чтобы избежать двусмысленности относительно бинарных операторов, ис- пользующих такие же символы. Операторы ! = и !— определены как отрицания операторов == и =-. В Ruby 1.9 операторы != и !~ можно переопределить. В более ранних версиях языка этого сделать невозможно. В Ruby 1.9 также можно перео- пределить унарный оператор !. Таблица 4.2. Операторы Ruby, расположенные по приоритетности (по нисходящей), с указанием числа операндов (N), взаимосвязанности (А) и возможности переопределения (М) Оператор(ы) N А М Операции ! - + 1 R Y Булево NOT (НЕ), поразрядное дополнение, унарный плюс1 ** 2 R Y Возведение в степень - 1 R Y Унарный минус (определяется с помощью -@)
4.6. Операторы 135 Оператор(ы) N A M Операции * / % 2 L Y Умножение, деление, деление по модулю (остаток) + - 2 L Y Сложение (или объединение), вычитание « » 2 L Y Поразрядный сдвиг влево (или добавление), поразряд- ный сдвиг вправо & 2 L Y Поразрядное AND (И) 1А 2 L Y Поразрядное OR (ИЛИ), поразрядное XOR (исключаю- щее ИЛИ) <<=>=> 2 L Y Порядок следования == === 1= =- 1- <=> 2 N Y Равенство, соответствие шаблону, сравнение2 м 2 L N Булево AND (И) II 2 2 L N N N Булево OR (ИЛИ) Создание диапазона и булевы триггеры ? 3 R N Условие Rescue 2 L N Модификатор обработки исключения **= *= /= %= + 2 R N Присваивания &&= &= ||= |= ж= defined? 1 N N Проверка определения и типа переменной not 1 R N Булево NOT (НЕ) (низкий уровень приоритета) and or 2 L N Булево AND (И), булево OR (ИЛИ) (низкий уровень приоритета) if unless while until 2 N N Модификаторы условий и цикла 1! не может быть переопределен в версиях, предшествующих Ruby 1.9. Унарный плюс опре- деляется с помощью +@. 2 != и ! - не могут быть переопределены в версиях, предшествующих Ruby 1.9. 4.6.1. Унарные операторы + и - Унарный оператор «минус» изменяет знак своего числового аргумента. Можно воспользоваться и унарным оператором «плюс», но на числовые операнды он не оказывает никакого воздействия — просто возвращает значение своего операнда. Он предоставлен для симметрии с унарным минусом и, разумеется, может быть переопределен. Заметьте, что унарный минус имеет уровень приоритета несколько меньший, чем унарный плюс; это обстоятельство рассматривается в следующем разделе, посвя- щенном оператору **. Именами методов, являющихся основой этих унарных операторов, являются -@ и +@. При переопределении операторов, их вызове в виде методов или при поиске документации для операторов следует использовать эти имена. Эти специальные имена необходимы для устранения путаницы между операторами унарного плюса и минуса и бинарными плюсом и минусом.
136 Глава 4. Выражения и операторы 4.6.2. Возведение в степень: ** Оператор ** выполняет возведение в степень своего первого операнда, исполь- зуя в качестве показателя степени свой второй операнд. Учтите, что использова- ние дробного числа в качестве второго операнда позволяет извлекать корень из какого-нибудь числа. Например, кубический корень из х — это х**(1.0/3.0). Ана- логично этому х**-у — это то же самое, что и 1/(х**у). Оператор ** имеет взаи- мосвязанность справа налево, поэтому x**y**z — это то же самое, что и x**(y**z). И наконец, следует отметить, что оператор ** имеет более высокий уровень при- оритета, чем оператор унарного минуса, поэтому -1**0.5 — это то же самое, что и - (1**0.5). А если на самом деле требуется извлечь квадратный корень из -1, нуж- но воспользоваться скобками: (-1)**0.5. (Мнимый результат не является числом (not-a-number), и выражение вычисляется в NaN.) 4.6.3. Арифметические операторы: +, % Операторы +, -, * и / выполняют сложение, вычитание, умножение и деление во всех классах, относящихся к числам, — Numeric. Целочисленное деление возвраща- ет целочисленный результат, а любой остаток отбрасывается. Остаток может быть вычислен с помощью оператора деления по модулю — %. Целочисленное деление на нуль выдает ошибку деления на нуль — ZeroDi visionError. Деление чисел с пла- вающей точкой на нуль возвращает плюс или минус бесконечность — Infinity. Если при делении чисел с плавающей точкой нуль делится на нуль, то возвра- щается NaN. Дополнительные сведения о целочисленной арифметике, имеющейся в Ruby и об арифметике чисел с плавающей точкой изложены в разделе 3.1.3. В классе String оператор + используется для объединения строк, оператор * — для повторения строк, а оператор К, для вставки в строку аргумента функции sprintf. В классе Array оператор + используется для объединения массивов, а оператор - используется для вычитания массивов. Оператор * используется в классе Array по-разному, в зависимости от класса второго операнда. Когда массив «умножает- ся» на число, в результате возвращается новый массив, повторяющий содержание массива, используемого в качестве операнда, указанное количество раз. Но когда массив умножается на строку, результат такой же, как и при использовании по отношению к массиву метода объединения join с передачей ему этой строки в ка- честве аргумента. 4.6.4. Сдвиг и добавление: << и >> В классах Fixnum и Bignum определены операторы « и », предназначенные для по- битового сдвига влево и вправо левого операнда. Правый операнд определяет ко- личество позиций побитового сдвига, а отрицательные значения приводят к сдви- гу в противоположном направлении: сдвиг влево на -2 позиции это то же самое, что и сдвиг вправо на 2 позиции. При сдвиге влево объекта класса Fixnum старшие
4.6. Операторы 137 биты никогда не «вытесняются». Если результат сдвига не помещается в Fixnum, возвращается значение Bignum. Но при правом сдвиге младшие биты операнда всегда утрачиваются. Сдвиг битов числа влево на 1 бит соответствует его умножению на 2. Сдвиг битов числа вправо на 1 бит соответствует его делению на 2. В следующих нескольких примерах числа показаны в двоичной записи и результаты также преобразуются в двоичную форму: (ОМОН « l).to_s(2) # => "ЮНО" 11 « 1 => 22 (ОЫ0110 » 2).to_s(2) # => "101” 22 » 2 => 5 Оператор « используется также в качестве оператора добавления, и в этом ка- честве он, видимо, более востребован. В классах String, Array и 10 он определен именно для этой цели, как и в ряде других «добавляемых» классов из стандартной библиотеки, таких как Queue и Logger: message = "hello" messages = [] message « " world" messages « message STDOUT « message # Строка # Пустой массив # Добавление к строке # Добавление сообщения к массиву # Вывод сообщения в стандартный выходной поток 4.6.5. Дополнение, объединение, пересечение: I и Л В классах Fixnum и Bignum эти операторы определены для выполнения побитовых операций NOT (НЕ), AND (И), OR (ИЛИ) и XOR (исключающего ИЛИ). Опе- ратор - является унарным с высоким уровнем приоритета, а остальные операторы являются бинарными со средним уровнем приоритета. Оператор - меняет каждый 0-бит своего целочисленного операнда на 1 и каждый 1-бит на 0, выдавая двоичное дополнение числа до единицы. Для любого целого числа х, -х — это тоже самое, что и -х-1. Оператор & выполняет операцию побитового AND двух целых чисел. Бит резуль- тата устанавливается в 1, только если соответствующий бит каждого операнда установлен в 1. Например: (ОМОЮ & 0Ы100).to_s(2) # => "1000" Оператор | выполняет операцию побитового OR двух целых чисел. Бит резуль- тата устанавливается в 1, только если любой из соответствующих битов каждого операнда установлен в 1. Например: (ОМОЮ I ObllOO). to_s(2) # => "1110" Оператор А выполняет операцию побитового XOR (исключающего OR) двух це- лых чисел. Бит результата устанавливается в 1, если один из соответствующих би- тов операндов (но не оба сразу) установлен в 1. Например: (ОМОЮ А 0ЫЮ0).to_s(2) # => "ПО"
138 Глава 4. Выражения и операторы Эти операторы применяются также и в других классах в соответствии с имею- щимися в них понятиями о логических операциях AND, OR и NOT. В массивах операторы & и | используются для операций пересечения и объединения. Когда к двум массивам применяется оператор &, возвращается новый массив, который содержит только те элементы, которые появляются в левом и (AND) в правом массиве. Когда к двум массивам применяется оператор |, возвращается новый массив, в котором содержатся все элементы, имеющиеся как в левом, так и в пра- вом массиве, то есть или (OR) в одном массиве, или в другом массиве. Подробно- сти и примеры изложены в разделе 9.5.2.7. В классах TrueClass, FalseClass и NilClass тоже определены операторы &, | и А (то есть все, кроме -), поэтому они могут использоваться в качестве булевых опе- раторов. Но все же следует заметить, что их вряд ли стоит использовать. Булевы операторы && и 11 (рассматриваемые далее в разделе 4.6.8) предназначены для бу- левых операндов и обладают большей эффективностью, поскольку они не вычис- ляют свой правый операнд, если его значение не повлияет на результат операции. 4.6.6. Сравнение: <г <=, >г >= и <=> В некоторых классах определен естественный порядок значений. Числа выстраи- ваются по величинам, строки — по алфавиту, даты — по хронологии. Операторы «меньше, чем» (<), «меньше, чем или равно» (<=), «больше, чем или равно» (>=) и «больше, чем» (>) выдвигают утверждения об относительном порядке двух зна- чений. Они вычисляются в true, если утверждение является истинным, и вычис- ляются в fal se в противном случае. (И, как правило, они выдают исключение, если их операнды имеют несовместимые типы данных.) В классах операторы сравне- ния могут определяться в индивидуальном порядке. Но проще и привычнее будет определить в классе единственный оператор <=>. Это оператор сравнения обще- го назначения, и он возвращает значение, указывающее относительный порядок двух операндов. Если левый операнд меньше, чем правый, оператор <=> возвра- щает -1. Если левый операнд больше, он возвращает +1. Если два операнда равны друг другу, оператор возвращает 0. И если сравнить два операнда не удается, он возвращает nil1. Если определен оператор <=>, то класс может просто включать модуль Comparable, в котором определяются другие операторы сравнения (вклю- чая оператор ==) в понятиях оператора <=>. Класс Module заслуживает отдельного упоминания: в нем реализованы операторы сравнения, показывающие взаимоотношения между подклассами (Module явля- ется надклассом класса Class). Для классов А и В, А < В вычисляется в true, если А является подклассом или производным класса В. В этом случае «меньше, чем» означает «более специализированный, чем» или «более узконаправленный тип, 1 Некоторые реализации этого оператора могут возвращать вместо -1 и +1 любое значение меньше нуля или любое значение больше нуля. Если осуществлять реализацию <=>, то она должна возвращать -1, 0 или +1. Но если использовать готовый <=>, нужно проверять зна- чение на меньше, чем или больше, чем нуль, вместо того чтобы предполагать, что результат всегда будет -1, 0 или +1.
4.6. Операторы 139 чем». Следует взять на заметку, что (как будет показано в главе 7) символ < также используется при объявлении подклассов: i Объявление класса А в качестве подкласса В class А < В end В классе Module оператор > определяется для работы аналогичной той, что вы- полняет оператор < при обратном размещении операндов. А операторы <= и >= в нем определены так, чтобы они также возвращали true, если оба операнда от- носятся к одному и тому же классу. Самое интересное, что операторы сравнения, определяемые в классе Modul е, определяют порядок сравниваемых значений лишь частично. Рассмотрим классы String и Numeric. Оба они являются подклассами класса Object, и ни один из этих классов не является подклассом второго класса. В таком случае, когда оба оператора не имеют непосредственной связи, операторы сравнения возвращают ni 1, а не true или fal se: String < Object Object > Numeric Numeric < Integer String < Numeric # true: String более узкий класс, чем Object # true: Object более общий класс, чем Numeric # false: Numeric не является более узким классом, чем Integer # nil: String и Numeric ничем не связаны друг с другом Если в классе определяется общий порядок его значений и а < b не является исти- ной, то нужно обеспечить, чтобы а >= b было истиной. Но когда в классе наподобие Module определяется лишь частичная упорядоченность, то такого предположения делать не стоит. 4.6.7. Равенство: = =, !=, =~,и === Символы == представляют собой оператор равенства. Он определяет, являют- ся ли два значения равными в соответствии с определением «равенства», при- нятым для левого операнда. Оператор != является простой противоположностью оператору ==: он вызывает ==, а затем возвращает противоположный результат. В Ruby 1.9 оператор != можно переопределить, но в Ruby 1.8 этого сделать не- возможно. Более подробное рассмотрение равенства объектов Ruby приведено в разделе 3.8.5. Символы =~ представляют собой оператор сопоставления с шаблоном. В классе Object этот оператор определен таким образом, что он всегда возвращал false. Для строк он переопределен таким образом, что в качестве правого операнда предпо- лагается использование регулярного выражения — Regexp. А для класса Regexp этот оператор переопределен таким образом, что в качестве правого операнда предполагается использование строки — String. Оба этих оператора возвращают nil, если строка не соответствует шаблону. Если строка соответствует шаблону, операторы возвращают целочисленный индекс, указывающий, с какой позиции начинается соответствие. (Учтите, что в булевых выражениях ni 1 действует как false, а любое целое число — как true.)
140 Глава 4. Выражения и операторы Оператор ! - является противоположностью оператора =~: он вызывает =~ и воз- вращает true, если =~ возвращает nil, или возвращает false, если =~ возвращает целое число. В Ruby 1.9 оператор !- можно переопределить, но в Ruby 1.8 этого сделать невозможно. Оператор === является оператором case-равенства. Он неявным образом исполь- зуется оператором выбора case (этот вопрос рассматривается в главе 5). В явном виде он используется гораздо реже оператора ==. В классах Range, Class и Regexp этот оператор определен в качестве разновидности оператора сопоставления с об- разцом или членства. Другие классы наследуют определение, существующее в классе Object, в котором вместо него просто вызывается оператор ==. Дополнительные сведения изложены в разделе 3.8.5. Учтите, что оператора !== не существует, и если требуется полу- чить отрицание оператора ===, то это нужно сделать самостоятельно. 4.6.8. Булевы операторы: &&, | |, 1, and, or, not Булевы операторы Ruby встроены в язык и не основаны на методах: в классах не может быть, к примеру, определен собственный метод &&. Причина в том, что булевы операторы могут применяться к любому значению и должны иметь не- изменное поведение для любого типа операндов. В Ruby определены специаль- ные значения true и false, но отсутствует булев тип — Boolean. Для обеспечения работы булевых операторов значения false и nil считаются ложными (false), а все другие значения, включая true, 0, NaN, □ и {} — считаются истинными (true). Исключением является оператор !; в Ruby 1.9 (но не в Ruby 1.8) его можно перео- пределить. Следует заметить, что можно также определить методы с именами and, or и not, но это всего лишь методы, которые никак не изменят поведения операто- ров с такими же именами. Другая причина принадлежности булевых операторов Ruby к ядру языка, а не к переопределяемыми методами, заключается в том, что бинарные операторы счи- таются «короткозамкнутыми». Если значение операции полностью определяется левым операндом, правый операнд игнорируется и даже никогда не вычисляется. Если правый операнд является выражением, имеющим побочные эффекты (та- ким как присваивание или вызов метода с побочными эффектами), то эти побоч- ные эффекты могут возникать, а могут и не возникать, в зависимости от значения левого операнда. Символы && представляют собой булев оператор AND. Он возвращает истинное значение, если оба его операнда, левый и (AND) правый, имеют истинные зна- чения. В противном случае он возвращает ложное значение. Заметьте, что в этом описании сказано «истинное значение» и «ложное значение», а не «значение true» и «значение false». Оператор && часто используется вместе с операторами сравне- ния, такими как == и <, в выражениях, похожих на следующее: х == 0 && у > 1
4.6. Операторы 141 Операторы сравнения и равенства обычно вычисляются в значения true и false, и в этом случае оператор && работает с реальными булевыми значениями. Но так бывает не всегда. Оператор может быть использован и таким образом: х && у В этом случае х и у могут быть чем угодно. Значение выражения — это либо зна- чение х, либо значение у. Если оба значения, и х и у, являются истинными, то значение выражения равно значению у. Если х является ложным значением, то значением выражения будет х. В противном случае у должен быть ложным значе- нием, и значением выражения будет у. На самом деле оператор && работает следующим образом. Сначала он вычисляет левый операнд. Если этот операнд равен ni 1 или false, то он возвращает это значе- ние и полностью игнорирует правый операнд. В противном случае левый операнд является истинным значением, и полное значение оператора && зависит от значе- ния правого операнда. В этом случае оператор вычисляет свой правый операнд и возвращает полученное значение. Из факта, что && может игнорировать правый операнд, можно извлечь выгоду в программном коде. Рассмотрим следующее выражение: х && print(x.to_s) Этот код выводит значение х в виде строки, но только в том случае, если х не равен nil или false1. Оператор 11 возвращает булево OR (ИЛИ) своих операндов. Он возвращает ис- тинное значение, если оба из его операндов являются истинными значениями. Если оба операнда являются ложными значениями, он возвращает ложное зна- чение. Подобно оператору &&, оператор 11 игнорирует свой правый операнд, если его значение не влияет на значение операции. Оператор 11 работает следующим образом: сначала он вычисляет свой левый операнд. Если его значение является любым, отличным от ni 1 или false, то он просто возвращает это значение. В про- тивном случае он вычисляет свой правый операнд и возвращает вычисленное зна- чение. Оператор 11 может использоваться для соединения нескольких выражений срав- нения или равенства: х< 0 || у < 0 || z < О # Является ли любая их этих координат отрицательным # числом? В этом случае операнды, используемые оператором 11, будут реальными значе- ниями true или false. Но оператор 11 не ограничен одной только работой с true и false. Одна из особенностей использования оператора 11 заключается в том, что он возвращает первое значение, не равное ni 1 из череды альтернатив: 1 То, что выражение может быть написано именно так, еще не означает, что так и следует делать. В главе 5 будет показано, что это выражение лучше написать следующим образом: print(x.to_s) if х
142 Глава 4. Выражения и операторы # Если аргумент х равен nil, то его значение берется из хэша пользовательских # предпочтений (preferences) или из константы значения по умолчанию. х = х || preferences!; :х] || Defaults: :Х Заметьте, что оператор && по приоритету стоит выше оператора 11. Рассмотрим следующее выражение: 1 || 2 && nil # => 1 Сначала выполняется оператор &&, и значение этого выражения равно 1. Но если сначала выполнялся бы оператор 11, значение было бы равно nil: (1 || 2) && nil # => nil Оператор ! выполняет унарное булево NOT (НЕ). Если операнд равен nil или false, то оператор ! возвращает true. В противном случае оператор ! возвращает false. Оператор ! обладает высшим уровнем приоритета. Значит, если нужно вычислить логическую инверсию выражения, которое само использует операторы, следует воспользоваться скобками: !(а && Ь) Кстати, один из принципов булевой логики позволяет переписать это выражение следующим образом: !а || !Ь Операторы and, or и not являются версиями &&, 11 и ! с низким уровнем приори- тета. Одним из поводов применения этих версий может стать то обстоятельство, что их имена являются английскими словами, что может облегчить чтение кода. Попробуйте, к примеру, прочитать следующую строку кода: if х > 0 and у > 0 and not defined? d then d = Math.sqrt(x*x + y*y) end Другим поводом для применения этих альтернативных версий булевых операто- ров может послужить тот факт, что они имеет более низкий уровень приоритета по сравнению с оператором присваивания. Значит, можно написать булево выра- жение, похожее на следующее, в котором значения присваиваются переменным до тех пор, пока не будет вычислено ложное значение: if а = f(x) and b = f(y) and c = f(z) then d = g(a,b,c) end Если вместо and использовать &&, то выражение просто не будет работать. Нуж- но отметить, что and и or обладают одинаковым уровнем приоритета (a not имеет уровень чуть выше). Поскольку and и or имеют одинаковый уровень приоритета, а у && и || разный уровень приоритета, при вычислении следующих двух выраже- ний получаются разные значения: х || у && nil # && выполняется первым => х х or у and nil # вычисление осуществляется слева направо => nil
4.6. Операторы 143 4.6.9. Диапазоны и триггеры:.. и ... Группы символов .. и ... мы уже рассматривали в разделе 3.5, где они о них шла речь как о части синтаксиса литерала диапазона — Range. Если начальная и ко- нечная точки диапазона сами по себе являются целочисленными литералами, как в примере 1. .10, Ruby-интерпретатор при синтаксическом разборе создает точ- ный Range-объект. Но если начальной и конечной точками являются выражения посложнее целочисленных литералов, как в примере х. .2*х, то назвать это Range- литералом будет уже трудно. Скорее это выражение создания диапазона. Из этого следует, что .. и ... являются операторами, а не простыми элементами синтаксиса литерала диапазона. Операторы .. и ... не определены как методы и не поддаются переопределению. У них относительно низкий уровень приоритета, значит, обычно их можно при- менять, не расставляя скобок вокруг левого или правого операндов: х+1.. х*х Значения этих операторов представляют собой Range-объект. Выражение х. .у со- ответствует выражению: Range.new(x.y) А х... у — выражению: Range. new(x, у, true) 4.6.9.1. Булевы триггеры Когда операторы .. и ... используются в таких условиях, как i f, или в таких ци- клах, как while (более подробно условия и циклы рассмотрены в главе 5), они не создают Range-объекты. Вместо этого они создают специальный вид булева выра- жения, называемого триггером (flip-flop). Триггерное выражение вычисляется в true или false, точно так же, как это делается в выражениях сравнения и равен- ства. Но совершенно непривычным свойством триггерного выражения является то, что его значение зависит от значений предыдущих вычислений. А это означает, что триггерное выражение имеет связанное с ним состояние; оно должно запоми- нать информацию о предыдущих вычислениях. Поскольку у него есть состояние, можно предположить, что триггер является каким-нибудь объектом. Но на самом деле это не так, он является Ruby-выражением, а Ruby-интерпретатор сохраняет нужное ему состояние (простое булево выражение) в представлении этого выра- жения внутри своего синтаксического анализатора. Памятуя об этом, рассмотрим триггер, применяемый в следующем коде. Учтите, что первый, встречающийся в коде оператор .. создает Range-объект, а второй — создает триггерное выражение: (1..10).each {|х| print х if х==3..х==5 } Триггер состоит из двух булевых выражений, объединенных оператором .., в кон- тексте условия или цикла. Триггерное выражение вычисляется в fal se до тех пор,
144 Глава 4. Выражения и операторы пока левое выражение вычисляется в true. Как только это выражение приобретает значение true, выражение «перебрасывается» в устойчивое состояние true. Оно будет оставаться в этом состоянии, и последующие вычисления будут возвращать true до тех пор, пока правое выражение вычисляется в true. Когда это случится, триггер «перебрасывается» назад, в устойчивое состояние false. Последующие вычисления выражения возвращают fal se до тех пор, пока левое выражение снова не станет равно true. В приведенном примере кода значение триггера вычисляется многократно для значений х от 1 до 10. Вычисление начинается с состояния false и вычисляется в false, когдах равен 1 и 2. Когдах==3, триггер перебрасывается в true и возвращает true. Он продолжает возвращать true, когда х равен 4 и 5. Но когда х==5, триггер перебрасывается обратно в false и возвращает false для всех оставшихся значе- ний х. В результате этот код выводит 345. Триггеры могут создаваться либо с оператором .., либо с оператором .... Разница в том, что при использовании оператора .. когда триггер перебрасывается в true, он возвращает true, но также тестирует свое правое выражение, чтобы посмотреть, не должно ли оно перебросить его внутреннее состояние обратно в false. При ис- пользовании оператора ... прежде тестируется правое выражение, ожидается сле- дующее вычисление. Рассмотрим следующие две строки: # Выводит "3". Перебрасывается в новое состояние и возвращается в прежнее. # когда х==3 (1..10).each {|х| print х if х==3..х>=3 } # Выводит "34". Преребрасывается в новое состояние, когда х == 3. и возвращается # в прежнее, когда х==4 (1..10).each {|х| print х if х==3...х>=3 } # Выводится "34" Триггеры являются довольно трудным для понимания свойством Ruby, и лучше, наверное, избегать их применения в программном коде. Но они встречаются не только в Ruby. Это свойство унаследовано от языка Perl, который в свою очередь унаследовал его от имевшихся в Unix средств обработки текста sed и awk1. Перво- начально триггеры предназначались для соответствия строкам текстового файла между начальным и конечным шаблонами. Польза от такого их применения не утратила своего значения. В следующей простой Ruby-программе демонстриру- ется применение триггера. Эта программа построчно считывает текстовый файл и выводит любую строку, содержащую текст «TODO». Вывод строк продолжается до тех пор, пока не будет считана пустая строка: ARGF.each do |line| # Для каждой строки стандартного ввода # или указанного файла print line if 1 ine=~/TODO/. . Ипе=~/Ж$/ # Вывод строк, когда триггер равен # true end 1 Группа символов .. создает триггеры в awk-стиле, а группа символов ... создает триггеры в sed-стиле.
4.6. Операторы 145 Дать формальное описание предыдущего поведения триггера довольно трудно. Проще понять работу триггера, изучив код, который ведет себя таким же образом. Следующая функция ведет себя наподобие триггера х==3.. х==5. Левые и правые условия жестко заданы в самой функции, а для хранения состояния триггера в ней используется глобальная переменная: $state = false def flipfl opt х) # Глобальное хранилище для состояния триггера # Проверка х на соответствие условиям триггера if !$state result = (x = # Если сохраненное состояние false, = 3) # result равен значению левого операнда if result Sstate = # Если result равен true. !(х == 5) # то сохраняется состояние, противоположное end result else Sstate = # значению правого операнда # Возвращение result # В противном случае, если сохраненное !(х == 5) # состояние равно true, сохранение true end end # состояния, противоположного правому операнду # Возвращение true без тестирования левого # операнда Когда будет определена эта функция триггера, можно написать следующий код, который выводит 345, точно так же, как и более ранний пример: (1..10).each {|х| print х if flipflop(x) } Следующая функция имитирует поведение трехточечного триггера х==3... х>=3: $state2 = false def flipflop2(x) if !$state2 $state2 = (x == 3) else $state2 = !(x >= 3) true end end # Теперь испытаем ее в работе (1..10).each {|х| print х if х==3...х>=3 } # Выводит "34" (1..10).each {|х| print х if flipflop2(x) } # Выводит "34" 4.6.10. Условный оператор: ?: Оператор ?: известен как условный оператор. Это единственный тернарный (с тремя операндами) оператор в Ruby. Первый операнд размещается перед зна- ком вопроса. Второй операнд размещается между вопросительным знаком и двое- точием. И третий операнд размещается после двоеточия. Оператор ?: всегда вычисляет свой первый операнд. Если значение первого операнда отличается от false или nil, значением выражения является значение
146 Глава 4. Выражения и операторы второго операнда. В противном случае, если первый операнд имеет значение false или nil, то значением выражения становится значение третьего операнда. В лю- бом случае один из операндов никогда не вычисляется (что является существен- ным обстоятельством, если он включает операции вроде присваивания, которые имеют побочные эффекты). Приведем пример использования этого оператора: "У вас #{п) #{п==1 ? 'сообщение' : 'сообщения (сообщений)'}" Нетрудно понять, что оператор ?: работает как компактный вариант оператора if-then-else. (Имеющийся в Ruby условный оператор if рассмотрен в главе 5.) Первый операнд является проверяемым условием, таким же как выражение, рас- положенное после 1 f. Второй операнд подобен коду, который следует за ключе- вым словом then. А третий операнд подобен коду, который следует за ключевым словом el se. Разумеется, различия между оператором ?: и оператором i f заключа- ются в том, что оператор i f позволяет иметь произвольное количество кода в сво- их условиях then и else, а оператор ?: позволяет использовать только одиночные выражения. Оператор ?: имеет очень низкий уровень приоритета, значит, брать операнды в скобки обычно не требуется. Если в первом операнде используется оператор defined? или в его вторых и третьих операндах выполняется присваивание, то без скобок не обойтись. Следует помнить, что в Ruby разрешены имена методов, заканчивающиеся вопро- сительным знаком. Если первый операнд оператора ?: заканчивается идентифи- катором, вокруг этого операнда нужно ставить скобки или ставить устраняющий неоднозначность пробел между этим операндом и вопросительным знаком. Если этого не сделать, Ruby-интерпретатор решит, что вопросительный знак оператора является частью предыдущего идентификатора. Например: x==3?y:z # Это вполне допустимо 3==x?y:z # Синтаксическая ошибка: х? воспринимается как имя метода (3==x)?y:z # Все в порядке: скобки помогают решить проблему 3==х ?y:z # Пробелы также решают проблему Вопросительный знак должен располагаться на той же строке, что и первый опе- ранд. В Ruby 1.8 двоеточие должно размещаться на той же строке, что и второй операнд. А вот в Ruby 1.9 перед двоеточием допускается присутствие символа но- вой строки. Но в таком случае после двоеточия должен ставиться пробел, чтобы не получилось так, что он будет представлять литерал обозначения. В таблице 4.2 (показанной ранее в этой главе) указано, что оператор ?: имеет взаи- мосвязанность справа налево. Если в одном и том же выражении оператор исполь- зуется дважды, группируется самый правый из них: a?b:c?d:e # Это выражение... а ? b : (с ? d : е) # вычисляется следующим образом.. (а ? b : с) ? d : е # Но не так, как показано здесь Но такая неопределенность в отношении оператора ?: возникает крайне редко. В следующем выражении используются три условных оператора для вычисления
4.6. Операторы 147 максимального значения трех переменных. Скобки в них не понадобились (хотя без пробела перед вопросительным знаком не обошлось), поскольку есть только один возможный способ синтаксического разбора этого оператора: max = х>у ? x>z ? х : z : y>z ? у : z max = х>у ? (x>z ? х : z) : (y>z ? у : z) # Вариант со скобками 4.6.11. Операторы присваивания Об операторах присваивания уже шла речь в разделе 4.5. Но здесь стоит отметить несколько моментов, касающихся операторов присваивания, используемых в выра- жениях. Во-первых, значением выражения присваивания является значение (или массив значений), которое размещается в правой части оператора присваивания. Во-вторых, операторы присваивания имеют взаимосвязанность справа налево. Совмещение этих двух утверждений заставляет выражения работать именно так, как показано на примере следующего выражения: х = у = z = О # Присваивание нуля переменным х. у и z х = (у = (z = 0)) # В этом эквивалентном выражении показан порядок вычисления В-третьих, следует учесть, что у присваивания очень низкий уровень приоритета. Правила приоритетности гласят, что практически все, что следует за оператором присваивания, будет вычислено еще до того, как будет выполнено присваивание. Основными исключениями являются операторы and, or и not. И наконец, следует учесть, что хотя операторы присваивания невозможно перео- пределить как методы, в составных операторах присваивания вроде += использу- ются переопределяемые операторы наподобие +. Переопределение оператора + не оказывает влияние на присваивание, выполняемое оператором +=, но влияет на выполняемое этим оператором сложение. 4.6.12. Оператор defined? Оператор defined? относится к унарному оператору, проверяющему, определен его операнд или нет. Обычно при использовании неопределенной переменной или метода выдается исключение. Если в выражений, расположенном справа от оператора defined?, используется неопределенная переменная или метод (вклю- чая операторы, определяемые как методы), defined? возвращает nil. Точно так же defined? возвращает nil, если операнд является выражением, которое использу- ет yield или super в неприемлемом контексте (то есть когда нет блока, которому передается управление, или нет вызываемого метода надкласса). Важно понять, что выражение, которое является операндом defined?, фактически не вычисляет- ся; оно просто проверяется, чтобы выяснить, может ли оно быть вычислено без ошибки. Обычно оператор defined? применяется следующим образом: # Вычисление f(x), но только если определены и f. и х у = f(х) if defined? f(x)
148 Глава 4. Выражения и операторы Если операнд определен, оператор defined? возвращает строку. Содержимое воз- вращаемой строки обычно не играет роли; важно лишь то, что оно является ис- тинным значением — не равно ни ni 1, ни false. Но тем не менее можно проверить значение, возвращенное этим оператором, чтобы что-нибудь узнать о типе выра- жения, расположенного справа от оператора. В табл. 4.3 приводится список воз- можных значений, возвращаемых этим оператором. Таблица 4.3. Значения, возвращаемые оператором defined? Тип выражения, используемого в качестве опе- ранда Возвращаемое значение Ссылка на определенную локальную переменную "local-variable" Ссылка на определенную в блоке локальную перемен- ную (только для Ruby 1.8) ” 1 ocal-vari able(inblock)" Ссылка на определенную глобальную переменную "global-variable” Специальные глобальные переменные регулярного выражения, $&,$+,$',$' и от $1 до $9, когда они определены после успешного соответствия (только для Ruby 1.8) Имя переменной в виде строки Ссылка на определенную константу "constant" Ссылка на определенную переменную экземпляра "instance-variable" Ссылка на определенную переменную класса "class variable" (заметьте, без дефиса) Nil "ni 1 ” (заметьте, это строка) true, false "true", "false" Self "self" yield, если есть блок, которому передается управле- ние (см. также имеющийся в Kernel метод block_gi ven?) "yield" super, в контексте, допускающем использование этого ключевого слова "super” Присваивание (само присваивание не выполняется) "assignment" Вызов метода, включая использование операторов, определенных как методы (сами методы не вызы- ваются и не должны иметь правильное количество аргументов; см. также Object. respond_to?) "method" Любые другие допустимые выражения, включая лите- ралы и встроенные операторы "expression" Любые выражения, использующие неопределенные переменные или имена методов, или использующие yield или super там, где они не разрешены Nil Оператор defi ned? имеет очень низкий уровень приоритета. Если нужно прове- рить, определены или нет две переменные, то вместо && следует воспользоваться оператором and: defined? a and defined? b # Это выражение работает defined? а && defined? b # А это вычисляется как: defined?^(а && defined? b))
4.6. Операторы 149 4.6.13. Операторы-модификаторы Предложения rescue, if, unless, while и until являются предложениями условий, циклов и обработки исключений, оказывающими влияние на процесс управления Ruby-программой. Они также могут быть использованы в качестве операторов- модификаторов в коде следующего содержания: print х if х В этой модифицирующей форме их можно считать операторами, в которых зна- чение правого выражения влияет на выполнение левого выражения. (Или, в слу- чае применения модификатора rescue, состояние исключения левого выражения влияет на выполнение правого операнда.) Наверное, не нужно характеризовать эти ключевые слова как операторы. В своих обеих формах — и предложений, и модификаторов выражений — они рассмотре- ны в главе 5. Их включение в перечень таблицы 4.2 в качестве ключевых слов обу- словлено стремлением показать уровень их приоритета по отношению к другим операторам. Заметьте, что все они имеют очень низкий уровень приоритета, но оператор-модификатор rescue по сравнению с присваиванием имеет более высо- кий уровень. 4.6.14. Что не относится к операторам Большинство операторов Ruby записываются с использованием знаков пунктуа- ции. В грамматике Ruby также используется ряд пунктуационных знаков, не яв- ляющихся операторами. Поскольку мы уже видели (или еще увидим) многие из этих знаков пунктуации, не относящиеся к операторам, в тексте этой книги, да- вайте здесь их и рассмотрим: О Круглые скобки являются необязательной составляющей синтаксиса опреде- ления и вызова метода. Лучше считать вызов метода специальным видом вы- ражения, чем считать () в качестве оператора вызова метода. Круглые скобки используются также для группировки, чтобы повлиять на порядок вычисления подвыражений. [] Квадратные скобки используются в литералах массивов, а также для запро- са и установки значений массивов и хэшей. В этом контексте они служат син- таксической изюминкой для вызова метода и ведут себя отчасти похоже на переопределяемые операторы с произвольным числом операндов. Их работа рассмотрена в разделах 4.4 и 4.5.3. (} Фигурные скобки служат в блоках альтернативой do-end, а также используются в литералах хэшей. Но ни в том, ни в другом случае как операторы они не ра- ботают.
150 Глава 4. Выражения и операторы . и : : . и : используются в уточненных именах, отделяя имя метода от объекта, для которого он вызывается, или имя константы от модуля, в котором она опреде- лена. Они не являются операторами, поскольку в правой части находится не значение, а идентификатор. :, . и => Эти знаки пунктуации являются не операторами, а разделителями. Точка с за- пятой (:) используется для разделения операторов, находящихся на одной и той же строке кода; запятая (,) используется для разделения аргументов ме- тодов и элементов в литералах массивов и хэшей; а стрелка (=>) используется для разделения хэш-ключей от хэш-значений в литералах хэшей. Двоеточие используется в качестве префикса литералов обозначений, а также используется в синтаксисе хэшей в Ruby 1.9. *, & и < Эти знаки пунктуации в ряде контекстов являются операторами, но они также используются и не в качестве операторов. Знак *, помещенный перед масси- вом в выражении присваивания или вызова метода, приводит к развертыва- нию или распаковке массива в отдельные элементы. Хотя иногда его называют оператором-звездочкой, на самом деле это не оператор; *а не может быть само- стоятельным выражением. Знак & может использоваться в объявлении метода перед именем его последне- го аргумента, и в таком случае любой блок, переданный методу, будет назначен этому аргументу. (Подробности изложены в главе 6.) Он также может быть ис- пользован в вызове метода для передачи ргос-объекта, как будто этот объект является блоком. Знак < используется в определении класса, чтобы указать для класса его над- класс.
Глава 5 Инструкции и управляющие структуры
152 Глава 5. Инструкции и управляющие структуры Рассмотрим следующую Ruby-программу, которая выполняет сложение двух чи- сел, переданных ей в командной строке, и выводит получившуюся сумму: х = ARGV[0].to_f у = ARGV[l].to_f sum = х + у puts sum # Превращение первого аргумента в число # Превращение второго аргумента в число # Сложение аргументов # Вывод суммы Эта простая программа состоит главным образом из присваивания значений пере- менным и вызовов методов. Простота этой программы обуславливается строго по- следовательным выполнением. Четыре строки кода выполняются одна за другой, без каких-либо ветвлений или повторений. Редко какая программа может быть столь же просто устроена. В этой главе дается представление об управляющих структурах Ruby, которые вносят изменения в последовательное выполнение, или в процесс управления программой. Здесь мы рассмотрим: О условия; О циклы; О итераторы и блоки; О инструкции, изменяющие ход программы, наподобие return и break; О исключения; О специальные инструкции BEGIN и END; О скрытые управляющие структуры, известные как нити (fibers) и продолжения (continuations). 5.1. Условия Условия — самая распространенная управляющая структура любого языка про- граммирования. Они являются способом указания компьютеру выполнять какой- либо код в определенных ситуациях: он выполняет его только в том случае, когда соблюдены заданные требования. Условие — это выражение, если оно вычисляет- ся в любое значение, отличное от fal se или ni 1, то условие считается соблюденным. Для выражения условий Ruby обладает богатым словарным запасом. Вариан- ты синтаксиса рассмотрены в следующих подразделах. При создании Ruby-кода можно выбрать тот вариант, который представляется наиболее элегантным для решения поставленной задачи. 5.1.1. If Наиболее простым из условий считается if. В наипростейшей форме оно выгля- дит следующим образом: if выражение код end
5.1. Условия 153 Программный код между if и end выполняется только в том случае, если выраже- ние вычисляется во что-нибудь отличное от false или nil. Код должен быть отде- лен от выражения символом новой строки, или точкой с запятой, или ключевым словом then1. Вот как выглядят два способа написания одного и того же простого условия: # Если х меньше 10, увеличить его значение if х < 10 # разделитель в виде символа новой строки х += 1 end if х < 10 then х += 1 end # разделителем является ключевое слово then Можно также использовать then в качестве лексемы-разделителя и поставить сра- зу после нее символ новой строки. Это придает коду устойчивость; он будет рабо- тать, даже если впоследствии символ новой строки будет удален: if х < 10 then х += 1 end Программистам, привыкшим работать на языке Си, или на языках, чей синтаксис является производным от синтаксиса этого языка, следует учесть две важных осо- бенности, присущих Ruby-инструкции if: О ставить скобки вокруг выражения условия необязательно (как правило, они и не используются). Вместо них ограничителем условия служит символ новой строки, точка с запятой или ключевое слово then; О ключевое слово end требуется использовать, даже если код, выполняющий- ся при соблюдении условия, состоит из одной-единственной инструкции. Рассмотренная ранее инструкция i f в форме модификатора предоставляет способ написания простых условий без использования ключевого слова end. 5.1.1.1. Else Инструкция i f может включать предложение el se для определения кода, который будет выполнен, если условие не является истиной: if выражение код else код end Программный код между if и else выполняется, если выражение вычисляет- ся в что-нибудь отличное от false или nil. В противном случае (если выражение вычисляется в false или nil) выполняется код между else и end. Как и в простей- шей форме i f, выражение должно быть отделено от кода, который за ним следует, 1 В Ruby 1.8 также допускается использование двоеточия, но в версии 1.9 этот синтаксис больше не поддерживается.
154 Глава 5. Инструкции и управляющие структуры символом новой строки, точкой с запятой или ключевым словом then. Ключевые слова el se и end всецело устанавливают границы второго фрагмента кода, и допол- нительные символы новой строки или разделители уже не требуются. Условие, включающее предложение el se, выглядит следующим образом: if data # Если массив существует. data «х # то к нему добавляется значение. else # В противном случае... data = [х] # создание нового массива, содержащего это значение. end # Окончание условия. 5.1.1.2. Elsif Если в пределах условной структуры нужно проверить более одного условия, между if и else можно добавить одно или более предложений elsif. Ключевое слово elsif является сокращенной формой предложения «else if». Следует заме- тить, что ключевое слово el si f содержит лишь одну букву е. Условная структура, использующая elsif, выглядит следующим образом: if выражение! код! elsif выражение? код2 elsif выражением кодМ else код end Если выражение! вычисляется во что-нибудь отличное от fа 1 se или ni 1, то выпол- няется код1. В противном случае вычисляется выражение2. Если его значение от- личается от fа 1 se или ni 1, то выполняется код2. Этот процесс продолжается до тех пор, пока выражение вычисляется во что-нибудь отличное от false или nil или пока не будут протестированы все предложения el si f. Если выражение, связанное с последним предложением el si f, вычисляется в fal se или nil и за предложением elsif следует предложение else, то выполняется код между else и end. Если пред- ложение el se отсутствует, то вообще не выполняется никакого кода. Предложение elsif похоже на if: выражение должно быть отделено от кода сим- волом новой строки, точкой с запятой или ключевым словом then. Посмотрите на пример условия ветвления по нескольким направлениям, в котором используется elsif: if х == 1 name = "один" elsif х == 2 name = "два”
5.1. Условия 155 elsif х == 3 then name = "три" elsif x == 4; name = "четыре" else name = "много" end 5.1.1.3. Возвращаемое значение Во многих языках 1 f-условие является инструкцией. Но в Ruby все является вы- ражением, даже управляющие структуры, которые обычно называются инструк- циями. Возвращаемое значение «инструкции» 1 f (то есть значение, являющееся результатами вычисления 1 f-выражения) является значением последнего выпол- ненного в коде выражения или nil, если ни один из кодовых фрагментов не был выполнен. Тот факт, что инструкция 1 f возвращает значение, означает, к примеру, что усло- вие ветвления по многим направлениям, показанное выше, может быть переписа- но в более элегантную форму: name = if х == 1 then "один" elsif х == 2 then "два" elsif х == 3 then "три" elsif x == 4 then "четыре" else "много" end 5.1.2. Работа if в качестве модификатора Когда if используется в своей обычной форме в виде инструкции, грамматика Ruby требует, чтобы в конце ставилось ключевое слово end. Но для простого, однострочного условия это выглядит несколько нелепо. Это вопрос синтаксиче- ского анализа, и решение заключается в использовании самого ключевого слова i f в качестве разделителя выполняемого кода и условного выражения. Вместо того чтобы написать: if выражение then код end можно просто написать: код if выражение В этой форме использования if известен как инструкция (или выражение) моди- фикатор. Такой синтаксис может быть привычен для Perl-программистов, но если такая привычка не выработана, то следует учесть, что сначала следует выполняе- мый код, а затем следует выражение. Например: puts message if message # Вывод сообщения, если таковое имеет место В этом синтаксисе больше выделяется исполняемый код, а условия, при которых он выполняется, заметны в меньшей степени. Использование такого синтаксиса
156 Глава 5. Инструкции и управляющие структуры может придать коду лучшую читаемость, когда условия несложны или когда они почти всегда истинны. Несмотря на то что условие записано последним, его вычисление будет прово- диться в первую очередь. Если оно вычисляется во что-нибудь, кроме false или nil, то код выполняется, и его значение используется в качестве возвращаемого значения выражения-модификатора. В противном случае код не выполняется, и возвращаемым значением выражения-модификатора является nil. Совершенно очевидно, что этот синтаксис не допускает применения никаких разновидностей предложения el se. Для использования 1 f в качестве модификатора за ним тут же должен следовать оператор-модификатор или выражение, без какой-либо вставки, разбивающей строку. Вставка в предыдущий пример символа новой строки превращает его в вызов немодифицируемого метода, за которым следует незавершенная ин- струкция 1 f: puts message # Не ограниченный условиями метод If message # Незавершенная инструкция! Модификатор 1 f имеет очень низкий уровень приоритета и имеет более слабую привязку, чем оператор присваивания. При его использовании нужно составить четкое представление о том выражении, которое подвергается модификации. К примеру, следующие две строки кода отличаются друг от друга: у = х.invert if х.respond_to? :invert у = (х.invert if х.respond_to? :invert) В первой строке модификатор применяется к выражению присваивания. Если для х не определен метод по имени invert, то ничего и не произойдет, и значе- ние у не подвергнется изменению. Во второй строке модификатор if применя- ется только к вызову метода. Если для х не определен метод invert, то выраже- ние-модификатор будет вычислено в ni 1, и именно это значение будет присвоено переменной у. Модификатор if привязывается к одному, самому близкому выражению. Если нужно модифицировать более одного выражения, можно воспользоваться круг- лыми скобками или группирующей инструкцией begin. Но такой подход пробле- матичен, поскольку читатели не знают, что код является частью условия, пока они не доберутся до его завершения. Кроме того, при таком варианте использования модификатора 1 f теряется краткость, являющаяся основным преимуществом это- го синтаксиса. Когда задействовано более одной строки кода, обычно использует- ся не модификатор 1 f, а традиционная 1 f-структура. Сравните следующие три варианта, расположенные рядом друг с другом: if выражение begin ( строка1 строка! строка! строка2 строка2 строка2 end end if выражение ) end if выражение
5.1. Условия 157 Заметьте, что выражение, подвергающееся 1 f-модификации, может и само быть модифицируемым выражением. Поэтому к выражению можно присоединять не- сколько модификаторов 1 f: # Вывод message, если message существует и определен метод вывода puts message If message If defined? puts Но подобные повторения модификатора i f трудно читаются, и разумнее будет объединить два условия в одно выражение: puts message if message and defined? puts 5.1.3. Unless Ключевое слово unless является инструкцией или модификатором, противопо- ложным i f: при его использовании код выполняется, только если связанное с ним выражение вычисляется в false или nil. У него такой же синтаксис, как и у if, за исключением отсутствия той возможности, которую давало предложение elsif: # инструкция unless для ветвления в одном направлении unless условие код end # инструкция unless для ветвления в двух направлениях unless условие код else код end # unless модификатор код unless условие Инструкция unless так же как и инструкция if требует, чтобы условие и код были разделены символом новой строки, точкой с запятой или ключевым словом then. Так же как и if, инструкции unless являются выражениями и возвращают значе- ние выполняемого кода или nil, если ничего не выполняется: # Вызов метода to_s для объекта о, кроме тех случаев, когда о равно nil s = unless о.nil? # разделитель в виде символа новой строки o.to_s end s = unless о.nil? then o.to_s end # разделитель в виде ключевого слова then Для однострочных условий, подобных этому, понятнее будет выглядеть unless в виде модификатора: s = o.to_s unless о.nil? Для условия unless Ruby не имеет предложения, эквивалентного elsif. Тем не менее если использовать чуть больше текста, можно создать unless-инструкцию, имеющую множественное ветвление:
158 Глава 5. Инструкции и управляющие структуры unless х == О puts "х не равен О" else unless у == О puts "у не равен О" else unless z == О puts "z не равен О” else puts ''все переменные равны О” end end end 5.1.4. Case Инструкция case является условием, имеющим множественное ветвление. Суще- ствуют две формы этой инструкции. Простая (и редко используемая) форма — не более чем альтернативный синтаксис для if-elsif-else. Два расположенных ря- дом выражения являются эквивалентными: name = case name = if x == 1 then "один" when х == 1 then "один" elsif x == 2 then "два" when x == 2 then "два" elsif x == 3 then "три" when x == 3 then "три" elsif x == 4 then "четыре" when x == 4 then "четыре” else " 'много" else "много" end end Изучив этот код, можно понять, что инструкция case возвращает значение, точно так же, как это делает инструкция 1 f. Так же как и в инструкции 1 f, ключевое сло- во then, которое следует за предложениями when, может быть заменено символом новой строки или точкой с запятой1: case when х == 1 "один" when х == 2 "два" when х == 3 "три" end Инструкция case проверяет каждое из всех своих when-предложений в порядке их написания до тех пор, пока не найдет одно из них, вычисляемое в true. Если одно из таких предложений будет найдено, вычисляются инструкции, находящиеся 1 В Ruby 1.8 также допускается использование двоеточия, как и в случае с инструкцией i f, но в версии 1.9 этот синтаксис больше не поддерживается.
5.1. Условия 159 между этим when и следующим when, else или end. Последнее вычисленное пред- ложение становится возвращаемым значением инструкции case. Как только будет найдено предложение when, вычисляемое в true, никакие другие предложения when уже не рассматриваются. Предложение el se инструкции case является необязательным, но если оно при- сутствует, то должно быть размещено в конце инструкции, после всех предложе- ний when. Если ни одно из предложений when не будет вычислено в true и будет использовано предложение else, то будет выполнен код между else и end. Зна- чение последнего вычисленного в этом коде выражения становится значением инструкции case. Если ни одно из предложений when не будет вычислено в true и не будет задействовано предложение el se, тогда никакой код не будет выполнен и значение инструкции case будет равно ni 1. Предложение when в составе инструкции case может иметь более одного (с запятой в качестве разделителя) связанного с ним выражения. Если любое из этих выра- жений вычисляется в true, то выполняется код, связанный с этим предложением when. В данной простой форме case-инструкции использование запятых, которые работают так же, как и оператор 11, вряд ли может показаться удобным: case when х == 1, у == 0 then "х равен единице, или у равен нулю" # Довольно # невразумительный синтаксис when х == 2 || у == 1 then "х равен двум, или у равен единице" # А этот # воспринимается намного проще end Все до сих пор рассмотренные нами примеры использования инструкции case де- монстрируют ее наипростейшую, наименее распространенную форму. На самом деле инструкция case намного мощнее показанного выше варианта. Заметьте, что в большинстве примеров левая часть выражения каждого when-предложения одна и та же. В самой распространенной форме инструкции case мы выносим это по- вторяющееся, расположенное слева от предложения when выражение и связываем его с самим ключевым словом case: name = case х when 1 # Значение, сравниваемое с х "один" when 2 then "два" # Ключевое слово then вместо символа новой # строки when 3; "три" # Точка с запятой вместо символа новой строки else end "много" # И в конце необязательное предложение else У такой формы инструкции case связанное с case выражение вычисляется только один раз, а затем его значение сравнивается со значениями, получаемыми в ре- зультате вычислений выражений, связанных с when-предложениями. Сравнения проводятся в том порядке, в котором записаны предложения when, и выполняется код, связанный с первым совпавшим предложением when. Если совпадений не най- дено, выполняется код, связанный с предложением el se (если таковое существует).
160 Глава 5. Инструкции и управляющие структуры У этой формы инструкции case возвращаемое значение такое же, как и у простей- шей формы: это значение последнего вычисленного выражения, или nil, если не было предложения el se или совпадений со значениями предложения when. Изучая особенности инструкции case, важно понять, как происходит сравнение значений when-предложений со значением того выражения, которое следует за ключевым словом case. Это сравнение выполняется с использованием оператора ===. Этот оператор вызывается для значения выражения, связанного с when, и ему передается значение выражения, связанного с case. Поэтому приведенная ранее инструкция case эквивалентна следующей инструкции (за исключением того, что в ранее приведенном коде х вычисляется только один раз): name = case when 1 === х then "один" when 2 === x then "два" when 3 — x then "три" else "много" end Оператор === является оператором case-равенства. Для многих классов, к числу которых относится и рассмотренный ранее класс Fixnum, оператор === ведет себя точно так же, как и оператор ==. Но для некоторых классов этот оператор опре- делен весьма своеобразно. В классе Class оператор === определен таким образом, что он осуществляет проверку, является ли правый операнд экземпляром класса, указанного левым операндом. В классе Range этот оператор определен для про- верки, попадает ли значение справа в диапазон, указанный слева. В классе Regexp он определен для проверки, соответствует ли текст справа шаблону, указанному слева. В Ruby 1.9, в классе Symbol оператор === определен таким образом, чтобы проверять равенство обозначений или строк. С учетом этих определений case- равенства можно создавать довольно интересные case- инструкции: # Выполнение различных действий в зависимости от класса, к которому принадлежит х puts case х when String then "строка" when Numeric then "число" when TrueClass. FalseClass then "булево значение" else "другой тип данных" end # Вычисление подоходного налога в США на 2006 год с использованием case # и объектов Range tax = case income when 0..7550 income * 0.1 when 7550..30650 755 + (income-7550)*0.15 when 30650..74200 4220 + (income-30655)*0.25 when 74200..154B00
5.1. Условия 161 15107.5 + (income-74201)*0.28 when 154800..336550 37675.5 + (income-154800)*0.33 else 97653 + (Income-336550)*0.35 end f Получение и обработка пользовательского ввода с игнорированием комментариев # и выход, # когда пользователь введет while line=gets.chomp do слово "quit" # Цикл, запрашивающий при каждом проходе # пользовательский ввод case line when /*\s*#/ next when /Aquit$/i break else puts line.reverse end # Если ввод похож на комментарий... # переход на следующую строку. # Если введено слово "quit" (в любом регистре)... # выход из цикла. # В противном случае... # реверсирование и вывод пользовательского ввода. end Элемент when может содержать более одного связанного с ним выражения. Не- сколько выражений отделяются друг от друга запятыми, и оператор === вызыва- ется для каждого из них. То есть можно запускать один и тот же блок кода с более чем одним значением: def hasVal ue?(x) case x when nil. []. 0 false else true end # Определение метода no имени hasValue? # Условие, имеющее множественное ветвление на основе # значения х # If nil===x || []===х || ”"===х || 0===х then # возвращаемое методом значение равно false # В противном случае # метод возвращает значение true CASE В СРАВНЕНИИ СО SWITCH Программисты, работающие на Java, а также все, кто привык к языкам, имею- щим синтаксис, производный от языка Си, знакомы с инструкцией множе- ственного ветвления switch, которая похожа на Ruby-инструкцию case. Но между ними есть ряд существенных различий. • В Java и родственных ему языках инструкция носит название switch, а ее предложения называются case и default. В Ruby case используется как название инструкции, а ее предложениями служат when и else. • Имеющаяся в других языках инструкция switch просто передает управ- ление в начало соответствующего элемента case. С этой позиции программа
162 Глава 5. Инструкции и управляющие структуры продолжает выполняться и может «осуществить сквозной проход» по другим элементам case, до тех пор пока не будет достигнут конец инструкции switch или не встречена инструкция break или return. Этот сквозной проход позволяет нескольким предложениям case ссылаться на один и тот же блок кода. В Ruby для решения этой же задачи разрешено связывание с каждым предложением when нескольких разделенных запятыми выражений. Ruby-инструкция case не допускает сквозного прохода. • В Java и в большинстве других компилирующих языков, имеющих Си- подобный синтаксис, выражения, связанные с каждым case-элементом, должны на момент компиляции быть константами, а не произвольными выражениями, вычисляемыми во время выполнения программы. Зачастую это позволяет компилятору реализовать инструкцию switch с использованием очень быстрой поисковой таблицы. Для Ruby-инструкции таких ограничений не существует, и ее выполнение эквивалентно использованию инструкции if с повторяющимися предложениями elsif. 5.1.5. Оператор ?: Условный оператор ?:, рассмотренный ранее в разделе 4.6.10, очень похож в по- ведении на инструкцию 1 f, в нем символ ? заменяет then, а символ ; заменяет else. Он представляет собой компактный способ выражения условий: def how_many_messages(n) # обработка единичной и множественной формы "У вас " + n.to_s + (п==1 ? " сообщение." : " сообщения (сообщений).") end 5.2. Циклы В этом разделе дается описание имеющихся в Ruby простых инструкций органи- зации цикла: whi 1 е, unti 1 и for. В Ruby также есть возможность определения само- дельных цикличных конструкций, известных как итераторы. По всей видимости, итераторы (рассмотренные в разделе 5.3) используются в Ruby намного чаще, чем встроенные в язык инструкции организации цикла. 5.2.1. While и until В Ruby основными инструкциями организации цикла являются whi 1 е и unti 1. Они выполняют фрагмент кода пока (while) определенное условие является истин- ным или до тех пор пока (until) условие не станет истинным. Например: х = 10 # Инициализация переменной счетчика цикла while х >= 0 do # Циклическое выполнение, пока х больше или равен 0 puts х # Вывод значения х
5.2. Циклы 163 х - 1 х end х = О until х > 10 do puts х х = х + 1 end # Вычитание 1 из х # Завершение цикла # Счет до 10 с использованием цикла until # Начинаем с 0 (вместо -1) # Циклическое выполнение до тех пор. поке х не станет # больше 10 # Завершение цикла Условием цикла является булево выражение, размещенное между ключевыми словами whl 1 е или unt 11 и do. Тело цикла содержит код Ruby, размещенный между ключевыми словами do и end. Цикл while проводит вычисление своего условия. Если значение отличается от false или ni 1, он выполняет код тела цикла, а затем переходит к новому вычислению своего условия. Таким образом код тела цикла выполняется повторно от нуля и более раз, пока условие остается истинным (или, если точнее, не-fal se и не-nl 1). Цикл until имеет противоположный смысл. Условие проходит проверку и код тела выполняется, если условие вычисляется в fа 1 se или ni 1. Значит, код тела вы- полняется нуль или более раз, пока условие вычисляется в false или nil. Учтите, что любой цикл until может быть превращен в while простой инверсией условия. Большинство программистов хорошо знакомы с циклами while, но многие из них раньше никогда не пользовались циклами untl 1. Поэтому можно отдавать предпо- чтение циклам while, за исключением тех случаев, когда until существенно улуч- шает доходчивость кода. Ключевое слово do в цикле while или untl 1 похоже на клю- чевое слово then в инструкции 1 f: оно может быть полностью опущено, если между условием и телом цикла размещается символ новой строки (или точка с запятой)1. 5.2.2. While и until в качестве модификаторов Если код тела цикла представляет собой единственное Ruby-выражение, этот цикл можно выразить в очень компактной форме, используя while или untl 1 в ка- честве модификатора, следующего за выражением. Например: х = 0 # Инициализация переменной цикла puts х = х + 1 while х < 10 # Вывод и приращение в одном выражении В этом синтаксисе модификатора в качестве разделителя тела цикла от условий цикла используется само ключевое слово whl 1 е, и необходимость в ключевых сло- вах do (или символе новой строки) и end отпадает. Сравните этот код с более тра- диционным циклом while, записанном в одной строке: х = О while х < 10 do puts х = х + 1 end 1В Ruby 1.8, вместо ключевого слова do может использоваться двоеточие. В Ruby 1.9 такая замена запрещена.
164 Глава 5. Инструкции и управляющие структуры Ключевое слово until может использоваться в качестве модификатора точно так же, как и while: а = [1.2.3] # Инициализация массива puts a.pop until a.empty? # Извлечение элементов из массива, пока он не опустеет Заметьте, что при использовании while и until в качестве модификаторов они должны размещаться на той же самой строке, что и тело цикла, которое подверга- ется модификации. Если между телом цикла и ключевым словом while или until стоит символ новой строки, Ruby-интерпретатор будет считать тело цикла в каче- стве немодифицируемого выражения, a whi 1е или unti 1 рассматривать как начало обыкновенного цикла. Когда whi 1 е и unti 1 используются в качестве модификаторов для единственного Ruby-выражения, сначала проверяется условие цикла, даже если оно записано по- сле тела цикла. Тело цикла выполняется нуль или более раз, как будто оно являет- ся частью формата обычного цикла while или until. У этого правила есть одно исключение. Когда вычисляемое выражение является составным, ограниченным ключевыми словами begl п и end, перед тем как проверя- ется условие, сначала выполняется код тела: х = 10 begin # Инициализация переменной цикла # Начало составного выражения, которое выполняется как # минимум единожды puts X X = X - 1 end until x == 0 # Вывод X # Уменьшение значения х # Завершение составного выражения и его циклическая # модификация В результате получается конструкция, во многом схожая с циклом do-wh 11 е в таких языках, как Си, C++ и Java. Несмотря на схожесть с циклом do-whl 1 е, имеющимся в других языках, такое необычное поведение модификаторов цикла с использо- ванием инструкции begl п не может естественно восприниматься на интуитивном уровне, и их использование в таком виде не рекомендуется. В будущих реализациях Ruby использование модификаторов while и until с be- gl n-end может быть и вовсе запрещено. Следует заметить, что группировка нескольких инструкций с помощью круглых скобок и применение модификатора until к этой группе выражений предотвра- щает подобное необычное поведение: х = О ( puts X X = х - 1 1 until х == О # Инициализация переменной цикла # Начало составного выражения, которое может не быть # выполненным ни разу # Вывод х # Уменьшение значения х # Завершение составного выражения и его циклическая # модификация
5.2. Циклы 165 5.2.3. Цикл for-in Цикл for или цикл for-in осуществляет последовательный перебор элементов перечисляемого объекта (такого как массив). При каждой итерации значение эле- мента присваивается заданной переменной цикла, а затем выполняется код тела цикла. Цикл for выглядит следующим образом: for переменная in коллекция do тело end Переменная — это одиночная переменная или список переменных, разделенных за- пятыми. Коллекция — это любой объект, в котором есть метод-итератор each. Метод each определен для массивов и хэшей, а также для многих других Ruby-объектов. Цикл for-in вызывает метод each указанного объекта. Поскольку этот итератор вы- дает значения, цикл for присваивает каждое значение (или каждый набор значе- ний) заданной переменной (или переменным) а затем выполняет код, размещенный в теле. Так же как и в циклах while и until, ключевое слово do является необяза- тельным и может быть заменено символом новой строки или точкой с запятой. Приведем несколько примеров for-циклов: # Вывод элементов массива array = [1,2,3,4,5] for element in array puts element end f Вывод ключей и значений хэша hash = {:а=>1, :b=>2, :С=>3} for key,value in hash puts "#{key} => #{value}" end Переменная или переменные цикла for не являются локальными переменными цикла; они остаются определенными даже после выхода из цикла. Аналогично этому новые переменные, определенные внутри тела цикла, продолжают свое су- ществование после выхода из цикла. Зависимость цикла for от метода-итератора each позволяет сделать предположе- ние, что циклы for во многом похожи на итераторы. Например, показанный ранее цикл for для перечисляемых ключей и значений хэша также может быть написан с использованием итератора each в явном виде: hash = {:а=>1, :Ь=>2. :С=>3} hash.each do |key.value| puts "#{key} => #{value}” end Единственное различие между for-версией цикла и each-версией состоит в том, что блок кода, следующий за итератором, определяет новую область видимости переменной.
166 Глава 5. Инструкции и управляющие структуры 5.3. Итераторы и перечисляемые объекты Хотя циклы whl 1 е, unti 1 и for являются основной частью языка Ruby, все же, наверное, проще писать циклы, используя специальные методы, известные как итераторы. Итераторы — одно из самых замечательных свойств Ruby, и при- меры, подобные следующим, часто встречаются во вводных курсах по языку Ruby: 3.times { puts "Спасибо!" } # Троекратное выражение благодарности data.each {|х| puts х } # Вывод каждого элемента х, имеющегося в data [1,2,3].map {|х| х*х } # Вычисление квадратов значений элементов массива factorial =1 # Вычисление факториала п 2.upto(n) {|х| factorial *= х } Все эти методы — times, each, map и upto, являются итераторами, и они взаимо- действуют с блоком кода, который следует за ними. За всем этим стоит сложная структура управления — yield. Инструкция yield временно возвращает управление от метода-итератора к мето- ду, вызвавшему итератор. Точнее, поток управления направляется от итератора к блоку кода, который связан с вызовом итератора. Когда программа доходит до конца блока, метод-итератор снова получает управление и выполнение возобнов- ляется с первой инструкции, следующей за yield. Как правило, чтобы реализо- вать некую циклическую конструкцию, метод-итератор вызывает инструкцию yield несколько раз. Эта схема потока управления показана на рис. 5.1. Подробное описание блоков и инструкции yield приведено ниже, в разделе 5.4. А сейчас со- средоточимся на самой итерации, а не на управляющей структуре, позволяющей организовать ее работу. В предыдущем примере можно было заметить, что блок может быть параметризи- рован. Вертикальные линии в начале блока похожи на круглые скобки в опреде- лении метода — внутри этих линий содержатся имена параметров. Инструкция yield похожа на вызов метода; за ней следуют от нуля и более выражений, чьи значения связаны с параметрами блока. ИТЕРАТОРЫ, НЕ ПРОВОДЯЩИЕ ИТЕРАЦИЮ В этой книге мы используем понятие «итератор», чтобы обозначить любые методы, использующие инструкцию yield. Но вообще-то они не обязаны выполнять функции итерации или цикла1. Примером этому может послу- жить метод tap, определенный (в Ruby 1.9) для класса Object. Он вызывает 1 В японском Ruby-сообществе термин «итератор» вышел из употребления, поскольку он подразумевает итерацию, которая фактически не является обязательной. Более простран- ной, но и более точной является формулировка вроде «метод, который предполагает нали- чие связанного с ним блока».
5.3. Итераторы и перечисляемые объекты 167 связанный с ним блок лишь один раз, передавая ему в качестве единственного аргумента свой получатель. Затем этот блок возвращает получатель. С его по- мощью довольно удобно «вклиниться» в цепочку методов, как в следующем коде, использующем tap для вывода отладочных сообщений: chars = "hello world" .tap {|x| puts "исходный объект: #{х.inspect}"} .each_char .tap {|x| puts "each_char возвращает: #{x.inspect}"} .to_a .tap {|x| puts "to_a возвращает: #{x.inspect}"} .map {|c| c.succ } .tap {|x| puts "map возвращает: #{x.inspect}" } .sort .tap {|x| puts "sort возвращает: #{x.inspect}"} Другой довольно распространенной функцией итераторов является автома- тическое высвобождение ресурсов. К примеру, в качестве итератора может быть использован метод File.open. Он открывает указанный файл и создает представляющий его объект File. Если с этим вызовом не связано ни одного блока, он просто возвращает объект File и снимает с себя ответственность за закрытие файла с вызываемым кодом. Но если при вызове File.open есть связанный с ним блок, он передает новый объект File этому блоку, а затем автоматически закрывает файл при возврате управления из блока. Этим обе- спечивается безусловное закрытие файла, которое освобождает программиста от лишних забот. В этом случае, когда с вызовом File.open связан блок кода, возвращаемое значение метода не является объектом File, независимо от того, какое значение было возвращено блоком. Рис. 5.1. Итератор, возвращающий управление вызвавшему его методу
168 Глава 5. Инструкции и управляющие структуры 5.3.1. Числовые итераторы Ядро Ruby API предоставляет несколько стандартных итераторов. Имеющийся в классе Kernel метод loop ведет себя как бесконечный цикл, запускающий связан- ный с ним блок снова и снова, пока в блоке не будет выполнена return, break или другая инструкция, осуществляющая выход из цикла. В классе Integer определены три часто используемых итератора. Метод upto вызы- вает связанный с ним блок по одному разу для каждого целого числа, находящего- ся между тем целым числом, для которого он вызван, и тем целым числом, которое ему передано в качестве аргумента. Например: 4.upto(6) {|х] print х} # => выводит "456" Как видно из примера, upto передает каждое целое число связанному с ним бло- ку и включает сюда как начальную, так и конечную точку итерации. В общем n. upto(m) запускает свой блок ш-п+1 раз. Метод downto очень похож на upto, но проводит итерацию по нисходящей, от наи- большего числа к наименьшему. Когда метод целое_число.times вызывается для целого числа п, он вызывает свой блок п раз, передавая последующим итерациям значения от 0 до п-1. Например: 3.times {|х| print х } # => выводит "012" В общем п.times является эквивалентом 0 upto(n-l). Если нужно выполнить числовую итерацию, используя числа с плавающей точ- кой, можно воспользоваться более сложным методом step, определяемым в классе Numeric. К примеру, следующий итератор начинает работать со значения 0 и прово- дит итерации с шагом 0.1 до тех пор, пока не достигнет значения Math:: PI: 0.step(Math::PI. 0.1) {|x| puts Math.sin(x) } 5.3.2. Перечисляемые объекты Для массивов, хэшей, диапазонов (Array, Hash, Range), а также для ряда других классов определен итератор each, который передает каждый элемент коллекции связанному с ним блоку. Наверное, в Ruby это самый востребованный итератор. Ранее уже было показано, что цикл for работает только для последовательного перебора элементов тех объектов, у которых имеются методы each. Можно при- вести следующие примеры each-итераторов: [1.2.3].each {|х| print х } # => выводит "123" (1..3).each {|х| print х } # => выводит "123", то же самое, что и l.upto(3) Итератор each имеется не только в традиционных классах «структуры данных». В классе ввода-вывода Ruby —10 определяется each-итератор, который передает строки текста, считанные из объекта ввода-вывода. Используя следующий код, в Ruby можно обрабатывать строки файла:
5.3. Итераторы и перечисляемые объекты 169 Fl 1е.open(f 11 ename) do |f| # Открытие файла с указанным именем, передача его # в виде переменной f f.each {|11ne| print line } # Вывод каждой строки, имеющейся в f end # Завершение блока и закрытие файла Многие классы, в которых определен метод each, включают также и модуль Enumerable, в котором определен ряд дополнительных специализированных ите- раторов, реализованных в виде надстройки над методом each. Одним из таких по- лезных итераторов является each_w1th_1ndex, позволяющий добавлять к предыду- щему примеру нумерацию строк: File.openlfl 1 ename) do |f| f,each_w1th_1ndex do |1Ine,number| print "#{number}: #{11ne}" end end Некоторыми наиболее востребованными Enumerable-итераторами являются мето- ды с рифмующимися именами: collect, select, reject и Inject. Метод collect (из- вестный также как тар) выполняет связанный с ним блок для каждого элемента перечисляемого объекта и собирает возвращенные блоком значения в массив: squares = [1,2,3].collect {|х| х*х} # => [1,4,9] Метод sei ect вызывает связанный с ним блок для каждого элемента перечисляе- мого объекта и возвращает массив элементов, для которых блок вернул значение, отличное от fal se или ni 1. Например: evens = (1..10).select {|х| х%2 == 0} # => [2,4,6,8,10] Метод reject является простой противоположностью методу sei ect; он возвраща- ет массив элементов, для которых блок вернул ni 1 или false. Например: odds = (1. .10).reject {|х| х%2 == 0} # => [1,3,5,7,9] Метод Inject чуть сложнее всех остальных. Он вызывает связанный с ним блок с двумя аргументами. Первый аргумент — это своеобразное накопленное значе- ние, полученное от предыдущих итераций. Второй аргумент — это следующий эле- мент перечисляемого объекта. Значение, возвращенное блоком, становится первым аргументом блока для следующей итерации, или становится возвращаемым зна- чением итератора после последней итерации. Исходным значением переменной- накопителя становится либо аргумент метода Inject, если таковой существует, либо первый элемент перечисляемого объекта. (В этом случае для первых двух элементов блок вызывается только один раз.) Поясним работу Inject на приме- рах: data = [2. 5. 3, 4] sum = data.Inject {|sum, x| sum + x } floatprod = data.Inject!1.0) {|p,x| p*x } max = data.Inject {|m,x| m>x ? m : x } # => 14 (2+5+3+4) #=> 120.0 (1.0*2*5*3*4) # => 5 (наибольший элемент) Более подробно модуль Enumerabl е и его итераторы рассмотрены в разделе 9.5.1.
170 Глава 5. Инструкции и управляющие структуры 5.3.3. Создание собственных итераторов Отличительная черта метода-итератора заключается в том, что он вызывает блок кода, связанный с вызовом метода. Это делается при помощи инструкции yield. Следующий метод представляет собой простейший итератор, который всего лишь дважды вызывает свой блок: def twice yield yield end Для передачи блоку значений аргумента, за инструкцией yield нужно поместить список выражений, разделенных запятыми. Как и при вызове метода, значения аргумента могут быть взяты в необязательные скобки. Использование yield по- казано в следующем простом итераторе: # Этот метод предусматривает наличие блока. Он генерирует п значений формы # т*1 + с. для 1 из диапазона О..п-l, и передает их по очереди связанному # с методом блоку, def sequence(n, m, с) 1 = О wh11e(1 < n) # Цикл п раз yield m*1 + с # Вызов блока и передача ему значения 1 += 1 # Увеличение значения 1 при каждом проходе end end # Вызов этого метода с блоком. # Он выводит значения 1, 6 и 11 sequence(3, 5. 1) {|у| puts у } Приведем еще один пример итератора Ruby, который передает своему блоку два аргумента. Стоит заметить, что реализация этого итератора внутри себя исполь- зует другой итератор: # Генерация п точек, равномерно расположенных по окружности радиусом # г с центром (0,0). Передача х и у координат каждой точки # связанному блоку. def circled,п) n.times do |1| # Заметьте, что этот метод реализован с помощью блока angle = Math::PI * 2 * 1 / n yield r*Math.cos(angle), r*Math.s1n(angle) end end # Этот вызов итератора приведет к следующему выводу: # (1.00, 0.00) (0.00, 1.00) (-1.00, 0.00) (-0.00, -1.00) circled,4) {|х,у| printf "d.2f, X.2f) ", х, у }
5.3. Итераторы и перечисляемые объекты 171 ТЕРМИНОЛОГИЯ: ПЕРЕДАЧА И ИТЕРАТОРЫ Возможно, в силу своего программистского прошлого кто-то может посчитать термины «передача» (yield) и «итератор» несколько запутанными. Показан- ный ранее метод sequence довольно ясно дает понять, почему yield назван именно так. После вычисления каждого числа в этой последовательности (sequence) метод передает управление (и вычисленное число) блоку, позволяя ему проводить обработку числа. Но не всегда все происходит так же очевидно. Бывает такой код, в котором может показаться, что это блок передает результат назад, тому методу, который его вызвал. Метод, подобный sequence, предполагающий наличие блока и вызывающий его многократно, называется «итератором», поскольку он своим поведением похож на цикл. Это может сбить с толку тех, кто привык работать с языками, подобными Java, где итераторы являются объектами. В Java клиентский код, использующий итератор, контролирует ситуацию и «извлекает» значения из итератора по мере надобности. В Ruby ситуацию контролирует метод-итератор, который «помещает» значения в блок, который в них нуждается. Эти терминоло- гические тонкости относятся к различиям между «внутренними итераторами» и «внешними итераторами», которые еще будут рассмотрены в этом разделе. Использование ключевого слова yield действительно очень похоже на вызов ме- тода. (Исчерпывающие подробности вызова метода изложены в главе 6.) Исполь- зовать скобки вокруг аргументов необязательно. Для развертывания массива на отдельные аргументы можно использовать оператор-звездочку — *. При исполь- зовании yield допускается даже передача хэш-литерала, не взятого в фигурные скобки. Но в отличие от вызова метода, за выражением yield не может следовать блок. Передать блок другому блоку невозможно. Если метод вызван без блока, в отношении этого метода возникает ошибка пере- дачи, поскольку ее некуда осуществлять. Иногда требуется создать метод, пере- дающий данные блоку, если таковой предоставлен, но предпринимающий некие действия по умолчанию (вместо выдачи ошибки) при вызове без блока. Для этого нужно воспользоваться методом block gi ven? для определения факта существова- ния блока, связанного с этим вызовом. Метод block_gi ven? и его синоним iterator? определены в классе Kernel, поэтому они работают как глобальные функции. При- ведем следующий пример: # Возвращение массива из п элементов формы m*i+c # Если блок задан, передача каждого элемента блоку def sequence!n. m. с) i, s = 0. [] # Инициализация переменных whiled < n) # Цикл n раз у = m*i + с # Вычисление значения yield у if block_given? # Передача, если блок предоставлен s « у # Сохранение значения i += 1 end s # Возвращение массива значений end
172 Глава 5. Инструкции и управляющие структуры 5.3.4. Нумераторы Нумератор является Enumerable-объектом, предназначенным для перечисления какого-нибудь другого объекта. Для использования нумераторов в Ruby 1.8 нуж- но включить в программу строку require 'enumerator'. В Ruby 1.9 нумераторы является встроенными, и применять requi re уже не требуется. (Чуть позже мы убедимся в том, что встроенные в Ruby 1.9 нумераторы обладают более развитой функциональностью, чем та, что предлагается библиотекой enumerator, входящей в состав Ruby 1.8.) Нумераторы относятся к классу Enumerable::Enumerator. Хотя экземпляр этого класса может быть создан непосредственно, при помощи ключевого слова new, ну- мераторы обычно создаются по-другому. Вместо этого используется метод to enum или его синоним — enum for; оба они определяются в классе Object. При использо- вании без аргументов, метод to enum возвращает нумератор, чей метод each просто вызывает метод each целевого объекта. Предположим, существует некий массив и метод, предполагающий использование перечисляемого объекта. Сам объект массива передавать нежелательно, поскольку он изменяемый, и вы не доверяете методу его изменение. Вместо создания полной защищающей копии массива, вы- зовем для него метод to_enum и передадим получившийся нумератор вместо само- го массива. В действительности для вашего массива создается перечисляемый, но неизменяемый объект-заместитель (или прокси-объект): # Вызывайте этот метод с нумератором вместо изменяемого массива. # Это будет весьма полезной защитной тактикой, помогающей избежать ошибок. process(data.to_enum) # Вместо process(data) Методу to_enum можно также передать аргументы, хотя использование его сино- нима — enum for представляется в данном случае более естественным. Первый аргумент должен быть обозначением, идентифицирующим метод-итератор. Ме- тод each получившегося нумератора будет вызывать указанный метод исходного объекта. Все оставшиеся аргументы enum for будут переданы этому указанному методу. В Ruby 1.9 класс String не является перечисляемым, но в нем определены три метода-итератора: each_char, each_byte и each_line. Предположим, нам нужно воспользоваться таким относящимся к Enumerable методом, как тар, и мы хотим, чтобы он взял за основу своей работы итератор each char. Это делается путем создание нумератора: s = "hello" s.enum_for(:each_char).map {|c| c.succ } # => ["i", "f", "m", "m". "p"] В Ruby 1.9 в большинстве случаев даже не нужно использовать to enum или enum_for явным образом, как это сделано в предыдущих примерах. Все благодаря встроенным в эту версию методам-итераторам (среди которых имеются число- вые итераторы times, upto, downto и step, а также each и родственные методы класса Enumerabl е), которые автоматически возвращают нумератор, когда вызываются без блока. Поэтому чтобы передать нумератор массива, а не сам массив, можно просто вызвать метод each: process(data.each_char) # Вместо process(data)
5.3. Итераторы и перечисляемые объекты 173 Этот синтаксис становится еще более естественным, если вместо each char вос- пользоваться псевдонимом chars1. К примеру, для отображения символов строки в виде массива символов можно использовать просто .chars .тар: "hello".chars.тар {|с| с.succ } # => ["1", "f", "т". "т". ”р"] Приведем некоторые другие примеры, имеющие отношение к перечисляемым объ- ектам, возвращаемым методами-итераторами. Обратите внимание, что объекты- нумераторы могут возвращать не только методы-итераторы, определенные в клас- се Enumerate е; то же самое могут сделать и числовые итераторы, аналогичные ti mes и upto: enumerator = 3.times # объект-нумератор enumerator.each {|x| print x } # Выводит "012" # downto возвращает нумератор с методом select 10.downto(l).select {|x| хЖ2==0} # => [10.В.6.4.2] # Итератор each_byte возвращает нумератор с методом to_a "hello".each_byte.to_a # => [104, 101, ЮВ. ЮВ. Ill] Точно такое же поведение можно получить в своих собственных методах-итера- торах, возвращая self.to_enum, без предоставления блока. Приведем, к примеру, версию ранее показанного итератора twice, который возвращает нумератор, если блок ему не предоставлен: def twice if block_given? yield yield else self,to_enum(:twice) end end В Ruby 1.9 для объектов-нумераторов определен метод wi th i ndex, его нет в библи- отеке enumerator Ruby 1.8. Метод with_index просто возвращает новый нумератор, который добавляет к итератору параметр индексации. К примеру, следующее вы- ражение возвращает нумератор, на выходе которого получаются символы строки и их индексы внутри строки: enumerator = s,each_char.with_index И в заключение следует иметь в виду, что нумераторы, как в Ruby 1.8, так и в Ruby 1.9, являются объектами класса Enumerable и могут использоваться с ци- клом for. Например: for line, number in text.each_line.with_index print "#{number+l}: #{line}" end ' В выпуске Ruby 1.9.0 chars был опущен, но эта оплошность была сразу же исправлена.
174 Глава 5. Инструкции и управляющие структуры 5.3.5. Внешние итераторы Наше обсуждение нумераторов было сфокусировано на использовании их в ка- честве прокси-объектов, принадлежащих классу Enumerable. Но в Ruby 1.9 нуме- раторы нашли другое очень важное применение: они используются как внешние итераторы. Нумератор можно использовать для последовательного перебора элементов коллекции путем многократного вызова метода next. Если элементы заканчиваются, этот метод выдает исключение Stop Iteration: iterator = 9.downto(l) # Нумератор в качестве внешнего итератора begin # Значит, ниже можно использовать rescue print iterator.next while true # Повторные вызовы метода next rescue Stopiteration # Когда значений больше нет puts "...бабах!" # Ожидаемое условие, не относящееся # к исключительным end ВНЕШНИЕ ИТЕРАТОРЫ В СРАВНЕНИИ С ВНУТРЕННИМИ В своей книге, посвященной паттернам проектирования, «банда четырех» абсолютно четко определяет и различает внутренние и внешние итераторы1. • Главное — решить, какая сторона управляет итерацией — итератор или клиент, который его использует. Когда итерация управляется клиентом, итератор называется внешним, а когда она управляется итератором — внутренним. Клиенты, применяющие внешний итератор, должны провести обработку очередного элемента и запросить следующий элемент у итератора явным образом. В отличие от этого, внутреннему итератору клиент передает выполняемую операцию, а затем итератор применяет эту операцию к каждому элементу... • Внешние итераторы гибче внутренних. К примеру, провести сравнение двух коллекций с внешним итератором не составляет труда, а с внутренним итератором это сделать практически невозможно... Но с другой стороны, внутренние итераторы проще в использовании, поскольку сами определяют для вас логику итерации. В Ruby методы-итераторы наподобие each являются внутренними итератора- ми; они управляют итерацией и «помещают» значения в блок кода, связанный с вызовом метода. Нумераторы используют метод each для внешних итераций, но в Ruby 1.9 и последующих версиях нумераторы к тому же и работают как внешние итераторы — клиентский код может последовательно «извлекать» значения из нумератора с помощью метода next. 1 Гамма, Хелм, Джонсон и Влиссидес. «Приемы объектно-ориентированного проектирова- ния. Паттерны проектирования». СПб.: Питер, 2006.
5.3. Итераторы и перечисляемые объекты 175 Пользоваться внешними итераторами довольно просто: когда нужен еще один элемент, достаточно вызвать next. Когда уже не осталось ни одного элемента, next выдает исключение Stopiteration. Выдача исключения для ожидаемого условия завершения, а не для неожиданного и исключительного события может пока- заться довольно странным приемом. (Stopiteration — это потомок StandardError и IndexError; следует заметить что это, пожалуй, единственный из классов исклю- чений, у которого в имени отсутствует слово ошибка — «error».) В этой техноло- гии внешней итерации Ruby является последователем языка Python. Благодаря тому, что прекращение цикла рассматривается в качестве исключения, значитель- но упрощается логика цикла; отпадает необходимость проверять возвращаемое методом значение next на соответствие специальному значению завершения ите- рации, и перед вызовом next не требуется вызывать предикат next?. Для упрощения организации цикла с использованием внешних итераторов метод Kernel .loop включает (в Ruby 1.9) скрытое предложение rescue и осуществляет выход именно тогда, когда выдается Stopiteration. Таким образом показанный ра- нее код обратного отсчета может быть записан намного проще: iterator = 9.downto(l) loop do # Выполнение цикла, пока не будет выдано исключение Stopiteration print iterator.next # Вывод следующего элемента end puts ".. .бабах!" Многие внешние итераторы могут быть перезапущены путем вызова метода rewi nd. Но следует заметить, что rewi nd работает не во всех нумераторах. Если нумератор основан на объекте, подобном объекту File, который последовательно считыва- ет строки, вызов rewind не приведет к перезапуску итерации с самого начала. В общем, если новый вызов each по отношению к исходному Enumerabl е-объекту не приводит к перезапуску итерации с начального элемента, то вызов rewi nd тоже не приведет к перезапуску. После того как итератор запущен (то есть после первого вызова next), нумератор не может быть клонирован или продублирован. Как правило, нумератор мож- но клонировать перед вызовом next или после того как был выдан Stopiteration или вызван rewind. Обычно нумераторы с методами next создаются из Enumerable- объектов, имеющих метод each. Если для каких-то целей определяется класс, предоставляющий метод next для внешней итерации вместо метода each для вну- тренней итерации, можно просто реализовать each в виде next. На самом деле пре- вратить внешне итерируемый класс, в котором реализован метод next, в класс Enumerable также просто, как подмешать модуль (с помощью метода include, кото- рый рассматривается в разделе 7.5): module Iterable include Enumerable # Определение итераторов вдобавок к each def each # И определение each вдобавок к next loop { yield self.next } end end
176 Глава 5. Инструкции и управляющие структуры Другой способ использования внешнего итератора состоит в передаче его во вну- тренний метод-итератор: def Iterate(iterator) loop { yield iterator.next } end iterate(9.downto(D) {|x| print x } Приведенная ранее цитата из книги «Паттерны проектирования» ссылается на одно из ключевых свойств внешних итераторов: они решают проблему параллель- ной итерации. Предположим, что есть две Enumerable-коллекции и нужно итери- ровать их элементы парами: первые элементы каждой коллекции, потом вторые элементы и т. д. Без внешнего итератора пришлось бы превращать одну из коллек- ций в массив (при помощи метода to_a, определенного в Enumerable), чтобы полу- чить доступ к его элементам во время итерации другой коллекции с помощью each. В примере 5.1 показана реализация трех методов-итераторов. Все три восприни- мают произвольное количество объектов Enumerable и итерируют их различными способами. Один из них является простой последовательной итерацией, исполь- зующей только внутренние итераторы; два остальных представляют собой парал- лельные итерации и могут быть выполнены только с использованием внешних итерационных свойств нумераторов. Пример 5.1. Параллельная итерация с использованием внешних итераторов # Вызов метода each по очереди для каждой коллекции. # Это еще не параллельная итерация, и ей не требуются нумераторы. def sequence(*enumerables. &block) enumerables.each do | enumerable] enumerabl e. each( &Ы ock) end end # Итерация указанных коллекций с чередованием их элементов. # Без внешних итераторов это сделать невозможно. # Обратите внимание на использование редко применяемого предложения else # в структуре begin-rescue, def interleave(*enumerables) # Превращение перечисляемых коллекций в массив нумераторов. enumerators = enumerables.map {|e| e.to_enum } # Выполнение цикла, пока не закончатся нумераторы. until enumerators.empty? begin e = enumerators.shift # Получить первый нумератор yield e.next # Взять его значение next и передать блоку rescue Stopiteration # Если элементы закончились, оставаться # в бездействии else # Если исключения не выдано, enumerators « е # вернуть нумератор end end end
5.3. Итераторы и перечисляемые объекты 177 # Итерация указанных коллекций, передача набора значений # по одному из каждой коллекции. См. также Enumerable.zip. def bundle(*enumerables) enumerators = enumerables.map {|e| e.to_enum } loop { yield enumerators.map {|e| e.next) } end # Примеры работы этих методов-итераторов a.b.c = [1,2,3], 4..6, 'а'..'е' sequence(а,b.с) {|х| print х) interleaved,b.с) {|х| print х} bundled,Ь,с) {|х| print х) # Выводит "123456abcde" # Выводит "14a25b36cde" # '[1, 4, "а"][2, 5, "Ь"][3, 6, "с"] Метод bundle из примера 5,1 аналогичен методу Enumerable.zip. В Ruby 1.8 метод zip должен сначала превратить свои Enumerable-аргументы в массивы, а затем ис- пользовать эти массивы в процессе проведения итерации в отношении перечис- ляемого объекта, для которого эта итерация была вызвана. Но в Ruby 1.9 метод zi р может использовать внешние итераторы. Это позволяет (как правило) поднять эффективность использования времени и пространства памяти, а также дает воз- можность работать с безразмерными коллекциями, которые не могут быть преоб- разованы в массивы конечного размера. 5.3.6. Итерация и параллельное изменение Как правило, итерация в основной имеющейся в Ruby коллекции классов осу- ществляется по «живым» объектам, а не по закрытым копиям или «моменталь- ным снимкам» этих объектов, при этом не предпринимается никаких попыток обнаружить или предотвратить параллельное изменение коллекции в процессе ее итерации. К примеру, если в отношении массива вызывается метод each и блок, связанный с эти вызовом, вызывает метод shi ft для этого же массива, результаты итерации могут быть непредсказуемыми: а = [1.2,3.4,5] a.each {|х| puts "#{х},#{а.shift}" } # Выводит "1,1\п3,2\п5,3" Такое же непредсказуемое поведение можно заметить, если один поток вно- сит в коллекцию изменения, а второй в это же самое время выполняет итерацию коллекции. В качестве одного из способов избежать подобного развития событий можно перед выполнением итерации создать защитную копию коллекции. К при- меру, следующий код добавляет к модулю Enumerable метод each_in_s naps hot: module Enumerable def each_in_snapshot &block snapshot = self.dup # Создание закрытой копии объекта Enumerable snapshot.each &block # и выполнение итерации копии end end
178 Глава 5. Инструкции и управляющие структуры 5.4. Блоки Использование блоков является основой использования итераторов. В предыду- щем разделе главное внимание уделялось итераторам как разновидности кон- струкции, предназначенной для организации цикла. При обсуждении этого вопро- са подразумевалось также и использование блоков, но они не являлись предметом рассмотрения. Теперь же мы обратим внимание именно на блоки. В последующих подразделах объясняются: О синтаксис, используемый для связи блока с вызовом метода; О «возвращаемое значение» блока; О область видимости переменных, находящихся внутри блока; О разница между параметрами блока и параметрами метода. 5.4.1. Синтаксис блока Блоки не могут использоваться автономно; они могут быть лишь допустимым продолжением вызова метода. При этом блоки можно размещать после вызова ме- тода; если метод не является итератором и никогда не вызывает блок при помощи ключевого слова yield, блок будет молчаливо проигнорирован. Границами блоков служат фигурные скобки или ключевые слова do и end. Открывающая фигурная скобка или ключевое слово do должны быть на той же самой строке, что и вызов метода, иначе Ruby посчитает окончание строки за конец инструкции и вызовет метод без блока: # Вывод чисел от 1 до 10 l.upto(lO) {|х| puts х } l.upto(lO) do |х| puts X end l.upto(lO) {|x| puts x } # Вызов метода и блок в фигурных скобках на одной # и той же строке # Границами блока служат ключевые слова do-end # Блок не задан # Синтаксическая ошибка: блок не следует за # вызовом По общепринятому соглашению фигурные скобки используются, когда блок по- мещается на одной строке, а ключевые слова do и end используется, когда блок занимает несколько строк. Но этим соглашение не исчерпывается; Ruby-napcep привязывает { строго к той лексеме, которая предшествует этому символу. Если не заключить аргументы метода в круглые скобки и использовать в качестве огра- ничителя блока фигурную скобку, то блок будет связан с последним аргументом метода, а не с самим методом, что может не соответствовать задуманному. Чтобы избежать подобной неприятности, нужно заключать аргументы в круглые скобки или ограничивать блок ключевыми словами do и end:
5.4. Блоки 179 1.upto(3) {|х| puts x } l.upto 3 do |x| puts x end l.upto 3 {|x| puts x } # Задействованы круглые и фигурные скобки # Круглые скобки не используются, блок ограничен # с помощью do-end # Синтаксическая ошибка: попытка передать блок # числу 3! Так же как и методы, блоки могут иметь параметры, которые отделяются друг от друга запятыми и ограничиваются символами вертикальной черты (|), а в осталь- ном они очень похожи на параметры методов: # Итератор Hash.each передает своему блоку два аргумента hash.each do |key, value) # Для каждой имеющейся в хэше пары ключ-значение (key,value) puts "#{key): #{value}" # Вывод ключа и значения end # Завершение блока По общепринятому соглашению параметры блока записываются на той же стро- ке, на которой расположен вызов метода и открывающая фигурная скобка или ключевое слово do, но с точки зрения синтаксиса придерживаться этого правила необязательно. 5.4.2. Значение блока Во всех ранее показанных в этой главе примерах использования итераторов метод-итератор передавал значения в связанный с ним блок, но при этом игно- рировал значения, возвращаемые блоком. Но так бывает не всегда. Рассмотрим метод Array, sort. Если с вызовом этого метода связать какой-нибудь блок, то это- му блоку будет передаваться пара элементов, а задачей блока будет их сортиров- ка. Возвращаемое блоком значение (-1, 0 или 1) показывает порядок следования этих двух аргументов. Относящееся к блоку «возвращаемое значение» доступно методу-итератору в виде значения инструкции yield. «Возвращаемое блоком значение» — это просто значение последнего вычисленно- го в блоке выражения. Поэтому для сортировки массива из слов от самого длинно- го к самому короткому, можно написать следующий код: # Блок воспринимает два слова и "возвращает" их относительный порядок words.sort! {|х.у| у <=> х } Фраза «возвращаемое значение» взята нами в кавычки совсем неспроста: исполь- зуемое обычно для возвращения из блока ключевое слово return здесь не подой- дет. Появление return внутри блока приводит к возвращению из содержащегося в блоке метода (а не из метода-итератора, который передал управление блоку). Разумеется, бывают моменты, когда именно этого и нужно добиться. Но если нуж- но вернуться из блока к методу, вызвавшему yi el d, то пользоваться return не стоит. Если требуется заставить блок вернуть управление вызвавшему его методу еще до того, как будет вычислено последнее выражение, или если нужно вернуть бо- лее одного значения, то вместо return следует воспользоваться инструкцией next, (return, next и родственная им инструкция break будут подробно рассмотрены
180 Глава 5. Инструкции и управляющие структуры в разделе 5.5.) Приведем пример, в котором для возвращения из блока использу- ется инструкция next: array.collect do |x| next 0 if x == nil # Преждевременное возвращение, если значение х # равно nil next х, х*х # Возвращение двух значений end Следует заметить, что такой прием использования инструкции next не нашел ши- рокого применения, и этот код можно переписать и без его использования: array.collect do |x| if x == nil 0 else [x. x*x] end end 5.4.3. Блоки и область видимости переменных Блоки определяют новую область видимости переменных: переменные, созданные внутри блока, существуют только в пределах этого блока и за его пределами не определены. Но нужно иметь в виду, что локальные переменные метода доступны любому блоку внутри этого метода. Поэтому если в блоке присваивается значе- ние переменной, которая уже была определена за его пределами, новая, локальная переменная блока не создается, вместо этого новое значение присваивается той переменной, которая уже существовала. Иногда именно этого и следует добиться: total = О data.each {|х| total += х } # Подсчет суммы элементов массива data puts total # Вывод суммы Но иногда изменять значение переменных в охватывающей области видимости совсем не нужно, и это происходит непреднамеренно. Особое беспокойство эта проблема вызывает, когда дело касается параметров блока в Ruby 1.8. В этой вер- сии Ruby если параметр блока имеет имя, совпадающее с именем существующей переменной, то вызов блока приводит к присваиванию значения существующей переменной, а не к созданию новой локальной переменной блока. К примеру, в сле- дующем коде возникает проблема, связанная с использованием одного и того же идентификатора i в качестве параметра блока в двух вложенных блоках: l.upto(lO) do |i| l.upto(lO) do |1| print "#{i} " end print " ==> Строка #{i}\n" # 10 строк # no 10 столбцов каждая # Вывод номера столбца # Попытка вывести номер строки, но получается # номер столбца end
5.4. Блоки 181 В Ruby 1.9 дело обстоит иначе: параметры блока всегда являются локальными по отношению к своему блоку и вызов блока не приводит к присваиванию значений уже существующим переменным. Если Ruby 1.9 был вызван с ключом -w, то он выдаст предупреждение, если параметр блока имеет такое же имя, что и у суще- ствующей переменной. Это поможет избежать написания кода, который в верси- ях 1.8 и 1.9 будет работать по-разному. Ruby 1.9 отличается еще одним важным аспектом. Синтаксис блока был расширен, чтобы дать возможность объявлять локальные переменные блока, локальность которых гарантируется, даже если переменная с таким именем уже существует в охватывающей блок области видимости. Для этого нужно продолжить список параметров блока точкой с запятой и списком локальных переменных блока с за- пятой в качестве разделителя их имен. Приведем пример: х = у = О l.upto(4) do |х;у| У = X + 1 puts у*у end [х.у] # Локальные переменные # х и у - локальные переменные блока # х и у "затенены" от внешних переменных # Использование у в качестве рабочей переменной # Вывод 4. 9. 16. 25 # => [0.0]: блок не изменил значений этих переменных В этом коде переменная х является параметром блока: она получает значение, ког- да блок вызывается инструкцией yield. В свою очередь у — это локальная пере- менная блока. Она не получает никакого значения от вызова, осуществляемого с помощью yield, но у нее есть значение nil, пока блок не присвоит ей какое- нибудь другое значение. Цель объявления этой локальной переменной блока — дать гарантию, что полезное значение какой-нибудь существующей переменной не будет непреднамеренно затерто. (Такое может, к примеру, произойти, когда блок копируется из одного метода и вставляется в другой.) Если Ruby 1.9 вызыва- ется с ключом -w, то при затенении локальной переменной блока от той перемен- ной, которая ранее уже существовала, будет получено предупреждение. Разумеется, у блоков может быть более одного параметра и более одной локаль- ной переменной. Приведем пример блока с двумя параметрами и тремя локальны- ми переменными: hash.each {|key,value: i.j.k| } 5.4.4. Передача аргументов блоку Как уже было отмечено, параметры блока очень похожи на аргументы метода. Но это не одно и то же. Значения аргументов, следующих за ключевым словом yield, присваиваются параметрам блока, следуя правилам, которые ближе к правилам присваивания значения переменной, чем к правилам вызова метода. Поэтому ког- да итератор выполняет yield k, v, чтобы вызвать блок, объявленный с параметра- ми | key, vа 1 ue |, то эквивалентом этому процессу является следующая инструкция присваивания: key, value = k.v
182 Глава 5. Инструкции и управляющие структуры Итератор Hash.each_pai г передает пару ключ-значение следующим образом1: {:опе=>1}.each_pa1r {|key,value| ... } # key=:one, value=l To, что вызов блока использует присваивание значения переменной, особенно от- четливо видно в Ruby 1.8. Следует напомнить, что в Ruby 1.8 параметры являются локальными по отношению к блоку только в том случае, если они не использова- лись до этого в качестве локальных переменных охватывающего этот блок метода. Если они уже были локальными переменными, то им просто присваиваются зна- чения. В Ruby 1.8 в качестве параметров блока могут использоваться фактически любые переменные, в том числе глобальные и переменные экземпляра: {:опе=>1}.each_pa1г {|$key, @value| ... } # В Ruby 1.9 такой код больше не # работает В этом итераторе значение глобальной переменной $кеу устанавливается в :опе, а значение переменной экземпляра Oval ие — в 1. Как отмечалось ранее, в Ruby 1.9 параметры блока становятся локальными по отношению к блоку. А это также означает, что параметры блока не могут больше быть глобальными переменными или переменными экземпляра. Итератор Hash.each передает пары ключ-значение в виде двух элементов одного массива. Но довольно часто встречается код, подобный следующему: hash.each {|k,v| ... } # Ключ и значение присваиваются # параметрам к и v Этот код также работает благодаря параллельному присваиванию. Передаваемое значение в виде двухэлементного массива присваивается переменным к и v: k.v = [key. value] В соответствии с правилами параллельного присваивания (изложенными в разде- ле 4.5.5), расположенный справа одиночный массив развертывается, а его элемен- ты присваиваются нескольким переменным, стоящим слева от оператора. Но назвать работу вызова блока в точности соответствующей правилам парал- лельного присваивания нельзя. Представьте себе итератор, передающий блоку два значения. По правилам параллельного присваивания можно надеяться на воз- можность объявления блока с одним параметром и получения двух значений, ав- томатически заполняющих созданный для нас массив. Но все это работает иначе: def two: yield 1,2; end two {|x| p x } two {|x| p x } two {|*x| p x } two {|x.| p x } # Итератор, передающий два значения # Ruby 1.8: выдает предупреждение и выводит [1,2], # Ruby 1.9; выводит 1, и не выдает никакого # предупреждения # В обеих версиях: выводится [1.2]; без предупреждения # В обеих версиях: выводится 1: без предупреждений 1 В Ruby 1.8 each_pa1r передает блоку два отдельных значения. В Ruby 1.9 итератор each_ pal г является синонимом для each и передает единственный аргумент в виде массива, о чем вскоре будет рассказано. Но показанный здесь код исправно работает в обеих версиях.
5.4. Блоки 183 В Ruby 1.8 когда у блока один параметр, то несколько аргументов пакуются в мас- сив, но такой прием не приветствуется, в силу чего и выдается предупреждение. В Ruby 1.9 первое переданное значение присваивается параметру блока, а второе значение молчаливо отбрасывается. Если нужно, чтобы несколько переданных зна- чений были упакованы в массив и присвоены единственному параметру блока, то об этом нужно заявить явным образом, установив в качестве префикса параметра символ звездочки — *, точно так же, как это делается при объявлении метода. (Объ- явление метода и его параметры подробно рассматриваются в главе 6.) Заметьте также, что второе переданное значение можно отбросить и явным образом, объявив параметр блока списком, заканчивающимся запятой, как бы говоря при этом: «Есть еще один параметр, но он не нужен, и я даже не удосужился подобрать ему имя». Хотя в данном случае поведение, демонстрируемое вызовом блока, не похоже на параллельное присваивание, но оно также не похоже и на вызов метода. Если объявить метод с одним аргументом, а затем передать ему два аргумента, то Ruby даже не станет выводить предупреждение, а сразу же выдаст ошибку. В Ruby 1.8 префикс * может иметь только последний параметр блока. В Ruby 1.9 это ограничение снято, и любой параметр блока независимо от его позиции в спи- ске может иметь префикс *: def five: yield 1.2,3.4.5: end five do |head, *body, tail | body print head, body, tail end # Передача пяти значений # Все лишние значения попадают в массив # Выводится "1[2,3,4]5" Инструкция yield, точно так же как и при вызове методов (который рассматри- вается в разделе 6.4.4), допускает использование в качестве последнего значения аргумента «обнаженных» хэшей. То есть если последний передаваемый yi el d аргу- мент является литералом хэша, то фигурные скобки можно опустить. Поскольку для итераторов передача хэшей нехарактерна, нам пришлось специально выду- мать пример, иллюстрирующий это положение: def hashiter: yield :а=>1, :b=>2: end # Заметьте, фигурные скобки отсутствуют hashiter {|hash| puts hash[:a] } # Выводит 1 В Ruby 1.9 завершающий параметр блока может иметь префикс &, указывающий на то, что он должен получить какой-то блок, связанный с вызовом данного блока. Но следует вспомнить, что вызов yield может не иметь связанного с ним блока. В главе 6 мы узнаем, что блок может быть превращен в Ргос-объект и блоки могут быть связаны с Ргос-вызовами. Чтобы лучше разобраться в следующем примере кода, нужно прочитать главу 6: # Этот Ргос-объект предполагает наличие блока printer = lambda {|&b| puts b.call } # Вывод значения, возвращенного b printer.call { "hi" } # Передача блока другому блоку! Важное отличие параметров блока от аргументов метода состоит в том, что па- раметры блока не допускают наличия присвоенных значений по умолчанию,
184 Глава 5. Инструкции и управляющие структуры которые вполне допустимы для аргументов метода. Поэтому следующий код не имеет права на существование: [1,2,3].each {|х,у=10| print х*у } # SyntaxError! В Ruby 1.9 для создания Ргос-объектов определяется новый синтаксис, позволяю- щий аргументам иметь значения по умолчанию. Подробности отложим до пере- хода к изучению Ргос-объектов в главе 6, но этот код может быть переписан сле- дующим образом: [1,2,3].each &->(х,у=10) { print х*у } # Выводит "102030" 5.5. Изменение хода работы программы Кроме условий, циклов и итераторов Ruby поддерживает ряд инструкций, изме- няющих ход Ruby-программ. К ним относятся: return Приводит к выходу из метода и возвращает значение той инструкции, которая его вызвала. break Приводит к выходу из цикла (или итератора). next Заставляет цикл (или итератор) пропустить оставшуюся часть текущей итера- ции и перейти к следующей итерации. redo Перезапускает цикл или итерацию с самого начала. retry Перезапускает итератор, заново вычисляя все выражение. Далее в этой главе мы увидим, что ключевое слово retry может также использоваться при обра- ботке исключений. Throw-catch Весьма универсальная управляющая структура, которая по своему названию и характеру работы похожа на механизм выдачи и обработки исключений. Ключевые слова throw и catch не относятся к основному механизму исключе- ний Ruby (он реализован с помощью ключевых слов raise и rescue, которые бу- дут рассмотрены далее). Они используются как своеобразная многоуровневая или помеченная инструкция break. Все эти инструкции подробно рассмотрены в следующих подразделах. 5.5.1. Return Инструкция return заставляет метод, в котором она находится, вернуть управ- ление вызвавшему его коду программы. Возможно все, кто знаком с Си, Java
5.5. Изменение хода работы программы 185 или родственными им языками, понимают предназначение инструкции return на интуитивном уровне. Но пропускать этот раздел все же не стоит, поскольку поведение return внутри блока может и не укладываться в ваши интуитивные понятия. За инструкцией return может также следовать выражение или список выражений, разделенных запятыми. Если выражение не указывается, возвращаемое методом значение равно nil. Если за инструкцией следует одно выражение, то оно стано- вится значением, которое возвращается методом. Если за ключевым словом return следует более одного выражения, то возвращаемым методом значением становит- ся массив, содержащий значения этих выражений. Следует заметить, что большинству методов инструкция return не требуется. Ког- да ход программы достигает окончания метода, он возвращает управление вы- звавшей его инструкции. В таком случае возвращается значение последнего вы- ражения метода. Большинство Ruby-программистов не используют return, когда в ней нет необходимости. Вместо того чтобы в последней строчке метода написать return х, они просто пишут х. Инструкция return пригодится, когда нужно будет вернуться из метода преждев- ременно или вернуть из него более одного значения. Например: # Возвращение двух копий х. если х не равен nil def double(x) return nil if x == nil # Преждевременное возвращение return x, x.dup # Возвращение нескольких значений end При первом знакомстве с блоками Ruby вполне естественно представить их как некую разновидность вложенной функции или мини-метода. И если их представ- лять себе именно таким образом, то можно предположить, что return просто пере- дает управления итератору, вызвавшему блок. Но блоки не являются методами, поэтому ключевое слово return работает иначе. В действительности return ведет себя на удивление логично; оно всегда приводит к выходу из того метода, в кото- рый включен блок, независимо от того, насколько глубоко эта инструкция вложе- на внутри блоков1. Следует учесть, что метод, охватывающий блок, — это не тот метод, который его вызывает. Когда инструкция return применяется в блоке, она не является причи- ной только лишь для возвращения из блока. Она также не является причиной только лишь для возвращения из вызвавшего блок итератора. Инструкция return всегда вызывает возвращение из охватывающего метода. Этот охватывающий ме- тод, называемый также лексически охватывающим методом, является тем самым методом, при просмотре исходного кода которого виден размещенный в нем блок. На рис. 5.2. показано поведение инструкции return, расположенной в блоке. 1 При рассмотрении лямбда-функций в разделе 6.5.5.1 мы увидим, что из этого правила есть и исключение. В существующей разновидности функции, созданной из блока, — лямбде — поведение инструкции return внутри нее отличается от ее поведения в обыч- ном блоке.
Глава 5. Инструкции и управляющие структуры Рис. 5.2. Работа инструкции return в блоке ующем коде определяется метод, использующий return для возвращения [ения, осуществляемого непосредственно из блока: едение индекса первого появления элемента target внутри массива array юзвращение nil е. что этот код всего лишь дублирует метод Array.index id(array. target) •ay.each_with_index do |element.!ndex| return index if (element == target) # возвращение из метода find I # Если элемент не найден. # возвращение значения nil укция return, имеющаяся в этом коде, не вызывает возвращение управления ка в вызвавший его итератор. Она также не вызывает возвращение управ- из итератора each with index в вызвавший его метод. А вот метод find она тяет вернуть управление туда, откуда он был вызван. . Break :пользовании внутри цикла инструкция break передает управление за пре- икла первому же следующему за циклом выражению. Читатели, знакомые ava или с подобными им языками, уже знают, как использовать break в ци-
5.5. Изменение хода работы программы 187 whiled ine = gets.chop) break if line == "quit” puts eval(line) end puts "До свидания” # Начало цикла # Если выполняется эта инструкция break... # ...то управление передается сюда При использовании внутри блока инструкция break передает управление за пре- делы блока, за пределы итератора, вызвавшего этот блок, и оно попадает к перво- му выражению, следующему за вызовом итератора. Например: f.each do |line| # Итерация по строкам файла f break if line == "quit\n" # Если выполняется эта инструкция break... puts eval(line) end puts "До свидания" # ...то управление передается сюда Как видно из примеров, использование break внутри блока с точки зрения лек- сики ничем не отличается от использования этой инструкции внутри цикла. Но если рассмотреть стек вызовов, то работа инструкции break в блоке организова- на несколько сложнее, поскольку она заставляет вернуть управление из метода- итератора, с которым связан блок. Этот процесс показан на рис. 5.3. Рис. 5.3. Работа инструкции break в блоке Следует заметить, что в отличие от return, инструкция break никогда не приво- дит в возвращению из лексически охватывающего метода. Инструкция break может появляться лишь внутри лексически охватывающего цикла или внутри блока. Ее использование в другом контексте приводит к возникновению ошибки LocalJumpError.
188 Глава 5. Инструкции и управляющие структуры 5.5.2.1. Break со значением Вспомним, что в Ruby все синтаксические конструкции являются выражениями и у всех них могут быть значения. В инструкции break может быть определено значение для прерываемого цикла или итерации. За ключевым словом break мо- жет следовать выражение или список выражений, разделенных запятыми. Если break используется без выражения, то значение выражения цикла или возвращае- мое значение метода-итератора равно nil. Если в инструкции break используется одно выражение, то значение этого выражения становится значением выражения цикла или возвращаемым значением итератора. А если в инструкции break ис- пользуется несколько выражений, то значения этих выражений помещаются в массив и этот массив становится значением выражения цикла или возвращаемым значением итератора. Для сравнения цикл while, завершающийся обычным образом без использования break, всегда имеет значение nil. Возвращаемое значение итератора, нормально завершающего свою работу, определяется методом-итератором. Множество ите- раторов, таких как times и each, просто возвращают объект, для которого они были вызваны. 5.5.3. Next Инструкция next заставляет цикл или итератор завершить текущую итерацию и приступить к следующей. Программистам, работающим на языках Си и Java, эта структура управления известна под именем continue. А вот как next работает в цикле: whileCline = gets.chop) # Начало цикла next if 1ine[0,l] == "#" # Если строка является комментарием, переход # к следующей строке puts eval(line) # При выполнении инструкция next, управление передается сюда end Когда инструкция next используется внутри блока, она приводит к немедленному выходу из блока и возвращению управления методу-итератору, который затем мо- жет начать новую итерацию путем повторного вызова блока: f.each do |1ine| # Итерация по строкам файла f next if line[0,l] == "#" # Если строка является комментарием, переход # к следующей строке puts eval(line) # При выполнении инструкция next, управление передается сюда end Использование инструкции next в блоке лексически аналогично ее использова- нию в цикле whi 1 е, until или for-in. Но при рассмотрении последовательности вы- зовов открывается более сложная картина, показанная на рис. 5.4.
5.5. Изменение хода работы программы 189 Рис. 5.4. Работа инструкции next в блоке NEXT, BREAK И RETURN Полезно будет сопоставить рис. 5.4 с рис. 5.2 и 5.3. Инструкция next приводит к тому, что блок передает управление тому методу-итератору, который его вызвал. Инструкция break приводит к тому, что блок передает управление своему итератору, а итератор передает управление охватывающему методу. А инструкция return приводит к тому, что блок передает управление итератору, итератор передает управление охватывающему методу, а охватывающий метод передает управление тому выражению, из которого он был вызван. Инструкция next может использоваться только внутри цикла или блока, а когда она используется в другом контексте, возникает ошибка Local JumpError. 5.5.З.1. Next и значение блока Ключевое слово next по аналогии с ключевыми словами return и break может ис- пользоваться само по себе или со следующим за ним выражением, или списком выражений с запятой в качестве разделителя. Когда инструкция next использует- ся в цикле, любые следующие за ней значения игнорируются. Но в блоке выраже- ние или список выражений становятся «возвращаемым значением» инструкции yield, вызвавшей блок. Если за next не следует никаких выражений, то значением yield становится nil. Если за next следует одно выражение, то значение этого вы- ражения становится значением yi el d. А если за next следует список выражений, то значением yield становится массив значений этих выражений. При рассмотрении инструкции return мы постарались объяснить, что блоки не являются функциями и что инструкция return не заставляет блок возвращать
190 Глава 5. Инструкции и управляющие структуры управление вызвавшему его итератору. А сейчас вы можете убедиться, что именно это делается с помощью инструкции next. А вот как выглядит код, в котором ин- струкция может использоваться для этой цели: squareroots = data.collect do |x| next 0 if x < О # Возвращение 0 для отрицательных значений Math.sqrt(x) end Обычно значением инструкции yield становится значение последнего имеющего- ся в блоке выражения. Как и в случае с использованием инструкции return, часто бывает так, что для определения этого значения вовсе не обязательно использо- вать инструкцию next. Предыдущий код, к примеру, может быть написан следую- щим образом: squareroots = data.collect do |x| if (x < 0) then 0 else Math.sqrt(x) end end 5.5.4. Redo Инструкция redo перезапускает текущую итерацию цикла или итератора. Но эта инструкция не является аналогом инструкции next. Инструкция next передает управление в конец цикла или блока, чтобы могла начаться следующая итерация, a redo передает управление назад, к началу цикла или блока, чтобы итерация могла начаться заново. Для тех, кто взялся за Ruby после работы на Си-подобных язы- ках, redo, возможно, станет новым элементом структуры управления. Инструкция redo передает управление первому выражению в теле цикла или бло- ка. При этом перепроверки условий цикла не происходит и следующий элемент из итератора не извлекается. Следующий цикл while в обычных условиях завер- шается после трех итераций, но инструкция redo заставляет его провести четыре итерации: i = О whiled < 3) # Выводит ”0123” вместо "012" # При выполнении redo управление возвращается сюда print i i += 1 redo if 1 == 3 end Инструкция redo не нашла широкого применения, поэтому многие примеры вро- де этого приходится придумывать специально. Но одно из реальных применений связано с исправлением ошибок в процессе приглашения пользователя к вводу информации. В следующем коде redo используется в блоке именно с этой целью: puts "Введите, пожалуйста, первое задуманное слово” words = Mappie banana cherry) # краткая форма для ["apple", # "banana”, "cherry"]
5.5. Изменение хода работы программы 191 response = words.collect do |word| # При выполнении redo управление print word + "> " response = gets.chop if response.size == 0 word.upcase! redo end response передается сюда # Приглашение пользователя к вводу # Получение ответа # Если пользователь ничего не ввел # Выделение приглашения заглавными # буквами # и переход к началу блока # Возврат ответа 5.5.5. Retry Инструкция retry обычно используется в предложении rescue для повторного вы- полнения блока кода, выдавшего исключение, как это изложено в разделе 5.6.3.5. Но в Ruby 1.8 инструкция retry находит и другое применение: она перезапускает итерацию, основанную на работе итератора (или любой вызов метода) с самого начала. В таком качестве инструкция retry используется крайне редко, поэтому в Ruby 1.9 она была удалена из языка. По этой же причине это свойство языка в новом коде использовать не рекомендуется. В блоке инструкция retry приводит не просто к повторному выполнению блока, а к выходу из блока и метода-итератора, а затем к новому вычислению выражения итератора и перезапуску итерации. Рассмотрим следующий код: п = 10 п.times do |х| print х if х == 9 n -= 1 retry end end # Итерация n раз от 0 до n-1 # Вывод номера итерации # Если мы дошли до 9 # Уменьшение значения п (мы не хотим в следующий # раз добраться до 9!) # Перезапуск итерации В этом коде retry используется для перезапуска итератора, но в нем есть меры предохранения от входа в бесконечный цикл. При первом вызове код выводит числа 0123456789, а затем он производит перезапуск. При втором вызове код выво- дит числа 012345678 и не производит перезапуска. Вся прелесть инструкции retry состоит в том, что она не вызывает каждый раз повторный запуск итератора тем же самым образом. Она приводит к полному перевычислению выражения итератора, а значит, аргументы итератора (и даже объект, для которого он вызван) могут быть разными при каждом повторном запуске итератора. Без привычки к работе с такими высокодинамичными язы- ками, как Ruby, подобное перевычисление может показаться трудным для вос- приятия.
192 Глава 5. Инструкции и управляющие структуры Использование инструкции retry не ограничено одними блоками; ее применение всегда приводит к перевычислению вызова ближайшего охватывающего метода. Это означает, что она может быть использована (в версиях до Ruby 1.9) для напи- сания итераторов, подобных следующему, который работает как цикл whi 1 е: # Этот метод работает наподобие цикла while: если х не равен nil и не равен false, # вызывается блок, а затем предпринимается попытка перезапуска цикла и новой # проверки условия. # Этот метод слегка отличается от настоящего цикла while: # в качестве ограничителей тела цикла можно использовать фигурные скобки в стиле # языка Си. А переменные, # используемые только внутри тела цикла, остаются локальными для блока. def repeat_while(x) if х # Если условие не было равно nil или false yield # Запуск тела цикла retry # Выполнение retry и перевычисление условия цикла end end 5.5.6. Throw и catch Инструкции throw и catch являются методами класса Kernel, определяющими управляющую структуру, которую можно себе представить как многоуровневую инструкцию break. Инструкция throw не просто передает управление из текущего цикла или блока, но фактически может передать его на любое число уровней, ста- новясь причиной выхода из блока, при определении которого использована ин- струкция catch. В отношении этой инструкции не требуется даже, чтобы она раз- мещалась в том же методе, что и инструкция throw. Она может быть в вызывающем методе или даже где-нибудь еще дальше по стеку вызовов. В языках, подобных Java и JavaScript, циклы могут быть поименованы или по- мечены произвольным префиксом. Когда это сделано, управляющая структура, известная как «помеченное прерывание» («помеченный break»), приводит к вы- ходу из поименованного цикла. В Ruby метод catch определяет помеченный блок кода, а метод throw приводит к выходу из этого блока. Но throw и catch намного универсальнее помеченного break. К примеру, эта пара может быть использована с любой разновидностью инструкций и ее применение не ограничено циклами. Подходя к объяснению более строго, можно сказать, что throw может распростра- няться вверх по стеку вызовов, чтобы привести к выходу из блока в вызывающем методе. Те, кто знаком с языками, подобными Java и JavaScript, наверное, узнают в throw и catch ключевые слова, используемые в этих языках для выдачи и обработки ис- ключений. В Ruby работа с исключениями организована по-другому, в нем ис- пользуются ключевые слова raise и rescue, которые будут рассмотрены чуть поз- же. Но параллель с исключениями проведена неспроста. Вызов throw очень похож
5.5. Изменение хода работы программы 193 на выдачу исключения. И тот путь, по которому throw распространяется сквозь лексический контекст, а затем вверх по стеку вызовов, является почти таким же, как и распространение и восхождение по стеку исключения. (Как происходит рас- пространение исключения, мы рассмотрим в этой главе чуть позже.) Но несмотря на схожесть с исключением, лучше все же рассматривать throw и catch в качестве универсальной (хотя и не часто используемой) управляющей структуры, а не ме- ханизма исключений. Если нужно просигнализировать об ошибке или возникно- вении исключительных условий, то вместо throw используется raise. В следую- щем коде показано, как throw и catch могут быть использованы для «прерывания» вложенных циклов: for matrix in data do # Обработка глубоко вложенной # структуры данных. catch :missing_data do # Пометка этой инструкции для # возможности прерывания. for row in matrix do for value in row do throw :missing_data unless value # Прерывание сразу двух # циклов. # В противном случае здесь происходит обработка данных. end end end # Сюда мы попадаем после того, как вложенные циклы завершают обработку каждой # матрицы. # Мы также приходим сюда, если программа отбрасывается к метке :missing_data. end Заметьте, что метод catch воспринимает аргумент-обозначение и блок. Он вы- полняет блок и возвращает управление по выходу из блока или когда инструкция throw применяется с указанным обозначением. Инструкция throw также предпо- лагает применение в качестве аргумента обозначения и заставляет вернуть управ- ление из вызова соответствующей инструкции catch. Если обозначению, пере- данному throw, не соответствует ни один из вызовов инструкции catch, выдается исключение NameError. При вызове инструкций catch и throw вместо обозначений можно использовать строковые аргументы, которые затем подвергнутся внутрен- нему преобразованию в обозначения. Одной из особенностей throw и catch является способность работать даже в том случае, если они размещаются в разных методах. Мы можем переделать этот код, поместив наиболее глубоко вложенный цикл в отдельный метод, но поток управ- ления все равно будет работать вполне корректно. Если инструкция throw так и не будет вызвана, вызов catch приведет к возвраще- нию значения последнего выражения, размещенного в ее блоке. Если инструкция throw все же будет вызвана, то значение возвращаемого соответствующей ин- струкцией catch выражения по умолчанию будет равно nil. Но путем передачи throw второго аргумента можно определить произвольное значение, возвращаемое инструкцией catch. Возвращаемое catch значение поможет отличить нормальное
194 Глава 5. Инструкции и управляющие структуры завершение блока от ненормального его завершения при использовании инструк- ции throw, что в свою очередь позволит вам написать код, производящий какую- нибудь специальную обработку, необходимую для ответа на throw-завершение. На практике throw и catch не нашли широкого применения. Если возникнет же- лание применить catch и throw внутри одного и того же метода, то лучше будет рассмотреть возможность переделки catch в отдельный метод и замены throw ин- струкцией return. 5.6. Исключения и их обработка Исключение является объектом, представляющим некую разновидность исключи- тельных обстоятельств; оно свидетельствует о возникновении нештатного режима работы. Это может быть ошибка программирования — попытка деления на нуль, попытка вызова метода для объекта, в котором он не определен, или передача ме- тоду неверного аргумента. Или же оно может возникнуть в результате появления каких-то внешних условий — создания сетевого запроса в момент падения сети или попытки создания объекта при недостаточных ресурсах оперативной памяти. Когда возникают подобные ошибки или обстоятельства, выдается (или запускает- ся) исключение. При выдаче исключения исходные настройки приводят к останов- ке Ruby-программ. Но при этом имеется возможность объявления обработчиков исключений, которые представляют собой блоки кода, исполняемые в том случае, если исключение было выдано при выполнении какого-нибудь другого блока кода. В этом смысле исключения являются разновидностью управляющей инструкции. Выдача исключения приводит к передаче управления коду обработки исключения. Это похоже на использование инструкции break для выхода из цикла. Но, как мы вскоре убедимся, исключения в корне отличаются от действий инструкции break; они могут передавать управление за пределы многих охватывающих блоков и вы- брать весь стек вызовов, чтобы добраться до обработчика исключения. Для выдачи исключения в Ruby используется принадлежащий классу Kernel метод raise, а для обработки исключения используется предложение rescue. Ис- ключения, выданные с помощью метода raise, являются экземплярами класса Exception или одного из его многочисленных подклассов. Рассмотренные ранее методы throw и catch не предназначены для сообщения об исключении и его об- работки, но переход по обозначению, выдаваемому инструкцией throw, осущест- вляется так же, как и при выдаче исключения с помощью метода raise. Объекты исключений, переходы при выдаче исключений, метод raise и предложение rescue будут подробно рассмотрены в следующих подразделах. 5.6.1. Классы и объекты исключений Объекты исключений являются экземплярами класса Exception или одного из его многочисленных подклассов. Обычно для этих подклассов не определяются
5.6. Исключения и их обработка 195 какие-то новые методы или новое поведение, но они позволяют классифициро- вать исключения по типам. Иерархия класса показана на рис. 5.5. Object +--Exception +--NoMemoryError +--ScriptError | +--LoadError | +--NotImplenientedError | +--SyntaxError +--SecurityError # В версии 1.8 бып StandartError +--SignalException | +--Interrupt +--SystemExit +--SystemStackError # В версии 1.8 бып StandartError +--StandardError +--ArgumentError +--FiberError # Введен в версии 1.9 +--I0Error | +--E0FError +--IndexError | +--KeyError | +--Stoplteration +--LocalJumpError +--NameError # Введен в версии 1.9 # Введен в версии 1.9 | +--NoMethodError +--RangeError | +--FloatDomainError +--RegexpError +--RuntimeError +--SystemCallError +--ThreadError +--TypeError +--ZeroDivisionError Рис. 5.5. Иерархия Ruby-класса Exception Знать все эти подклассы исключений совсем не обязательно. Их имена сами гово- рят об их предназначении. Важно отметить, что многие из этих подклассов явля- ются расширениями класса, известного как StandardError. Они относятся к «обыч- ным» исключениям, которые обычные Ruby-программы пытаются подвергнуть обработке. Другие исключения, относящиеся к более низкому уровню, считают- ся более серьезными или менее пригодными для выправления неблагоприятных обстоятельств, и в обычных Ruby-программах за их обработку, как правило, не берутся. Если для поиска документации, относящейся к этим классам исключений, ис- пользовать инструментальное средство ri, то окажется, что большинство из них не снабжено документацией. Отчасти это обусловлено тем, что в большинстве своем
196 Глава 5. Инструкции и управляющие структуры они не добавляют каких-нибудь новых методов к тем, которые определены в базо- вом классе Exception. О каждом конкретном классе исключения важно знать, при каких условиях он может быть задействован при выдаче исключения. Как прави- ло, эти сведения присутствуют в описании тех методов, которые выдают исключе- ния, а не в описаниях классов исключений как таковых. 5.6.1.1. Методы объектов исключений В классе Exception определяются два метода, возвращающие подробности исклю- чения. Метод message возвращает строку, в которой могут быть предоставлены подробности случившегося, изложенные простым человеческим языком. Если происходит выход из Ruby-программы с необрабатываемым исключением, это со- общение обычно отображается конечному пользователю, но главное, для чего оно выдается — помочь программисту обнаружить причину проблемы. Другой важный метод объекта исключения — backtrace. Этот метод возвращает строковый массив, представляющий стек вызовов и точку, в которой было выдано исключение. Каждый элемент массива является строкой, имеющей следующую форму: Имя_файла : номер_строки в имя_метода В первом элементе массива определяется позиция, в которой было выдано исклю- чение; во втором элементе определяется позиция, в которой был вызван метод, ра- бота которого стала причиной выдачи исключения; в третьем элементе определя- ется позиция, из которой был вызван метод, содержащий предыдущую позицию, и т. д. (Метод cal 1 er, определенный в классе Kernel, возвращает трассировку стека в таком же формате; его работу можно испытать, воспользовавшись инструмен- тальным средством irb.) Обычно объекты исключений создаются методом raise. После этого метод raise устанавливает соответствующую трассировку стека ис- ключения. При создании собственного объекта исключения установку трасси- ровки стека можно провести по своему усмотрению, воспользовавшись методом set_backtrace. 5.6.1.2. Создание объектов исключений Далее будет показано, что объекты исключений обычно создаются методом raise. Но свои собственные объекты можно создать и при помощи обыкновенного мето- да new или при помощи другого метода класса под названием exception. Оба этих метода воспринимают единственный необязательный строковый аргумент. Если он указан, то строка становится значением метода message. 5.6.1.3. Определение новых классов исключений Зачастую при определении модуля Ruby-кода принято определять его собствен- ный подкласс StandardError для тех исключений, которые характерны для этого модуля. Это может быть банальный однострочный подкласс: class MyError < StandardError: end
5.6. Исключения и их обработка 197 5.6.2. Выдача исключений с помощью raise Исключение выдает имеющийся в классе Kernel метод raise. Иногда, если есть предположение, что исключение может привести к выходу из программы, исполь- зуется его синоним — метод fail. Для вызова raise существует несколько спосо- бов. О Если метод raise вызывается без аргументов, он создает новый объект Run- timeError (не имеющий сообщения) и выдает исключение с этим объектом. Или же если raise используется без аргументов внутри предложения rescue, то он просто заново выдает обрабатываемое исключение. О Если г a i se вызывается с аргументом, в качестве которого выступает единствен- ный объект класса Exception, он выдает это исключение. Несмотря на свою простоту, этот способ использования raise не получил широкого распростра- нения. О Если raise вызывается с единственным строковым аргументом, он создает новый объект исключения RuntimeError с указанной строкой в качестве его со- общения и выдает соответствующее исключение. Это самый распространенный способ использования метода raise. О Если первым аргументом метода raise является объект, для которого определен метод exception, то raise вызывает этот метод и выдает исключение, соответству- ющее возвращенному им объекту класса Except! on. В классе Except! on определен метод except! on, поэтому объект класса можно определять для любой разновид- ности исключений в качестве первого аргумента метода rai se. В качестве необязательного второго аргумента rai se воспринимает строку. Ес- ли строка определена, она передается методу exception в качестве его первого аргумента. Эта строка предназначена для использования в качестве сообщения выдаваемого исключения. Метод raise воспринимает также необязательный третий аргумент, в качестве которого может быть определен массив строк, которые будут использоваться в качестве обратной трассировки объекта исключения. Если третий аргумент не определен, метод raise сам устанавливает обратную трассировку исключе- ния (используя метод caller класса Kernel). В следующем коде определяется простой метод, выдающий исключение при вы- зове с параметром, чье значение оказалось недопустимым: def factorial(п) # Определение метода factorial # с аргументом п raise "неверный аргумент" if n < 1 # Выдача исключения для # неприемлемого п return 1 if n == 1 # factorial(1) равен 1 n * factorial(n-1) # Рекурсивное вычисление других # факториалов end
198 Глава 5. Инструкции и управляющие структуры Этот метод вызывает raise с единственным строковым аргументом. То же самое исключение можно выдать еще несколькими равнозначными способами: raise RuntimeError, "неверный аргумент" if n < 1 raise RuntimeError.new("неверный аргумент") if n < 1 raise RuntimeError.exception("неверный аргумент") if n < 1 В этом примере исключение класса ArgumentError, наверное, будет уместнее, чем RuntimeError: raise ArgumentError if n < 1 Также полезнее будет и более подробное сообщение об ошибке: raise ArgumentError. "Ожидался аргумент >= 1, а был получен #{n}” if n < 1 Смысл выданного исключения состоит в указании проблемы, возникшей при вы- зове метода factori al, которая не связана с кодом внутри метода. Исключение, вы- данное этим кодом, будет иметь обратную трассировку, первый элемент которой покажет, где был вызван метод raise. Во втором элементе массива будет уста- новлен код, который вызвал factorial с неприемлемым аргументом. Если нужно указать непосредственно на код, вызвавший проблему, то в качестве третьего ар- гумента можно предоставить методу raise собственную трассировку стека, полу- ченную от метода cal 1 er класса Kernel: if n < 1 raise ArgumentError. "Ожидался аргумент >= 1, а был получен #{n}", caller end Обратите внимание, что метод factori al проверяет, входит ли его аргумент в допу- стимый диапазон, но не проверяет, относится ли он к правильному типу данных. Можно добавить более тщательную проверку на ошибочный ввод, добавив к ме- тоду в качестве первой строки следующий код: raise ТуреЕггог, "Ожидался целочисленный аргумент" if not n.is_a? Integer С другой стороны, обратите внимание на то, что случится, если в качестве аргу- мента определенному выше методу factorial будет передана строка. Ruby сравни- вает аргумент п с целым числом 1 с помощью оператора < Если аргумент является строкой, сравнение не имеет смысла, и оно потерпит неудачу, выдавая ТуреЕггог. Если аргумент является экземпляром какого-нибудь класса, в котором не опреде- лен оператор <, то мы получим вместо этого ошибку NoMethodError. Дело в том, что это исключение все равно может произойти, даже если в нашем коде не будет вызван метод raise. Поэтому важно знать, как обрабатывать исклю- чения, даже если мы сами никогда их не выдавали. Обработка исключений рас- смотрена в следующем разделе. 5.6.3. Обработка исключений с помощью rescue Метод raise принадлежит классу Kernel. В отличие от него, предложение rescue вхо- дит в основную часть языка Ruby. По своей сути rescue — это не инструкция, а скорее
5.6. Исключения и их обработка 199 предложение, которое может быть присоединено к другим Ruby-инструкциям. Чаще всего предложение rescue присоединяется к инструкции begin, которая су- ществует лишь для того, чтобы отделить блок кода, внутри которого должны быть обработаны исключения. Инструкция begin с предложением rescue выглядит сле- дующим образом: begin # Здесь размещается любое количество Ruby-инструкций. # Обычно они выполняются без исключений, и это # выполнение продолжается после инструкции end. rescue # Это предложение rescue: здесь размещается код обработки исключения. # Если исключение выдается для кода, размещенного выше, или оно # распространяется вверх # из одного из методов, вызываемых выше, то исключение передает управление # в это место. end 5.6.З.1. Обозначение объекта исключения В предложении rescue глобальная переменная $! ссылается на обрабатываемый объект Exception. Восклицательный знак хорошо запоминается: исключение это тоже своего рода восклицание. Если программа включает в себя строку: require 'English' то вместо нее можно воспользоваться глобальной переменной $ERROR_INFO. Но лучше все же вместо $! или $ERROR_INFO определить имя переменной для объ- екта исключения в самом предложении rescue: rescue => ex Тогда инструкции этого rescue-предложения смогут использовать переменную ех для ссылки на объект Exception, в котором описывается исключение. Напри- мер: Begin # Обработка исключений в этом блоке х = factorial(-1) # Обратите внимание на недопустимый # аргумент rescue => ex # Сохранение исключения в переменной ех puts "#{ех.class}: #{ех.message}" # Обработка исключения путем вывода # сообщения end # Завершение блока begin-rescue Учтите, что предложение rescue не определяет новую область видимости перемен- ной, и переменная, чье имя указано в предложении rescue, сохраняет свою види- мость даже после завершения предложения rescue. Если в предложении rescue ис- пользуется переменная, то объект исключения может сохранять свою видимость и по окончании обработки rescue, даже когда $! уже не содержит установленного значения.
200 Глава 5. Инструкции и управляющие структуры 5.6.3.2. Обработка исключений по типам Показанное здесь предложение rescue обрабатывает любое исключение, относя- щееся к StandardError (или его подклассу), и игнорирует любой объект Exception, не относящийся к StandardError. Если нужно обработать нестандартные исключе- ния, не входящие в иерархию StandardError, или если нужно обработать только вполне определенные типы исключения, в предложение rescue следует включить один или более классов исключений. Вот как должно быть написано предложение rescue, обрабатывающее любую разновидность исключения: rescue Exception А вот как должно быть написано предложение rescue для обработки ArgumentError и присвоения объекта исключения переменной е: rescue ArgumentError => е Если вернуться к ранее определенному нами методу factorial, то он может вы- давать исключения ArgumentError или ТуреЕггог. А вот как предложение rescue мог- ло бы быть написано для обработки исключений, относящихся к обоим этим ти- пам, и присвоения объекта исключения переменной error: rescue ArgumentError. ТуреЕггог => error Здесь, наконец, мы видим синтаксис предложения rescue в самом общем виде. За ключевым словом rescue следует нуль или более разделенных запятыми выраже- ний, каждое из которых должно вычисляться в объект класса, являющийся пред- ставителем класса Expression или его подкласса. За этими выражениями может дополнительно следовать группа символов => и имя переменной. А теперь представим, что нам нужно обработать оба исключения, и ArgumentError и ТуреЕггог, но разными способами. Для запуска разного кода на основе класса объекта исключения можно воспользоваться инструкцией case. Но элегантнее будет просто использовать несколько выражений rescue. Таких выражений в ин- струкции begi п может быть от нуля и более: begin х = factorial (1) rescue ArgumentError => ex puts "Попробуйте еще раз co значением >= 1" rescue ТуреЕггог => ex puts "Попробуйте воспользоваться целым числом" end Следует учесть, что Ruby-интерпретатор пытается сопоставить исключения с предложениями rescue в порядке их написания. Поэтому наиболее характерные подклассы исключений должны быть указаны в первую очередь, а за ними уже должны следовать более общие типы. Если, к примеру, исключение, вызванное ошибкой конца файла — EOFError, нужно обработать несколько иначе, чем исклю- чение, вызванное ошибкой ввода-вывода — ЮЕггог, нужно обязательно поместить предложение rescue для EOFError первым, иначе это исключение обработает код
5.6. Исключения и их обработка 201 ЮЕггог. Если нужно воспользоваться «универсальным» предложением rescue, об- рабатывающим любое исключение, не обработанное предыдущими выражения- ми, в качестве последнего rescue-предложения следует использовать предложение rescue Exception. 5.6.3.3. Распространение исключений После ознакомления с предложением rescue, можно приступить к более деталь- ному объяснению распространения исключений. Когда выдается исключение, управление тут же передается из исходной точки и переходит вверх до тех пор, пока не будет найдено подходящее предложение rescue, предназначенное для обработки исключения. При выполнении метода raise Ruby-интерпретатор вы- сматривает, есть ли в охватывающем блоке связанное с ним предложение rescue. Если его нет (или если предложение rescue не определено для обработки этого вида исключения), интерпретатор проверяет блок, который охватывает наш охва- тывающий блок. Если где-нибудь в методе, вызвавшем raise, подходящего пред- ложения rescue не найдется, то этот метод осуществит выход самостоятельно. Если выход из метода произойдет из-за выдачи исключения, то он будет отличать- ся от нормального возврата. Метод не имеет возвращаемого значения, и объект исключения продолжает распространение с того места, откуда был вызван метод. Исключение распространяется за пределы охватывающих блоков в поисках пред- ложения rescue, определенного для его обработки. И если такого предложения найдено не будет, этот метод возвращается к тому месту, откуда он был вызван. Движение продолжается вверх по стеку вызовов. Если обработчик исключения отсутствует, Ruby-интерпретатор выводит сообщение об исключении и обратную трассировку и прекращает работу. В качестве конкретного примера рассмотрим следующий код: def explode # Этот метод выдает RuntimeError в Ш # случаев raise "Вам!" If rand(10) == О end def risky begin 10.times do Explode end rescue ТуреЕггог RuntimeError.. puts $! end "привет" # Этот блок # содержит другой блок # который может выдать исключение. # Предложения rescue здесь нет, поэтому # исключение # распространяется за пределы блока. # Это предложение rescue не может обработать # поэтому оно пропускается и исключение # распространяется наружу. # Это обычное возвращаемое значение, если # исключения не произошло. продолжение
202 Глава 5. Инструкции и управляющие структуры end # Предложения rescue здесь нет, поэтому # исключение распространяется # наружу, к месту вызова метода. def defuse begin puts risky rescue RuntimeError => e puts e.message # Следующий код может дать сбой из-за выдачи # исключения. # Попытка вызвать вывод возвращаемого # значения. # Если получаем исключение, # то вместо этого выводится сообщение об # ошибке. end end defuse Исключение выдается в методе explode. Этот метод не содержит предложения rescue, поэтому исключение распространяется за его пределы к вызывающему методу по имени ri sky. У метода ri sky есть предложение rescue, но оно заявлено лишь в качестве обработчика исключений ТуреЕггог, а не исключений RuntimeError. Исключение распространяется за пределы лексических блоков метода ri sky, а за- тем распространяется вверх к вызывающему методу по имени defuse. У метода defuse имеется предложение rescue для обработки исключений RuntimeError, по- этому управление передается этому предложению rescue, и распространение ис- ключения останавливается. Заметьте, что в этот код включено использование итератора (имеется в виду метод Integer.times) со связанным с ним блоком. Для простоты изложения мы сказали, что исключение распространяется за пределы этого лексического блока. Но на са- мом деле с целью распространения исключения такие блоки ведут себя во многом схоже с вызовами методов. Исключение распространяется из блока наверх, к тому итератору, который его вызвал. Предопределенные, организующие цикл итерато- ры вроде Integer. times сами по себе исключений не обрабатывают, поэтому исклю- чение распространяется выше по стеку вызовов от итератора times к методу risky, который его вызвал. 5.6.3.4. Исключения, выдаваемые в процессе обработки исключений Если исключительная ситуация возникает в процессе выполнения предложения rescue, исключение, обработка которого велась изначально, отбрасывается, а но- вое исключение распространяется с того места, в котором оно было выдано. Сле- дует учесть, что это новое исключение не может быть обработано тем предложени- ем rescue, которое следует за тем, в котором это исключение возникло. 5.6.3.5. Retry в предложении rescue Когда внутри предложения rescue используется инструкция retry, она переза- пускает выполнение блока кода, к которому присоединено предложение rescue.
5.6. Исключения и их обработка 203 Когда причиной исключения является отказ, возникший из-за переходного про- цесса, такого как перегрузка сервера, то, возможно, есть смысл обработать исклю- чение путем простой повторной попытки. Но многие другие исключения являются следствием ошибок программирования (ТуреЕггог, ZeroDi visionError) или ошибок непереходных процессов (EOFError или NoMemoryError). Для технологии обработки этих исключений инструкция retry явно не подходит. Приведем простой пример, в котором retry используется в попытке дождаться, пока не будет устранен отказ сети. В нем осуществляется попытка прочитать со- держимое, на которое указывает URL, и повторная попытка после отказа. Всего эа один раз осуществляется не более четырех попыток, и при этом используется «экспоненциальная задержка», предназначенная для увеличения времени ожида- ния между попытками: require 'open-uri' tries = О begin tries += 1 ореп('http://www.example.coni/') {|f| rescue OpenURI:: НТТРЕггог => e puts e.message if (tries < 4) sleep(2**tries) retry end end # Сколько раз осуществлялась попытка # чтения с указанного URL # Именно здесь начинается повторная # попытка # Попытка вывести содержимое, на которое # указывает URL puts f.readlines } # Если получена ошибка HTTP # Вывод сообщения об ошибке # Если четыре попытки еще не исчерпаны... # Ждем 2, 4 или В секунд # А затем повторяем попытку! 5.6.4. Предложение else Инструкция begi п после своих предложений rescue может включать предложение else. Можно предположить, что предложение else является неким универсальным вариантом предложения rescue, обрабатывающим любое исключение, которое не нашло себе соответствующего предложения rescue из тех, что следовали ранее. Но else предназначено не для этого. Предложение el se является альтернативой пред- ложениям rescue; оно задействуется в том случае, если надобности в использова- нии какого-нибудь из предложений rescue не возникло. То есть код предложения else выполняется в том случае, если код тела инструкции begin выполняется до конца без выдачи исключений. Размещение кода в предложении el se во многом похоже на добавление его в ко- нец всей инструкции begi п. Единственное отличие состоит в том, что при исполь- зовании предложения else любое исключение, выданное этим предложением, не обрабатывается инструкциями rescue. Предложения el se не нашли в Ruby широ- кого применения, но стилистически они могут быть полезны для подчеркивания
204 Глава 5. Инструкции и управляющие структуры разницы между обычным завершением блока кода и завершением его выполнения с выдачей исключения. Следует заметить, что использование предложения el se без одного и более пред- ложений rescue не имеет смысла. Ruby-интерпретатор разрешает это сделать, но выдает предупреждение. После предложения else никакие предложения rescue размещаться уже не могут. И наконец, следует заметить, что код в предложении else выполняется только в том случае, если код инструкции begin полностью выполнен и «достиг» конца. Если возникнет исключительная ситуация, то вполне очевидно, что предложение else выполнено не будет. Но присутствующие в инструкции begin инструкции break, return, next и им подобные также могут воспрепятствовать выполнению предложения el se. 5.6.5. Предложение ensure Инструкция begin может иметь одно завершающее предложение. Если это до- полнительное предложение ensure используется, то оно должно появляться после всех предложений rescue и else. Оно может быть использовано и само по себе, в отсутствие любых предложений rescue или el se. Предложение ensure содержит код, который выполняется всегда, независимо от того, что случилось с кодом, который следует за ключевым словом begin. О Если этот код выполняется до конца, то управление передается предложению el se, если таковое присутствует, а затем оно передается предложению ensure. О Если код выполняет инструкцию возвращения — return, то при ее выполнении пропускается предложение el se и перед возвращением управление переходит непосредственно к предложению ensure. О Если код, следующий за ключевым словом begin, выдает исключение, управле- ние передается соответствующему предложению rescue, азатем — предложению ensure. О Если предложения rescue отсутствуют или если ни одно из предложений rescue не может обработать исключение, то управление передается непосредственно предложению ensure. Код предложения ensure выполнятся до того, как исклю- чение будет распространено за пределы охватывающих блоков или вверх по стеку вызовов. Предложение ensure предназначено для того, чтобы гарантировать, что мы поза- ботились о совершении таких служебных действий, как закрытие файлов, разрыв подключения к базе данных и передача или отмена транзакций. Это довольно мощная управляющая структура, которой следует пользоваться в тех случаях, если задействован какой-нибудь ресурс (такой как описатель файла или под- ключение к базе данных), чтобы гарантировать его должное высвобождение или очистку.
5.6. Исключения и их обработка 205 Следует заметить, что предложение ensure усложняет распространение исключений. В ранее приведенных объяснениях мы не стали рассматривать предложения ensure. При распространении исключения оно не просто каким-то волшебным образом пе- репрыгивает из того места, где было выдано к тому месту, где будет обработано. На самом деле задействуется целый процесс его распространения. Ruby-интерпретатор просматривает охватывающие блоки и поднимается вверх по стеку вызовов. В каж- дой инструкции begin он выискивает предложение rescue, которое может обра- ботать исключение. И он выискивает связанные с этой инструкцией предложе- ния ensure и выполняет все подобные предложения, встречающиеся на его пути. Предложение ensure может отменить распространение исключения, положив начало какому-нибудь другому пути передачи управления. Если предложение ensure выдает новое исключение, то оно распространяется вместо исходного. Если предложение ensure включает инструкцию return, то распространение исключе- ния останавливается и осуществляется возвращение из охватывающего метода. Такие управляющие инструкции, как break и next, вызывают похожие эффекты: распространение исключения прекращается и осуществляется определенная для них передача управления. Предложение ensure усложняет также понятие возвращаемого методом значения. Хотя предложения ensure обычно используются для получения гарантий, что код будет выполнен даже в случае возникновения исключительных ситуаций, они также работают, чтобы гарантировать, что код будет запущен перед возвращени- ем из метода. Если тело инструкции begi п включает инструкцию return, код пред- ложения ensure будет запущен перед тем, как метод сможет вернуть управление туда, откуда он был вызван. Более того, если предложение ensure само содержит инструкцию return, оно изменит значение, возвращаемое методом. К примеру, следующий код возвращает значение 2: begin return 1 # Перед возвращением к месту вызова переход # к предложению ensure ensure return 2 # Замена возвращаемого значения этим новым # значением end Заметьте, что предложение ensure не изменяет возвращаемое методом значение, если в нем в явном виде используется инструкция return. К примеру, следующий метод возвращает 1, а не 2: def test begin return 1 ensure 2 end end Если инструкция begin не распространяет исключение, то значением инструкции является последнее вычисленное в begin, rescue или else предложение. Код пред- ложения ensure выполняется в обязательном порядке, но он не оказывает влияния на значение инструкции begi п.
206 Глава 5. Инструкции и управляющие структуры 5.6.6. Rescue в определениях метода, класса и модуля Во время рассмотрения обработки исключения ключевые слова rescue, else и ensure характеризовались как части инструкции begin. На самом деле они так- же могут быть использованы как предложения инструкции def (определяющей метод), инструкции class (определяющей класс) и инструкции module (опреде- ляющей модуль). Определение методов рассматривается в главе 6, а определение классов и модулей — в главе 7. Следующий код является упрощенной структурой определения метода, в кото- ром используются предложения rescue, el se и ensure: def method_name(x) # Сюда помещается тело метода. # Обычно тело метода выполняется до конца без выдачи исключений # и передает обычным порядком управление в то место, откуда был вызван метод, rescue # Сюда помещается код обработки исключения. # Если в теле метода выдано исключение или если один из # вызываемых им методов выдал исключение, # управление переходит к этому блоку. else # Если в теле метода не возникло исключительных ситуаций. # выполняется код этого предложения, ensure # Код в этом предложении выполняется независимо от того, что # происходит в теле метода. Он запускается, если метод выполнен до конца, если # он выдал исключение или если он выполняет инструкцию return, end 5.6.7. Rescue в качестве модификатора инструкции Вдобавок к своему использованию в качестве предложения, ключевое слово rescue может быть также использовано в качестве модификатора инструкции. Любую инструкцию можно продолжить ключевым словом rescue и другой инструкцией. Если первая инструкция выдаст исключение, вместо нее будет выполнена вторая инструкция. Например: # Вычисление факториала х. или использование 0. если метод выдает исключение у = factorial(х) rescue О Этот код эквивалентен следующему: у = begin factorial (х) rescue О end
5.7. BEGIN и END 207 Преимущество синтаксиса модификатора инструкции состоит в том, что отпада- ет необходимость в ключевых словах begin и end. При использовании в этом ка- честве ключевое слово rescue должно быть без дополнений в виде имен классов исключений и имени переменной. Модификатор rescue обрабатывает любое ис- ключение StandardError, но не обрабатывает исключений других типов. В отличие от модификаторов i f и whi 1 е, модификатор rescue имеет более высокий приоритет (как показано в таблице 4.2 в предыдущей главе), чем операторы присваивания. Это означает, что он применяется только в правой части присваивания (так же, как в приведенном выше примере), а не в отношении всего выражения присваивания. 5.7. BEGIN и END BEGIN и END являются зарезервированными словами Ruby, которые объявляют код, исполняемый в самом начале и в самом конце Ruby-программы. (Обратите внимание, что BEGIN и END, которые пишутся заглавными буквами, полностью от- личаются от begi п и end, которые пишутся строчными буквами.) Если в програм- ме присутствует более одной инструкции BEGIN, они выполняются в том поряд- ке, в котором их встречает интерпретатор. Если в программе присутствует более одной инструкции END, они выполняются в порядке, обратном тому, в котором их встречает интерпретатор, — т. е. первая выполняется последней. Эти инструкции используются в Ruby нечасто. Они унаследованы из языка Perl, который в свою очередь унаследовал их у языка обработки текста Awk. За BEGIN и END должна следовать открывающая фигурная скобка, некоторое коли- чество Ruby-кода и закрывающая фигурная скобка. Фигурные скобки являются обязательными элементами; использование do и end здесь запрещено. Например: BEGIN { # Сюда помещается глобальный код инициализации } END { # Сюда помещается глобальный код завершения программы } Между инструкциями BEGIN и END есть небольшое отличие. Инструкции BEGIN вы- полняются перед всем остальным, включая любой окружающий код. Это озна- чает, что в них определяется область видимости локальных переменных, которая полностью отделена от окружающего кода. Лучше всего размещать BEGIN в самой верхней части кода; инструкция BEGIN внутри условия или цикла будет выполнена без учета окружающих ее условий. Рассмотрим следующий код: if (false) BEGIN { puts "if": # Эта строка будет выведена на печать а = 4; # Эта переменная определена только здесь } продолжение &
208 Глава 5. Инструкции и управляющие структуры else BEGIN { puts "else" } # Эта строка также будет выведена на печать end 10.times {BEGIN { puts "loop" }} # Вывод на печать состоится только один раз Код, связанный с тремя инструкциями BEGIN, будет выполнен лишь один раз, неза- висимо от контекста, в котором он появляется. Переменные, определенные вну- три блоков BEGIN, не будут видимы за пределами этого блока, и никакие перемен- ные за пределами этого блока еще не будут определены. Инструкции END ведут себя по-другому. Они выполняются в процессе обычного выполнения программы, поэтому они используют локальные переменные вместе с окружающим кодом. Если инструкция END находится внутри условий, которые не выполняются, то связанный с нею код при завершении программы никогда не показывается на выполнение. Если инструкция END располагается внутри цикла, выполняемого более одного раза, то код, связанный с нею, все равно показывается лишь однажды: а = 4; if (true) END { puts "if"; puts a } else END { puts "else" } end 10.times {END { puts "loop" }} # Эта инструкция END выполняется # Этот код показывается на выполнение # Переменная находится в области видимости # выводится "4" # Эта инструкция не выполняется # Эта инструкция выполняется однократно Метод at_exit, определенный в классе Kernel, предоставляет альтернативу ин- струкции END; он показывает блок кода, который будет выполнен непосредственно перед выходом интерпретатора из работы. Как и в случае с END-блоками, код, свя- занный с первым вызовом at exi t, будет выполнен последним. Если метод at exi t вызывается внутри цикла несколько раз, то связанный с ним блок при выходе ин- терпретатора из работы будет выполнен несколько раз. 5.8. Потоки, нити (fibers) и продолжения (continuations) В этом разделе дается представление о потоках, являющихся управляющими структурами Ruby для параллельного выполнения, а также двух более изо- щренных структурах управления, называемых нити (fibers) и продолжения (continuations).
5.8. Потоки, нити (fibers) и продолжения (continuations) 209 5.8.1. Потоки для параллельного выполнения Поток выполнения является последовательностью Ruby-инструкций, которые запускаются (или производят впечатление того, что запускаются) в параллель с основной последовательностью инструкций, выполняемых интерпретатором. Потоки представлены объектами класса Thread, но их также можно рассматривать как управляющие структуры для параллельного выполнения. Параллельное про- граммирование в Ruby подробно рассматривается в разделе 9.9. А данный раздел является кратким обзором, в котором показано, как создавать потоки. Использование блоков в Ruby существенно упрощает создание новых потоков. Нужно просто вызвать метод Thread. new и связать с ним блок. Будет создан новый поток выполнения, который запустит код, имеющийся в блоке. Тем временем ис- ходный поток вернется из вызова Thread. new и продолжит выполнение со следую- щей инструкции. Вновь созданный поток завершит работу, когда произойдет вы- ход из блока. Возвращаемое значение блока станет доступным через метод value объекта класса Thread. (Если вызвать этот метод до того, как поток завершит свою работу, вызов будет заблокирован до тех пор, пока поток не вернет значение.) Следующий код показывает, как можно использовать потоки для параллельного считывания содержимого нескольких файлов: # Этот метод ожидает массив имен файлов. # Он возвращает массив строк, хранящих содержимое указанных файлов. # Метод создает по одному потоку для каждого указанного файла, def readfiles (filenames) # Создание массива потоков из массива имен файлов. # Каждый поток приступает к чтению файла. threads = filenames.map do |f| Thread.new { File.read(f) } end # Теперь создается массив содержимого файлов путем вызова метода value # каждого потока. Если нужно, этот метод блокируется до тех пор. пока поток # не завершит # работу с выдачей значения. threads.map {|t| t.value } end Более подробно потоки и параллельное выполнение кода в языке Ruby рассмотре- ны в разделе 9.9. 5.8.2. Нити для сопрограмм В Ruby 1.9 вводится структура управления, известная как нить (fiber) и представ- ленная объектом класса Fiber. Название «нить» использовалось в других местах для своего рода легкого потока, но нити в Ruby точнее будет охарактеризовать как сопрограммы, или, если выразиться точнее, как полусопрограммы. Наиболее
210 Глава 5. Инструкции и управляющие структуры распространенным примером использования сопрограмм является реализация ге- нераторов'. объектов, которые способны вычислять частичный результат, вернуть этот результат вызывающей программе и сохранить состояние вычисления, чтобы вызывающая программа могла продолжить это вычисление для получения сле- дующего результата. В Ruby класс Fiber используется для получения автоматиче- ского преобразования внутренних итераторов, таких как метод each, в нумераторы или внешние итераторы. Следует заметить, что нити (fibers) являются новаторской и сравнительно ма- лопонятной управляющей структурой; непосредственное использование класса Fiber большинству Ruby-программистов никогда и не понадобится. Если вы ни- когда раньше не использовали в программировании сопрограммы или генерато- ры, то поначалу они могут показаться трудными для понимания. В таком случае нужно тщательно изучать примеры и пробовать создавать свои собственные при- меры их использования. Нить, как и поток, имеет тело кода. Чтобы определить код, который будет ра- ботать в нити, нужно создать нить, воспользовавшись методом Fiber.new, и свя- зать с ней блок кода. В отличие от потока, тело нити не начинает выполняться тотчас же. Чтобы запустить нить, нужно вызвать метод resume для объекта класса Fiber, который ее представляет. При первом вызове для нити метода resume управ- ление передается к началу тела нити. Затем эта нить работает до окончания своего тела или до того, как ею будет выполнен метод класса Fiber.yield. Метод fiber, yield возвращает управление вызывающей программе и совершает выход из вы- зова resume. Он также сохраняет состояние нити, чтобы следующий вызов resume смог подхватить нить в том состоянии, в котором она была оставлена. Приведем простой пример: f = Fiber.new { puts "Вас приветствует нить" Fiber.yield puts "Нить с Вами прощается" # Строка 1 # Строка 2 # Строка 3 # Строка 4 Создание новой нити переход на строку 9 } # Строка 5 переход на строку 11 puts "Вас приветствует вызывающая программа" f.resume # Строка 6 # Строка 7 # Строка В переход на строку 2 puts "Вызывающая программа с Вами прощается" f.resume # Строка 9 # Строка 1( :переход на строку 4 # Строка 11: Тело нити не выполняется сразу же после создания, поэтому этот код создает нить, но не выдает никаких данных до тех пор, пока не дойдет до строки 7. За- тем управление передается вперед и назад за счет вызовов методов resume и fiber, yield, поэтому сообщения из нити и вызывающей программы чередуются. Код производит следующий вывод: Вас приветствует вызывающая программа Вас приветствует нить Вызывающая программа с Вами прощается Нить с Вами прощается
5.8. Потоки, нити (fibers) и продолжения (continuations) 211 Здесь стоит отметить, что «передача» (yielding), выполняемая методом класса Fiber.yield, совсем не похожа на передачу, осуществляемую инструкцией yield. Метод fiber.yield передает управление из текущей нити обратно вызвавшей ее программе. А инструкция yield передает управление от метода-итератора связан- ному с методом блоку. 5.8.2.1. Аргументы нити и возвращаемые значения Нити и вызывающие их программы могут обмениваться данными посредством аргументов и возвращаемых значений методов resume и yield. Аргументы первого вызова resume передаются блоку, связанному с нитью: они становятся значениями параметров блока. При последующем вызове аргументы метода resume становятся возвращаемым значением метода Fi ber.yi el d. И наоборот, любые аргументы Fiber.yield становятся возвращаемым значением метода resume. А когда происходит вход из блока, возвращаемым значением resume также становится последнее вычисленное выражение. Все это демонстрируется в следующем коде: f = Fiber.new do |message| puts "Вызывающая программа сказала: #{message}'' message2 = Fiber.yieldCFIpneeT") # При первом вызове resume # возвращается "Привет" puts " Вызывающая программа сказала: #{message2}" "Отлично" # При втором вызове resume # возвращается "Отлично" end response = f. resume! "Привет") puts "Нить сказала: #{response}" response2 = f.resumeCKaK дела?") puts "Нить сказала: #{response2}" # Блоку передается "Привет" # Fiber.yield возвращает "Как # дела?" Вызывающая программа передает нити два сообщения, а нить возвращает вызы- ваемой программе два ответа. При этом выводится следующая информация: Вызывющая программа сказала: Привет Нить сказала: Привет Вызывающая программа сказала: Как дела? Нить сказала: Отлично В коде вызывающей программы сообщения всегда являются аргументами метода resume, а ответы всегда являются возвращаемыми значениями этого метода. В теле нити все сообщения, кроме первого, получены в качестве возвращаемого значения метода fiber.yield, а все ответы, кроме последнего, переданы в качестве аргумен- тов метода fiber.yield. Первое сообщение получено через параметры блока, а по- следний ответ является возвращаемым значением самого блока.
212 Глава 5. Инструкции и управляющие структуры 5.8.2.2. Реализация генераторов с помощью нитей В показанных ранее примерах нитей реальной пользы не прослеживалось. В этом разделе мы покажем некоторые более типичные примеры их использования. Сна- чала мы создадим генератор чисел Фибоначчи — объект Fiber, возвращающий следующие друг за другом числа последовательности Фибоначчи при каждом вы- зове resume: # Возвращение нити для вычисления def fibonacci_generator(xO,yO) Fiber.new do x.y = xO, yO loop do Fiber.yield у последовательности x.y = y.x+y end end end g = fibonacci_generator(0.1) 10.times { print g.resume. " " } Этот код выводит первые десять Фибоначчи # Задание исходных данных # последовательности в хО.уО # Инициализация х и у # Эта нить работает бесконечно # Передача следующего числа # Обновление значений х и у # Создание генератора # И его использование Фибоначчи: 1 1 2 3 5 В 13 21 34 55 Поскольку нить является не самой понятной управляющей структурой, лучше, наверное, при написании генераторов скрыть ее API. Приведем другую версию генератора чисел Фибоначчи. В ней определяется собственный класс и реализо- ваны такие же next и rewi nd API, как и в нумераторах: class FibonacciGenerator def initialize @x.@y = 0,1 @fiber = Fiber.new do loop do @x.@y = @y. @x+@y Fiber.yield @x end end end def next # Возвращение следующего числа Фибоначчи @fiber.resume end def rewind # Перезапуск последовательости @x,@y = 0,1 end end
5.8. Потоки, нити (fibers) и продолжения (continuations) 213 # Создание генератора # Вывод первых 10 чисел # Начало новой последовательности # Повторный вывод первых 10 чисел g = FibonacciGenerator.new 10.times { print g.next. " " } g.rewind; puts 10.times { print g.next. " " } Следует заметить, что этот класс, Fibonacci Generator, можно сделать перечисляе- мым, включив в него модуль Enumerabl е и добавив следующий метод each (который впервые был использован в разделе 5.3.5): def each loop { yield self.next } end И наоборот, предположим, что есть перечисляемый объект, на основе которого нужно создать генератор в стиле нумератора. Можно воспользоваться следую- щим классом: class Generator def initialize(enumerable) denumerable = enumerable create_fiber end def next dfiber.resume end def rewind create_fiber end private def create_fiber dfiber = Fiber.new do denumerable.each do |x| Fiber.yield(x) end raise Stopiteration end end end g = Generator.new(l..10) loop { print g.next } g. rewind g = (1.. 10),to_enum loop { print g.next } # Запоминание перечисляемого объекта # Создание нити для перечисления его # элементов # Возвращение следующего элемента # путем возобновления работы нити # Запуск нумератора с самого начала # путем создания новой нити # Создание нити, осуществляющей нумерацию # Создание новой нити # Использование метода each # Но выдерживание паузы во время подсчета # возвращаемых значений # Выдача исключения, когда значения # заканчиваются # Вот так создается генератор из # перечисляемого объекта # А вот так он используется как нумератор # Вот так его работа начинается заново # То же самое делает метод to_enum
214 Глава 5. Инструкции и управляющие структуры Хотя от изучения реализации это класса Generator есть определенная польза, сам класс по своим возможностям не превосходит функциональных возможностей метода to_enum. 5.8.2.3. Расширенные возможности нитей Модуль fiber, принадлежащий стандартной библиотеке, предоставляет дополни- тельные, более мощные возможности использования нитей. Чтобы ими восполь- зоваться, нужно включить в программу следующую строку: require 'fiber' Но везде где только можно следует воздерживаться от использования этих до- полнительных возможностей, поскольку: О они поддерживаются не всеми реализациями. К примеру, JRuby не может под- держивать их на существующих виртуальных машинах Java; О они обладают такой мощью, что их неправильное применение способно при- вести к аварии виртуальной машины Ruby. Основные возможности класса Fiber осуществляются с помощью полусопро- грамм. Они не являются настоящими сопрограммами, поскольку между вызы- вающей программой и нитью имеет место существенная асимметрия: вызываю- щая программа использует resume, а нить использует yield. Но если востребована библиотека fiber, класс Fiber получает метод transfer, позволяющий любой нити передавать управление любой другой нити. Приведем пример, в котором две нити используют метод transfer для передачи управления (и значений) вперед и назад: require 'fiber' f = g = nil f = Fiber.new {|x| #1: puts "fl: #{x}" # 2: выводит "fl: 1" x = g.transfer(x+l) # 3: передает 2 строке В puts "f2: #{x}" # 4: выводит "f2: 3" x = g.transfer(x+l) # 5: возвращает 4 строке 10 puts "f3: #{x}" # 6: выводит "f3: 5" x + 1 #7: возвращает 6 строке 13 g = Fiber.new {|x| # B: puts "gl: #{xj" # 9: выводит "gl: 2" x = f,transfer(x+l) #10: возвращает 3 строке 3 puts "g2: #{x}" #11: выводит ”g2: 4" x = f.transfer(x+l) #12: возвращает 5 строке 5 i 1 puts f.transfer(l) #13: передает 1 строке 1 Этот код выводит следующую информацию: fl: 1 gl: 2
5.8. Потоки, нита (fibers) и продолжения (continuations) 215 f2: 3 g2: 4 f3: 5 6 Возможно, метод transfer так никогда и не пригодится, но его существование по- могает объяснить само название «нить». Нити можно рассматривать как незави- симые пути выполнения при единственном потоке выполнения программы. Но в отличие от потоков, здесь нет диспетчера передачи управления между нитями; Нити должны планировать свою работу самостоятельно с использованием метода transfer. В дополнение к методу transfer библиотека fiber определяет также метод экзем- пляра alive?, чтобы можно было понять, находится тело нити в работе или нет, и метод класса current для возвращения объекта Fi ber, которому в данный момент передано управление. 5.8.3. Продолжения Продолжения (Continuations) представляют другую, не менее сложную и мало- понятную управляющую структуру, которая большинству программистов может быть никогда и не понадобится. Продолжения формируются с помощью метода callcc класса Kernel и объекта Continuation. В Ruby 1.8 продолжения являются ча- стью основной платформы, но в Ruby 1.9 они были заменены нитями и перемеще- ны в стандартную библиотеку. Для работы с ними в Ruby 1.9 нужно запросить их явным образом: require 'continuation' Сложность осуществления мешает другим реализациям Ruby (к примеру, J Ruby, реализации на основе языка Java) поддерживать продолжения. Поскольку их дальнейшая активная поддержка не предусматривается, продолжения должны рассматриваться как некий антиквариат, и их не следует использовать во вновь создаваемом Ruby-коде. Если вы располагаете кодом, написанным на языке Ru- by 1.8, работа которого основана на продолжениях, то его можно переделать на использование нитей, поддерживаемых в Ruby 1.9. Метод callcc класса Kernel выполняет свой блок, передавая вновь созданный объ- ект Continuation в качестве единственного аргумента. У объекта Continuation име- ется метод call, который заставляет вызов callcc вернуть управление вызвавшей его программе. Значение, переданное методу call, становится возвращаемым зна- чением вызова callcc. В этом смысле callcc похож на catch, а метод call объекта Conti nuati on похож на throw. Но продолжения бывают разными, поскольку объект Conti nuati on может быть со- хранен в переменной за пределами блока callcc. Метод call этого объекта может вызываться повторно, заставляя управление переходить на первую инструкцию, следующую за вызовом callcc.
216 Глава 5. Инструкции и управляющие структуры Следующий код демонстрирует, как продолжения могут быть использованы для определения метода, который работает наподобие инструкции goto из языка про- граммирования Бейсик: # Глобальный хэш для отображения номеров строк (или символов) для продолжений $1 Ines = {} # Создание продолжения и отображение его на указанный номер строки def line(symbol) callcc {|c| $11nes[symbol] = c } end # Поиск продолжения, связанного с номером, и переход на него def goto(symbol) $11nes[symbol].call end # Теперь можно симулировать программирование на Бейсике 1 = О line 10 puts 1 += 1 goto 10 if 1 < 5 # Объявление этого места строкой 10 # Переход назад на строку 10, если # условие соблюдено line 20 puts 1 -= 1 goto 20 If 1 : > 0 # Объявление этого места строкой 20
Глава 6 Методы, proc- и lambda-объекты И ЗАМКНУТЫЕ ВЫРАЖЕНИЯ
218 Глава 6. Методы, ргос- и lambda-объекты и замкнутые выражения Метод — это поименованный блок параметризированного кода, связанный с од- ним или более объектами. В вызове метода указывается его имя, объект, для ко- торого он вызывается (иногда называемый получателем), и нуль или более зна- чений аргументов, которые присваиваются поименованным параметрам метода. Значением выражения вызова метода становится последнее вычисленное в мето- де значение. Во многих языках делается различие между функциями, не имеющими связан- ных с ними объектов, и методами, которые вызываются для объекта-получателя. Поскольку Ruby является сугубо объектно-ориентированным языком, все его ме- тоды действительно являются методами и связаны по крайней мере с одним из объектов. Мы еще не касались вопроса определения Ruby-классов, поэтому при- меры методов, определяемых в этой главе, похожи на глобальные функции, не имеющие связанных с ними объектов. На самом деле Ruby определяет и вызывает их неявным образом в качестве закрытых методов класса Object. Методы являются основной частью Ruby-синтаксиса, но не относятся к тем зна- чениям, которыми могут оперировать Ruby-программы. То есть Ruby-методы не являются такими же объектами, как строки, числа и массивы. Тем не менее суще- ствует возможность получения объекта Method, представляющего заданный метод, благодаря которой методы можно вызывать опосредованно, через объекты Method. Методы не являются единственной имеющейся в Ruby формой параметризиро- ванного исполняемого кода. Блоки, с которыми мы познакомились в разделе 5.4, являются исполняемыми фрагментами кода, которые могут иметь параметры. В отличие от методов, блоки не имеют имен и могут быть вызваны лишь косвен- ным образом через метод-итератор. Блоки, как и методы, не являются объектами обработки в языке Ruby. Тем не ме- нее существует возможность создать объект, представляющий блок, чем иногда и пользуются в Ruby-программах. Блок может быть представлен Ргос-объектом. Как и объект Method, блок кода можно выполнить посредством представляющего его Ргос-объекта. Существует две разновидности Ргос-объектов, которые называ- ются ргос и lambda, поведение которых слегка различается. И ргос и lambda явля- ются функциями, в отличие от методов, вызываемых для какого-нибудь объекта Важным свойством ргос и 1 ambda является то, что они являются замкнутыми выра- жениями: они сохраняют доступ к локальным переменным, которые присутство- вали в области видимости, в тот момент, когда они определялись, даже когда ргос или 1 ambda вызываются из другой области видимости. Методы в Ruby имеют довольно богатый и сложный синтаксис, им посвящены первые четыре раздела этой главы. Сначала будет объяснено, как определить про- стые методы, а затем за этим вводным разделом последуют три более сложных, в которых рассматриваются имена методов, использование круглых скобок и па- раметры методов. Нужно учесть, что вызов метода является разновидностью вы- ражения, которое было рассмотрено в разделе 4.4. В первых четырех разделах этой главы изложены дополнительные подробности вызова методов. После рассмотрения методов будет уделено внимание ргос и lambda, мы объ- ясним, как они создаются и вызываются, а также рассмотрим те тонкости, ко- торые составляют различия между ними. Отдельный раздел будет посвящен
6.1. Определение простых методов 219 использованию proc и 1 ambda в качестве замкнутых выражений. Затем последует раздел, посвященный объекту Method, который ведет себя фактически так же, как и lambda. Окончание главы будет посвящено расширенному исследованию функ- ционального программирования на языке Ruby. 6.1. Определение простых методов В примерах, приводимых в этой книге, встречается множество вызовов методов, а синтаксис вызова метода был подробно описан в разделе 4.4. Теперь мы уделим внимание синтаксису определения методов. В этом разделе объясняются основы определения методов. За ним последуют еще три раздела, в которых более подроб- но рассматриваются имена методов, использование круглых скобок и аргументы методов. В этих дополнительных разделах объясняется более сложный материал, имеющий отношение как к определению, так и к вызову методов. Методы определяются с помощью ключевого слова def. За ним следует имя ме- тода и дополнительный список параметров, заключенный в круглые скобки. За перечнем параметров следует Ruby-код, составляющий тело метода, а окончание метода помечается ключевым словом end. Имена параметров могут быть исполь- зованы в качестве переменных внутри тела метода, а значения этих поименован- ных параметров берутся из аргументов вызова метода. Возьмем для примера сле- дующий метод: # Определение метода по имени 'factorial' с единственным параметром 'п' def factorial (n) if n < 1 # Проверка годности значения аргумента raise "аргумент должен быть > О" elsif n == 1 # Если аргумент равен 1, 1 # то значение вызова метода равно 1 else # В противном случае факториал п равен п, # умноженному на n * factorial(п-1) # факториал п-1 end end В этом коде определяется метод по имени factorial. У этого метода есть один па- раметр по имени п. Идентификатор п используется в качестве переменной в теле метода. Это рекурсивный метод, поэтому тело метода включает его вызов. Этот вызов представляет собой просто имя метода, за которым следует значение аргу- мента, заключенное в круглые скобки. 6.1.1. Значение, возвращаемое методом Метод может быть завершен нормальным или ненормальным образом. Ненор- мальное завершение происходит в том случае, если метод выдает исключение. Показанный ранее метод factorial завершается ненормальным образом, если в ка- честве его аргумента передается число меньше 1. Если происходит нормальное
220 Глава 6. Методы, proc- и lambda-объекты и замкнутые выражения завершение метода, то значением выражения вызова метода будет значение по- следнего вычисленного в теле метода выражения. В методе factori al этим послед- ним выражением будет либо выражение 1, либо выражение n*factorial (п-1). Ключевое слово return используется для принудительного возврата из метода до окончания его работы. Если за ключевым словом return следует выражение, то возвращается значение этого выражения. Если выражение за ним не следует, то возвращаемое значение равно ni 1. В следующем варианте метода factorial задей- ствовано ключевое слово return: def factorial(n) raise "недопустимый аргумент" if n < 1 return 1 if n == 1 n * factorial(n-1) end Ключевое слово return также может использоваться в последней строке тела этого метода, чтобы подчеркнуть, что значение этого выражения является значением, возвращаемым этим методом. Но по сложившейся практике если надобность в ис- пользовании ключевого слова return не возникла, то оно опускается. Ruby-методы могут возвращать более одного значения. Для этого инструкцию return следует задействовать в явном виде и разделить возвращаемые значения запятыми: # Преобразование декартовых координат (х.у) в полярные (величина, угол) def polar(x.y) return Math.hypot(y,x). Math.atan2(y,x) end Когда мы имеем дело более чем с одним возвращаемым значением, эти значения собираются в массив, и массив становится единственным возвращаемым значени- ем метода. Вместо использования инструкции return с несколькими значениями, можно просто создать массив значений: # Преобразование полярных координат в декартовы def cartesian(magnitude, angle) [magnitude*Math.cos(angle), magnitude*Math.sin(angle)] end Методы, представленные в такой форме, обычно предназначены для использова- ния параллельного присваивания (рассмотренного в разделе 4.5.5), чтобы каждое из этих возвращаемых значений присваивалось отдельной переменной: distance, theta = polar(x.y) х.у = cartesian(distance,theta) 6.1.2. Методы и обработка исключений Инструкция def, определяющая метод, может включать код обработки исключе- ний в форме выражений rescue, el se и ensure. Такой же код может включать в себя
6.1. Определение простых методов 221 и инструкция begin. Эти выражения обработки исключений следуют после завер- шения тела метода, но перед ключевым словом end инструкции def. При опреде- лении небольших методов лучше всего связать выражения rescue с инструкцией def. При этом отпадает необходимость использования инструкции begin и сопут- ствующих ей дополнительных уровней отступа. Более подробно этот вопрос рас- смотрен в разделе 5.6.6. 6.1.3. Вызов метода для объекта Методы всегда вызываются для какого-нибудь объекта. (Иногда такой объект на- зывают получателем в соответствии с парадигмой объектно-ориентированного программирования, согласно которой методы называются «сообщениями», кото- рые «передаются» объектам-получателям.) Внутри тела метода ключевое слово sei f является ссылкой на объект, для которого этот метод был вызван. Если при вызове метода объект не указан, то этот метод неявным образом вызывается для объекта, на который указывает sei f. Как определяются методы для классов объектов, мы узнаем в главе 7. А сейчас следует заметить, что мы уже сталкивались с вызовами методов для объектов, рас- сматривая код, подобный следующему: first = text. i ndex( pattern) Как и многие другие объектно-ориентированные языки, Ruby использует для от- деления объекта от метода, который для него вызывается, символ точки (.). В при- веденном примере кода значение переменной pattern передается методу по имени index, вызываемого для объекта, ссылка на который хранится в переменной text, и сохраняет возвращаемое значение в переменной f i rst. 6.1.4. Определение синглтон-методов Все определенные до сих пор методы являются глобальными. Если поместить какую-нибудь из ранее показанных инструкций def внутри инструкции class, то определяемые методы станут методами экземпляра класса; эти методы определя- ются для всех объектов, являющихся экземплярами класса. (Классы и методы эк- земпляров класса рассматриваются в главе 7.) Но инструкцию def можно также использовать при определении метода для един- ственного указанного объекта. Для этого за ключевым словом def нужно просто указать выражение, вычисляемое в объект. За этим выражением должна следо- вать точка и имя определяемого метода. Получающийся в результате этого метод известен как синглтон-метод (метод, определенный в единственном экземпляре), поскольку он доступен только в отдельно взятом объекте: о= "message" # Строка, являющаяся объектом def o.printme # Определение для этого объекта синглтон-метода puts self end o.printme # Вызов синглтон-метода
222 Глава 6. Методы, proc- и lambda-объекты и замкнутые выражения Методы класса (рассматриваемые в главе 7), такие как Math.sin и File.delete, на самом деле являются синглтон-методами. Math является константой, ссылаю- щейся на объект Module, a F11е — константой, ссылающейся на объект Class. Эти два объекта имеют синглтон-методы с именами si п и del ete соответственно. Реализации Ruby обычно рассматривают значения Fixnum и Symbol как непосред- ственные значения, а не как настоящие ссылки на объект. (О чем рассказывалось в разделе 3.8.1.1.) По этой причине определить синглтон-методы для объектов Fixnum и Symbol невозможно. Чтобы соблюсти последовательность, следует заме- тить, что синглтон-методы запрещены также для объектов Numeric. 6.1.5. Отмена определения методов Методы определяются с помощью инструкции def, а отменить их определение можно с помощью инструкции undef: def sum(x.y); х+у: end # Определение метода puts sum(l,2) # Его использование undef sum # И отмена его определения В этом коде инструкция def определяет глобальный метод, а инструкция undef от- меняет его определение. Инструкция undef также работает с классами (которые будут рассматриваться в главе 7) для отмены определений имеющихся в классе методов экземпляра. Интересно отметить, что undef может использоваться для отмены определения унаследованных методов, не оказывая при этом никакого влияния на определение метода в классе, из которого он был унаследован. Пред- положим, что в классе А определен метод т, а класс В является подклассом классаА, в силу чего наследует т. (Подклассы и наследование также являются предметом рассмотрения главы 7.) Если экземплярам класса В не нужно разрешать вызов т, то в теле подкласса можно воспользоваться выражением undef m. Инструкция undef используется нечасто. На практике более распространено пере- определение метода с использованием новой инструкции def, которая отменяет определение или удаляет метод. Заметьте, что за инструкцией undef должен следовать единственный идентифика- тор, определяющий имя метода. Эта инструкция не может быть использована для отмены определения синглтон-метода с использованием того же приема, который был применен инструкцией def для определения этого метода. Внутри класса или модуля для отмены определения методов можно воспользо- ваться методом undefjnethod (закрытым методом, определенным в классе Module). Ему нужно передать обозначение, представляющее имя метода, чье определение отменяется.
6.2. Имена методов 223 6.2. Имена методов По имеющемуся соглашению, имена методов начинаются со строчной буквы. (Име- на методов могут начинаться и с заглавных букв, но тогда они станут похожими на имена констант.) Когда имя метода длиннее одного слова, составляющие его слова принято отделять друг от друга знаком подчеркивания таким_образом, а не использовать смешанный регистр такимОбразом. РАЗРЕШЕНИЕ ИМЕНИ МЕТОДА В этом разделе рассматриваются имена, присваиваемые методам при их опре- делении. Родственной темой является разрешение имени метода: как Ruby- интерпретатор отыскивает определение метода, имя которого указано в выра- жении вызова метода? Ответ на этот вопрос подождет своей очереди, пока мы не перейдем к рассмотрению классов Ruby. Ему будет посвящен раздел 7.8. Имена методов могут (но не обязаны) заканчиваться знаком равенства, вопроси- тельным или восклицательным знаком. Знак равенства свидетельствует о том, что метод является установщиком (setter), который может быть вызван при исполь- зовании синтаксиса присваивания. Методы-установщики описаны в разделе 4.5.3, а дополнительные примеры использования этих методов представлены в разде- ле 7.1.5. Суффиксы в виде вопросительных и восклицательных знаков не имеют для Ruby-интерпретатора какого-то особого значения, но их использование обу- словлено возможностью применения двух весьма полезных соглашений об именах. Первое соглашение состоит в том, что любой метод, чье имя оканчивается знаком вопроса, возвращает значение, отвечающее на вопрос, поставленный вызовом ме- тода. К примеру, метод empty? (пустой?), вызванный для массива, возвращает true, если в массиве нет элементов. Подобные методы называются также предикатами, обычно они возвращают одно из булевых значений true или false, но не обяза- тельно, поскольку когда требуется получить булево значение, любое значение, от- личное от false или nil, ведет себя как true. (К примеру, принадлежащий классу Numeric метод nonzero? возвращает nil, если число, для которого он вызван, равно нулю, а в противном случае он просто возвращает это число.) Второе соглашение состоит в том, что любой метод, чье имя оканчивается воскли- цательным знаком, должен применяться с оглядкой. К примеру, объект массива располагает методом sort, который создает копию этого массива, а затем проводит сортировку этой копии. Но для него также определен и метод sort!, который про- водит сортировку именно этого массива. Восклицательный знак показывает, что при использовании этой версии нужно проявлять осмотрительность. Зачастую методы, чье имя оканчивается восклицательным знаком, являются мутаторами (mutators), изменяющими внутреннее состояние объекта. Но так бывает не всегда; существует множество мутаторов, чьи имена не оканчиваются восклицательным
224 Глава 6. Методы, proc- и lambda-объекты и замкнутые выражения знаком, и ряд немутаторов, имеющих его в конце своего имени. Методы, произво- дящие мутацию (такие как Array.fill), не имеющие варианта, не производящего мутацию, обычно не имеют в конце своего имени восклицательного знака. Рассмотрим глобальную функцию exit: она заставляет Ruby-программу прекра- тить свое выполнение управляемым образом. Есть также вариант по имени exit!, который немедленно прекращает работу программы без запуска каких-либо бло- ков END или закрытия обработчиков прерываний, зарегистрированных с помощью at exit. Метод exit! не является мутатором; он представляет собой «опасный» ва- риант метода exi t и помечен восклицательным знаком (!), чтобы напомнить про- граммисту, что им следует пользоваться осмотрительно. 6.2.1. Методы-операторы Многие Ruby-операторы, в частности +, * и даже оператор индексации массива [], реализованы с помощью методов, которые могут быть определены в ваших соб- ственных классах. Определение оператора производится путем определения мето- да с таким же «именем», как и у оператора. (Единственным исключением являются операторы унарных плюса и минуса, чьи методы носят имена +@ и -@.) Ruby предо- ставляет возможность такого определения, даже если имя метода сплошь состоит из знаков пунктуации. Можно остановиться на следующем определении метода: def -Kother) # Определение бинарного оператора плюс: # х+у превращается в х.+(у) self,concatenate(other) end В таблице 4.2 главы 4 показано, какие Ruby-операторы определены в виде мето- дов. Эти операторы представляют собой всего лишь имена методов, составленные из знаков пунктуации, которыми можно воспользоваться: можно изобрести новые операторы или определить методы, чьи имена состоят из других последователь- ностей знаков пунктуации. Дополнительные примеры определения операторов на основе методов показаны в разделе 7.1.6. Методам, определяющим унарные операторы, аргументы не передаются. Методам, определяющим бинарные операторы, передается один аргумент, и они должны работать с ключевым словом self и этим аргументом. Операторы доступа к эле- ментам массива — [] и []= представляют собой особый случай, поскольку могут вызываться с любым количеством аргументов. Для оператора []= последний аргу- мент всегда представляет присваиваемое значение. 6.2.2. Псевдонимы методов Нередко у Ruby-методов имеется более одного имени. В самом языке имеется ключевое слово alias, которое служит для определения нового имени существую- щего метода. Используется оно следующим образом: alias aka also_known_as # alias новое_имя существующее_имя
6.2. Имена методов 225 После выполнения этой инструкции идентификатор aka будет ссылаться на тот же метод, что и al so_known_as. Присвоение методам псевдонимов является одной из характерных черт языка Ruby, придающих ему выразительность и естественность. Когда у метода множе- ство имен, можно выбрать одно из них, которое наиболее естественно вписывается в код. К примеру, в классе Range определен проверочный метод, определяющий, по- падает значение в диапазон или нет. Этот метод можно вызвать, воспользовавшись именем include? или именем member?. Если диапазон рассматривается как своео- бразный набор, то имя member? может выглядеть естественнее, чем другие имена. Присвоение методам псевдонимов имеет и более практичный смысл, заключаю- щийся в наращивании функциональных возможностей метода. Следующий при- мер демонстрирует общепринятый способ для наращивания функциональности существующих методов: def hel lo puts "Всем привет" end # Простой метод приветствия # Предположим, что его нужно чем-то дополнить... alias original_hello hello # Присвоение методу дублирующего имени def hello # А теперь определение нового метода со старым # именем puts "Минуточку внимания" original_hello puts "Проверка связи " end # Который что-то делает, # Потом вызывает исходный метод # А потом еще что-то делает В этом примере работа велась с глобальными методами. Но чаще всего alias ис- пользуется с методами экземпляра класса. (Которые будут рассмотрены в гла- ве 7.) При этом alias должен использоваться внутри того класса, методу которого создается псевдоним. Классы в Ruby могут «открываться заново» (что также рас- сматривается в главе 7) — это означает, что ваш код может взять существующий класс, «открыть» его с помощью инструкции class, а затем воспользоваться ин- струкцией al i as, как показано в примере, чтобы дополнить или изменить суще- ствующие методы класса. Этот прием называется «выстраиванием цепочки псев- донимов» (alias chaining) и подробно рассматривается в разделе 8.11. ПРИСВАИВАНИЕ ПСЕВДОНИМОВ НЕ ЯВЛЯЕТСЯ ПЕРЕОПРЕДЕЛЕНИЕМ Методы Ruby могут иметь два имени, но два метода не могут пользоваться одним и тем же именем. В языках со статическими типами данных методы могут различаться по числу и типам своих аргументов, и два и более методов могут пользоваться Одним и тем же именем, пока они воспринимают разное количество или различные типы аргументов. Такая разновидность переопреде- ления в Ruby невозможна.
226 Глава 6. Методы, ргос- и lambda-объекты и замкнутые выражения С другой стороны, в переопределении методов в Ruby нет особой надоб- ности. Методы могут воспринимать аргументы любого класса и могут быть написаны для осуществления разных действий на основе типа переданных им аргументов. К тому же (как мы позже увидим) аргументы методов Ruby могут быть объявлены со значениями по умолчанию, и эти аргументы могут быть опущены при вызовах методов. Это позволяет одному и тому же методу быть вызванным с различным количеством аргументов. 6.3. Методы и круглые скобки В большинстве случаев при вызове методов Ruby позволяет опускать круглые скобки. В результате этого в простых ситуациях код выглядит намного проще. Но в более сложных ситуациях это приводит к синтаксической неразберихе и пута- нице. Именно эти обстоятельства и станут предметом рассмотрения в следующих подразделах. 6.3.1. Необязательные скобки При вызовах методов скобки опускаются во многих свойственных Ruby стилях написания программного кода. К примеру, следующие две строки кода абсолютно равнозначны: puts "Hello World" putsCHello World”) В первой строке puts выглядит как ключевое слово, инструкция или встроенная в язык команда. Эквивалентная ей вторая строка показывает, что это всего лишь вызов глобального метода, в котором опущены круглые скобки. Хотя вторая из этих двух форм более понятна, первая форма более компактна, чаще используется и, наверное, выглядит естественнее. Теперь рассмотрим следующий код: greeting = "Hello” size - greeting.length Если вы привыкли работать с другими объектно-ориентированными языками, то можете подумать, что 1 ength — это свойство, поле или переменная строковых объектов. Но Ruby является сугубо объектно-ориентированным языком, и его объекты полностью инкапсулированы; единственным способом наладить с ними взаимодействие является вызов их методов. В этом коде greeting. 1 ength является вызовом метода. Метод length не предполагает использования аргументов и вы- зывается без скобок. Эквивалентом ранее приведенному коду служит следующий код: size = greeting. lengthO
6.3. Методы и круглые скобки 227 Включение необязательных скобок подчеркивает, что это вызов метода. Если при вызове метода, не имеющего аргументов опустить скобки, возникнет иллюзия до- ступа к свойству, что случается довольно часто. Круглые скобки зачастую опуска- ются, когда у вызываемого метода нет аргументов или имеется всего лишь один аргумент. А вот опускание скобок при наличии нескольких аргументов, как по- казано в следующем примере, распространено значительно реже: х = 3 # х является числом х.between? 1.5 # То же самое, что и х.between?(1,5) Скобки также могут быть опущены вокруг списка параметров при определении метода, хотя трудно будет согласиться с тем, что это сделает код понятнее и лег- че для чтения. К примеру, в следующем коде определяется метод, возвращающий сумму своих аргументов: def sum х, у х+у end 6.3.2. Обязательные скобки Если есть код, который при опущенных скобках может быть неоднозначно ис- толкован, то Ruby требует их обязательного использования. Наиболее типичным случаем является вложенный вызов методов в виде f g х. у. В Ruby вызов, вы- раженный в такой форме, означает f(g(x,y)). Но Ruby 1.8 выдает предупрежде- ние, поскольку код может быть истолкован так же, как f(g(x) ,у). В Ruby 1.9 такое предупреждение было удалено. Следующий код, использующий определенный ранее метод sum, выводит 4, но вызывает выдачу предупреждения в Ruby 1.8: puts sum 2. 2 Чтобы избавиться от предупреждения, нужно переписать код, поставив скобки вокруг аргументов: puts sum(2.2) Следует заметить, что установка скобок вокруг вызова внешнего метода не устра- няет двусмысленности: puts(sum 2.2) # Что это значит. puts(sum(2,2)) или puts(sum(2), 2)? Выражение, содержащее вызов вложенных функций, имеет неоднозначное толко- вание лишь в том случае, когда в нем используется более одного аргумента. Ruby- интерпретатор может истолковать следующий код совершенно однозначно: puts factorial х # Это может означать лишь puts(factorial(х)) Хотя здесь и не возникает никакой неоднозначности, если скобки вокруг х опу- щены, Ruby 1.8 все равно выдает предупреждение. Иногда отсутствие скобок не ограничивается простым предупреждением и является настоящей синтаксиче- ской ошибкой.
228 Глава 6. Методы, proc- и lambda-объекты и замкнутые выражения К примеру, следующие выражения в отсутствие скобок могут быть истолкованы абсолютно неоднозначно, и Ruby даже не пытается выстроить на их счет какое- либо догадки: puts 4. sum 2.2 # Ошибка: к чему относится вторая запятая, к первому или # ко второму методу? [sum 2,2] # Ошибка: это два элемента массива или один? Есть еще один полезный совет, связанный с необязательностью использования скобок. Когда вы действительно используете круглые скобки при вызове метода, открывающая скобка обязательно должна следовать непосредственно за именем метода без промежуточного пробела. Причина заключается в двойном назначе- нии скобок: они могут использоваться вокруг списка аргументов в вызове метода и они же могут использоваться для группировки выражений. Рассмотрим следую- щие два выражения, различающиеся всего одним пробелом: square(2+2)*2 # square(4)*2 = 16*2 = 32 square (2+2)*2 # square(4*2) = square(8) = 64 В первом выражении скобки представляют часть вызова метода. А во втором они представляют группировку выражения. Чтобы сократить возможную путаницу, нужно всегда использовать скобки вокруг вызова метода, если в каком-нибудь из аргументов тоже используются скобки. Второе выражение для большей ясности может быть написано следующим образом: square((2+2)*2) Завершим этот разговор о скобках еще одним приемом. Следует напомнить, что следующее выражение является неоднозначным и вызывает выдачу предупре- ждения: puts(sum 2.2) # Что это может означать, puts(sum(2,2)) или puts(sum(2), 2)? Лучшим способом разрешения этой неоднозначности станет взятие в скобки ар- гументов метода sum. А другой способ заключается во вставке пробела между puts и открывающей скобкой: puts (sum 2,2) Добавление пробела превращает скобки, связанные с вызовом метода, в скобки группировки выражения. Поскольку эти скобки приводят к группировке подвы- ражения, запятая уже не рассматривается как разделитель аргументов для вызова метода puts. 6.4. Аргументы метода Простая форма объявления метода включает после имени метода список имен ар- гументов, разделенных запятыми (в необязательных скобках). Но об аргументах методов Ruby можно рассказать намного больше. В следующих подразделах будет рассмотрено:
6.4. Аргументы метода 229 О как объявить параметр, имеющий значение по умолчанию, чтобы при вызове метода соответствующий ему аргумент можно было опустить; О как объявить метод, принимающий любое количество аргументов; О как имитировать поименованные аргументы метода, используя специальный синтаксис для передачи хэша в метод; О как объявить метод, чтобы блок, связанный с вызовом метода, обрабатывался как аргумент метода. 6.4.1. Параметры по умолчанию При определении метода для некоторых или для всех параметров можно опреде- лить значения по умолчанию. Если воспользоваться этой возможностью, то метод может быть вызван с таким количеством значений аргументов, которое меньше, чем количество объявленных параметров. Если аргументы опущены, то вместо них используется объявленное для параметра значение по умолчанию. Значение по умолчанию объявляется путем сопровождения имени параметра знаком равен- ства и значением: def prefix(s, len=l) s[0.1en] end В этом методе объявлены два параметра, но у второго из них имеется значение по умолчанию. Значит, это метод можно вызвать либо с одним, либо с двумя ар- гументами: prefixC'Ruby". 3) # => "Rub" prefixC'Ruby”) # => ”R” Значения по умолчанию, объявленные для параметров, необязательно должны быть константами: они могут быть произвольными выражениями и могут ссылать- ся на переменные экземпляра и на предыдущие параметры из списка. Например: # Возвращение последнего символа s или подстроки от указанного индекса # и до конца строки def suffix(s. index=s.size-1) sfindex, s.size-index] end Объявленные по умолчанию значения параметров вычисляются не при синтак- сическом разборе, а при вызове метода. В следующем методе значение по умол- чанию [] при каждом вызове приводит к созданию нового пустого массива, а не использует снова тот массив, который был бы создан при определении метода: # Добавление значения х к массиву а, возвращение а. # Если массив не указан - работа с пустым массивом. def append(x, а=[]) а « х end
230 Глава 6. Методы, proc- и lambda-объекты и замкнутые выражения В Ruby 1.8 параметры метода, имеющие значения по умолчанию, должны появ- ляться в списке после всех обычных параметров. В Ruby 1.9 это ограничение не- сколько смягчено и обычные параметры могут появляться после параметров, име- ющих значения по умолчанию. Но при этом остается требование держать вместе все параметры, имеющие значения по умолчанию, — к примеру, нельзя объявить два параметра со значениями по умолчанию, поместив между ними обычный па- раметр. Когда метод имеет более одного параметра со значением по умолчанию и метод вызывается с аргументами для некоторых, но не для всех из этих параме- тров, то они заполняются слева направо. Предположим, что метод имеет два параметра и оба эти параметра имеют значения по умолчанию. Тогда метод можно вызвать без аргументов, а также с одним или с двумя аргументами. Если указать один аргумент, то он будет присвоен первому параметру, а второй параметр воспользуется своим значением по умолчанию. Но способа указать значение для второго параметра и воспользоваться значением по умолчанию для первого не существует. 6.4.2. Список аргументов переменной длины и массивы Иногда нужно создать методы, способные воспринимать произвольное количе- ство аргументов. Для этого впереди одного из параметров метода ставится звез- дочка (*). В теле метода этот параметр будет ссылаться на массив, содержащий нуль или более аргументов, переданных в этой позиции. Например: # Возвращение наибольшего из одного или более переданных аргументов def max(first, *rest) # Предположим, что обязательный первый аргумент имеет наибольшее значение max = first # Теперь осуществим циклический перебор каждого из необязательных аргументов, # выискивая самый большой rest.each {|х| max = х if х > max } # возвращение самого большого найденного аргумента max end Метод max имеет как минимум один обязательный аргумент, но может принимать любое количество дополнительных аргументов. Первый аргумент становится до- ступным благодаря параметру fi rst. Все дополнительные аргументы хранятся в массиве rest. Метод max можно вызвать следующим образом: max(l) # first=l, rest=[] max(l,2) # first=l, rest=[2] max(l,2,3) # first=l. rest=[2,3] Следует заметить, что в Ruby все перечисляемые (Enumerable) объекты автомати- чески имеют метод max, поэтому определенный здесь метод абсолютно бесполе- зен.
6.4. Аргументы метода 231 Префикс * может иметь только один параметр. В Ruby 1.8 это параметр может по- являться после всех обычных параметров и после всех параметров, для которых определены значения по умолчанию. Он должен быть последним параметром ме- тода, за исключением того случая, когда метод имеет параметр с префиксом & (рас- смотренный ниже). В Ruby 1.9 параметр с префиксом * также должен появляться после любых параметров с определенными значениями по умолчанию, но за ним могут дополнительно следовать обычные параметры. Он также должен появлять- ся перед любыми параметрами с префиксом &. 6.4.2.1. Передача методам массивов Мы увидели, как звездочка (*) может быть использована в объявлении метода, чтобы несколько аргументов могли быть собраны или объединены в один массив. Она также может быть использована в вызове метода для рассеивания, развер- тывания или разбиения массива (или диапазона, или нумератора) на элементы, чтобы каждый элемент становился отдельным аргументом метода. Иногда сим- вол * называют оператором-звездочкой, хотя его нельзя считать настоящим опе- ратором. Его применение мы уже видели, когда рассматривали параллельное при- сваивание в разделе 4.5.5. Предположим, что нужно найти максимальное значение в массиве (а о том, что массивы в Ruby имеют встроенный метод max, мы как бы и не в курсе!). Можно передать элементы массива определенному ранее методу max: data = [3. 2. 1] m= max(*data) # first = 3, rest=[2,l] => 3 Рассмотрим, что произойдет без использования звездочки (*): m= max(data) # first = [3,2,1], rest=[] => [3,2,1] В таком случае массив рассматривается как первый и единственный аргумент и наш метод max возвращает этот первый аргумент, не производя с ним никаких сравнений. Звездочка (*) может также быть использована с методами, которые возвраща- ют массивы, для развертывания этих массивов, чтобы использовать их в других вызовах методов. Рассмотрим определенные ранее в этой главе методы polar и cartesian: # Преобразование точки (х.у) в полярные координаты, а затем обратно в декартовы х.у = cartesian(*polаг(х, у)) В Ruby 1.9 нумераторы также являются разбиваемыми объектами. К примеру, для поиска в строке буквы с наибольшим значением кода можно написать следующую строку: max(*"hello world" ,each_char) # => 'w'
232 Глава 6. Методы, ргос- и lambda-объекты и замкнутые выражения 6.4.3. Отображение аргументов на параметры Когда определение метода включает параметры со значениями по умолчанию или параметры, имеющие префикс звездочка (*), присвоение значений аргументов па- раметрам несколько усложняется. В Ruby 1.8 на позиции специальных параметров накладываются ограничения и значения аргументов присваиваются параметрам слева направо. Первые ар- гументы присваиваются обычным параметрам. Если остаются еще аргументы, то они присваиваются тем параметрам, которые имеют значения по умолчанию. А если остаются еще какие-нибудь аргументы, то они присваиваются массиву. Ruby 1.9 должен проявить несколько большую сообразительность насчет способа отображения аргументов на параметры, поскольку порядок следования параме- тров уже не носит столь же обязательный характер. Предположим, что есть метод, объявленный с о обычными параметрами, d параметрами, имеющими значения по умолчанию, и одним параметром-массивом, имеющим префикс-звездочку (*). Те- перь предположим, что этот метод вызывается с а аргументами. Если а меньше, чем о, будет выдана ошибка ArgumentError; поскольку не предостав- лен минимум обязательных аргументов. Если а больше или равно о и меньше или равно о + d, то самые левые а - о параме- тры со значениями по умолчанию получат присвоенные им значения аргументов. Параметры, оставшиеся (справа) — о + d - а, имеющие значения по умолчанию, не будут иметь присвоенных им значений аргументов и воспользуются своими значениями по умолчанию. Если а больше, чем о + d, то параметр-массив, чье имя имеет префикс *, получит для сохранения а - о - d значений аргументов; в противном случае массив будет пуст. После того как будут проделаны эти вычисления, аргументы будут отобра- жены на параметры слева направо, при этом соответствующее количество значе- ний аргументов будет присвоено каждому параметру. 6.4.4. Использование хэшей для поименованных аргументов Когда у метода есть более двух или трех обязательных аргументов, программист может испытывать затруднения при вызове метода, связанные с необходимостью помнить правильный порядок следования этих аргументов. В некоторых языках программирования разрешается оформлять вызов методов таким образом, что- бы указывать имя параметра в явном виде для каждого передаваемого аргумента. В Ruby такой синтаксис вызова метода не поддерживается, но можно добиться похожего эффекта, если написать метод, который в качестве своего аргумента или одного из своих аргументов воспринимает хэш: # Этот метод возвращает массив из п чисел. Для любого индекса 1, 0 <= 1 < п, # значение элемента а[1] равно m*j+c. Аргументы n, m и с передаются в виде ключей # в хзше, поэтому запоминать их порядок не требуется.
6.4. Аргументы метода 233 def sequence(args) # Извлечение аргументов из хзша. # Заметьте, что для указания значений по умолчанию на тот случай, если в хэше # не определен нужный ключ, использован оператор ||. п = args[:п] || О m = args[:m] || 1 с = argsE :с] 11 О а = [] # Сначала массив будет пуст n. times {|i| а « m*i+c } # Вычисление значения каждого элемента массива а # Возвращение массива Этот метод можно вызвать с аргументов в виде хэш-литерала: sequencet{:n=>3, :m=>5}) # => [0, 5, 10] Чтобы улучшить поддержку такого стиля программирования, Ruby позволяет опустить фигурные скобки вокруг хэш-литерала, если он является последним аргументом метода (или если единственным следующим за ним аргументом яв- ляется аргумент блока, имеющий префикс &). Хэш без фигурных скобок иногда называют голым хэшем (bare hash), и если используется именно такой хэш, то это становится похожим на передачу отдельных поименованных аргументов, порядок следования которых можно менять как угодно: sequencef :m=>3, :n=>5) # => [0, 3, 6, 9, 12] Как и в других Ruby-методах, круглые скобки также можно опустить: # Синтаксис хэша, используемый в Ruby 1.9 sequence с:1, m:3. n:5 # => [1, 4, 7, 10, 13] Если круглые скобки опущены, то нужно опустить и фигурные скобки. Если фи- гурные скобки, не заключенные в круглые, будут следовать за именем метода, то Ruby будет считать, что методу передается блок: sequence {:m=>3, :n=>5} # Синтаксическая ошибка! 6.4.5. Блоки-аргументы Вспомним, что в разделе 5.3 мы усвоили, что блок является фрагментом Ruby- кода, который связан с вызовом метода, и что итератор — это метод, для которого предполагается наличие блока. Блок может следовать за любыми вызовами ме- тода, и любой метод, имеющий связанный с ним блок, может вызвать код в этом блоке с помощью инструкции yield. Чтобы освежить вашу память, приведем сле- дующий код, являющийся блочно-ориентированным вариантом метода sequence, который ранее был создан в этой главе: # Генерация последовательности из п чисел m*i + с и передача ее блоку def sequence2(n. m, с) i = 0 whilefi < n) # Цикл из п проходов продолжение &
234 Глава 6. Методы, proc- и lambda-объекты и замкнутые выражения yield i*m + с # Передает блоку следующий элемент # последовательности 1 += 1 end end # А вот как этой версией метода можно воспользоваться sequence2(5. 2, 2) {|х| puts х } # Выводит числа 2, 4, 6, В, 10 Одной из особенностей блоков является их анонимность. Они не передаются методу в традиционном смысле слова, у них нет имени, и они вызываются с ис- пользованием ключевого слова, а не метода. Если вы предпочитаете получить не- сколько более явное управление блоком (чтобы его можно было передать, к при- меру, какому-нибудь другому методу), добавьте к своему методу завершающий аргумент и снабдите его имя префиксом в виде амперсанда1. После этого такой аргумент будет ссылаться на блок, переданный методу, если таковой имеется. Зна- чением аргумента будет Ргос-объект, а вместо использования yield, нужно будет вызывать принадлежащий Ргос-объекту метод cal 1: def sequence3(n, m, c. &b) # Явно указанный аргумент для получения # блока в виде Ргос-объекта 1 = О whiled < п) b.calld*m + с) # Вызов Ргос-объекта с помощью имеющегося # у него метода call 1 += 1 end end # Обратите внимание, что блок по-прежнему передается за пределами круглых скобок sequence3(5, 2, 2) {|х| puts х } Подобное использование амперсанда изменяет лишь определение метода. Вызов метода остается прежним. Мы остановились на блоке-аргументе, объявленном вну- три скобок определения метода, но сам блок по-прежнему определяется за предела- ми круглых скобок, используемых при вызове метода. Ранее в этой главе мы дважды упоминали, что специальный параметр должен за- мыкать список. Блоки-аргументы, имеющие в качестве префикса знак амперсан- да, действительно должны быть последними в списке. Поскольку блоки переда- ются в вызовах методов необычным способом, поименованные блоки-аргументы отличаются от всего остального и не мешают параметрам в виде массива или хэша, для которых были опущены круглые и фигурные скобки. К примеру, следующим двум методам нельзя отказать в праве на существование: def sequence5(args, &b) # Передача аргументов в виде хэша, за которым # следует блок п, щ, с = args[:n], args[:m], args[:c] 1 Для параметров метода, имеющих префикс &, мы воспользовались термином «блок- аргумент» вместо термина «блок-параметр». Причина в том, что фраза «блок-параметр» перекликается со списком параметров самого блока (таким как | х |).
6.4. Аргументы метода 235 1 = о whiled < п) b.call(i*m + с) 1 += 1 end end # Предполагается один или более аргуметов, за которыми следует блок def max(first. *rest, &block) max = first rest.each {|x| max = x if x > max } block.call(max) max ЯВНАЯ ПЕРЕДАЧА PROC-ОБЪЕКТОВ Если создается свой собственный Ргос-объект (чуть позже в этой главе мы увидим, как это делается) и его нужно явным образом передать методу, то это делается точно так же, как и передача любого другого значения — Ргос явля- ется таким же объектом, как и все остальные. В таком случае использовать амперсанд в определении метода не нужно: # Эта версия предполагает использование не блока, а Ргос-объекта, # созданного явным образом def sequence4(n, m. с. b) # Для аргумента b амперсанд не используется 1 = О whiled < п) b.call(i*m + с) # Ргос-объект вызывается явным образом i += 1 end end р = Proc.new {|x| puts x } # Явное создание Ргос-объекта sequence4(5, 2. 2. р) # И передача его в качестве обычного # аргумента Эти методы вполне работоспособны, но следует заметить, этих сложностей можно было избежать, оставив блоки анонимными и вызывая их с помощью инструкции yield. Стоит также заметить, что инструкция yield по-прежнему работает в мето- дах, определенных с использованием параметра с префиксом &. Даже если блок был преобразован в Ргос-объект и передан в виде аргумента, он по-прежнему может быть вызван в виде анонимного блока, как будто блок-аргумент и не применялся. 6.4.5.1. Использование & в вызове метода Ранее мы уже видели, что в определении метода можно использовать звездочку (*), чтобы определить, что несколько аргументов должны быть запакованы в мас- сив, а затем можно использовать звездочку в вызове метода, чтобы указать на то,
236 Глава 6. Методы, proc- и lambda-объекты и замкнутые выражения что массив должен быть распакован и его элементы стали отдельно взятыми зна- чениями аргументов. Амперсанд (&) тоже может быть использован в определени- ях и вызовах. Только что мы видели, как знак & в определении метода позволял обычному блоку, связанному с вызовом метода, использоваться в внутри метода в качестве поименованного Proc-объекта. Когда амперсанд (&) используется перед Proc-объектом в вызове метода, Proc-объект рассматривается наравне с обычным блоком, который следует за вызовом метода. Рассмотрим следующий код, который складывает содержимое двух массивов: а, b = [1,2,3], [4,5] # Начнем с данных. sum = a.inject(O) {|total,х| total+x } # => 6. Сложение элементов массива а sum = b.inject(sum) {|total,х| total+x } # => 15. Добавление к этой сумме # элементов массива Ь. Итератор inject был рассмотрен ранее, в разделе 5.3.2. Если вы не помните, что это такое, просмотрите его документацию, вызвав команду ri Enumerable.inject. Здесь важно отметить, что два блока совершенно идентичны. Вместо того чтобы заставлять Ruby-интерпретатор дважды проводить синтаксический анализ одно- го и того же блока, можно создать Proc-объект, представляющий этот блок, и дваж- ды воспользоваться одним и тем же Ргос-объектом: а, b = [1,2,3], [4,5] # Начнем с данных. summation = Proc.new {|total,x| total+x } # Proc-объект для суммирования sum = a.injectCO, &summation) # => 6 sum = b.inject(sum, &summation) # => 15 Если знак & используется в вызове метода, он должен появиться перед последним аргументом вызова. Блоки могут быть связаны с любым вызовом метода, даже когда метод не предусматривает использование блока и ни разу не использует инструкцию yield. Точно так же любой вызов метода может иметь в качестве по- следнего аргумент со знаком &. В вызовах метода знак & обычно появляется перед Ргос-объектом. Но фактически его появление разрешено перед любым объектом, имеющим метод to_proc. Класс Method (рассматриваемый далее в этой главе) об- ладает таким методом, поэтому объекты класса Method могут быть переданы итера- торам точно так же, как и Ргос-объекты. В Ruby 1.9 метод to_proc определен в классе Symbol, позволяя обозначениям иметь префикс & и передаваться итераторам. Когда обозначение передается подобным образом, то оно воспринимается как имя метода. Proc-объект, возвращаемый ме- тодом to proc, вызывает метод, указанный в качестве своего первого аргумента, передавая все оставшиеся аргументы указанному методу. Классический случай: на основе заданного строкового массива нужно создать новый массив, состоящий из этих же строк, символы которых преобразованы в символы верхнего регистра. Symbol .to proc позволяет выполнить это действие весьма элегантным образом: words = ['and', 'but’, ’car'] uppercase = words.map &:upcase upper = words.map {|w| w.upcase } # Массив слов # Преобразование в верхний регистр # с использованием String.upcase # А это эквивалент кода, использующий блок
6.5. Ргос и lambda 237 6.5. Ргос и lambda Блоки в Ruby — это синтаксическая структура; они не являются объектами и с ними нельзя работать как с объектами. Тем не менее есть возможность создать объект, представляющий блок. В зависимости от того, как этот объект создается, он называется ргос или lambda. Объекты ргос, ведут себя наподобие блоков, а объ- екты 1 ambda по поведению больше напоминают методы. Но обе эти разновидности являются экземплярами класса Ргос. В следующих подразделах мы рассмотрим: О как создавать Ргос-объекты в обеих формах — ргос и lambda; О как вызывать Ргос-объекты; О как определить количество аргументов, ожидаемых Ргос-объектом; О как определить идентичность двух Ргос-объектов; О чем объекты ргос и lambda отличаются друг от друга. 6.5.1. Создание Ргос-объектов Один из способов создания Ргос-объекта мы уже видели: для этого нужно связать блок с методом, который был определен с использованием блока-аргумента, име- ющего префикс в виде амперсанда. Такому методу ничто не мешает вернуть Ргос- объект за пределы этого метода: # Этот метод создаете ргос из блока def makeproc(&р) # Превращение связанного блока в Ргос # и сохранение его в р р # Возвращение Ргос-объекта end Определив такой вот makeproc метод, можно создать Ргос-объект самостоятельно: adder = makeproc {|х,у| х+у } Теперь переменная adder ссылается на Ргос-объект. Ргос-объекты, созданные таким образом, относятся к ргос, а не к lambda. Для всех Ргос-объектов определен метод call, при вызове которого запускается код, содержащийся в блоке, на основании которого был создан ргос. Например: sum = adder.call (2.2) # => 4 Вдобавок к тому, что они могут быть вызваны, Ргос-объекты могут быть переданы методам, сохранены в структурах данных и подвергнуты иным манипуляциям, как и все другие объекты Ruby. Наряду с созданием ргос путем вызова метода, в Ruby имеется еще три метода, создающих Ргос-объекты (как ргос, так и 1 ambda). Обычно именно они и использу- ются, поэтому необходимость в определении какого-нибудь метода makeproc, напо- добие того, что был показан ранее, отпадает. В дополнение к этим Ргос-создающим
238 Глава 6. Методы, proc- и lambda-объекты и замкнутые выражения методам, Ruby 1.9 также поддерживает новый литеральный синтаксис для опреде- ления lambda-объектов. В следующих подразделах рассматриваются методы Proc, new, lambda и proc, а также объясняется новый литеральный синтаксис для созда- ния lambda-объектов, появившийся в Ruby 1.9. 6.5.1.1. Proc.new В некоторых предыдущих примерах этой главы мы уже сталкивались с приме- нением метода Proc. new. Это обычный new-метод, поддерживаемый большинством классов, и это наиболее понятный способ создания нового экземпляра класса Proc. Метод Proc.new не предполагает использования аргументов и возвращает Proc-объект, относящийся к разновидности proc (но не lambda). Когда Proc.new вы- зывается со связанным блоком, он возвращает proc, представляющий этот блок. Например: р = Proc.new {|х.у| х+у } Если Proc.new вызывается без блока из тела метода, который имеет связанный с ним блок, то он возвращает proc, представляющий блок, связанный с этим охва- тывающим методом. Использование Proc.new таким способом составляет альтер- нативу использованию блока-аргумента, имеющего в определении метода имя с префиксом в виде амперсанда. К примеру, следующие два метода полностью эк- вивалентны: def invoke(&b) b.call end def invoke Proc.new.call end 6.5.1.2. Kernel.lambda Другая технология создания Ргос-объектов связана с применением метода lambda. Этот метод определен в модуле Kernel, поэтому ведет себя как глобальная функ- ция. По его имени можно догадаться, что тип Ргос-объекта, возвращаемого этим методом —lambda, а не proc. Метод lambda не предполагает использования аргумен- тов, но с его вызовом должен быть связан какой-нибудь блок: is_positive = lambda {|х| х > 0 } ПРЕДЫСТОРИЯ LAMBDA Объекты lambda и метод lambda названы так по ассоциации с лямбда- вычислениями, ответвления математической логики, примененного в языках функционального программирования. В Лиспе также используется термин «lambda» для ссылки на функцию, с которой можно работать как с объек- том.
6.5. Ргос и lambda 239 6.5.1.З. Kernel.proc В Ruby 1.8 глобальный метод ргос является синонимом для lambda. Несмотря на такое имя, он возвращает lambda, а не ргос. В Ruby 1.9 эта несуразица устранена; в этой версии языка ргос является синонимом для Proc. new. Из-за этой неоднозначности в коде Ruby 1.8 метод ргос лучше не использовать, поскольку поведение вашего кода может измениться, если интерпретатор будет обновлен до новой версии. Если вы используете код Ruby 1.9 и уверены, что он никогда не будет запущен на интерпретаторе Ruby 1.8, то методом ргос, как более привлекательным сокращенным вариантом для Proc. new, можно пользоваться без опасений. 6.5.1.4. Литералы Lambda В Ruby 1.9 поддерживается совершенно новый синтаксис для определения объ- ектов lambda в виде литералов. Начнем с lambda в Ruby 1.8, созданных с помощью метода 1 ambda: succ = 1 ambda {| х | х+1} В Ruby 1.9 все это можно превратить в литерал, для чего нужно сделать следую- щее. О Заменить имя метода lambda на знаки пунктуации ->. О Переместить список аргументов за пределы фигурных скобок, поместив его перед ними. О Заменить ограничители списка аргументов с 11 на (). Проделав эти манипуляции, мы получим lambda-литерал, используемый в Ruby 1.9: succ = ->(х){ х+1 } Теперь succ содержит Ргос-объект, которым можно воспользоваться как и всеми другими подобными объектами: succ.cal 1 (2) # => 3 Введение этого синтаксиса в Ruby вызвало некоторые споры и требует привы- кания. Заметьте, что символы, составляющие стрелку, отличаются от тех, что ис- пользуются в хэш-литералах. В 1 ambda-литерале используется стрелка, составлен- ная с помощью дефиса, а в хэш-литерале для этого используется знак равенства. Как и в блоках, используемых в Ruby 1.9, список параметров lambda-литерала мо- жет включать объявление локальных переменных блока, которые защищены от переписывания переменными с такими же именами из охватывающей области видимости. Для этого нужно просто продолжить список параметров точкой с за- пятой и списком локальных переменных: # Этот lambda-объект воспринимает 2 аргумента и объявляет 3 локальные переменные f = ->(х.у: i.j.k) { ... }
240 Глава 6. Методы, proc- и lambda-объекты и замкнутые выражения Одним из преимуществ этого нового 1 ambda-синтаксиса по сравнению с традици- онными блоковыми методами создания 1 ambda-объектов является то, что синтак- сис Ruby 1.9 позволяет объявлять lambda-объекты с аргументами по умолчанию, как это делается при объявлении методов: zoom = ->(x,y.factor=2) { [x*factor, y*factor] } Как и при объявлении методов, скобки в 1 ambda-литералах являются необязатель- ными элементами, поскольку список параметров и список локальных переменных полностью отделены друг от друга элементами ->, : и {. Все три ранее представ- ленные 1 ambda-литерала можно переписать, придав им следующий вид: succ = ->х { х+1 } f = -> Х.у: i.j.k { ... } zoom = ->х,у,factor=2 { [x*factor. y*factor] } Конечно же, параметры 1 ambda-литерала и локальные переменные являются нео- бязательными компонентами и могут быть полностью опущены. Самый краткий lambda-литерал, не воспринимающий никаких аргументов и возвращающий nil, выглядит следующим образом: ->{} Одним из преимуществ этого синтаксиса является его компактность. Им можно воспользоваться, когда нужно передать 1 ambda-объект методу в качестве аргумента или передать его в другой 1 ambda-объект: def compose(f.g) # Составление композиции из 2 lambda-объектов ->(х) { f.cal 1(g.cal 1(x)) } end succOfSquare = compose(->x{x+l}. ->x{x*x}) succOfSquare.cal1(4) # => 17: вычисляется (4*4)+l Lambda-литералы создают Proc-объекты, которые нельзя отождествлять с блоками. Если нужно передать 1 ambda-литерал методу, который ожидает блок, перед этим литералом нужно поставить префикс &, как это делается с любым другим Ргос- объектом. Посмотрим, как можно отсортировать массив чисел в убывающем по- рядке, используя и блок, и 1 ambda-литерал: data.sort {|a.b| b-a } # Версия, использующая блок data.sort &->(a,b){ b-a } # Версия, использующая 1ambda-литерал В данном случае очевидно, что обычный синтаксис, в котором используется блок, выглядит проще. 6.5.2. Вызов объектов Proc и Lambda Proc и 1 ambda являются объектами, а не методами, и они не могут быть вызваны тем же способом, что и методы. Если р ссылается на Proc-объект, то р нельзя вы- звать как метод. Но поскольку р — это объект, можно вызвать метод этого объ- екта р. Мы уже упоминали, что в классе Proc определен метод call. Вызов этого
6.5. Proc и lambda 241 метода приводит к выполнению кода исходного блока. Аргументы, переданные методу call, становятся аргументами для блока, а возвращаемое блоком значение становится возвращаемым значением метода call: f= Proc.new {|x.у| 1.0/(1.0/x + 1.0/y) } z = f.cal 1(x.y) В классе Proc определен также оператор доступа к массиву, который работает так же, как метод call. Это означает, что proc или lambda можно вызвать, используя синтаксис, похожий на вызов метода, в котором круглые скобки заменены на ква- дратные. К примеру, показанный выше вызов proc может быть заменен следую- щим кодом: Z = f[Х.у] В Ruby 1.9 дается еще один способ вызова Proc-объекта; в качестве альтернативы квадратным скобкам можно использовать круглые скобки, имеющие в качестве префикса символ точки: Z = f.(х.у) Сочетание . () похоже на вызов метода, у которого пропущено имя метода. Это сочетание не является определяемым оператором, скорее это синтаксическая осо- бенность, вызывающая метод call. Она может быть использована с любым объ- ектом, для которого определен метод call, и не ограничивается рамками исполь- зования Ргос-объектов. 6.5.3. Арность Proc Арность proc или lambda — это количество ожидаемых ими аргументов. (Своим происхождением это слово обязано суффиксу «агу» в таких словах, как unary, binary, ternary и т. д.) В Ргос-объектах определен метод arity, возвращающий ко- личество ожидаемых ими аргументов. Например: lambda] 11} .arity # => 0. Аргументов не ожидается lambda]|х| х}.arity # => 1. Ожидается один аргумент lambda]jx.y| х+у].arity # => 2. Ожидаются два аргумента Путаница с понятием арности возникает, когда Proc воспринимает произволь- ное количество аргументов благодаря завершающему аргументу с префиксом- звездочкой (*). Когда Proc допускает использование необязательных аргументов, метод arity возвращает отрицательное число в форме -п-1. Эта форма возвращае- мого значения показывает, что Ргос требует использования п аргументов, но может также дополнительно принять и необязательные аргументы. Выражение -п-1 из- вестно как дополнение до п, и его можно инвертировать с использованием опера- тора -. Поэтому если а г 1 ty возвращает отрицательное число т, то ~т (или -т-1) дает количество обязательных аргументов: lambda ] |*args|} .arity # => -1. —1 = -(-l)-l = 0 обязательных аргументов lambda ] jfirst, *rest|} .arity # => -2. —2 = -(-2)-l = 1 обязательный аргумент
242 Глава 6. Методы, ргос- и lambda-объекты и замкнутые выражения И еще одно полезное замечание по использованию метода arity. В Ruby 1.8 Ргос- объект, объявленный вообще без параметров (то есть без использования симво- лов 11) может быть вызван с любым количеством аргументов (и эти аргументы будут проигнорированы). Метод arity возвращает -1, чтобы показать, что обяза- тельных аргументов нет. Но в Ruby 1.9 ситуация изменилась: объявленный таким образом Ргос-объект имеет арность, равную 0. Если это 1 ambda-объект, то его вызов с любым количеством аргументов будет ошибочным: puts lambda {}.arity # -1 в Ruby 1.8: 0 в Ruby 1.9 6.5.4. Идентичность Ргос-объектов В классе Ргос определен метод ==, позволяющий определить, являются ли два Ргос- объекта идентичными. Но при этом важно понять, что одного лишь совпадения исходного кода еще недостаточно, чтобы два ргос- или lambda-объекта считались идентичными: lambda {|х| х*х } == lambda {|х| х*х } # => false Метод == возвращает true лишь в том случае, если один Ргос-объект является кло- ном или дубликатом другого такого же объекта: р = lambda {|х| х*х } q = p.dup р == q # => true: два ргос-объекта идентичны p.objectjd == q.object_id # => false: они не представляют один и тот же объект 6.5.5. Чем lambda-объекты отличаются от ргос-объектов Объект ргос является блоком в форме объекта, и он ведет себя как блок А поведе- ние lambda-объекта слегка изменено, он ведет себя скорее как метод, а не как блок. Вызов ргос-объекта похож на передачу управления блоку, в то время как вызов 1 ambda-объекта похож на вызов метода. В Ruby 1.9 с помощью метода 1 ambda? можно определить, к какой разновидности относится Ргос-объект, к ргос или к lambda. Этот предикат возвращает true для 1 ambda-объектов и fа 1 se — для ргос. В следующих под- разделах различия между ргос- и 1 ambda-объектами рассмотрены более подробно. 6.5.5.1. Инструкция return в блоках, ргос- и lambda-объектах Вернемся к материалам главы 5 и вспомним, что инструкция return осуществляет возвращение из лексически охватывающего метода, даже если инструкция содер- жится внутри блока. Инструкция return, размещенная в блоке, не осуществляет
6.5. Proc и lambda 243 возвращение из блока к вызвавшему его итератору, она вызывает возвращение из метода, который вызвал итератор. Например: def test puts "вход в метод" 1.times { puts "вход в блок"; return } # Осуществляет выход из метода test puts "выход из метода” # Эта строка никогда не выполняется end test Объект proc похож на блок, поэтому если вызвать proc, в котором выполняется инструкция return, он предпримет попытку осуществить возвращение из метода, охватывающего блок, который был преобразован в ргос-объект. Например: def test puts "вход в метод" р = Proc.new { puts "вход в proc"; return } p.call # Вызов proc приводит к возвращению # из метода puts "выход из метода" # Эта строка никогда не выполняется end test Но при использовании инструкции return в ргос-объекте возникают определен- ные трудности, поскольку ргос-объекты часто передаются от метода к методу. На момент вызова ргос-объекта возвращение из лексически охватывающего метода могло уже состояться: def procBuilder(message) # Создание и возвращение ргос- # объекта Proc.new { puts message: return } # return приводит к возвращению из # procBullder # Но возвращение из procBullder уже состоялось в этом месте! end def test puts "вход в метод" р = procBuilder("Bxofl в proc") p.call # Выводится "вход в proc" # и выдается ошибка LocalJumpError! puts "выход из метода" # Эта строка никогда не выполняется end test Превращая блок в объект, мы также получаем возможность передавать этот объ- ект от метода к методу и использовать его «вне контекста». Поступая таким обра- зом, мы рискуем «вернуться» из метода, возвращение из которого уже произошло, что, собственно, и демонстрирует рассматриваемый пример. В подобных случаях Ruby выдает ошибку Local JumpError. Чтобы внести исправления в этот придуманный нами пример, нужно, конеч- но же, убрать совершенно лишнюю инструкцию return. Но return не всегда бывает
244 Глава 6. Методы, proc- и lambda-объекты и замкнутые выражения лишней, и положение можно исправить другим образом, воспользовавшись не proc-, а 1 ambda-объектом. Как мы уже выяснили, 1 ambda-объекты больше похожи на методы, чем на блоки. Поэтому инструкция return, размещенная в 1 ambda-объекте, осуществляет возвращение из самого lambda-объекта, а не из метода, который окружает то место, где он был создан: def test puts "вход в метод" р = lambda { puts "вход в lambda": return } p.call # Вызов lambda не приводит # к возвращению из метода puts "выход из метода" # Теперь эта строка Непременно* # выполняется end test Поскольку инструкция return в lambda-объекте осуществляет возвращение только из самого lambda-объекта, волнения по поводу возникновения ошибки Local JumpError уже позади: def lambdaBuiIder(message) # Создание и возвращение lambda lambda { puts message: return } end # return осуществляет возвращение # из lambda def test puts "вход в метод” 1 = lambdaBuilderC’Bxofl в lambda") 1.call # Выводится "вход в lambda" puts "выход из метода" end # Это выполняемая строка test Б.5.5.2. Break в блоках, proc- и lambda-объектах На рис. 5.3 показано поведение инструкции break в блоке; она вызывает возвра- щение из блока в его итератор, а затем возвращение из итератора в метод, кото- рый его вызвал. Поскольку ргос-объекты работают как блоки, можно ожидать, что break делает то же самое и в ргос-объекте. Но проверить это непросто. Когда ргос- объект создается с помощью метода Proc. new, то Proc. new является тем самым ите- ратором, из которого break осуществит возвращение. Но к тому времени, когда мы можем вызвать ргос-объект, выход из итератора уже будет осуществлен. Поэтому нет никакого смысла помещать инструкцию break, относящуюся к более высокому уровню, в ргос-объект, создаваемый с помощью метода Proc.new: def test puts "вход в метод test” proc = Proc.new { puts "вход в proc": break } proc.call # LocalJumpError: возвращение из # итератора уже произошло
6.5. Ргос и lambda 245 puts "выход из метода test" end test Если ргос-объект создан с &-аргументом для метода-итератора, то его можно вы- звать и заставить итератор осуществить возвращение управления: def iterator(&proc) puts "вход в итератор" ргос.call # Вызов ргос puts "выход из итератора" # Никогда не выполняется, если ргос # выполняет break end def test iterator { puts "вход в proc": break } end test Lambda-объекты похожи на методы, поэтому размещение инструкции break в вы- сокоуровневом lambda-объекте без охватывающего цикла или итератора, работу которых нужно прервать, не имеет никакого практического смысла! Скорее всего, следующий код потерпит неудачу, потому что в 1 ambda-объекте нечего прерывать. Фактически высокоуровневая инструкция break будет работать как инструкция return: def test puts "вход в метод test" lambda = lambda { puts "вход в lambda": break: puts "выход из lambda" } lambda.call puts "выход из метода test" end test 6.5.5.3. Другие инструкции управления ходом выполнения программы Высокоуровневая инструкция next работает в блоке, ргос- или 1 ambda-объекте со- вершенно одинаково: она заставляет инструкцию yield или метод call, которые вызвали блок, ргос- или 1 ambda-объект, вернуть управление. Если за next следует выражение, тогда значение этого выражения становится возвращаемым значением блока, ргос- или 1 ambda-объекта. Инструкция redo также работает в ргос- и 1 ambda-объектах одинаково: она передает управление в начало ргос- или 1 ambda-объекта. Применение инструкции retry в ргос- и 1 ambda-объектах запрещено: ее использова- ние всегда вызывает выдачу ошибки Local JumpError. Инструкция raise ведет себя в блоках, ргос- и 1 ambda-объектах совершенно одинаково. Исключения всегда рас- пространяются вверх по стеку вызовов. Если блок, ргос- или 1 ambda-объект выдает исключение, а локальное предложение rescue отсутствует, исключение сначала
246 Глава 6. Методы, proc- и lambda-объекты и замкнутые выражения распространяется на метод, который вызвал блок с помощью инструкции yield или вызвал proc- или 1 ambda-объектс помощью инструкции call. Б.5.5.4. Аргументы, передаваемые ргос- и lambda-объектам Вызов блока с помощью инструкции yi el d похож на вызов метода, но все-таки это не одно и то же. Разница заключается в том способе, которым при вызове значе- ния аргументов присваиваются тем переменным параметров, которые были объ- явлены в блоке или методе. Инструкция yield использует семантику передачи, а вызов метода использует семантику вызова. Семантика передачи аналогична параллельному присваиванию и рассмотрена в разделе 5.4.4. Можно догадаться, что вызов ргос-объекта использует семантику передачи, а вызов 1 ambda- объекта использует семантику вызова: р = Proc.new {|х,у| print х.у } p.call(1) # х,у=1: nil используется для пропущенного # г-значения: Выводится lnil p.cal1(1,2) # х,у=1,2: 2 1-значения. 2 г-значения: Выводится # 12 р.cal 1(1,2.3) # х,у=1,2,3: лишнее г-значение отбрасывается: # Выводится 12 p.call ([1,2]) #х,у=[1,2]: массив автоматически # распаковывается: Выводится 12 Этот код показывает, что имеющийся в proc-объекте метод cal 1 обрабатывает по- лученные аргументы, проявляя при этом некоторую гибкость: молча отбрасывает лишнее, молча добавляет ni 1 для опущенных аргументов и даже занимается рас- паковкой массивов. (Или делает то, что здесь не показано, — занимается упаков- кой нескольких аргументов в один массив, когда proc-объект ожидает только один аргумент.) Lambda-объекты подобной гибкостью не обладают; как и методы они должны быть вызваны с точным количеством аргументов, объявленном при их создании: 1 = lambda {|х.у| print х.у } 1.call(1.2) 1.call(1) 1.call(1,2.3) 1.cal 1([1.2]) 1 .call (*[1.2]) # Эта строка работает # Неверное количество аргументов # Неверное количество аргументов # Неверное количество аргументов # Работает: есть явное указание (*) # на распаковку массива 6.6. Замкнутые выражения В языке Ruby proc- и 1 ambda-объекты являются замкнутыми выражениями. Тер- мин «замкнутое выражение» появился на заре информатики; он относится как
б.б. Замкнутые выражения 247 к функции, которую можно вызвать, так и к переменной, которая связана с этой функцией. При создании ргос- и lambda-объектов получившийся Ргос-объект со- держит не только выполняемый блок, но также и привязки ко всем переменным, которые используются блоком. Как известно, блоки могут использовать локальные переменные и параметры ме- тода, определенные за пределами этого блока. К примеру, в следующем коде блок, связанный с итератором col 1 ect, использует параметр метода п: # умножение каждого элемента массива данных на п def multi ply (data, n) data.collect {|x| x*n } end puts multiply([1.2,3]. 2) # Выводится 2,4,6 Но еще интереснее, или даже удивительнее то, что если блок был превращен в ргос- или 1 ambd а-объект, он может иметь доступ к п даже после того, как из это- го метода, чьим параметром была эта переменная, уже был осуществлен возврат управления. Это может быть продемонстрировано в следующем коде: # Возвращение lambda-объекта, который удерживает или "замыкает” параметр п def multipl 1ег(п) lambda {|data| data.collect{|х| x*n } } end doubler = multiplier(2) # Получение lambda-объекта, который знает, как # удвоить число puts doubler.call([1,2,3]) # Выводится 2,4,6 Метод multiplier возвращает lambda-объект. Поскольку этот lambda-объект ис- пользуется за пределами области определения переменных, в которой он сам был определен, мы называем его замкнутым выражением; он инкапсулирует или «за- мыкает» (или просто удерживает) привязку к параметру п того метода, в котором он был определен. 6.6.1. Замкнутые выражения и совместно используемые переменные Важно понять, что замкнутые выражения не только сохраняют значения перемен- ных, на которые они ссылаются, — они сохраняют сами переменные и продлевают время их существования. Иными словами, переменные, используемые в lambda- или ргос-объектах, не приобретают статическое связывание при создании этих объектов. Напротив, связывание является динамическим, и значения переменных выискиваются при задействовании 1 ambda- или ргос-объектов. В качестве примера в следующем коде определяется метод, возвращающий два lambda-объекта. Поскольку эти объекты определены в одной и той же области ви- димости переменных, они имеют совместный доступ к переменным в этой обла- сти. Когда один 1 ambda-объект изменяет значение совместно используемой пере- менной, то новое значение становится доступным другому 1 ambda-объекту:
248 Глава 6. Методы, ргос- и lambda-объекты и замкнутые выражения # Возвращение пары lambda-объектов, использущей # совместный доступ к локальной переменной. def accessor_pair(initialValue=nil) value = initial Value # Локальная переменная, совместно используемая # возвращаемыми lambda. getter = lambda { value } # Возвращение значения локальной переменной. setter = lambda {|х| value = х } # Изменение значения локальной переменной. return getter.setter # Возвращение пары lambda-объектов вызывающей # программе. end getX, setX = accessor_pair(0) puts getX[] setXClO] puts getX[] # Создание доступа lambda-объектов к исходному # значению 0. # Выводится 0. Обратите внимание на квадратные # скобки, используемые # взамен метода call. # Изменение значения посредством одного # замкнутого выражения. # Выводится 10. Изменения видны при использовании # другого объекта. Тот факт, что 1 ambda-объекты, созданные в единой области определения перемен- ных, имеют совместный доступ к переменным, является свойством, способствую- щим возникновению ошибок, или причиной их возникновения. Когда есть метод, возвращающий более одного замкнутого выражения, всегда нужно обращать осо- бое внимание на переменные, используемые этими выражениями. Рассмотрим следующий код: # Возвращение массива, состоящего из lambda-объектов, перемножающих аргументы def multipliers(*args) x = nil args.map {|x| lambda {|y| x*y }} end double, triple = multipliers(2,3) puts double.cal1(2) # В Ruby l.B выводится 6 Метод multipliers использует итератор map и блок, чтобы возвратить массив, со- стоящий из lambda-объектов (создаваемых внутри блока). В Ruby 1.8 блок аргу- ментов не всегда является локальным по отношению к блоку (о чем говорилось в разделе 5.4.3), и поэтому все создаваемые 1 ambda-объекты оказываются в ситуа- ции, при которой они используют совместный доступ к переменной х, которая яв- ляется локальной переменной метода mul t i pl iers. Как уже говорилось, замкнутые выражения не захватывают текущее значение переменной: они захватывают саму переменную. Все созданные здесь 1 ambda-объекты используют переменную х со- вместно. Эта переменная имеет только одно значение, и все возвращенные 1 ambda- объекты используют одно и то же значение. Именно поэтому 1 ambda-объект, кото- рому мы дали имя double, в конечном итоге утраивает значение своего аргумента, вместо того чтобы его удваивать. Именно для этого кода проблема исчерпывается с переходом на Ruby 1.9, посколь- ку аргументы блока в этой версии языка всегда локальны по отношению к блоку.
6.6. Замкнутые выражения 249 Но все же стоит быть всегда начеку, создавая lambda-объекты внутри цикла и ис- пользуя переменные цикла (такие как индекс массива) внутри lambda-объектов. 6.6.2. Замкнутые выражения и связывания В классе Ргос определен метод по имени binding (связывание). При вызове этого метода в отношении ргос- или lambda-объекта возвращается Binding-объект, пред- ставляющий связывания, действующие для этого замкнутого выражения. ПОДРОБНЕЕ О СВЯЗЫВАНИЯХ Мы рассматривали связывания замкнутых выражений, как будто это простое отображение имен переменных на значения этих переменных. На самом деле связывания затрагивают не только переменные. Они удерживают всю инфор- мацию, необходимую для выполнения метода, такую как значение self, и блок, если таковой имеется, который будет вызван с помощью инструкции yield. Сам по себе Bi ndi ng-объект не имеет каких-либо интересующих нас методов, но он может быть использован в качестве второго аргумента глобальной функции eval (рассматриваемой в разделе 8.2), предоставляя содержимое, на основе которого вычисляется строка кода Ruby. В Ruby 1.9 для Binding-объекта определен свой собственный метод eval, которому можно отдать предпочтение. (Для более под- робного изучения Kernel.eval и Binding.eval стоит воспользоваться инструмен- тальным средством ri.) Использование объекта Binding и метода eval открывает для нас лазейку, сквозь которую можно манипулировать поведением замкнутого выражения. Взглянем еще раз на ранее представленный код: # Возвращение lambda-объекта, который удерживает или "замыкает" параметр п def multiplier(n) lambda {|data| data.collect{|x| x*n } } end doubler = multiplier!?) # Получение lambda-объекта, который знает, # как удвоить число puts doubler.call([1,2.3]) # Выводится 2,4,6 Теперь предположим, что нужно изменить поведение doubler: eval("n=3". doubler.binding) # Или doubler.binding.eval("n=3") # в Ruby 1.9 puts doubler.call(Cl.2,3]) # Теперь этот код выводит 3,6.9! В качестве сокращенного варианта метод eval позволяет передавать Ргос-объект на- прямую, вместо того чтобы передавать Binding-объект, относящийся к Ргос. Поэто- му мы можем заменить вызов метода eval, показанный выше, следующим кодом: eval("n=3". doubler)
250 Глава 6. Методы, ргос- и lambda-объекты и замкнутые выражения Связывание — это свойство не только замкнутых выражений. Метод Kernel. Ы nd 1 ng возвращает Binding-объект, который представляет действующие связывания в том месте, из которого он был вызван. 6.7. Объекты класса Method Методы и блоки в Ruby — это выполняемые конструкции языка, но не объекты. Ргос и lambda являются объектными версиями блоков; они могут быть выполне- ны, а также ими можно манипулировать как данными. Ruby обладает мощными возможностями для метапрограммирования (или отражения), и методы, как ни странно, могут быть представлены как экземпляры класса Method. (Метапрограм- мирование рассматривается в главе 8, но объекты класса Method будут представ- лены в этой главе.) Следует учесть, что вызов метода через объект класса Method менее эффективен, чем его вызов, осуществленный напрямую. Method-объекты ис- пользуются намного реже, чем lambda- и proc-объекты. В классе Object определен метод по имени method. При передаче ему имени метода в виде строки или обозна- чения он возвращает Method-объект, представляющий указанный для получателя метод (или выдает ошибку NameError, если метода с таким именем не существует). Например: m = 0.method(:succ) # Method-объект представляет метод succ. вызываемый # в отношении числа 0. # которое относится к классу Fixnum В Ruby 1.9 для получения Method-объекта можно также воспользоваться методом publ icjnethod. Он работает так же как и method, но игнорирует защищенные и за- крытые методы (рассматриваемые в разделе 7.2). Класс Method не является подклассом Ргос, но ведет себя во многом так же, как и он. Method-объекты, так же, как и Proc-объекты, вызываются с помощью метода call (или оператора []). А метод arity, определенный в классе Method, похож на метод arity, определенный в классе Ргос. Для вызова Method-объекта m используется сле- дующий синтаксис: puts m.call # То же самое, что и puts O.succ. # Или использование puts m[]. Вызов метода через Method-объект не изменяет семантику вызова и не изменяет значений инструкций, управляющих ходом выполнения программы, таких как return и break. Метод call объекта Method использует семантику вызова метода, а не семантику передачи управления. Поэтому поведение Method-объектов более похоже на поведение lambda-, а не ргос-объектов. В работе Method-объекты очень похожи на Ргос-объекты и зачастую могут исполь- зоваться вместо них. Когда требуется именно Ргос-объект, можно воспользовать- ся методом Method .to_proc и преобразовать Method-объект в Ргос-объект. Поэтому с Method-объектами можно использовать префикс в виде амперсанда и передавать их методам вместо блоков. Например:
6.7. Объекты класса Method 251 def square(x); x*x; end puts (1. . 10) .map(&method(:square)) ОПРЕДЕЛЕНИЕ МЕТОДОВ С ПОМОЩЬЮ PROC-ОБЪЕКТОВ В дополнение к получению Method-объекта, представляющего метод, и пре- образования его в Ргос, мы можем пойти и в другом направлении. Метод define_method (класса Module) в качестве аргумента ожидает обозначение и создает метод с указанным в нем именем, используя связанный с ним блок в качестве тела метода. Вместо использования блока, в качестве второго ар- гумента можно также передать Ргос- или Method-объект. Одно существенное различие между Method- и Proc-объектами заключается в том, что Method-объекты не являются замкнутыми выражениями. Подразумевается, что методы Ruby являются абсолютно самодостаточными, и они никогда не имеют до- ступ к локальным переменным за пределами своей собственной области опреде- ления. Поэтому единственным связыванием, сохраненным объектом Method, явля- ется значение sei f — объекта, для которого был вызван метод. В Ruby 1.9, в классе Method, определены три метода, которые недоступны в вер- сии 1.8: метод name возвращает имя нашего метода в виде строки, метод owner воз- вращает класс, в котором наш метод был определен, и метод receiver возвраща- ет объект, с которым связан наш метод. Для любого объекта метода m значение п.receiver.class должно указывать на класс или подкласс того класса, который возвращается вызовом метода m. owner. 6.7.1. Несвязанные объекты метода В дополнение классу Method в Ruby также определен класс UnboundMethod (несвя- занный метод). Как следует из этого имени, UnboundMethod-объект представляет ме- тод, не имеющий связи с объектом, в отношении которого он должен быть вызван. Поскольку UnboundMethod-объект является несвязанным, он не может быть вызван, и в классе UnboundMethod не определен метод call или метод []. Для получения UnboundMethod-объекта используется метод 1 nstancejnethod любого класса или модуля: unbound_plus = Fixnum.instance_method("+") В Ruby 1.9 для получения UnboundMethod-объекта можно также воспользоваться методом publ i c_i nstancejnethod. Он работает так же, как и i nstancejnethod, но игно- рирует защищенные и закрытые методы (рассматриваемые в разделе 7.2). Для вызова несвязанного метода сначала его нужно связать с объектом, используя метод bind: plus_2 = unboundjjlus.bind(2) # Связывание метода с объектом 2
252 Глава 6. Методы, ргос- и lambda-объекты и замкнутые выражения Метод bind возвращает Method-объект, который может быть вызван с помощью своего метода call: sum = plus_2.call(2) # => 4 Другой способ получения UnboundMethod-объекта заключается в использовании ме- тода unbind, который определен в классе Method: plus_3 = plus_2.unbind.bind(3) В Ruby 1.9 для объекта UnboundMethod определены методы name и owner, которые ра- ботают точно так же, как и их аналоги в классе Method. 6.8. Функциональное программирование Ruby не является функциональным языком программирования в том смысле, в котором к этому типу языков относятся Лисп и Haskell, но имеющиеся в Ruby блоки, ргос- и lambda-объекты неплохо вписываются в функциональный стиль программирования. При использовании блока с итератором из модуля Enumerable, таким как тар или i nject, вы осуществляете программирование в функциональном стиле. Приведем пример использования тар- и inject-итераторов: # Вычисление среднего и среднеквадратического отклонения массива чисел mean = a.inject {|х,у| х+у } / a.size sumOfSquares = a.map{|х| (x-mean)**2 }.inject{|х,у| х+у } standardDeviation = Math.sqrt(sumOfSquares/(a.size-1)) Если вас привлекает стиль функционального программирования, то к встроен- ным в Ruby классам совсем не трудно добавить свойства, облегчающие работу в этом стиле. В остальной части главы исследуется ряд возможностей для работы с функциями. Код в этом разделе встречается довольно часто, и он представлен для расширения кругозора, а не в качестве рекомендаций по совершенствованию стиля программирования. В частности, представленное в коде следующего раз- дела очень частое переопределение операторов, скорее всего, приведет к созданию таких программ, которые будут с трудом читаться и обслуживаться другими спе- циалистами. Этот материал несколько опережает события, и код, который будет в нем пред- ставлен, предполагает знакомство с главой 7. Поэтому при первом прочтении кни- ги остаток этой главы можно пропустить. 6.8.1. Применение функции к перечисляемым объектам Итераторы тар и inject относятся к двум наиболее важным итераторам из всех, что определены в модуле Enumerable. Каждый из них предполагает использова- ние блока. Если нужно написать программу с функциональным уклоном, то нам
6.8. Функциональное программирование 253 подошли бы методы, имеющие соответствующие функции, которые позволили бы применить эти функции к указанному перечисляемому объекту: # Этот модуль определяет методы и операторы для функционального программирования, module Functional # Применение этой функции к каждому элементу указанного перечисляемого # объекта, приведет к возвращению # массива результатов. Это измененный варант Enumerable.тар. # Символ | используется как псевдоним оператора. Читайте "|" как "касательно" # или "применительно к". # # Пример: # а = [[1.2].[3.4]] # sum = lambda {|х.у| х+у} # sums = sum|а # => [3,7] def apply(enum) enum.map &self end alias | apply # Использование этой функции "сокращает" перечисляемый объект до размера # одного элемента. # Это измененный вариант Enumerable.inject. # В качестве псевдонима оператора используется группа символов <=. # Мнемоническое правило: оператор <= похож на иглу для инъекций # Пример: # data = [1,2,3.4] # sum = lambda {|х.у| х+у} # total = sum<=data # => 10 def reduce(enum) enum.inject &self end alias <= reduce end # Добавление этих методов функционального программирования к классам Ргос # и Method. class Proc: include Functional: end class Method: include Functional: end Обратите внимание на то, что мы определили методы в модуле по имени Funet i опа 1, азатем мы включили этот модуль в классы Ргос и Method. Таким образом, apply и reduce работают как для Ргос-, так и для Method-объектов. Многие из представ- ленных далее методов также являются методами, определенными в этом модуле Functional, поэтому они работают как для Ргос-, так и для Method-объектов. Используя определенные выше функции apply и reduce, мы можем переделать наши статистические вычисления: sum = lambda {|х.у| х+у } # Функция для сложения двух чисел mean = (sum<=a)/а.size # Или sum.reduce(a), или a.inject(&sum) продолжение &
254 Глава 6. Методы, ргос- и lambda-объекты и замкнутые выражения deviation = lambda {|х| x-mean } # Функция для вычисления отклонения от mean square = lambda {|х| х*х } # Функция для вычисления квадрата числа standardDevi at ion = Math.sqrt((sum<=square|(devi ati on|a))/(a.si ze-1)) Заметьте, что последняя строка имеет очень компактный формат, а все нестан- дартные операторы затрудняют ее чтение. Также обратите внимание на то, что опе- ратор | определяет взаимосвязанность операндов слева направо, даже когда мы определяем его самостоятельно. Поэтому синтаксис использования нескольких функций в отношении перечисляемых объектов требует применения скобок. Именно поэтому нужно было написать square | (devi ati on | а), а не squa re | devi ati on | a. 6.8.2. Составление функций Если есть две функции f и д, то порой возникает необходимость определить но- вую функцию h, которая представляет собой сочетание f(g()), или f в сборе eg. Для автоматического составления функциональных композиций можно написать следующий метод: module Functional # Возвращение нового lambda-объекта, вычисляющего self[f[args]]. # Использование * в качестве оператора-псевдонима для compose. # Примеры, в которых псевдоним * используется для этого метода. # # f = 1ambda {|х| х*х } # g = lambda {|х| х+1 } # (f*g)[2] # => 9 # (g*f)[2] # => 5 # # def polar(x.y) # [Math.hypot(y.x), Math.atan2(y,x)] # end # def cartesian(magnitude. angle) # [magnitude*Math.cos(angle), magnitude*Math,sin(angle)] # end # p.c - method :polar, method :cartesian # (c*p)[3,4] # => [3,4] # def compose(f) if seif.respond_to?(:arity) && self.arity -- 1 lambda {|*args| seif[f[*args]] } else lambda {|*args| self[*f[*args]] } end end # звездочка (*) - вполне естественный оператор для составления функциональной композиции. alias * compose end
6.8. Функциональное программирование 255 В примерах кода, приведенных в комментариях, демонстрируется использование compose с Method-объектами и с 1 ambda-объектами. Новым оператором для составле- ния функциональных композиций (*) можно воспользоваться и для незначитель- ного упрощения наших вычислений среднеквадратического отклонения. Если воспользоваться теми же определениями lambda-объектов sum, square и deviation, можно придать вычислению следующий вид: standardDev i ati on = Math. sqrt ((sum<=square*devi ati on |a)/(a.size-1)) Отличие в том, что перед применением square и deviation к массиву а из них со- ставляется единая функция. 6.8.3. Частично применяемые функции В функциональном программировании частичное применение представляет со- бой процесс, при котором берется функция и неполный набор значений аргумен- тов и создается новая функция, являющаяся аналогом исходной функции с зара- нее установленными определенными аргументами. Например: product = lambda {|х, у| х*у } # Функция двух аргументов (вычисление # произведения) double = lambda {|х| products.х) } # Применение одного аргумента # (вычисление удвоения) Частичное применение может быть упрощено с помощью соответствующих мето- дов (и операторов), определяемых в нашем модуле Functional: module Functional # # Возвращение lambda-эквивалента этой функции с применением одного или более # начальных аргументов. # Когда определен только один аргумент, проще будет воспользоваться # псевдонимом ». # Пример: # product = lambda {|х,у| х*у} # doubler - lambda » 2 # def apply_head(*first) lambda {|*rest| self[*first.concat(rest)]} end # # Возвращение lambda-эквивалента этой функции с применением одного или более # конечных аргументов. # Когда определен только один аргумент, проще будет воспользоваться # псевдонимом «. # Пример: # difference - lambda {|х,у| х-у } # decrement = difference « 1 # def apply_tail(*last) продолжение &
256 Глава 6. Методы, ргос- и lambda-объекты и замкнутые выражения lambda {|*rest| seif[*rest.concat(1ast)]} end # Это альтернативы методов в виде операторов. Угловые скобки указывают # сторону, в которую вставляется аргумент. alias » apply_head # g = f » 2 -- установка первого аргумента в 2 alias « apply_tail # g = f « 2 -- установка последнего аргумента в 2 end Используя эти методы и операторы, можно определить нашу функцию doubl е про- сто как product»2. Частичное применение можно использовать для превращения нашего вычисления отклонения в нечто более абстрактное, путем построения на- шей функции devi ati on из более универсальной функции di fference: difference = lambda {|x,y| x-y } # Вычисление разницы двух чисел deviation = difference«mean # Приложение второго аргумента 6.8.4. Функции, обладающие мемоизацией Мемоизация — это термин функционального программирования, применяемый в отношении кэширования результатов вызова функции. Если функция при пе- редаче ей одних и тех же аргументов всегда возвращает одно и то же значение, есть смысл полагать, что такие же аргументы будут использованы неоднократно, и если вычисление осуществляется с затратой каких-либо существенных вычис- лительных ресурсов, мемоизация может оказаться весьма полезной оптимизаци- ей. Автоматизировать мемоизацию для Ргос- и Method-объектов можно с помощью следующего метода: module Functional # # Возвращение нового lambda-объекта, который кэширует результаты этой функции # и вызывает функцию # только в том случае, если предоставлены новые аргументы. # def memoize cache = {} # Пустой кэш. lambda захватывает его в свое замкнутое # пространство. lambda {|*args| # Заметьте, что ключ хэша - это сплошной массив аргументов! unless cache.has_key?(args) # Если результата для этих # аргументов в кэше нет, cacheEargs] = self[*args] # вычисление и кэширование # результата end cacheEargs] # Возвращение результата из кэша } end # Унарный оператор + (который, возможно, и не понадобится), применяемый для # мемоизации
6.8. Функциональное программирование 257 # Мнемоническое правило: Оператор + означает "улучшенный" alias +@ memoize # cached_f = +f end Посмотрим, как можно воспользоваться методом memoize или унарным операто- ром +: # Мемоизация рекурсивной функции вычисления факториала factorial = lambda {|х| return 1 if х==0; x*factorial[х-1]; }.tnemoize # Или использование синтаксиса с унарным оператором factorial = +lambda {|х| return 1 if х==0; x*factorial[x-l]; } Следует заметить, что представленная здесь функция факториала является ре- курсивной. Она вызывает мемоизированную функцию самой себя, которая осу- ществляет оптимальное кэширование. Она не стала бы работать точно так же, если сначала была бы определена рекурсивная немемоизированная версия функции, а затем определена отличная от нее мемоизированная версия: factorial = lambda {|х| return 1 if х==0; x*factorial[х-1]; } cached_factori al = +factorial # Рекурсивные вызовы # не кэшируются! 6.8.5. Классы Symbol, Method и Proc Между классами Symbol, Method и Ргос существует довольно тесное родство. Мы уже познакомились с методом method, который воспринимает аргумент в виде обо- значения (относящегося к классу Symbol) и возвращает Method-объект. В Ruby 1.9 к классу Symbol добавлен весьма полезный метод to_proc. Он дает возможность обозначению иметь префикс & и передаваться итератору в качестве блока. При этом предполагается, что в обозначении указывается имя метода. Когда вызыва- ется Ргос-объект, созданный с использованием метода to_proc, он вызывает метод, указанный в его первом аргументе, передавая этому методу любое количество оставшихся аргументов. Посмотрим, как этим можно воспользоваться: #Приращение значений элементов массива с помощью метода Fixnum.succ [1.2,3] ,map(&: succ) #=>[2,3.4] Без использования метода Symbol ,to_proc пришлось бы изъясниться более про- странно: [1,2,3].m ap {|n| n.succ } Метод Symbol ,to_proc первоначально разрабатывался как расширение для Ruby 1.8 и, как правило, реализовывался в следующем виде: class Symbol def to_proc lambda {[receiver, *args| receiver.sendfself, *args)}
258 Глава 6. Методы, ргос- и lambda-объекты и замкнутые выражения В этой реализации для вызова метода, указанного с помощью обозначения, ис- пользуется метод send (рассмотренный в разделе 8.4.3). Но это можно сделать и следующим образом: class Symbol def to_proc lambda {|receiver, *args| receiver.method(self)[*args]} end end Вдобавок к методу to_proc можно определить некоторые родственные и, возмож- но, весьма полезные утилиты. Начнем с класса Module: class Module # Доступ к методу экземпляра с помощью нотации, употребляемой для массивов. # Возвращается UnboundMethod. alias [] instancejnethod end Здесь просто определяется сокращенная запись для метода i nstance method, опре- деленного в классе Module. Вспомним, что этот метод возвращает объект, пред- ставляющий не связанный с каким-либо объектом метод — UnboundMethod-объект, который не может быть вызван до тех пор, пока не будет связан с конкретным экземпляром своего класса. Посмотрим на примере, как используется эта новая нотация (обратите внимание на привлекательность индексирования класса име- нами его методов!): Stringt:reverse].bind("hel1 о").cal 1 # => "olleh" Связывание несвязанного метода может быть также упрощено с помощью при- мерно такого же синтаксического приема: class UnboundMethod # Позволяет использовать [] в качестве альтернативы методу bind. alias [] bind end Имея такой псевдоним и используя уже существующий псевдоним [] для вызова метода, можно придать этому коду следующий вид: String[:reverse]["hel1 о"][] # => "olleh" Первая пара скобок индексирует метод, вторая пара его связывает, а третья — вы- зывает. Затем, если мы собираемся использовать оператор [] для поиска методов экзем- пляров класса, то, наверное, следует присмотреться к использованию []= для определения методов экземпляра: class Module # Определение метода экземпляра с именем sym и телом f. # Пример: String[:backwards] = lambda { reverse } def []-(sym, f)
6.8. Функциональное программирование 259 self.instance_eval { define_method(sym, f) } end end Определение этого оператора []= может вызвать путаницу — это будет уже расши- ренный Ruby. Метод def 1 nejnethod является закрытым методом класса Module. Мы воспользовались методом 1nstance_eval (публичным методом класса Object) для запуска блока (включающего вызов закрытого метода), как будто он был внутри модуля, в котором был определен метод. С методами 1nstance_eval и def 1 nejnethod мы еще встретимся в главе 8. Воспользуемся этим новым оператором []= для определения нового метода Enume- rable.average: Enumerable!]: average] = lambda do sum. n = 0.0, 0 self.each {|x| sum += x; n += 1 } if n == 0 nil else sum/n end end Здесь мы воспользовались операторами [] и []= для получения и установки ме- тодов экземпляра класса или модуля. Что-либо подобное можно сделать и для синглтон-методов объекта (к которым относятся методы класса, принадлежащие классу или модулю). Синглтон-метод может быть у любого объекта, но вряд ли есть смысл определять оператор [] для класса Object, поскольку этот оператор определен во многих подклассах. Поэтому для синглтон-методов мы можем найти другой подход и определить операторы в классе Symbol: # # Добавление к классу Symbol операторов [] и []= для установки синглтон-методов f объектов и для доступа к ним # Читайте : как "метод", а [] как "принадлежащий". # Таким образом :т[о] читается как "метод т, принадлежащий о”. # class Symbol # Возвращение принадлежащего объекту метода, имя которого указано в данном # обозначении. Это может # быть синглтон-метод, принадлежащий объекту (такой как метод класса), или метод экземпляра. # определенный с помощью объект.класс или унаследованный из надкласса. # Примеры: creator = :new[Object] # Метод класса Object.new doubler = :*[2] # Метод *. принадлежащий Fixnum 2 [](obj) obj.method(self) продолжение & # # # def end
260 Глава 6. Методы, ргос- и lambda-объекты и замкнутые выражения # Определение синглтон-метода, принадлежащего объекту о. # с использованием Ргос- или Method-объекта f в качестве его тела. # Здесь обозначение используется в качестве имени метода. # Примеры: # # :singleton[o] = lambda { puts "это синглтон-метод, принадлежащий о" } # :class_method[String] = lambda { puts "это метод класса" } # # Учтите, что создать методы экземпляра этим же способом невозможно. # def []=(o,f) # В приведенном ниже блоке мы не можем использовать self, поскольку он # будет вычислен # в контексте другого объекта. Поэтому мы должны присвоить значение self # переменной. sym = self # Это объект, для которого определяются синглтон-методы. eigenclass = (class « о; self end) # definejnethod является закрытым методом, поэтому для его выполнения мы # должны воспользоваться методом instance_eval. eigenclass.instance_eval { define_method(sym, f) } end end Имея в своем распоряжении уже определенный метод Symbol. [], а также ранее рассмотренный модуль Funct 1 опа 1, мы можем написать довольно заумный (и труд- ночитаемый) код: dashes = puts dashesElO] у = ( # Метод *. принадлежащий объекту # Выводится ”---------" # Иной способ записи у = 2*х + 1 Определение []= для класса Symbol похоже на определение []= для класса Module тем, что в нем используется метод instance_eval для вызова метода defl nejnethod. Разница в том, что синглтон-методы не определены внутри класса, как методы эк- земпляра, а определены в обособленном классе (eigenclass) объекта. С обособлен- ными классами мы еще встретимся в главе 7.
Глава 7 Классы и модули
262 Глава 7. Классы и модули Ruby — это объектно-ориентированный язык в самом прямом смысле этого поня- тия: каждое значение в Ruby является объектом (или по крайней мере ведет себя как объект). Каждый объект является экземпляром класса. В классе определяется набор методов, воздействующих на объект. Классы могут расширяться или быть подклассом других классов и наследовать или переопределять методы своих над- классов. Классы также включают или наследуют методы из модулей. Ruby-объекты обладают абсолютной инкапсуляцией: доступ к их состоянию мо- жет быть получен только через определенные в них методы. К переменным экзем- пляра, работа с которыми ведется с помощью этих методов, за пределами объекта непосредственный доступ отсутствует. Есть возможность определения методов доступа, которые играют роли получателей и установщиков и создают впечатле- ние, что доступ к состоянию объекта ведется напрямую. Эти пары методов досту- па известны как атрибуты, и они отличаются от переменных экземпляра. С точ- ки зрения видимости определяемые в классе методы могут быть «открытыми», «защищенными» или «закрытыми», что оказывает влияние на место и способ их вызова. В отличие от инкапсулированного состояния объекта, Ruby-классы совершен- но открыты. Любая Ruby-программа может добавлять методы к существующим классам и даже добавлять «синглтон-методы» к отдельным объектам. В большинстве своем объектно-ориентированная архитектура Ruby является ча- стью ядра этого языка. Другие составляющие, такие как создание атрибутов и объ- явление видимости методов, реализуются с помощью методов, а не ключевых слов языка. Эта глава начинается с подробного руководства, в котором показывается, как определить класс и добавить к нему методы. За этим руководством следуют разде- лы с углубленной тематикой, в рамках которой рассмотрены следующие вопросы: О видимость методов; О подклассы и наследования; О создание и инициализация объектов; О модули как пространства имен и как включаемые «подмешиваемые» миксин- модули; О синглтон-методы и обособленные классы (eigenclass); О алгоритм разрешения имени метода; О алгоритм разрешения имени константы. 7.1. Определение элементарного класса Обзор классов начнем с подробного руководства по созданию класса по имени Pol nt, предназначенного для представления геометрической точки с координата- ми X и Y. В следующих подразделах будет показано как: О определить новый класс; О создать экземпляры класса;
7.1. Определение элементарного класса 263 О написать для класса метод-инициализатор; О добавить к классу методы доступа к атрибутам; О определить для класса операторы; О определить метод-итератор и сделать класс перечисляемым; О переопределить важные методы класса Object, такие как to_s, ==, hash и <=>; О определить методы класса, переменные класса, переменные экземпляра класса и константы. 7.1.1. Создание класса Классы в Ruby создаются с помощью ключевого слова cl ass: class Point end Подобно большинству имеющихся в Ruby конструкций, определение класса огра- ничивается ключевым словом end. Вдобавок к определению нового класса клю- чевое слово class создает новую константу для ссылки на класс. Имена класса и константы совпадают, поэтому все имена классов должны начинаться с большой буквы. Внутри тела класса, но за пределами любого определяемого в классе метода экзем- пляра ключевое слово sei f ссылается на определяемый класс. Подобно большинству существующих в Ruby инструкций, class является выра- жением. Значением выражения class является значение последнего выражения, встречающегося внутри тела класса. Обычно последним выражением внутри класса является инструкция def, с помощью которой определяется метод. Значе- нием инструкции def всегда является ni 1. 7.1.2. Создание экземпляра класса Point Хотя в класс Pol nt еще ничего не помещено, но возможность получить его экзем- пляр уже имеется: р = Point.new Константа Point хранит объект класса, представляющий наш новый класс. Все объекты класса имеют метод по имени new, который создает новый экземпляр. Но проделать что-либо интересное со свежеиспеченным Pol nt-объектом, который был сохранен в локальной переменной р, пока что невозможно, поскольку мы еще не определили методы для этого класса. Тем не менее у нового объекта можно спросить, к какому классу объектов он относится: р.class # => Point p.is_a? Point # => true
264 Глава 7. Классы и модули 7.1.3. Инициализация класса Point При создании новых Point-объектов нам нужно инициализировать их двумя чис- лами, представляющими координаты X и Y этих объектов. Во многих объектно- ориентированных языках это делается с помощью «конструктора». А в Ruby зто делается с помощью метода initialize: class Point def initialized,у) @x, @y = x, у end end Здесь всего лишь три новых строчки кода, но нам следует обратить внимание на ряд важных обстоятельств. Ключевое слово def было подробно рассмотрено в главе 6. Но в той главе внимание было сосредоточено на глобальных функциях, которыми можно воспользоваться из любого места программы. А если ключевое слово def используется так же, как здесь, с четко выраженным именем метода вну- три определения класса, оно определяет для этого класса метод экземпляра. Это метод, который вызывается в отношении экземпляра класса. При вызове метода экземпляра значение sei f является экземпляром того класса, в котором определен этот метод. Вторым обстоятельством, требующим осмысления, является то, что метод 1 nitia- lize имеет в Ruby специальное назначение. Метод new, принадлежащий объекту класса, создает новый экземпляр объекта, а затем он автоматически вызывает в отношении этого объекта метод initialize. Все аргументы, которые были пере- даны методу new, передаются методу initialize. Поскольку наш метод initialize ожидает два аргумента, то теперь мы должны предоставить при вызове Point .new два значения: р = Point.new(O.O) Вдобавок к тому, что метод initialize вызывается автоматически методом Point, new, метод initialize автоматически делается закрытым. Объект может вызвать для себя метод initialize, но вы не можете в явном виде вызвать initialize в от- ношении р, чтобы заново инициализировать его состояние. Теперь посмотрим на тело метода initialize. Там воспринимаются два переданных нами значения, сохраненные в локальных переменных х и у, и эти значения присва- иваются переменным экземпляра @х и @у. Переменные экземпляра всегда начина- ются с символа @, и они всегда «принадлежат» тому объекту, на который ссылается sei f. Каждый экземпляр нашего класса Poi nt имеет свою собственную копию этих двух переменных, которая содержит его собственные координаты X и Y. В заключение следует предупредить программистов, привыкших работать на Java и родственных ему языках. В статически типизированных языках переменные, включая переменные экземпляра, требуют объявления. Известно, что перемен- ные в Ruby объявлять не требуется, но у вас может сохраниться желание написать что-либо подобное:
7.1. Определение элементарного класса 265 # Неправильный код! class Point @х = 0 # Создание переменной экземпляра @х # и присваивание ей значения по умолчанию. ОШИБОЧНЫЙ ШАГ! @у = О # Создание переменной экземпляра @у # и присваивание ей значения по умолчанию. ОШИБОЧНЫЙ ШАГ! def initial ize(x,у) @х, @у = х, у # А теперь инициализация ранее созданных @х и @у. end end Этот код совершенно не оправдывает надежд Java-программиста. Переменные экземпляра всегда анализируются в контексте self. Когда вызывается метод initial i ze, sei f содержит в себе экземпляр класса Point. Но код за пределами этого метода выполняется как часть определения класса Point. Когда выполняются эти два первых присваивания, self ссылается на сам класс Point, а не на экземпляр класса. Переменные @х и @у внутри метода initialize совершенно отличаются от тех переменных, которые находятся за пределами этого метода. ИНКАПСУЛЯЦИЯ ПЕРЕМЕННЫХ ЭКЗЕМПЛЯРА Доступ к переменным экземпляра объекта может быть предоставлен только методами экземпляра этого объекта. Код, который не находится внутри метода экземпляра, не может читать или устанавливать значение переменной экзем- пляра (если только не используется одна из технологий отражения, которые рассматриваются в главе 8). 7.1.4. Определение метода to_s Практически любой определяемый вами класс должен иметь метод экземпляра to_s для возвращения строки, представляющей объект. Эту возможность трудно не оценить по достоинству при отладке программы. Посмотрим, как это можно будет сделать в случае с классом Poi nt: class Point def initialize(x,у) @x, @y = x, у end def to_s # Возвращение строки, которая представляет данную точку "(#@х,#@у)" # Обыкновенная вставка переменных экземпляра в строку end end После определения этого нового метода можно создать точки и вывести значения их координат:
266 Глава 7. Классы и модули р = new Point(l,2) # Создание нового Point-объекта puts р # Выводит "(1.2)" 7.1.5. Методы доступа и атрибуты В нашем классе Pol nt используются две переменные экземпляра. Но мы уже выяс- нили, что значения этих переменных доступны только каким-нибудь методам эк- земпляра. Если пользователям класса Pol nt нужно работать с координатами точек X и Y, им следует предоставить методы доступа, которые возвращают значения переменных: class Point def initialize(x.y) @x, @y = X, у end def x # Метод доступа (или получатель) для @х @х end def у # Метод доступа для @у @у end end После определения этих методов можно написать следующий код: р = Po1nt.new(l,2) q = Po1nt.new(p.x*2, р.у*3) Выражения р. х и р. у могут выглядеть как ссылки на переменные, но на самом деле они являются вызовами методов без круглых скобок. А если нужно, чтобы класс Point был изменяющимся (что, возможно, и не такая уж здравая затея), следует добавить методы-установщики для установки значе- ний переменных экземпляра: class MutablePoint def Initial 1ze(x.у): @x. @y = x, y; end def x; @x: end def y: @y: end # Метод-получатель для @x # Метод-получатель для @y def x=(value) @x = value end # Метод-установщик для @х def y=(value) @y = value end # Метод-установщик для @у end
7.1. Определение элементарного класса 267 Вспомним, что выражения присваивания могут быть использованы для вызова методов-установщиков, подобных этим. Поэтому, определив эти методы, мы мо- жем написать следующий код: р = Point.new(l.l) р.х = О р.у = О ИСПОЛЬЗОВАНИЕ МЕТОДОВ-УСТАНОВЩИКОВ ВНУТРИ КЛАССА Определив для своего класса метод-установщик наподобие х=, можно испы- тать соблазн применить его внутри других методов экземпляра своего класса. То есть вместо того, чтобы написать @х=2, можно написать х=2, намереваясь осуществить применительно к self неявный вызов х=(2). Но из этого, конеч- но же, ничего не выйдет; использование выражения х=2 приведет просто к созданию новой локальной переменной. Новички, едва изучившие методы-установщики и осуществление присваи- ваний в языке Ruby, допускают такую ошибку довольно часто. Нужно при- держиваться того правила, что выражения присваивания только тогда будут вызывать метод-установщик, когда это будет делаться применительно к объ- екту. Если появляется желание использовать метод-установщик внутри класса, в котором он определяется, его нужно вызывать явным образом, используя self. Например: self.x=2. Подобное сочетание переменной экземпляра с обычными методами-получателями и методами-установщиками встречается настолько часто, что Ruby предоставля- ет для работы с ним соответствующий способ автоматизации. Для этого в классе Module определены методы attr reader и attr accessor. Все классы являются моду- лями (класс Class является подклассом класса Module), поэтому эти методы можно вызывать внутри любого определения класса. Оба метода воспринимают любое ко- личество атрибутов, указанных с помощью обозначений. Метод attr reader создает для переменных экземпляра с такими же именами обычные методы-получатели. Метод attr_accessor создает методы-получатели и методы-установщики. Таким об- разом, при определении изменяющегося класса Pol nt можно написать следующее: class Point attr_accessor :х, :у # Определение методов доступа для наших # переменных экземпляра end А если требуется определить неизменяющуюся версию класса, то нужно написать следующее: class Point attr_reader :х, :у # Определение методов чтения для наших # переменных экземпляра end
268 Глава 7. Классы и модули Каждый из этих методов способен воспринимать имя или имена атрибутов в виде строки, а не в виде обозначения. Принято, конечно, использовать обозначения, но мы можем написать код и в таком виде: attr_reader "х", "у" Есть еще один похожий метод с более коротким именем — att г, но он ведет себя в Ruby 1.8 и Ruby 1.9 по-разному. В версии 1.8 с помощью att г можно при каждом вызове определить только один атрибут. Если использовать только один аргумент в виде обозначения, то будет определен метод-получатель. Если за обозначением будет следовать значение true, то будет определен и метод-установщик: attr :х # Определение для переменной @х обычного метода-получателя attr :у, true # Определение для переменной @у метода-получателя # и метода-установщика В Ruby 1.9 метод attr может использоваться точно так же, как и в версии 1.8, но он также может быть использован и в качестве синонима метода attr reader. Методы attr, attr_reader и attr_accessor создают для нас методы экземпляра. Это является примером метапрограммирования, возможность использования которо- го является важной особенностью языка Ruby. В главе 8 приводится множество других примеров метапрограммирования. Заметьте, что attr и родственные ему методы вызываются внутри определения класса, но за пределами любых опреде- лений методов. Они выполняются только один раз, при определении класса. Ни- каких проблем с эффективностью работы здесь не возникает: создаваемые ими методы-получатели и методы-установщики работают так же быстро, как и те, которые определяются вручную. Нужно учесть, что эти методы могут создавать лишь самые обыкновенные получатели и установщики, которые непосредственно воздействуют на значения переменных экземпляра с одним и тем же именем. Если нужны более сложные методы доступа, такие как методы-установщики, которые устанавливают значения переменных с разными именами, или методы- получатели, которые возвращают значения, вычисленные на основе значений двух разных переменных, то их придется определять самостоятельно. 7.1.6. Определение операторов Нам хотелось бы, чтобы оператор + осуществлял векторное сложение двух Point- объектов, оператор * осуществлял в Роi nt-объекте умножение на скалярную вели- чину и унарный оператор - осуществлял операцию, эквивалентную умножению на -1. Такие операторы, как +, которые основаны на применении методов — это просто методы, именами которым служат знаки пунктуации. Поскольку суще- ствуют унарные и бинарные формы оператора -, для унарного минуса в Ruby ис- пользуется метод по имени Посмотрим на версию класса Point с определения- ми математических операторов: class Point attr_reader :х, :у # Определение методов доступа к нашим # переменным экземпляра
7.1. Определение элементарного класса 269 def initial i ze(x,y) @х,@у = х, у end def +(other) # Определение + для векторного сложения Point.new(@x + other.х. @у + other.у) end def # Определение унарного минуса для изменения Point.new(-@x, -@у) # знака обеих координат на противоположный end def *(scalar) # Определение * для умножения на скаляр Point.new(@x*scalar, @y*scalar) end end Посмотрите на тело метода +. В нем есть возможность использовать переменную экземпляра @х, относящуюся к объекту, на который указывает sei f, то есть к объ- екту, в отношении которого вызывается метод. Но он не может получить доступ к переменной @х в другом Point-объекте. Для этого в Ruby просто нет синтаксиче- ских конструкций; все ссылки на переменные экземпляра косвенно используют self. Поэтому наш метод + зависит от методов-получателей для х и для у. (Чуть позже будет показано, что есть возможность ограничить видимость методов таким образом, чтобы объекты одного и того же класса могли использовать методы друг друга, но воспользоваться ими из кода за пределами этого класса было бы невоз- можно.) ПРОВЕРКА ТИПОВ И DUCK-ТИПИЗАЦИЯ Наш метод + не осуществляет никакой проверки типов; в нем просто пред- полагается, что переданный объект относится к приемлемому типу. В Ruby- программировании зачастую о таком понятии как «приемлемость» просто за- бывают. В случае с нашим методом + подойдет любой объект, имеющий методы х и у, если эти методы не требуют аргументов и возвращают число в каком-либо виде. Нас не тревожит, является аргумент точкой или нет, если он выглядит и ведет себя как точка. Такой подход иногда называют «duck-типизацией» («утиной типизацией»), в соответствии с поговоркой «если существо ходит как утка, и крякает как утка значит это утка». Если мы передадим методу + неприемлемый объект, Ruby выдаст исключение. К примеру, попытка прибавить к точке число 3 приведет к выдаче следующего сообщения об ошибке: NoMethodError: undefined method 'х' for 3:Fixnum from ./point.rb:37:in '+’ В переводе это означает, что Fixnum 3 не имеет метода по имени х и эта ошибка возникла в методе + класса Point. Этой информации вполне достаточно для определения источника проблемы, но кое-что все же непонятно. Проверка класса аргументов метода может облегчить отладку кода, в котором исполь- зуется этот метод. Посмотрим на версию метода с проверкой класса:
I Глава 7. Классы и модули def -«-(other) raise ТуреЕггог, "Ожидался аргумент класса Point " unless other.is_a? Point Point.new(@x + other.x. @y + other.у) end А вот как выглядит более пространная версия проверки типов, предостав- ляющая уточненные сообщения об ошибках, но по-прежнему допускающая duck-типизацию: def +(other) raise ТуреЕггог, "Ожидался аргумент, похожий на точку" unless other.respond_to? :х and other.respond_to? :y Point.new(@x + other.x, @y + other.у) end Обратите внимание, что зта версия метода по-прежнему предполагает, что методы х и у возвращают числа. Если один из этих методов, к примеру, вернет строку, мы получим невразумительное сообщение об ошибке. Другой подход к проверке типов заключается в констатации факта. Мы про- сто можем обработать любое исключение, выданное в процессе выполнения метода, и выдать более приемлемое для нас исключение: def +(other) # Предположение, что other похоже на Point Point.new(@x + other.х, @y + other.у) rescue # Если что-нибудь выше пошло не так raise ТуреЕггог, # Выдача нашего собственного исключения "Сложение Point с аргументами, которые не квакают, как Point!" end ратите внимание, что наш метод * ожидает числовой операнд, а не точку. Если р гяется точкой, то можно написать р*2. Но в том виде, в котором наш класс напи- [, мы не можем написать 2*р. В этом втором выражении вызывается метод *, при- [лежащий классу Integer, который «не знает», как работать с Point-объектами, скольку класс Integer не знает, как умножать на точку, он запрашивает у точки 4ощь путем вызова своего метода coerce. (Более подробно этот вопрос рассмо- н в разделе 3.8.7.4.) Если нужно, чтобы выражение 2*р возвращало такой же ультат, что и выражение р*2 можно определить метод coerce: ри попытке передать Point-объект методу *, определенному в классе Integer, оследний вызовет этот метод, определяемый в классе Point, а затем попробует еремножить элементы массива. Вместо осуществления преобразования типа, мы ереставляем порядок следования операндов, чтобы вызвать определяемый ыше метод *. coerce(other) [self, other]
7.1. Определение элементарного класса 271 7.1.7. Доступ к массивам и хэшам с помощью метода [ ] Для доступа к массивам и хэшам в Ruby используются квадратные скобки и любо- му классу разрешается определить метод □ и использовать квадратные скобки по своему усмотрению. Давайте определим для нашего класса метод [], чтобы Рот nt- объекты могли рассматриваться как массивы длиной в два элемента, доступные только для чтения, или как доступные только для чтения хэши с ключами : х и ;у: # Определение метода []. позволяющего Point походить на массив или на хэш #с ключами :х и :у def [](index) case index when 0, -2: @x # Индекс 0 (или -2) указывает на координату X when 1. -1: @у # Индекс 1 (или -1) указывает на координату Y when :х, "х": @х # Ключ хэша для X в виде обозначения или строки when :у, "у": @у # Ключ хэша для Y в виде обозначения или строки else nil # При неприемлемых индексах массивы и хэши возвращают nil end end 7.1.8. Перечисление координат Если Pol nt-объект может вести себя как массив с двумя элементами, то, наверное, нам нужно получить возможность выполнять итерации этих двух элементов, как это делается с настоящими массивами. Посмотрим на определение итератора each для нашего класса Point. Поскольку Point всегда содержит только два элемента, наш итератор не должен осуществлять циклический перебор; он может просто дважды вызвать инструкцию yield: # Этот итератор передает Х-координату связанному с ним блоку, а затем передает # Y-координату, после чего возвращает управление. Он позволяет считать точку # перечисляемым объектом, как будто это массив, состоящий из двух элементов. # Этот метод each востребован в модуле Enumerable, def each yield @x yield @y end После определения этого итератора можно написать следующий код: р = Point.new(1.2) p.each {|х| print х } # Выводится "12" Но что более важно, определение итератора each позволяет нам подмешивать ме- тоды, определенные в модуле Enumerabl е, которые определены в понятиях each. При добавлении всего лишь одной строчки наш класс получает более 20 итераторов: include Enumerable
Глава 7. Классы и модули пи эта строчка задействована, можно написать вот такой весьма интересный х: 1ринадлежит ли точка Р началу координат? 11? {|х| х == 0 } # True, если блок возвращает true для всех элементов L.9. Равенство точек и текущем определении нашего класса два различных экземпляра Point-объ- 'ов никогда не будут равны друг другу, даже при одинаковых X и Y координа- с Чтобы исправить это положение, нужно предоставить реализацию оператора (Возможно, чтобы освежить память об имеющихся в Ruby различных поняти- равенства, потребуется перечитать раздел 3.8.5 главы 3.) смотрим на разработанный для Ро 1 nt метод ==: ==(о) If о.1s_a? Point @х==о.х && @у==о.у elsif false end # self == о? # Если о является Pol nt-объектом. # то сравниваются поля. # Если о не является Point-объектом, # то, по определению, self != о. DUCK-ТИПИЗАЦИЯ И РАВЕНСТВО Ранее определенный нами оператор + вообще не проводит проверку типов: он работает с любым переданным в качестве аргумента объектом, имеющим методы х и у, возвращающие числа. Этот метод == реализован по-другому; вместо того чтобы разрешить duck-типизацию, он требует, чтобы аргумент был Point-объектом. Для реализации был выбран именно такой вариант. Для показанной выше реализации метода == было выбрано такое определение равенства, при котором объект не может быть равен Point-объекту, если он сам не является Point-объектом. Реализация может быть строже или либеральнее, чем эта. В показанной выше реализации используется предикат is_a?, проверяющий класс аргумента. Это позволяет экземпляру подкласса Point быть равным Point. В более строгой реализации был бы использован предикат instance of?, чтобы запретить ис- пользование экземпляров подкласса. В показанной выше реализации также используется оператор == для сравнения X и Y координат. Для чисел опе- ратор == позволяет проводить преобразование типов, это значит, что точка (1,1) будет равна точке (1.0,1.0). Возможно, так все и должно быть, но в более строгой реализации определения равенства для сравнения координат можно было бы воспользоваться предикатом eql?. Более либеральное определение равенства могло бы поддерживать duck- типизацию. Но некоторые меры предосторожности все же не помешают. Наш метод == не станет выдавать NoMethodError, если используемые в качестве аргументов объекты не будут иметь методов х и у. Вместо этого он просто вернет false:
7.1. Определение элементарного класса 273 def ==(о) # self == о? @х == о.х && @у == о.у # Предполагается, что о имеет вполне # приемлемые методы х и у rescue # Если это предположение не оправдалось false end # то self != о Помните, в разделе 3.8.5 отмечалось, что в Ruby-объектах для проверки равенства также определен метод eql?. По умолчанию метод eql?, как и оператор ==, прове- ряет идентичность объекта, а не равенство его содержимому. Но довольно часто возникает потребность в том, чтобы eql? работал точно так же, как и оператор ==, и этого можно добиться путем назначения псевдонима: class Point alias eql? == end С другой стороны, есть два довода в пользу сохранения различий между eql? и ==. Во-первых, в некоторых классах eql ? определяется для осуществления более стро- гого сравнения, чем то, что обеспечивается оператором ==. К примеру, в классе Numeri с и его подклассах оператор == допускает преобразование типов, a eql ? — нет. Если мы полагаем, что пользователи нашего класса Point могут пожелать сравни- вать экземпляры двумя различными способами, то можно было бы последовать следующему примеру. Поскольку точки представляют собой всего лишь два чис- ла, то имело бы смысл следовать тем же установкам, которые применялись здесь к классу Numeric. Наш метод eql? мог бы во многом походить на метод ==, но для сравнения координат точек вместо == использовать eql ?: def eql?(o) if о.instance_of? Point @x.eql?(o.x) && @y.eql?(o.y) elsif false end end Между прочим стоит заметить, что это верный подход для любых классов, ко- торые реализуют коллекции (наборы, списки, деревья) произвольных объектов. Оператор == должен сравнивать экземпляры коллекций, используя их операто- ры ==, а метод eql ? должен сравнивать эти экземпляры, используя их методы eql ?. Второй причиной выбора реализации метода eql?, отличающейся от реализации оператора ==, может стать потребность в придании экземплярам класса особого поведения при их использовании в качестве ключей хэша. Класс Hash использует eql? для сравнения хэш-ключей (но не значений). Если оставить eql? без опреде- ления, то хэши будут сравнивать экземпляры вашего класса, проверяя идентич- ность объекта. Значит, если значение связано с ключом р, у вас будет возможность
274 Глава 7. Классы и модули извлечь это значение только лишь из точно такого же объекта р. Объект q не бу- дет работать, даже если р == q. Изменяющиеся объекты в качестве ключей хэшей работают неважно, но если оставить eql? без определения, это позволит искусно обойти эту проблему. (Более подробно хэши и изменяющиеся ключи рассмотрены в разделе 3.4.2.) Поскольку eql? используется для хэшей, то отдельной реализацией этого метода лучше никогда не заниматься. Если определять метод eql ?, то нужно также опреде- лять и метод hash для вычисления хэш-кода для вашего объекта. Если два объекта по утверждению метода eql ? равны, то их методы hash должны возвращать одина- ковые значения. (Два неравных объекта могут возвращать один и тот же хэш-код, но этого следует избегать во что бы то ни стало.) Реализация оптимальных методов hash может представлять немалую трудность. К счастью, есть довольно простой способ вычисления абсолютно адекватных хэш- кодов практически для любого класса: просто смешивайте хэш-коды всех объек- тов, упоминающихся вашим классом. (Или если выразиться точнее: объединяйте хэш-коды всех объектов, сравниваемых с помощью вашего метода eql ?.) Весь фо- кус в том, чтобы смешать хэш-коды надлежащим образом. Следующий метод hash нельзя признать удачным: def hash @x.hash + @у.hash end Проблема с применением этого метода состоит в том, что он возвращает один и тот же хэш-код как для точки (1.0), так и для точки (0,1). Это вполне допусти- мо, но плохо работает, когда точки используются в качестве ключей хэша. Вместо этого нам нужно все немного запутать: def hash code = 17 code = 37*code + @x.hash code = 37*code + @y.hash # Подобные строки нужно добавить для каждой значимой переменной экземпляра Code # Возвращение получившегося кода end Этот универсальный рецепт хэш-кода должен подойти для большинства Ruby- классов. Он и его константы 17 и 37 позаимствованы из книги Джошуа Блоха (Joshua Bloch) «Effective Java» (Prentice Hall). 7.1.10. Упорядочение Point-объектов Предположим, нам понадобилось определить взаимный порядок Pol nt-объектов, чтобы их можно было сравнить и сортировать. Для упорядочения точек существу- ет масса способов, но мы выберем их систематизацию на основе расстояния от начала координат. Это расстояние (или магнитуда) вычисляется по теореме Пи- фагора: квадратный корень из суммы квадратов координат X и Y.
7.1. Определение элементарного класса 275 Для определения такой упорядоченности для Рот nt-объектов нам нужно лишь определить оператор <=> (рассмотренный в разделе 4.6.6) и включить модуль Comparable. После того как это будет сделано, будут подмешаны реализации опе- раторов равенства и отношений, основанных на нашей реализации определяемого нами же оператора общего назначения <=>. Этот оператор должен сравнивать self с переданным ему объектом. Если sei f меньше этого объекта (в нашем случае бли- же к началу координат), он должен вернуть -1. Если два объекта равны, он дол- жен вернуть 0. И если sei f больше, чем объект, переданный в качестве аргумента, метод должен вернуть 1. (Если типы sei f и объекта, переданного в качестве аргу- мента, несовместимы, то он должен вернуть ni 1.) Наша реализация оператора <=> показана в следующем примере кода, по поводу которого следует сделать два за- мечания. Во-первых, мы не стали связываться с методом Math. sqrt и обошлись простым сравнением суммы квадратов координат. Во-вторых, после вычисления суммы квадратов мы просто поручили все остальное оператору <=>, определенно- му в классе Float: include Comparable # Подмешивание методов из модуля Comparable. i Определение порядка следования точек на основе их расстояния от начала i координат. # Этот метод востребован модулем Comparable, def <=>(other) return nil unless other.1nstance_of? Point @x**2 + @y**2 <=> other.x**2 + other.y**2 end Учтите, что в модуле Comparable определенный нами метод <=> используется для определения метода ==. Наш оператор сравнения, работа которого основана на вычислении расстояния, приводит к созданию метода ==, который рассматрива- ет точки (1.0) и (0,1) как равные друг другу. Но поскольку в нашем классе Point явным образом определен свой собственный метод ==, метод == модуля Comparable никогда не вызывается. В идеале операторы == и <=> должны иметь согласующие- ся определения равенства. Сделать это для нашего класса Pol nt не представлялось возможным, и мы получили операторы, которые позволили делать следующее: p.q = Point.new(l.O), Polnt.new(O.l) p == q # => false: p не равно q p < q # => false: p не меньше q p > q #=> false: p не больше q И наконец, следует отметить, что в модуле Enumerable определяется ряд методов, и в их числе sort, ml п и max, которые работают, когда объекты превращаются в пере- числяемые за счет определения оператора <=>. 7.1.11. Изменяющийся Point-объект Разработанный нами класс Point является неизменяемым: после того как объект создан, для изменения X и Y координат точки общедоступного API не имеется.
276 Глава 7. Классы и модули Возможно, так и было задумано. Но давайте пойдем в обход и придумаем какие- нибудь методы, которые можно было бы добавить в том случае, если потребуется сделать точки изменяемыми. Прежде всего, нам понадобятся методы-установщики х= и у=, позволяющие осу- ществлять непосредственную установку значений координат X и Y. Эти методы можно определить в явном виде или просто изменить нашу строчку attr reader на attr_accessor: attr_accessor :х, :у Затем нужно, наверное, изменить оператор +, поскольку нам требуется прибавить координаты точки q к координатам точки р, и изменить точку р, вместо того чтобы создавать и возвращать новый Point-объект. Назовем этот метод add! с воскли- цательным знаком, сигнализирующим, что метод изменяет внутреннее состояние объекта, для которого он вызван: def add!(р) # Прибавление р к self, возвращение измененного self @х += р.х @у += р.у self end Если существует не осуществляющая мутацию версия такого же метода, то при определении метода-мутатора к его имени обычно добавляется лишь восклица- тельный знак. В данном случае использование имени add! имеет смысл лишь в том случае, если нами также определен метод add, возвращающий новый объект, а не изменяющий своего получателя. Не осуществляющая мутацию версия метода- мутатора часто пишется путем простого создания копии self-объекта и вызова мутатора для скопированного объекта: def add(p) q = self, dup q.add!(p) end # Версия add!, не осуществляющая мутацию # Создание копии self # Вызов метода-мутатора для копии В этом простом примере наш метод add работает точно так же, как уже определен- ный нами оператор +, поэтому в нем нет особой необходимости. Но если мы не определяем не осуществляющий мутацию метод add, то стоит подумать о том, что- бы убрать восклицательный знак из имени add! и разрешить использование имени метода в его изначальном смысле («add», то есть добавить, а не «прибавить») для указания на то, что это имя относится к методу-мутатору. 7.1.12. Быстрое и простое создание изменяющихся классов Если вам нужен изменяющийся класс Point, то одним из путей его создания мо- жет стать использование класса Struct, являющегося в Ruby основным классом для генерации других классов. Сгенерированные с его помощью классы содержат
7.1. Определение элементарного класса 277 методы доступа для полей с указанными именами. Существуют два способа созда- ния нового класса с помощью Struct. new: | Struct. new(" Pol nt". :x, :y) Point = Struct.new(:x, :y) # Создание нового класса Struct::Point # Создание нового класса, присвоенного # константе Point ПРИСВАИВАНИЕ ИМЕН АНОНИМНЫМ КЛАССАМ Вторая строка кода основана на любопытном факте, касающемся классов: если константе присвоить объект класса, не имеющий имени, то имя этой константы становится именем класса. Такое же поведение можно наблюдать, если использовать конструктор Class.new: С = Class.new с = С.new c.class.to_s # Новый класс, не имеющий тела, присваивается # Константе # Создание экземпляра класса # => ”С”: имя константы становится именем класса Класс, созданный с помощью Struct. new, можно использовать как и всякий другой класс. Имеющийся в нем метод new будет предполагать наличие значений для каж- дого поля, указанного по имени, а его методы экземпляра предоставят для этих полей методы доступа для чтения и записи: р= Point.new(l,2) # => #<struct Point х=1, у=2> р.х # => 1 Р-У # => 2 р.х = 3 # => 3 Р.х # => 3 Для классов, созданных с помощью Struct, также определяются операторы [] и []= для индексации в стиле массивов и хэшей и даже предоставляются итерато- ры each и each_pai г для прохода по значениям, хранящимся в экземплярах струк- туры: p[:x] = 4 # => 4: то же самое. ЧТО И р.х = p[:x] # => 4: то же самое. ЧТО и р.х p[l] # => 2: то же самое. что и Р-У p.each {|с| print с} # выводит "42" p.each_pair {|n.c| print п.с } # выводит "х4у2" Классы, основанные на Struct, имеют работоспособный оператор ==, могут ис- пользоваться в качестве ключей хэша (но с той оговоркой, что они являются из- меняющимися), и для них даже определен весьма полезный метод to_s: q = Point.new(4,2) q == p # => true продолжение &
278 Глава 7. Классы и модули h = {q => 1} # создание хэша, использующего q в качестве ключа h[p] # => 1: извлечение значения, используя р # в качестве ключа q.to_s # => "#<struct Point х=4, у=2>" Класс Poi nt, определенный в виде структуры, не имеет специфичных для точек ме- тодов, наподобие add! или оператора <=>, которые были ранее определены в этой главе. Но это не означает, что мы не можем их добавить. Определения классов в Ruby не являются статическими. Любой класс (включая классы, определенные с помощью Struct. new) может быть «открыт» и иметь добавленные к нему методы. Посмотрим на класс Poi nt, изначально определенный как подкласс Struct, с добав- ленными к нему методами, специально предназначенными для работы с точками: Point = Struct.new(:х. :у) # Создание нового класса, присвоенного константе # Point class Point # Открытие класса Point для новых методов def add!(other) # Определение метода add! self.x += other.х self.у += other.у self end include Comparable # Включение модуля для этого класса def <=>(other) # Определение оператора <=> return nil unless other.instance_of? Point self.x**2 + self.y**2 <=> other.x**2 + other.y**2 end end В начале раздела упоминалось, что класс Struct разработан для создания изменя- ющихся классов. Но путем внесения небольших поправок из класса, основанного на классе Struct, можно сделать неизменяющийся класс: Point = Struct.new(:х, :у) # Определение изменяющегося класса class Point # Открытие класса undef х=,у=,[]= # Удаление определений методов-мутаторов end 7.1.13. Метод класса Давайте выберем для сложения точек, то есть Point-объектов, несколько иной подход. Вместо вызова объекта экземпляра для одной точки и передачи этому методу другой точки, давайте напишем метод по имени sum, который воспринима- ет любое количество Point-объектов, складывает их вместе и возвращает новый Poi nt-объект. Этот метод не относится к методу экземпляра, вызванному для объ- екта Poi nt. Его лучше назвать методом класса, вызываемым посредством самого класса Poi nt. Метод sum можно вызвать следующим образом: total = Point.sum(pl, р2, рЗ) # pl, р2 и рЗ являются Point-объектами
7.1. Определение элементарного класса 279 Следует учесть, что выражение Ро 1 nt ссылается на С1 ass-объект, который представ- ляет наш класс точки. Все, что нужно для определения метода класса для клас- са Point, — это определить для Point-объекта синглтон-метод. (Синглтон-методы были рассмотрены в разделе 6.1.4.) Для определения синглтон-метода воспользу- емся как обычно инструкцией def, но укажем объект, для которого определяется метод, а также имя метода. Наш метод класса sum определяется следующим образом: class Point attr_reader :х, :у # Определение методов доступа к нашим переменным # экземпляра def Point.sum(*po1nts) # Возвращение суммы произвольного количества точек х = у = О points.each {|р| х += р.х; у += р.у } Polnt.new(x.y) end # ...вся остальная часть определения класса опущена... end В этом определении метода класса имя класса указано явным образом и отража- ет синтаксис, используемый для вызова метода. Метод класса также может быть определен с использованием self вместо имени класса. Таким образом, определе- ние этого метода можно записать в следующем виде: def self.sum(*po1nts) # Возвращение суммы произвольного количества точек х = у = О points.each {|р| х += р.х; у += р.у } Polnt.new(x.y) end Использование self вместо Point делает код менее понятным, но это пример при- менения принципа исключения повторений — DRY (Don’t Repeat Yourself). Если sei f применить вместо имени класса, то можно изменить имя класса, не испыты- вая потребности в редактировании определенных в нем методов класса. Для определения методов класса существует еще один технологический прием. Хотя он менее понятен, чем предыдущий, но может пригодиться при определении сразу нескольких методов класса и, скорее всего, может повстречаться в том коде, который уже был кем-то создан до вас: i Открытие объекта Point, чтобы к нему можно было добавить методы class « Point # Синтаксис для добавления методов к одиночному объекту def sum(*po1nts) # Это метод класса Point.sum х = у = О points.each {|р| х += р.х; у += р.у } Polnt.new(x.y) end # Сюда могут быть добавлены другие методы класса end
280 Глава 7. Классы и модули Эта технология может быть также использована внутри определения класса, где мы можем вместо повторяющегося имени класса использовать sei f: class Point # Сюда помещаются методы экземпляров class « self # Сюда помещаются методы класса end end Более подробно этот синтаксис будет рассмотрен в разделе 7.7. 7.1.14. Константы Для многих классов будет полезным определить ряд связанных с ними констант. Нашему классу Point могут пригодиться следующие константы: class Point def initialize(x,у) @x,@y = x, у end # Метод, проводящий инициализацию ORIGIN = Point.new(O.O) UNIT_X = Point.new(l.O) UNIT_Y = Point.new(O.l) # Сюда помещается все остальное определение класса end Внутри определения класса на эти константы можно ссылаться непосредственно, по их именам. Разумеется, за пределами этого определения, в ссылках на них в ка- честве префикса должно использоваться имя класса: Point::UNIT_X + Point::UNIT_Y # => (1.1) Заметьте, что поскольку наши константы в этом примере ссылаются на экзем- пляры класса, они не могут быть определены до тех пор, пока не определен метод initialize этого класса. Также следует учесть, что вполне допускается определение константы в классе Point за пределами определения этого класса: Point::NEGATIVE_UNIT_X = Point.new(-l.O) 7.1.15. Переменные класса Переменные класса видны методам класса и методам экземпляров класса, а также самому коду определения класса и используются ими совместно. Как и перемен- ные экземпляра, переменные класса инкапсулированы; они могут использоваться
7.1. Определение элементарного класса 281 । реализацией класса, но они невидимы для пользователей класса. Переменные класса имеют имена, начинающиеся с символов @@. В нашем классе Роз nt потребность в переменных класса отсутствует, но в учебных целях давайте представим, что нам следует осуществлять сбор данных о количе- стве созданных Point-объектов и их усредненных координатах. Вот как можно было бы написать код, выполняющий эту задачу: class Point # Инициализация наших переменных класса в самом определении класса @@п = 0 ©©totalX = 0 ©©totalY = 0 # Количество созданных точек # Сумма всех координат X # Сумма всех координат Y def initialize(x.y) @x,@y = x, у # Метод инициализации класса # Установка начальных значений для переменных # экземпляра # Использование переменных класса в этом методе экземпляра для сбора данных @@n += 1 # Отслеживание количества созданных точек ©©totalX += х # Добавление этих координат к итоговому результату ©©totalY += у end # Метод класса для отчета по собранным данным def self.report # Здесь переменные класса используются в методе класса puts "Количество созданных точек: #@@п" puts "Средняя X координата: #{@@totalX.to_f/@@n}" puts "Средняя Y координата: #{@@totalY.to_f/@@n}" end end Одна из особенностей, которую следует отметить в этом коде, заключается в том, что переменные класса используются в методах экземпляра, методах класса и в са- мом определении класса за пределами каких-либо методов. Переменные класса в корне отличаются от переменных экземпляра. Мы видели, что переменные эк- земпляра всегда вычисляются в отношении объекта sei f. Поэтому переменная эк- земпляра, на которую осуществляется ссылка в определении класса или в методе класса, абсолютно отличается от переменной класса, на которую осуществляется ссылка в методе экземпляра. С другой стороны, переменные класса всегда вычис- ляются в отношении объекта класса, созданного с помощью охватывающей ин- струкции class, использующейся для определения класса. 7.1.16. Переменные экземпляра класса Классы являются объектами и, как и все остальные объекты, могут иметь пере- менные экземпляра. Переменные экземпляра класса, которые часто называют
282 Глава 7. Классы и модули переменными экземпляра, — это не одно и то же, что и переменные класса. Но они довольно похожи на них и зачастую могут быть использованы вместо переменных класса. Переменные экземпляра, используемые внутри определения класса, но за преде- лами определения метода экземпляра и есть переменные экземпляра класса. По- добно переменным класса, переменные экземпляра класса связаны с классом, а не с любым отдельным экземпляром класса. Недостаток переменных экземпляра класса заключается в том, что они не могут быть использованы внутри методов экземпляра, как это можно сделать с переменными класса. Другой недостаток за- ключается в потенциальной возможности перепутать их с обычными переменны- ми экземпляра. Без отличительных префиксов в виде знаков пунктуации будет трудно запомнить, с чем именно связана переменная, с экземплярами или с объ- ектом класса. Одно из самых важных преимуществ переменных экземпляра класса над пере- менными класса связано с запутанным поведением переменных класса, когда на основе существующего класса создается подкласс. Чуть позже мы еще вернемся к этому вопросу. Давайте перенесем нашу версию класса Poi nt со сводной статистикой на новую основу, где вместо переменных класса будут использоваться переменные экзем- пляра класса. Единственная имеющаяся здесь сложность состоит в том, что пере- менные экземпляра не могут использоваться из методов экземпляра, что застав- ляет нас переместить код, собирающий статистику, за пределы метода initialize (который является методом экземпляра) в метод класса new, используемый для создания точек: class Point # Инициализация наших @п = 0 @totalX = 0 ©totalY = 0 переменных экземпляра класса в самом определении класса # Количество созданных точек # Сумма всех координат X # Сумма всех координат Y def initialize(x,у) @x,@y = x, у end # Метод инициализации класса # Установка начальных значений для переменных # экземпляра def self.new(x.y) # Метод класса для создания новых Point-объектов # Использование переменных экземпляра в этом методе класса для сбора данных @п += 1 ©totalX += х ©totaTY += у # Отслеживание количества созданных точек # Добавление этих координат к итоговому результату super # Вызов фактического определения new для создания # Point-объекта # Дополнительные сведения о super будут изложены позже end
7.2. Область видимости методов: открытые, защищенные и закрытые методы 283 # Метод класса для отчета по собранным данным def self.report # Здесь переменные экземпляра класса используются в методе класса puts "Количество созданных точек: #@п" puts "Средняя X координата: #{@totalX.to_f/@n}" puts "Средняя Y координата: #{@totalY.to_f/@n}" end end Поскольку переменные экземпляра класса являются просто переменными экзем- пляра объектов класса, то для создания для них методов доступа можно восполь- зоваться методами attr, attr_reader и attr_accessor. Тонкость здесь в том, чтобы вызвать эти методы метапрограммирования в правильном контексте. Вспомните, что один из способов определения методов класса заключается в использовании синтаксиса class « self. Такой же синтаксис позволяет нам определить методы доступа к атрибутам для переменных экземпляра класса: class « self attr_accessor :n. :totalX, :totalY end После определения этих методов доступа можно ссылаться на наши исходные данные, используя выражения Point.n, Point.totalX и Point.total Y. L2. Область видимости методов: открытые, защищенные и закрытые методы Методы экземпляров могут быть открытыми (public), закрытыми (private) или защищенными (protected). Тем, кому доводилось заниматься программированием на других объектно-ориентированных языках, эти понятия могут быть знакомы. Но в любом случае обратите внимание на то, что эти термины имеют в Ruby не- сколько иное значение, чем в других языках. Методы обычно бывают открытыми, пока они не будут явным образом объявлены закрытыми или защищенными. Одним из исключений является метод initial! ze, который всегда подразумевается закрытым. Другими исключениями являются любые «глобальные» методы, объявленные за пределами определения класса — эти методы определяются как защищенные методы экземпляра класса Object. От- крытые методы могут быть вызваны отовсюду — на их использование ограниче- ния не накладываются. Закрытый метод является внутренним по отношению к реализации класса, и он может быть вызван только другими экземплярами методов этого класса (или, как мы позже увидим, из его подклассов). Подразумевается, что закрытые методы вы- зываются в отношении self-объекта и не могут быть вызваны в отношении объ- екта явным образом.
284 Глава 7. Классы и модули Если m является закрытым методом, то его можно вызвать в функциональном сти- ле, как ш. Вызвать его в форме о. m или даже sei f. m невозможно. Защищенный метод похож на закрытый в том смысле, что он может быть вызван только из реализации класса или его подклассов. От закрытого метода он отлича- ется тем, что может быть вызван для любого экземпляра класса в явном виде и не ограничен всего лишь неявным вызовом в отношении sei f-объекта. К примеру, защищенный метод может быть применен для определения метода доступа, по- зволяющего экземплярам класса совместно использовать внутреннее состояние друг друга, но не дает возможности пользователям класса иметь доступ к этому состоянию. Защищенные методы определяются реже всех остальных, и в них к тому же до- вольно трудно разобраться. Правило, определяющее, когда защищенный метод может быть вызван, более формально может быть описано следующим образом: защищенный метод, определенный классом С, может быть вызван в отношении объекта о методом в объекте р только в том случае, если классы, к которым отно- сятся о и р, оба являются подклассами, или самим классом С. Область видимости методов объявляется тремя методами с именами public, pri- vate и protected. Все они являются методами экземпляра класса Module. Все клас- сы являются модулями, и внутри определения класса (но за пределами опреде- лений методов) self ссылается на определяемый класс. Поэтому public, private и protected могут быть использованы сами по себе, как будто они являются ключевы- ми словами языка. Но фактически они являются вызовами методов в отношении объекта sei f. Есть два способа вызова этих методов. Без аргументов они опреде- ляют, что все последующие определения методов будут иметь указанную область видимости. В инструкции class они могут использоваться следующим образом: class Point # Сюда помещаются открытые методы # Следующие методы будут защищенными protected # Сюда помещаются защищенные методы # Следующие методы будут закрытыми private # Сюда помещаются закрытые методы end Методы могут также быть вызваны с указанием имени одного или нескольких ме- тодов в качестве аргументов (в виде обозначений или строк). При вызове таким способом они изменяют область видимости указанных методов. При такой прак- тике объявление области видимости должно следовать после определения метода. Один из подходов заключается в том, что все закрытые и защищенные методы объявляются сразу, в конце класса. Другой подход заключается в том, что область видимости объявляется для каждого закрытого или защищенного метода сразу же
7.2. Область видимости методов: открытые, защищенные и закрытые методы 285 после его определения. Вот как, к примеру, выглядит класс с закрытым служеб- ным методом и защищенным методом доступа: class Widget def х @х end protected :x def utllltyjnethod nil end private :utllltyjnethod # Метод доступа для @x # Объявление его защищенным # Определение метода # и объявление его закрытым Следует помнить, что в Ruby методы public, private и protected применяются только к методам. Переменные экземпляра и класса являются инкапсулирован- ными и практически закрытыми, а константы являются практически открытыми. Способов сделать переменные экземпляра доступными из-за пределов класса не существует (разумеется, кроме как путем определения метода доступа). Как не существует и способов определения константы, которая была бы недоступна для внешнего использования. Иногда полезно определить, что метод класса должен быть закрытым. Если, к при- меру, в вашем классе определяются фабричные (factory) методы, то может понадо- биться сделать метод new закрытым. Для этого используется метод private_class_method, задающий одно или более имен методов в виде обозначений: private_class_method :new Закрытый метод класса можно опять сделать открытым с помощью метода publ 1 с_ classjnethod. Ни один из этих методов не может быть вызван без аргументов тем же способом, которым могут быть вызваны методы publ 1 с, protected и pr 1 vate. По своему устройству Ruby очень открытый язык. Возможность определить, что некоторые методы являются закрытыми или защищенными, побуждает к приме- нению хорошего стиля программирования и удерживает от случайного использо- вания методов, не являющихся частью открытого API классов. Но важно понять, что имеющиеся в Ruby возможности для метапрограммирования превращают вызов закрытых и защищенных методов и даже доступ к инкапсулированным переменным экземпляра в довольно тривиальную задачу. Для вызова закрытого служебного метода, определенного в предыдущем коде, можно воспользоваться методом send или можно использовать метод 1nstance_eval для вычисления блока в контексте объекта: w = Widget.new w.send :utllltyjnethod w.1nstance_eval { utllltyjnethod } w.1nstance_eval { @x } # Создание объекта Widget # Вызов закрытого метода! # Другой способ его вызова # Чтение переменной экземпляра объекта w
286 Глава 7. Классы и модули Если нужно вызвать метод по имени, но не хочется непреднамеренно вызвать за- крытый метод, о котором вы не знаете, то можно (в Ruby 1.9) вместо send восполь- зоваться методом publ 1 c send. Он работает так же, как и send, но не вызывает закры- тые методы, когда вызывается с явно указанным получателем. Метод publ ic_send рассмотрен в главе 8, там же рассмотрены методы send и i nstance_eval. 7.3. Подклассы и наследование Большинство объектно-ориентированных языков, включая Ruby, предоставля- ют механизм образования подклассов, позволяющий создавать новые классы, чье поведение базируется на поведении существующего класса, но претерпевает по сравнению с ним некоторые изменения. Рассмотрение процесса образования под- классов мы начнем с определения основной терминологии. Тем, кто уже занимал- ся программированием на Java, C++ или подобных им языках, возможно, эти по- нятия уже знакомы. При определении класса можно указать, что он является расширением другого класса, известного как надкласс, или то, что он наследуется из этого класса. Если мы определяем класс Ruby, который расширяет класс Gem, то говорим, что Ruby яв- ляется подклассом для Gem, a Gem является надклассом для Ruby. Если при определе- нии класса надкласс не указывается, то ваш класс по умолчанию является расши- рением класса Object. Класс может иметь любое количество подклассов, а каждый класс имеет единственный надкласс, за исключением Object, у которого надкласс отсутствует. Тот факт, что классы могут иметь несколько подклассов, но только один надкласс, означает, что они могут быть выстроены в древовидную структуру, которую мы называем иерархией классов Ruby. Класс Object является в этой иерархии корневым, и все классы прямо или кос- венно являются его наследниками. Потомками класса является подкласс этого класса, плюс подклассы подклассов и т. д. по рекурсии. Предками класса являются надкласс, плюс надкласс надкласса и т. д. вверх, вплоть до класса Object. В гла- ве 5 на рис. 5.5 показана часть иерархии классов Ruby, которая включает класс Exception и всех его потомков. На этом рисунке можно увидеть, что предками клас- са EOFError являются классы ЮЕггог, StandardError, Exception и Object. Синтаксис для расширения класса довольно прост. Просто нужно к инструкции class добавить символ < и имя надкласса. Например: class PointOD < Point # Определение класса PointOD в качестве подкласса # для класса Point end В следующем разделе мы наполним этот трехмерный класс Point содержимым, показывая, как методы наследуются из надкласса и как переопределить или на- растить унаследованные методы, чтобы определить для подкласса новое пове- дение.
7.3. Подклассы и наследование 287 BASICOBJECT В RUBY 1.9 В Ruby 1.9 Object уже не является корневым классом иерархии. Для этой цели служит новый класс по имени BasicObject, a Object является подклассом BasicObject. Класс BasicObject исключительно прост, в нем почти нет собствен- ных методов, и он весьма полезен в качестве надкласса для делегирующих классов-оболочек (как тот, что показан в примере 8.5 в главе 8). Создавая класс в Ruby 1.9, вы по-прежнему создаете расширение класса Object, пока надкласс не будет указан явным образом, и большинству программистов никогда не понадобится использовать или расширять класс BasicObject. СОЗДАНИЕ ПОДКЛАССОВ С ИСПОЛЬЗОВАНИЕМ STRUCT Ранее в этой главе мы уже видели, как можно использовать Struct.new для авто- матического генерирования простых классов. Можно также создать подкласс для класса, созданного на основе Struct, чтобы к нему можно было добавить методы вдобавок к тем, что уже были сгенерированы: class Point3D < Struct.new("Polnt3D". :x, :y, :z) # Надкласс Struct предоставляет методы доступа. ==, to_s и т. д. # Сюда добавляются методы для работы с точками end 7.3.1. Наследуемые методы Мы определили класс Ро 1 nt 3D как обычный подкласс для класса Point. Сам по себе он заявлен как расширение класса Point, но у него нет тела класса, поэтому он ничего к этому классу не добавляет. Объект Pol nt3D это фактически то же самое, что и Point-объект. Можно наблюдать только одно отличие, которое заключается взначении, возвращаемом методом class: р2 = Point.new(l ,2) рЗ = Poi nt3D. new( 1,2) print p2.to_s. p2.class # Выводит "(l,2)Point" print p3.to_s, p3.class # Выводит "(1.2)Point3D" Значение, возвращаемое методом class, отличается, но в этом примере больше впечатляет наличие одинаковых свойств. Наш объект Point3D унаследовал метод to_s, определенный классом Point. Он также унаследовал метод initialize, позво- ляющий создать Ро1п130-объект с помощью вызова того же метода new, который использовался для создания объекта Point1. 'У Java-программистов это обстоятельство может вызвать удивление. Классы Java опреде- ляют для инициализации специальные методы конструктора, которые не передаются по на- следству. В Ruby initialize является обычным методом, как и все остальные передаю- щимся по наследству.
288 Глава 7. Классы и модули В этом коде есть и еще один пример наследования метода: оба класса, и Point и PointSD, унаследовали метод class от объекта Object. 7.3.2. Переопределение методов При определении нового класса мы добавляем ему новое поведение путем опреде- ления новых методов. Но не менее важным является то обстоятельство, что мы можем настроить наследованное у класса поведение за счет переопределения уна- следованных методов. К примеру, в классе Object определен метод to_s, предназначенный для преобразо- вания объекта в строку самым универсальным способом: о = Object.new puts o.to_s # Выводится что-либо подобное "#<0bject:0xb7f7fce4>" При определении метода to_s в классе Point мы переопределили метод to_s, уна- следованный у класса Object. В отношении объектно-ориентированного программирования и подклассов важ- но усвоить, что в тот момент, когда методы вызываются, проводится динамиче- ский поиск и находится соответствующее определение или переопределение ме- тода. То есть вызовы методов во время синтаксического анализа не связываются статически, а точнее, методы выискиваются во время выполнения их вызовов. Вот пример, демонстрирующий эту важную особенность: # Приветствие мировому сообществу class WorldGreeter def greet # Отображение приветствия puts "#{greeting} #{who}" end def greeting # Какое приветствие использовать "Hello" end def who # Кого приветствовать "World" end end # Приветствие мирового сообщества по-испански class SpanishWorldGreeter < WorldGreeter def greeting # Переопределение метода greeting "Hol a" end end # Мы вызываем метод, определенный в WorldGreeter, который вызывает # переопределенную версию greeting в SpanishWorldGreeter и выводит "Hola World" SpanishWorldGreeter.new.greet
73. Подклассы и наследование 289 Тем, кто уже занимался объектно-ориентированным программированием, по- ведение этой программы будет вполне обычным и очевидным. Но новичкам оно может показаться сложным и запутанным. Мы вызываем метод greet, унаследо- ванный у класса Wo г 1 dGreeter. Этот метод greet взывает метод greeting. На момент определения greet метод greeting возвращал «Hello». Но мы создали подкласс на основе Worl dGreeter, и объект, в отношении которого мы вызываем greet, имеет но- вое определение метода greeting. Когда мы вызываем greeting, Ruby ищет соот- ветствующее определение этого метода для того объекта, в отношении которого он был вызван, и мы получаем соответствующее испанское приветствие взамен английского. Поиск соответствующего определения метода в процессе выполне- ния программы называется разрешением имени метода и подробно рассмотрен в разделе 7.8. Следует заметить, что было бы весьма разумно определить абстрактный класс, который вызывает некоторые неопределенные «абстрактные» методы, определе- ние которых оставлено для подклассов. Противоположностью понятия абстракт- ный является понятие конкретный. Класс, который расширяет абстрактный класс, является конкретным, если он определяет все абстрактные методы своих предков. Например: # Этот класс является абстрактным: он не определяет greeting или who # Какой-то особый синтаксис здесь не нужен: любой класс, вызывающий методы. # предназначенные для реализации в подклассе, является абстрактным. class AbstractGreeter def greet puts "#{greet1ng} #{who}" end end # Конкретный подкласс class WorldGreeter < AbstractGreeter def greeting: "Hello": end def who; "World"; end end MorldGreeter.new.greet # отображает "Hello World" 7.З.2.1. Переопределение закрытых методов Закрытые методы не могут быть вызваны за пределами класса, в котором они определены. Но они наследуются подклассами. Это означает, что подклассы мо- гут их вызывать и переопределять. При создании подкласса на основе класса, созданного кем-нибудь другим, нужно проявлять особую осторожность. Классы часто используют закрытые методы в ка- честве внутренних вспомогательных методов. Они не являются частью открытого API класса и не предназначены для всеобщего обзора. Если не прочитать исхо- дный код класса, вы не будете даже знать имен закрытых методов, определенных
290 Глава 7. Классы и модули для его внутреннего использования. Если случится так, что метод (безотноситель- но его области видимости), определяемый в вашем подклассе, имеет такое же имя как у закрытого метода в надклассе, то может получиться непреднамеренное пе- реопределение внутреннего служебного метода надкласса, и это почти наверняка вызовет непредусмотренное поведение. Из этого можно сделать вывод, что в Ruby создавать подкласс можно только в том случае, если вы знакомы с реализацией надкласса. Если вы хотите полагаться только на открытое API класса, а не на его реализацию, то вы должны расширить функциональность класса путем инкапсуляции и делегирования ему полномо- чий, а не за счет наследования из этого класса. 7.3.3. Дополнение поведения путем выстраивания цепочки Иногда при переопределении метода не требуется заменять его полностью, а про- сто нужно дополнить его поведение путем добавления некоторого количества но- вого кода. Для этого нам нужен способ вызова переопределенного метода из мето- да, подвергающегося переопределению. Этот способ называется выстраиванием цепочки и достигается с помощью использования ключевого слова super. Ключевое слово super работает как специальный метод вызова: оно вызывает метод с таким же именем, как и у текущего метода в надклассе текущего класса. (Учтите, что надкласс сам по себе не должен иметь определение этого метода — он может наследовать его от одного из своих предков.) Для super можно определить аргументы, точно так же как и для обычного вызова метода. Одним из весьма рас- пространенных и важных мест применения цепочки методов является относя- щийся к классу метод initialize. Вот как можно было бы написать метод initialize нашего класса Point3D: class Point3D < Point def initialized,y,z) # Передача наших первых двух аргументов методу initialize надкласса super(х,у) # и самостоятельная работа с третьим параметром @z = z; end end Если использовать super как пустое ключевое слово — без аргументов и круглых скобок, — то все аргументы, переданные текущему методу, передаются методу над- класса. Тем не менее следует заметить, что методу надкласса передаются текущие значения параметров метода. Если метод изменил значения переменных своих па- раметров, то измененные значения передаются вызову метода надкласса. Как и при обычном вызове метода, круглые скобки вокруг аргументов super явля- ются необязательным элементом. Но поскольку пустое ключевое слово super име- ет специальное назначение, если нужно передать нулевые аргументы из метода,
73. Подклассы и наследование 291 который сам имеет один или более аргументов, следует воспользоваться парой пустых круглых скобок. 7.3.4. Наследование методов класса Методы класса могут быть унаследованы и переопределены точно так же, как и методы экземпляра. Если в нашем классе Point определен метод класса sum, то наш подкласс Рот nt3D унаследует этот метод. То есть если в классе Рот nt3D не определен свой собственный метод класса по име- ни sum, то выражение Рот nt3D. sum вызывает тот же самый метод, что и выражение Point, sum. По стилистическим соображениям предпочтительнее вызывать методы класса че- рез объект класса, в котором они определены. Специалист, обслуживающий код, при виде выражения Point3D.sum станет искать определение метода sum в классе Point3D, и найти класс Point ему будет нелегко. При вызове метода класса с явно указанным получателем, нужно избегать зависимости от наследования — следует всегда вызывать метод класса через класс, в котором он определен1. Внутри тела метода класса можно вызывать другие методы класса без указания явного получателя — по умолчанию они вызываются в отношении self, а значе- нием sei f в методе класса служит класс, в отношении которого он был вызван. Здесь, в теле метода класса, такое наследование методов класса является весьма полезным: оно дает возможность вызывать метод класса неявным образом, даже если этот метод класса определен в надклассе. В заключение следует заметить, что методы класса могут использовать ключевое слово super точно так же, как это делают методы экземпляра для вызова из над- класса метода с таким же именем. 7.3.5. Наследование и переменные экземпляра Довольно часто возникает такое впечатление, что переменные класса в Ruby мо- гут наследоваться. Рассмотрим, к примеру, следующий код: class Point3D < Point def initialized,у,z) super(x.y) @z = z: end def to_s "(#@x. #@y. #@z)" # Являются ли переменные @x и @y унаследованными? end end 'Исключением служит метод class.new — он наследуется каждым новым определенным нами классом и в отношении него же вызывается.
292 Глава 7. Классы и модули Метод to_s в классе Point3D ссылается на переменные @х и @у из надкласса Point. Этот код работает вполне предсказуемо: Point3D.new(1.2,3).to_s # => "(1. 2, 3)" Поскольку он оправдывает ожидания, создается впечатление, что эти переменные унаследованы. Однако работа Ruby строится не так. Все Ruby-объекты имеют на- бор переменных экземпляра. Эти переменные не определяются классом этого объекта, а просто создаются, когда им присваиваются значения. Поскольку пере- менные экземпляра не определяются в классе, они не имеют никакого отношения к созданию подкласса и механизму наследования. В этом коде в классе Point3Dопределяется метод initialize, который выстраивает цепочку к методу initialize своего надкласса. Связанный в цепочку метод при- сваивает значения переменным @х и @у, что делает эти переменные реально суще- ствующими для конкретного экземпляра класса Рот nt3D. У программистов, ранее работавших на Java или на других языках со строгой ти- пизацией, где классы определяют набор полей для своих экземпляров, может сло- житься мнение, что эта особенность требует некоторого привыкания. На самом деле здесь нет ничего сложного: имеющиеся в Ruby переменные экземпляра не наследуются и не имеют никакого отношения к механизму наследования. Причи- на, по которой иногда создается впечатление, что они унаследованы, заключается в том, что переменные экземпляра создаются методами, которые сначала присваи- вают им значения, и эти методы зачастую наследуются или являются частью це- почки. Это довольно важное заключение. Поскольку переменные экземпляра не имеют отношения к наследованию, из этого следует, что переменная экземпляра, используемая подклассом, не в состоянии «заслонить» переменную экземпляра в надклассе. Если подкласс использует переменную экземпляра с таким же име- нем, как и у переменной, используемой одним из его предков, он перепишет зна- чение переменной своих предков. Это может быть сделано намеренно, для изме- нения поведения предка или это может быть сделано по недосмотру. В последнем случае это практически всегда вызывает ошибку. Так же как и при рассмотренном ранее наследовании закрытых методов, это еще одна причина, по которой зани- маться расширением Ruby-классов безопасно лишь в том случае, если вы знакомы с реализацией надкласса (и контролируете ситуацию). И наконец, вспомним, что переменные экземпляра класса — это просто перемен- ные экземпляра Class-объекта, который представляет сам класс. По этой причине они не наследуются. Более того, объекты Point и Point3D (мы ведем речь о самих С1 ass-объектах, а не о классах, которые они представляют) оба являются всего лишь экземплярами объекта Cl ass. Между ними нет никаких взаимоотношений, и не су- ществует способа, позволяющего одному из них наследовать переменные у другого. 7.3.6. Наследование и переменные класса Переменные класса совместно используются классом и всеми его подклассами. Если в классе А определена переменная @@а, то подкласс В может использовать эту
73. Подклассы и наследование 293 переменную. Хотя внешне это может казаться наследованием, на самом деле это не так. Разница становится понятной, стоит только задуматься о присваивании значения переменной класса. Если подкласс присваивает значение переменной класса, ко- торая уже используется надклассом, он не создает своей собственной закрытой копии переменной класса, а вместо этого изменяет значение, видимое надклассом. Он также изменяет совместно используемое значение, видимое всеми другими подклассами надкласса. Если запустить Ruby 1.8 с ключом -w, то он выдаст об этом предупреждение. Ruby 1.9 такого предупреждения не выдает. Если классы используют переменные класса, то любой подкласс может изменить поведение класса и всех его потомков путем изменения значения совместно ис- пользуемой переменной класса. Это вполне весомый аргумент в пользу примене- ния переменных экземпляров класса вместо переменных класса. Совместное применение переменных класса показано в следующем коде, который выводит 123: class А lvalue = 1 def A.value; @@value; end end print A.value class В < A: @@value = 2; end print A.value class C < A; @@value = 3: end print B.value # Переменная класса А # Метод доступа к этой переменной # Отображение значения переменной класса А # Подкласс изменяет значение общей переменной # Надкласс видит измененную переменную # Другой подкласс изменяет общую переменную # Первый подкласс видит значение, присвоенное # вторым подклассом 7.3.7. Наследование констант Константы наследуются и могут быть переопределены практически так же, как и методы экземпляра. Тем не менее между наследованием методов и наследовани- ем констант есть весьма существенная разница. К примеру, наш класс Рот nt3D может использовать константу ORIGIN, определенную в его надклассе Point. Хотя самый понятный стиль написания программ предпо- лагает уточнять принадлежность константы к классу, в котором она определена, Point3D может также ссылаться на эту константу без уточнения в виде ORIGIN или даже как Рот nt3D:: ORIGIN. Наследование констант проявляет интересные особенности, когда класс, вроде Рот nt3D, переопределяет значение константы. Возможно, класс пространственных точек нуждается в константе по имени ORIGIN для ссылки на такую точку, поэтому Poi nt 3D, вероятно, должен включать следующую строку: ORIGIN = Рот nt3D. new( 0,0,0)
294 Глава 7. Классы и модули Известно, что когда переопределяется значение константы, Ruby выдает преду- преждение. Но в данном случае мы имеем дело с заново создаваемой константой. Теперь у нас есть две константы: Point::ORIGIN и Point3D:: ORIGIN. Существенная разница между константами и методами заключается в том, что по- иск констант осуществляется в лексической области видимости места использо- вания, перед тем как он осуществляется в иерархии наследования (подробности изложены в разделе 7.9). А это означает, что если Point3D наследует методы, ис- пользующие константу ORIGIN, поведение этих унаследованных методов не будет изменяться, когда в Рот nt3D будет определена его собственная версия ORIGIN. 7.4. Создание и инициализация объектов Обычно объекты в Ruby создаются при помощи вызова метода new, принадлежа- щего их классу. В этом разделе объясняется работа этого механизма, а также дру- гих механизмов (таких как клонирование и демаршализация), создающих объ- екты. В каждом подразделе объясняется, как можно настроить инициализацию только что созданных объектов. 7.4.1. New, allocate и initialize Каждый класс наследует метод класса new. Перед этим методом стоят две задачи: он должен распределить память под новый объект — фактически ввести его в раз- ряд существующих, и он должен инициализировать этот объект. Обе эти задачи он делегирует соответственно методам al 1 ocate и initialize. Если бы метод new был написан на Ruby, то его код был бы примерно похож на следую- щий: def new(*args) о = self.allocate # Создание нового объекта этого класса о.initialize(*args) # Вызов метода инициализации объектов с нашими # аргументами о # Возвращение нового объекта: возвращаемое значение end # метода initialize игнорируется Метод al 1 ocate является методом экземпляра класса Class, и он наследуется всеми объектами класса. Он предназначен для создания нового экземпляра класса. Его можно использовать отдельно для создания неинициализированного экземпляра класса. Но попытки его переопределения ни к чему не приведут — Ruby всегда вы- зывает этот метод напрямую, игнорируя любые переопределенные версии. Метод initialize является методом экземпляра. Этот метод нужен большинству классов, и каждый класс, являющийся расширением класса, отличного от Object, должен использовать ключевое слово super для выстраивания цепочки к мето- ду initialize, принадлежащего надклассу Типичной задачей метода initialize является создание переменных экземпляра для объекта и присваивание им их
ТА. Создание и инициализация объектов 295 исходных значений. Как правило, значение этих переменных экземпляра извлека- ются из аргументов, которые передаются клиентским кодом методу new, а этот ме- тод передает их методу initialize. Метод 1 ni ti al 1 ze не обязан возвращать инициа- лизированный объект. Фактически значение, возвращаемое методом initialize, игнорируется. Ruby без всяких видимых признаков делает метод initialize за- крытым, что делает невозможным его явный вызов по отношению к объекту. CLASS::NEW И CLASS#NEW В классе Class определяются два метода по имени new. Один из них, Class#new, является методом экземпляра, а другой, Class::new, — методом класса (здесь мы используем устраняющее неоднозначность соглашение об именах, принятое в инструментальном средстве ri). Первый метод является методом экземпляра, который здесь уже рассматривался; он наследуется всеми объектами класса, становясь методом класса, относящимся к подклассу, и используется для создания и инициализации новых экземпляров. Метод класса Class::new является версией метода класса, принадлежащей само- му классу Class, и он может быть использован для создания новых классов. 7.4.2. Фабричные методы Зачастую бывает полезным разрешить экземплярам класса быть инициализиро- ванными более чем одним способом. Чаще всего эта задача решается путем предоставления параметрам метода initi- alize значений по умолчанию. К примеру, с методом initialize, определенным следующим образом, можно вызвать метод new как с двумя, так и с тремя аргумен- тами: class Point # Инициализация Point с двумя или тремя координатами def 1nitial 1ze(x, у, z=ni 1) @x,@y,@z = x, y. z end end Но иногда параметров co значениями по умолчанию оказывается недостаточно, и кроме метода new нужно создать фабричные методы для создания экземпляров нашего класса. Предположим, что нам нужно иметь возможность инициализации Poi nt-объектов, используя либо декартовы, либо полярные координаты: class Point # Определение метода initialize обычным способом ... def initialize(x.у) # Ожидаются декартовы координты @х,@у = х.у end продолжение &
296 Глава 7. Классы и модули # Но при этом объявление фабричного метода new закрытым private_class_method :new def Рот nt.cartes I ап(х.у) # Фабричный метод для декартовых координат new(x.y) # Метод new по-прежнему можно вызывать из других # методов класса end def Pol nt.polar(r. theta) # Фабричный метод для полярных координат new(r*Math.cos(theta), r*Math.sin(theta)) end end Этот код по-прежнему зависит от методов new и 1 ni tial 1 ze, но он делает метод new закрытым, поэтому пользователи класса Point не могут вызывать его непосред- ственно. Вместо этого они должны использовать один из специальных фабричных методов. 7.4.3. Dup, clone и initialize_copy Другой способ ввода новых объектов в разряд существующих связан с результа- тами работы методов dup и clone (рассмотренных в разделе 3.8.8). Если вызвать эти методы, то будет распределена память под новый экземпляр класса того объ- екта, в отношении которого они вызваны. Затем они копируют все переменные экземпляра и все содержимое объекта-получателя во вновь распределенный объ- ект. Метод clone идет при копировании чуть дальше, чем метод dup — он копирует также синглтон-методы объекта-получателя и замораживает копию объекта, если оригинал был заморожен. Если в классе определен метод по имени initial ize_copy, то clone и dup вызовут этот метод в отношении копируемого объекта после копирования переменных эк- земпляра из оригинала. (Метод с 1 one вызывает initial! ze_copy до заморозки копии объекта, поэтому методу initial! ze_copy разрешены внесения изменений.) Методу initial! ze_copy исходный объект передается в виде аргумента, и он имеет возмож- ность производить в копируемом объекте любые изменения. Но он не может соз- дать свой собственный объект; значение, возвращаемое методом initialize_copy, игнорируется. Ruby обеспечивает такую же как и для метода initialize неизмен- ную закрытость метода initial ize_copy. Когда методы clone и dup копируют переменные экземпляра из исходного объекта в копию, они копируют ссылки на значения этих переменных, а не сами значе- ния. Иными словами, указанные методы выполняют поверхностное копирование. И это является одной из причин, по которой многим классам может понадобить- ся изменить поведение этих методов. Посмотрим на код, определяющий метод initial!ze_copy для углубленного копирования внутреннего состояния: class Point # Точка в n-мерном пространстве def initialize(*coords) # Прием произвольного количества координат
7.4. Создание и инициализация объектов 297 @coords = coords # Сохранение координат в массиве end def initialize_copy(orig) # Если кто-нибудь копирует этот Point-объект @coords = @coords.dup # создание также и копии массива координат end end Показанный здесь класс сохраняет свое внутреннее состояние в массиве. Если объект скопирован с использованием clone или dup в отсутствие метода i ni ti al ize_ copy, то он будет ссылаться на тот же самый массив состояния, на который ссыла- ется и исходный объект. Изменения, осуществленные в копии, будут отражаться на состоянии оригинала. Если такое поведение нам не подходит, нужно опреде- лить метод initialize copy для создания наряду с обычным копированием еще и копии массива. Для некоторых классов, к примеру для тех, что определяют перечисляемые типы, может потребоваться строго ограничить количество существующих экземпляров. Такие классы нуждаются в закрытии своих методов new, а также, вероятно, нужда- ются в предотвращении создания копий. Один из способов решения этой задачи показан в следующем коде: class Season NAMES = Xw{ Весна Лето Осень Зима } # Массив с названиями времен года INSTANCES = [] # Массив объектов Season def initialize(n) @n = n end def to_s NAMESE@n] end # Состояние времени года это всего лишь # его индекс в массивах NAMES и INSTANCES # Возвращение названия времени года # Этот код создает экземпляры данного класса для представления времен года # и определяет константы для ссылок на эти экземпляры. # Учтите, что это нужно сделать после определения метода initialize. NAMES.each_with_index do |name.index] instance = new(index) # Создание нового экземпляра INSTANCES[index] = instance # Сохранение его в массиве экземпляров const_set name, instance # Определение константы для сылки на него end # Теперь, после создания всех необходимых экземпляров, нужно # предотвратить создание других экземпляров private_class_method :new,:al locate # Придание закрытого характера # методам-фабрикам private :dup, :clone # Придание закрытого характера # копирующим методам end
298 Глава 7. Классы и модули В этом коде использованы некоторые приемы метапрограммирования, которые станут более понятными после прочтения главы 8. Ключевым моментом кода яв- ляется последняя строка, в которой методам dup и clone придается характер за- крытых. Другой технический прием предотвращения копирования объектов заключается в использовании метода undef для удаления методов clone и dup. Еще один под- ход заключается в переопределении методов clone и dup, чтобы они выдавали ис- ключение с сообщением об ошибке, в котором конкретно говорится, что создание копий запрещено. Такое сообщение об ошибке может оказаться весьма полезным для программистов, использующих ваш класс. 7.4.4. Marshal_dump и marshal_load Третий способ создания объектов связан с вызовом метода marshal .load для вос- создания объектов, которые ранее были маршализированы (или «сериализирова- ны», то есть переведены в последовательность) с помощью метода marshal .dump. Метод marshal .dump сохраняет класс объекта и рекурсивно маршализирует значе- ние каждой из его переменных экземпляра. И этот механизм неплохо справляется со своей задачей — большинство объектов могут быть сохранены и восстановлены за счет использования этих двух методов. Некоторые классы нуждаются в изменении осуществляемого способа маршали- зации (и демаршализации). Одна из причин заключается в предоставлении более компактного представления состояний объектов. Другая причина связана с пре- дотвращением сохранения непостоянных данных, таких как содержимое кэша, которое нуждается в очистке при демаршализации объекта. Настроить способ маршализации объекта можно за счет определения в классе метода экземпляра marshal_dump; он должен возвращать другой объект (строку или массив из выбран- ных значений переменных экземпляра). Если определяется собственный метод marshal dump, то нужно, конечно, опреде- лить и соответствующий ему метод marshal_load. Метод marshal_load будет вы- зван на заново распределенном пространстве памяти (с использованием метода allocate), но с инициализированным экземпляром класса. Ему будет передана ре- конструированная копия объекта, возвращенная методом marshal dump, и он дол- жен инициализировать состояние объекта-получателя на основе состояния того объекта, который ему был передан. В качестве примера давайте вернемся к многомерному классу Point, начало соз- дания которого мы уже ранее рассматривали. Если к нему добавить ограничение, предписывающее всем координатам быть целыми числами, то можно сэкономить несколько байтов на размере маршализованного объекта за счет упаковки массива целых чисел в строку (чтобы лучше разобраться в коде, можно для изучения мето- да array. pack воспользоваться инструментальным средством ri): class Point # Точка в n-мерном пространстве def initialize(*coords) # Прием произвольного количества координат
7.4. Создание и инициализация объектов 299 @coords = coords end # Сохранение координат в массиве def marshal_dump @coords.pack("w*") end # Упаковка координат в строку и их маршализация def marshal_load(s) @coords = s.unpack("w*") End # Распаковка координат из демаршалиэованной # строки и использование их # для инициализации объекта end Если создается класс вроде ранее показанного класса Season, для которого отклю- чались методы clone и dup, то вдобавок нужно будет реализовать свои собствен- ные методы маршализации, поскольку формирование дампа объекта и загрузка объекта является простым способом создания его копии. Маршализацию можно полностью предупредить, определив, что методы marshal_dump и marshal_load долж- ны выдавать исключение, но это не самое лучшее решение. Более элегантным бу- дет внести в демаршализацию такие настройки, при которых метод Marshal. 1 oad не создавал бы копию, а возвращал существующий объект. Чтобы выполнить задуманное, нужно определить другую пару перенастроенных методов маршализации, поскольку возвращаемое значение метода marshal_load игнорируется. Метод dump является методом экземпляра, который должен воз- вращать состояние объекта в виде строки. Соответствующий ему метод _1 oad яв- ляется методом класса, который принимает строку, возвращенную методом dump, и возвращает объект. Методу load разрешено создавать новый объект или воз- вращать ссылку на уже существующий объект. Мы добавим эти методы к классу, чтобы разрешить маршализацию объектов Season, но предотвратить их копирование: class Season # Мы намерены разрешить маршализацию объектов Season, но не хотим # копирования новых экземпляров при их демаршализации. def _dump(limit) # Перенастроенный метод маршализации @n.to_s # Возвращение индекса в виде строки end def self._load(s) # Перенастроенный метод демаршализации INSTANCES[Integer(s)] # Возвращение существующего экземпляра end end 7.4.5. Шаблон Синглтон (Singleton) Синглтон — это класс, у которого имеется только один экземпляр. Синглтоны могут использоваться для хранения глобальных состояний программы внутри
300 Глава 7. Классы и модули объектно-ориентированной среды и могут составить разумную альтернативу ме- тодам класса и переменным класса. СИНГЛТОН-ТЕРМИНОЛОГИЯ В этом разделе рассматривается «Шаблон Синглтон» — известный в объектно- ориентированном программировании шаблон разработки. В Ruby к примене- нию термина синглтон нужно проявлять особое внимание из-за его перегру- женности. Метод, добавляемый к единичному объекту, а не к классу объектов, известен как синглтон-метод (он рассматривается в разделе 6.1.4). Неявный объект класса, к которому добавляются такие синглтон-методы, иногда на- зываются синглтон-классом (хотя в этой книге вместо этого используется термин обособленный класс (eigenclass), определение которому дается в раз- деле 7.7). Правильная реализация синглтона требует применения ряда показанных ра- нее приемов. Методы new и allocate должны быть закрытыми, методы dup и clone не должны допускать создания копий и т. д. К нашему удовольствию, модуль S1 ng 1 eton, принадлежащий стандартной библиотеке, делает эту работу за нас; нуж- но лишь затребовать его строкой require 'singleton', а затем включить Singleton в свой класс. Этим включением определяется метод класса instance, не восприни- мающий никаких аргументов и возвращающий синглтон-экземпляр класса. Для выполнения инициализации синглтон-экземпляра класса нужно определить ме- тод initialize. Но следует учесть, что этому методу не должно передаваться ника- ких аргументов. В качестве примера вернемся к классу Point, с которого начиналась эта глава, и за- ново взглянем на проблему сбора статистики по созданию точек. Вместо хранения этой статистики в переменных класса, принадлежащих самому классу Point, мы воспользуемся синглтон-экземпляром класса PointStats: require 'singleton' class PointStats include Singleton # Модуль Singleton не относится к встроенным # Определение класса # Превращение его в синглтон def initialize # Обычный метод инициализации @n. ©totalX. ©totalY = 0, 0.0, 0.0 end def record(point) # Запись новой точки @n += 1 @totalX += point. x @totalY += point.у end def report # Отчет о статистике точек
7.5. Модули 301 puts "Количество созданных точек: #@п" puts "Усредненная координата X: #{@totalX/@n}" puts "Усредненная координата Y: #{@totalY/@n}" end end Располагая таким классом, для нашего класса Point можно написать следующий метод initialize: def initialize(x,у) @x.@y = х.у Poi ntStats.i nstance.record(seif) end Модуль Singleton автоматически создает для нас метод класса instance, и мы вызываем обычный метод экземпляра record в отношении этого синглтон- экземпляра. По аналогии с этим, когда требуется запросить статистику точек, нужно написать следующую строку: Poi ntStats. i nstance. report 7.5. Модули Модули, как и классы, представляют собой поименованную группу методов, кон- стант и переменных класса. Определение модулей очень похоже на определение классов, но вместо ключевого слова class используется ключевое слово modulе. Но в отличие от класса, модуль не может иметь экземпляров и не может иметь струк- тур, аналогичных подклассам. Модули автономны; никакой «иерархии модулей» или наследования просто не существует. Модули используются как пространство имен и как миксины (mixins). В следую- щих подразделах рассматриваются оба варианта использования. Точно так же как объект класса является экземпляром класса Class, объект мо- дуля является экземпляром класса Module. A Class является подклассом класса Module. Это означает, что все классы являются модулями, но не все модули явля- ются классами. Классы, как и модули, могут быть использованы как пространства имен. Но классы не могут быть использованы в качестве миксинов. 7.5.1. Модули как пространства имен Когда необходимость в объектно-ориентированном программировании не воз- никает, модули являются хорошим способом группировки родственных методов. Представим, к примеру, что вы создаете методы для кодирования и декодиро- вания двоичных данных в текст и из текста, используя для этого систему коди- рования Base64. При этом потребностей в специальных объектах кодирования- декодирования не возникает, поэтому определять класс не имеет смысла. Все, что
302 Глава 7. Классы и модули для этого нужно — два метода: один для кодирования, а другой для декодирова- ния. Мы можем определить всего два глобальных метода: def base64_encode end def base64_decode end Чтобы предотвратить столкновения с пространствами имен, в которых имеются другие кодирующие и декодирующие методы, мы снабдили имена наших методов префиксом base64. Это решение вполне работоспособно, но большинство програм- мистов предпочитают по возможности избегать добавления методов в глобальное пространство имен. Поэтому более удачным решением является определение двух методов внутри модуля Base64: module Base64 def self.encode end def self.decode end end Заметьте, что мы определили методы с использованием префикса self., который делает их «методами класса» этого модуля. Мы можем также еще раз воспользо- ваться именем модуля в явном виде и определить методы следующим образом: module Base64 def Base64.encode end def Base64.decode end end При использовании такого приема для определения методов используется боль- ше повторений, но в них намного лучше отражается синтаксис вызова этих ме- тодов: # Методы модуля Base64 вызываются следующим образом: text = Base64.encode(data) data = Base64.decode(text) Обратите внимание, что имена модулей должны начинаться с заглавной буквы, как и имена классов. При определении модуля создается константа с таким же именем, как и у модуля. Значением этой константы служит Module-объект, пред- ставляющий модуль. Модули также могут содержать константы. Наша реализация Base64 скорее всего будет содержать константу для хранения строки из 64 символов, используемых в качестве цифр в Base64:
7.5. Модули 303 module Base64 DIGITS = 'ABCDEFGHIJKLMNDPQRSTUVWXYZ' \ 'abcdefghijklmnopqrstuvwxyz' \ '01234567В9+/' end За пределами модуля Base64 на эту константу можно сослаться в форме Base64::DIGITS. Внутри модуля наши методы encode и decode могут ссылаться на нее просто по имени DIGITS. Если возникает потребность совместного использования двумя методами каких- нибудь данных, не хранящихся в константах, то для этого можно воспользовать- ся переменной класса (с префиксом @@), точно так же как это можно сделать при определении этих методов в классе. 7.5.1.1. Вложенные пространства имен Модули, включая классы, могут быть вложенными. В результате этого создают- ся вложенные пространства имен, но никаких других последствий не возникает: классы или модули, вложенные вовнутрь других классов или модулей, не имеют никакого особого доступа к классам или модулям, внутри которых они находятся. Чтобы продолжить работу с нашим примером Base64, представим, что нам нужно определить специальные классы для кодирования и декодирования. Поскольку классы Encoder и Decoder по-прежнему являются родственными, мы вложим их в модуль: module Base64 DIGITS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz01234567B9+/' class Encoder def encode end end class Decoder def decode end end # Служебная функция, совместно используемая обоими классами def Base64.helper end end Создав подобную структуру кода, мы определили два новых класса, Base64: : Encoder и Base64:: Decoder. Внутри модуля Base64 два класса могут ссылаться друг на друга по своим простым, несоставным именам, без префикса Base64. И каждый из классов может использовать константу DIGITS без префикса. А теперь рассмотрим служебную функцию Base64.helper. Вложенные классы Encoder и Decoder не имеют какого-то особого доступа к методам того модуля,
304 Глава 7. Классы и модули в котором они содержатся и они должны ссылаться на этот вспомогательный ме- тод по его полному составному имени: Base64. he 1 per. Поскольку классы являются модулями, они также могут быть вложенными. Вло- женность одного класса во внутреннее пространство другого класса влияет только на пространство имен внутреннего класса и не дает этому классу никакого особо- го доступа к методам или переменным внешнего класса. Если вашей реализации класса нужен вспомогательный класс, класс-посредник (proxy class) или какой- нибудь другой класс, не являющийся частью открытого API, то можно рассмотреть вопрос вложения этого внутреннего класса в тот класс, который им пользуется. Тем самым пространство имен будет упорядочено, но в любом случае это не сде- лает вложенный класс закрытым. Разрешение имен констант при вложенности модулей будет рассмотрено в разделе 7.9. 7.5.2. Использование модулей в качестве миксинов Второй вариант использования модулей представляет собой более мощный меха- низм, чем первый. Если в модуле определены не методы класса, а методы экзем- пляра, то эти методы экземпляра могут быть подмешаны к другим классам. Хо- рошо известными примерами миксин-модулей являются Enumerable и Comparable. В модуле Enumerable определяются полезные итераторы, которые реализованы на основе итератора each. В самом модуле Enumerable метод each не определяется, но любой класс, в котором он определен, может подмешать модуль Enumerabl е для не- медленного добавления многих полезных итераторов. Модуль Comparable исполь- зуется аналогичным образом; в нем определены операторы сравнения на основе универсального оператора сравнения <=>. Если в вашем классе определен опера- тор <=>, то к нему можно подмешать Comparable, чтобы без каких-либо усилий по- лучить операторы <, <=, ==, >, >= и метод between?. Чтобы подмешать модули к классу, используется метод incl ude. Этот метод обыч- но используется в форме, похожей на применение ключевого слова самого языка: class Point include Comparable end На самом деле он является закрытым методом экземпляра класса Module, который неявным образом вызывается в отношении sei f — класса, в который включается модуль. В форме вызова метода этот код выглядел бы следующим образом: class Point i nclude(Comparablе) end Поскольку i ncl ude является закрытым методом, он должен вызываться как функ- ция, и мы не можем написать seif.include(Comparable). Метод include восприни-
7.5. Модули 305 мает любое количество Module-объектов для подмешивания, поэтому класс, в ко- тором определены each и <=>, может включать строку: include Enumerable, Comparable Включение модуля затрагивает метод проверки типа 1 s_a? и работу оператора case- равенства ===. К примеру, String подмешивается в модуль Comparable, а в Ruby 1.8 он также подмешивается в модуль Enumerabl е: “text".is_a? Comparable # => true Enumerable === "text" # => true в Ruby 1.8, false в версии 1.9 Учтите, что метод instanceof? проверяет только класс своего получателя, но не надкласс или модули, поэтому следующий код выдаст ложный результат: "text".instance_of? Comparable # => false Хотя каждый класс является модулем, метод include не позволяет классу быть включенным в другой класс. Аргументами include должны быть не классы, а мо- дули, объявленные с помощью инструкции modul е. Но включать один модуль в другой вполне допустимо. При этом методы экзем- пляров включаемых модулей просто становятся методами экземпляра того моду- ля, в который они включаются. В качестве примера рассмотрим следующий код из главы 5: module I terablе # Классы, в которых определен метод next, должны # включать этот модуль include Enumerable # Определение итераторов, дополняющих итератор each, def each # И определение each дополнительно к next loop { yield self.next } end end Обычно модули подмешиваются с помощью метода Module, include. Другой спо- соб связан с использованием метода Object. extend. Этот метод превращает ме- тоды экземпляра указанного модуля или модулей в синглтон-методы объекта- получателя. (А если объектом-получателем является экземпляр класса Cl ass, то методы получателя становятся методами класса, относящимися к этому клас- су-) Приведем следующий пример: countdown = Object.new # Обычный старый объект def countdown.each # Итератор each в виде синглтон-метода yield 3 yield 2 yield 1 end countdown.extend(Enumerable) # Теперь объект обладает всеми методами # модуля Enumerable print countdown.sort # Выводится "[1, 2, 3]"
306 Глава 7. Классы и модули 7.5.3. Включаемые модули пространства имен Существует возможность определения модулей, которые определяют простран- ство имен, но все же позволяют своим методам быть подмешиваемыми. Подоб- ным образом работает модуль Math: Math.sln(O) # => 0.0: Math - пространство имен Include 'Math' # Пространство имен Math можно включить sln(O) # => 0.0: Теперь доступ к функциям упростился Так же работает и модуль Kernel: мы можем вызывать его методы через простран- ство имен Kernel или как закрытые методы класса Object, в который включен этот модуль. Если требуется создать модуль наподобие Math или Kernel, определите свои методы как методы экземпляров модуля. Затем воспользуйтесь модулем module_function для преобразования этих методов в «функции модуля». Метод module_function, являющийся закрытым методом экземпляра класса Module, во многом похож на методы public, protected и private. Он воспринимает в качестве аргументов любое количество имен методов (в виде обозначений или строк). Первичный эффект от вызова module_function состоит в том, что он делает копии указанных методов класса. Вторичный эффект заключается в том, что он делает методы экземпляра закрытыми (зачем это нужно, мы скоро узнаем). Точно так же как и методы publ 1 с, protected и private, метод module_function может быть вызван без аргументов. При этой форме вызова любые методы экземпляра, последовательно определенные в модуле, станут функциями модуля: они пре- вратятся в открытые методы класса и закрытые методы экземпляра. После вы- зова метода module_function без аргументов его действие остается в силе для всей остальной части определения модуля, поэтому если нужно определить методы, не являющиеся функциями модуля, то их следует определить в первую очередь. На первый взгляд может показаться удивительным, что метод modul e_function пре- вращает методы экземпляра модуля в закрытые. Истинная причина такого пре- вращения не связана с управлением доступом, поскольку вполне очевидно, что к методам также открыт доступ через пространство имен модуля. Настоящей при- чиной превращения методов в закрытые служит стремление ограничить их вызов применением функционального стиля, без явного указания получателя. (Их на- зывают функциями модуля, а не методами модуля, поскольку они должны быть вызваны в стиле функций.) Принуждение включенных функций модуля к вызову без указания получателя снижает вероятность того, что они будут приняты за на- стоящие методы экземпляра. Допустим, что мы определяем класс, методы которо- го осуществляют множество тригонометрических вычислений. Чтобы облегчить себе задачу, мы включаем модуль math. Затем мы можем вызвать метод sin в виде функции, вместо того чтобы вызывать его в виде Math.sin. Метод sin в неявном виде вызывается в отношении sei f, но на самом деле мы не предполагаем, что он будет с sei f что-то делать.
7.6. Загрузка и востребование модулей 307 При определении функции модуля нужно избегать использования sei f, посколь- ку значение sei f будет зависеть от того, как эта функция вызвана. Конечно, можно определить функцию модуля, задав ей разное поведение, зависящее от того, как она вызвана. Но если есть такое намерение, то разумнее будет просто определить один метод класса и один метод экземпляра. 7.6. Загрузка и востребование модулей Ruby-программы могут быть разбиты на несколько файлов, и наиболее естест- венным разделением программы на части будет размещение каждого значимого класса или модуля в отдельном файле. Затем эти файлы могут быть заново собра- ны в единую программу (а при удачном исполнении могут быть задействованы и другими программами) с использованием методов require или load. Эти методы являются глобальными функциями, определенными в классе Kernel, но использу- ются как ключевые слова языка. Точно такой же метод гequi re используется и для загрузки файлов из стандартной библиотеки. Методы load и require выполняют одну и ту же задачу, хотя require используется намного чаще, чем 1 oad. Обе функции способны загружать и выполнять указанные файлы исходного кода Ruby. Если файл, предназначенный для загрузки, указан с использованием абсолютного пути или относится к рабочему каталогу пользо- вателя, указанному с помощью символа ~, то будет выполнена загрузка указанно- го файла. Но обычно для файла указывается относительный путь, и методы 1 oad и requi re разыскивают его относительно каталогов пути загрузки Ruby (подроб- ности пути загрузки будут указаны ниже). Несмотря на практически полное сходство, между методами load и require есть существенная разница. О Наряду с загрузкой исходного кода, метод requi re может также загружать дво- ичные расширения для Ruby. Разумеется, двоичные расширения зависят от реа- лизации, но в реализациях на основе языка Си они обычно приобретают форму библиотечных файлов общего пользования с расширениями .so или .dll. О Метод load предполагает использование полного имени файла с расширением. Методу require обычно передается имя библиотеки без расширения, а не имя файла. В этом случае он ищет файл с именем библиотеки, взяв это имя за осно- ву, и добавив к нему соответствующее расширение, используемое для файлов исходного кода или для файлов библиотек. Если в каталоге имеется файл с рас- ширением для исходного кода — .rb, а также файл с расширением, используемым для двоичных файлов, requi re предпочтет для загрузки файл с исходным кодом, а не двоичный файл. О Метод load может загружать один и тот же файл несколько раз. Метод require пытается предотвратить многократную загрузку одного и того же файла. (Но requi re может допустить в этом вопросе промашку, если используются два раз- ных, но с его точки зрения эквивалентных пути к одному и тому же библиотеч-
308 Глава 7. Классы и модули ному файлу. В Ruby 1.9 метод requi re осуществляет расширение относительна путей в абсолютные, затрудняя возникновение подобных заблуждений.) Мете requi re отслеживает загруженные файлы, добавляя их имена к глобальном массиву $" (который также известен под именем $LOADED_FEATURES). Метод 1о< этого не делает. О Метод 1 oad загружает указанный файл с текущим уровнем $SAFE. Метод requiг загружает указанную библиотеку с уровнем SSAFE, установленным в 0, даж! если код, вызывающий requi re, имеет более высокое значение этой переменной Система безопасности Ruby и переменная SSAFE более подробно рассмотрены i разделе 10.5. (Нужно учесть, что если для переменной SSAFE установлено значе- ние выше нуля, requi re откажется загружать какой-либо файл с сомнительным именем или из общедоступного для записи каталога. Но теоретически для requi re считается вполне безопасным загружать файлы с пониженным уровнем SSAFE.) В следующих подразделах изложена более подробная информация о по- ведении методов load и require. 7.6.1. Путь загрузки । Использующийся в Ruby путь загрузки представляет собой массив, доступ- ный либо через глобальную переменную $LOAD_PATH, либо через переменную (Эта глобальная переменная запоминается тем, что двоеточие используется как символ-разделитель в указании пути на Юникс-подобных операционных систе- мах.) Каждый элемент массива является именем каталога, в котором Ruby будет искать файлы для загрузки. Поиск в каталогах идет от начала к концу массива. Элементы $LDAD_PATH в Ruby 1.8 должны быть строками, но в Ruby 1.9 они могут быть не только строками, но и любыми объектами, имеющими метод to path, ко- торый возвращает строку. Содержащееся в $LDAD_PATH значение по умолчанию зависит от используемой вами реализации Ruby, от операционной системы, на которой запущен язык, и даже от того, где вы его установили в своей файловой системе. Обычное значение для Ruby 1.8, полученное при помощи команды ruby -е ’ puts $: ’, выглядит следующим образом: /usr/lib/site_ruby/1.8 /usr/1ib/site_ruby/l.8/1386-1inux /usr/1ib/site_ruby /usr/1ib/ruby/1.8 /usr/1ib/ruby/1.8/1386-1 inux В каталог /usr/lib/ruby/1.8/ устанавливается стандартная библиотека Ruby. Каталог/usr/lib/ruby/1.8/i386-linux/ содержит двоичные расширения Linux для стан- дартной библиотеки. Имеющиеся в пути каталоги site_ruby предназначены для библиотек, определенных для места установки. Учтите, поиск ведется сначала в местных каталогах, а это означает, что вы можете переопределять стандартную би- блиотеку, используя установленные здесь файлы. Текущий рабочий каталог — «.»
7.6. Загрузка и востребование модулей 309 шершает путь поиска. Это каталог, из которого пользователь вызывает вашу lu by-программу, но он не является тем каталогом, в который она установлена. IRuby 1.9 используемый по умолчанию путь загрузки намного сложнее. Вот как ыглядит его обычное значение: 'usr/local /11Ь/ ruby/gems/1.9/gems/rake-0.7.3/11Ь /usr/1 ocal /11 b/ruby/gems/1.9/gems/rake-0.7.3/Ы n /usr/local/11b/ruby/s1te_ruby/l. 9 /usr/local/11b/ruby/s1te_ruby/l.9/1686-1 Inux /usr/local/lib/ruby/site_ruby /usr/1 oca 1 /11b/ ruby/vendor_ruby/1.9 i /usr/1 oca 1 /11b/ ruby/vendor_ruby/1.9/1686-11 nux .'/usr/local /11b/ruby/vendor_ruby /usr/local/11 b/ruby/1.9 /usr/1 oca 1 /11 b/ ruby/1.9/1686-11 nux В пути загрузки, используемом в Ruby 1.9, есть одно незначительное изменение — включение каталогов vendor_ruby, поиск в которых происходит после site_ruby и перед стандартной библиотекой. Эти каталоги предназначены для настроек, предоставленных поставщиками операционной системы. Более существенное изменение пути загрузки в Ruby 1.9 — это включение катало- гов установки RubyGems. В показанном выше пути в первых двух каталогах про- изводится поиск пакета rake, установленного командой gem из системы управления пакетами RubyGems. В приведенном примере установлен только один gem-пакет, но если на вашей системе имеется множество gem-пакетов, то ваш путь загрузки по умолчанию может стать очень длинным. (При запуске программ, не исполь- зующих gem-пакеты, вы можете получить незначительный прирост скорости ра- боты путем вызова Ruby с параметром командной строки - -disable-gems, которая предотвращает добавление этих каталогов к пути загрузки.) Если устанавливает- ся более одной версии gem-пакета, то в путь загрузки, используемый по умолча- нию, включается версия с самым высоким номером версии. Для изменения пути, используемого по умолчанию, применяется метод Kernel .gem. Система управления пакетами RubyGems встроена в Ruby 1.9: команда gem рабо- тает сразу после установки Ruby и может быть использована для установки новых пакетов, чьи каталоги установки автоматически добавляются к используемому по умолчанию пути загрузки. В Ruby 1.8 система RubyGems должна быть установ- лена отдельно (хотя некоторые поставки Ruby 1.8 могут включать ее автомати- ческую установку), а каталоги установки gem-пакетов никогда не добавляются к пути загрузки, используемому по умолчанию. Чтобы компенсировать этот не- достаток, программам, написанным для версии Ruby 1.8, нужен модуль rubygems. Включение этого модуля приводит к замене используемого по умолчанию метода requi re новой версией, которая знает, где искать установленные gem-пакеты. До- полнительная информация о RubyGems содержится в разделе 1.2.5. С помощью ключа командной строки -I интерпретатора Ruby в путь поиска можно добавить новые каталоги. Для указания нескольких каталогов следует
310 Глава 7. Классы и модули воспользоваться несколькими ключами -1 или использовать один ключ -1 и от- делить несколько каталогов друг от друга с помощью двоеточия (или точки с за- пятой в Windows). Программы Ruby также могут модифицировать свой собственный путь загрузки, изменив содержимое массива $LOAD_PATH. Приведем несколько примеров: # Удаление текущего каталога из пути загрузки $:.pop if $:.last == ' .' # Добавление каталога установки текущей программы # в начало пути загрузки $LOAD_PATH.unshift File.expand_path($PROGRAM_NAME) # Добавление значения переменной среды окружения к концу пути $LOAD_PATH « ENVE'MY_LIBRARY_DIRECTORY'] И наконец, нужно учесть, что путь загрузки можно полностью обойти путем пере- дачи методу load или requi re абсолютных имен файлов (которые начинаются с / или ~). 7.6.2. Выполнение загруженного кода Методы load й requi re выполняют код, содержащийся в указанном файле, немед- ленно. Но вызов этих методов не является эквивалентом простой замены вызова load или requi re кодом, содержащимся в файле1. Файлы, загруженные с помощью load или require, выполняются в новой высоко- уровневой области видимости, которая отличается от той, в которой был вызван метод load или метод require. Загруженный файл может видеть все глобальные переменные и константы, которые были определены на момент его загрузки, но он не имеет доступа к локальной области определения, из которой инициировалась загрузка. Из всего этого можно сделать следующие выводы. О Локальные переменные, определенные в той области видимости, из которой вызывался метод load или метод requi re, загруженному файлу не видны. О Любые локальные переменные, создаваемые загружаемым файлом, отбрасыва- ются сразу же после завершения загрузки; они не видны за пределами файла, в котором определены. О В начале загружаемого файла значение sei f всегда является основным объектом, так же как в начале работы Ruby-интерпретатора. То есть вызов 1 oad или requi re внутри вызова метода не распространяет объект-получатель на загружаемый файл. 1 Для Си-программистов изложим эту мысль иначе: методы load и require отличаются от имеющейся в Си директивы #i ncl ude. Передача файла с загружаемым кодом глобаль- ной функции eval больше похожа на включение ее непосредственно в файл: eval (File. read( f i 1 ename)). Но даже это нельзя считать аналогом, поскольку eval не устанавливает локальных переменных.
7.6. Загрузка и востребование модулей 311 О Текущая вложенность модуля в рамках загружаемого файла игнорируется. Можно, к примеру, открыть класс, а затем загрузить файл, содержащий опреде- ления методов. Файл будет обработан в области видимости верхнего уровня, а не внутри какого-нибудь класса или модуля. 7.6.2.1. Изолированная загрузка У метода load есть одно редко используемое свойство, которое мы еще не рассма- тривали. При использовании в вызове второго аргумента, отличающегося от ni 1 или false, метод «изолирует» указанный файл и загружает его в безымянный мо- дуль. Это означает, что загруженный файл не может воздействовать на глобальное пространство имен; любые определенные в нем константы (включая классы и мо- дули) попадают в безымянный модуль. Изолированную загрузку можно исполь- зовать в качестве меры предосторожности (или как способ сведения к минимуму ошибок, вызванных конфликтами пространств имен). В разделе 10.5 будет показано, что Ruby при запуске сомнительного кода в изо- лированной программной среде не позволяет этому коду вызывать метод require и оставляет возможность использовать load только для изолированных загрузок. Когда файл загружается в безымянный модуль, он по-прежнему может устанав- ливать значения глобальных переменных, а переменные с установленными им зна- чениями будут видимы коду, который его загружал. Предположим, вами написан файл util.rb, в котором определяется модуль Uti 1, содержащий полезные служебные методы. Если нужно, чтобы эти методы были доступны, даже если ваш файл загру- жен изолированно, в конце файла следует добавить следующую строку: SUtil = Util # Сохранение ссылки на этот модуль в глобальной переменной Теперь тот код, который загружает util.rb в безымянное пространство имен, может получить доступ к служебным функциям через глобальную константу $Uti 1, а не через обычную константу Util. В Ruby 1.8 безымянный модуль можно даже передать назад коду загрузки: if Module.nesting.size >0 # Если загрузка осуществлялась # в изолированный модуль $wrapper = Module.nesting[0] # Передача модуля назад тому коду, который # его загружал end Дополнительные сведения о методе Module.nesting содержатся в разделе 8.1.1. 7.6.3. Автоматически загружаемые модули Методы autol oad классов Kernel и Modul е допускают «ленивую» загрузку по мере необходимости. Глобальная функция autol oad позволяет регистрировать имя нео- пределенной константы (обычно представляющей имя класса или модуля) и имя библиотеки, в которой она определена.
312 Глава 7. Классы и модули При первой ссылке на эту константу указанная библиотека загружается с помо- щью метода requi re. Например: # Востребование библиотеки 'socket' при первом же испольэовнии TCPSocket autoload :TCPSocket, "socket" В классе Modul е определяется своя собственная версия метода autoload для работы с константами, вложенными в другой модуль. Чтобы проверить наличие ссылок на константы, вызывающие загрузки файлов, используется метод autoload? или Module.autoload?. Этот метод предполагает ис- пользование аргумента в виде обозначения. Если файл был загружен при ссыл- ке на константу, указанную в обозначении, метод autoload? возвращает имя этого файла. В противном случае (если автозагрузка не запрашивалась или если файл уже был загружен без автозагрузки) метод autol oad? возвращает nil. 7.7. Синглтон-методы и обособленные классы (eigenclass) В главе 6 мы изучили возможность определения синглтон-методов, о которых можно сказать, что они определены не для класса объектов, а лишь для отдельно взятого объекта. Чтобы определить синглтон-метод sum для объекта Point, можно создать следующий код: def Point.sum # Сюда помещается тело метода end В этой главе уже говорилось, что методы класса, принадлежащие какому-нибудь классу, — это не что иное, как синглтон-методы, относящиеся к экземпляру класса Cl ass, которым этот класс представлен. Синглтон-методы объекта не определяются в классе этого объекта. Но они яв- ляются методами и должны быть связаны с каким-нибудь классом. Синглтон- методы объекта являются методами экземпляра безымянного обособленного класса (eigenclass), связанного с этим объектом. «Eigen» — это слово, взятое из немецкого языка, которое приблизительно означает «свой», «собственный», «определенный для» или «характерный для». Обособленный класс (eigenclass) также называют синглтон-классом, или (в редких случаях) метаклассом. Термин «обособленный класс» («eigenclass») еще не имеет статуса однозначно принятого в Ruby-сообществе, но именно он использован в этой книге. Для открытия обособленного класса объекта и добавления к нему методов в Ruby определен специальный синтаксис. Он составляет альтернативу поочередному определению синглтон-методов, вместо которого мы можем сразу определить любое количество методов экземпляра обособленного класса объекта. Чтобы от- крыть обособленный класс объекта о, используется синтаксис class « о. Напри- мер, мы можем определить методы класса для Poi nt следующим образом:
7.7. Синглтон-методы и обособленные классы (eigenclass) 313 class « Point def classjnethodl # Это метод экземпляра обособленного класса. End # А также он является методом класса, принадлежащим Point def class_method2 end end Если открыть обособленный класс для объекта класса внутри определения само- го класса, то вместо повторения имени класса можно воспользоваться ключевым словом sei f. Приведем еще раз пример, который ранее уже использовался в этой главе: class Point # Сюда помещаются методы экземпляра class « self # Сюда помещаются методы класса, представляющие собой методы экземпляра # обособленного класса end end К синтаксическим тонкостям нужно относиться очень внимательно. Обратите вни- мание, что между следующими тремя строками имеется весьма существенная раз- ница: class Point # Создание или открытие класса Point class Point3D < Point # Создание подкласса на основе класса Point class « Point # Открытие обособленного класса для объекта Point Вообще-то код будет намного понятнее, если методы класса определить как от- дельные синглтон-методы без явного открытия обособленного класса. При открытии обособленного класса объекта ключевое слово self ссылается на этот обособленный класс. Благодаря этому код для получения обособленного класса объекта о выглядит следующим образом: eigenclass = class « о; self: end Можно формализовать этот прием в методе класса Object, чтобы можно было за- прашивать обособленный класс любого объекта: class Object def eigenclass class « self: self: end end end Пока вы не приступите к сложному метапрограммированию на языке Ruby, толь- ко что показанная здесь служебная функция eigenclass вам вряд ли пригодится. Но разобраться с обособленными классами все же стоит, поскольку они будут вре- мя от времени встречаться в уже существующем коде и поскольку они являются важной составляющей алгоритма разрешения имен Ruby-методов, который будет рассмотрен в следующем разделе.
314 Глава 7. Классы и модули 7.8. Поиск метода Когда Ruby вычисляет выражение вызова метода, сначала он должен вычислить, какой именно метод следует вызвать. Этот процесс называется поиском метода, или разрешением имени метода. Для выражения вызова метода o.m, Ruby осу- ществляет разрешение имени, выполняя следующие шаги. 1. Сначала он проверяет обособленный класс объекта о на наличие синглтон- методов по имени т. 2. Если метода с именем m в обособленном классе не найдено, Ruby ищет класс, к которому принадлежит объект о, а в нем ищет метод экземпляра по имени т. 3. Если метод m в классе не найден, Ruby ведет поиск среди методов экземпляра любых модулей, включенных в класс, к которому принадлежит объект о. Если этот класс включает более одного модуля, то поиск в них ведется в порядке, обратном тому, в котором они включались. То есть сначала поиск ведется в мо- дулях, которые подключались в последнюю очередь. 4. Если метод экземпляра m не найден в классе, к которому принадлежит объект о, или в его модулях, то поиск перемещается вверх по иерархии наследования к надклассу. Для каждого класса в иерархической цепочке наследования по- вторяются шаги 2 и 3, и это повторение длится до тех пор, пока не будет про- смотрен каждый класс-предок и включенные в него модули. 5. Если по завершению поиска метод по имени m так и не будет найден, то вместо него будет вызван метод по имени methodjnissing (метод отсутствует). Чтобы найти соответствующее определение этого метода, алгоритм разрешения име- ни начинает свою работу с шага 1. В модуле Kernel предоставлена исходная реализация метода methodjnissi ng, поэтому этот второй проход разрешения имени обречен на успех. Более подробно метод methodjnissing рассмотрен в разделе 8.4.5. Рассмотрим конкретный пример использования этого алгоритма. Предположим, что у нас есть следующий код: message = "hello" message.world Нам нужно вызвать метод по имени world в отношении экземпляра "hello" класса String. Разрешение имени будет работать в следующем порядке. 1. Проверка обособленного класса на наличие синглтон-методов. В данном случае таковые отсутствуют. 2. Проверка класса St г i ng. В нем нет метода экземпляра по имени worl d. 3. Проверка модулей Comparable и Enumerable класса String на наличие метода эк- земпляра по имени world. Ни в одном из модулей такой метод не определен. 4. Проверка надкласса String, коим является класс Object. В классе Object метод по имени world также не определен.
7.8. Поиск метода 315 5. Проверка модуля Kernel, который включен в класс Object. В нем метод world также не найден, поэтому теперь мы переключаемся на поиск метода по имени methodjnissing. 6. Поиск метода methodjnissing во всех вышеперечисленных местах (обособленном классе объекта String, в классе String, в модулях Comparable и Enumerable, в классе Object и в модуле Kernel). Первое определение метода methodjni ss i ng мы находим в модуле Kernel, поэтому именно этот метод мы и вызываем. Все что он делает — это выдает исключение: NoMethodError: undefined method 'world' for "hello":String Может показаться, что от Ruby требуется осуществление полного поиска при каж- дом вызове метода. Но в обычных реализациях успешные результаты поиска ме- тода будут кэшированы, поэтому последующие поиски метода с таким же именем (если не проводились какие-либо определения методов) будут выполняться очень быстро. 7.8.1. Поиск метода класса Алгоритм разрешения имени для методов класса точно такой же, как и для мето- дов экземпляра, но здесь кроется небольшая хитрость. Начнем с простого, бесхи- тростного случая. Есть класс С, в котором не определены его собственные методы класса: class С end Вспомним, что после определения подобного класса константа С ссылается на объект, являющийся экземпляром класса Class. Любые определяемые нами мето- ды класса — это просто синглтон-методы объекта С. Поскольку класс С определен, то мы, скорее всего, напишем выражение вызова метода, в котором используется метод класса new: с = С.new Чтобы найти метод new, Ruby сначала ищет синглтон-методы в обособленном классе объекта С. У нашего класса отсутствуют какие-либо методы класса, поэто- му здесь искать нечего. После поиска в обособленном классе, согласно алгоритму разрешения имени, поиск ведется в объекте класса, которому С принадлежит. Классом для С является Class, поэтому Ruby продолжает поиск методов в классе Class и находит там метод экземпляра по имени new. Здесь все написано правильно. Алгоритм разрешения имени метода для метода класса С. new сводится к определению местонахождения метода экземпляра Cl ass. new. Различия между методами экземпляра и методами класса помогают обрисо- вать парадигму объектно-ориентированного программирования, но истина в том, что в Ruby — где классы представлены объектами — эти различия носят несколько искусственный характер. Каждый вызов метода, будь то метод экземпляра или
316 Глава 7. Классы и модули метод класса, имеет объект-получатель и имя метода. Согласно алгоритму разре- шения имен проводится поиск соответствующего определения метода для этого объекта. Наш объект С является экземпляром класса Class, поэтому, само собой разумеется, мы можем вызывать методы экземпляра класса Class через С. Более того, Class наследует методы экземпляра у Modul е, Object и Kernel, поэтому эти уна- следованные методы также доступны в качестве методов объекта С. Единственная причина того, что мы пользуемся названием «методы класса», состоит в том, что объект С выдается за класс. Наш метод класса С.new найден в качестве метода экземпляра класса Class. Но если бы он там найден не был, то согласно алгоритму разрешения имен продол- жился бы, как и в случае поиска методов экземпляра. После неудачного поиска в классе Class поиск велся бы в модулях (у Cl ass их нет), а затем в надклассе Module. Затем поиск велся бы в модулях класса Module (которые тоже отсутствуют), и в заключение — в надклассе класса Module, в классе Object и его модуле Kernel. Хи- трость, о которой упоминалось в начале раздела, кроется в том факте, что методы класса наследуются точно так же, как и методы экземпляра. В качестве примера определим метод класса Integer.parse: def Integer.parse(text) text.to_i end Поскольку Fixnum является подклассом Integer, мы можем вызвать этот метод с помощи следующего выражения: n = Fixnum.parse("l”) Из рассмотренного ранее описания алгоритма разрешения имени метода мы зна- ем, что Ruby сначала будет искать синглтон-методы в обособленном классе Fixnum. Затем он будет искать методы экземпляра в классах Class, Module, Object и Kernel. И где же он найдет метод parse? Метод класса Integer — это всего лишь синглтон- метод объекта Integer, а значит, он определен в обособленном классе объекта Integer. Так как же этот обособленный класс объекта Integer был вовлечен в алго- ритм разрешения имен? Объекты классов — особенные: у них есть надклассы. Обособленные классы объ- ектов класса тоже особенные: у них тоже есть надклассы. Обособленный класс обычного объекта пребывает в одиночестве и не имеет надклассов. Воспользуемся именами Fixnum' и Integer' для ссылок на обособленные классы объектов Fixnum и Integer. Надклассом Fixnum’ является Integer'. Теперь, разобравшись в этой хитрости, мы можем более полно объяснить работу алгоритма разрешения имен методов и сказать, что Ruby ищет синглтон-методы в обособленном классе объекта, а также он ведет поиск в надклассе (и всех пред- ках) этого обособленного класса. Поэтому при поиске метода класса Fixnum Ruby сначала проверитсинглтон-методы классов Fixnum, Integer, Numeric и Object, азатем проверяет методы экземпляров классов Class, Module, Object и Kernel.
7.9. Поиск констант 317 7.9. Поиск констант Когда ссылка на константу не содержит никаких уточнений насчет пространства имен, Ruby-интерпретатор должен найти соответствующее определение констан- ты. Для этого он использует алгоритм разрешения имен, точно так же как и при поиске определений методов. Но при разрешении имен констант все делается со- всем по-другому, не так как с разрешением имен методов. Ruby сначала пытается разрешить ссылку на константу в лексической области види- мости ссылки. Это означает, что сначала проверяется класс или модуль, который охва- тывает ссылку на константу, на предмет определения константы. Если определение не отыщется, то проверяется следующий охватывающий класс или модуль. Этот про- цесс продолжается до тех пор, пока охватывающие классы или методы не закончатся. Следует учесть, что верхнеуровневые, или «глобальные» константы не считаются частью лексической области видимости, и в ходе этой части поиска констант не рассматриваются. Метод класса Module. nesting возвращает список классов и моду- лей, в которых ведется поиск на этом этапе, именно в том порядке, в котором этот поиск ведется. Если определения константы в лексически охватывающей области найдено не бу- дет, то Ruby предпримет следующую попытку по поиску константы в иерархии наследования, проверяя предков того класса или модуля, который ссылается на константу. Метод ancestors, определяемый в охватывающем классе или модуле, возвращает список классов и модулей, в которых поиск ведется на этом этапе. Если в иерархии наследования определения константы найдено не будет, то будут проверены определения высокоуровневых констант. Если для требуемой константы определений найдено не будет, то вызывается метод constjnissing — если таковой имеется, — принадлежащий охватывающему классу или модулю, и дается возможность предоставить значение для этой кон- станты. Метод-перехватчик constjnissing рассматривается в главе 8, а его исполь- зование показано в примере 8.3. В поиске констант есть несколько особенностей, о которых стоит упомянуть более подробно. О Константам, определенным в охватывающих модулях, при поиске отдается предпочтение по отношению к тем константам, которые определены во вклю- ченных модулях. О В модулях, включаемых в класс, поиск ведется до того, как он проводится в над- классе данного класса. О Класс Object является составной частью иерархии наследования всех классов. Константы верхнего уровня, определенные за пределами любого класса или модуля, похожи на методы верхнего уровня: они всегда считаются определенны- ми в классе Object. Поэтому когда на константу верхнего уровня ссылаются из класса, то ее разрешение происходит при поиске в иерархии наследования. Но когда на константу ссылаются из определения модуля, то после поиска в пред- ках этого модуля нужно провести явную проверку в классе Object.
318 Глава 7. Классы и модули О Модуль Kernel является предком класса Object. Это означает, что константы, определенные в Kernel, ведут себя как константы верхнего уровня, но могут быть переопределены настоящими константами верхнего уровня, которые определе- ны в классе Object. В примере 7.1. определяются и разрешаются константы в шести различных обла- стях видимости и демонстрируется работа только что рассмотренного алгоритма поиска имен констант. Пример 7.1. Разрешение имен констант module Kernel # Константы, определенные в Kernel A = B = C = D = E = F = "определена в kernel” end # Высокоуровневые, или "глобальные" константы, определенные в Object A = B = C = D = E = "определена на верхнем уровне" class Super # Константы определены в надклассе А=В=С=О= "определена в надклассе" end module Included # Константы определены во включаемом модуле А = В = С = "определена во включаемом модуле" end module Enclosing # Константы определены в охватывающем модуле А = В = "определена в охватывающем модуле" class Local < Super include Included # Локально определенная константа А = "определена локально" # Список модулей, в которых ведется поиск, расположенных в порядке # ведения этого поиска # EEnclosing::Local, Enclosing, Included, Super, Object, Kernel] search = (Module.nesting + self.ancestors + Object.ancestors).uniq end puts A # Выводит "определена локально" puts В # Выводит "определена в охватывающем модуле" puts C # Выводит "определена во включаемом модуле" puts 0 # Выводит "определена в надклассе" puts E # Выводит "определена на верхнем уровне" puts F # Выводит "определена в kernel" end
Глава 8 Отражение и метапрограммирование
320 Глава 8. Отражение и метапрограммирование Мы уже убедились в том, что Ruby — очень динамичный язык; он позволяет вставлять новые методы в классы в процессе выполнения программы, создавать псевдонимы для существующих методов и даже определять методы для отдельно взятых объектов. Вдобавок ко всему этому он обладает богатым API для отра- жения. Отражение, которое также называют интроспекцией, или самоанализом, определяет способность программы исследовать свое состояние и свою структуру. Ruby-программа, к примеру, может получить список методов, определенных клас- сом Hash, запросить значение поименованных переменных экземпляра в пределах указанного объекта или осуществить проход по всем Regexp-объектам, определен- ным интерпретатором на данный момент. В действительности API отражения идет еще дальше и позволяет программе изменять свое состояние и структуру. Ruby-программа может динамически устанавливать значения поименованных переменных, вызывать поименованные методы и даже определять новые классы и новые методы. Использующийся в Ruby API отражения, наряду с универсальной динамической природой этого языка, с имеющейся в нем управляющей структурой на основе использования блоков и итераторов и синтаксисом, в котором необязательно применять круглые скобки, — делают его идеальным языком для метапрограм- мирования. В самом простом определении метапрограммирование — это создание программ (или программных сред), помогающих создавать другие программы. Иначе говоря, метапрограммирование представляет собой набор технологических приемов, предназначенных для расширения синтаксиса Ruby с целью облегчения процесса программирования. Метапрограммирование тесно связано с идеей напи- сания проблемно-ориентированных языков (domain-specific languages, или DSL). В Ruby DSL обычно используют вызовы методов и блоков в качестве ключевых слов в расширениях языка, направленных на решение определенного круга задач. Первые несколько разделов этой главы представляют имеющийся в Ruby API отражения. Этот API на удивление богат всевозможными средствами и состоит из значительного количества методов. Большинство этих методов определено в классах Kernel, Object и Module. При чтении вводных разделов следует помнить, что отражение само по себе еще не является метапрограммированием. Как правило, метапрограммирование каким- либо образом расширяет синтаксис или поведение Ruby и зачастую привлекает к этому процессу более одной разновидности отражения. После введения в API отражения, относящегося к ядру Ruby, в этой главе осуществляется переход к де- монстрации примеров основных технологий метапрограммирования, в которых используется этот API. Следует учесть, что этой главе рассматриваются довольно сложные вопросы. До- биться успехов в Ruby-программировании можно и не читая этого материала. Возможно, есть смысл сначала прочитать оставшиеся главы этой книги, а уж по- том вернуться к чтению этой главы. Считайте эту главу своеобразным выпускным экзаменом: если вы поймете суть приведенных в ней примеров (особенно самых длинных, помещенных в конце главы), значит вы освоили Ruby!
8.1. Типы, классы и модули 321 8.1. Типы, классы и модули Наиболее часто используются методы отражения, определяющие тип объекта, то есть экземпляром какого класса он является и на какие методы реагирует. Боль- шинство из этих весьма значимых методов уже были представлены ранее, в раз- деле 3.8.4. Вот их краткий обзор: о.class Возвращает класс объекта о. с.superclass Возвращает надкласс объекта с. o.instance_of? с Определяет, верно ли, что о. cl ass == с. o.is_a? с Определяет, является ли о экземпляром класса с или любого из его подклассов. Если с является модулем, этот метод проверяет, включает ли о.class (или лю- бой из его предков) этот модуль. o.kind_of? с kind_of? является синонимом для is_a?. с === о Для любого класса или модуля с определяет, является ли истинным выражение o.1s_a?(c). o.respond_to? name Определяет, имеет ли объект о открытый или защищенный метод с указанным именем. Если ему в качестве второго аргумента передать true, то проверяет так- же наличие закрытого метода. 8.1.1. Предки и модули В дополнение к уже рассмотренным методам есть еще ряд родственных методов отражения для определения предков класса или модуля и для определения, какие модули включены в класс или модуль. Эти методы легче всего усвоить на при- мерах: module A: end module В: include A: end: class С: Include В: end: С < В В < А С < А Fixnum < Integer Integer < Comparable # Пустой модуль # Модуль В включает А # Класс С включает модуль В # => true: С включает В # => true: В включает А # => true # => true: все числа fixnum относятся к целым числам # => true: все целые числа можно сравнивать продолжение &
322 Глава 8. Отражение и метапрограммирование Integer < Fixnum String < Numeric A.ancestors B.ancestors C.ancestors String.ancestors # => false: не все целые числа относятся к fixnum # => nil: строки числами не являются # => [А] # => [В. А] # => [С, В, A, Object. Kernel] # => [String, Enumerable. Comparable. Object. Kernel] # Учтите: В Ruby 1.9 строки больше не являются # перечисляемыми объектами (Enumerable) С.include?(В) С.include?(А) В.include?(А) А. 1 ncl ude? (А) А.1nclude?(В) A.includedjnodules B.includedjnodules C.includedjnodules # => true # => true # => true # => false # => false # => [] # => [A] # => [B. A, Kernel] В этом коде показан метод include?, который относится к открытым методам эк- земпляра класса Module. Но в нем также показаны два вызова метода include (без вопросительного знака), который относится к закрытым методам экземпляра класса Module. Будучи закрытым, этот метод может быть вызван только неявным образом в отношении sei f, что ограничивает его использование телом определе- ния класса или модуля. Здесь метод i ncl ude используется таким образом, будто он является ключевым словом в примере метапрограммирования в основном синтак- сисе языка Ruby. Родственным для закрытого метода include является открытый метод object, extend. Этот метод расширяет объект путем превращения методов экземпляра каждого из указанных модулей в синглтон-методы объекта: module Greeter; def hi; "hello": end: end # Некий надуманный модуль s - "string object" s.extend(Greeter) # Добавление hi к s в качестве синглтон-метода s.hi # => "hello" String.extend(Greeter) # Добавление hi к String в качестве метода класса String.hi # => "hello" Метод класса Module.nesting не связан с включением модуля или с предком; но зато он возвращает массив, определяющий вложенность модулей в текущем ме- сте. Module.nesting[0] представляет текущий класс или модуль, a Module.nesting[l] — охватывающий класс или модуль, и т. д: module М class С Module.nesting # => [М::С, М] end end
8.2. Вычисление строк и блоков 323 8.1.2. Определение классов и модулей Классы и модули являются экземплярами классов Cl ass и Modul е. Их можно также создавать динамическим способом: И = Module.new С = Class.new D = Class.new(C) { include M D.to s # Определение нового модуля М # Определение нового класса С # Определение подкласса для С # который включает модуль М # => ”D”: класс получает имя константы Привлекательной особенностью Ruby является присваивание динамически соз- даваемого безымянного модуля или класса константе, причем имя этой константы используется в качестве имени модуля или класса (и возвращается его методами name и to_s). 8.2. Вычисление строк и блоков Одним из наиболее мощных и простых свойств отражения, имеющихся в Ruby, является метод eva 1. Если ваша Ruby-программа в состоянии генерировать впол- не приемлемую строку Ruby-кода, то метод Kernel .eval может этот код вычислить: х = 1 eval "х + 1" # => 2 Метод eval является очень мощной функцией, но пока вы не приступите к созда- нию программы-оболочки (вроде irb), подобные выполняемые строки Ruby-кода, введенные пользователем, вам вряд ли понадобятся. (А в сетевой среде вызов eval в отношении текста, введенного пользователем, всегда несет угрозу безопасности, поскольку в тексте может встречаться вредоносный код.) Иногда малоопытные программисты прибегают к использованию метода eval в качестве своеобразной подпорки. Если он подобным же образом используется и в вашем коде, то нужно найти возможность исключить такое применение. После всего сказанного следу- ет заметить, что есть более полезные способы использования eval и ему подобных методов. 8.2.1. Связывания и метод eval Binding-объект представляет состояние связываний Ruby-переменных в опреде- ленный момент времени. Объект Kernel .binding возвращает связывания, действу- ющие в месте вызова. Binding-объект может быть передан методу eval в качестве второго аргумента, и указанная вами строка будет вычислена в контексте этих связываний. Если, к примеру, мы определим метод экземпляра, возвращающий Binding-объект, который представляет переменные, связанные внутри объекта, то после этого можно будет воспользоваться этими связываниями для запроса
324 Глава 8. Отражение и метапрограммирование и установки значений переменных экземпляра этого объекта. Добиться этого можно следующим образом: class Object def bindings binding end end # Открытие Object для добавления нового метода # Обратите внимание, что имя этого метода дано во # множественном числе # Это предопределенный метод класса Kernel class Test def initial1ze(x): end # Простой класс с переменной @х = х: end экземпляра t = Test.new(lO) eval("@x", t. bindings) # Создание нового объекта # => 10: Значение, взятое из t Учтите, что фактически для считывания значения, принадлежащего объекту переменной экземпляра, определять подобные методы Object.bindings не нуж- но. Существует несколько методов, которые нам вскоре предстоит рассмотреть, предоставляющих более простой способ для запроса (или установки) значений переменных экземпляра, принадлежащих объекту. В разделе 6.6.2 мы уже выяснили, что для Ргос-объекта определяется открытый метод binding, возвращающий Binding-объект, представляющий связывания пере- менных, действующих для тела этого Ргос-объекта. К тому же метод eval вместо Bi ndi ng-объекта допускает в качестве второго аргу- мента передачу Ргос-объекта. В Ruby 1.9 метод eval определен для Binding-объектов, поэтому вместо передачи глобальному методу eval Bi ndi ng-объекта в качестве второго аргумента можно вы- звать метод eval в отношении Bi ndi ng-объекта. Какой при этом сделать выбор — во- прос чисто стилистический, поскольку обе технологии абсолютно равнозначны. 8.2.2. Instance_eval и class_eval В классе Object определен метод по имени instance eval, а в классе Module — метод по имени class_eval. (Метод module_eval является синонимом для class_eval.) Оба этих метода вычисляют Ruby-код так же, как и метод eval, но с двумя существен- ными отличиями. Первое отличие состоит в том, что они вычисляют код в контексте указанного объекта или в контексте указанного модуля — в процессе вычисления кода объект или модуль является значением self. Приведем несколько примеров: о.instance_eval("@х”) # Возвращает значение переменной # экземпляра @х, принадлежащей о # Определение метода экземпляра 1еп для класса String, возвращающего длину строки String.class_eval("def len: size: end")
8.3. Переменные и константы 325 f А вот другой способ сделать то же самое # Код, взятый в кавычки, ведет себя так, будто он находится внутри "class String" # и "end" String.class_eval ("alias len size") # Использование instance_eval для определения метода класса String.empty # Заметьте, что кавычки внутри кавычек воспринимаются с трудом... String. instance_eval ("def empty; ": end") Следует учесть небольшое, но весьма существенное различие между 1 nstance_eval и cl ass eval, проявляющееся при вычислении кода, содержащего определение ме- тода. Метод 1 nstance_eval определяет синглтон-методы объекта (в результате чего при вызове этого метода в отношении объекта класса получаются методы класса). Метод cl ass eval определяет обычные методы экземпляра. Второе важное отличие этих двух методов от глобального метода eval состоит в том, что instance_eval и class_eval могут воспринимать для вычисления блок кода. Когда им вместо строки передается блок кода, код, содержащийся в этом блоке, вычисляется в соответствующем контексте. Приведем соответствующие альтернативные варианты для ранее показанных вы- зовов: o.instance_eval { @х } String.class_eval { def len size end String.class_eval { alias len size } String.instance_eval { def empty: "": end } 8.2.3. Instance_exec и class_exec В Ruby 1.9 определены еще два метода, производящие вычисления: instance exec и class_exec (и его псевдоним module_exec). Эти методы вычисляют блок (но не строку) кода в контексте объекта-получателя, как и методы instance_eval и class_ eval. Разница в том, что ехес-методы воспринимают аргументы и передают их бло- ку. Таким образом, блок кода вычисляется в контексте указанного объекта с пара- метрами, чьи значения приходят из-за пределов объекта. 8.3. Переменные и константы Методы Kernel, Object и Module определяют методы отражения для вывода имен (в виде строк) всех определенных глобальных переменных, определенных на данный момент локальных переменных, всех имеющихся в объекте переменных
326 Глава 8. Отражение и метапрограммирование экземпляра, всех переменных класса, принадлежащих классу или модулю, и всех констант, принадлежащих классу или модулю: global_variables # => ["$DEBUG". "$SAFE", ...] х = 1 # Определение локальной переменной local variables # => ["х”] # Определение простого класса class Point def 1nitialize(x.y); @x,@y = х.у; end @@classvar = 1 ORIGIN = Point.new(O.O) end Point::ORIGIN.instance_variables Point.class_variables Point.constants # Определение переменных экземпляра # Определение переменной класса # Определение константы # => ["Ру". "@х"] # => C"@@classvar"] # => ["ORIGIN"] Методы global_variables, instance_variables, class_variables и constants возвраща- ют массивы строк в Ruby 1.8 и массивы обозначений в Ruby 1.9. Метод 1оса1_ vari abl es возвращает массив строк в обеих версиях языка. 8.3.1. Запрос, установка и проверка переменных Вдобавок к выводу имен определенных переменных и констант в Ruby-классах Object и Module также определяются методы отражения для запроса, установки и удаления переменных экземпляра, переменных класса и констант. В них нет специально предназначенных методов для запроса или установки локальных пе- ременных или глобальных переменных, но для этих целей можно воспользоваться методом eval: х = 1 varname = "х" eval(varname) eval("varname = ’$g'") eval("#{varname} = x") eval(varname) # => 1 # Установка значения varname в "$g" # Установка $g в 1 # => 1 Учтите, что eval вычисляет свой код в пространстве имен, имеющем временный характер. Метод eval может изменить значение уже существующих переменных. Но любые определенные им переменные экземпляра являются локальными по от- ношению к вызову метода eval и прекращают свое существование, когда он воз- вращает управление. (Это похоже на запуск вычисляемого кода в теле блока — переменные, являющиеся локальными по отношению к блоку, за его пределами не существуют.) Можно запрашивать, устанавливать и проверять существование переменных эк- земпляра, относящихся к любому объекту, и переменных класса и констант, отно- сящихся к любому классу или модулю:
8.3. Переменные и константы 327 о = Object, new o.instance_variable_set( :@х. 0) o.instance_variable_get( :@х) о. instance_vari abl e_def i ned? (: @x) Object.class_variable_set( :@@x, 1) Object, cl ass_vari abl e_get( :@@x) Object. cl ass_vari abl e_defi ned? (:@@x) # Заметьте, что здесь требуется префикс @ # => О # => true # Закрытый в Ruby 1.8 # Закрытый в Ruby 1.8 # => true: В Ruby 1.9 и более поздних # версиях Math.const_set(:EPI. Math::E*Math::PI) Math.const_get(:EPI) Math. const_defi ned? :EPI # => 8.53973422267357 # => true В Ruby 1.9 методам const_get и const_def i ned? в качестве второго аргумента можно передать false, чтобы указать им на поиск только в текущем классе или модуле и на игнорирование унаследованных констант. В Ruby 1.8 методы для запросов и установок переменных класса являются закры- тыми. В этой версии их можно вызвать через метод cl ass eval: String.class_eval { class_variable_set(:@@х, 1) } # Установка @@х в String String.class_eval { class_variable_get(:@@x) } # => 1 В классах Object и Module определяются закрытые методы для отмены определе- ний переменных экземпляра, переменных класса и констант. Все они возвращают значения удаленных переменных или констант. Поскольку эти методы являются закрытыми, их невозможно вызвать напрямую в отношении объекта, класса или модуля, и нужно использовать метод eval или метод send (рассматриваемый далее): o.instance_eval { remove_instance_variable :@х } String.class_eval { remove_class_variable(:@@х) } Math.send :remove_const. :EPI # Использование send для вызова закрытого метода Когда ссылка делается на неопределенную константу, то вызывается принадле- жащий модулю метод constjnissing, если таковой существует. Этот метод мож- но определить для возвращения значения названной константы. (К примеру, это свойство может быть использовано для реализации средства автозагрузки, в ко- тором классы или модули загружаются по мере надобности.) Приведем простой пример: def Symbol.const_missing(name) name # Возвращение имени константы в виде обозначения end Symbol::Test # => :Test: неопределенная константа вычисляется # в обозначение
328 Глава 8. Отражение и метапрограммирование 8.4. Методы В классах Object и Module определяется ряд методов, предназначенных для выво- да списка имен, а также запроса, вызова и определения методов. Мы рассмотрим каждую категорию в порядке очередности. 8.4.1. Вывод имен и проверка наличия методов В классе Object определяются методы для вывода списка имен методов, опреде- ленных для объекта. Эти методы возвращают массивы, состоящие из имен мето- дов. Эти имена являются строками в Ruby 1.8 и обозначениями в Ruby 1.9: о = "a string" о.methods о.publ1c_methods о.publ1c_methods(false) о.protectedjnethods o.privatejnethods o.private_methods(false) def o.single: 1: end o.singletonjnethods # => [ имена всех открытых методов ] # => то же самое # Исключение унаследованных методов # => []: таковых не имеется # => массив из всех закрытых методов # Исключение унаследованных закрытых методов # Определение синглтон-метода # => ["single"] (или С:single] в версии 1.9) Вместо того чтобы запрашивать экземпляр класса, можно также запросить класс, в котором эти методы определены. Следующие методы определены в классе Mo- dule. Подобно методам, определенным в классе Object, они возвращают массивы строк в Ruby 1.8 и массивы обозначений в Ruby 1.9: String.instancejnethods == "s".public_methods # => true String.instance_methods(false) == "s".public_methods(false) # => true String.public_instancejnethods == String.instancejnethods # => true String.protected_instance_methods # => [] String.private_instance_methods(false) # => ["initialize_copy", # "initialize"] Вспомним, что принадлежащие классу или модулю методы класса являются синглтон-методами Class- или Module-объекта. Поэтому для вывода имен методов класса используется метод Object.singletonjnethods: Math.singleton_methods #=>["acos". "loglO", "atan2", ... ] В дополнение к этим методам вывода имен в классе Module определяется ряд пре- дикатов для осуществления проверок на наличие в указанном классе или модуле указанного метода экземпляра: String.public_method_defined? :reverse String.protected_method_defined? :reverse String.private_method_defined? :initialize String.method_defined? :upcase! # => true # => false # => true # => true
8,4. Методы 329 Предикат Module.method_defined? проверяет наличие указанного открытого или защищенного метода. У него такое же предназначение, как и предиката Object, respond to?. В Ruby 1.9 в качестве второго аргумента можно передать false, указав тем самым, что унаследованные методы нужно проигнорировать. 8.4.2. Получение объектов метода Для запроса указанного метода нужно вызвать метод method в отношении любого объекта или метод instance_method в отношении любого модуля. Первый из них возвращает связанный с получателем и объект класса Method, который можно вы- звать, а второй возвращает объект класса UnboundMethod. В Ruby 1.9 за счет вызова методов publ icjnethod и publ ic_instance_method поиск можно ограничить откры- тыми методами. Эти методы и возвращаемые ими объекты уже рассматривались в разделе 6.7: "s".method(:reverse) # => Method-объект String.instance_method(:reverse) # => UnboundMethod-объект 8.4.3. Вызов Method-объектов Несколько раньше в этой главе, а также в разделе 6.7 уже упоминалось, что можно использовать метод method в отношении любого объекта, чтобы получить Method- объект, который представляет метод, названный в отношении этого любого объек- та. Method-объекты, точно так же как и Proc-объекты, имеют метод call; этот метод можно использовать для вызова метода. В большинстве случаев вызвать указанный метод определенного объекта намного проще с помощью метода send: "hello".send :upcase # => "HELLO": вызывается метод экземпляра Math.send(:sin. Math::PI/2) # => 1.0: вызывался метод класса Метод send вызывает в отношении своего получателя метод, указанный в его пер- вом аргументе, передавая этому методу любое количество остальных аргументов. Имя «send» (отправка) возникло согласно идиоме объектно-ориентированного программирования, в которой вызов метода называется «отправкой сообщения» объекту. Метод send может вызывать для объекта любой указанный метод, включая закры- тые и защищенные методы. Нам уже попадался метод send, который использовался для вызова закрытого ме- тода remove_const объекта Module. Поскольку на самом деле глобальные функции являются закрытыми методами класса Object, метод send можно использовать для вызова этих методов в отношении любого объекта (хотя делать это таким спосо- бом вряд ли когда-нибудь захочется): “hello".send :puts. "world" # Выводится "world"
330 Глава 8. Отражение и метапрограммирование В качестве альтернативы методу send в Ruby 1.9 определяется метод public send. Этот метод работает так же, как и send, но игнорирует закрытые и защищенные методы и вызывает только открытые методы: "hello".public_send :puts, "world" # Выдается ошибка NoMethodError Метод send является одним из самых важных методов, определяемых в клас- се Object, но имеет довольно распространенное имя и в подклассах может быть переопределен. Поэтому в Ruby в качестве его синонима определяется метод _ send__, при попытке удаления или предопределения которого выдается предупре- ждение. 8.4.4. Методы определения, отмены определения и присвоения псевдонимов Если нужно определить новый метод экземпляра какого-нибудь класса или мо- дуля, следует воспользоваться методом definejnethod. Этот метод экземпляра, принадлежащий классу Module, в качестве своего первого аргумента воспринимает имя нового метода (в виде обозначения). Тело метода представлено либо Method- объектом, переданным в качестве второго аргумента, либо блоком. Важно усвоить, что definejnethod является закрытым методом. Чтобы его использовать с целью вызова, нужно быть внутри класса или модуля: # Добавление метода экземпляра по имени "ш" к классу "с" с телом "Ь" def add_method(c, m, &b) c.class_eval { definejnethod(m, &b) } end add_method(String, :greet) { "Hello. " + self } "world".greet # => "Hello, world" ОПРЕДЕЛЕНИЕ МЕТОДОВ ДОСТУПА К АТРИБУТАМ Методы attr_reader и attr_accessor (которые были рассмотрены в разделе 7.1.5) также определяют новые методы для класса. Они, как и метод define_method, являются закрытыми методами класса Module и могут быть легко созданы на основе реализации define_method. Эти методы, создающие другие методы — превосходный пример того, какую пользу приносит метод define_method. Следует заметить, что закрытый характер этих методов не создает затрудне- ний, поскольку они предназначены для использования внутри определения класса.
8.4. Методы 331 Для определения метода класса (или любого синглтон-метода) с помощью define method нужно вызвать этот метод в отношении обособленного класса (eigenclass): def add_class_method(c. m, &b) eigenclass = class «с; self: end eigenclass.class_eval { define_method(m, &b) } end add_class_method(String, :greet) {|name| "Hello, " + name } String, greet ("world") # => "Hello, world" В Ruby 1.9 проще будет вызвать метод define_singleton_method, определяемый в классе Object: String.define_singleton_method( :greet) {|name| "Hello, " + name } Недостатком метода defi ne method является его неспособность задавать тело ме- тода, предполагающее использование блока. Если нужно динамически создать метод, воспринимающий блок, понадобится использование инструкции def со- вместно с методом class eval. А если создаваемый метод достаточно динамичен, то передать блок методу class eval может не представиться возможным, и вместо этого придется указать определение метода в виде вычисляемой строки. Соответ- ствующие примеры будут приведены в этой главе. Для создания синонима или псевдонима существующего метода обычно исполь- зуется инструкция alias: alias plus + # Назначение "plus" синонимом оператора + Но при динамическом программировании иногда вместо этого приходится ис- пользовать метод alias_method. Этот метод, так же как и метод define_method, явля- ется закрытым методом класса Module. Будучи методом, он может воспринимать в качестве аргументов не жестко заданные в исходном коде обязательные иден- тификаторы, а два произвольных выражения. (В этом качестве он также требует использования запятой между своими аргументами.) Метод al ias_method часто ис- пользуется для выстраивания цепочки псевдонимов существующих методов. При- ведем простой пример, который еще будет встречаться в этой главе: # Создание псевдонима для метода m в классе (или модуле) с def backup(c, m, prefix="original") n = :"#{prefix}_#{m}" # Вычисление псевдонима c.class_eval { # Этот код предопределен закрытостью alias_method alias_method n. m # Создание псевдонима n для метода m } end backup(String, :reverse) "test",original_reverse # => "tset"
332 Глава 8. Отражение и метапрограммирование Из материалов раздела 6.1.5 известно, что для отмены определения метода можно воспользоваться инструкцией undef. Эта инструкция работает только в том слу- чае, если имя метода можно выразить в программе жестко заданным идентифика- тором. Для применения динамического удаления метода, чье имя было вычислено программой, имеется на выбор два метода: remove method или undefjnethod. Оба они относятся к закрытым методам класса Modul е. Метод removejnethod удаляет опреде- ление метода из текущего класса. Если метод определен в надклассе, то эта версия не будет унаследована. Метод undefjnethod работает строже: он предотвращает лю- бой вызов указанного метода через экземпляр класса, даже если это была унасле- дованная версия данного метода. Если при определении класса необходимо предотвратить в дальнейшем любые динамические внесения изменений в этот класс, следует просто вызвать принад- лежащий этому классу метод freeze. После такой «заморозки» класс не может быть изменен. 8.4.5. Обработка неопределенных методов Когда алгоритм разрешения имени (рассмотренный в разделе 7.8) не может найти метод, то вместо него он ищет метод по имени method missing. При вызове этого метода первым аргументом служит обозначение, указывающее на имя не найден- ного метода. За этим обозначением следуют все аргументы, которые должны были быть переданы исходному методу. Если с вызовом метода связан какой-нибудь блок, то этот блок также передается методу methodjnissing. Исходная реализация методаmethodjnissing, принадлежащая модулю Kernel, про- сто выдает исключение NoMethodError. Это исключение, не будучи перехваченным, приводит к выходу из программы с сообщением об ошибке, чего обычно и следует ожидать при попытке вызова несуществующего метода. Определение для класса своего собственного метода method missing дает возмож- ность обработать любой вид вызова в экземплярах класса. Метод-перехватчик method missing является одним из наиболее мощных динамических средств языка и одной из наиболее востребованных технологий метапрограммирования. Чуть позже в этой главе мы еще увидим примеры его применения. А сейчас рассмотрим следующий пример кода, в котором метод method missing добавляется к классу Hash. Это позволяет запрашивать или устанавливать значение любого указанного ключа, как будто ключ является именем метода: class Hash # Разрешение запросов и установок значений хэша, как будто они являются # атрибутами. # Имитация методов-получателей и методов-установщиков для любого ключа. methodjnissing(key, *args) text = key.to_s if text[-l,l] == "=" # Если ключ заканчивается # символом =, установка значения
8.5. Перехватчики 333 self[text.chop.to_sym] = args[0] el se self [key] end end end h= {} h.one = 1 puts h.one # Удаление = из ключа # В противном случае... # ... возвращение значения ключа # Создание пустого хзш-объекта # То же самое, что и h[:one] = 1 # Выводит 1. Так же, как при # использовании puts h[:one] 8.4.6. Установка видимости метода В разделе 7.2 были термины public, protected и private. Они похожи на ключе- вые слова языка, но на самом деле являются закрытыми методами экземпляра, определенными в классе Module. Эти методы часто используются в статической части определения класса. Но с помощью метода class_eval они также могут быть использованы в динамическом процессе: String.class_eval { private :reverse } "hello".reverse # NoMethodError: метод 'reverse' является закрытым На них похожи методы pri vate_cl assjnethod и publ ic_cl assjnethod, за исключением того, что они работают с методами класса и сами по себе являются открытыми: # Придание методам Math характера закрытых # Теперь нужно включать Math, чтобы вызвать его методы Math.pri vate_classjnethod *Math.singletonjnethods 8.5. Перехватчики В классах Module, Class и Object реализован ряд методов обратного вызова, или перехватчиков. Они не определяются по умолчанию, но если их определить для модуля, класса или объекта, то они будут вызываться при возникновении опреде- ленных событий. При этом предоставляется возможность расширить поведение Ruby при создании подклассов на основе классов, при включении модулей или при определении методов. Имена методов-перехватчиков (за исключением неко- торых, не рекомендуемых к применению и не рассматриваемых в данной книге) оканчиваются на «ed>. Когда определяется новый класс, Ruby вызывает для надкласса этого нового класса метод класса inherited, передавая ему в качестве аргумента объект нового класса. Это позволяет классам наращивать поведение или вводить ограничения для своих потомков. Вспомним, что методы класса наследуются, поэтому метод inherited будет вызван, если он определен в любом из предков нового класса. Для
334 Глава 8. Отражение и метапрограммирование получения уведомления о каждом новом определяемом классе нужно определить метод Ob j ect. i nher i ted: def Object.inherited(c) puts "класс #{c} < #{self}" end Когда модуль включается в класс или в другой модуль, вызывается метод класса i ncl uded включаемого модуля, использующий в качестве аргумента объект класса или модуля, в который он был включен. Это дает включаемому модулю возмож- ность как угодно наращивать или изменять возможности класса — фактически это дает возможность модулю определять свое собственное значение для метода j ncl ude. Кроме добавления методов к классу, в который он включен, модуль с ме- тодом included может также изменять существующие методы класса, например: module Final # Класс, включающий Final, не может иметь # подклассов def self.included(c) # При включении в класс с c.instance_eval do # Определение метода класса для с def jnherited(sub) # Для определения подклассов raise Exception, # И аварийного завершения с выдачей исключения "Попытка создания подкласса #{sub} для Final-класса #{self}" end end end end Точно так же если в модуле определен метод класса по имени extended, этот ме- тод будет вызываться при каждом использовании модуля для расширения объ- екта (с помощью метода Object.extend). Разумеется, в качестве аргумента метода extended будет использоваться расширяемый объект, и метод extended может мани- пулировать им как угодно. В дополнение к перехватам для слежения за классами и модулями, в которые они включены, они также осуществляют перехват с целью слежения за методами классов и модулей и синглтон-методами произвольных объектов. Если для любо- го класса или модуля определить метод класса по имени method_added, то он будет вызван, когда для класса или модуля определяется метод экземпляра: def String.method_added(name) puts "К классу String добавлен новый метод экземпляра #{name}" end Учтите, что метод класса method_added наследуется подклассами того класса, в ко- тором он определен. Но перехватчику не передаются никакие параметры класса, поэтому сообщить о том, был ли добавлен указанный метод к классу, в котором определен метод method_added, или к его подклассу, невозможно. Эту проблему можно обойти, определив перехватчик inherited для любого класса, в котором определен перехватчик method_added. Затем метод inherited может определить ме- тод method_added для каждого подкласса.
8.6. Трассировка 335 Когда для какого-нибудь объекта определяется синглтон-метод, для этого объекта вызывается метод singleton_method_added, которому передается имя нового метода. Учтите, что для классов синглтон-методы являются методами класса: def String.singleton_method_added(name) puts "К классу String добавлен новый метод класса #{name}" end Интересно, что Ruby вызывает этот перехватчик singleton_method_added при пер- воначальном определении самого метода-перехватчика. Продемонстрируем еще одно применение перехватчика. В данном случае singleton_method_added опреде- лен как метод экземпляра любого класса, включающего модуль. Уведомление вы- дается в отношении любого синглтон-метода, добавленного к экземплярам этого класса: # Включение этого модуля в класс предохраняет экземпляры этого класса от # добавления # к ним синглтон-методов. Любые добавленные синглтон-методы тут же удаляются. module Strict def singleton_method_added(name) STDERR.puts "Предупреждение: к Strict-объекту добавлен синглтон #{name}" eigenclass = class « self: self: end eigenclass.class_eval { removejnethod name } end end В дополнение к method_added и s i ng 1 eton_method_added существуют перехватчики для отслеживания фактов удаления или отмены определения методов экземпляров и синглтон-методов. Когда из класса или модуля удаляется метод или отменяет- ся его определение, то для этого модуля вызываются перехватчики method_removed и method_undefined. Когда из объекта удаляется синглтон-метод или отменяется его определение, то для этого объекта вызываются перехватчики si ng 1 eton_method_ removed и singleton_method_undefined. И в заключение следует заметить, что методы methodjnissing и constjnissing, рас- смотренные в этой главе, также ведут себя как методы-перехватчики. 8.6. Трассировка В Ruby определен ряд средств для трассировки выполнения программы. В основ- ном их польза проявляется при отладке кода и выводе осмысленных сообщений об ошибках. Два самых простых средства — это реально существующие ключевые слова языка:___FILE_и____LINE_. Эти выражения ключевых слов всегда вычисля- ются в имя файла и номер строки внутри этого файла, на которой они появляются, позволяя сообщению об ошибке указать точное место, в котором оно было сгене- рировано: STDERR.puts "#{_FILE__}:#{__LINE_): неверные данные"
336 Глава 8. Отражение и метапрограммирование Попутно следует заметить, что методы Kernel .eval, Object.instance_eval и Module, class eval воспринимают имя файла (или какое-нибудь другое строковое значе- ние) и номер строки в качестве двух своих конечных аргументов. Если вычисля- ется код, извлеченный из файла какого-нибудь типа, то эти аргументы можно ис- пользовать при вычислениях для определения значений___FILE___и__LINE__. Вы, наверное, обратили внимание, что когда исключение выдается, но не обраба- тывается, то на консоль выводится сообщение об ошибке, в котором содержит- ся информация об имени файла и номере строки. Эта информация, конечно же, основана на значениях___FILE__и____LINE_. Каждый объект Exception имеет свя- занную с ним обратную трассировку, которая указывает точное место выдачи ис- ключения, где был вызван метод, вызвавший исключение, где был вызван этот метод и т. д. Метод Except!on.backtrace возвращает строковый массив, в котором содержится вся эта информация. В первом элементе этого массива указывается место выдачи исключения, а каждый последующий элемент представляет собой состояние стека на одну позицию выше. Но чтобы получить текущую трассировку стека, необязательно выдавать исклю- чение. Текущее состояние стека вызовов возвращается методом Kernel .caller в той же форме, которая возвращается методом Exception. backtrace. Если аргумен- ты не используются, метод caller возвращает трассировку стека, первым элемен- том которой является тот метод, вызвавший метод, из которого был вызван caller. То есть cal1ег[0] указывает на место, из которого был вызван текущий метод. Ме- тод caller можно также вызвать с аргументом, указывающим, сколько стековых фреймов извлекать с начала обратной трассировки. По умолчанию используется значение 1, a caller(0)[0] указывает на то место, из которого был вызван метод caller. Это, к примеру, означает, что caller[0] — то же самое, что и caller(0)[l], и что cal 1ег(2) — это то же самое, что и caller[l.. -1]. Трассировки стека, возвращаемые Exception. backtrace и Kernel .call er, также вклю- чают имена методов. До появления Ruby 1.9 для извлечения имен методов прихо- дилось проводить разбор строки трассировки стека. Но в Ruby 1.9 имя текущего выполняемого метода (в виде обозначения) можно получить, воспользовавшись методом Kernel.__method_или его синонимом — Kernel.___callee_. Метод method хорошо сочетается с FILE и LINE : raise "Неверное утверждение в #{ method } из #{ FILE }:#{ LINE }" Учтите, что__method_возвращает имя, под которым метод был определен перво- начально, даже если метод был вызван через псевдоним. Вместо того чтобы отобразить лишь имя файла и номер строки, в которой произо- шла ошибка, можно пойти еще дальше и вывести эту самую строку кода. Если в программе определить глобальную константу по имени SCRIPT_LINES__и присво- ить ей значение хэша, то методы requi re и 1 oad будут добавлять к этому хэшу запи- си для каждого загружаемого файла. Ключами хэша будут служить имена файлов, а значениями, связанными с этими ключами, будут массивы, содержащие строки этих файлов. Если нужно включить в хэш и основной файл (а не только востре- буемые файлы), то инициализацию следует провести следующим образом: SCRIPT_LINES_ = {_FILE_ => File.readlines(_FILE_)}
8.7. Модули Objectspace и GC 337 После этого можно будет получать текущую строку исходного кода из любого ме- ста своей программы, воспользовавшись следующим выражением: SCRIPT_LINES_[_FILE_][_LINE_-1] С помощью метода Kernel ,trace_var Ruby позволяет отслеживать присваивание значений глобальным переменным. Этому методу нужно передать обозначение с именем глобальной переменной и строку или блок кода. Когда значение назван- ной переменной будет изменяться, будет вычислена строка или вызван блок кода. Когда указан блок кода, новое значение переменной передается ему в качестве аргумента. Для прекращения отслеживания переменной следует вызвать метод Kernel .untrace var. В следующем примере обратите внимание на использование caller[l] для определения того места в программе, из которого был вызван блок отслеживания переменной: # Вывод сообщения при каждом изменении значения переменной $SAFE trace_var(:$SAFE) {|v| puts "Значение переменной $SAFE установлено в #{v} из #{cal1er[l]}" Завершает список методов трассировки метод Kernel .set_trace_func, который ре- гистрирует Proc-объекты, вызванные после выполнения каждой строки Ruby- программы. Метод set_trace_func может оказаться полезен при создании модуля отладки, позволяющего осуществлять построчный проход программы, но здесь вдаваться в подробности этого процесса мы не собираемся. 8.7. Модули ObjectSpace и GC В модуле ObjectSpace определены несколько низкоуровневых методов, которые могут порой пригодиться для отладки и метапрограммирования. Наиболее при- мечательным из них является метод-итератор each_object, которые может извле- кать любой объект (или любой экземпляр указанного класса), о котором известно интерпретатору: # Вывод списка всех известных классов ObjectSpace.each_object(Class) {|с| puts с } Метод ObjectSpace._id2ref является противоположностью методу Object, objected: в качестве аргумента он воспринимает идентификатор объекта (ID) и возвращает соответствующий объект или выдает ошибку RangeError, если объекта с таким ID не существует. Метод-завершитель ObjectSpace.define_fjnalizer позволяет вызвать зарегистри- рованный Ргос-объект или блок кода, когда указанный объект подпадает под про- цесс сбора мусора. Но при регистрации подобного завершителя следует проявлять осторожность, поскольку блоку завершителя не разрешено использовать объект, подвергшийся процессу сбора мусора. Любые значение, необходимые для завер- шения существования объекта, должны быть записаны в области видимости блока
338 Глава 8. Отражение и метапрограммирование завершителя, чтобы они были доступны без получения значения из объекта, на ко- торый он ссылается. Для удаления всех блоков-завершителей, зарегистрирован- ных за объектом, следует воспользоваться методом ObjectSpace. undefine_finalizer. И последним методом ObjectSpace является метод ObjectSpace.garbage_collect, ко- торый заставляет работать имеющийся в Ruby сборщик мусора. Функции, выпол- няемые при сборке мусора, доступны также в модуле GC. Метод GC.start является синонимом для метода ObjectSpace. garbage_col 1 ect. Сборка мусора может быть вре- менно отключена с помощью метода GC. di sabl е и вновь включена методом GC. епаЫе. Сочетание методов _id2ref и define_fInal jzer позволяет определять объекты «со слабыми ссылками», которые содержат ссылки на значения, не препятствуя этим значениям попадать под сборку мусора, если эти объекты по каким-то причинам становятся недоступными. В качестве примера можно обратиться к классу WeakRef, принадлежащему стандартной библиотеке (в lib/weakref.rb). 8.8. Создание своих собственных управляющих структур Использование в Ruby блоков, наряду с присущим этому языку синтаксисом, не требующим применения круглых скобок, существенно облегчает определение методов-итераторов, которые похожи на управляющие структуры и ведут себя соответствующим образом. Простым примером может послужить метод loop, принадлежащий классу Kernel. В этом разделе мы создадим еще три подобных примера. Приводимые здесь примеры используют существующий в Ruby API, предназначенный для создания потоков; возможно, чтобы разобраться во всех тон- костях, придется прочитать раздел 9.9. 8.8.1. Отложенное и повторяющееся выполнение: after и every В примере 8.1. определяются глобальные методы с именами after и every. Каждый из этих методов должен иметь связанный с ним блок и воспринимает числовой аргумент, представляющий собой количество секунд. Метод after создает новый поток и тут же возвращает Thread-объект. Только что созданный поток бездейству- ет указанное количество секунд, а затем вызывает (без аргументов) предоставлен- ный методу блок. Метод every действует похожим образом, но вызывает блок пе- риодически, бездействуя между вызовами указанное количество секунд. Вторым аргументом метода every является значение, передаваемое блоку при его первом вызове. Возвращаемое после каждого вызова значение становится тем самым значением, которое передается блоку при следующем вызове. Блок, связанный с методом every, для предотвращения последующих вызовов может использовать инструкцию break.
8.8. Создание своих собственных управляющих структур 339 Приведем в качестве примера код, использующий after и every: require 'afterevery' l.upto(5) {|i| after i { puts i} } # Замедленный вывод чисел от 1 до 5 sleep(5) # Ожидание в течение пяти секунд every 1, 6 do |count| # Теперь замедленный вывод чисел от 6 до 10 puts count break if count == 10 count +1 # Следующее значение счетчика end sleep(6) # Предоставление коду времени # на работу Вызов метода sleep в конце кода предотвращает выход из этого примера про- граммы до того момента, когда поток, созданный методом every, завершит свой подсчет. Теперь, показав пример использования методов after и every, мы готовы представить их реализацию. Если в реализации метода Thread. new попадутся непо- нятные места, не забудьте обратиться к разделу 9.9. Пример 8.1. Методы after и every # # Определение методов класса Kernel after и every для отсрочки выполнения блока # кода. # Примеры: # # after 1 { puts "готово" } # every 60 { redraw_clock } # # Оба метода возвращают Thread-объекты. Для отмены выполнения кода нужно для # возвращенных объектов вызвать метод kill. # # Учтите, что это весьма упрощенная реализация. Более серьезный вариант реализации # использовал бы для всех задач единый глобальный поток таймера, позволяющий # заполучить способ извлечения значения блока с отложенным выполнением, # и предоставить способ ожидания завершения всех незаконченных задач. # # Выполнение блока после бездействия в течение указанного количества секунд, def after(seconds, &block) Thread.new do # В новом потоке... sleep(seconds) # Первое бездействие block.call # Затем вызов блока end # И сразу же возвращение Thread-объекта end # Повторяющееся бездействие, после которого осуществляется выполнение блока. # Передача значения блоку при первом вызове. # При последующих вызовах передача значения прежнего вызова. def every(seconds. value=nil, &block) , Л и-иллл пи/’/зиид _a>
340 Глава 8. Отражение и метапрограммирование Пример 8.1 (продолжение) Thread.new do loop do sleep(seconds) value = block.call (value) end end end # В новом потоке... # Бесконечный цикл (или цикл до # срабатывания в блоке инструкции break) # Бездействие # И вызов блока # Теперь повторение... # every возвращает Thread-объект 8.8.2. Безопасное применение потоков путем синхронизации блоков При написании программ, использующих несколько потоков, важно, чтобы два потока не пытались изменять один и тот же объект в одно и то же время. Один из путей создания таких условий заключается в помещении кода, который должен быть обезопасен от потока, в блок, связанный с вызовом метода synchronize объек- та Mutex. Этот вопрос также подробно рассматривается в разделе 9.9. В примере 8.2 мы пойдем чуть дальше и с помощью глобального метода по имени synchronized создадим имитацию присущего языку Java ключевого слова synchronized. Метод synchronized предполагает использование блока и одного аргумента в виде объ- екта. Он получает мьютекс, связанный с объектом, и использует для вызова бло- ка метод Mutex, synchronize. Дело усложняется тем, что в отличие от Java-объекта, Ruby-объект не имеет связанного с ним мьютекса. Поэтому в примере 8.2 также определяется метод экземпляра по имени mutex, принадлежащий классу Object. Интересно, что в реализации этого метода mutex метод synchronized используется в своем новом стиле — в виде ключевого слова! Пример 8.2. Простые синхронизированные блоки require 'thread' # Ruby 1.8 хранит Mutex в этой библиотеке # Получение мьютекса, связанного с объектом о. а затем вычисление # блока, находящегося под защитой этого мьютекса. # Все это работает как ключевое слово synchronized из языка Java. def synchronlzed(o) о.mutex.synchronize { yield } end # Метод Object.mutex еще не существует, и мы должны его определить. # Этот метод возвращает уникальный мьютекс для каждого объекта # и всегда возвращает один и тот же мьютекс для любого конкретного объекта. # Он осуществляет "ленивое" создание мьютексов, требующих синхронизации для # безопасного использования потоков. class Object # Возвращение мьютекса для этого объекта, создание его по необходимости. # Сложность в том, чтобы обеспечить невозможность одновременного вызова
8.9. Не найденные методы и константы 341 # этого метода двумя потоками и в конечном счете предотвратить создание # двух разных мьютексов, def mutex # Возвращение мьютекса, если объект его уже имеет return Р__mutex if @__mutex # В противном случае мьютекс для объекта нужно создать. # Чтобы сделать это в безопасном режиме, мы должны синхронизировать наш # объект класса. synchronized(self.class) { # Вторичная проверка: пока мы вводим этот блок synchronized, # мьютекс мог уже создать какой-нибудь другой поток. Р__mutex = @___mutex || Mutex.new } # Возвращаемое значение - @__mutex end end # Определенный выше метод Object.mutex нуждается в блокировке класса, # если объект еще не имеет мьютекса. Если класс до сих пор не имеет своего # собственного мьютекса, то класс класса (Class-объекта) # будет блокирован. Чтобы предотвратить бесконечную рекурсию, нам нужно # убедиться, что Class-объект имеет мьютекс. Class.instance_eval { @__mutex = Mutex.new } 8.9. He найденные методы и константы Метод method missing является ключевой частью имеющегося в Ruby алгоритма поиска методов (о чем уже упоминалось в разделе 7.8), и он предоставляет мощ- ный способ захвата и обработки произвольных вызовов, относящихся к объекту. Метод const_miss 1 ng, определенный в классе Module, выполняет такую же функцию в отношении алгоритма поиска констант и позволяет на лету осуществлять вы- числение или «ленивую» инициализацию констант. В следующих далее примерах продемонстрирована работа обоих методов. 8.9.1. Работа const_missing с кодовыми константами Юникода В примере 8.3 определяется модуль Unicode, предназначенный для определения констант (строк в кодировке UTF-8) для каждого элемента Юникода от U+0000 и до U+10FFFF. Единственным практически приемлемым способом поддержки та- кого количества констант является использование метода constjni ssi ng. В коде выдвигается предположение, что если уже была ссылка на константу, значит вполне возможна и повторная ссылка, поэтому метод const missing вызывает ме- тод Module.const set для определения реально существующей константы и ссылки на каждое вычисляемое ею значение.
342 Глава 8. Отражение и метапрограммирование Пример 6.3. Работа const_missing с кодовыми константами Юникода # Этот модуль предоставляет константы, определяющие строки формата UTF-8 # для всех кодов Юникода. Для их ленивого определения в нем используется метод # constjnissing. # Примеры: # copyright = Unicode::U00A9 # euro = Unicode::U20AC # infinity = Unicode::U221E module Unicode # Этот метод позволяет определить константы кодов Юникода ленивым образом. def self,const_missing(name) # Неопределенная константа передается в виде # обозначения # Проверка правильности оформления имени константы. # Заглавная U сопровождается шестнадцатеричным числом # в диапазоне 0000-10FFFF. if name.to_s =~ rU([0-9a-fA-F]{4.5} 110[0-9a-fA-F]{4})$/ # $1 - это подходящее шестнадцатеричное число. # Преобразование в целое число. codepoint = $l.to_i(16) # Преобразования числа в строку формата UTF-8 с помощью магии # Array.pack. utf8 = [codepoint].pack("U") # Превращение строки UTF-8 в неизменяемую. utf8.freeze # Определение реально существующей константы для ускорения поиска # и. если поиск состоится, возвращения текста в формате UTF-8. const_set(name. utf8) else # Выдача ошибки для константы, переданной в неприемлемой форме. raise NameError, "Неопределенная константа: Unicode::#{name}" end end end 8.9.2. Отслеживание вызовов методов с помощью метода method_missing В этой главе мы уже показывали расширение класса Hash, использующее метод methodjnissing. Теперь, в примере 8.4, мы покажем использование метода method_ missing для передачи произвольных вызовов от одного объекта к другому. В дан- ном примере мы делаем это, чтобы вывести трассировочные сообщения для объ- екта. В примере 8.4 определяется метод экземпляра Object.trace класса TracedObject. Метод trace возвращает экземпляр TracedObject, который использует метод method_ mi ssing для захвата вызовов, их трассировки и передачи их отслеживаемому объ- екту. Воспользоваться этим методом можно следующим образом:
8.9. Не найденные методы и константы 343 а = [l,2,3].trace("a") a. reverse puts а[2] puts a.fetch(3) При этом будет выдана следующая трассировочная информация: Вызов: a. reversed в tracel.rb:66 Возвращение: [3. 2. 1] из a.reverse к tracel.rb:66 Вызов: a.fetch(3) в tracel.rb:67 Выдача: IndexError;index 3 out of array из a.fetch Обратите внимание, что в дополнение к демонстрации использования метода method_miss1ng в примере 8.4 также показывается применение методов Module, instancejnethods, Module.undefjnethod и Kernel .caller. Пример 6.4. Вызов трассировочных методов с помощью метода method_missing # Вызов трассировочного метода в отношении любого объекта для получения нового # объекта, который ведет себя так же. как и оригинал, но отслеживает # все вызовы методов в отношении этого объекта. Если отслеживается более одного # объекта, указывается имя, появляющееся в выходной информации. По умолчанию # сообщения будут отправляться на STDERR, но можно указать любой поток (или любой # объект, воспринимающий строки в качестве аргумента для оператора «). class Object def trace(name="", stream=STDERR) # Возвращение TracedObject, проводящего отслеживание и делегирующего нам # все остальное. TracedObject.new(self, name, stream) end end # Этот класс использует method_missing для отслеживания вызовов методов, # а затем делегирования их какому-нибудь другому объекту. Из него удаляется # большинство его собственных методов экземпляров, чтобы их нельзя было заполучить # на пути к методу methodjnissIng. # Учтите, что будут отслеживаться лишь тем методы, которые были вызваны посредством TracedObject. # Если делегирующий объект вызывает метод, относящийся к нему же, то такой вызов # отслежен не будет. class TracedObject # Отмена определения всех некритичных для работы открытых методов экземпляра. # Обратите внимание на использование Module.instancejnethods # и Module.undefjnethod. instancejnethods.each do |m| m = m.to_sym # В Ruby 1.8 вместо обозначений возвращаются строки next if m == :object_id || m == :_______id__ || m == :__send__ undefjnethod m end продолжение &
344 Глава 8. Отражение и метапрограммирование Пример 8.4 (продолжение) # Инициализация данного экземпляра TracedObject. def initialized, name, stream) @o = o # Объект, в адрес которого происходит делегирование @n = name # Имя объекта, которое будет фигурировать # в сообщениях отслеживания @trace = stream # Место отправки сообщений отслеживания end # Это ключевой метод класса TracedObject. Он вызывается практически для любого # вызова метода в отношении TracedObject. def method_missing(*args, &block) m = args.shift # Первый аргумент - имя метода begin # Отслеживание вызова метода. arglist = args.map {|а| a.inspect}.joinC. ') @trace « "Вызов: #{@n}.#{m}(#{arglist}) в #{caller[O]}\n" # Вызов метода в отношении нашего объекта-делегата и получение # возвращаемого значения. г = @o.send m, *args, &block # Отслеживание обычного возвращения из метода. @trace « "Возвращение: #{r.inspect} из #{@n}.#{m} к #{caller[O]}\n" # Возвращение любого значения, возвращенного объектом-делегатом, г rescue Exception => е # Отслеживание аварийного возвращения из метода. @trace « "Выдача: #{е.class}:#{е} из #{@n},#{m}\n" # и повторная выдача любого исключения, выданного объектом-делегатом, raise end end # Возвращение к объекту-делегату. def __delegate @о end end 8.9.3. Объекты, синхронизированные благодаря делегированию В примере 8.2 мы встречались с глобальным методом synchronized, воспринимаю- щим объект и выполняющим блок под защитой мьютекса, связанного с этим объ- ектом. Основная часть примера состояла из реализации метода Object.mutex. Сам метод synchronized особой сложностью не отличался: def synchronized(o) о.mutex.synchronize { yield } end
8.10. Динамически создаваемые методы 345 В примере 8.5 проводится модификация этого метода, чтобы при его вызове без блока он возвращал установленную вокруг объекта оболочку SynchronizedObject. Сам по себе SynchronlzedObject является делегирующим классом-оболочкой, основанным на использовании метода methodjnlsslng. Он очень похож на класс TracedObject из примера 8.4, но написан в виде подкласса имеющегося в Ruby 1.9 класса Basi cOb ject, поэтому ему не требуется явное удаление методов экземпляра, как в случае с подклассом класса Object. Учтите, что код в этом примере не может использоваться автономно; для его работы требуется ранее определенный метод Object, mutex. Пример 8.5. Синхронизированные методы, полученные с помощью method_missing def synchronized(o) if block_given? о.mutex.synchronize { yield } else SynchronizedObject.new(o) end end # Делегирующий класс-оболочка использует метод methodjnlsslng для безопасного # применения потоков. # Вместо того чтобы расширять класс Object и удалять из этого расширения методы, # мы просто расширяем класс BasicObject, определяемый в Ruby 1.9. BasicObject не # является наследником класса Object или класса Kernel, поэтому методы BasicObject # не могут вызывать какие-либо методы высокого уровня: их там просто нет. class SynchronlzedObject < BasicObject def initialize(o): ^delegate = o: end def __delegate; @delegate: end def method_missing(*args, &block) @delegate.mutex.synchronize { ©delegate.send *args, &block } end end 8.10. Динамически создаваемые методы Важной технологической составляющей метапрограммирования является исполь- зование методов, создающих другие методы. Примером этому могут послужить методы attr_reader и attr_accessor (рассмотренные в разделе 7.1.5). Эти закрытые методы экземпляра, определенные в классе Module, используются наподобие клю- чевых слов внутри определений классов. В качестве своих аргументов они вос- принимают имена атрибутов и динамически создают методы с этими именами. Приводимые далее примеры являются вариантами этих методов создания средств доступа к атрибутам, и в них демонстрируются два разных подхода к динамиче- скому созданию подобных методов.
346 Глава 8. Отражение и метапрограммирование 8.10.1. Определение методов с помощью dass_eval В примере 8.6 определяются закрытые методы экземпляра класса Module с имена- ми readonly и readwrite. Эти методы по характеру работы похожи на attr_reader и attr accessor и приводятся здесь, чтобы показать порядок реализации подобных методов. По сути, их реализация не представляет никаких трудностей: сначала в реализации readonly и readwrite формируется строка Ruby-кода, содержащая ин- струкцию def, необходимая для определения соответствующих методов доступа. Затем эта строка кода вычисляется с помощью метода class eval (который уже рассматривался в этой главе). При таком использовании метода с 1 ass eval строка кода подвергается небольшому общему синтаксическому анализу. Но преимуще- ство состоит в том, что для определяемых нами методов не требуется непосред- ственного применения каких-нибудь API отражения; они могут запрашивать или устанавливать значения переменной экземпляра напрямую. Пример 8.6. Методы атрибутов, создаваемые с помощью clBSS_eval class Module private # Оба следующих метода являются закрытыми # Этот метод работает так же, как и attr_reader, но имеет более короткое имя def readonly(*syms) return if syms.size == 0 # Если аргументы отсутствуют - ничего не # делать code = "" # Начинаем с пустой строки кода # Генерация строки Ruby-кода для определения методов атрибута чтения # Обратите внимание на то. как обозначение вставляется в строку кода. syms.each do |s| # Для каждого обозначения code « "def #{s}; @#{s}; end\n" # Определение метода end # В заключение сгенерированный код вычисляется методом class_eval, чтобы # создать методы экземпляра. class_eval code end # Этот метод работает как attr_accessor. но имеет более короткое имя. def readwrite(*syms) return if syms.size == 0 code = "" syms.each do |s| code « "def #{s}: @#{s} end\n" code « "def #{s}=(value): @#{s} = value; end\n" end class_eval code end end
8.10. Динамически создаваемые методы 347 8.10.2. Определение методов с помощью define_method Пример 8.7 показывает еще один подход к созданию методов доступа к атрибутам. Метод attributes немного похож на метод readwrite, определенный в примере 8.6. Вместо того чтобы воспринимать в качестве аргументов любое количество имен атрибутов, он предусматривает использование одного хэш-объекта. Этот хэш бу- дет использовать имена атрибутов в качестве своих ключей и отображать имена атрибутов на значения по умолчанию, используемые для этих самых атрибутов. Метод cl ass_attrs работает так же, как и метод attri butes, но определяет не атри- буты экземпляра, а атрибуты класса. Вспомним, что Ruby разрешает опускать фигурные скобки вокруг хэш-литералов, когда они являются заключительным аргументом в вызове метода. Поэтому метод attributes может быть вызван следующей строкой кода: class Point attributes :х => 0. :у => О end В Ruby 1.9 мы можем воспользоваться более компактным хэш-синтаксисом: class Point attributes х:0, у:0 end Это еще один пример, подтверждающий гибкость Ruby-синтаксиса в создании ме- тодов, которые ведут себя как ключевые слова. Реализация метода attributes в примере 8.7 существенно отличается от реализа- ции метода readwrite в примере 8.6. Вместо определения строки Ruby-кода и ее вычисления с помощью метода с 1 ass eva 1 метод attri butes определяет тело метода доступа к атрибутам в блоке и определяет методы с помощью метода def i nejnethod. Поскольку эта технология определения методов не позволяет вставлять иденти- фикаторы в тело метода напрямую, следует положиться на такие методы отра- жения, как instance_variable_get. По этой причине методы доступа к атрибутам, определенные с помощью метода attributes, наверное, менее эффективны, чем те, которые определены с помощью метода readwri te. Интересной особенностью метода attributes является то, что он не хранит исход- ные значения для атрибутов в явном виде в каких-нибудь переменных класса. Вместо этого исходные значения для каждого атрибута захватываются областью видимости блока, используемого для определения метода. (Дополнительный ма- териал о подобных замкнутых выражениях содержится в разделе 6.6.) Метод class_attrs определяет атрибуты класса очень просто: он вызывает метод attributes для обособленного класса нашего класса. Это означает, что получаю- щиеся при этом методы используют переменные экземпляра класса (которые рас- сматривались в разделе 7.1.16), а не обычные переменные класса.
348 Глава 8. Отражение и метапрограммирование Пример 8.7. Создание методов доступа к атрибутам с помощью define_method class Module # Этот метод определяет методы чтения и записи атрибута для указанных ему # атрибутов, но предусмаривает использование хэш-аргумента, отображающего # имена атрибутов на их исходные значения. Генерируемые методы чтения # атрибутов возвращают исходное значение, если переменная экземпляра еще # не была определена. def attributes(hash) hash.each_pair do |symbol, default) # Для каждой пары # атрибут-исходное_значение getter = symbol # Имя метода-получателя setter = :"#{symbol}=" # Имя метода-установщика variable = :"@#{symbol}" # Имя переменной экземпляра definejnethod getter do # Определение метода-получателя if instance_variable_defined? variable instance_variable_get variable # Возвращение переменной, # если она определена else default # Иначе возвращение исходного значения end end definejnethod setter do |value) # Определение метода-установщика instance_variable_set variable. # Установка значения переменной value # экземпляра по значению аргумента end end end # Этот метод работает так же, как и метод attributes, но в отличие от него # определяет методы класса путем вызова attributes в отношении обособленного # класса, а не в отношении self. # Обратите внимание, что определяемые классы используют переменные экземпляра # класса, а не обычные переменные класса. def class_attrs(hash) eigenclass = class « self; self; end eigenclass.class_eval { attributes(hash) } end # Оба метода являются закрытыми private attributes, :class_attrs end 8.11. Выстраивание цепочки псевдонимов Как уже выяснилось, при метапрограммировании в Ruby часто задействуется ди- намическое определение методов. Не менее часто используется и динамическое
8.11. Выстраивание цепочки псевдонимов 349 изменение методов. При этом применяется технология, которую мы называем вы- страиванием цепочки псевдонимов (alias chaining)1. Организация ее работы вклю- чает следующее: О сначала создается псевдоним для метода, который будет подвержен изменениям. Этот псевдоним предоставляет имя для неизмененной версии метода; О затем определяется новая версия метода. Эта новая версия должна вызвать неизмененную версию метода, используя псевдоним, но перед этим или после этого в новую версию могут быть добавлены любые необходимые вам функцио- нальные возможности. Учтите, что эти два этапа могут применяться многократно (до тех пор пока при каждом таком применении будут использоваться разные псевдонимы), создавая цепочку методов и псевдонимов. В этот раздел включены три примера выстраивания цепочки псевдонимов. Пер- вый из них осуществляет статическое выстраивание такой цепочки; то есть в нем используются обычные инструкции alias и def. А во втором и третьем примерах этот процесс приобретает динамичность; при выстраивании в них цепочки псевдо- нимов из произвольно указываемых методов используются методы al ias method, def i nejnethod и class_eval. 8.11.1. Отслеживание загрузки файлов и определение новых классов В примере 8.8 представлен код, отслеживающий все загрузки файлов и все новые определения классов, происходящие в программе. Соответствующий отчет выво- дится при выходе из программы. Этот код можно задействовать для «дооснаще- ния» существующей программы, чтобы лучше разобраться в происходящих в ней событиях. Одним из способов использования этого кода станет включение в самое начало программы следующей строки: require 'classtrace'. Но проще всего вос- пользоваться ключом -г при запуске Ruby-интерпретатора: ruby -rclasstrace my_program.rb --traceout /tmp/trace Ключ - г загружает указанную библиотеку до запуска программы. Дополнитель- ные сведения об аргументах командной строки Ruby-интерпретатора изложены в разделе 10.1. В примере 8.8 используется статическое выстраивание цепочки псевдонимов для отслеживания всех вызовов методов Kernel. requi re и Kernel. 1 oad. Для отслежива- ния определений новых классов в нем определяется метод-перехватчик Object, inherited. А для выполнения блока кода в случае прерывания работы программы, в нем задействуется метод Kernel .at exit. (В этом примере также задействована 1 Этот прием также называется обезьяньим латанием, но так как этот термин приобрел иро- нический оттенок, мы решили от него отказаться. В качестве шуточной альтернативы ино- гда используется термин утиное латание.
350 Глава 8. Отражение и метапрограммирование инструкция END, рассмотренная в разделе 5.7.) Помимо выстраивания цепочек псевдонимов для методов require и load и определения метода Object.inherited, единственным изменением, вносимым этим кодом в глобальное пространство имен, является определение модуля по имени ClassTrace. Все необходимые для отслеживания сведения об обстановке хранятся в констан- тах внутри этого модуля, поэтому мы не засоряем пространство имен глобальны- ми переменными. Пример 8.8. Отслеживание загрузки файлов и определений новых классов # Мы определяем этот модуль, чтобы сохранить нужное нам глобальное состояние, # таким образом мы не вносим в глобальное пространство имен ничего, кроме самого # необходимого. module ClassTrace # Этот массив содержит наш список загруженных файлов и получивших определения # новых классов. # Каждый элемент является подмассивом, содержащим новый класс, получивший # определение, или загруженный файл и фрейм стека, указывающий, где он был # определен или загружен. Т = [] # Массив для храненния загруженных файлов # Теперь определяем константу OUT для указания, куда направлять выходную # информацию с результатами отслеживания. # Значение по умолчанию указывает на STDERR,, но оно может извлекаться и из # аргументов командной строки if х = ARGV.index("--traceout") # Если аргумент присутствует, OUT = File.open(ARGV[x+l], "w”) # Открыть указанный файл ARGV[x,2] = nil # и удалить аргументы else OUT = STDERR # В противном случае установить исходное # значение, указывающее на STDERR end end # Выстраивание цепочки псевдонимов, шаг 1: Определение псевдонимов для исходных # методов alias original_require require alias original_load load # Выстраивание цепочки псевдонимов, шаг 2: Определение новых версий методов def require(file) ClassTrace::Т « [fi1e,cal 1er[O]] # Запоминание, что было загружено и где original_require(file) # Вызов исходного метода end def load(*args) ClassTrace::Т « [args[O],caller[O]] # Запоминание, что и где было загружено original_load(*args) # Вызов исходного метода end
8.11. Выстраивание цепочки псевдонимов 351 # Этот метод-перехватчик вызывается при каждом определении нового класса def Object.inherited(c) ClassTrace::T « [c,caller[0]] # Запоминание что и где было определено end # Метод Kernel,at_exit регистрирует блок, запускаемый при выходе из программы f Мы используем его для выдачи отчета о собранных нами сведениях о файлах и классах at_exit { о = ClassTrace::OUT о.puts "="*60 о.puts "Загруженные файлы и определенные классы:” о.puts ”="*60 ClassTrace::Т.each do |what.where| if what.is_a? Class # Отчет об определении класса # (с иерархией) о.puts "Определен: #{what.ancestors.join('<-')} из #{where}" else # Отчет о загруженном файле о.puts "Загружен: #{what} из #{where}" end end 8.11.2. Выстраивание цепочки методов для обеспечения безопасности работы потоков В двух ранее показанных в этой главе примерах затрагивался вопрос безопасности работы потоков. В примере 8.2 был определен метод synchronized (основанный на методе Object.mutex), который выполнял блок, защищенный мьютекс-объектом. Затем, в примере 8.5, метод synchronized был переопределен, чтобы при вызове без блока он возвращал окружающую объект оболочку SynchronizedObject, закры- вающую доступ к любому методу, вызываемому через этот помещенный в обо- лочку объект. Теперь, в примере 8.9, мы еще раз проведем наращивание метода synchronized, чтобы при его вызове внутри определения класса или модуля вы- страивалась цепочка псевдонимов указанных методов для добавления синхрони- зации. Выстраивание цепочки псевдонимов осуществляется нашим методом Module.syn- chronizejnethod, который для определения подходящих псевдонимов для любого заданного метода (включая методы-операторы вроде +) использует в свою очередь вспомогательный метод Module.create_alias. После определения этих новых методов в классе Module в примере 8.9 проводится новое переопределение метода synchronized. Когда метод вызывается внутри клас- са или модуля, он вызывает метод synchroni zejnethod для каждого переданного ему обозначения. Но в то же время представляет интерес возможность его вызова и без аргументов; при таком использовании он добавляет синхронизацию к следующему
352 Глава 8. Отражение и метапрограммирование определяемому методу экземпляра. (Для получения уведомления о добавлении нового метода в нем используется метод-перехватчик method_added.) Учтите, что код, приводимый в этом примере, основан на использовании метода Object , mutex, определение которого приводилось в примере 8.2, и класса SynchronizedObject из примера 8.5. Пример 8.9. Выстраивание цепочки псевдонимов для обеспечения безопасной работы потоков # Определение метода Module.synchronizejnethod, который выстраивает цепочку # псевдонимов методов экземпляра таким образом, чтобы они синхронизировались. # к примеру, перед запуском. class Module # Это вспомогательная функция для выстраивания цепочки псевдонимов. # Получая имя метода (в виде строки или обозначения) и префикс, она создает # для метода уникальный псевдоним и возвращает имя псевдонима в виде # обозначения. Любые знаки пунктуации в исходном имени метода будут # преобразованы в числа, позволяя создавать псевдонимы для операторов. def create_alias(original, prefix="alias") # Прикрепление префикса к исходному имени и преобразование знаков # пунктуации aka = ”#{prefix}_#{original}” aka.gsub!(/([\=\|\&\+\-\*\/\л\!\?\-\Ж\<\>\L\]])/) { num = $l[0] # В Ruby l.B символ -> порядковый номер num = num.ord if num.is_a? String # В Ruby 1.9 символ -> # порядковый номер + num.to_s } # Продолжение добавления подчеркиваний, пока не будет получено имя, # которое еще не использовалось aka += while method_defined? aka or private_method_defined? aka aka = aka.to_sym # Преобразование псевдонима в обозначение aliasjnethod aka, original # Фактическое создание псевдонима aka # Возвращение псевдонима end # Включение в цепочку псевдонимов указанного метода для добавления к нему # синхронизации def synchronize_method(m) # Сначала создание псевдонима для несинхронизированной версии метода, aka = create_alias(m. "unsync") # Теперь переопределение оригинала для вызова псевдонима # в синхронизированном блоке. # Нам нужно, чтобы определяемый метод мог воспринимать блок, поэтому мы # не можем воспользоваться методом definejnethod и вынуждены вместо этого # вычислять строку с помощью метода class_eval. Учтите, что все. что
8.11. Выстраивание цепочки псевдонимов 353 # находится между ЭД{ и соответствующей скобкой }, является не блоком, # а строкой в двойных кавычках. class_eval ЭД{ def #{m}(*args, &block) synchronized(self) { #{aka}(*args, &block) } end } end fТеперь этот глобальный метод synchronized может использоваться тремя различеными f способами. def synchronized(*args) # Способ первый: с одним аргументом и блоком, синхронизируя объект # и выполняя блок if args.size == 1 && block_given? args[0].mutex.synchronize { yield } # Способ второй: с одним аргументом, не являющимся обозначением, и без блока, # возвращая оболочку SynchronizedObject elsif args.size == 1 and not args[0].is_a? Symbol and not block_given? SynchronizedObject.new(args[0]) # Способ третий: при вызове в отношении модуля без блока, выстраивая цепочку # указанных методов для добавления к ним синхронизации. Или, если аргументы # не используются, выстраивая цепочку из определяемых далее методов. elsif self.is_a? Module and not block_given? if (args.size >0) # Синхронизация указанных методов args.each {|m| self.synchronizejnethod(m) } else # Если методы не указаны - синхронизация определяемых далее методов eigenclass = class«self; self: end eigenclass.class_eval do # Использование обособленного класса # для определения методов класса # Определение method_added для уведомления при определении # следующего метода def 1 nejnethod :method_added do |name| # Сначала удаление этого метода-перехватчика eigenclass.class_eval { removejnethod :method_added } # Затем синхронизация только что добавленного метода self.synchronizejnethod name end end end # Способ четвертый: любые другие варианты вызова считаются ошибкой else raise ArgumentError. "Неверные аргументы для synchronized" end end
354 Глава 8. Отражение и метапрограммирование 8.11.3. Выстраивание цепочки методов для осуществления отслеживания Пример 8.10 является вариантом примера 8.4, поддерживающим отслеживание указанных методов объекта. В примере 8.4 для определения метода Object.trace, возвращающего объект, являющийся оболочкой отслеживания, использованы де- легирование полномочий и метод methodjnissing. В этой версии используется вы- страивание цепочки для непосредственного изменения методов. В ней определя- ются методы trace! и untrace!, чтобы выстроить и расстроить цепочку указанных методов объекта. Интересной особенностью этого примера является то, что в нем выстраивание це- почки происходит другим способом, нежели в примере 8.9; в нем просто опреде- ляются синглтон-методы объекта, а внутри синглтон-методов используется метод super для выстраивания цепочки к исходному определению метода экземпляра. При этом никакие псевдонимы методов не создаются. Пример 8.10. Выстраивание цепочки для осуществления отслеживания с помощью синглтон-методов # Определение методов экземпляра trace! и untrace! для всех объектов. # trace! "сцепляет" указанные методы путем создания синглтон-методов, # добавляющих отслеживание, а затем использующих super для вызова оригинала. # untrace! удаляет синглтон-методы для прекращения отслеживания. class Object # Отслеживание указанных методов, отправляя выходную информацию на STDERR, def trace!(*methods) 0_traced = @_traced || [] # Запоминание набора отслеживаемых методов # Если методы не были указаны, использование всех открытых методов, # определяемых напрямую (не унаследованных) классом этого объекта methods = publ 1 cjnethods (false) if methods.size == 0 methods.map! {|m| m.to_sym } # Преобразование строк в обозначения methods -= @_traced # Удаление уже отслеживаемых методов return If methods.empty? # Досрочное возвращение, если делать нечего @_traced |= methods # Добавление методов к отслеживаемым # Отслеживание факта начала отслеживания этих методов STDERR « "Отслеживание #{methods.joint'. ')} для #{object_1d}\n" # Синглтон-методы определяются в обособленных классах eigenclass = class « self; self; end methods.each do |m| # Для каждого метода m # Определение отслеживаемой синглтон-версии метода т. # Вывод информации по отслеживанию и использование метода super # для вызова метода экземпляра, подвергающегося отслеживанию. # Нужно, чтобы определяемые методы могли воспринимать блоки, поэтому # definejnethod использовать нельзя и вместо этого следует
8.11. Выстраивание цепочки псевдонимов 355 # вычислять строковое значение. # Следует заметить, что все находящееся между IQ{ # и соответствующей закрывающей скобкой }, является строкой # в двойных кавычках, а не блоком. Также следует заметить, что # здесь применены два уровня вставки в строку. #{} вставляется # при определении синглтон-метода. А \#{} вставляется, когда # синглтон-метод вызывается. eigenclass.class_eval ЭД{ def #{m}(*args, &block) begin STDERR « "Вход: #{m}(\#{args.Joint', ’)})\n” result = super STDERR « "Выход: #{m} c \#{result}\n" result rescue STDERR « "Авария: #{m}: \#{$!.class}: \#{$!.message}" raise end end } end end # Прекращение отслеживания указанных методов # или всех отслеживаемых методов def untrace!(*methods) if methods.size == 0 # Если методы не указаны, methods = @_traced # задействуются все отслеживаемые # на данный момент методы STDERR « "Прекращение отслеживания всех методов для #{object_id}\n" else # В противном случае прекращение отслеживания methods.map! {|т| m.to_sym } # Преобразование строк в обозначения methods &= @_traced # всех указанных отслеживаемых методов STDERR « "Прекращение отслеживания #{methods.joint', ')} для #{object_id}\n" end @_traced -= methods # Удаление их из набора отслеживаемых методов # Удаление отслеживаемых синглтон-методов из обособленного класса # Заметьте, что для class_eval здесь используется блок, а не строка (class « self; self; end),class_eval do methods.each do |m| removejnethod m # undefjnethod может работать некорректно end end # Если отслеживаемых методов больше не осталось. # удаление нашей переменной экземпляра продолжение #
356 Глава 8. Отражение и метапрограммирование Пример 8.10 (продолжение) If @_traced.empty? remove_instance_varlable :@_traced end end end 8.12. Предметно-ориентированные языки Довольно часто целью метапрограммирования на языке Ruby является создание предметно-ориентированных языков (domain-specific languages, DSL). DSL — это простое расширение Ruby-синтаксиса (с применением методов, похожих на клю- чевые слова) или API, позволяющий решать задачи или представлять данные в бо- лее естественном виде, чем без их применения. В качестве примеров мы возьмем решение проблем из области вывода данных в формате XML и определим для их решения два DSL — наипростейший и более сложный1. 8.12.1. Простой вывод XML с помощью метода method_missing Для генерации XML-вывода начнем с простого класса по имени XML. Покажем при- мер возможного применения XML: pagetitle = "Проверочная страница для XML.generate" XML.generate(STDOUT) do html do head do title { pagetitle } comment "Это проверка" end body do hl(:style => "font-family:sans-ser1f") { pagetitle } ul :type=>"square" do li { Time.now } li { RUBYVERSION } end end end end Этот код не похож на XML, он скорее похож на какой-нибудь Ruby-код. Сгенери- рованные им выходные данные (с некоторыми разрывами строк, добавленными для разборчивости) выглядят следующим образом: 1 Полноценное решение этой проблемы можно увидеть в Builder API Джима Вейрича (Jim Weirich) по адресу: http://builder.rubyforge.org
8.12. Предметно-ориентированные языки 357 <html><head> <titlе>Проверочная страница для XML.generate</title> <!-Это проверка --> </head><body> <hl style='font-family:sans-serif'>npoBepo4HaB страница для XML.generate </hl> <ul type='square'> <1 i>2007-08-19 16:19:58 -0700</li> <li>1.9.0</li> </ulx/body></html> При реализации этого класса и поддерживаемого им синтаксиса генерации XML мы опирались на: О имеющуюся в Ruby структуру блоков; О свойственное Ruby необязательное применение круглых скобок при вызове методов; О имеющийся в Ruby синтаксис передачи методу хэш-литералов без фигурных скобок; О методmethodjnissing. В примере 8.11 демонстрируется реализация этого простого DSL. Пример 8.11. Простой DSL, предназначенный для генерации XML-вывода class XML # При создании экземпляра этого класса указывается поток или объект, # содержащий выходную информацию. Это может быть любой объект, # отзывающийся на «(строка). def initial ize(out) @out = out # Запоминание места отправки выходной информации end # Вывод указанного объекта в виде символьных данных - CDATA, возвращение nil. def content(text) @out « text.to_s ni 1 end # Вывод указанного объекта в виде комментария, возвращение nil. def comment(text) @out « #{text} ni 1 end # Вывод тега с указанным именем и атрибутами. # Если есть блок, вызов его для вывода или возвращения содержимого. # Возвращение ni1. def tag(tagname, attributes={}) # Вывод имени тега @out « "<#{tagname}” , . продолжение &
358 Глава 8. Отражение и метапрограммирование Пример 8.11 (продолжение) # Вывод атрибутов attributes.each {|attr.value| @out « " #{attr}='#{value}'" } if block_given? # В этом блоке есть содержимое @out « ’>' # Завершение открытого тега content = yield # Вызов блока для вывода или возвращения содержимого if content # Если возвращено какое-нибудь содержимое. @out « content.to_s # вывод его в виде строки end @out « ”</#{tagnanie}>" # Закрытие тега else # В противном случае, если это пустой тег, то он просто закрывается. @out « '/>' end nil # Теги выводятся сами по себе, поэтому никакое содержимое не # возвращается end # Расположенный ниже код превращает все это из обычного класса в DSL. # Во-первых: любой неизвестный метод рассматривается как имя тега, alias methodjnissing tag # Во-вторых: для нового экземпляра класса запускается блок, def self.generate(out, &block) XML.new(out).i nstance_eva1(&Ы ock) end end 8.12.2. Проверяемый вывод XML-кода с помощью генерации метода Класс XML, показанный в примере 8.11, подойдет для генерации правильно вы- строенного XML, но в нем отсутствует проверка на отсутствие ошибок, гаранти- рующая, что выходная информация соответствует любой конкретной грамматике XML. В следующем далее примере 8.12 мы добавили несколько простых проверок на отсутствие ошибок (хотя и не вполне достаточных для гарантии полной надеж- ности, для которой потребовался бы намного более обширный пример). На самом деле здесь два DSL в одном примере. Первый из них — это DSL для определения грамматики XML: набора тегов и разрешенных для каждого тега атрибутов. Ис- пользуется он следующим образом: class HTMLForm < XMLGrammar element :form. :action => REQ. :method => "GET". :enctype => "application/x-www-form-urlencoded", :name => OPT
8.12. Предметно-ориентированные языки 359 element : input. element :textarea, element :button, end :type => "text", :name => OPT, :value => OPT, imaxlength => OPT. :size => OPT, :src => OPT. :checked => BOOL. :disabled => BOOL, readonly => BOOL :rows => REQ, :cols => REQ, :name => OPT, disabled => BOOL, readonly => BOOL :name => OPT, :value => OPT, :type => "submit", disabled => OPT Этот первый DSL определяется методом класса XMLGrammar.element. Он использу- ется путем создания на основе XMLGrammar нового класса. Метод element предпола- гает использование в качестве своего первого аргумента имени тега, а в качестве второго аргумента — хэша с допустимыми атрибутами. Ключами хэша служат имена атрибутов. Эти имена могут отображаться на устанавливаемые для атрибу- тов значения по умолчанию, на константу REQ для обязательных атрибутов или на константу ОРТ — для необязательных. Вызов метода el ement приводит к генерации метода с указанным именем в определенном вами подклассе. Определяемый вами подкласс класса XMLGrammar является вторым DSL, который можно использовать для генерации XML-выхода, соответствующего определен- ным вами правилам. Класс XMLGrammar не содержит метода methodjnissing, поэтому он не разрешит использовать тег, не являющийся частью грамматики. А метод tag, предназначенный для вывода тегов, при проверке ваших атрибутов выдаст ошиб- ку. Нам следует воспользоваться подклассом, генерирующим грамматику наподо- бие класса XML из примера 8.11: HTMLForm.generate(STDOUT) do comment "Это простая HTML-форма" form :name => "регистрация". :action => "http://www.example.com/register.cgi" do content "Имя:" input :name => "имя" content "Адрес:" textarea :name => "адрес", :rows=>6, :cols=>40 do "Введите, пожалуйста, свой почтовый адрес" end button { "Submit" } end end Реализация класса XMLGrammar показана в примере 8.12. Пример 8.12. DSL для проверяемого вывода XML class XMLGrammar # Создайте экземпляр этого класса, указав поток или объект для хранения # выходной информации. Объект должен отзываться на метод «(Строка). def 1nitialize(out) @out = out # Запоминание места отправки выходной информации end продолжение &
360 Глава 8. Отражение и метапрограммирование Пример 8.12 (продолжение) # Вызов блока для экземпляра, выводящего информацию в указанный поток. def self,generate(out, &block) new(out).instance_eval(&block) end # Определение разрешенного грамматикой элемента (или тега). # Этот метод класса является DSL, предназначенным для спецификации # грамматики, и в нем определяются методы, которые ложатся в основу DSL, # предназначенного для XML-вывода. def self.element(tagname, attributes={}) @allowed_attributes ||= {} @allowed_attributes[tagname] = attributes class_eval £Q{ def #{tagname}(attributes={}, &block) tag(:#{tagname}.attributes,&block) end } end # Эти константы используются при определении значений атрибутов. ОРТ = :opt # для необязательных атрибутов REQ = :req # для обязательных атрибутов BOOL = .-bool # для атрибутов, значение которых заключается в их # собственных именах def self.allowed_attributes @allowed_attributes end # Вывод указанного объекта в виде символьных данных - CDATA, возвращение nil. def content(text) @out « text.to_s nil end # Вывод указанного объекта в виде комментария, возвращение nil. def comment(text) @out « #{text} nil end # Вывод тега с указанным именем и атрибутами. # Если есть блок, вызов его для вывода или возвращения содержимого. # Возвращение niI. def tag(tagname. attributes={}) # Вывод имени тега @out « "<#{tagname}"
8.12. Предметно-ориентированные языки 361 # Получение атрибутов, разрешенных для этого тега. allowed = self.class.allowed_attributes[tagname] # Сначала нужно убедиться, что каждый из атрибутов разрешен. # Допуская, что они разрешены, вывод всех указанных атрибутов. attributes.each_paiг do |key.value| raise "неизвестный атрибут: #{key}" unless al lowed.include?(key) @out « ” #{key}='#{value}'" end # Теперь просмотр разрешенных атрибутов, выявление обязательных # атрибутов, которые были опущены, и атрибутов со значениями по # умолчанию, которые можно вывести. allowed.each_pair do |key,value) # Если этот атрибут уже был выведен - ничего не делать. next if attributes,has_key? key if (value == REQ) raise "Обязательный атрибут ’#{key}' пропущен в <#{tagname}>" elsif value.is_a? String @out « " #{key}='#{value} end end if block_given? # В этом блоке есть содержимое @out « '>' # Завершение открытого тега content = yield # Вызов блока для вывода или возвращения содержимого if content # Если возвращено какое-нибудь содержимое @out « content.to_s # Вывод его в виде строки end @out « "</#{tagname}>" # Закрытие тега else # В противном случае, если это пустой тег. то он просто закрывается. @out « '/>' end nil # Теги выводятся сами по себе, поэтому никакое содержимое # не возвращается end end

Глава 9 Платформа Ruby
364 Глава 9. Платформа Ruby В библиотеке ядра Ruby определен богатый и мощный API, который служит в ка- честве платформы, на которой создаются ваши программы. И на изучение и осво- ение этого API, в особенности ключевых классов St г 1 ng, Array, Hash, Enumerabl e и 10, стоит потратить усилия. Если не знать о существовании методов, определяемых этими классами, то можно впустую убить время, заново изобретая то, что вам уже и так предоставлено. Данная глава содержит документацию по этим методам. Но она представляет собой не какой-то там подробный API-справочник, а попытку проиллюстриро- вать работу важных методов всех наиболее значимых базовых классов и модулей и некоторых наиболее часто используемых классов из стандартной библиотеки на коротких примерах кода. Цель главы — дать возможность ознакомиться с широ- ким кругом существующих методов, чтобы затем вспомнить при надобности об их существовании и найти описание нужного метода с помощью инструментального средства ri. Это довольно объемная глава, она разбита на разделы, охватывающие следующие темы: О строки и работа с текстом; О регулярные выражения; О числа и математические вычисления; О работа с датами и временем; О модуль Enumerable и коллекции Array, Hash и Set; О ввод-вывод и работа с файлами; О работа в сети; О потоки и параллельные вычисления. Вы увидите, что код в начале главы будет представлен в форме однострочных фрагментов, демонстрирующих работу отдельных методов. Но ближе к концу, когда будут описываться работа в сети и потоки, примеры станут длиннее, и в них будет показано, как решаются часто встречающиеся задачи, вроде создания сете- вого клиента или использования потоков для параллельной обработки элементов коллекции. 9.1. Строки В главе 3 был рассмотрен используемый в Ruby синтаксис строкового литерала, а также St г 1 ng-операторы для объединения (+), наращивания («), повторения (*) и индексирования ([]) строк. В этом разделе мы подробно остановимся на этой теме, демонстрируя работу названных методов, относящихся к классу String. В подразделах, которые следуют за этим кратким обзором API, конкретные об- ласти рассматриваются более подробно. Начнем с методов, которые предоставляют поименованные альтернативы для не- которых операторов, рассмотренных в главе 3:
9.1. Строки 365 s = "hello" s.concatC world”) # Синоним для «. Вызывающее мутацию добавление к s. # Возвращает новое значение s. s.insert(5. " there”) # То же самое, что и s[5] = " there". Изменяет s. # Возвращает новое значение s. s.si 1ce(0,5) s.si 1 cel(5.6) # То же самое, что и s[0,5]. Возвращает подстроку. # Стирание. То же самое, что и s[5,6]="". # Возвращает только что удаленную подстроку. s.eql?("hello world") # True. То же самое, что и ==. А вот несколько методов для запроса длины строки: s.length s.size s.bytesize s.empty? "”.empty? # => 5: подсчитывает символы в 1.9, байты в 1.8 # => 5: size является синонимом предыдущего метода # => 5: длина в байтах; только в Ruby 1.9 # => false # => true В число строковых методов для поиска в строке и для замены содержимого входят следующие методы, к некоторым из которых мы еще вернемся, когда будем рас- сматривать регулярные выражения: s = "hello" # Обнаружение позиции соответствия подстроке или шаблону s.index( 'Г) # =5 > 2: индекс первого символа 1 в строке s.index(?l) # =5 - 2: работает и с кодами символов, s. 1 ndex(/1+/) # =5 • 2: и с регулярными выражениями s.index( 'Г ,3) # =х > 3: индекс первого символа 1 в строке в позиции 3 # или после нее s.index('Ruby') # =х > nil: искомая строка не найдена s.rindex(' 1') # =5 > 3: индекс самого правого символа 1 в строке s.rindex('1'.2) # =5 > 2: индекс самого правого символа 1 в строке # в позиции 2 или после нее # Проверка наличия префиксов и суффиков: Ruby 1.9 и более поздних версиях s.start_with? "hell" # => true. Обратите внимание, что "start” # в единственном числе (не "starts”) s.end_with? "bells” # => false # Проверка наличия подстроки s.1nclude?("11") # => s. 1 nclude?(?H) # => true: "hello" включает ”11" false: "hello" не включает символа H # Соответствие шаблону, содержащему регулярные выражения s =- /EaelouJ{2}/ # => nil: "hello" не содержит сдвоенных гласных s.match(/[aeiou]/) {|т| m.to_s} # => "е": возвращает первую гласную # Разбиение строки на подстроки на основе разделителя строки или шаблона "this is it".split # => ["this", "is", "it"]: разбиение по пробелам no # умолчанию "hello".split('l’) # => ["he". "о"] л . и продолжение &
366 Глава 9. Платформа Ruby "1, 2,3".split(/,\s*/) # => использование запятой # и необязательного пробела в качестве разделителя # Разбиение строки на две части плюс разделитель. Только в Ruby 1.9. # Этот метод всегда возвращает массив из трех строк: "banana".partitioncan") #=>["b", "an", "ana"] "banana".rpartitionCan") # => ["ban", "an", "а"]: начинает работу справа "al23b".partition(/\d+/) # => ["a". "123", "b"]: работает и с регулярными # выражениями # Поиск и замена первых (sub, sub!) или всех (gsub. gsub!) # появлений указанной строки или шаблона. # Подробнее sub и gsub будут рассмотрены позже, вместе с регулярными выражениями. s.subCl", "L") # => "hello": Замена первого появления s.gsubC'l". "L") # => "heLLo": Замена всех появлений s.sub!(/(.)(.)/, '\2\1') # => "ehllo”: Поиск соответствия первым двум буквам # и их перестановка s.sub!(/(.)(.)/, "\\2\\1") # => "ehllo": При использовании двойных кавычек # обратные слэши удваиваются # sub и gsub могут также вычислить строку замены внутри блока # Поиск соответствия первой букве каждого слова и замена этой буквы на заглавную "hello world".gsubC/\b./) {|match| match.upcase } # => "Hello World" В последней строке этого примера для преобразования строкового объекта в верх- ний регистр использован метод upcase. В классе Stri ng определен ряд методов для работы с регистрами (но в нем не опре- делены методы для проверки регистра или категории символа): # Методы изменения регистра s - "world" s.upcase s.upcase! s.downcase s.capitalize # Эти методы работают только с символами ASCII # -> "WORLD" # => "WORLD": Непосредственное изменение s # => "world" # -> "World": Перевод первого символа в верхний, # а остальных в нижний регистр s.capitalize! s.swapcase # => "World": Непосредственное изменение s # => "WORLD": Изменение регистра каждого символа # Сравнение строк без учета регистра. (Только для ASCII-текстов) # casecmp работает как <-> и возвращает -1 для меньшего. О для равного, +1 для # большего значения "world".casecmp("WORLD") # -> О "a".casecmp("B”) # -> -1 (<=> в этом случае возвращает 1) В классе String определен ряд полезных методов для добавления или удаления пробельного символа. Многие из них существуют в обычной версии и в версии мутатора (с окончанием !): s - "hello\r\n s. chomp! # Строка с элементом конца строки # -> "hello": удаление с конца одного конца строки
9.1. Строки 367 s.chomp # => "hello": нет элемента конца строки, # соответственно нет и изменений s.chomp! # => nil: возвращение nil, указывающего # на отсутствие изменений s.chompC'o") $/ = # => "hell": удаление "о" с конца строки # Установка глобального разделителя записей $/ # на точку с запятой "hello;".chomp # => "hello": теперь chomp удаляет # точки с запятой и окончания # chop удаляет замыкающие символы или элементы конца строки (\п, \г или \г\п) s = "hello\n" s.chop! s.chop "".chop "".chop! # => "hello": элемент конца строки удален, s изменен. # => "hell": удален последний символ, s не изменен. # => символы для удаления отсутствуют # => nil: изменять нечего # Удаление всех пробельных символов (включая \t, \г, \п) # слева, справа или с обеих сторон # strip!, Istrip! и rstrip! вносят изменения в существующую строку. s - ”\t hello \n" s.strip s.Istrip s. rstrip # Пробельные символы в начале и в конце # => "hello” # => "hello \n” # => "\t hello” # Выравнивание строки по левому краю, по правому краю или по центру поля шириной # в п знакомест. # Для этих методов нет мутаторов. Обратите также внимание на метод printf. s - X s.l Just(3) s.rjust(3) s.center(3) s.center(5, # => "x " # => " X" # => " x " # => "--x--": разрешается заполнение символами, # отличными от пробела s.center(7, # => "-=-х-=-": разрешается заполнение # и несколькими символами Строки могут быть пронумерованы побайтово или построчно с помощью итерато- ров each_byte и each_l 1 пе. В Ruby 1,8 метод each является синонимом для each_l 1 пе, а класс String включает модуль Enumerable. Использования итератора each и род- ственных ему итераторов следует избегать, поскольку из Ruby 1.9 метод each уда- лен и уже не делает строку перечисляемым объектом. В Ruby 1.9 (и в библиотеку jcode в Ruby 1.8) добавлен итератор each char и разрешена посимвольная нумера- ция строк: s - "А\пВ" # Три ASCII-символа на двух строчках s.each_byte {|b| print b. "" } # Выводит "65 10 66 " s.each_line {|1| print 1.chomp} # Выводит "АВ" # Последовательная итерация символов как односимвольных строк продолжение &
368 Глава 9. Платформа Ruby # Работает в Ruby 1.9 или в 1.8 с библиотекой jcode: s.each_char { |с| print с, " " } # Выводит "А \п В " # Нумерация каждого символа как односимвольной строки # Не работает для многобайтовых строк в версии 1.8 # Работает (но неэффективно) для многобайтовых строк в версии 1.9: O.uptoCs.length-1) {|n| print s[n,l], ” ”} # В Ruby 1.9 bytes, s.bytes.to_a s.11nes.to_a s.chars.to_a lines и chars являются псевдонимами # => [65,10,66]: псевдоним для each_byte # => ["А\п","В"]: псевдоним для each_11ne # => ["А”, "\п", "В"] псевдоним для each_char В классе String определен ряд методов для парсинга чисел из строк и для преоб- разования строк в обозначения: ”10”.to_i "10",to_1(2) ”10x”.to_i ” 10".to_1 ”ten".to_l "10".oct ”10".hex "Oxff”.hex "1.1 dozen”.to_f "6.02e23".to_f "one”,to_sym "two".intern # => 10: преобразование строки в целое число # => 2: аргумент представляет систему счисления между # двоичной и тридцатишестеричной # => 10: нечисловой суффикс игнорируется. # То же самое касается oct, hex # => 10: лидирующие пробельные символы игнорируются # => 0: при неприемлемом вводе исключение не выдается # => 8: парсинг строки как восьмеричного целого числа # => 16: парсинг строки как шестнадцатеричного целого числа # => 255: шестнадцатеричные числа могут начинаться с Ох # => 1.1: парсинг лидирующего числа с плавающей точкой # => 6.02е+23: поддерживается экспоненциальная нотация # => :опе - преобразование строки в обозначение # => :two -- Intern является синонимом to_sym И в завершение несколько смешанных методов класса St г 1 ng: # Приращение строки: "a”.succ # => ”b": наследник "а”. Есть также метод succ! "aaz”.next # => "aba": next является синонимом. Есть также метод next! ”a”.upto(”e”) {|с| print с } # Выводится "abode. Upto - итератор на основе succ. # Реверсирование строки: "hellо".reverse # => "olleh”. Есть также метод reverse! # Отладка ”hello\n”.dump # => ”\”hello\\n\"”: Нейтрализация специальных символов "helloXn".inspect # Работает наподобие dump # Перевод из одного набора символов в другой "hello”.trU’aeiou”, "AEIOU") # => "hETTO”: перевод гласных из строчных # в заглавные. Also tr! "hello”.trCaelou", " ") # => "h 11 перевод гласных в пробелы "bead".tr_s("ae1ou", " ") # => "b d": преобразование и удаление дубликатов
9.1. Строки 369 # Получение контрольных сумм "hellо".sum # => 532: слабая 16-битная контрольная сумма "hello".sum(8) # => 20: 8-битная контрольная сумма вместо 16-битной "hello".crypt("ab") # => "ablOJrMf6tlhw": однопроходная криптографическая # контрольная сумма. # Передача двух буквенно-цифровых символов # в качестве случайного набора ("salt"). # Результат может зависеть от платформы # Подсчет, удаление букв "hello".count('aeiou') "hello".delete('aeiou') "hello".squeeze('a-z') и их дубликатов # => 2: подсчет гласных букв в нижнем регистре # => "hll": удаление гласных букв в нижнем регистре. # Есть также метод delete! # => "helo": удаление дубликатов указанного ряда # букв. Есть также метод squeeze! # Если аргументов более одного, берется их пересечение. # Аргументы, начинающиеся с символа А, инвертируются. "hello",count('a-z', ,Aaeiou’) # => 3: подсчет согласных букв в нижнем "hello".delete('a-z', ,Aaeiou') # => регистре "ео: удаление согласных букв # в нижнем регистре 9.1.1. Форматирование текста Как известно, имеющиеся в Ruby строковые литералы в двойных кавычках позво- ляют вставлять в строки произвольные Ruby-выражения. Например: п, animal = 2, "mice" "#{n+l} blind #{animal}" # => '3 blind mice' Этот синтаксис вставки в строковый литерал был рассмотрен в главе 3. В Ruby также поддерживаются и другие технологии вставки значений в строки: в классе String определяется оператор форматирования %, а в модуле Kernel определяются глобальные методы pri ntf и spri ntf. Эти методы и оператор % очень похожи на функцию pri ntf, получившую популяр- ность благодаря языку программирования Си. Одно из преимуществ форматиро- вания в стиле pri ntf над обычной вставкой в строковый литерал состоит в том, что оно позволяет точно управлять шириной поля, что делает его особенно полезным для генерации отчетов с использованием ASCII-символов. Другое преимущество состоит в том, что это форматирование позволяет указать число значащих цифр для отображения в числах с плавающей точкой, что делает его полезным для науч- ных (и некоторых финансовых) приложений. И наконец, форматирование в стиле pri ntf разъединяет значения, предназначенные для форматирования, со строкой, в которую они вставляются. Этим можно воспользоваться для интернационализа- ции и локализации приложения. Далее приводятся примеры использования оператора Я. Полную документацию по директивам форматирования, которые используются этими методами, можно найти в описании метода Kernel. spri ntf:
370 Глава 9. Платформа Ruby # Альтернативные варианты вставок, рассмотренных ранее printfC'Xd blind Xs'. n+1, animal) # Выводится '3 blind mice', возвращается nil sprintf('Xd blind Xs’ , n+1, animal) # => '3 blind mice' 'Xd blind Xs' X [ n+1, animal] # Использование массива в правой части, если # используется более одного аргумента # Форматирование чисел 'Xd' X 10 # => '10': Xd для десятичных целых чисел 'Xx' X 10 # => 'а': шестнадцатеричные целые числа 'XX' X 10 # => 'А': шестнадцатеричные целые числа в верхнем регистре 'Xo' X 10 # => '12': восьмеричные целые числа Xf X 1234.567 # => '1234.567000': числа с плавающей точкой без сокращений 'Хе' X 1234.567 # => '1.234567е+03': принуждение к экспоненциальной нотации ' ХЕ ’ X 1234.567 # => '1.234567е+03': экспоненциальная нотация с буквой Е # в верхнем регистре 'Xg' X 1234.567 # => '1234.57': шесть значащих цифр 'Xg' X 1.23456E12 # => ’1.23456е+12': Использование Xf или Хе # в зависимости от величины # Ширина поля ' X5s' X '<«' # ' <«': выравнивание по правому краю в поле # в пять символов шириной 'X-5s' X '»>' # '»> ': Выравнивание по левому краю в поле # в пять символов шириной 'X5d' X 123 # ' 123’: поле шириной в пять символов 'X05d' X 123 # '00123': заполнение нулями в поле шириной в пять символов # Точность 'X.2f' X 123.456 # '123.46': две цифры после десятичной точки ’Х.2е' X 123.456 # # '1.23е+02': две цифры после десятичной точки = трем значащим цифрам 'Х.бе' X 123.456 # '1.234560е+02': обратите внимание на добавленный нуль 'Х.4д' X 123.456 # '123.5': четыре значащих цифры # Объединение поля и точности 'Ж6.4д' % 123.456 # ' 123.5’: четыре значащих цифры в поле шириной в шесть # символов 'ЖЗб' % 'ruby' # 'ruby': строковый аргумент, превышающий ширину поля 'ЖЗ.Зб' % 'ruby' # 'rub': уточнение, заставляющее усекать строку # Несколько форматируемых аргументов args - ['Syntax Error', 'test.rb', 20] "Xs: in 'Xs' line Xd" X args # Массив аргументов # => "Syntax Error: in 'test.rb' line 20" # Те же аргументы, вставленные в другом порядке! Пригодится для # интернационализации. "X2$s:X3$d: Xl$s" X args # => "test.rb:20: Syntax Error” 9.1.2. Упаковка и распаковка двоичных строк Строки в Ruby наряду с текстовыми могут содержать и двоичные данные. При ра- боте с двоичными файловыми форматами или двоичными сетевыми протоколами
9.2. Регулярные выражения 371 может пригодиться пара методов — Array.pack и String.unpack. Метод Array.pack используется для кодирования элементов массива в двоичную строку, а метод String.unpack используется для декодирования двоичной строки, извлечения из нее значений и возвращения этих значений в массиве. Обе операции, и кодирова- ния и декодирования, проводятся под управлением строки форматирования, где буквы указывают на тип данных и кодировку, а числа указывают на количество повторений. Разобраться с созданием этих строк форматирования не так-то лег- ко, а полный список буквенных кодов можно найти в документации по методам Array. pack и Stri ng. unpack. Приведем ряд простых примеров: а = [1,2.3,4,5.6.7.8,9.10] b = а.pack('110') с = b.unpack!' 1*') с == а m = 'hello world' data = [m.size, m] template = 'Sa*' b = data.pack(template) b.unpack(template) # Массив из 10 целых чисел # Упаковка 10 4-байтовых целых чисел (1) # в бинарную строку b # Декодирование всех (*)4-байтовых целых чисел из b # => true # Сообщение для закодирования # Сначала длина, потом байты # Число в диапазоне 0...255. любое количество # ASCII-символов # => "\v\000helIo world” # => [И, "hello world"] 9.1.3. Строки и кодировки Определенные в классе String методы encoding, encode, encode! и force_encoding, а также класс Encoding, были рассмотрены в разделе 3.2.6. Если вы собираетесь написать программу с использованием Юникода или другой многобайтовой ко- дировки, то, возможно, вам потребуется прочитать этот раздел еще раз. 9.2. Регулярные выражения Регулярное выражение (известное также как regexp, или regex) описывает тексто- вый шаблон. В имеющимся в Ruby классе Regexp1, реализуются регулярные вы- ражения, а в обоих классах, Regexp и String, определяются методы и операторы соответствия шаблону. Подобно многим другим языкам программирования, под- держивающим регулярные выражения, имеющийся в Ruby синтаксис Regexp явля- ется близким (но не абсолютно точным) последователем синтаксиса языка Perl 5. 9.2.1. Литералы регулярных выражений Литералы регулярных выражений разделяются символами прямого слэша: /Ruby?/ # Соответствует тексту "Rub", за которым следует # необязательный символ "у" 1 Программисты, работающие на JavaScript, должны учесть, что в имени Ruby-класса ис- пользуется символ е в нижнем регистре, в отличие от имеющегося в JavaScript класса RegExp.
372 Глава 9. Платформа Ruby Закрывающий слэш-символ не является настоящим разделителем, поскольку за литералом регулярного выражения может следовать от одного и более необяза- тельных символов-флажков (или символов-модификаторов), определяющих до- полнительную информацию о том, как должна быть произведена проверка на со- ответствие шаблону. Например: /ruby?/1 /./mu # Не зависит от регистра: соответствует "ruby" или "RUB" и т. д. # Соответствует символам Юникода в многострочном режиме Доступные символы-модификаторы показаны в табл. 9.1. Таблица 9.1. Символы-модификаторы регулярного выражения Модификатор Описание I м Игнорирование регистра при проверке текста на соответствие Шаблон должен проверяться на соответствие многострочному тексту, поэтому символ разделителя строк рассматривается как обычный символ, позволяя символу точки (.) соответствовать символам разделителей строк X Применение расширенного синтаксиса, позволяющего пробельным символам и комментариям появляться в регулярном выражении О Однократное выполнение вставки #{} при первом вычислении литерала регу- лярного выражения U,e,S,n Интерпретация регулярного выражения как набора символов Юникода (UTF-8), EUC, SJIS или ASCII. Если ни один из этих модификаторов не указан, то пред- полагается, что регулярное выражение использует исходную кодировку Как и при работе со строковыми литералами, для задания разделителей которых используется группа символов ЭД, Ruby позволяет в начале регулярного выраже- ния ставить группу символов Хг, за которой следует выбранный вами разделитель. Это может пригодиться в том случае, когда описываемый шаблон содержит мно- жество символов прямых слэшей, которые не хочется подвергать нейтрализации: Жг|/] # Соответствует одиночному символу слэша, нейтрализация # не требуется Хг[</(.*)>]1 # С этим синтаксисом также разрешено применение символов-флажков Синтаксис регулярных выражений придает символам О, [],{},.,?,+,*, |, ж и $ специальное назначение. Если нужно описать шаблон, включающий один из этих символов буквально, для нейтрализации их предопределенного действия исполь- зуется символ обратного слэша. Если нужно описать шаблон, включающий об- ратный слэш, то этот символ следует удвоить: /\(\)/ # Соответствует открывающей и закрывающей скобкам /\\/ # Соответствует одиночному символу обратного слэша Литералы регулярных выражений ведут себя подобно строковым литералам в двойных кавычках и могут включать такие эскейп-последовательности, как \п, \t и (в Ruby 1.9) \u (полный список эскейп-последовательностей приведен в та- блице 3.1 в главе 3): money = /[$\u20AC\u{a3}\u{a5J]/ # Соответствует знакам доллара, евро, # фунта или йены
9.2. Регулярные выражения 373 Подобно строковым литералам в двойных кавычках, литералы регулярных вы- ражений позволяют делать вставки произвольных Ruby-выражений с использо- ванием синтаксиса #{}: prefix = "," /#{prefix}\t/ # Соответствует запятой, за которой # следует ASCII-символ TAB Учтите, что вставка делается на ранней стадии, перед тем как содержимое регу- лярного выражения подвергается синтаксическому анализу. Это означает, что все специальные символы во вставляемом выражении становятся частью синтаксиса регулярного выражения. Обычно при каждом вычислении литерала регулярно- го выражения вставка делается заново. Но если используется модификатор о, эта вставка выполняется однократно, при первом синтаксическом анализе кода. По- ведение модификатора о лучше пояснить на примере: [1,2].шар{|х| /#{х}/} # => [/1/, /2/] [1,2],шар{|х| /#{х}/о} # => [/1/, /1/] 9.2.2. Фабричные методы регулярных выражений В качестве альтернативы литералам регулярных выражений эти выражения мож- но также создавать с помощью метода Regexp. new или его синонима Regexp. comp 11 е: Regexp.new("Ruby?") # /Ruby?/ Regexp.new("ruby?". Regexp::IGNORECASE) # /ruby?/i Regexp.compile(".", Regexp::MULTILINE. "u") # /./mu Для нейтрализации действий имеющихся в строке специальных символов регу- лярных выражений перед ее передачей конструктору Regexp используется метод Regexp, escape: pattern = ”[a-z]+" # Одна или более букв suffix = Regexp.escapeCO") # Эти символы должны рассматриваться буквально г = Regexp.new(pattern + suffix) # I[a-z]+\(\)/ В Ruby 1.9 фабричный метод Regexp.union создает шаблон, который «объединя- ет» любое количество строковых или Regexp-объектов. (То есть получившийся шаблон соответствует любым строкам, которые соответствуют составляющим его шаблонам.) Ему передается любое количество аргументов или одиночный массив из строк и шаблонов. Этот фабричный метод пригодится для создания шаблонов, соответствующих любому слову или перечню слов. Строки, переданные методу Regexp. uni on, в отличие от тех, которые передаются методам new и compi 1 е, проходят автоматическую нейтрализацию специальных символов: # Соответствие любому из пяти названий языков. pattern = Regexp.union!"Ruby”, "Perl", "Python", /Java(Seript)?/) # Соответствие пустым круглым, квадратным или фигурным скобкам. # Нейтрализация осуществляется автоматически: Regexp.union!"!)", "[]", "{}") # => /\(\)|\[\]|\{\}/
374 Глава 9. Платформа Ruby 9.2.3. Синтаксис регулярных выражений Многие языки программирования поддерживают регулярные выражения, исполь- зуя синтаксис, который приобрел популярность благодаря языку Perl. Всесто- роннее рассмотрение этого синтаксиса не включено в эту книгу, но приводимые далее примеры проведут нас по элементам грамматики регулярных выражений. За этим учебным пособием последует таблица 9.2, в которой содержится сводная справка по синтаксису регулярных выражений. Учебный материал сконцентри- рован на синтаксисе регулярных выражений, используемых в Ruby 1.8, но в нем демонстрируется и ряд свойств, доступных только в Ruby 1.9. За дополнительны- ми сведениями можно обратиться к книге Джефри Фридла (Jeffrey Е. Е Friedl) «Регулярные выражения» (издательство «Питер»). # Литеральные символы /ruby/ # Соответствует "ruby". Большинство символов просто # соответствует само себе. /¥/ # Соответствует знаку йены. В Ruby 1.9 # и Ruby 1.В поддерживаются многобайтовые символы # Классы символов /[Rr]uby/ /rubEye]/ /Eaeiои]/ /[0-9]/ # Соответствует "Ruby" или "ruby" # Соответствует "ruby” или "rube" # Соответствует любой одной гласной в нижнем регистре # Соответствует любой цифре; то же самое, # что и /[01234567В9]/ /Ea-z]/ /ЕА-Z]/ /Ea-zA-ZO-9]/ /EAaeiои]/ # Соответствует любой ASCII-букве в нижнем регистре # Соответствует любой ASCII-букве в верхнем регистре # Соответствует любому символу из тех, что упомянуты выше # Соответствует всему, что не относится к гласным # в нижнем регистре /Еж0-9] # Соответствует всему, что не относится к цифрам # Классы специальных символов /./ # Соответствует любому символу, за исключением символа # новой строки /. /ш # В режиме многострочности символ . соответствует также # и символу новой строки Ad/ AD/ /\s/ AS/ Aw/ # Соответствует цифровым символам: /ЕО-9]/ # Соответствует нецифровым символам: /Еж0-9]/ # Соответствует пробельным символам: /Е \t\r\n\f]/ # Соответствует непробельным символам: /Еж \t\r\n\f]/ # Соответствует одиночному словарному символу: # /EA-Za-zO-9_]/ AW/ # Соответствует несловарному символу: /EAA-Za-zO-9_]/ # Повторение /ruby?/ # Соответствует "rub" или "ruby": символ у является # необязательным
9.2. Регулярные выражения 375 /ruby*/ /ruby+/ /\d{3}/ /\d{3,}/ /\d{3,5}/ # Соответствует "rub” плюс 0 или более символов у # Соответствует "rub" плюс 1 или более символов у # Соответствует исключительно трем цифрам # Соответствует трем или более цифрам # Соответствует 3, 4 или 5 цифрам # Нежадное повторение: Соответствует наименьшему количеству повторений /<.*>/ # Жадное повторение: соответствует "<ruby>perl>" /<.*?>/ # Нежадное повторение: соответствует "<ruby>" # в "<ruby>perl>" # Нежадность также выражается в виде: ??. +? и {n.m}? # Группировка с помощью круглых скобок /\D\d+/ /(\D\d)+/ /([Rr]uby(. )?)+/ # Группировка отсутствует: + означает повторение \d # Группировка: + означает повторение пары \D\d # Соответствует "Ruby". "Ruby. ruby, ruby" и т. д. # Обратные ссылки: /([Rr])uby&\lails/ /(["'])[Л1]*\1/ повторное соответствие указанной ранее группе # Соответствует ruby&rails или Ruby&Rails # Строка в одинарных или двойных кавычках # \1 обозначает соответствие всему, что # первой группе # \2 обозначает соответствие всему, что # второй группе, ит. д. соответствует соответствует # Поименованные группы и обратные ссылки в Ruby 1.9: выражение соответствует # четырехбуквенному палиндрому /(?<first>\w)(?<second>\w)\k<second>\k<first>/ /(?,f’irst,\w)(?'second,\w)\k'second,\k,fjrst'/ # Альтернативный синтаксис # Альтернативы /ruby|rube/ # Соответствует "ruby" или "rube" /rub(y|le))/ # Соответствует "ruby" или "ruble" /ruby(!+|\?)/ # за "ruby" может следовать один или более знаков ! или # один знак ? # Привязки: указывают позицию соответствия ЛПиЬу/ # Соответствует "Ruby" в начале строки или # внутренней строки текста /Ruby$/ # Соответствует "Ruby” в конце строки или # строки текста /\ARuby/ # Соответствует "Ruby" в начале строки /Ruby\Z/ # Соответствует "Ruby" в конце строки /\bRuby\b/ # Соответствует "Ruby" в границах слова /\brub\B/ # \В означает не в границе слова: # соответствует "rub" в "rube" и "ruby", но не # соответствует такому же отдельному слову /Ruby(?=!)/ # Соответствует "Ruby", если сопровождается # восклицательным знаком _ продолжение &
376 Глава 9. Платформа Ruby /Ruby(?!!)/ # Соответствует "Ruby", если не сопровождается # восклицательным знаком # Специальный /R(?#comment)/ синтаксис, использующий круглые скобки # Соответствует "R". Вся остальная часть является # комментарием /R(?i)uby/ # Независимость от регистра при проверке соответствия # "uby" /R(?i:uby)/ /rub(?:y|1e))/ # То же самое # Только группировка, обратная ссылка \1 не создается # Модификатор # символов / # Это не # регуляр R (uby)+ \ /X В таблице 9.2 в этих кодовы х разрешает использование комментариев и игнорирование пробельных комментарий Ruby. Это часть игнорируемого литерала ного выражения. # Соответствует одиночной букве R, # сопровождаемой одним или более "uby" # Использование обратного слэша для неигнорируемого пробела # Закрывающий ограничитель. Не забудьте про модификатор х! приведена сводка синтаксических правил, продемонстрированных х примерах. Таблица 9.2. Син таксис регулярных выражений Синтаксис Соответствие Классы символ IOB Соответствует любому одиночному символу за исключением символа новой строки. Использование модификатора m позволяет ему соответство- вать также и символу новой строки [...] ["...] Соответствует любому одиночному символу в квадратных скобках Соответствует любому одиночному символу из тех, что не находятся в квадратных скобках \w \И \s Соответствует символам, использующимся в словах Соответствует символам, не использующимся в словах Соответствует пробельному символу. Служит эквивалентом [ \t\n\r\f] \S \d \D Соответствует непробельному символу Соответствует цифрам. Служит эквивалентом [0-9] Соответствует нецифровым символам Последователь >ности, альтернативы, группы и ссылки ab a | b (re) Соответствует выражению а, за которым следует выражение b Соответствует либо выражению а, либо выражению Ь Группировка: группирует ге в синтаксическую единицу, которая может быть использована с *, +, ?, | и т. д. Также осуществляет «захват» текста, соответствующего ге, для последующего использования (?:re) Производит группировку, как и (), но без захвата соответствующего текста
9.2. Регулярные выражения 377 Синтаксис Соответствие (?<имя>ге) Группирует подвыражение и захватывает текст, соответствующий ге, как и при использовании (), а также создает именную метку для подвыраже- ния. Используется в Ruby 1.9 (?'имя'ге) Создает именную метку для захвата, так же как и в предыдущем случае. Одиночные кавычки вокруг имени могут быть заменены угловыми скобка- ми. Используется в Ruby 1.9 \1..,\9 Соответствует тому же тексту, который соответствует n-ному сгруппиро- ванному подвыражению \10 Соответствует тому же тексту, который соответствует n-ному сгруппиро- ванному подвыражению, если ранее встречалось именно такое количе- ство подвыражений. Или же соответствует символу с заданной восьмерич- ной кодировкой \к<имя> Соответствует тому же тексту, который соответствует захваченной группе с меткой имя \g<n> Повторно соответствует группе п. При этом п может быть именем группы или номером группы. Сопоставьте \д, который повторно определяет со- ответствие или повторно создает указанную группу, с обычной обратной ссылкой, которая пытается соответствовать тому же тексту, что и в пер- вый раз. Используется в Ruby 1.9 Повторение По умолчанию повторение характеризуется «жадностью» — ему соответ- ствует наибольшее количество появлений. Для «нежадного» соответствия нужно сопроводить квантификаторы *, +, ? или {} символом ?. Тогда соответствие будет связано с наименьшим количеством появлений при сохранении возможности проверки на соответствие оставшейся части вы- ражения. В Ruby 1.9 квантификатор следует сопроводить символом + для «собственнического» (без отслеживания в обратном порядке) поведения ге* ге+ ге? Соответствует нулю или большему количеству появлений ге Соответствует единственному или большему количеству появлений ге Необязательное соответствие: соответствует нулю или единственному появлению ге ге{ п} ге{п,} re{n,m} Соответствует в точности п появлениям ге Соответствует п или большему количеству появлений ге Соответствует по крайней мере п из максимально возможных m появле- ний ге Привязки Привязки не соответствуют символам, а соответствуют позициям между символами с нулевой шириной, «привязывая» соответствие к позиции, которая отвечает определенному условию Соответствует позиции в начале текста $ \А \Z Соответствует позиции в конце текста Соответствует позиции в начале строки Соответствует позиции в конце строки. Если строка заканчивается сим- волом новой строки, то соответствует позиции непосредственно перед символом новой строки \z \G \b Соответствует позиции в конце строки Соответствует позиции, в которой закончилось последнее соответствие Соответствует границам слов, когда находится за пределами квадратных скобок. Внутри квадратных скобок соответствует символу забоя (0x08) \B Соответствует позициям, не совпадающим с границами слов продолжение &
378 Глава 9. Платформа Ruby Таблица 9.2 (продолжение) Синтаксис Соответствие (?=ге) Положительное предварительное утверждение: гарантирует, что следую- щие символы соответствуют ге, но эти символы в соответствующий текст не включаются (?!ге) Отрицательное предварительное утверждение: гарантирует, что следую- щие символы не соответствуют ге (?<=ге) Положительное состоявшееся утверждение: гарантирует, что предыдущие символы соответствуют ге, но эти символы в соответствующий текст не включаются. Используется в Ruby 1.9 (?<!ге) Отрицательное состоявшееся утверждение: гарантирует, что предыдущие символы не соответствуют ге. Используется в Ruby 1.9 Разное (?флажки_уста- новки -флажки_ сброса) Ничему не соответствует, но устанавливает флажки, указанные как флажки_установки, и сбрасывает флажки, которые указаны как флаж- ки сброса. Эти две строки являются комбинациями модификаторов i, m и х, расположенных в любой последовательности. Указанная таким спо- собом установка флажков действует с места ее появления в выражении и сохраняется до конца выражения или до конца группы, заключенной в круглые скобки, частью которой она является, или до тех пор, пока она не будет подменена другим выражением установки флажков (?флажки_уста- новки -флажки_ сброса:х) (?#...) (?>ге) Соответствует х, применяя указанные флажки только для этого подвы- ражения. Это группа, не подвергающаяся захвату, наподобие (?:...), с добавлением флажков Комментарий: весь текст внутри скобок игнорируется Соответствует ге независимо от всего остального выражения, не при- нимая во внимание, является или нет это соответствие причиной несо- ответствия всего остального выражения. Пригодится для оптимизации некоторых сложных регулярных выражений. Круглые скобки не приводят к захвату соответствующего текста 9.2.4. Определение соответствия шаблону с использованием регулярных выражений В Ruby основным оператором соответствия шаблону является =-. Один из опе- рандов должен быть регулярным выражением, а другой — строкой (этот оператор реализован одинаково как в Regexp, так и в St г 1 ng, и где именно находится регуляр- ное выражение — справа или слева — не имеет значения.) Оператор =~ проверяет свой строковый операнд, чтобы определить, соответствует ли он или любая подстрока шаблону, определяемому регулярным выражением. Если соответствие будет найдено, оператор возвращает строковый индекс начала первого найденного соответствия. В противном случае он возвращает nil: pattern = /Ruby?/i # Соответствует "Rub" или "Ruby", регистр не учитывается pattern =~ "backrub" # Возвращается 4. "rub ruby" =~ pattern # О pattern =~ "г" # nil
9.2. Регулярные выражения 379 После использования оператора =~ наши интересы могут распространяться и на другие обстоятельства соответствия, кроме позиции, в которой оно начинается. После любого успешного (не-ni 1) соответствия глобальная переменная содер- жит объект MatchData, который, в свою очередь, содержит полную информацию о соответствии: "hello" =~ /e\w{2}/ string $~.to_s $~.pre_match $~.post_match # Г. соответствует символу е, сопровождаемому двумя # словарными символами # "hello”: строка целиком # "ell": соответствующая часть строки # "h": часть до соответствия # "о": часть после соответствия Оператор $~ по отношению к потоку и методу является специальной локальной переменной. Двум потокам, запущенным параллельно, будут видны различные значения этой переменной. А метод, который использует оператор =~, не изменя- ет значение той переменной которая видна вызывающему методу. Чуть позже мы еще поговорим о переменной и родственных ей глобальных переменных. Объектно-ориентированной альтернативой этой волшебной и несколько загадоч- ной переменной служит метод Regexp.last_match. При его вызове без аргументов возвращается то же самое значение, что и при ссылке на $-. Когда использовавшееся при проверке на соответствие регулярное выражение со- держит подвыражения, заключенные в круглые скобки, объект MatchData имеет бо- лее информативное содержимое. В этом случае объект MatchData может сообщить нам о тексте (а также начальном и конечном смещении этого текста), который соответствует каждому подвыражению: # Это шаблон с тремя подшаблонами pattern = /(Ruby|Perl )(\s+Knocks | sucks)!/ text = "RubyVtrocks!" # Текст, сравниваемый с шаблоном pattern =~ text # => 0: текст соответствует шаблону с первого символа data = Regexp.1 astjnatch # => Получение подробностей соответствия data.size # => 4: объекты MatchData ведут себя как массивы datatO] # => "RubyVtrocks!": соответствующий текст целиком data[l] # => "Ruby"; текст, соответствующий первому подшаблону data[2] # => ’At": текст, соответствующий второму подшаблону data[3] # => "rocks": текст, соответствующий третьему подшаблону data[l,2] # => ["Ruby", "\t”] data[l..3] # => ["Ruby", "\t". "rocks"] data.values_at(1.3) # => ["Ruby", "rocks"]; только выборочные индексы data.captures # => ["Ruby", ”\t”. "rocks"]: только подшаблоны Regexp.last_match(3) # => "rocks": то же самое, что и Regexp.last_match[3] # Начальные и конечные data.begin(O) data.begi n(2) data,end(2) data.offset(3) позиции соответствий # => 0: начальный индекс всего соответствия # => 4: начальный индекс соответствия второму подшаблону # => 5: конечный индекс соответствия второму подшаблону # => [5,10]: начало и конец соответствия третьему # подшаблону
380 Глава 9. Платформа Ruby В Ruby 1.9 если шаблон включает именованные захваты, то объект MatchData, по- лученный на основе этого шаблона, может использоваться как хэш, с именами за- хваченных групп (в виде строк или обозначений) в качестве ключей. Например: # Только в Ruby 1.9 pattern = /(?<1ang>Ruby|Perl) (?<ver>\d(\.\d)+) (?<review>rocks|sucks)!/ if (pattern =~ "Ruby 1.9.1 rocks!") $~[:lang] # => "Ruby" $~[:ver] #=>"1.9.1" $-["review"] # => "rocks" offset!:ver) # => [5,10] начальное и конечное смещение номера версии end # Имена захватываемых групп и отображение имен групп на номера групп pattern.names #=>["lang", "ver", "review"] pattern.named_captures # => {”lang"=>[l],"ver"=>[2],”review"=>[3]} ИМЕНОВАННЫЕ ЗАХВАТЫ И ЛОКАЛЬНЫЕ ПЕРЕМЕННЫЕ В Ruby 1.9 если регулярное выражение содержит именованные захваты, по- являющиеся именно в левой половине оператора =-, то имена захватываемых групп становятся локальными переменными, а текст, соответствующий шабло- ну, присваивается в качестве значений этим переменным. Если соответствие найдено не было, этим переменным присваивается значение nil. Приведем пример: # Только в Ruby 1.9 if /(?<lang>\w+) (?<ver>\d+\.(\d+)+) (?<review>\w+)/ =- "Ruby 1.9 rules!" lang# => "Ruby" ver# => "1.9" review# => "rules" end Такое волшебное поведение проявляется только в том случае, если регуляр- ное выражение появляется в коде в буквальном виде. Если шаблон сохранен в переменной или в константе, или возвращен методом, или же шаблон поя- вился в правой половине, то оператор =- подобного присваивания значений локальным переменным не осуществляет. Если оператор =- переписывает уже определенные переменные, то в Ruby, вызванном с использованием ключа -w, выдается предупреждение. Вдобавок к оператору =~ в классах Regexp и String также определяется метод match. Этот метод похож на оператор установки соответствия, за исключением возвра- щаемого индекса того места, где найдено соответствие, он возвращает объект MatchData или ni 1, если текстовых соответствий найдено не было. Используется этот метод следующим образом: if data = pattern.match(text) # Или: data = text.match(pattern) handle_match(data) end
9.2. Регулярные выражения 381 В Ruby 1.9 с вызовом метода match также можно связывать блок. Если соответ- ствий не найдено, блок игнорируется и match возвращает ni 1. А если соответствие найдено, объект MatchData передается блоку, и метод match возвращает то, что будет возвращено блоком. Таким образом в Ruby 1.9 этот код может быть выражен более кратко: pattern.match(text) {|data| handlejnatch(data) } Другое изменение, внесенное в Ruby 1.9, состоит в том, что методы match допол- нительно в качестве второго аргумента воспринимают целое число, указывающее стартовую позицию для поиска соответствия. 9.2.4.1. Глобальные переменные, связанные с данными, соответствующими шаблону В Ruby позаимствован синтаксис регулярных выражений, используемый в языке Perl и, так же как и в Perl, после каждого поиска соответствия устанавливаются значения специальных глобальных переменных. Программисты, работавшие на Perl, могут счесть применение таких специальных переменных весьма полезным. Если же программировать на Perl не приходилось, то они могут показаться со- вершенно нечитаемыми! В таблице 9.3 приведена сводка этих переменных. Пере- менные, перечисленные во втором столбце, являются псевдонимами, доступными при включении в программу строки requi ге ' Engl i sh'. Таблица 9.3. Специальные глобальные переменные регулярных выражений Глобальная переменная Англоязычный вариант Альтернатива $LAST_MATCH_INFO Regexp.last_match $MATCH Regexp.last_match[O] $ $PREMATCH Regexp.last_match. pre_match $' $ POSTMATCH Regexp. Iast_match .post_match $1 Отсутствует Regexp. Iast_match [1] $2, и т. д Отсутствует Regexp.Iast_match[2] и т. д $+ $LAST_PAREN_MATCH Regexp.last_match[-1] Самой важной переменной табл. 9.3 является $-. Все остальные переменные явля- ются ее производными. Если переменной $- присвоить ссылку на объект MatchData, то значения всех остальных глобальных переменных будут изменены. Все осталь- ные глобальные переменные предназначены только для чтения, и их значение не может быть установлено напрямую. И наконец, важно запомнить, что переменная $- и все переменные, являющиеся ее производными, являются локальными по от- ношению к потоку выполнения и методу. Это означает, что два Ruby-потока могут одновременно проводить операции установки соответствия, не мешая друг другу, это также означает, что значения этих переменных, которые находятся в поле зре- ния вашего кода, не будут изменяться, когда код вызывает метод, вычисляющий установку соответствия шаблону.
382 Глава 9. Платформа Ruby 9.2.4.2. Установка соответствия шаблону с использованием строк В классе String определяется ряд методов, воспринимающих аргументы в виде регулярных выражений. Если проиндексировать строку с помощью регулярного выражения, то будет возвращена та часть строки, которая соответствует шаблону. Если за регулярным выражением следует целое число, то возвращается соответ- ствующий элемент объекта MatchData: ”rubyl23"[/\d+/] # "123” "rubyl23”[/([a-z]+)(\d+)/,l] # "ruby" ”rubyl23”[/([a-z]+)(\d+)/,2] # "123” Метод slice является синонимом для оператора индексирования строки []. Его вариант slice! возвращает то же значение, что и slice, но также имеет и побоч- ный эффект, заключающийся в удалении возвращенной подстроки из исходной строки: г = "rubyl23” r.slice!(/\d+/) # Возвращает "123", изменяет значение г на "ruby" Метод split разбивает строку на массив подстрок, используя в качестве своего разделителя строку или регулярное выражение: s = "one, two, three" s.split # ["one,"."two,","three"]: no умолчанию в качестве # разделителя используется пробельный символ # ["one"."two"."three"]: жестко заданный разделитель s.splitC, ") s.split(/\s*.\s*/) # ["one","two","three"]: пробелы вокруг запятой # необязательны Метод index ищет в строке соответствие символу, подстроке или шаблону, возвра- щая стартовый индекс. Если в нем качестве аргумента используется регулярное выражение, то он работает почти так же, как и оператор =~, но при этом он допу- скает использование второго аргумента, определяющего позицию символа, с ко- торого начинается поиск. Это позволяет находить не первое соответствие: text = "hello world" pattern = /1/ first = text.index(pattern) # 2: первое соответствие начинается co второго # символа n = Regexp.lastjnatch.end(O) # 3: конечная позиция первого соответствия second = text.index(pattern, n) # 3: возобновление поиска с этого места last = text.rindex(pattern) # 9: rindex осуществляет поиск с конца 9.2.4.3. Поиск и замена Наиболее важными методами, использующими регулярные выражения и опреде- ляемыми в классе String, являются методы sub (для осуществления подстановок) и gsub (для глобальных подстановок) и их варианты работы по месту примене- ния — sub! иgsub!.
9.2. Регулярные выражения 383 Все эти методы осуществляют операции поиска и замены, использующие ша- блон регулярного выражения. Методы sub и sub! осуществляют подстановку при первом соответствии шаблону. Методы gsub и gsub! осуществляют подстановку во всех местах соответствия шаблону. Методы sub и gsub возвращают новую стро- ку, оставляя исходную строку в неизменном виде. Методы sub! и gsub! изменяют строку, для которой они вызываются. Если в строке происходят какие-либо из- менения, эти методы-мутаторы возвращают измененную строку. Если изменения не происходят, они возвращают ni 1 (что делает метод удобным для применения в инструкциях i f и циклах whl 1 е): phone = gets # Чтение телефонного номера phone.sub! (/#.*$/. "") # Удаление комментариев в стиле Ruby phone.gsub!C/\D/. "") # Удаление всего, кроме цифр Эти методы поиска-замены не требуют использования регулярных выражений; в качестве заменяемого текста можно использовать обычную строку: text.gsublC"rails", "Rails") # Замена "rails” на "Rails" по всему тексту Но регулярные выражения придают их работе большую гибкость. Если, к при- меру, нужно получить первую заглавную букву для всех «rails» не затрагивая при этом «grails», следует воспользоваться регулярным выражением: text.gsub!(/\brai1s\b/. "Rails") # Запись "Rails" с большой буквы по всему тексту Причиной рассмотрения методов поиска-замены в этом специально посвященном им подразделе является то, что подстановка необязательно должна быть простой строкой текста. Предположим, что нужна строка подстановки, которая зависит от деталей найденного соответствия. Методы поиска-замены обрабатывают строку подстановки перед осуществлением подстановок. Если строка содержит обратные слэши, за которыми следует одна цифра, то эта цифра используется в качестве индекса внутри объекта и текст из объекта MatchData используется вместо обратного слэша и цифры. К примеру, если стро- ка содержит эскейп-последовательность \0, используется весь соответствующий текст. Если строка подстановки содержит \ 1, то в подстановке используется текст, соответствующий первому подвыражению. Представленный далее код осуществ- ляет независящий от регистра поиск слова «ruby» и ставит вокруг него HTML-теги полужирного выделения текста, сохраняя слово, написанное с большой буквы: text.gsubC/\bruby\b/i, ’<b>\0</b>') Учтите, что при использовании строки подстановки, взятой в двойные кавычки, символы обратного слэша следует удваивать. Может появиться соблазн попробовать сделать то же самое, используя обычную подстановку в строку, взятую в двойные кавычки: text.gsubC/\bruby\b/i. "<b>#{$&}</b>") Но этот вариант работать не будет, поскольку в данном случае подстановка вы- полняется над строковым литералом до того, как он будет передан gsub. А это будет до того, как произойдет установка соответствия шаблону, поэтому переменные,
подобные $&, еще не определены или содержат значения, оставшиеся с предыду- щей установки соответствия шаблону. В Ruby 1.9 вы можете обращаться к именованной захватываемой группе, исполь- зуя именованный синтаксис обратной ссылки \к: # Удаление парных кавычек из строки ге = /(?<quote>['”])(?<body>[A'”]*)\k<quote>/ puts "Это 'цитаты’”.gsub(re, '\k<body>') Строки подстановки могут также обращаться к тексту, отличающемуся от того, который соответствует захваченным группам. Для подстановки значений $&,$’,$' и $+ следует использовать синтаксические со- четания \&, \', \' \+. Вместо использования статической подстановки строки мето- ды поиска-замены также могут быть вызваны с блоком кода, который вычисляет строку подстановки динамически. Аргументом блоку служит текст, соответствую- щий шаблону: # Использование последовательной капитализации имен языков программирования # (приведение их к формату с первой заглавной буквой) text = "RUBY Java perl PyThOn" # Изменяемый текст lang = /ruby|java|perl|python/1 # Шаблон соответствия text.gsub!(lang) {|1| 1.capitalize } # Исправляющая капитализация Внутри блока кода можно использовать переменную $- и родственные ей ранее перечисленные в табл. 9.3 глобальные переменные: pattern = /([])([А\1]*)\1/ # Строка, помещенная в одинарные или двойные кавычки text.gsub!(pattern) do if ($1 == "") # Если строка была в двойных кавычках. "’#$2"' # заменить на строку в одинарных кавычках else # А если она была в одинарных кавычках - "\"#$2\"" # заменить на строку в двойных кавычках end end Э.2.4.4. Кодировка регулярных выражений В Ruby 1.9 Regexp-объекты имеют метод encoding, очень похожий на тот, что есть у строк. Кодировку регулярных выражений можно указать явным образом с по- мощью модификаторов: и — для UTF-8, s — для SJIS, е — для EUC-JP и п — для отсутствующей кодировки. Кодировку UTF-8 можно также указать в явном виде, включив эскейп-последовательность \и в регулярное выражение. Если кодировка в явном виде не указана, то используется кодировка источника. Но если все сим- волы в регулярном выражении относятся к ASCII, то используется ASCII, даже если кодировка источника является каким-нибудь из поднаборов ASCII. Если предпринимается попытка найти соответствие между шаблоном и строкой, которые имеют несовместимую кодировку, то в Ruby 1.9 операции установления соответствия шаблону выдают исключение. Метод fixed_encoding? возвращает
9.3. Числа и математические операции 385 true, если регулярное выражение имеет кодировку, отличную от ASCII. Если fixed_encoding? возвращает false, то шаблон можно использовать для установки соответствия с любой строкой, имеющей кодировку ASCII или поднабора ASCII, без всяких опасений. 9.3. Числа и математические операции В главе 3 рассматривались имеющиеся в Ruby различные подклассы Numeric, объ- яснялось, как в Ruby пишутся числовые литералы, а также было дано описание арифметики Ruby, работающей с целыми числами и с числами с плавающей точкой. В этом разделе дается расширенный материал по той же теме и рассматриваются числовые API и другие классы, связанные с проведением математических опера- ций. 9.3.1. Числовые методы В классе Numeric и его подклассах определяется ряд полезных предикатов для определения класса или проверки значения числа. Одни из предикатов работают только со значениями типа Float, а другие — только со значениями типа Integer: #Предикаты общего назначения 0. zero? 1.0. zero? 0.0. nonzero? 1. nonzero? 1. integer? 1.0.Integer? 1.scalar? 1.0.seal ar? Complex(l,2) .scalar? # => true (это число - нуль?) # => false # => nil (работает как false) # => 1 (работает как true) # => true # => false # => false: это не комплексное число. Ruby 1.9. # => false: это не комплексное число. Ruby 1.9. # => true: комплексное число. Следует включить строку # requires 'complex'. # Предикаты для работы с целыми числами O.even? O.odd? # => true: четное число (Ruby 1.9) # => false: не относится к нечетным числам # Предикаты для работы с числами с плавающей точкой ZERO, INF, NAN = 0.0, 1.0/0.0, 0.0/0.0 # Константы, используемые для проверки ZERO, fl nite? INF. finite? NAN. finite? # => true: это конечное число? # => false # => false ZERO. Infl nite? # => nil: зто бесконечное число? Положительное или # отрицательное? продолжение
386 Глава 9. Платформа Ruby INF.infinite? -INF.infinite? NAN. infl nite? # => 1 # => -1 # => nil ZERO.nan? INF.nan? NAN.nan? # => false: это число на самом деле не число (not-a-number)? # => false # => true В классе Float определяются методы для округления чисел. Большинство из этих методов также определяются в классе Numeric, поэтому они могут быть использо- ваны с числами любого типа: # Методы округления 1.1.ceil -1.1.ceil 1.9.floor -1.9.floor 1.1.round 0.5.round # => 2: максимум: наименьшее целое число >= его аргумента # => -1: максимум: наименьшее целое число >= его аргумента # => 1: минимум: наибольшее целое число <= его аргумента # => -2: минимум: наибольшее целое число <= его аргумента # => 1: округление до ближайшего целого числа # => 1: если значение между двумя целыми числами - округление # в сторону бесконечности -0.5.round 1.1.truncate -1.1.to_i # => -1: или округление в сторону минус бесконечности # => 1: отсечение дробной части: округление в сторону нуля # => -1: синоним для truncate В классе Float определяются также несколько других представляющих интерес методов и констант: # Абсолютное значение и знак -2.0.abs -2.0<=>0.0 # => 2.0: абсолютное значение # => -1: использование оператора <=> для вычисления знака # числа # Константы Float::МАХ # => 1.79769313486232е+308: значение может зависеть от # используемой платформы Float::MIN Float::EPSILON # -> 2.2250738585072е-308 # => 2.22044604925031е-16: разница между смежными числами # с плавающей точкой 9.3.2. Модуль Math В модуле Math определяются константы PI и Е и методы для тригонометрических и логарифмических вычислений, плюс несколько методов различного назначе- ния. Методы, определяемые в модуле Math относятся к «функциям модуля» (рас- сматриваемым в разделе 7.5.3), а это означает, что они могут быть вызваны через пространство имен Math, или путем включения модуля и вызова их в виде глобаль- ных функций. Приведем несколько примеров: # Константы Math::PI Math::Е # => 3.14159265358979 # => 2.71828182845905
9.3. Числа и математические операции 387 # Корни Math. sqrt (25.0) 27.0**(1.0/3.0) #Логарифмы Math .loglO(lOO.O) Math.1ogCMath::E**3) Math. Iog2(8) Math.log(16. 4) Math.exp(2) # Тригонометрия include Math sin(PI/2) cos(O) tan(PI/4) asin( 1.0)/PI sinh(O) asinhC 1.0) # => 5.0: квадратный корень # => 3.0: корень кубический, вычисленный # с помощью оператора ** # => 2.0: логарифм по основанию 10 # => 3.0: натуральный логарифм (по основанию е) # => 3.0: логарифм по основанию 2. В Ruby 1.9 и выше. # => 2.0: 2-й аргумент 1од() является основанием. Ruby 1.9. # => 7.3B905609B93065": то же самое, что и Math::E**2 # Сокращение набираемого текста: теперь префикс Math # можно опустить. # => 1.0: синус. Аргумент в радианах, а не градусах. # => 1.0: косинус. # => 1.0: тангенс. # => 0.5: арксинус. См. также acos и atan. # => 0.0: гиперболический синус. Есть также cosh, tanh. # => 0.0: инверсный sinh. Есть также acosh, atanh. # Преобразования декартовой точки (х.у) в полярные координаты (theta, г) theta = atan2(y,x) # Угол между осью X и прямой (0,0)-(х,у) г = hypot(x.y) # Гипотенуза: sqrt(x**2 + у**2) # Разложение числа с плавающей точкой х в мантиссу f и экспоненту е, чтобы # х = f*2**e f.e = frexp(1024.0) # => [0.5. 11] х = ldexp(f. e) # => 1024: вычисление x = f*2**e # Функция ошибок erf(O.O) erfc(O.O) # => 0.0: функция ошибок # => 1.0: l-erf(x): дополнительная функция ошибок 9.3.3. Десятичная арифметика Имеющийся в стандартной библиотеке класс BigDecimal является весьма по- лезной альтернативой классу Float, особенно для финансовых вычислений, где нужно избегать наследования ошибок округлений, возникающих при использо- вании двоичной арифметики чисел с плавающей точкой (рассматриваемой в раз- деле 3.1.4). Объекты BigDecimal могут иметь неограниченное количество значащих цифр и практически неограниченный размер (поддерживаются числа в степени более одного миллиарда). Но важнее всего то, что они используют десятичную арифметику и предоставляют высокоточное управление режимами округлений. Приведем примеры кода, использующего BigDecimal: require "bigdecimal" # Загрузка стандартной библиотеки dime = BigDecimal("0.1") # Передача конструктору строки, а не Float-числа продолжение &
388 Глава 9. Платформа Ruby 4*dime-3*dime == dime # true c BigDecimal, но false при использовании Float # Вычисление ежемесячной процентной выплаты по ипотеке с использованием # BigDecimal. # Использование режима "Банковского округления" и ограничение вычислений # до 20 цифр BigDecimal.mode(BigDecimal::ROUND_MODE, BigDecimal::ROUND_HALF_EVEN) BigDecimal.1imit(20) principal = BigDecimal("200000”) # Конструктору всегда передается строка apr = BigDecimal("6.5") # Ежегодная процентная выплата years =30 # Срок ипотеки в количестве лет payments = years*12 # В году 12 ежемесячных выплат interest = арг/100/12 # Преобразование ежегодной процентной выплаты # (APR) в ежемесячные доли х = (interest+l)**payments # Учтите, что возводится в степень BigDecimal monthly = (principal * interest * x)/(x-l) # Вычисление ежемесячной выплаты monthly = monthly.round(2) # Округление до двух десятичных разрядов monthly = monthly.to_s("f") # Преобразование в удобную для чтения строку Чтобы получить более подробную информацию о BigDecimal API, воспользуй- тесь инструментальным средством ri, а полноценную документацию можно найти в файле ext/bigdecimal/bigdecimal_en.html исходного установочного пакета Ruby. 9.3.4. Комплексные числа С помощью класса Complex, входящего в состав стандартной библиотеки, можно представлять комплексные числа и управлять ими (суммой вещественного и мни- мого числа). В классе Complex, как и в классе BigDecimal, определяются все обычные арифметические операторы, а для работы с комплексными числами даже перео- пределяются методы модуля Math. Приведем несколько примеров: require “complex" с = Complex(0.5.-0.2) z = Complex.new(0.0, 0.0) 10.times { z = z*z + c } magnitude = z.abs x = Math.sin(z) Math.sqrt(-l.O),to_s Math.sqrt(-1.0)==Compl ex:: I # Модуль Complex является частью стандартной # библиотеки # .5-.21. # Работает и форма Complex.new, но ее применять # необязательно # Итерация для вычисления фракталов Julia set # Абсолютная величина комплексного числа # Тригонометрические функции работают также # и с комплексными числами # => "1.01": квадратный корень из -1 # => true 9.3.5. Рациональные числа Рациональные числа (частные от делений двух целых чисел) представляет класс Rational, входящий в состав стандартной библиотеки. В нем определяются
9.3. Числа и математические операции 389 арифметические операторы для работы с рациональными числами. Лучше всего он работает с библиотекой mathn, в которой целочисленное деление переопределя- ется для создания рациональных чисел. Библиотека mathn производит также ряд других «унификаций» Ruby-арифметики и обеспечивает бесконфликтную со- вместную работу классов Integer, Rational и Complex: require "rational" # Загрузка библиотеки penny = Rational(1, 100) # Penny - это одна сотая require "mathn" # Превращает целочисленное деление в производство # рациональных чисел nickel = 5/100 dime = 10/100 quarter = 1/4 change = 2*quarter + 3*penny # Рациональный результат: 53/100 (1/2 * l/3).to_s # "1/6": mathn выводит рациональные числа # в виде дроби 9.3.6. Векторы и матрицы Для представления числовых матриц и векторов в библиотеке matrix определя- ются классы Matrix и Vector, а также операторы для выполнения арифметических действий над ними. Вопросы линейной алгебры не входят в тематику этой книги, но представленный далее пример кода использует класс Vector для представления двумерной точки и использует Matrix-объекты 2x2 для представления преобразо- ваний масштаба и вращения точки: require "matrix" # Представление точки (1,1) в виде вектора [1.1] unit = Vector[l,l] # Матрица тождественного преобразования identity = Matrix.identity(2) # матрица 2x2 identity*unit == unit # true: преобразования нет # Эта матрица масштабирует точку с помощью sx.sy sx.sy = 2.0, 3.0: scale = Matrix[[sx,0], [0, sy]] scale*unit # => [2.0, 3.0]: масштабированная точка # Эта матрица вращается против часовой стрелки вокруг начала координат theta = Math::PI/2 # 90 градусов rotate = MatriхЕ[Math.cos(theta), -Math.sin(theta)], [Math.sin(theta), Math.cos(theta)]] rotate*unit # [-1.0. 1.0]: поворот на 90 градусов # Два преобразования сразу scale * (rotate*unit) # [-2.0. 3.0]
390 Глава 9. Платформа Ruby 9.3.7. Случайные числа Случайные числа в Ruby генерируются с помощью функции Kernel. rand. Без ар- гументов она возвращает псевдослучайное F1 oat-число, значение которого больше чем или равно 0.0 и меньше чем 1.0. При использовании целочисленного аргу- мента max возвращается псевдослучайное целое число, значение которого больше или равно 0 и меньше чем max. Например: rand # => 0.9643951965051В6 rand #=> 0.390523655919935 rand(lOO) # => Bl rand(lOO) # => 32 Если нужно получить повторяющуюся последовательность псевдослучайных чи- сел (возможно, для проведения тестов), следует в качестве начального числа гене- ратора случайных чисел задать предопределенное значение: srand(O) # Задание предопределенного начального значения [rand(lOO).rand(lOO)] # => [44,47]: псевдослучайная последовательность srand(O) # Перезапуск с тем же начальным значением для # получения повторяющейся последовательности [rand(lOO), rand(lOO) ] #=>[44,47] 9.4. Работа с датой и временем Дата и время представлены в классе Т1 те. Этот класс является тонкой надстрой- кой над функциями работы с датой и временем, предоставляемыми операционной системой. Поэтому на некоторых платформах этот класс может не представлять даты до 1970 или после 2038 года. Классы Date и DateTime, принадлежащие стан- дартной библиотеке date, не имеют таких ограничений, но здесь они не показаны: # Создание Time-объектов Time.now # Возвращает time-объект, представляющий текущее время Time.new # Синоним для Time.now Time.local(2007, 7, В) Time.local(2007. 7. В. 9, 10) Time.utc(2007, 7, B, 9, 10) Time.gm(2007, 7, 8. 9. 10, 11) # July B, 2007 # July B, 2007, 09:10am, местное время # July В. 2007, 09:10 UTC # July B. 2007, 09:10:11 GMT # (то же самое, что и UTC) # Одна микросекунда до наступления нового тысячелетия в Лондоне. # Мы будем использовать этот Time-объект во многих, приведенных ниже примерах, t = Time.utc(2000. 12. 31. 23. 59. 59. 999999) # Компоненты Time-объекта t.year # => 2000 t.month # => 12: Декабрь
9.4. Работа с датой и временем 391 t.day # => 31 t.wday # => 0: день недели: 0 - Воскресенье t.yday # => 366: день года: 2000 год был високосным t.hour # => 23: 24-часовой формат времени t.mln # => 59 t.sec # => 59 t.usec # => 999999: микросекунды, а не миллисекунды t.zone # => "UTC": название часового пояса # Получение всех компонентов в массиве, который содержит # [сек,мин,час.день.месяц,год,день_недели,день_года,летнее_время,часовой_пояс] # Заметьте, что микросекунды здесь теряются values = t.to_a # => [59. 59. 23. 31. 12. 2000, 0. 366, false. "UTC"] # Массив в этой форме можно передать Time.local и Time.utc values[5] += 1 # Приращение года Time.utc(*values) # => Mon Dec 31 23:59:59 UTC 2001 # Часовые пояса t.zone t.utc? t.utc_offset t.localtime и летнее время # => "UTC": возвращает часовой пояс # => true: t в часовом поясе UTC # => 0: UTC имеет нулевое смещение от UTC # Приведение к местному часовому поясу. # Вносит необратимые изменения в Time-объект! t.zone t.utc? t.utc_offset t.gmtime t.getlocal t.getutc t.isdst # => "PST" (или ваш часовой пояс) # => false # => -2ВВ00: за В часов до UTC # Обратное приведение к UTC. Еще один мутатор. # Возвращение нового Time-объекта в местном часовом поясе # Возвращение нового Time-объекта в UTC # => false: UTC не имеет летнего времени (DST). # обратите внимание на отсутствие знака «?». t.getlocal.isdst # => false: зимой летнего времени не бывает. # Предикаты дней недели: Ruby 1.9 t. Sunday? t. monday? t.tuesday? # => true # => false # И T. Д. # Форматирование даты и времени t.to_s # => "Sun Dec 31 23:59:59 UTC 2000": Ruby l.B t.to_s # => "2000-12-31 23:59:59 UTC": Ruby 1.9 использует ISO-B601 t.ctime # => "Sun Dec 31 23:59:59 2000": еще один основной формат # Метод strftime вставляет компоненты даты и времени в строку шаблона # Форматирование, независимое от местных настроек t.strftimeCXY-Xm-M ЯН:ЯМ:Я$") # => "2000-12-31 23:59:59": формат ISO-8601 t.strftime("£H:£M") # => "23:59": 24-часовой формат времени t.strftime("Я1:ЯМ Яр") # => "11:59 РМ": 12-часовой формат времени продолжение
392 Глава 9. Платформа Ruby # Форматы, зависящие от местных установок t.strftimeCXA, ЯВ И”) t.strftimeCXa, W И Ху") t.strftimeCXx") t.strftimeC'XX”) t.strftimeC'Xc") # => "Sunday, December 31" # => "Sun, Dec 31 00": год представлен # двумя цифрами # => "12/31/00": формат, зависящий от местной # установки # => "23:59:59" # то же самое, что и ctime # Разбор даты и времени require 'parsedate' # Универсальная библиотека синтаксического анализа # даты-времени include ParseDate # Включение parsedateO в качестве глобальной функции datestring = "2001-01-01" values = parsedate(datestring) # [2001, 1, 1, nil, nil, nil, nil, nil] t = Time.local(*values) # => Mon Jan 01 00:00:00 -0800 2001 s = t.ctime # => "Mon Jan 1 00:00:00 2001" Ti me. 1 oca 1 (*pa rsedate(s))==t s = "2001-01-01 00:00:00-0500" v = parsedate(s) t = Time.local (*v) # Арифметика в классе Time now = Time.now past = now - 10 future = now + 10 future - now # => true # полночь в Нью-Йорке # => [2001, 1. 1. 0. 0. 0. "-0500", nil] # Утрата информации о часовом поясе! # Текущее время # 10 сек назад. Time - число => Time # 10 сек вперед Time + число => Time # => 10 Time - Time => количество секунд # Сравнения в классе Time past <=> future past < future now >= future now == now # => -1 # => true # => false # => true # Вспомогательные методы для работы с единицами времени, отличными от секунд class Numeric # Преобразование временных интервалов по отношению к секундам def milliseconds; self/1000.0; end def seconds: self: end def minutes; self*60: end def hours: self*60*60: end def days; self*60*60*24: end def weeks; seif*60*60*24*7; end # Преобразование секунд в другие интервалы def to_mi11iseconds; self*1000; end def to_seconds: self: end def tojninutes: self/60.0: end def to_hours: seif/(60*60.0): end
9.5. Коллекции 393 def to_days; seif/(60*60*24.0): end def to_weeks: self/(60*60*24*7.0): end end expires = now + 10.days # 10 дней от настоящего времени expires - now # => 864000.0 секунд (expires - now),to_hours # => 240.0 часа # Время во внутреннем представлении как количество секунд с начала эпохи # (зависит от используемой платформы) t = Time.now.to_i # => 1184036194 секунд с начал эпохи Time.at(t) # => Секунды с начала эпохи к Time-объекту t = Time.now.to_f # => 1184036322.90872: включает 908720 микросекунд Time.at(O) # => Wed Dec 31 16:00:00 -0800 1969: начало эпохи по # местному времени 9.5. Коллекции В этом разделе рассматриваются имеющиеся в Ruby классы коллекций. Коллек- ция — это любой класс, представляющий собой коллекцию значений. Ключевыми классами коллекций в Ruby являются массивы Array и хэши Hash, а стандартная би- блиотека добавляет еще и класс наборов — Set. В каждый из этих классов коллек- ций подмешивается модуль Enumerable, следовательно, все методы перечислений, имеющиеся в этом модуле, являются универсальными методами коллекций. 9.5.1. Перечисляемые объекты Модуль Enumerabl е является миксином, реализующим вдобавок к итератору each целый ряд полезных методов. Все рассматриваемые далее классы Array, Hash и Set включают модуль Enumerable, поэтому в них реализованы все рассматриваемые в этом разделе методы. Другими заслуживающими внимания перечисляемыми классами являются класс диапазонов Range и класс ввода-вывода — 10. Модуль Enumerable был вкратце рассмотрен в разделе 5.3.2. В этом разделе он рассматрива- ется более подробно. Следует отметить, что некоторые перечисляемые классы обладают естественным порядком перечисления, которому следуют их методы each. К примеру, в масси- вах Array элементы нумеруются в порядке возрастания индекса массива. Диапазо- ны Range имеют нумерацию в возрастающем порядке. А в объектах ввода-вывода 10 нумеруются строки текста в том порядке, в котором они считываются из ис- ходного файла или сокета. В Ruby 1.9 в классах хэшей Hash и наборов Set (которые основаны на Hash) элементы нумеруются в том порядке, в котором они туда встав- лялись. Но до Ruby 1.9 элементы в этих классах нумеровались, по сути, в произ- вольном порядке. Многие методы модуля Enumerable возвращают обработанную версию перечисляе- мых коллекций или отобранную подколлекцию их элементов. Чаще всего, когда
394 Глава 9. Платформа Ruby Enumerable-метод возвращает коллекцию (а не отдельное значение, отобранное из коллекции), эта коллекция представляет собой массив. Но так бывает не всегда. К примеру, в классе Hash метод reject переопределен так, чтобы вместо массива возвращался хэш. Каким бы в точности ни было возвращаемое значение, коллек- ция, возвращаемая Enumerable-методом, сама по себе всегда будет перечисляемой. 9.5.1.1. Выполнение итераций и преобразование коллекций По определению любой перечисляемый объект должен иметь итератор each. Мо- дуль Enumerable предоставляет простой вариант each_with_index, который выдает элемент коллекции и целое число. Для массивов это целое число является индексом массива. Для 10-объектов целое число является номером строки (отсчет ведется с 0). Для других объектов оно представляет собой индекс, который был бы у коллекции, если бы она была пре- образована в массив: (5..7).each {|х| print х } # Выводится "567" (5..7).each_with_index {|x.i| print x.i } # Выводится "506172" В Ruby 1.9 в модуле Enumerable определяется метод cycle, который осуществляет повторную итерацию элементов коллекции в бесконечном цикле до тех пор, пока в связанном с ним блоке эта итерация не будет прервана в явной форме, путем ис- пользования инструкций break или return или выдачей исключения. При первом проходе по элементам перечисляемого объекта метод cycle сохраняет элементы в массиве, а затем проводит последующие итерации с использованием этого мас- сива. Это означает, что после первого прохода по коллекции вносимые в нее из- менения не будут влиять на поведение кода метода сус 1 е. Итераторы each_slice и each_cons выдают подмассивы коллекций. Они доступны в Ruby 1.8 при включении в программу строки require 'enumerator' и являются частью корневой библиотеки в Ruby 1.9. Выражение each slice(n) осуществляет итерацию перечисляемых значений «отрезками», размером в п-элементов: (1.10).each_slice(4) {|х| print х } # Выводится "[1,2,3,4][5,6,7,8][9.10]" Метод each_cons похож на метод each_slice, но использует в отношении перечис- ляемой коллекции «сдвигающееся окно»: (1..5),each_cons(3) {|х| print х } # Выводится ”[1,2,3][2,3.4][3,4,5]" Метод collect применяет блок при обработке каждого элемента коллекции и со- бирает значения, возвращаемые блоком, в новый массив. Его синонимом являет- ся метод тар; он отображает элементы коллекции на элементы массива, применяя блок к каждому элементу: data = [1.2.3,4] # Перечисляемая коллекция roots = data.collect {|х| Math.sqrt(x)} # Сбор квадратных корней наших данных words = Xw[hello world] # Еще одна коллекция upper = words.map {|х| x.upcase } # Отображение к верхнему регистру
9.5. Коллекции 395 Метод zi р перемежает элементы одной перечисляемой коллекции элементами дру- гих коллекций, которых может быть от нуля или более, и передает массив элемен- тов (по одному из каждой коллекции) связанному с методом блоку. Если такой блок не предоставлен, возвращаемое значение является массивом массивов в Ruby 1.8 или объектом-нумератором в Ruby 1.9 (вызов метода to_a в отношении объекта- нумератора генерирует массив массивов, который был бы возвращен в Ruby 1.8): (1..3).zIpC[4.5,6]) {|х| print х.inspect } # Выводит "[1,4][2,5][3,6]" (1..3).ziр([4,5,6],[7,В]) {|х| print х} # Выводит "14725836" (1. .3).zip( 'а'..’с’) {|х,у| print х,у } # Выводит "1а2ЬЗс" В модуле Enumerable определяется метод to_a (и его синоним entries), который преобразует любую перечисляемую коллекцию в массив. Метод to_a включен в этот раздел, поскольку вполне очевидно, что преобразование включает в себя итерацию коллекции. Элементы получающегося в результате массива выдаются в том порядке, в котором их выводит итератор each: (l,.3).to_a #=>[1,2,3] (1. .3).entries # => [1,2,3] Если включить в программу строку require 'set', то все перечисляемые объекты получат также метод, преобразующий их в наборы, — to set. Наборы подробно рассматриваются в разделе 9.5.4: require 'set' (1.,3).to_set # => #<Set: {1, 2, 3}> 9.5.1.2. Нумераторы и внешние итераторы Полное описание нумераторов и порядка их использования дано в разделах 5.3.4 и 5.3.5. Этот раздел представляет собой иллюстрированное примерами краткое повторение того подробного описания, которое дано в главе 5. Нумераторы относятся к классу Enumerable: Enumerator, в котором определено на удивление мало методов для такой мощной итеративной конструкции. Нумерато- ры главным образом являются свойством Ruby 1.9, но некоторые функциональ- ные возможности нумерации, если затребовать библиотеку enumerator, доступны и в Ruby 1.8. Нумератор создается вызовом метода to_enum или с помощью его псевдонима enum_for, или просто путем вызова метода-итератора без ожидаемого им блока: е = [1.,10].to_enum # Использует Range.each е = "test",enum_for(:each_byte) # Использует String.each_byte e = "test",each_byte # Использует String.each_byte Объекты-нумераторы являются перечисляемыми объектами с методом each, осно- ванном на каком-нибудь другом методе-итераторе какого-то другого объекта. Вдо- бавок к тому, что они являются перечисляемыми прокси-объктами, нумераторы также ведут себя как внешние итераторы. Для получения элементов коллекции с использованием внешнего итератора нужно лишь многократно вызывать метод next до тех пор, пока он не выдаст исключение Stopiteration. Итератор Kernel .loop
396 Глава 9. Платформа Ruby проводит для нас аварийное восстановление после выдачи Stopiteration. После того как next выдает Stopiteration, последующий вызов приведет, как правило, к началу новой итерации, с тем предположением, что исходный метод-итератор допускает повторяющиеся итерации (к примеру, итераторы, осуществляющие чтение из файлов, этого не допускают). Если допускается проведение повторяю- щихся итераций, можно перезапустить внешний итератор до выдачи Stopiteration, вызвав метод rewi nd: "Ruby".each_char.max iter = "Ruby".each_char loop { print iter.next } print iter.next iter.rewind print iter.next # => "у": Метод перечисления нумератора # Создание нумератора # Выводится "Ruby"; нумератор используется # как внешний итератор # Выводится "R": итератор перезапускается автоматически # Принуждение итератора к перезапуску # Снова выводится "R" Если есть некий нумератор е, то можно получить новый нумератор е. wi th_i ndex. В соответствии со своим именем этот новый нумератор выдает индекс (или номер итерации) наряду с тем значением, которое выдавал бы оригинальный итератор: # Выводится "0:R\nl:u\n2:b\n3:y\n" "Ruby",each_char.with_index.each {|c,i | puts ”#{i}:#{c} } В классе Enumerable: Enumerator определяется метод to_splat, который подразуме- вает возможность использования в качестве префикса нумератора звездочки для «расширения» его с выдачей отдельных значений для вызова метода или парал- лельного присваивания. 9.5.1.З. Сортировка коллекций Одним из самых важных в модуле Enumerable является метод sort. Он преобразу- ет перечисляемую коллекцию в массив и сортирует элементы этого массива. По умолчанию сортировка осуществляется в соответствии с результатами примене- ния к элементам метода <=>. Но если методу сортировки предоставлен блок, то ему передаются пары элементов, а он должен возвращать -1, 0 или +1, показывая их относительный порядок: w = Set['apple','Beet','carrot'] w.sort w.sort {|a,b| b<=>a } w.sort {ja.bj a.casecmp(b) } w.sort {ja.bj b.size<=>a.size} # Набор слов для сортировки # ['Beet','apple','carrot']: по алфавиту # ['carrot'.'apple','Beet']: обратная сотировка # ['apple','Beet','carrot']: без учета регистра # ['carrot','apple','Beet']: обратная по длине Если блок, связанный с методом sort, для осуществления сравнения должен про- делать объемные вычисления, то вместо него эффективнее будет применить метод sort_by. Блок, связанный с методом sort_by, будет вызван по одному разу для каж- дого элемента коллекции и должен вернуть для этого элемента числовой «ключ сортировки». Затем коллекция будет отсортирована в возрастающем порядке ключа сортировки. Таким образом, ключ сортировки вычисляется для каждого элемента только один раз, вместо двойного вычисления для каждого сравнения:
9.5. Коллекции 397 # Сортировка, независящая от регистра words = ['carrot', 'Beet', 'apple'] words.sort_by {|x| x.downcase} # => ['apple', 'Beet', 'carrot'] 9.5.1.4. Поиск в коллекциях В модуле Enumerable определяются несколько методов для поиска в коллекции от- дельного элемента. Метод include? и его синоним member? ищут элемент, равный их аргументу (с по- мощью оператора ==): primes = Set[2, 3, 5. 7] primes.include? 2 # => true primes.member? 1 #=> false Метод fi nd и его синоним detect применяют связанный с ними блок по очереди к каждому элементу коллекции. Если блок возвращает что-либо отличное от false или nil, то метод find возвращает этот элемент и останавливает итерацию. Если блок всегда возвращает ni 1 или false, то find возвращает ni 1: # Поиск первого подмассива, включающего число 1 data = [[1,2], [0,1], [7,В]] data.find {|х| х.include? 1} # => [1,2] data.detect {|х| х.include? 3} # => nil: такого элемента нет В Ruby 1.9 метод find_index работает так же, как и метод find, но возвращает ин- декс соответствующего элемента, а не сам этот элемент. Как и метод fl nd, он воз- вращает nil, если соответствий найдено не будет: data.find_index {|x| x.Include? 1} # => 0: соответствие найдено в первом элементе data.find_index {jxj х.include? 3} # => nil: такого элемента нет Следует заметить, что возвращаемое значение метода f i nd_i ndex не приносит осо- бую пользу для коллекций вроде хэшей и наборов, в которых не используются числовые индексы. В модуле Enumerable определяются и другие методы поиска, возвращающие не от- дельные элементы, а совпадающие коллекции. Мы рассмотрим эти методы в сле- дующем разделе. 9.5.1.5. Выбор подколлекции Метод select отбирает и возвращает элементы коллекции, для которых блок воз- вращает значение, отличное от nil или false. Синонимом этому методу служит метод f i nd_a 11; он работает так же, как и метод f i nd, но возвращает массив соот- ветствующих элементов: (1. .8) .select {|х| х£2==0} # => [2,4,6,8]: выбор четных элементов (1.,B).find_all {|х| х£2==1} # => [1,3,5,7]: поиск всех нечетных элементов Метод reject является прямой противоположностью методу select; в массиве воз- вращаются те элементы, для которых блок вернул false или ni 1.
398 Глава 9. Платформа Ruby primes = [2,3,5.7] primes.reject {|x| хЖ2==0} # => [3,5.7]: отбрасывание четных элементов Если нужно одновременно и отбирать и отбрасывать элементы коллекции, вос- пользуйтесь методом partition. Он возвращает массив, составленный из двух мас- сивов. Первый подмассив содержит элементы, для которых блок вернул истинное значение, а второй подмассив содержит элементы, для которых блок вернул лож- ное значение: (1. .8).partition {|х| хЖ2=0} # => [[2. 4. 6. 8]. [1. 3. 5. 7]] Метод group_by в Ruby 1.9 является обобщением метода partition. Вместо того чтобы рассматривать блок в качестве предиката и возвращать две группы, метод group_by берет возвращаемое блоком значение и использует его в качестве хэш- ключа. Он отображает этот ключ на массив из всех элементов коллекции, для ко- торых блок вернул это значение. Например: # Группировка имен языков программирования по их первым буквам langs = М java perl python ruby ] groups = langs.group_by {|lang| lang[0] } groups # => {"j"->["java"], "p"=>["perl", "python"], "r"=>["ruby"]} Метод grep возвращает массив элементов, соответствующих значению аргумента, определяя соответствие с помощью оператора case-равенства (===) этого аргу- мента. Когда он используется с аргументом, представляющим собой регулярное выражение, этот метод работает наподобие принадлежащей Юниксу утилиты ко- мандной строки grep. Если с вызовом связан блок, он используется для обработки соответствующих элементов, как будто для результатов, возвращенных методом grep, были вызваны методы collect или тар: langs = java perl python ruby ] langs.grep(/xp/) # => [perl, python]: начинаем c 'p' langs.grep(/xp/) {|x| x.capitalize} # => [Perl. Python]: превращаем первые буквы # в заглавные data = [1. 17. 3.0, 4] 1nts = data.grep(Integer) # => [1, 17, 4]: только целые числа small = 1nts.grepCO..9) # [1.4]: только числа диапазона В Ruby 1.9 рассмотренные ранее методы отбора дополняются методами fl rst, take, drop, take_wh11 e и drop_wh11 e. Метод fl rst возвращает первый элемент перечисляе- мого объекта или, если ему передан целочисленный аргумент п, возвращает мас- сив из первых п элементов. Методы take и drop предполагают использование цело- численного аргумента. Метод take ведет себя точно так же, как и метод first; он возвращает массив, составленный из первых п элементов перечисляемого объекта- получателя. Метод drop является его противоположностью; он возвращает массив элементов перечисляемого объекта, но без первых п элементов: р (1..5).first(2) #=>[1.2] р (1..5).take(3) #=>[1,2,3] р (1. .5).drop(3) #=>[4.5] Методы take_wh11e и drop_wh11e вместо целочисленного аргумента предпо- лагают использование блока. Метод take while по очереди передает элементы
9.5. Коллекции 399 перечисляемого объекта блоку до тех пор, пока блок в первый раз не вернет false или nil. Затем он возвращает массив предыдущих элементов, для которых блок вернул true. Метод drop_while также по очереди передает элементы блоку до тех пор, пока блок в первый раз не вернет false или nil. Но затем он возвращает мас- сив, содержащий элемент, для которого блок вернул fal se, и все последующие эле- менты: [1,2,3,nil,4].take_while {|х| х } # => [1,2,3]: отобрать до получения nil [nil, 1, 2],drop_while {|х| !х } # => [1,2]: отбросить лидирующие значения nil 9.5.1.6. Усечение коллекций Иногда нужно провести усечение перечисляемой коллекции до единственного значения, в котором заключается некое свойство коллекции. Методы mi п и max осу- ществляют усечение, возвращая наименьший или наибольший элемент коллек- ции (предполагая, что взаимное сравнение элементов осуществляется с помощью метода <=>): [10, 100, l].min ['а','с','b’].max [10, 'a', []].min # => 1 # => 'С # => ArgumentError: несовместимые элементы Методы min и max, так же как и метод sort, для сравнения двух элементов могут воспринимать блок. В Ruby 1.9 вместо этого проще воспользоваться методами min_by и max_by: langs = £w[java perl python ruby] langs.max {|a,b| a.size <=> b.size } langs.max_by {|word| word.length } # У какого языка самое длинное имя? # => "python": блок произвел 2 сравнения # => "python": только в Ruby 1.9 В Ruby 1.9 также определяются методы mi птах и mi nmax by, которые вычисляют как минимальное, так и максимальное значение коллекции и возвращают их в виде двумерного массива [min,max]: (1..100).mi птах # => [1,100] min, max как числа (1.,100).minmax_by {|n| n.to_s } # => [1.99] min, max как строки Предикаты any? и all? также приводят к усечению. Они применяют блок преди- ката к элементам коллекции. Метод all? возвращает true, если предикат является истиной (то есть не равен ni 1 или не равен false) для всех элементов коллекции. Метод any? возвращает true, если предикат является истиной для одного из эле- ментов. В Ruby 1.9 метод попе? возвращает true только в том случае, если предикат никогда не возвращает истинное значение. Также в Ruby 1.9 метод one? возвраща- ет true только в том случае, если предикат вернул истинное значение для одного, и только одного элемента коллекции. Будучи вызванными без блоков, эти методы ведут самостоятельную проверку элементов коллекции: с = -2..2 с.all? {|х| х>0} с.any? {|х| х>0} # => false: не все значения > 0 # => true: некоторые значения > 0 продолжение &
400 Глава 9. Платформа Ruby с.none? {|х| х>2} с.one? {|х| х>0} с.one? {|х| х>2} с.one? {|х| х==2} [1, 2. 3].а11? [nil, false].any? [].none? # => true: нет значений > 2 # => false: более одного значения > О # => false: нет значений > 2 # => true: одно значение == 2 # => true: нет значений nil или false # => false: нет значений true # => true: нет значений не-false и не-nil Еще одним дополнением в Ruby 1.9 служит метод count: он возвращает количество элементов коллекции, равных указанному значению, или количество элементов, для которых связанный с ним блок вернул true: а = [1,1,2,3.5.8] a.count(l) # => 2: единице равны два элемента a.count {|х| х % 2 == 1} # => 4: нечетными являются четыре элемента И наконец, inject является универсальным методом для усечения коллекции. В Ruby 1.9 в качестве псевдонима для inject определяется метод reduce. Блок, свя- занный с вызовом метода inject, предполагает использование двух аргументов. Первый из них является накопленным значением, а второй — элементом, взя- тым из коллекции. Накопленным значением для первой итерации является тот аргумент, который передан методу inject. Значение, возвращенное блоком после одной итерации, становится накопленным значением для следующей итерации. Значение, возвращенное после последней итерации, становится возвращаемым значением метода inject. Приведем несколько примеров: # Сколько отрицательных чисел? (-2..10).inject(O) {|num. х| х<0 ? num+1 : num } # => 2 # Сумма длины слов $w[pea queue are].inject(O) {(total. word| total + word.length } # => 11 Если методу inject не передано никаких аргументов, то при первом вызове блока ему передаются первые два элемента коллекции. (Или если в ней всего лишь один элемент, inject просто его и возвращает.) Такая форма использования inject при- годится для осуществления ряда стандартных операций: sum = (1..5).inject {(total,х| total + х} prod = (1..5).inject {(total.x| total * x} max = [1.3,2].inject {|m,x| m>x ? m : x} [1].inject {(total.x| total + x} # => 15 # => 120 # => 3 # => 1: блок здесь вообще не # вызывается В Ruby 1.9 методу i nject вместо указания блока можно передать обозначение, ука- зывающее имя метода (или оператора). Каждый элемент коллекции будет пере- дан указанному методу накопленного значения, и результат работы метода станет новым накопленным значением. При таком вызове метода с обозначением чаще всего используется синоним reduce: sum = (1..5).reduce(:+) prod = (1.,5).reduce(:*) letters = ('a'..'e').reduce("-”, :concat) # => 15 # => 120 # => "-abode"
9.5. Коллекции 401 9.5.2. Массивы Массивы, наверное, наиболее существенная и широко используемая в Ruby- программировании структура данных. Литералы массивов и операторы индек- сирования уже рассматривались в разделе 3.3. Этот раздел строится на предше- ствующем материале и демонстрирует довольно богатый API, разработанный для класса Array. 9.5.2.1. Создание массивов Массивы могут быть созданы с помощью литералов массивов или за счет приме- нения метода класса Array .new или оператора класса Array. []. Примеры: [1.2,3] [] [] ЗДа b с] ArrayEl,2.3] # Основной литерал массива # Пустой массив # Массивы являются изменяемыми объектами: этот пустой массив # отличается от предыдущего #=>['а’. 'Ь', 'с']: массив слов # => [1,2,3]: то же самое, что и литерал массива # Создание массивов с помощью метода new() empty = Array.new # []: возвращает новый пустой массив nils = Array.new(3) # [nil, nil, nil]: три элемента nil copy = Array.new(nils) # Создание новой копии существующего массива zeros = Array.new(4, 0) # [0, 0, 0, 0]: четыре нулевых элемента count = Array.new(3){|1| 1+1} # [1,2,3]: три элемента, вычисленные блоком # С a=Array.new(3,'а’) а[0].upcase! а a=Array.new(3){'b'} а[0].upcase!: а повторяющимися объектами нужно проявлять # => [' а',' а', ’ а' ]: # Перевод в верхний # => ['А','А','А']: # => [1Ь ’, ’ Ь ’, ’ Ь ’ ]: # Перевод в верхний # => ['B'.'b'.'b']: осторожность три ссылки на одну и ту же строку регистр первого элемента массива все они одна и та же строка! Три отдельных строковых объекта регистр первого из них остальные остались в нижнем регистре Вдобавок к имеющимся в классе Array фабричным методам в ряде других клас- сов определяются методы to_a, возвращающие массивы. В частности, с помощью метода to_a может быть преобразован в массив любой перечисляемый объект, та- кой как Range или Hash. Также такие операторы массивов, как +, и многие методы массивов, такие как slice, вместо того чтобы непосредственно изменить массив- получатель, создают и возвращают новые массивы. 9.5.2.2. Размер массива и его элементы Представленный далее код показывает, как определить длину массива, и демон- стрирует разнообразные способы извлечения из массива элементов и подмассивов: # Длина массива [1,2,3].length # => 3 продолжение &
402 Глава 9. Платформа Ruby [].size [].empty? [nil],empty? [1.2.nil].niterns [1.2,3].niterns {|x # => 0: синоним length # => true # => false # => 2: количество элементов, не равных nil | х>2} # => 1: количество элементов, соответствующих блоку # (Ruby 1.9) # Индексирование о a = £w[a bed] a[0] a[-l] a[a.size-1] a[-a.size-1] a[5] a[-5] a.at(2) тдельных элементов # => ['a', 'b'. 'С. 'd'] # => 'а': первый элемент # => 'd': последний элемент # => 'd': последний элемент # => 'а': первый элемент # => nil: такого элемента нет # => nil: такого элемента нет # => 'с': то же. что и [] для отдельного # целочисленного аргумента a.fetch(l) a.fetch(-l) a.fetch(5) a.fetch(-5) a.fetch(5, 0) a.fetch(5){|x|x*x} a. first a. last a.choice # => 'Ь': также похоже на [] и at # => 'd': работает с отрицательными аргументами # => IndexError!: не допускает выхода за границы массива # => IndexError!: не допускает выхода за границы массива # => 0: при выходе за границы возвращает второй аргумент # => 25: при выходе за границы вычисляет значение # => 'а': первый аргумент # => 'd': последний аргумент # Ruby 1.9: возвращает один случайно выбранный элемент # Индексирование n a[0.2] a[0..2] a[0...2] a[l.l] a[-2,2] a[4.2] a[5,l] a.slice(0..1) a.first(3) a.last(l) одмассива # => ['а','Ь']: два элемента, начиная с нулевого # => ['а'.'Ь','с']: элементы с индексами в диапазоне # => ['а'.'Ь']; использование трех точек вместо двух # => ['Ь']: отдельный элемент в виде массива # => ['c'.'d']: последние два элемента # => []: пустой массив в самом конце # => nil: за ним уже ничего нет # => ['а'.'Ь']: slice - синоним для [] # => ['а'.'Ь'.'с']: первые три элемента # => ['d']: последний элемент массива # Извлечение произ a.values_at(0.2) a.values_at(4, 3. a.values_at(O. 2.. a.values_at(O..2,1 вольных значений # => ['а'.'с'] 2, 1) # => [nil. 'd'.'c'.'b'] 3. -1) # => ['a'.'c'.'d'.'d'] . .3) # => ['а',1 b', 'С , 'b'. 'С .'d'] 9.5.2.3. Измен ение значений элементов массива Представленный ных элементов ма и замены одних зг далее код демонстрирует приемы изменения значений отдель- зеива, вставки значений в массив, удаления значений из массива гачений другими:
9.5. Коллекции 403 а = [1.2,3] а[0] = О а[-1] = 4 а[1] = nil # Начальное содержимое массива # Изменение значения элементов # Изменение значения существующего элемента: а равен [0.2.3] # Изменение значения последнего элемента: а равен [0.2,4] # Установка значения 2-го элемента в nil: а равен [0.nil.4] # Добавление значения в массив а = [1.2.3] а[3] = 4 а[5] = 6 а « 7 а « В « 9 # Новое содержимое массива # Добавление к нему четвертого элемента: а равен [1.2.3.4] # Можно пропустить элемент: а равен [1.2.3.4.nil.6] # => [1.2.3.4.nil.6.7] # => [1.2.3.4.nil ,6.7,В.9] из оператора можно выстроить # цепочку а = [1,2,3] а + а а.concat([4.5]) # Новое, более короткое содержимое массива # => [1,2,3.1,2,3]: Конкатенация + выдает новый массив # => [1,2,3.4.5]: непосредственное изменение: заметьте, что # символ ! отсутствует # Вставка элемента с помощью метода insert а = ['а’. 'Ь', 'с'] a.insertd, 1,2) # Теперь а содержит ['а'.1,2,'b','с']. # Работает так же. как и а[1,0] = [1,2] # Удалением возвращение) отдельных элементов по индексу а = [1,2,3.4,5.6] a.delete_at(4) # => 5: а теперь равен [1,2,3.4,6] a.delete_at(-l) # => 6: а теперь равен [1.2.3.4] a.delete_at(4) # => nil: а не изменился # Удаление элементов по значению a.delete(4) # => 4: а равен [1,2.3] а[1] =1 # а теперь равен [1,1,3] a.deleted) # => 1: а теперь равен [3]: удалены обе единицы а = [1.2.3] a.deletejf {|х| х£2==1} # Удаление нечетных значений: а теперь равен [2] a.reject! {|х| х£2==0} # То же самое, что и delete_if: а теперь равен [] # Удаление элементов и подмассивов с помощью метода slice! а = [1.2.3.4.5.6.7.В] a.slice!(0) # => 1: удаление элемента 0: а равен [2,3,4.5.6,7,В] a.slice!(-l,l) # => [В]: удаление подмассива с конца: # а равен [2.3.4.5,6.7] a.slice!(2..3) # => [4.5]: работа с диапазонами: а равен [2.3,6,7] a.slice!(4,2) # => []: пустой массив сразу за окончанием: а не изменился a.slice! (5,2) #=>nil: а теперь содержит [2.3.6.7,nil]! # Замена подмассива с помощью метода []= # Для удаления нужно присвоить пустой массив продолжение
404 Глава 9. Платформа Ruby # Для вставки нужно осуществить присвоение значений части массива с нулевой шириной а = ('а'..'е1).to_a # => ['a'.'b'.'c'.'d'.'e'] а[0,2] = ['А','В'] # а теперь содержит ['А', 'В', 'c'. 'd', ’е’] а[2... 5]=[ 'С.' D'. ' Е' ] # а теперь содержит ['А', 'В', 'С. 'D', 'Е'] а[0,0] = [1,2,3] # Вставка элементов в начало массива а а[0..2] = [] # Удаление этих элементов а[-1.1] = [7‘] # Замена значения последнего элемента другим значением а[-1.1] = 7' # Единственный элемент необязательно оформлять массивом а[1.4] = nil # Ruby 1.9: а теперь содержит ['A',nil] # Другие методы а = [4.5] а.replасе([1,2.3]) # # # Ruby 1.8: а теперь содержит ['A']: nil работает так же. как и [] а теперь содержит [1,2,3]: в а копируются его a.fill(O) # # аргументы а теперь содержит [0.0,0] a.fill(nil.1.3) # а теперь содержит [0.nil.nil .nil] a.fill('а'.2..4) # а теперь содержит [O.nil,'а','а','а'] а[3].upcase! # а теперь содержит [O.nil,'А','А','А'] a.fill(2..4) { 'b' } # а теперь содержит [O.nil,'b'.'Ь'.'Ь'] а[3].upcase! # а теперь содержит [O.nil,'b','В','Ь'] a.compact # => [0,'Ь','В','Ь']: копия с удаленными значениями nil a.compact! # Непосредственное удаление элементов nil: массив а a.clear # # теперь содержит [О.'Ь'.'В'.'Ь'] а теперь содержит [] 9.5.2.4. Проведение итерации, поиска и сортировки в массивах В класс Array подмешивается модуль Enumerable, поэтому для работы с массивами доступны все итераторы этого модуля. Вдобавок к этому в классе Array опреде- ляется ряд собственных важных итераторов и связанных с ними методов поиска и сортировки: а = ['а','Ь','с'] a.each {| elt| print elt } a.reverse_each {|e| print e} a.cycle {|e| print e } a.each_index {|i| print i} a.each_with_index{|e,i| print e.i} a.map {|x| x.upcase} a.map! {|x| x.upcase} a.collect! {|x| x.downcase!} # Основной итератор each выводит "abc" # Специализирован для массива: выводит "cba" # Ruby 1.9: бесконечно выводит # "abcabcabc..." # Специализирован для массива: выводит "012" # Из Enumerable: выводит ”аОЬ1с2” # Из Enumerable: возвращает ['А','В','С'] # Специализирован для массива: вносит # непосредственные изменения # collect! синоним метода тар! # Методы поиска а = £w[h е 1 1 о]
9.5. Коллекции 405 а.1nclude?(' е') а.1nclude?('w') a.index('1') a.index('L') a.rindexCl') a.index {|c| c =~ /[aeiou]/} a.rindex {|c| c =~ /[aeiou]/} # => true # => false # => 2: индекс первого соответствия # => nil: соответствий не найдено # => 3: поиск с конца # => 1: индекс первой гласной. Ruby 1.9 # => 4: индекс последней гласной. Ruby 1.9 # Сортировка a.sort # => ВДе h 1 1 о]: копирование а и сортировка копии a.sort! # Непосредственная сортировка: а теперь содержит # ['e'.'h'.'l'.'l'.'o'] а = [1.2,3,4,5] # Новый массив для сортировки на четные # и нечетные числа a.sort! {|a.b| а£2 <=> ЬЖ2} # Сравнение элементов по модулю 2 # Перемешивание массивов: противоположность сортировке: Только в Ruby 1.9 а = [1,2,3] puts a.shuffle # Сначала массив упорядочен # Перемешивание в произвольном порядке. # например: [3,1,2]. Есть также метод shuffle! 9.5.2.5. Сравнение массивов Два массива равны друг другу лишь в том случае, если у них одинаковое количе- ство элементов, элементы имеют одни и те же значения и расположены в одина- ковом порядке. Метод == проверяет равенство элементов массивов, вызывая для элементов обычный метод ==, а метод eql? проверяет равенство их элементов, вы- зывая для них обычный метод eql?. В большинстве случаев эти два метода про- верки равенства возвращают одинаковые результаты. К классу Array не подмешивается модуль Comparabl е, но в нем реализован оператор <=> и определено упорядочение массивов. Это упорядочение аналогично строко- вому упорядочению, и массивы, состоящие из кодов символов, сортируются точ- но так же, как и соответствующие String-объекты. Массивы сравниваются поэлементно, начиная с элементов, имеющих индекс 0. Если какая-нибудь пара элементов не равна, то метод сравнения массивов воз- вращает то же самое значение, что и метод сравнения элементов. Если все пары элементов равны и два массива имеют одинаковую длину, то массивы считаются равными и оператор <=> возвращает 0. Или же один из массивов является пре- фиксом другого. В таком случае более длинный массив считается больше того, что короче. Следует заметить, что пустой массив [ ] является префиксом любого другого массива и всегда меньше любого непустого массива. Также если пары эле- ментов массива не поддаются сравнению (к примеру, если один элемент является числом, а второй — строкой), то оператор <=>, вместо того чтобы вернуть -1, 0 или +1, возвращает nil: [1,2] <=> [4.5] # => -1, поскольку 1 < 4 [1.2] <=> [0,0.0] # => +1. поскольку 1 > 0 продолжение#
406 Глава 9. Платформа Ruby [1,2] <=> [1.2.3] [1.2] <=> [1,2] [1.2] <=> [] # => -1. поскольку первый массив короче # => 0: массивы равны друг другу # => +1, [] всегда меньше непустого массива 9.5.2.6. Массивы в качестве стеков и очередей Методы push и pop добавляют и удаляют элементы в конце массива. Они позволя- ют использовать массив в качестве стека, работающего по принципу «последним пришел, первым вышел»: а = [] a.push(l) а. push(2.3) a .pop a. pop a. pop a. pop # => [1]: а теперь содержит [1] # => [1,2.3]: а теперь содержит [1,2.3] # => 3: а теперь содержит [1,2] # => 2: а теперь содержит [1] # => 1: а теперь ничего не содержит - [] # => nil: а по-прежнему ничего не содержит - [] Метод shl ft похож на метод pop, но он удаляет и возвращает первый, а не послед- ний элемент массива. Метод unshi ft похож на метод push, но он добавляет элемен- ты в начало, а не в конец массива. Методы push и shl ft можно использовать для ре- ализации очереди, работающей по принципу «первым пришел, первым вышел»: а = [] a.push(l) а.push(2) a.shift a.push(3) a.shift a.shift a.shift # => [1]: а равен [1] # => [1.2]: а равен [1.2] # => 1: а равен [2] # => [2,3]: а равен [2,3] # => 2: а равен [3] # => 3: а равен [] # => ni1: а равен [] 9.5.2.7. Массивы в качестве наборов В классе Array реализованы операторы &, | и -, предназначенные для осуществле- ния набороподобных операций пересечения, объединения и определения разли- чий. Вдобавок к этому в нем определяется метод include? для проверки присут- ствия, или принадлежности значения к массиву. В нем даже определяются методы uni q и uni q! для удаления из массива продублированных значений (наборы не до- пускают дубликатов). Класс Array не считается эффективным средством реали- зации наборов (для этой цели больше подходит класс Set, принадлежащий стан- дартной библиотеке), но он может быть удобен для представления сравнительно небольших наборов данных: [1.3.5] & [1.2.3] [1.1.3.5] & [1,2,3] [1.3.5] | [2,4,6] [1.3.5.5] | [2.4.6.6] [1,2,3] - [2,3] [1.1.2.2.3.3] - [2, 3] # => [1.3]: пересечение наборов # => [1.3]: удаление дубликатов # => [1.3.5,2,4,6]: объединение наборов # => [1,3,5,2,4.6]: удаление дубликатов # => [1]: определение различия наборов # => [1,1]: удаляются не все дубликаты
9.5. Коллекции 407 small = 0..10.to_a # Набор небольших чисел even = 0..50.map {|х| х*2} # Набор четных чисел small even = small & even # Набор пересечений small even. Include?(8) # => true: проверка на принадлежность к набору [1, 1, nil, nil],uniq # => [1, nil]: удаление дубликатов # Есть еще вариант uniq! Следует заметить, что операторы & и | - не определяют порядок возвращаемых ими элементов. Эти операторы следует использовать только в том случае, если массивы действительно представляют собой неупорядоченный набор элементов. В Ruby 1.9 в классе Array определяется набор методов комбинаторики для вычис- ления перестановок (permutations), сочетаний (combinations) и декартовых про- изведений (Cartesian products): а = [1,2,3] # Повторение всех возможных двухэлементных подмассивов (порядок имеет значение) a.permutation(2) {|х| print х } # Выводит "[1,2][1,3][2,1][2,3][3,1][3,2]" # Повторение всех возможных двухэлементных подмассивов # (порядок не имеет значения) a.combination^) {|х| print х } # Выводит "[1, 2][1, 3][2, 3]" # Возвращение декартова произведения двух наборов a.products’a','b']) # => [[1,"а"],[1,"Ь"],[2."а"],[2,"Ь”],[3,"а"].[3.”Ь"]] [1,2].product([3,4].[5.6]) # => [[1.3,5].[1.3,6],[1.4.5].[1,4,6] и т. Д. ] Э.5.2.8. Ассоциативные методы работы с массивами Методы assoc и rassoc позволяют рассматривать массив как ассоциативный мас- сив или хэш. Чтобы они работали, массив должен быть массивом массивов, имея, как правило, следующую структуру: [[ключ1, значение!]. [ключ2, значение2], [ключЗ, значениеЗ], ...] В классе Hash определены методы, которые превращают хэш в такую форму вло- женных массивов. Метод assoc ищет вложенный массив, первый элемент которого соответствует предоставленному аргументу. Он возвращает первый соответству- ющий вложенный массив. Метод rassoc делает то же самое, но возвращает первый вложенный массив, чей второй элемент соответствует аргументу: h = { :а => 1. :Ь => 2} а = h.to_a a.assoc(:а) a.assoc(:b).last a.rassoc(l) а.rassoc(2).first a.assoc(:c) a.transpose # Начнем с этого хэша # => [[:b,2], [:a.l]]: ассоциативный массив # => [:a,l]: подмассив для ключа :а # => 2: значение для ключа :Ь # => [:а,1]: подмассив для значения 1 # => :Ь: ключ для значения 2 # => ml # => [[:а. :b], [1, 2]]: Перестановка местами # строк и столбцов продолжение
408 Глава 9. Платформа Ruby Э.5.2.9. Разнообразные методы для работы с массивами В классе Array определяется несколько разнородных методов, которые не подпа- дают ни под одну из ранее рассмотренных категорий: # Преобразование в [1.2.3].join [1,2,3].join(", ") [1.2.3].to_s [1.2.3].to_s [1,2,3].inspect строки # => ”123": превращение элементов в строки и их объединение # => "1, 2, 3": необязательный разделитель # => "[1, 2, 3]" в Ruby 1.9 # => "123" в Ruby 1.8 # => "[1. 2. 3]": пригодится при отладке в версии 1.8 # Двоичное преобразование [1,2,3,4].раск("СССС") [1.2].раск(' s2') [1234].pack("i") с помощью метода pack. См. также String.unpack. # => "\001\002\003\004" # => "\001\000\002\000" # => "\322\004\000\000" # Другие методы [0,1]*3 [1. [2, [3]]].flatten [1. [2. [3]]].flatten(l) [1,2,3].reverse а=[1.2.3].ziр([:а.:b.;c]) a.transpose # => [0,1,0,1,0,1]: * - это оператор повторения # => [1,2,3]: рекурсивное сглаживание; # есть еще вариант flatten! # => [1,2,[3]]: указание номера уровней; Ruby 1.9 # => [3,2,1]: переворачивание; # есть еще вариант reverse! # => [[1,:а],[2.:b].[3,:с]]: метод модуля Enumerable # => [[1.2.3].[:а.:b,:с]]: перестановка местами строк # и столбцов 9.5.3. Хэши Хэши были представлены в разделе 3.4, в котором объяснялось, что такое синтак- сис хэш-литерала и как работают операторы [] и [] = для извлечения и помещения в хэш пар ключ-значение. В этом разделе более подробно будет рассмотрен Hash API. В хэшах используются такие же операторы квадратных скобок, как и в масси- вах, и можно заметить, что многие Hash-методы аналогичны методам класса Array. 9.5.З.1. Создание хэшей Хэши могут быть созданы с помощью литералов, метода Hash. new или оператора [] самого класса Hash: { :one => 1, :two => 2 } { :one, 1, :two, 2 } { one: 1, two: 2 } {} Hash.new Hash[;one. 1. :two, 2] # Основной синтаксис хэш-литерала # To же самое с использованием нерекомендуемого # синтаксиса Ruby 1.8 # То же самое, синтаксис Ruby 1.9. Ключи являются # обозначениями # Новый пустой хэш-объект # => {}: создание пустого хэша # => {one:l, two:2}
9.5. Коллекции 409 Напомним, что в разделе 6.4.4 говорилось, что фигурные скобки вокруг хэш- литерала, являющегося последним аргументом при вызове метода, можно опу- стить: puts :а=>1. :Ь=>2 puts а:1, Ь:2 # В этом вызове фигурные скобки опущены # Так же работает и синтаксис Ruby 1.9 9.5.3.2. Индексирование хэшей и проверка принадлежности элемента к хэшу Хэши очень эффективно работают при поиске значения, связанного с заданным ключом. Также можно (хотя и не так эффективно) найти ключ, с которым связа- но значение. Тем не менее следует заметить, что многие ключи могут отображать- ся на одно и то же значение, и в таком случае будет возвращен произвольный ключ: h = { :one => 1. hE:one] h[:three] h.assoc :one h.Index 1 h.Index 4 h.rassoc 2 two => 2 } # => 1: поиск значения, связанного с ключом # => nil: такого ключа в хэше нет # => [:one. 1]: поиск пары ключ-значение. Ruby 1.9 # => :one: поиск ключа, связанного co значением # => nil: отображения на такое значение нет # => [:two, 2]: пара ключ-значение, соответствующая # значению. Ruby 1.9. В классе Hash определяется несколько тождественных методов для проверки при- надлежности к коллекции: h = { :а => 1. :Ь => 2 } # Проверка на наличие ключей в хэше: работает быстро h.key?(:а) # true: :а - ключ в h h.has_key?(:b) # true: has_key? - синоним для key? h.1nclude?(:c) # false: Include? - еще один синоним h.member?(:d) # false: member? - и еще один синоним # Проверка на наличие значения: работает медленно h.value?(l) # true: 1 - значение в h h.has_value?(3) # false: has_value? - синоним для value? При запросе значений хэша метод fetch является альтернативой []. Он предостав- ляет варианты обработки случаев, когда указанного ключа в хэше не существует: h = { :а => 1, :Ь => 2 } h.fetch(:a) # => 1: для существующих ключей работает как [] h.fetch(:c) # Для несуществующего ключа выдает IndexError h.fetch(:c, 33) # => 33: если ключ не найдется, использует # указанное значение h.fetch(:c) {|k| k.to_s } # => "с": если ключ не найдется, вызывает блок
410 Глава 9. Платформа Ruby Если за одно обращение из хэша нужно извлечь более одного значения, следует воспользоваться методом val ues_at: h = { :а => 1, :b => 2. :с => 3 } h.values_at(:с) # => [3]: Значения возвращаются в виде массива h.values_at(:а, :b) # => [1, 2]: передача любого количества аргументов h.values_at(:d, :d. :a) # => [nil, nil, 1] Воспользовавшись методом select, можно извлечь ключи и значения, отобранные с помощью блока: h = { :а => 1. :Ь => 2, :С => 3 } h.select {|k,v| v % 2 == 0 } # => [:b,2] Ruby l.B h.select {jk.vj v % 2 == 0 } # => {:b=>2} Ruby 1.9 Этот метод переопределяет метод Enumerable.seiect. В Ruby 1.8 метод select воз- вращает массив, состоящий из пар ключ-значение. В Ruby 1.9 он был изменен и вместо массива возвращает хэш из отобранных ключей и значений. 9.5.3.3. Хранение в хэше ключей и значений В хэше связь ключа со значением осуществляется с помощью оператора []= или его синонима — метода store: h = {} # Начнем с пустого хэша h[:a] = 1 # Отображение :а=>1. h теперь содержит {:а=>1} h.store(:b.2) # Более громоздкий вариант: h теперь содержит {:а=>1. :Ь=>2} Для замены в хэше всех пар ключ-значение копией пар из другого хэша использу- ется метод repl асе: # Замена всех пар в h парами из другого хэша h.replасе({1=>:а. 2=>:b} # h теперь равен хэшу, использованному # в качестве аргумента Методы merge, merge! и update позволяют объединять отображения из двух хэшей: # Объединение хэшей h и j в новый хэш к. # Если у h и j имеются общие ключи, используется значение из хэша j k = h.merge(j) {:а=>1.:b=>2}.merge(:а=>3,:с=>3) # => {:а=>3,:Ь=>2,:с=>3} h.merge!(j) # Непосредственное изменение h. # Если предоставлен блок, то он используется для решения, какое значение # использовать h.merge!(j) {|key.h.j| h } # Использование значения из h h.merge(j) {|key,h,j| (h+j)/2 } # Использование среднего из двух значений # Метод update является синонимом для merge! h = {a:l,b:2} # Использование синтаксиса Ruby 1.9 и отказ от скобок h.update(b:4,с:9) {|key,old.new| old } # h теперь содержит {а:1, b:2. с:9} h.update(b:4,c:9) # h теперь содержит {а:1, Ь:4, С:9}
9.5. Коллекции 411 9.5.3.4. Удаление записей из хэша Простым отображением ключа на ni 1 удалить его из хэша невозможно. Вместо этого следует воспользоваться методом del ete: h = {:а=>1, :b=>2} h[:a] = nil h.include? :a h.delete :b h.include? :b h.delete :b # h теперь содержит {:a=> nil, :b=>2 } # => true # => 2: возвращается удаленное значение: # h теперь содержит {:a=>nil} # => false # => nil: ключ не найден # Вызов блока в том случае, если ключ не найден h.delete(:b) {|k| raise IndexError, k.to_s } # IndexError! Если воспользоваться итераторами delete_if и reject! (и итератором reject, кото- рый работает с копией своего получателя), то можно удалить из хэша несколько пар ключ-значение. Следует отметить, что reject переопределяет одноименный метод модуля Enumerable и возвращает не массив, а хэш: h = {:а=>1, :b=>2, :с=>3. :d=>"four"} h.reject! {|k,v| v.is_a? String } h.delete_if {|k,v| k.to_s < 'b' } h.reject! {|k,v| k.to_s < 'b' } h.delete_i f {|k.v| k.to_s < 'b' } h.reject {|k,v| true } # => {:a=>l, :b=>2, :c=>3 } # => {:b=>2, :c=>3 } # => nil: изменения не произошло # => {:b=>2, :c=>3 }: хэш не изменился # => {}: хзш h не изменился И наконец, воспользовавшись методом clear, можно удалить из хэша все пары ключ-значение. Имя этого метода не заканчивается восклицательным знаком, но он вносит в свой получатель непосредственные изменения: h.clear # h теперь содержит {} 9.5.3.5. Получение массивов из хэшей В классе Hash определяются методы для извлечения данных из хэшей в массивы: h = { :а=>1, :Ь=>2, :с=>3 } # Размер хэша: количество пар ключ-значение h.length h.size h.empty? {}.empty? # => 3 # => 3: size является синонимом length # => false # => true h.keys h.values h. to_a h. fl atten h.sort h.sort {|a,b| # => [:b, :c, :a]: массив ключей # => [2,3,1]: массив значений # => [[:b,2],[:с,3],[:а, 1]]: массив из пар # => [:Ь, 2, :с, 3, :а, 1]: сглаженный массив. Ruby 1.9 # => [[:а,1],[:Ь,2],[:с,3]]: отсортированный массив из пар а[1]<=>Ь[1] } # Сортировка пар не по ключам, а по значениям
412 Глава 9. Платформа Ruby 9.5.3.6. Хэш-итераторы Обычно потребность в извлечении в массив имеющихся в хэш ключей, значений или пар не слишком высока, поскольку в класс Hash подмешан модуль Enumerable, а также в нем определены и другие полезные итераторы. В Ruby 1.8 Hash-объекты не гарантируют какой-либо порядок, в котором осуществляется итерация их зна- чений. Но в Ruby 1.9 элементы хэша перебираются в порядке их вставки в хэш, и именно этот порядок показан в следующих примерах: h = { :а=>1, :Ь=>2, :с=>3 } # Итератор each() перебирает пары [ключ,значение] h.each {|pair| print pair } # Выводит "[:а, 1][:Ь, 2][:с, 3]" # Он также работает с двумя аргументами блока h.each do |key. value| print "#{key}:#{value} " # Выводит "a:l b:2 c:3" end # Перебор ключей или значений, или и того и другого h.each_key {|k| print k } # Выводит "abc" h.each_value {|v| print v } # Выводит "123" h.each_pair {|k,v| print k.v } # Выводит "alb2c3". Работает так же. как и each Итератор each выдает массив, содержащий ключ и значение. Синтаксис вызова блока позволяет этому массиву подвергаться автоматическому расширению в от- дельные параметры ключа и значения. В Ruby 1.8 итератор each pai г выдает ключ и значение в виде двух различных ве- личин (что может несколько улучшить производительность). В Ruby 1.9 метод each pai г является простым синонимом метода each. Хотя метод shift и не является итератором, его можно использовать для пере- бора в хэше пар ключ-значение. Как и одноименный метод для работы с масси- вом, он удаляет из хэша и возвращает один элемент (в данном случае один массив [ключ,значение]): h = { :а=> 1. :Ь=>2 } print h.shift[l] while not h.empty? # Выводит "12" 9.5.3.7. Значения по умолчанию Обычно если будет запрошено значение ключа, с которым не связано никакое зна- чение, хэш возвращает nil: empty = {} empty["one"] # nil Но такое поведение хэша можно изменить, указав для него значение по умолча- нию: empty = Hash.new(-l) empty["one"] empty.default = -2 empty["two"] empty.default # Указание значения по умолчанию при создании хэша # => -1 # Изменение значения по умолчанию на какое-нибудь другое # => -2 # => -2: возвращение значения по умолчанию
9.5. Коллекции 413 Вместо задания единственного значения по умолчанию можно задать блок кода для вычисления значений для ключей, у которых нет связанных с ними значений: # Если ключ не определен, возвращается ближайшее к ключу следующее значение plusl = Hash.new {|hash, key| key.succ } plusltl] plusl["one"] plusl. default_proc # 2 # "onf": см. описание метода String.succ # Возвращает Ргос-объект, который вычисляет значения # по умолчанию # => 11: значение по умолчанию, возвращенное для ключа 10 plusl.default(lO) При использовании блока подобного этому, который вычисляет значения по умолчанию, вычисляемое значение обычно связывается с ключом, поэтому по- вторное вычисление при новом запросе ключа уже не требуется. Это легко реали- зуемая форма «ленивого» вычисления (и именно этим объясняется, почему блоку вычисления значения по умолчанию передается сам хэш-объект вместе с ключом): # Этот инициализированный "ленивым" образом хзш отображает целые числа # на их факториалы fact = Hash.new {|h.k| h[k] = if fact # {}: он fact[4] #24: 4! fact # {1=>1, k > 1: k*h[k-l] else 1 end } начинается с пустого значения равен 24 2=>2, 3=>6. 4=>24}: теперь у хэша есть записи Следует учесть, что установка для хэша атрибута default переопределяет любой блок, переданный конструктору Hash. new. Если значения по умолчанию для хэша не нужны или если нужно переопределить их, предоставив собственные значения по умолчанию, то вместо использования квадратных скобок для извлечения значений следует воспользоваться методом fetch, который уже был рассмотрен: fact.fetch(5) # IndexError: ключ не найден 9.5.3.8. Хэш-коды, равенство ключей и изменяемые ключи Чтобы объект мог использоваться в качестве хэш-ключа, у него должен быть метод hash, возвращающий для этого объекта целочисленный хэш-код. Классы, в кото- рых не определен их собственный метод eql ?, могут просто воспользоваться мето- дом hash, унаследованным от класса Object. Но если для проверки равенства объ- ектов определен метод eql?, то нужно определить и соответствующий метод hash. Если два различных объекта считаются равными, их методы hash должны возвра- щать одинаковые значения. В идеале два не равных друг другу объекта должны иметь разные хэш-коды. Этот вопрос рассматривался в разделе 3.4.2, а раздел 7.1.9 включает пример реализации метода hash. Обычно для проверки равенства хэш-ключей хэши используют метод eql?. Но в Ruby 1.9 для Hash-объекта можно вызвать метод compare by identity, чтобы за- ставить его вместо этого метода использовать для сравнения ключей метод equal ?. Если воспользоваться этим способом, то хэш будет в качестве хэш-кода для каж- дого объекта использовать object_1d.
414 Глава 9. Платформа Ruby Следует заметить, что compare_by_1dent1ty является методом экземпляра, который затрагивает только тот Hash-объект, для которого он был вызван. После такого вы- зова вернуть хэш к обычному способу проверки на равенство уже невозможно. Чтобы определить, проверяется ли данный хэш на равенство хэш-кодов или на идентичность объектов, используется предикат compare_by_identity?. Нужно пом- нить, что два литерала обозначений, имеющие одинаковые символы, вычисляют- ся в один и тот же объект, но два строковых литерала с одинаковыми символами вычисляются в разные объекты. Поэтому литералы обозначений могут быть ис- пользованы в качестве ключей хэшей, которые сравниваются на идентичность, а строковые литералы не могут. Как отмечалось в разделе 3.4.2, при использовании изменяемых объектов в ка- честве хэш-ключей нужно проявлять особую осторожность. (Строки — отдель- ный случай: класс Hash делает закрытую внутреннюю копию строковых ключей.) Если используются изменяемые ключи и один из них будет изменен, то для Hash- объекта, чтобы гарантировать его правильную работу, необходимо вызвать метод rehash: key = {:а=>1} h = { key => 2 } h[key] key.cl ear h[key] h.rehash h[key] # Этот хэш будет ключом другого хэша! # Этот хэш имеет изменяемый ключ # => 2: получение значения, связанного с ключом # Изменение ключа # => nil: для измененного ключа значений не найдено # Исправление хэша после внесения изменений # => 2: теперь значение снова найдено 9.5.3.9 Разнообразные методы для работы с хэшами Метод invert не подпадает ни под одну из предыдущих категорий. Он переставля- ет местами ключи и значения хэша: h = {:а=>1, :Ь=>2} h.invert # => {1=>:а, 2=>:b}: перестановка местами ключей и значений Как и в случае с массивами, метод Hash. to_s в Ruby 1.8 не приносит особой поль- зы, и для превращения хэша в строку в форме строкового хэш-литерала можно отдать предпочтение методу inspect. В Ruby 1.9 методы to_s и inspect работают одинаково: {:а=>1, :b=>2}.to_s # => "alb2" в Ruby 1.8: "{:а=>1, :Ь=>2}" в 1.9 {:а=>1, :b=>2}.inspect # => ”{:а=>1, :Ь=>2}" для обеих версий 9.5.4. Наборы Набор — это коллекция значений, не имеющая дубликатов. В отличие от массивов, у элементов набора порядок отсутствует. Хэш может считаться набором, состоя- щим из пар ключ-значение. И наоборот, набор может быть реализован за счет ис- пользования хэша, в котором набор элементов сохранен в виде ключей, а значения
9.5. Коллекции 415 игнорируются. Отсортированный набор навязывает своим элементам порядок следования (но не допускает к ним произвольный доступ, как в массивах). Ха- рактерной особенностью реализаций наборов является то, что они демонстриру- ют быстрое проведение операций проверки на принадлежность, а также операций вставки и удаления. Ruby не предлагает встроенного типа наборов, но в стандартную библиотеку включены классы Set и SortedSet, которыми можно воспользоваться, если сначала включить в программу следующую строку: require 'set' Set API во многом похож на API Array и Hash. Многие методы и операторы клас- са Set воспринимают в качестве своих аргументов любые перечисляемые объекты. SORTEDSET Класс SortedSet наследуется от Set, и в нем самом не определяется никаких новых методов; он просто гарантирует, что элементы набора будут перебирать- ся (или выводиться, или преобразовываться в массив) в отсортированном по- рядке. SortedSet не позволяет предоставлять для сравнения элементов набора пользовательский блок и требует, чтобы все элементы набора сравнивались друг с другом в соответствии со своим, используемым по умолчанию опера- тором <=>. Поскольку SortedSet API не отличается от основного Set API, он здесь рассматриваться не будет. 9.5.4.1. Создание наборов Поскольку Set не относится к базовым Ruby-классам, литеральный синтаксис для создания наборов отсутствует. Библиотека set добавляет к модулю Enumerable ме- тод to_set, с помощью которого набор может быть создан из любого перечисляе- мого объекта: (1. ,5).to_set [l,2,3].to_set # => #<Set: {5. 1, 2. 3, 4}> # => #<Set: {1. 2. 3}> В качестве альтернативы любой перечисляемый объект может быть передан мето- ду Set.new. Если предоставлен блок, то он используется (как и с тар-итератором) для предобработки перечисляемых значений перед их добавлением в набор: Set.new(l..5) Set.new([1.2.3]) Set.new([1,2.3]) {|x| x+1} # => #<Set: {5. 1. 2, 3. 4}> # => #<Set: {1. 2. 3}> # => #<Set: {2. 3. 4}> Если предпочтение отдается перечислению элементов набора без предваритель- ного их помещения в массив или в другой перечисляемый объект, то используется оператор [], определяемый в классе Set: Set["cow", "pig", "hen"] # => #<Set: {"cow", "pig", "hen"}>
416 Глава 9. Платформа Ruby Э.5.4.2. Проверка, сравнение и комбинирование наборов Наиболее распространенной операцией над наборами обычно является проверка на принадлежность: s = Set.newd. .3) s.include? 1 s.member? 0 # => #<Set: {1. 2. 3}> # => true # => false: member? - синоним Можно также проверить наборы на их принадлежность другим наборам. Набор S является поднабором Т, если все элементы S являются также элементами Т. Так- же можно сказать, что Т является наднабором для S. Если два набора равны, то они в равной степени являются поднаборами и наднаборами по отношению друг к другу. S является настоящим поднабором (proper subset) Т, если он представляет собой поднабор Т, но не равен Т. В таком случае Т является настоящим надна- бором S: s = Set[2. 3, 5] t = Set[2. 3. 5. 7] s.subset? t t.subset? s s.proper_subset? t t.superset? s t.proper_superset? s s.subset? s s.proper_subset? s # => true # => false # => true # => true # => true # => true # => false В классе Set определяются такие же методы вычисления длины, как и в классах Array и Hash: s = Set[2. 3. 5] s.length s.size s.empty? Set.new.empty? # => 3 # => 3: синоним для length # => false # => true Новый набор может быть создан путем комбинирования двух существующих наборов. Для этого существует несколько способов, для которых в классе Set определяются операторы &, |, - и А (плюс к этому обладающие именами методы- псевдонимы): # Имеются два простых набора primes = Set[2, 3, 5, 7] odds = Set[l. 3. 5. 7. 9] # Пересечение - это набор primes & odds primes.intersection(odds) # Объединение - это набор primes | odds значений, присутствующих в обоих наборах # => #<Set: {5. 7. 3}> # а это псевдоним с явно обозначенным именем значений, присутствующих в одном или в другом наборе # => #<Set: {5. 1. 7. 2, 3. 9}>
9.5. Коллекции 417 primes.uni on(odds) # псевдоним с явно обозначенным именем # a-b: элементы, за исклюением тех, которые также присутствуют в b primes-odds # => #<Set: {2}> odds-primes # => #<Set: {1, 9}> primes.difference(odds) # псевдоним с явно обозначенным именем # aAb набор значений, появляющихся в одном, но не в обоих наборах: (a|b)-(a&b) primes А odds # => #<Set: {1, 2. 9}> В классе Set для некоторых из этих методов также определяются варианты-му- таторы, и мы их вскоре рассмотрим. 9.5.4.3. Добавление и удаление элементов набора В этом разделе рассматриваются методы, добавляющие элементы в набор или удаляющие их из него. Они являются методами-мутаторами, которые непосред- ственно изменяют набор-получатель, вместо того чтобы возвращать измененную копию и оставлять исходный набор в неизменном виде. Поскольку в версии, не производящей мутации, эти методы не существуют, то в их именах восклицатель- ный знак не используется. Оператор « добавляет к набору одиночный элемент: s = Set[] s « 1 s.add 2 s « 3 « 4 « 5 s.add 3 s.add? 6 s.add? 3 # начнем с пустого набора # => #<Set: {1}> # => #<Set: {1, 2}>: add - синоним оператора « # => #<Set: {5. 1, 2, 3. 4}>: может выстраиваться цепочка # => #<Set: {5, 1, 2, 3. 4}>: значение не изменяется # => #<Set: {5, 6. 1, 2, 3, 4}> # => nil: набор не изменяется Чтобы добавить к набору более одного значения, следует воспользоваться мето- дом merge, который может воспринимать в качестве аргумента любой перечисляе- мый объект. Фактически merge является версией-мутатором метода union: s= (1. .3) .to_set # => #<Set: {1. 2, 3}> s.merge(2..5) # => #<Set: {5, 1, 2, 3. 4}> Для удаления из набора одиночного элемента используется метод delete или ме- тод del ete?. Эти методы работают аналогично методам add и add?, но у них нет эк- вивалентных им операторов: s = (1..3).to_set s.delete 1 s.delete 1 s. del ete? 1 s.delete? 2 # => #<Set: {1, 2, 3}> # => #<Set: {2. 3}> # => #<Set: {2, 3}>: изменений не произошло # => nil: возвращает nil, когда ничего не изменилось # => #<Set: {3}>: или же возвращает набор Чтобы удалить из набора сразу несколько значений, следует воспользоваться ме- тодом subtract. Аргументом этого метода может служить любой перечисляемый объект, а сам метод работает как версия-мутатор метода di fference:
418 Глава 9. Платформа Ruby s = (1..3).to_set # => #<Set: {1, 2. 3}> s.subtracts. .10) # => #<Set: {1}> Для выборочного удаления элементов из набора используются delete_if или re- ject!. Как и в случае с классами Array и Hash, эти два метода являются эквива- лентными, за исключением возвращаемого значения в том случае, когда набор не изменяется. Метод del ete i f всегда возвращает набор-получатель. А метод reject! возвращает набор-получатель, если он был изменен, или nil, если значения из него не удалялись: primes = Set[2, 3. 5. 7] primes.deletejf {|х| хЯ2==1} primes.delete_if {|х| хЖ2==1} primes.reject! {|x| x£2==l} # набор простых чисел # => #<Set: {2}>: удаление нечетных чисел # => #<Set: {2}>: без изменений # => nil: без измеений # Получить пересечение непосредственно в наборе можно следующим образом: s = (1..5).to_set t = (4..8),to_set s.reject! {|x| not t.include? x} # => #<Set: {5, 4}> И наконец, методы cl ear и repl асе работают так же, как с массивами и хэшами: s = Set.newd. .3) s.replaced. .4) s.clear s.empty? # Исходный набор # Замена всех элементов. # Аргумент - любой перечисляемый объект # => #<Set: {}> # => true Э.5.4.4. Итераторы наборов Наборы являются перечисляемыми объектами, и в классе Set определяется итера- тор each, который однократно передает блоку каждый элемент набора. В Ruby 1.9 Set ведет себя так же, как и класс Hash, на основе которого построена его реализа- ция, и осуществляет перебор элементов в порядке их вставки в набор. В верси- ях, предшествующих Ruby 1.9, перебор осуществляется в произвольном порядке. Для SortedSet элементы передаются в отсортированном порядке по возрастающей. В дополнение к этому итератор тар! изменяет каждый элемент набора с помощью блока, внося в набор непосредственные изменения. Его синонимом служит метод collect!: s = SetCl. 2. 3. 4, 5] s.each {|хj print х } s.map! {|xj x*x } s.collect! {|x| x/2 } # => #<Set: {5. 1. 2. 3. 4}> # Выводит "51234": до Ruby 1.9 - порядок произвольный # => #<Set: {16. 1. 25. 9. 4}> # => #<Set: {0. 12. 2. 8. 4}> 9.5.4.5. Разнообразные методы для работы с наборами В классе Set определяются эффективные методы для разбиения наборов на под- наборы и для сглаживания набора из поднаборов для получения одного большого набора. В дополнение к этому в нем определяются несколько вполне обычных ме- тодов, которые мы покажем в первую очередь:
9.6. Файлы и каталоги 419 s = (1.,3).to_set s. to_a s. to_s s. inspect s == Set[3,2,1] # => [1. 2, 3] # => "#<Set:0xb7e8f938>": вряд ли будет полезен # => "#<Set: {1, 2. 3}>": полезный метод # => true: для сравнения элементов набора использует eql? Метод classify предусматривает использование блока и по очереди выдает каж- дый элемент набора блоку. Возвращаемое значение представляет собой хэш, ко- торый отображает значения, возвращаемые блоком, на наборы элементов, возвра- тивших эти значения: # Классификация элементов на четные или нечетные s = (0..3).to_set # => #<Set: {0. 1, 2, 3}> s.classify {|х| хЯ2} # => {0=>#<Set: {0. 2}>, l=>#<Set: {1. 3}>} Метод divide работает аналогичным образом, но возвращает набор, состоящий из поднаборов, а не из хэшей, отображающих значения на поднаборы: s.divide {|х| хЖ2} # => #<Set: {#<Set: {0. 2}>, #<Set: {1. 3}>}> Если связанный блок предполагает использование двух аргументов, то метод di - vide работает совсем по-другому. В этом случае блок должен возвращать true, если два значения принадлежат одному и тому же поднабору, и или же возвра- щать false: # Набор слов # Разбиение на поднаборы по первой букве #<Set:{"cow"}>, #<Set:{"ape", "ant"}>}> # Набор слов # Разбиение его на поднаборы # Сглаживание поднаборов # => true s = Mant ape cow hen hog].to_set s.divide {|x.y| x[0] == y[0]} # => #<Set:{#<Set:{"hog". "hen"}>, Если имеется набор, состоящий из других наборов (которые также могут рекур- сивно включать наборы), то его можно сгладить, эффективно соединив (за счет объединения) все содержащиеся наборы путем использования метода fl atten или метода flatten!, выполняющего объединение непосредственно в самом наборе: s = £w[ant ape cow hen hog].to_set t = s.divide {|x.y| x[0] == y[0]} t.flatten! t == s 9.6. Файлы и каталоги В классе File определяется довольно много методов класса для работы с фай- лами и записями в файловой системе: к примеру, методы для проверки размера или существования указанного файла и методы для отделения имени файла от предшествующего ему имени каталога. Будучи методами класса, они не работа- ют с File-объектами; вместо этого имена файлов указываются в виде строк. Ана- логично этому в классе Dir определяются методы класса для работы с именами файлов и чтения этих имен из каталогов файловой системы. В следующих далее подразделах будет показано, как нужно:
420 Глава 9. Платформа Ruby О работать с именами файлов и каталогов, и воздействовать на них; О выводить содержимое каталогов; О проверять файлы, определяя их тип, размер, время последнего изменения и зна- чения других атрибутов; О удалять, переименовывать и осуществлять подобные этим операции над фай- лами и каталогами. Следует заметить, что рассматриваемые здесь методы запрашивают файлы и воз- действуют на них, но не читают и не записывают их содержимое. Вопросы чтения и записи содержимого файлов рассматривались в разделе 9.7. УКАЗАНИЕ ИМЕН ФАЙЛОВ В RUBY 1.9 Многие рассматриваемые в этом разделе методы работы с файлами и катало- гами предусматривают использование от одного и более аргументов, указы- вающих на файлы. Как правило, файлы и пути, составленные из каталогов, указываются в виде строк. В Ruby 1.9 можно воспользоваться и нестроковыми объектами, если у них имеется метод to_path, возвращающий строку. 9.6.1. Имена файлов и каталогов Имеющиеся в классах Fl 1 е и D1 г методы класса работают с файлами и каталогами, чьи имена им указаны. В Ruby используются имена файлов в стиле ОС Юникс, где в качестве разделителя каталогов используется символ /. Символ прямого слэша можно использовать с именах файлов, даже если Ruby ис- пользуется на платформе Windows. Но в ОС Windows Ruby также может рабо- тать с именами файлов, в которых используется символ обратного слэша, а также с теми именами, в начале которых присутствует имя дисковода. Значение кон- станты File:: SEPARATOR должно быть равно ' / ’ во всех реализациях Ruby. Значение константы File: :ALT_SEPARATORравно '\' в Windows и равно nil при использовании других платформ. В классе Fl 1 е определяется ряд методов для работы с именами файлов: full = '/home/matz/bin/ruby,ехе' f11e=Fl1е.basename(ful1) # => File.basename(full. '.exe') # => d1r=F11e.d1rname(full) # => File.dirname(file) # => Flle.split(full) # => File.extname(full) # => File.extname(file) # => File.extname(dir) # => File.join('home','matz') # => File.join('','home','matz') # => 'ruby.exe': только имя файла 'ruby': имя с удаленным расширением '/home/matz/Ьтп': без символа / в конце '.': текущий каталог ['/home/matz/bin', 'ruby.exe'] '.exe' '.exe’ 'home/matz': относительный путь '/home/matz': абсолютный путь
9.6. Файлы и каталоги 421 Метод File.expand_path превращает относительный путь в путь, указанный в пол- ном формате. Если предоставлен второй, необязательный аргумент, то сначала он подставляется в качестве каталога для первого аргумента. Затем результат пре- вращается в абсолютный путь. Если он начинается с символа ~, который соответ- ствует стилю имен файлов ОС Юникс, каталог вычисляется относительно теку- щего пользователя или указанного пользовательского домашнего каталога. В противном случае каталог вычисляется относительно текущего рабочего ката- лога (обратите внимание на рассматриваемый ниже метод Dir.chdir, с помощью которого рабочий каталог можно изменить): D1r.chd1r("/usr/b1n") # Текущий рабочий каталог ’’/usr/bin" File.expand_path("ruby") # => "/usr/bin/ruby" File.expand_path("~/ruby") # => "/home/david/ruby" File.expand_path("~matz/ruby") # => "/home/matz/ruby" File.expand_path("ruby". "/usr/local/bin") # => "/usr/local/bin/ruby” File.expand_path("ruby", ".,/local/bin”) # => "/usr/local/bin/ruby" File.expand_path("ruby", "-/bin") # => "/home/david/bin/ruby" Метод F i 1 e. i dent i ca 1 ? проверяет, ссылаются ли два имени файла на один и тот же файл. Такое бывает, если имена файлов одинаковы, но от метода будет больше пользы, когда эти имена отличаются друг от друга. К примеру, два отличающихся друг от друга имени могут ссылаться на один и тот же файл, если одно из них яв- ляется относительным именем файла, а второе абсолютным. Одно из них может включать «..» для перехода на один уровень вверх, а затем указывать на спуск вниз по иерархии каталогов. Или два различных имени могут ссылаться на один и тот же файл, если одно из имен является символьной ссылкой или ярлыком (или жестко заданной ссылкой на тех платформах, которые их поддерживают) на дру- гое имя. Но следует заметить, что File, identical? возвращает лишь true, если два имени ссылаются на один и тот же файл и этот файл существует. Также следует учесть, что File, identical? не производит расширения символа - в домашний ка- талог, как это делает метод File. expand_path: File.identical?("ruby". "ruby") File.identical?("ruby". "/usr/bin/ruby") File.identical?("ruby". "../bin/ruby") Fi 1e.identical?("ruby", "rubyl. 9") # => true, если файл существует # => true, если текущий рабочий # каталог - /usr/bin # => true, если текущий рабочий # каталог - /usr/bin # => true, если это ссылка И наконец, метод File, fnmatch проверяет соответствие имени файла указанному шаблону. Этот шаблон не является регулярным выражением, но похож на ша- блоны соответствия файлам, используемые в системных оболочках. При этом одиночному символу соответствует «?», любому количеству символов соответ- ствует «*», а любому количеству уровней каталогов соответствует «**». Как и в регулярных выражениях, символы в квадратных скобках представляют собой воз- можные варианты. Метод fnmatch не допускает использования возможных вари- антов в фигурных скобках (как это делает рассматриваемый ниже метод Di г .glob). Обычно метод fnmatch должен вызываться с использованием третьего аргумента
422 Глава 9. Платформа Ruby File::FNM_PATHNAME, который предотвращает соответствие «*» символу «/». Если нужно определить соответствие тем «скрытым» файлам и каталогам, имена кото- рых начинаются с «.», следует добавить константу File:: FNM_DDTMATCH. В этом раз- деле приводится лишь несколько примеров использования метода fnmatch. Чтобы получить о нем более полное представление, воспользуйтесь командой ri File, fnmatch. Учтите, что его синонимом является метод File, fnmatch?: File.fnmatch("*.rb", "hello.rb”) # => true File.fnmatchC*. [ch]", "ruby.c") # => true File.fnmatch("*.Cch]", "ruby.h") # => true File.fnmatchC?.txt". "ab.txt”) #=> false flags = File::FNM_PATHNAME | File::FNM_DDTMATCH File.fnmatch("lib/*.rb", "lib/a.rb". flags) File.fnmatch("lib/*.rb", "1ib/a/b.rb". flags) File.fnmatch("lib/**/*.rb". "lib/a.rb". flags) File.fnmatch!"1ib/**/*.rb". "lib/a/b.rb”, flags) # => true # => false # => true # => true 9.6.2. Вывод содержимого каталогов Простейшим способом вывода содержимого каталога является использование ме- тода Di г.entries или итератора Di г. foreach: # Получение имен всех файлов, имеющихся в каталоге config/ filenames = Dir.entriesC’config”) # Получение имен в виде массива Dir.foreachCconfig") {|filename| ... }# Перебор имен Постоянный порядок следования имен, возвращаемых этими методами, не гаран- тируется, и в эти имена (на Юникс-подобных платформах) включается «.» (теку- щий каталог) и «..» (родительский каталог). Для получения списка файлов, соот- ветствующих заданному шаблону, следует воспользоваться оператором Di г. []: Diг['*.data'] # Файлы с расширением "data" Di rE'ruby.*'] # Любое имя файла, начинающееся с "ruby." Dir['?' ] # Любое имя файла, состоящее из одного символа Dir[’*.[ch]'] # Любой файл, заканчивающийся на .с или ,h Dir[{java.rb}'] # Любой файл, заканчивающийся на .java или .rb Dir['*/*.rb'] # Любая Ruby-программа в любом непосредственно # примыкающем подкаталоге Diг[’**/*.rb'] # Любая Ruby-программа в любом порожденном каталоге Более эффективной альтернативой Dir[] является метод Dir.glob. (Глагол «glob» является старым термином ОС Юникс для определения соответствия имени фай- ла в системной оболочке.) По умолчанию этот метод работает так же, как и Di г[], но если ему передан блок, то он вместо того, чтобы вернуть массив, выдает по одному соответствующие имена файлов. Кроме этого метод glob воспринимает необязательный второй аргумент. Если ему в качестве второго аргумента передать константу File::FNM_DDTMATCH (вспомните рассмотренный ранее метод File, fnmatch), то в результат будут включены файлы,
9.6. Файлы и каталоги 423 чьи имена начинаются на «.» (В Юникс-системах такие файлы считаются скры- тыми и по умолчанию их имена не выводятся): Dir.glob('*.rb') {|f| ... } # Перебор всех Ruby-файлов Dir.glob('*') # Файлы с именами, начинающимися на # не включаются Dir.glob(,File::FNM_DOTMATCH) # Как и при использовании Dir.entries, # включаются файлы, начинающиеся на '.' Показанные здесь методы вывода содержимого каталогов и все методы, опреде- ляемые в классах Fi 1 е и Di г, вычисляющие относительные пути, делают все это от- носительно «текущего рабочего каталога», который является значением, глобаль- ным по отношению процессу работы Ruby-интерпретатора. Запрос и установка текущего рабочего каталога осуществляются с помощью методов getwd и chdi г: puts Di г.getwd Di г.chdir("..") Dir.chdir("../sibling") Dir.chdir("/home”) Di r.chdir home = Dir.pwd # Вывод текущего рабочего каталога # Изменение текущего рабочего каталога на # родительский каталог # Возвращение его обратно, к каталогу-потомку # Его изменение на абсолютно указанный каталог # Изменение его на домашний каталог пользователя # pwd является псевдонимом getwd Если методу chdi г передать блок, то при выходе из него исходное значение ката- лога будет восстановлено. Но при этом следует отметить, что хотя изменение ка- талога ограничено по времени, оно имеет глобальную область видимости и оказы- вает влияние на другие потоки выполнения. Два потока не вправе одновременно вызывать Di г. chdi г с блоком. 9.6.3. Проверка файлов В классе File определяется довольно много методов получения метаданных, ха- рактеризующих указанные файлы или каталоги. Многие методы возвращают низ- коуровневую информацию, зависящую от используемой операционной системы. Здесь будут показаны только широко используемые методы, обладающие наи- большей переносимостью. Полный список методов, определяемых в классах File и Fi 1 е:: Stat, можно получить с помощью инструментального средства ri. Следующие далее простые методы возвращают основную информацию о файле. Большинство из них являются предикатами, возвращающими true или fal se: f = "/usr/bin/ruby" # Имя файла для показанных ниже примеров # Определение наличия и типов файлов. File.exist?(f) # Существует ли указанный файл? Также: File.exists? Fi1е.fi1е?(f) # Это существующий файл? File.di rectory?(f) # Или это существующий каталог? File.symlink?(f) # В обоих случаях является ли это символьной # ссылкой? .. продолжение
424 Глава 9. Платформа Ruby # Методы определения размера файла. Для установки размера файла следует # воспользоваться методом File.truncate. File.size(f) Flle.size?(Г) File.zero?(f) # Размер файла в байтах. # Размер в байтах или nil, если файл пустой. # True если файл пустой. # Определение прав доступа к файлу. Для установки прав доступа следует # воспользоваться методом File.chmod (работа которого зависит от # используемой системы). File.readable?(f) # Доступен ли файл для чтения? Fi1е.writable?(f) # Доступен ли файл для записи? В середине слова # "writable" отсутствует буква "е". Fi 1 е.executablе?(f) # Является ли файл исполняемым? File.world_readable?(f) # Общедоступен ли он для чтения? Ruby 1.9. File.world_writable?(f) # Общедоступен ли он для записи? Ruby 1.9. # Атрибуты времени и даты # воспользоваться методом File.mtime(f) File.atime(f) файла. Для установки этих атрибутов File.utime. # => Время последнего изменения # => Время последнего доступа в следует в виде Time-объекта виде Time-объекта Другим способом определения типа имени (файл, каталог, символьная ссылка и т. д.) является метод ftype, который возвращает строку, указывающую этот тип. Предположим, что /usr/biп/ruby — это символьная ссылка (или ярлык) на/usr/bin/ rubyl.9: File, -ftype (" /usr/bi n/ruby ”) File.ftypeC/usr/bi n/rubyl. 9") Fi 1 e.ftypeC/usr/1 ib/ruby") File.ftype(”/usr/bin/ruby3.0") # => "link" # => "file" # => "directory" # SystemCallError: Такого файла или каталога # не существует Если нужно получить сразу несколько характеристик файла, то более эффектив- ным может оказаться вызов метода stat или 1 stat. (Метод stat следует по сим- вольным ссылкам; метод Istat возвращает информацию о самой ссылке.) Эти методы возвращают объект File::Stat, у которого имеются методы экземпляра с такими же именами (но без аргументов), как у методов класса, принадлежащих классу File. Эффективность использования метода stat состоит в том, что Ruby нужно лишь одно обращение к операционной системе для получения всех связанных с фай- лом метаданных. После этого Ruby-программа может получить всю нужную ей информацию из объекта Fi 1 е:: Stat: s = File.stat("/usr/bin/ruby") s.file? # => true s.di rectory? # => false s.ftype # => "file" s.readable? # => true s.writable? # => false s.executable? # => true
9.6. Файлы и каталоги 425 s.size # => 5492 s.atime # => Mon Jul 23 13:20:37 -0700 2007 Полный список методов, принадлежащих объекту File:: Stat, можно получить, если использовать в отношении этого объекта инструментальное средство ri. И напоследок еще один метод проверки файлов общего назначения — Kernel .test. Своему существованию он обязан исторической совместимостью с командой test оболочки Юникс. Возможности метода test в значительной степени перекрыва- ются методами класса, принадлежащими классу File, но его применение можно встретить в существующих Ruby-сценариях. Чтобы составить о нем полное пред- ставление, воспользуйтесь инструментальным средством ri: # Проверка отдельных файлов test ?е. "/usr/bin/ruby" test ?f. "/usr/bin/ruby" test ?d. "/usr/bin/ruby" test ?r, "/usr/bin/ruby" test ?w. "/usr/bin/ruby" test ?M, "/usr/bin/ruby" test ?s. "/usr/bin/ruby” # Проверка двух файлов: f и g test ?-. f. g test ?<. f. g test ?>. f. g test ?=. f. g # File.exist?("/usr/bin/ruby") # File.file?("/usr/bin/ruby") # Fi1e.di rectory?("/usr/bin/ruby") # File.readable?("/usr/bin/ruby") # File.writeable?("/usr/bin/ruby") # File.mtimeC/usr/bin/ruby") # File.size?("/usr/bin/ruby") # File.identical(f.g) # File(f).mtime < File(g).mtime # File(f).mtime > File(g).mtime # File(f).mtime == File(g).mtime 9.6.4. Создание, удаление и переименование файлов В классе Fi 1 е не определяется никаких специальных методов для создания фай- лов. Чтобы создать файл, нужно просто открыть его для записи, записать нуль или более байтов и закрыть его. Если не хочется затереть данные существующего файла, его нужно открыть в режиме добавления: # Создание (или перезаписывание данных) файла по имени "test" File.open("test". "w") {} # Создание (без затирки данных) файла по имени "test" File.openCtest". "а") {} Для изменения имени файла используется метод File, rename: File.rename("test". "test.old") # Сначала текущее, а затем новое имя Для создания символьной ссылки на файл используется метод File, syml ink: File.symlink("test.old". "oldtest") # Адресат ссылки. имя ссылки С помощью File.link можно создать «жесткую ссылку», в тех системах, которые ее поддерживают: File.linkCtest.old". "test2") # Адресат ссылки, имя ссылки
426 Глава 9. Платформа Ruby И наконец, чтобы удалить файл или ссылку, используется метод File.delete или его синонимом File.unllnk: File.delete!"test2") # Может быть также вызван с несколькими аргументами File.uni Ink("oldtest”) # для удаления нескольких названных файлов На системах, которые поддерживают усечение файла до указанного количества (возможно, до нуля) байтов, для этой цели можно воспользоваться методом File, truncate. Для установки времени последнего доступа и внесения изменений в файл используется метод F i 1 е. ut i me. А для изменения прав доступа к файлу — за- висящий от используемой платформы метод File, chmod: f = "log.messages" atime = mtime = Time.now File.truncatelf, 0) File.utime(atime, mtime, File.chmod(0600. f) # Имя файла # Новые данные о времени последнего доступа # и времени внесения изменений # Стирание всего существовавшего содержимого f) # Изменение атрибутов времени # Права доступа, используемые в Юниксе # -rw------: следует отметить восьмеричную # форму аргумента Для создания нового каталога используется метод Dir.mkdir. Для удаления ката- лога используется метод Di г. rmdi г или один из его синонимов, Di г. delete, или Dir. unlink. Для того чтобы удалить каталог, он должен быть пустым: Dir.mkdir!"temp") File.open("temp/f". "w") {} File.open("temp/g", "w") {} Fi1e.delete!*Di r["temp/*"]) Dir.rmdir("temp") # Создание каталога # Создание в нем файла # Создание еще одного файла # Удаление всех файлов из каталога # Удаление каталога 9.7. Ввод и вывод данных Объект ввода-вывода (ID-объект) — это поток данных: источник байтов или сим- волов, из которого можно производить считывание, или приемник байтов или символов, в который можно производить запись. Класс File является подклассом класса 10. 10-объекты также представляют потоки «стандартного входа» и «стан- дартного выхода», используемые для чтения из и записи в консольное устройство. Модуль stringio, принадлежащий стандартной библиотеке, позволяет создавать потоковые оболочки вокруг строковых объектов. И наконец, сокет-объекты, ис- пользуемые при работе с сетями (рассматриваемые далее в этой главе), также яв- ляются 10-объектами. 9.7.1. Открытие потоков Перед осуществлением ввода или вывода должен существовать 10-объект, из ко- торого будут считываться или в который будут записываться данные. В классе 10
9.7. Ввод и вывод данных 427 определяются фабричные методы new, open, рореп и pi ре, но это низкоуровневые методы, чья работа зависит от применяемой операционной системы, и здесь мы их рассматривать не будем. В следующих далее подразделах рассматриваются наиболее распространенные способы получения 10-объектов. (А в раздел 9.8 включены примеры создания 10- объектов, поддерживающих сетевую связь.) 9.7.1.1. Открытие файлов Один из наиболее распространенных примеров ввода-вывода — это файловое чте- ние или запись данных. В классе File определяется ряд полезных методов (рас- сматриваемых далее), позволяющих считывать все содержимое файла за один вызов. Тем не менее зачастую вместо этого открывается файл с целью получения File-объекта и последующего использования 10-методов для чтения данных из файла или их записи в него. Для открытия файла используется метод File.open (или File.new). Первым аргу- ментом служит имя файла. Оно обычно указывается в виде строки, но в Ruby 1.9 можно использовать любой объект, имеющий метод to_path. Имена файлов, если только они не заданы с указанием абсолютного пути, рассматриваются относи- тельно текущего рабочего каталога. Для разделения каталогов следует использо- вать символы прямого слэша — если работа ведется в Windows, то Ruby автомати- чески преобразует их в обратные слэши. Вторым аргументом для метода File, open служит короткая строка, указывающая, как именно должен быть открыт файл: f = File.open("data.txt", "г") # Открытие файла data.txt для чтения out = File.openCout.txt". "w") # Открытие файла out.txt для записи Второй аргумент метода File.open является строкой, указывающей «режим от- крытия файла». Он должен представлять собой одно из значений, перечисленных в следующей далее таблице. Чтобы предотвратить в Windows автоматическое преобразование признаков конца строки, следует добавить символ «Ь». При рабо- те с текстовыми файлами к строке режима можно добавить название кодировки символов. При работе с двоичными файлами к строке следует добавить « : binary». Этот режим рассматривается в разделе 9.7.2. Режим Описание "г" ”г+” Открытие для чтения. Этот режим используется по умолчанию Открытие для чтения и записи с начала файла. Выдает сбой, если файл не суще- ствует V "W+" "а" "а+" Открытие для записи. Создает новый или усекает существующий файл То же, что и "w", но допускается также и чтение файла Открытие для записи, но с добавлением в конец файла, если он уже существует То же, что и "а", но допускается также и чтение файла За методом File, open (но не за Fi 1 е. new) может следовать блок кода. Если блок пре- доставлен, то метод File.open не возвращает File-объект, а вместо этого передает
428 Глава 9. Платформа Ruby его блоку и автоматически его закрывает, когда происходит выход из блока. Зна- чение, возвращаемое блоком, становится значением, возвращаемым методом File, open: File.open(”log.txt”, "a”) do |log| # Открытие для добавления log.putsCINFO: Logging a message") # Вывод данных в файл end # Автоматическое закрытие файла 9.7.1.2. Kernel.open Определяемый в классе Kernel метод open работает так же, как и File.open, но при этом обладает большей гибкостью. Если имя файла начинается с символа |, то оно рассматривается как команда операционной системы, и возвращаемый поток ис- пользуется для чтения из и записи в этот процесс выполнения команды. Разуме- ется, все это зависит от применяемой платформы: # Какова продолжительность работы сервера? uptime = ореп("|uptime”) {|f| f.gets } Если загружена библиотека open-uri, то метод open также может быть использован для чтения из http и ftp URL-адресов, как будто это файлы: require "open-uri" # Востребование библиотеки f = open("http://www.davidflanagan.com/") # Веб-страница в роли файла webpage = f.read # Чтение ее как одной большой строки f.close # Не забывайте закрывать страницу! В Ruby 1.9 если используемый в качестве аргумента метода open объект имеет ме- тод to_open, будет вызван этот метод и возвращен 10-объект. 9.7.1.З. StringlO Еще один способ получения 10-объекта связан с использованием библиотеки st г i ng i о, и чтением из, и записи в строку: require "stringio" input = StringlO.open("now is the time") # Чтение из этой строки buffer = ”” output = StringlO.open(buffer, "w") # Запись в buffer Класс StringlO не является подклассом 10, но в нем определяется множество та- ких же методов, как и в классе 10, а типизация по общим признакам, так называе- мая duck-типизация, как правило, позволяет использовать StringlO-объект вместо 10-объекта. 9.7.1.4. Предопределенные потоки данных В Ruby предопределяется целый ряд потоков данных, которые могут быть ис- пользованы без необходимости их создания или открытия. Глобальные констан- ты STDIN, STDOUT и STDERR представляют собой соответственно стандартный вход- ной поток, стандартный выходной поток и стандартный поток вывода сообщений
9.7. Ввод и вывод данных 429 об ошибках. По умолчанию эти потоки подключены к пользовательской консо- ли или какому-нибудь окну терминала. В зависимости от того, как вызывался Ruby-сценарий, вместо этого они могут использовать файл или даже любой дру- гой процесс в качестве источника ввода или назначения вывода данных. Любая Ruby-программа может читать данные из стандартного ввода и записывать их в стандартный вывод (для нормальных выходных данных программы) или в стан- дартный вывод для сообщения об ошибке (для сообщений об ошибках, которые должны быть видимы, даже если стандартный вывод будет перенаправлен в файл). Значения глобальных переменных Jstdin, $stdout и $stderr изначально устанавли- ваются такими же, как и у потоковых констант. Такие глобальные функции, как print и puts, по умолчанию ведут запись в $stdout. Если сценарий изменяет зна- чение этих глобальных переменных, то он изменит и поведение этих методов. Но настоящий «стандартный вывод» будет по-прежнему доступен через STDOUT. Еще одним предопределенным потоком является ARGF, или $<. Этот поток ведет себя по-особенному, в расчете на упрощение написания сценариев, которые чита- ют данные из файлов, указанных в командной строке, или из стандартного ввода. Если в командной строке присутствуют аргументы, предназначенные для Ruby- сценария (в виде массива ARGV или массива $*), то поток ARGF действует, как будто эти файлы были объединены вместе, и получившийся таким образом единствен- ный файл открыт для чтения. Чтобы все это работало должным образом, Ruby- сценарий, воспринимающий ключи командной строки, а не имена файлов, должен сначала обработать эти ключи и удалить их из массива ARGV. Если массив ARGV пуст, то ARGF — это одно и то же, что и STDIN. (Другие подробности, касающиеся потока ARGF, рассмотрены в разделе 10.3.1.) И наконец, поток DATA разработан для чтения текста, который появляется после окончания Ruby-сценария. Работа с ним возможна лишь при включении в сце- нарий отдельной строкой лексемы_END_. Этой лексемой отмечается окончание текста программы. С помощью потока DATA могут быть считаны любые строки, по- являющиеся после этой лексемы. 9.7.2. Потоки и кодировки Одним из наиболее существенных изменений, появившихся в Ruby 1.9, являет- ся поддержка многобайтовых кодировок символов. В разделе 3.2 мы видели, что множество изменений было внесено в класс String. Такие же изменения были вне- сены и в класс 10. В Ruby 1.9 каждый поток должен иметь две связанные с ним кодировки. Они из- вестны как внешняя и внутренняя кодировки и возвращаются методами externa 1_ encoding и internal_encoding, принадлежащих 10-объекту. Внешняя кодировка — это кодировка текста в том виде, в котором он хранится в файле. Внутренняя кодировка — это кодировка, используемая для представления текста внутри Ruby Если внешняя кодировка совпадает с желаемой внутренней кодировкой, то указы- вать внутреннюю кодировку не нужно: строки, считанные из потока, будут иметь
430 Глава 9. Платформа Ruby связанную с ним внешнюю кодировку (как в случае использования метода force_ encoding, определенного в классе String). С другой стороны, если потребуется, что- бы внутреннее представление текста отличалось от внешнего представления, то можно указать внутреннюю кодировку, и Ruby в процессе чтения перекодирует этот текст из внешней во внутреннюю кодировку, а в процессе записи — обратно во внешнюю кодировку. Кодировка любого 10-объекта (включая конвейеры и сетевые сокеты) задается ме- тодом set encoding. Внешняя и внутренняя кодировка задаются с помощью двух аргументов. Можно также указать две кодировки единым строковым аргументом, содержащим два названия кодировок, разделенные двоеточием. Но, как правило, единственный аргумент задает только внешнюю кодировку. В качестве аргумен- тов могут использоваться строки или Encoding-объекты. Сначала всегда указыва- ется внешняя кодировка, за которой следует необязательное указание внутренней кодировки. Например: f.set_encoding("iso-8859-l", "utf-8") f.set_encod i ng("i so -8859 -1:ut f - 8") f.set_encoding(Encoding::UTF-8) # Latin-1, перекодируемая в UTF-8 # To же самое, что и строкой выше # Текст в кодировке UTF-8 Метод set encodi ng работает для всех разновидностей 10-объектов. Но для файлов зачастую проще указать кодировку при их открытии. Это делается добавлением названия кодировки к строке режима работы с файлом. Например: in = File.openC’data.txt". ”r:utf-8”): # Чтение текста в кодировке UTF-8 out = File.openC’log”. "a:utf-8”); # Запись текста в кодировке UTF-8 in = Fi le. openC’data. txt”. ”г:iso8859-l:utf-8"); # Latin-1, перекодируемая в UTF-8 Следует заметить, что потребность указывать две кодировки для потока, предна- значенного для вывода данных, обычно не возникает. В этом случае внутренняя кодировка задается String-объектами, которые записываются в поток. Если вообще не указать никакой кодировки, то при чтении из файлов Ruby в ка- честве исходной берет кодировку, установленную по умолчанию (об этом уже говорилось в разделе 2.4.2), а при записи в файлы или при чтении или записи с использованием конвейеров и сокетов не устанавливает по умолчанию никакой кодировки (то есть принимает кодировку ASCII-8BIT/BINARY). По умолчанию исходная внешняя кодировка берется из локальных пользователь- ских установок, и зачастую это многобайтовая кодировка пользователя. Поэтому чтобы прочитать из файла двоичные данные, нужно явно указать на то, что нуж- ны байты, не имеющие отношения к кодировке, или же будут получены символы во внешней кодировке, установленной по умолчанию. Для этого нужно открыть файл в режиме ” г: bi nary” или после открытия файла передать Encoding:: BINARY ме- тоду set_encoding: File.openC’data", "r:binary") # Открыть файл для чтения двоичных данных В Windows двоичные файлы нужно открывать в режиме " rb: bi nary" или вызывать для потока binmode. Тем самым будет предотвращена автоматическая конверта-
9.7. Ввод и вывод данных 431 ция символов новой строки, осуществляемая Windows, и такой прием необходим только при работе на этой платформе. Не каждый метод, предназначенный для чтения данных из потока, принимает ко- дировку этого потока. Некоторые низкоуровневые методы чтения воспринимают аргумент, указывающий количество считываемых байтов. По своей природе эти методы возвращают не текстовые строки, а строки, состоящие из байтов без учета кодировки. А методы, в которых не указывается количество считываемых данных, кодировку соблюдают. 9.7.3. Чтение из потока В классе 10 определяется ряд методов для чтения из потоков. Они, конечно же, работают в том случае, если поток можно прочитать. Данные могут читаться из потоков STDIN, ARGF и DATA, но не из потоков STDOUT или STDERR. Файлы и StringlO- объекты по умолчанию открыты для чтения, пока не будет явным образом указа- но, что их следует открыть только для записи. 9.7.З.1. Чтение строк В классе 10 определяется несколько способов чтения строк из потока: lines = ARGF.read1 Ines # Читает все входящие данные, возвращает массив строк line = DATA.readline # Читает одну строку из потока print 1 while 1 = DATA.gets # Чтение, пока gets не вернет nil в конце файла DATA.each {|line| print line } # Перебор строк из потока до конца файла DATA.eachJine # Псевдоним для each DATA.lines # Нумератор для eachjine: Ruby 1.9 В отношении этих методов построчного чтения есть ряд важных замечаний. Во- первых, методы readl i пе и gets отличаются только способом обработки EOF (кон- ца файла — end-of-file: условия возникающего, когда из потока уже нечего читать). Метод gets возвращает nil, если он вызывается для потока в конце файла — EOF. Метод readline вместо этого выдает EDFError. Если количество ожидаемых строк неизвестно, следует использовать gets. Если ожидается еще одна строка (и ее от- сутствие должно рассматриваться как ошибка), то используйте readl 1 пе. Достиже- ние потоком конца файла можно проконтролировать с помощью метода eof?. Во-вторых, gets и readline неявным образом устанавливают значение глобальной переменной $_ равным строке текста, которую они вернули. Ряд глобальных ме- тодов, таких как print, используют если аргумент им в явном виде не передан. Поэтому цикл wh i 1 е в только что показанном коде может быть записан еще короче: print while DATA.gets Лучше всего полагаться на $_ в коротких сценариях, но в более длинных програм- мах более приемлемым стилем считается явное использование переменных, ис- пользуемых для хранения строк, считываемых из входных данных.
432 Глава 9. Платформа Ruby В-третьих, эти методы используются, как правило, для текстовых (а не для дво- ичных) потоков, и «строка» определяется как последовательность байтов вплоть до включаемого в нее признака конца строки (на многих платформах — символа новой строки). Строки, возвращаемые этими методами, включают признак конца строки (хотя у последней строки файла его может и не быть). Для его удаления ис- пользуется метод Stri ng. chomp!. Признак конца строки содержится в специальной глобальной переменной $/. Можно установить такое значение $/, которое изменит исходное поведение всех методов чтения строк, или можно просто любому из ме- тодов (включая итератор each) передать другой разделитель. К примеру, это мож- но сделать при чтении из файлов полей, разделенных запятыми, или при чтении двоичного файла, у которое есть какой-нибудь «символ-разделитель записей». В отношении признака конца строки есть два особых случая. Если в этом качестве указано значение nil, то методы чтения строк продолжают чтение, пока не будет достигнут конец файла (EOF), и возвращают все содержимое потока в виде одной строки. Если в качестве признака конца строки указать пустую строку — то методы чтения строк считывают данные по абзацам, выискивая в качестве раз- делителя пустую строку. В Ruby 1.9 методы gets и readline воспринимают в качестве необязательного пер- вого или второго аргумента, идущего после строки-разделителя, целое число. Если этот аргумент присутствует, то целое число указывает максимальное количество байтов, считываемых из потока. Присутствие этого ограничивающего аргумента обусловлено необходимостью предотвращения случайного считывания неожи- данно длинных строк, и эти методы являются исключениями из ранее упомянуто- го правила; они возвращают строку символов с учетом кодировки, несмотря на тот факт, что у них есть ограничивающий аргумент, выраженный в байтах. И наконец, методы чтения строк gets, readline и итератор each (а также его псев- доним each line) отслеживают количество считанных ими строк. Воспользовав- шись методом 1 i neno, можно запросить номер самой последней считанной строки, а установить значение номера строки можно с помощью метода доступа 11 пепо=. Следует заметить, что на самом деле метод 11 neno не подсчитывает количество символов новой строки в файле. Он подсчитывает количество вызовов методов чтения строк и при использовании разных символов разделения строк может воз- вращать различные результаты: DATA.lineno = О DATA.readline DATA.1ineno $. # Начало отсчета с нулевой строки, даже если данные находятся # в конце файла # Чтение одной строки данных # => 1 # => 1: магическая глобальная переменная, значение которой # устанавливается неявным образом 9.7.3.2. Чтение файлов целиком В классе 10 определяются три метода класса для чтения файла даже без откры- тия потока ввода-вывода. Метод 10. read читает весь файл целиком (или какую-
9.7. Ввод и вывод данных 433 то часть файла) и возвращает его в виде одной строки. Метод 10. readl Ines читает указанный файл целиком и помещает его содержимое в массив строк. И метод lO.foreach перебирает строки указанного файла. Ни один из этих методов класса не требует создания экземпляра 10-объекта: data = lO.readCdata") data = lO.readCdata", 4. 2) # Чтение и возвращение целиком всего файла # Чтение 4 байтов, начиная со второго байта data = lO.readCdata". nil. 6) # Чтение с шестого байта до конца файла # Чтение строк в массив words = 10.readl1nes("/usr/share/dict/words") # Построчное чтение и инициализация хэша words = {} IO.foreach("/usr/share/dict/words”) {|w| words[w] = true} Хотя эти методы класса определены в классе 10, они работают с указанными фай- лами, и их также часто можно увидеть в качестве вызываемых методов класса, принадлежащих классу File: File, read, Fi le. readl ines и File.foreach. В классе 10 также определяется метод экземпляра по имени read, подобный од- ноименному методу класса; без аргументов он читает текст до тех пор, пока не будет достигнут конец потока данных, и возвращает его в виде строки с учетом кодировки символов: # Альтернативный вариант для выражения text = File.readCdata.txt”) f = File.openCdata.txt") # Открытие файла text = f.read # Чтение его содержимого в виде текста f.close # Закрытие файла Метод экземпляра 10. read может также использоваться с аргументами для чтения из потока указанного количества байтов. Использование его в этом качестве рас- смотрено в следующем разделе. 9.7.3.3. Чтение байтов и символов В классе 10 также определяются методы для чтения из потока одного или более байтов или символов, но при переходе от Ruby 1.8 к Ruby 1.9 эти методы претер- пели существенные изменения, поскольку в Ruby изменилось само определение символов. В Ruby 1.8 байты и символы по сути одно и то же, и методы getc и readchar читают отдельный байт и возвращают его в виде Fixnum-объекта. Аналогично методу gets, метод getc в конце файла возвращает nil. А метод readchar, так же как и метод readl i пе, если вызывается в конце файла, то выдает EOFError. В Ruby 1.9 в методы getc и readchar были внесены изменения, позволяющие вме- сто Fixnum-объекта возвращать строку единичной длины. При чтении из потока с многобайтовой кодировкой эти методы читают необходимое количество байтов, чтобы прочитать один символ. Для побайтового чтения в Ruby 1.9 используются новые методы getbyte и readbyte.
434 Глава 9. Платформа Ruby Метод getbyte похож на методы getc и gets: в конце файла он возвращает ni 1. А ме- тод readbyte похож на методы readchar и read 11 пе: он выдает EOFError. Программы (подобные парсерам), которые производят посимвольное чтение из потока, временами нуждаются в помещении отдельного символа назад, в буфер потока, чтобы он был возвращен при следующем вызове метода чтения. Сделать это они могут с помощью метода ипдеЕс.Этот метод предполагает использование Fixnum-объекта в Ruby 1.8 и отдельного символа в Ruby 1.9. Символ, помещенный обратно в буфер, будет возвращен при следующем вызове метода getc или метода readchar: f = File.openCdata". ”r:binary") # Открытие файла данных для двоичного чтения с = f.getc # Считывание первого байта в виде целого числа f.ungetc(c) # Помещение этого байта назад в поток с = f.readchar # Его повторное чтение Другой способ чтения байта из потока связан с использованием итератора each_ byte. Этот метод передает каждый байт потока связанному блоку: f.each_byte {|b| ... } # Перебор всех оставшихся байтов f.bytes # Нумератор для each_byte: Ruby 1.9 Если за один вызов метода нужно прочитать более одного байта, то можно оста- новить свой выбор на одном из пяти методов, которые своим поведением слегка отличаются друг от друга. readbytes(n) Считывает строго п байтов и возвращает их в виде строки. Выставляет блоки- ровку, если это необходимо, до тех пор пока не поступят п байтов. По достиже- нии конца файла до того, как будут доступны п байтов, выдает EOFError. readpartial(n. buffer=ni1) Считывает байты между первым и n-ным и возвращает их в виде новой дво- ичной строки или, если в качестве второго аргумента передан String-объект, сохраняет считанные байты в этой строке (переписывая любой содержащийся в ней текст). Если для чтения доступен один или более байтов, этот метод их тотчас же возвращает (вплоть до максимального количества в п байтов). Вы- ставляет блокировку только в том случае, если недоступен ни один байт. Если вызвать этот метод, когда поток находится в конце файла, он выдает EOFError. read(n=nil. buffer=nil) Считывает п байтов (или меньше, если достигнут конец файла), при необхо- димости выставляет блокировку, пока не будут готовы все байты. Считанные байты возвращаются в виде двоичной строки. Если второй аргумент являет- ся существующим String-объектом, то считанные байты сохраняются в этом объекте (заменяя собой любое существовавшее до них содержимое) и метод возвращает этот строковый объект. Если поток находится в конце файла и ука- зано количество считываемых байтов — п, метод возвращает nil. Если метод вызван в конце файла и п опущен или равен nil, то метод возвращает пустую строку —
9.7. Ввод и вывод данных 435 Если п равен ni 1 или опущен, то этот метод читает оставшуюся часть потока и возвращает считанные байты в виде строки с кодировкой, а не в виде строки байтов, для которой кодировка не используется. read_nonblock(n, buffer=nil) Считывает байты (вплоть до максимального количества в п байтов), которые доступны для чтения в данный момент, и возвращает их в виде строки, исполь- зуя строку buffer, если таковая указана. Этот метод не осуществляет блокиров- ку. Если данные, готовые для считывания из потока, отсутствуют (к примеру, такое может произойти при чтении из сетевого сокета или из STDIN), этот метод выдает SystemCal 1 Error. Если метод вызван в конце файла, он выдает EOFError. В Ruby 1.9 используется новая версия этого метода. (В Ruby 1.9 также опреде- ляются другие методы ввода-вывода, не выставляющие блокировки, но они от- носятся к низкоуровневым и в данной книге не рассматриваются.) sysread(n) Этот метод работает так же, как и readbytes, но действует на низком уровне без буферизации. Вызовы sysread не следует смешивать с другими методами чте- ния строки или байта, поскольку эти методы несовместимы. Следующие далее примеры кода можно использовать при чтении двоичного файла: f = File.openC’data.bin", "rb:binary") # Предотвращение конверсии символов # новой строки и отсутствие кодировки magic = f.readbytes(4) # Сначала четыре байта, идентифицирующие тип файла exit unless magic == "INTS" # Магическое число пишется как "INTS" (ASCII) bytes = f.read # Чтение оставшейся части файла # Используется двоичная кодировка, стало быть это байтовая строка data = bytes.unpack!"i*") # Превращение байтов в целочисленный массив 9.7.4. Запись в поток Методы, определяемые в классе 10 для записи в поток, являются зеркальным от- ражением методов, предназначенных для чтения. Запись может осуществляться в потоки STDOUT и STDERR, как будто файлы открыты в любом режиме, отличном от "г" или "rb". Для записи в поток одиночных байтов или символов в классе 10 определяется единый метод putc. Этот метод воспринимает в качестве аргумента либо байто- вое значение, либо односимвольную строку, и поэтому он не претерпел изменений в Ruby 1.9 по сравнению в версией 1.8: о = STDOUT # Односимвольный вывод о.putc(65) # Запись отдельного байта 65 (заглавная буква А) o.putcC'B") # Запись отдельного байта 66 (заглавная буква В) o.putcC'CD") # Запись только первого байта строки
436 Глава 9. Платформа Ruby В классе 10 определяется несколько других методов для записи произвольных строк. Эти методы отличаются друг от друга количеством воспринимаемых аргу- ментов и тем, что одни из них добавляют, а другие не добавляют признаки конца строки. Следует напомнить, что в Ruby 1.9 текстовый вывод подвергается пере- кодировке во внешнюю кодировку потока, если таковая была указана: о = STDOUT # Вывод строки о « х о « X « у о.print о.print s о.print s.t o.printf fmt,*args о.puts о.puts X о.puts x.y o.puts [x.y] o.write s o.syswrite s # Вывод x.to_s # Может быть выстроен в цепочку: вывод x.to_s + y.to_s # Вывод $_ + $\ # Вывод s.to_s + $\ # Вывод s.to_s + t.to_s + J\ # Выводит fmU[args] # Вывод символа новой строки # Вывод х.to_s.chomp плюс символ новой строки # Вывод х.to_s.chomp, символ новой строки, у.to_s.chomp, # символ новой строки # То же самое, что и в предыдущей строке # Вывод s.to_s. возвращает s.to_s.length # Низкоуровневая версия write Потоки вывода, так же как строки и массивы, допускают добавления, и значения в них можно записывать с помощью оператора «. Метод puts является одним из самых распространенных методов вывода. Он превращает каждый из своих аргу- ментов в строку и записывает каждую строку в поток. Если строка еще не заверше- на символом новой строки, он добавляет такой символ. Если любой из аргументов метода puts представляет собой массив, то этот массив подвергается рекурсивно- му развертыванию и каждый элемент выводится в свою собственную строку, как будто он был непосредственно передан методу puts в качестве аргумента. Метод print конвертирует свои аргументы в строки и выводит их в поток. Если значение nil, используемое по умолчанию для глобального разделителя файлов $, было из- менено, то его новое значение выводится между каждым аргументом, переданным методу print. Если значение nil, используемое по умолчанию для разделителя вы- водимой записи $/, было изменено, то его новое значение выводится после того, как будут выведены все аргументы. Метод printf в качестве своего первого аргумента предусматривает использова- ние отформатированной строки и вставляет значения любых дополнительных аргументов в эту отформатированную строку, используя принадлежащий классу String оператор X. Затем он выводит строку со вставками без символа новой стро- ки или разделителя записи. Метод write просто выводит свой единственный аргумент точно так же, как это де- лает оператор «, и возвращает количество записанных байтов. И наконец, метод syswrite является низкоуровневой, небуферируемой, неперекодируемой версией метода write. Метод syswrite нужно использовать в исключительных случаях и не смешивать его с любыми другими методами записи.
9.7. Ввод и вывод данных 437 9.7.5. Методы произвольного доступа Некоторые потоки, представляющие, в частности, сетевые сокеты или пользова- тельский ввод с консоли, являются последовательным потоками: как только было произведено чтение из этих потоков или запись в них, вернуться назад уже невоз- можно. Другие потоки, такие как чтение из файлов или строк или запись в них, допускают произвольный доступ к данным с помощью рассматриваемых в этом разделе методов. Если попытаться воспользоваться этими методами по отноше- нию к потокам, которые не допускают произвольного доступа, они выдадут ис- ключение SystemCal1 Except!on: f = File.open("test.txt") f.pos # => 0: возвращает текущую позицию в байтах f.pos =10 # переход к позиции 10 f.tell # => 10: синоним для pos f.rewind # Возвращение к позиции 0. также сбрасывает счетчик строк в 0 f.seekdO, 10::SEEK_SET) # Переход к абсолютной позиции 10 f.seek(10, 10::SEEK_CUR) # Переход на 10 байтов от текущей позиции f.seek(-10, 10::SEEK_END) # Переход на 10 байтов от конца f.seek(0, 10::SEEK_END) # Переход в самый конец файла f.eof? # => true: теперь мы в конце файла Если в программе используются методы sysread или syswrite, то для произвольно- го доступа вместо метода seek следует использовать метод sysseek. Метод sysseek похож на метод seek, за исключением того, что он после каждого вызова возвраща- ет новую позицию в файле: pos = f.sysseek(0, 10::SEEK_CUR) f.sysseek(0, 10::SEEK_SET) f.sysseek(pos, 10::SEEK_SET) # Получение текущей позиции # Обратный переход в начало потока # Возвращение в исходную позицию 9.7.6. Закрытие, сброс и тестирование потоков После осуществления чтения из потока или записи в него поток нужно закрыть с помощью метода close. Тем самым будет сброшен любой буферизованный ввод или вывод, а также освобождены ресурсы операционной системы. Ряд методов, открывающих потоки, позволяют связывать с ними блок кода. Они передают от- крытый поток в блок и автоматически закрывают поток при осуществлении вы- хода из блока. Такое управление потоками гарантирует их правильное закрытие, даже если будет выдано исключение: File.open("test.txt") do |f| # Здесь используется поток f # Значение этого блока становится возвращаемым значением метода open End # Здесь поток f автоматически закрывается без нашего участия Альтернативой использованию блока служит использование собственного пред- ложения ensure:
438 Глава 9. Платформа Ruby begin f = File.open("test.txt") # Здесь используется поток f ensure f.close if f end Сетевые сокеты реализуются с использованием 10-объектов, которые внутри себя имеют раздельные потоки чтения и записи. Для индивидуального закрытия этих внутренних потоков можно воспользоваться методами close read и close_write. Хотя файлы могут быть открыты для чтения и записи одновременно, использо- вать методы close_read и close_write для этих 10-объектов нельзя. Имеющиеся в Ruby методы вывода данных (за исключением syswrite) для повы- шения эффективности своей работы буферизуют вывод. Выходной буфер в при- емлемые моменты, такие как вывод символа новой строки или чтение из соответ- ствующего входного потока, сбрасывается. Но бывают моменты, когда выходной буфер нужно сбросить явным образом, чтобы заставить выходные данные быть сразу же отосланными: out.print 'wait>' out. flush sleep(l) # Отображение приглашения # Принудительный сброс данных выходного буфера в адрес # операционной системы # Приглашение появляется еще до того, как мы успеем заснуть out.sync = true out.sync = false out.sync out.fsync # Автоматический сброс буфера после каждой записи # Не делать автоматический сброс # Возвращение к текущему режиму синхронизации # Сброс буфера вывода и запрос у операционной системы сброса # ее буферов # Если на текущей платформе это не поддерживается, то # возвращается nil В классе 10 определяется ряд предикатов для проверки состояния потока: f .eof? f .closed? f.tty? # true, если поток находится в состоянии конца файла # true, если поток был закрыт # true, если поток является интерактивным Единственный метод, требующий объяснения — это tty?. Этот метод и его псев- доним i satty (без вопросительного знака) возвращает true, если поток подключен к интерактивному устройству, такому как окно терминала или клавиатура, за ко- торым (предположительно) сидит человек. Эти методы возвращают false, если поток не относится к интерактивным потокам, то есть представляет собой файл, конвейер или сокет. Программа может использовать tty?, чтобы не выводить при- глашения на пользовательский ввод, если STDIN фактически был перенаправлен и данные, к примеру, поступают из файла.
9.8. Работа в сета 439 9.8. Работа в сети Возможности Ruby по работе в сети обеспечиваются стандартной библиотекой, а не классами ядра. По этой причине в следующих подразделах мы не пытаемся пере- числить каждый доступный класс или метод. Вместо этого в них на простых приме- рах демонстрируется, как выполнить обычные задачи, связанные с работой в сети. Более полные описания можно получить с помощью инструментального средства ri. На самом низком уровне работа в сети выполняется с помощью сокетов, кото- рые являются разновидностью 10-объекта. Располагая открытым сокетом, можно читать данные с другого компьютера или записывать их туда, как будто осуществ- ляется чтение из файла или запись в него. Иерархия классов сокета имеет несколь- ко запутанный характер, но ее детали в приводимых далее примерах не важны. Клиенты Интернета используют класс TCPSocket, а серверы Интернета — класс TCPServer (который также является сокетом). Все сокет-классы являются частью стандартной библиотеки, поэтому для использования их в Ruby-программе снача- ла нужно включить в нее следующую строку: require 'socket' 9.8.1. Самый простой клиент Для создания клиентских интернет-приложений используется класс TCPSocket. Экземпляр TCPSocket получается с помощью метода класса TCPSocket.open или его синонима TCPSocket.new. В качестве первого аргумента ему следует передать имя узла, к которому нужно подключиться, а в качестве второго аргумента — номер порта. (Номер порта должен быть целым числом в диапазоне от 1 до 65535, ука- занным в виде Fixnum- или String-объекта. Разные интернет-протоколы использу- ют различные порты. Например веб-службы по умолчанию используют порт 80. Вместо номера порта можно также передать в виде строки имя интернет-службы, такой как «http», но эта возможность еще не вполне достаточно задокументирова- на и может зависеть от используемой системы.) Располагая открытым сокетом, из него можно считывать данные, как из любого другого 10-объекта. Но следует помнить, что его, как и файл, после чтения необхо- димо закрыть. В следующем далее коде показан очень простой клиент, подключа- ющийся к заданному узлу и порту, читающий из сокета любые доступные данные с последующим выходом: require 'socket' # Сокеты находятся в стандартной библиотеке host, port = ARGV # Узел и порт берутся из командной строки s = TCPSocket.open(host, port) # Открытие сокета для указанного узла и порта while line = s.gets # Чтение строки из сокета puts line.chop # и ее вывод с признаком конца строки, используемым end # данной платформой s.close # Закрытие сокета по окончании работы
440 Глава 9. Платформа Ruby Как и Fi 1 е. open, метод TCPSocket. open может быть вызван с блоком. В такой форме он передает открытый сокет блоку и автоматически закрывает сокет по выходу из блока. Таким образом, этот код можно написать в следующем виде: require 'socket' host, port = ARGV TCPSocket.open(host, port) do |s| # Использование формы метода open с блоком кода while line = s.gets puts line.chop end end # Сокет автоматически закрывается Этот клиентский код подходит для использования со службами наподобие старо- модной (и утратившей уже свое значение) Юникс-службы «дневного времени». К подобным службам клиент не делает запросов, а просто подключается, и сервер посылает ответ. Если отыскать интернет-узел с запущенным сервером для тести- рования клиента не удастся, не стоит расстраиваться — в следующем разделе бу- дет показано, как создать такой же простой сервер времени. 9.8.2. Самый простой сервер Для создания интернет-серверов мы воспользуемся классом TCPServer. По сути TCPServer-объект является фабрикой для TCPSocket-объектов. Для указания пор- та вашей службы и создания TCPServer-объекта следует вызвать TCPServer.open. Затем для возвращенного TCPServer-объекта нужно вызвать метод accept. Этот метод ждет до тех пор, пока клиент подключается к указанному порту, а затем возвращает TCPSocket-объект, представляющий подключение к этому клиенту. Представленный далее код показывает, как можно создать простой сервер време- ни. Он ожидает подключения к порту 2000. Когда клиент подключается к этому порту, он отправляет клиенту текущее время и закрывает сокет, тем самым пре- рывая подключение клиента: require 'socket' server = TCPServer.open(2000) loop { client = server.accept cl lent.puts(Time.now.ctime) client.close } # Получение сокетов из стандартной библиотеки # Сокет для ожидания подключения к порту 2000 # Бесконечный цикл: сервер работает вечно # Ожидание подключения клиента # Отправка клиенту текущего времени # Отключение от клиента Для проверки работы этого кода его нужно запустить в фоновом режиме или в другом окне терминала. Затем запустить код простого клиента, показанный выше, использовав следующую командную строку: ruby cl lent.rb localhost 2000
9.8. Работа в сети 441 9.8.3. Датаграммы Как показано ранее, большинство интернет-протоколов реализуются с помощью классов TCPSocket и TCPServer. Менее затратной альтернативой служит использова- ние UDP-датаграмм (UDP — User Datagram Protocol, протокол датаграмм поль- зователя) с помощью класса UDPSocket. UDP дает возможность компьютерам отправлять другим компьютерам индиви- дуальные пакеты данных, без издержек на установку постоянного подключения. В приводимом далее клиентском и серверном коде демонстрируется следующее: клиент отправляет датаграмму, содержащую строку текста, указывающую узел и порт. Сервер, который должен быть запущен на этом узле и ожидать подклю- чения к этому порту, получает текст, переводит его в верхний регистр (понимаю, не слишком завидная роль для службы) и отправляет его назад во второй дата- грамме. Начнем с клиентского кода. Следует отметить, что хотя UDPSocket-объекты явля- ются объектами ввода-вывода, датаграммы существенно отличаются от других потоков ввода-вывода. Именно поэтому мы избегаем применения ID-методов и используем низкоуровневые методы отправки и получения, определенные в UDPSocket. Второй аргумент метода send определяет флажки. Он является обя- зательным, даже если никакие флажки не устанавливаются. Аргумент метода recvfrom определяет максимальный объем данных, который мы рассчитываем по- лучить. В данном случае мы ограничиваем объем передачи данных между своим клиентом и сервером одним килобайтом: require 'socket' # Используется стандартная библиотека host, port, request = ARGV # Получение аргументов из командной строки ds = UDPSocket.new ds.connect(host, port) ds.send(request, 0) response,address = ds.recvfrom(1024) puts response # Создание сокета датаграмм # Подключение к порту на узле # Отправка текста запроса # Ожидание ответа (максимум 1 Кб) # Вывод ответа Код сервера, так же как и код клиента, использует класс UDPSocket — для серверов, работающих с датаграммами специального класса UDPServer не предусмотрено. Для подключения к сокету вместо вызова метода connect наш сервер вызывает метод Ы nd, чтобы сообщить сокету, к какому порту следует ожидать подключения. Затем сервер точно так же, как и клиент, но уже в обратном порядке использует методы send и recvfrom. Он вызывает recvfrom, чтобы ждать до тех пор, пока на указанный порт не будет получена датаграмма. Как только это произойдет, он конвертирует полученный текст, приводя все символы к верхнему регистру, и отсылает его на- зад. Здесь важно отметить, что метод recvfrom возвращает два значения. Первое — это полученные данные. А второе — это массив, содержащий информацию о том, откуда получены эти данные. Из этого массива мы извлекаем информацию об узле и порте и используем ее для оправки клиенту ответа:
442 Глава 9. Платформа Ruby require 'socket' # Используется стандартная библиотека port = ARGVEO] # Порт, к которому ожидается подключение ds = UDPSocket.new ds.bind(nil, port) # Создание нового сокета # Настройка его на ожидание подключения # к порту loop do # Организация бесконечного цикла request.address=ds.recvfrom(1024) # Ожидание получения каких-нибудь данных response = request.upcase # Приведение текста запроса к верхнему # регистру clientaddr = address[3] clientname = address[2] clientport = address[l] ds.send(response. 0, clientaddr. clientport) # С какого 1р-адреса послан запрос? # Имя узла? # С какого порта он был послан? # Отправка ответа туда... # ...откуда пришел запрос # Регистрация клиентского подключения puts "Подключение из: #{cl1 entname} #{cl1entaddr} #{cl1 entport}" end 9.8.4. Более сложный пример клиента Представленный далее код относится к более развитому интернет-клиенту в сти- ле telnet. Он подключается к указанному узлу и порту, а затем работает в цикле, он считывает строки входных данных с консоли, отправляет их на сервер, а затем считывает и выводит полученный с сервера ответ. С его помощью демонстрируется определение локальных и удаленных адресов сетевого подключения, добавление обработки исключений и использование 10- методов read_nonblock и readpartial, рассмотренных ранее в этой главе. Код снаб- жен подробным комментарием и не требует разъяснений: require 'socket' # Сокеты из стандартной библиотеки host, port = ARGV # Сетевой узел и порт в командной строке begin # Begin используется для обработки исключений # Предоставление пользователю ответной информации при подключении. STDOUT.print "Подключение..." # Сообщение о происходящем STDOUT.flush # Обеспечение немедленного отображения s = TCPSocket.open(host, port) # Подключение STDOUT.puts "готово" # И сообщение о его осуществлении # А теперь отображение информации о подключении. local, peer = s.addr, s.peeraddr STDOUT.print "Осуществлено подключение к #{peer[2]}:#{peer[l]}" STDOUT.puts " используется локальный порт #{local[l]}" # Небольшое ожидание отправки с сервера какого-нибудь начального сообщения.
9.8. Работа в сети 443 begin siеер(0.5) msg = s.read_nonblock(4096) STDOUT.puts msg.chop rescue SystemCal1 Error # Если end # Полусекундное ожидание # Чтение того, что уже готово # И его отображение для чтения ничего не готово, то исключение просто игнорируется # Теперь начало цикла взаимодействия клиент-сервер. loop do STDOUT.print ’> ' # Приглашение на дисплее для локального ввода STDOUT.flush # Обеспечение отображения приглашения local = STDIN.gets # Чтение строки с консоли break if !local # Переход к началу цикла, если нет ввода с консоли s.putsdocal) # Отправка строки на сервер s.flush # Принуждение к отправке # Чтение ответа сервера и его вывод. # Сервер может послать более одной строки, поэтому для чтения всего, # что им послано, используется readpartial, поскольку все прибывает # одной порцией. response = s.readparti al(4096) # Чтение ответа, полученного с сервера puts(response.chop) # Отображение ответа пользователю end rescue # Если что-то не получилось, puts $! # отображение исключения пользователю ensure # И независимо от хода событий s.close if s # не забываем закрыть сокет end 9.8.5. Мультиплексированный сервер Показанный ранее в этом разделе простой сервер времени никогда не обслуживал подключение какого-нибудь клиента — он просто сообщал клиенту время и раз- рывал подключение. Многие более сложные серверы обслуживают подключение, и чтобы быть пригодными к использованию, они должны предоставлять возмож- ность подключения нескольких клиентов и одновременной работы с ними. Один из способов реализации такой работы связан с использованием потоков выполне- ния — работа с каждым клиентом запускается в своем собственном потоке. Чуть позже в этой главе мы покажем пример многопоточного сервера. Но здесь будет рассмотрена альтернатива, связанная с созданием мультиплексированного серве- ра, использующего метод Kernel. sei ect. Когда у сервера есть несколько подключенных клиентов, то он не может вызывать для сокета какого-нибудь одного клиента блокирующий метод наподобие gets. Если он заблокирует работу в ожидании ввода от одного клиента, то он не сможет получать ввод от других клиентов или принимать подключения новых клиентов.
444 Глава 9. Платформа Ruby Эта проблема решается с помощью метода select; он позволяет нам блокировать весь массив 10-объектов и возвращать управление при проявлении активности любого из этих объектов. Возвращаемым значением метода sei ect является массив массивов, в которых содержатся 10-объекты. Первым элементом массива является массив потоков (в данном случае сокетов), в котором имеются данные, которые будут прочитаны (или подключение, которое будет принято). Разобравшись таким образом с работой метода sei ect, можно приступить к изуче- ния следующего далее кода сервера. Реализуемая им служба довольно проста — он всего лишь реверсирует каждую строчку клиентского ввода и отправляет ее назад. Интерес представляет тот механизм, который обрабатывает несколько под- ключений. Следует отметить, что мы используем select для контролирования как TCPServer-объекта, так и каждого из TCPSocket-объектов. Также нужно отметить, что сервер обрабатывает случаи запроса клиентом отключения, а также случаи, когда клиент отключается неожиданно: # Этот сервер читает строку клиентского ввода, реверсирует ее и отправляет # обратно. # Если клиент отправляет строку "quit", то он отключается. # Для обработки нескольких сессий используется Kernel.select. require 'socket' server = TCPServer.open(2000) sockets = [server] log = STDOUT while true ready = select(sockets) readable = readyEO] readable.each do |socket| if socket == server client = server.accept sockets « client # Ожидание подключения к порту 2000 # Массив контролируемых сокетов # Отправка регистрационных сообщений н. # стандартный выход # Бесконечный цикл работы сервера # Ожидание готовности сокета # Эти сокеты могут подвергаться чтению # Циклический перебор читаемых сокетов # Если серверный сокет готов, # прием подключения нового клиента # и добавление его к набору сокетов # Сообщение клиенту, для чего и куда он подключился. client.puts "Служба реверсирования vO.Ol, запущенная на #{Socket.gethostname]" # И регистрация факта подключения клиента log.puts "Принято подключение от #{client.peeraddr[2]}" else # Или же, если клиент готов, input = socket.gets # чтение клиентского ввода # Если ввода нет, то клиент отключился if (input log.puts "Клиент на #{socket.peeraddr[2]} отключился." sockets.delete(socket) # Остановка ожидания на этом сокете socket.close # Закрытие сокета next # И переход к следующему end
9.8. Работа в сети 445 input.chop! # Урезание клиентствого ввода if (input == "quit") # Если клиент запросил завершение работы, socket.puts("Пока!"); # с ним следует попрощаться log.puts "Закрытие подключения к #{socket.peeraddr[2]}" sockets.delete(socket) # Прекращение ожидания на сокете socket.close # Прекращение сессии else # Или же, если клиент не завершил работу, socket.puts(input.reverse) # следует реверсировать ввод # и отослать его обратно end end end end 9.8.6. Извлечение веб-страниц Библиотеку сокетов можно использовать для реализации любого интернет-про- токола. Вот, к примеру, код для извлечения содержимого веб-страницы; require 'socket' # Нам нужны сокеты host = 'www.example.com' # Веб-сервер port =80 # HTTP-порт, используемый по умолчанию path = "/index.html" # Нужный нам файл # Это HTTP-запрос, отправляемый для извлечения файла request = "GET #{path} НТТР/1.0\r\n\r\n" socket = TCPSocket.open(host,port) # Подключение к серверу socket.print(request) # Отправка запроса response = socket.read # Чтение всего ответа целиком # Разбиение ответа по первой пустой строке на заголовки и тело headers,body = response.split("\r\n\r\n". 2) print body # И его отображение HTTP — довольно сложный протокол, и только что показанный простой код спо- собен справиться лишь с самыми примитивными случаями использования. Для работы с HTTP лучше отдать предпочтение предварительно подготовленной би- блиотеке наподобие Net: :НТТР. Следующий код является функциональным экви- валентом предыдущего: require 'net/http' host = 'www.example.com' path = '/index.html' http = Net::HTTP.new(host) headers, body = http.get(path) if headers.code == "200" # Необходимая нам библитека # Веб-сервер # Нужный нам файл # Создание подключения # Запрос файла # Проверка кода состояния # УЧТИТЕ: код дается не в числовом виде! продолжение &
446 Глава 9. Платформа Ruby print body # Вывод тела, если оно получено else # В противном случае puts "#{headers.code} #{headers.message}" # Вывод сообщения об ошибке end Подобные библиотеки существуют для работы с протоколами FTP, SMTP, POP и IMAP Подробные описания этих стандартных библиотек выходят за рамки на- шей книги. И наконец, следует напомнить, что ранее рассмотренная в этой главе библиотека open-uri позволяет решить задачу извлечения веб-страницы еще более простым способом: require 'open-uri' open("http://www.example.com/index.html") {| f | puts f.read } 9.9. Потоки и параллельные вычисления Традиционные программы располагают единственным «потоком выполнения»: составляющие программу операторы или команды выполняются последователь- но, до тех пор пока программа не закончится. Многопоточная программа имеет бо- лее одного потока выполнения. Внутри каждого потока операторы выполняются последовательно, но сами потоки, к примеру, на многоядерных процессорах, могут выполняться параллельно. Зачастую (к примеру, на одноядерных машинах с од- ним центральным процессором) несколько потоков фактически не выполняются параллельно, но параллельное вычисление симулируется за счет чередования вы- полнения потоков. Про такие программы, как обработка изображений, которые требуют больших объемов вычислений, говорят, что на них накладываются ограничения, связанные со скоростью вычислений. Реальный прирост скорости они могут получить только от многопоточной обработки, если имеется несколько центральных процессоров, запущенных в режиме параллельных вычислений. Но большинство программ не испытывают такой же полной зависимости от скорости вычислений. Многие из них, к примеру веб-браузеры, большую часть времени проводят в ожидании сете- вого или файлового ввода-вывода. Про подобные программы говорят, что них на- кладываются ограничения, связанные с процессами ввода-вывода. Такие програм- мы могут быть успешно разбиты на несколько потоков, даже если для их работы доступен лишь один центральный процессор. В одном потоке веб-браузер может формировать изображение, и в то же самое время другой поток может ожидать за- грузки из сети другого изображения. Ruby облегчает создание многопоточных программ за счет использования класса Thread. Чтобы запустить новый поток, нужно просто связать с вызовом Thread.new блок кода. Будет создан новый поток для выполнения кода, находящегося в блоке, а исходный поток будет тут же возвращен из Thread. new и продолжит выполнение со следующей инструкции:
9.9. Потоки и параллельные вычисления 447 # Здесь запускается Поток Nel Thread.new { # Помещаемый здесь код запускается в Потоке № 2 } # Этот код запускаеся в Потоке N»1 Рассмотрение потоков начнем с объяснения некоторых подробностей используе- мой в Ruby модели потоков и API. Во вводных разделах объясняются такие во- просы, как время существования потоков, их диспетчеризация и состояние. После предоставления необходимого вводного материала мы перейдем к представлению примеров кода и рассмотрению более сложных тем, таких как синхронизация по- токов. В заключение стоит заметить, что Ruby-программы могут также выполнять па- раллельные вычисления на уровне процессов операционной системы за счет за- пуска внешних исполняемых составляющих или за счет разветвления новых копий Ruby-интерпретатора. Но подобные действия, конечно же, зависят от при- меняемой операционной системы, их краткое рассмотрение будет представлено в главе 10. Для более подробной информации следует воспользоваться инстру- ментальным средством ri и просмотреть описания методов Kernel .system, Kernel. exec, Kernel .fork, Ю.рореп и модуля Process. ПОТОКИ И ЗАВИСИМОСТЬ ОТ ИСПОЛЬЗУЕМОЙ ПЛАТФОРМЫ Различные операционные системы реализуют работу потоков по-разному. И различные реализации Ruby накладывают Ruby-потоки поверх потоков операционной системы тоже по-разному. К примеру, стандартная реализация Ruby 1.8, выполненная на языке Си, использует только один собственный поток и запускает все Ruby-потоки в рамках этого одного собственного потока. Это означает, что потоки, осуществляемые в Ruby 1.8, весьма не требовательны к ресурсам, но они никогда не запускаются в параллель, даже на многоядерных процессорах. Ruby 1.9 в этом плане отличается: для каждого Ruby-потока он распределяет свой собственный поток операционной системы. Но поскольку некоторые используемые в этой реализации библиотеки языка Си сами по себе не обе- спечивают безопасности работы потоков, Ruby 1.9 проявляет крайнюю степень консервативности и никогда не допускает одновременного запуска более чем одного собственного потока. (Эти ограничения могут быть смягчены в более поздних выпусках версии 1.9, если Си-код удастся сделать более безопасным для работы потоков.) JRuby, реализация Ruby, выполненная на языке Java, отображает каждый Ruby- поток на Java-поток. Но реализация и поведение Java-потоков в свою очередь зависят от реализации виртуальной машины Java. Современные реализации языка Java, как правило, реализуют Java-потоки как собственные потоки операционной системы и позволяют осуществлять параллельную обработку данных на многоядерных процессорах.
448 Глава 9. Платформа Ruby 9.9.1. Время существования потоков Мы уже выяснили, что новые потоки создаются с помощью метода Thread new. Можно также воспользоваться его синонимами Thread.start и Thread.fork. За- пускать потоки после их создания уже не требуется; они приступают к работе автоматически, как только будут готовы ресурсы процессора. Значением вызова Thread , new становится Thread-объект. В классе Thread определяется ряд методов, осуществляющих запросы к выполняемым потокам и позволяющих управлять ими. Поток запускает код в связанном с вызовом метода Thread. new блоке, а затем оста- навливает выполнение. Значение последнего выражения, имеющегося в этом бло- ке, является значением потока, и оно может быть получено путем вызова мето- да value, принадлежащего Thread-объекту. Если поток выполнил код полностью, метод val ue тут же возвращает значение потока. В противном случае выполнение метода value блокируется и значение не возвращается до тех пор, пока не завер- шится работа потока. Метод класса Thread.current возвращает Thread-объект, представляющий те- кущий поток. Это позволяет потокам быть самоуправляемыми. Метод класса Thread.main возвращает Thread-объект, который представляет основной поток — т. е. исходный поток выполнения, который начинает свою работу при запуске Ruby-программы. 9.9.1.1. Основной поток Основной поток играет особую роль; Ruby-интерпретатор прекращает свою рабо- ту, когда основной поток будет выполнен. Это происходит даже в том случае, если основной поток создал другие потоки, которые еще выполняются. Поэтому нуж- но обеспечить, чтобы работа основного потока не заканчивалась, пока работают другие потоки. Один из способов осуществления этой задачи заключается в на- писании основного потока в виде бесконечного цикла. Другой способ заключается в явном ожидании, пока не завершится работа в интересующих нас потоках. Как уже упоминалось, для ожидания завершения потока можно вызвать метод value. Если сами значения потоков не представляют интереса, можно вместо этого ждать завершения работы с помощью метода join. Следующий метод ожидает завершения работы всех потоков, кроме основного и текущего (которые могут быть представлены одним и тем же потоком): # Ожидание завершения работы всех потоков (отличных от текущего и основного). # Предполагается, что во время ожидания новые потоки не создаются, def join_all main = Thread.main # Основной поток current = Thread.current # Текущий поток all = Thread.list # Все работающие потоки # Теперь для каждого потока вызывается метод join all.each {|t| t.join unless t == current or t == main } end
9.9. Потоки и параллельные вычисления 449 9.9.1.2. Потоки и необрабатываемые исключения Если исключение выдано в основном потоке и нигде не обрабатывается, Ruby- интерпретатор выводит сообщение и завершает работу программы. В потоках, от- личных от основного, необрабатываемые исключения приводят к остановке рабо- ты потока. Но при этом по умолчанию интерпретатор не выводит сообщение или не завершает работу программы. Если поток t завершил работу из-за необрабо- танного исключения, а другой поток s вызывает t. joi п или t. val ue, то исключение, выданное в потоке t, выдается в потоке s. Если нужно, чтобы любое исключение в любом потоке приводило к завершению работы программы со стороны интерпретатора, следует воспользоваться методом Thread.abort_on_excepti on=: Thread.abort_on_exception = true Если нужно, чтобы любое исключение в одном отдельно взятом потоке заставля- ло интерпретатор завершать работу программы, следует воспользоваться методом с таким же именем: t = Thread.new { ... } t.abort_on_exception = true 9.9.2. Потоки и переменные Одно из ключевых свойств потоков заключается в способности получения совмест- ного доступа к переменным. Поскольку потоки определены в блоках, они имеют доступ к тем переменным, которые находятся в области видимости блока (к ло- кальным переменным, переменным экземпляра, глобальным переменным и т. д.): х = О tl = Thread.new do # Этот поток может запрашивать и устанавливать значение переменной х end t2 = Thread.new do # Этот поток также может запрашивать и устанавливать значение х # А также он может запрашивать и устанавливать значения переменных tl и t2. end Когда два или более потоков осуществляют параллельное чтение и запись одних и тех же переменных, они должны позаботиться о корректности своих действий. Мы еще вернемся к этому вопросу, когда будем рассматривать синхронизацию ра- боты потоков. 9.9.2.1. Закрытые переменные потоков Переменные, определенные внутри блока, выполняемого потоком, являются за- крытыми переменными этого потока, и они невидимы для любого другого потока.
450 Глава 9. Платформа Ruby Это одно из простых следствий, вытекающих из правил, регулирующих в Ruby область видимости переменных. Зачастую нужно, чтобы у потока была своя собственная закрытая копия перемен- ной, гарантирующая неизменность его поведения при изменении значения ис- ходной переменной. Рассмотрим следующий код, который пытается создать три потока, выводящих (соответственно) числа 1, 2 и 3: п = 1 while п <= 3 Thread.new { puts n } n += 1 end При одних обстоятельствах и вариантах реализации этот код может работать как и ожидалось, и выводить числа 1, 2 и 3. Но при других обстоятельствах или вари- антах реализации этого может и не случиться. Вполне вероятно (если только что созданные потоки не заработают сразу же), что код выведет, к примеру, 4, 4 и 4. Каждый поток читает общую копию переменной п, а значение переменой изменя- ется по мере выполнения цикла. Значение, выводимое потоком, зависит от того, когда этот поток будет запущен относительно родительского потока. Для решения этой проблемы мы передаем текущее значение переменной п методу Thread.new и присваиваем текущее значение переменной параметру блока. По от- ношению к блоку его параметры являются закрытыми (но при этом следует учесть те предостережения, которые были изложены в разделе 5.4.3), и это закрытое зна- чение не подлежит совместному использованию всеми потоками: п = 1 while п <= 3 # Получение закрытой копии х текущего значения п Thread.new(n) {|х| puts х } п += 1 end Следует заметить, что другой способ решения этой проблемы связан с использо- ванием вместо цикла whi 1 е итератора. В этом случае значение п является закрытым по отношению к внешнему блоку и никогда не меняется в процессе выполнения этого блока: l.upto(3) {|n| Thread.new { puts n }} 9.9.2.2. Локальные переменные потоков Некоторые из имеющихся в Ruby специальных глобальных переменных являют- ся локальными по отношению к потоку, в разных потоках они могут иметь разные значения. Примерами могут послужить переменная $SAFE (рассмотренная в раз- деле 10.5) и переменная (представленная в таблице 9.3). Смысл заключается в том, что если два потока параллельно проводят проверку соответствия какому-
9.9. Потоки и параллельные вычисления 451 нибудь регулярному выражению, они будут видеть разные значения переменной и проведение проверки в одном потоке не будет создавать помех результатам проведения такой проверки в другом потоке. Класс Thread предоставляет образ действий, подобный хэшу. В нем определяют- ся методы экземпляров □ и []=, позволяющие связывать произвольные значения с обозначениями. (Если вместо обозначения используется строка, то она будет превращена в обозначение. В отличие от настоящих хэшей, класс Thread позволяет использовать в качестве ключей только обозначения.) Значения, связанные с эти- ми обозначениями, ведут себя как переменные, локальные по отношению к по- току Они не являются закрытыми, как переменные, локальные по отношению к блоку, поскольку любой поток может найти значение в любом другом потоке. Но они также и не относятся к переменным совместного использования, поскольку каждый поток может иметь свою собственную копию. В качестве примера предположим, что мы создали поток, загружающий файлы с веб-сервера. А из основного потока может потребоваться контролировать ход загрузки. Для этого каждый поток может сделать следующее: Thread.currentt:progress] = bytes_received Затем основной поток может определить общий объем загруженных байтов, ис- пользуя следующий код: total = О download_threads.each {|t| total += t[:progress] } Наряду с методами [] и []=, в классе Thread также определяется и метод key?, пред- назначенный для проверки существования для потока заданного ключа. Метод keys возвращает массив обозначений, представляющий определенные для потока ключи. Этот же код может быть переписан следующим образом, чтобы иметь воз- можность работать с потоками, которые еще не были запущены и в которых еще не определен ключ : progress: total = О down!oad_threads.each {|t| total += t[:progress] if t.key?(progress)} 9.9.3. Диспетчеризация потоков Зачастую Ruby-интерпретатор располагает числом запускаемых потоков, превы- шающим возможности процессора по их запуску. Когда недоступна реализация настоящей параллельной обработки, она симулируется путем распределения для потоков рабочего времени процессора. Процесс совместного использования по- токами центрального процессора называется диспетчеризацией потоков. В зависимости от реализации и используемой платформы, распределение потоков может осуществляться Ruby-интерпретатором или может управляться использу- емой операционной системой.
452 Глава 9. Платформа Ruby 9.9.3.1. Приоритеты потоков Первым фактором, влияющим на распределение потоков, является приоритет потока: при распределении отдается предпочтение потокам с более высоким уровнем приоритета. Точнее, поток только тогда получит рабочее время процес- сора, когда нет ожидающих запуска потоков с более высоким, чем у него, уровнем приоритета. Установка и запрос приоритета имеющегося в Ruby Thread-объекта осуществля- ются с помощью методов priority= и priority. Учтите, что способов установки приоритета потока до его запуска не существует. Но первое, что может сделать поток — это повысить или понизить уровень собственного приоритета. Только что созданный поток начинает свою работу с тем же уровнем приоритета, который имелся у создавшего его потока. Основной поток начинает свою работу с уровнем приоритета 0. Как и многие другие аспекты работы потоков, приоритеты зависят от реализации Ruby и типа используемой операционной системы. К примеру, при работе под Юникс непривилегированные потоки не могут повышать или понижать уровень своего приоритета. Поэтому в Ruby 1.9 (который использует собственные пото- ки), работающим под Linux, установки приоритетов игнорируются. 9.9.3.2. Переключения потоков и Thread.pass Когда несколько потоков, обладающих одинаковыми уровнями приоритета, тре- буют разделения рабочего времени процессора, то диспетчеру потоков следует решить, когда и на какое время запустить каждый из потоков. Некоторые дис- петчеры занимаются переключениями, в том смысле, что они позволяют потоку быть запущенным на определенный период времени, после чего будет запущен другой поток с таким же уровнем приоритета. Другие диспетчеры не занимаются переключением по времени: если поток был запущен, то он будет работать до тех пор, пока не перейдет в режим ожидания, не заблокируется при осуществлении операций ввода-вывода или не будет введен в активную фазу работы поток с более высоким уровнем приоритета. Если долговременный поток, требующий интенсивных вычислений (то есть та- кой поток, который никогда не блокируется процессами ввода-вывода), запущен диспетчером, не работающим в режиме переключений по времени, он «посадит на голодный паек» все другие потоки с таким же уровнем приоритета, и у них ни- когда не появится шанс быть запущенными. Чтобы избежать подобной ситуации, долговременные потоки, требующие интенсивных вычислений, должны время от времени вызывать метод Thread.pass, приглашая диспетчер уступить процессор другому потоку. 9.9.4. Состояния потоков Потоки в Ruby могут находиться в одном из пяти возможных состояний. Два наи- более интересных состояния относятся к действующим потокам: они могут быть
9.9. Потоки и параллельные вычисления 453 работающими или ожидающими. К работающим относятся те потоки, которые либо работают в данный момент времени, либо находятся в состоянии готовно- сти и имеют право быть запущенными, как только им будут выделены ресурсы процессора. Ожидающий поток — это тот, который находится в состоянии без- действия (вспомните метод Kernel. sleep), ожидания при осуществлении операций ввода-вывода или остановки по собственному желанию (чуть позже мы рассмо- трим связанный с этим метод Thread, stop). Обычно потоки переходят из работаю- щего в ожидающее состояние, и обратно. Для тех потоков, которые уже завершили свою работу, есть два состояния. Поток может быть завершен нормальным образом либо с выдачей исключения. И наконец, есть одно промежуточное состояние. Оно относится к потоку, который был прерван (чуть позже мы рассмотрим связанный с этим метод Thread. ki 11), но не был завершен, про такие потоки говорят, что они были преждевременно пре- кращены. 9.9.4.1. Запрос состояния потока В классе Thread определяется ряд методов экземпляра, предназначенных для про- верки состояния потока. Метод alive? возвращает t rue, если поток находится в со- стоянии работы или ожидания. Метод stop? возвращает true, если поток находит- ся в каком-нибудь состоянии, отличном от работающего. И наконец, метод status возвращает состояние потока. В следующей таблице показаны пять возвращае- мых значений, соответствующих пяти возможным состояниям. Состояние потока Возвращаемое значение Работающий "run" Ожидающий "sleep" Прерванный "aborting" Нормально завершенный False Завершенный с выдачей исключения Nil Э.9.4.2. Изменение состояния: пауза, возобновление работы и прерывание работы потоков Потоки создаются в работающем состоянии, и они имеют право приступить к ра- боте немедленно. Поток может сам себя поставить на паузу — для входа в состоя- ние ожидания используется метод Thread, stop. Он относится к методам класса, работающим с текущим потоком, — эквивалентного ему метода экземпляра не су- ществует, поэтому один поток не может заставить другой поток стать на паузу. Вы- зов Thread, stop равносилен вызову Kernel. sleep без аргументов: поток становится на вечную паузу (или на паузу до тех пор, пока его работа не будет возобновлена тем способом, который будет рассмотрен чуть позже). Потоки также временно переходят в состояние ожидания, если они вызывают ме- тод Kernel. sleep с аргументами. В таком случае они автоматически возобновляют
454 Глава 9. Платформа Ruby работу и снова входят в работающее состояние после истечения переданного ме- тоду количества секунд (приблизительно). Вызов блокирующих методов ввода- вывода может также перевести поток в состояние ожидания до завершения опе- рации ввода-вывода — фактически это унаследованное время ожидания операций ввода-вывода, благодаря которому организация поточной обработки приобретает смысл даже на однопроцессорных системах. Потоки, самостоятельно ставшие на паузу путем использования метода Thread, stop или метода Kernel.sleep, могут быть запущены снова (даже если время ожи- дания еще не истекло) методами экземпляров wakeup и run. Оба этих метода переключают поток из состояния ожидания в состояние работы. Метод run также возобновляет работу диспетчера потоков. Это может привести к тому, что текущий поток уступит ресурсы процессора и только что возобновив- ший работу поток сможет запуститься немедленно. Метод wakeup возобновляет работу указанного потока без уступки ему ресурсов процессора. Поток может самостоятельно переключиться из работающего состояния в со- стояние завершения работы просто по выходу из блока или по выдаче исключе- ния. Другим путем нормального завершения работы потока служит вызов метода Thread.exit. Следует учесть, что предложение ensure обрабатывается до того, как поток будет завершен этим способом. Поток может принудительно завершить работу другого потока путем вызова ме- тода экземпляра kill для того потока, работу которого нужно прервать. Метод terminate и exit являются синонимами метода kill. Эти методы переводят пре- рываемый метод в состояние нормального завершения. Прерванный поток перед фактическим завершением своей работы отрабатывает все предложения ensure. Метод kill! (и его синонимы terminate! и exit!) прерывают работу потока, но не позволяют выполняться никаким предложениям ensure. Все рассмотренные нами методы завершения работы потока заставляют поток войти в состояние нормально завершенного. Исключение можно выдать внутри другого потока с помощью вызова метода raise. Если поток не может обработать навязанное ему исключение, то он войдет в состояние завершения с выдачей ис- ключения. Предложения ensure обрабатываются так же, как это было бы в обыч- ном режиме распространения исключения. Прерывание работы потока — вещь опасная, если только не будет найден способ узнать, что поток не находится на полпути изменения общего состояния вашей системы. Прерывание потока одним из методов, чье имя заканчивается на !, пред- ставляется еще более опасным делом, поскольку прерванный поток может оста- вить открытыми файлы, сокеты или другие ресурсы. Если поток должен обладать способностью выхода из работы по команде, то лучше заставить его периодически проверять состояние переменной, используемой в качестве флажка, и прерывать его работу безопасным и элегантным образом, если этот флажок установлен, или сразу же, как только он будет установлен.
9.9. Потоки и параллельные вычисления 455 9.9.5. Составление списка потоков и их группы Метод Thread .list возвращает массив Thread-объектов, представляющих все дей- ствующие (работающие или ожидающие) потоки. Когда осуществляется выход из потока, он из этого массива удаляется. Каждый поток, отличный от основного, создается из какого-нибудь другого по- тока. Поэтому потоки могут быть организованы в древовидную структуру, где у каждого потока есть его родительский поток и набор дочерних потоков. Но класс Thread эту информацию не сохраняет: обычно потоки рассматриваются как авто- номные образования, а не в качестве подчиненных того потока, который их создал. Если нужно установить внутри поднабора потоков некий порядок, то можно соз- дать объект ThreadGroup и добавить к нему потоки: group = ThreadGroup.new 3.times {|n| group.add(Thread.new { do_task(n) }} Новые потоки в начале помещаются в группу, к которой принадлежит их роди- тель. Чтобы запросить, к какому ThreadGroup-объекту принадлежит поток, исполь- зуется метод group. А чтобы получить массив потоков, принадлежащих группе, в отношении объекта ThreadGroup используется метод 1 i st. Подобно методу класса Thread .list, метод экземпляра ThreadGroup. 1 i st возвращает только те потоки, рабо- та которых еще не завершилась. Метод 1 i st можно использовать для определения методов, которые работают со всеми потоками группы. Подобные методы могут, к примеру, снижать уровень приоритета всех методов группы. Свойством класса ThreadGroup, превращающим его в более полезное образование по сравнению с простым массивом потоков, является определяемый в нем метод закрытия — enclose. Как только группа потоков будет закрыта, удалить из нее по- токи или добавить новые уже будет невозможно. Потоки, входящие в группу, могут создавать новые потоки, и эти новые потоки также будут принадлежать этой груп- пе. Закрытый объект ThreadGroup понадобится, если нужно запустить код Ruby, не внушающий доверия, с установкой значения переменной $SAFE (рассматриваемой в разделе 10.5), и нужно отслеживать любые потоки, порождаемые этим кодом. 9.9.6. Примеры организации потоков Теперь, после объяснения имеющихся в Ruby моделей потоков и соответствующе- го API, мы рассмотрим ряд реальных примеров многопотокового кода. 9.9.6.1. Параллельное чтение файлов Наиболее часто потоки Ruby используются в программах, на которые наклады- ваются ограничения, связанные с процессами ввода-вывода. Они позволяют про- граммам поддерживать занятость системы даже при ожидании ввода данных от пользователя, ожидания получения их из файловой системы или по сети. К при- меру, в представленном далее коде определяется метод conread (для параллельного
456 Глава 9. Платформа Ruby чтения), который воспринимает массив, состоящий из имен файлов, и возвраща- ет хэш, отображающий эти имена на содержимое представленных ими файлов. В примере используются потоки для параллельного чтения этих файлов, а в дей- ствительности он предназначен для использования с модулем open-uri, позволяю- щим открывать с помощью метода Kernel .open HTTP и FTP URL-адреса и читать по этим адресам данные, как из обычных файлов: # Параллельное чтение файлов. Для извлечения URL используется модуль "open-uri" # Передается массив имен файлов. Возвращается хэш, отображающий имена файлов на их # содержимое def conread(filenames) h = {} # Пустой хэш результатов # Создание по одному потоку для каждого файла filenames.each do |fi1 ename| # Для каждого указанного файла h[f11 ename] = Thread.new do # Создается поток, соответствующий # имени файла open(filename) {|f| f.read } # Открытие и чтение файла end # Значение потока - это содержимое файла end # Перебор элементов хэша, ожидание завершения работы каждого потока. # Замена потока в хэше его значением (содержимым файла) h.each_pair do |filename, thread| begin h[f11 ename] = thread.value # Отображение имени файла на его # содержимое rescue h[filename] = $! # Или выдача исключения end end end 9.9.Б.2. Многопоточный сервер Другой почти что канонический случай использования потоков относится к напи- санию серверов, способных поддерживать одновременное подключение несколь- ких клиентов. Мы уже видели, как это делается путем мультиплексирования с ис- пользованием метода Kernel. select, но несколько более простое (хотя, возможно, и менее масштабируемое) решение связано с использованием потоков: require 'socket' # Этот метод предполагает наличие сокета, подключенного к клиенту. # Он читает строки, полученные от клиента, реверсирует их и отправляет назад. # Этот метод одновременно могут запускать несколько потоков, def hand!e cl ient(c) while true input = c.gets.chop # Чтение строки ввода от клиента
9.9. Потоки и параллельные вычисления 457 break if !input break if input=="quit" c.puts(j nput.reverse) c.flush end c.close end server = TCPServer.open(2000) while true client = server.accept Thread.start(client) do |c| handle_client(c) end end # Выход, если ввод отсутствует # или если клиент сам запросил выход # В противном случае ответ клиенту # Форсирование отправки ответа # Закрытие сокета клиента # Ожидание подключения к порту 2000 # Бесконечный цикл сервера # Ожидание подключения клиента # Запуск нового потока # И обработка клиента в этом потоке 9.9.6.3. Параллельные итераторы Хотя задачи, на которые накладываются ограничения процессов ввода-вывода, являются типичным случаем использования потоков Ruby, их использование только лишь этим не ограничивается. Представленный далее код добавляет метод conmap (для параллельного отображения) к модулю Enumerable. Он работает напо- добие метода тар но обрабатывает каждый элемент массива ввода, используя от- дельный поток: module Enumerable # Открытие модуля Enumerable def conmap(&block) # Определение нового метода, предполагающего # использование блока threads = [] # Начало работы с пустым массивом потоков self.each do |item| # Для каждой перечисляемой записи # вызов блока в новом потоке и запоминание потока threads « Thread.new { block.call (item) } end # Теперь отображение массива потоков на их значения threads.map {|t| t.value } # И возвращение массива значений end end А вот похожая параллельная версия итератора each: module Enumerable def concurrently map {|item| Thread.new { yield item }}.each {|t| t.join } end end Код довольно лаконичен и сложен для понимания: способность разобраться в его работе будет свидетельствовать о хороших познаниях в синтаксисе Ruby и его итераторах.
458 Глава 9. Платформа Ruby Следует напомнить, что в Ruby 1.9 стандартные итераторы, которым не передан блок, возвращают объект-нумератор. Значит, если воспользоваться определенным ранее методом concurrently и Hash-объектом h, можно создать следующий код: h.each_paiг.concurrently {|*pair| process(pair)} 9.9.7. Исключение потоков и взаимная блокировка Если два потока имеют общий доступ к одним и тем же данным и по крайней мере один из потоков вносит в эти данные изменения, то нужно позаботиться о том, чтобы ни один из потоков не мог даже увидеть данные в неопределенном состоя- нии. Это называется исключением потоков. Объяснить необходимость их исклю- чения можно на двух примерах. Предположим сначала, что два потока обрабатывают файлы и каждый поток уве- личивает значение совместно используемой переменной, чтобы отследить общее количество обработанных файлов. Проблема в том, что это увеличение значения переменной не является атомарной операцией. Это означает, что все происходит не за один шаг: чтобы увеличить значение, Ruby-программа должна прочитать это значение, прибавить к нему 1, а затем сохранить это новое значение в той же самой переменной. Предположим, что счетчик имел значение 100, и представим последующее чередование выполнения двух потоков. Первый поток читает значе- ние 100, но перед тем как он сможет прибавить 1, диспетчер останавливает работу первого потока и дает возможность запуститься второму потоку. Теперь второй поток читает значение 100, добавляет к нему 1 и сохраняет в переменной счетчика значение 101. После чего этот же второй поток приступает к чтению нового файла, что приводит к его блокировке и позволяет первому потоку возобновить работу. Теперь первый поток прибавляет 1 к 100 и сохраняет результат. Оба потока увели- чили значение счетчика, но его значение равно 101 вместо 102. Другой классический пример необходимости исключения потока связан с при- ложениями, осуществляющими электронные банковские операции. Предпо- ложим, один поток обрабатывает перевод денег со сберегательного счета на те- кущий, а другой поток генерирует месячные отчеты, отправляемые клиентам. Без организации правильного исключения генерирующий отчеты поток может прочитать данные с текущего счета клиента после того, как средства уже будут списаны со сберегательного счета, но перед тем, как они будут зачислены на те- кущий счет. Подобные проблемы решаются за счет использования скоординированного ме- ханизма блокировки. Каждый поток, которому требуется доступ к совместно используемым данным, должен сначала заблокировать эти данные. Блокировка представлена Mutex-объектом (сокращение от «mutual exclusion», что означает «взаимное исключение»). Чтобы заблокировать мьютекс, нужно вызвать принад- лежащий ему метод lock.
9.9. Потоки и параллельные вычисления 459 После завершения чтения или изменения общих данных для мьютекса вызывает- ся метод unlock. Метод 1 ock блокируется, когда вызывается в отношении мьютекса, который уже был заблокирован, и он не возвращает управление до тех пор, пока код, который его вызвал, не получит состоявшуюся блокировку. Если каждый по- ток, имеющий доступ к общим данным, правильно осуществляет блокировку и разблокировку мьютекса, то ни один из потоков не увидит данные в неопределен- ном состоянии и вышеописанных проблем не возникнет. В Ruby 1.9 Mutex является основным классом, а в Ruby 1.8 он является частью стандартной библиотеки thread. Вместо открытого использования методов lock и unlock, зачастую используется метод synchronize вместе со связанным с ним бло- ком. Метод synchronize блокирует мьютекс, запускает имеющийся в блоке код, а затем разблокирует мьютекс в предложении ensure, обеспечивая тем самым пра- вильную обработку исключений. Вот как выглядит простая модель примера ве- дения банковского счета, в которой для синхронизации доступа потоков к общим данным счета используется Mutex-объект: require 'thread' # Для класса Mutex в Ruby 1.8 # BankAccount имеет имя, текущий счет и сберегательный счет. class BankAccount def init(name. checking, savings) @name.@checking.@savings = name,checking.savings @lock = Mutex.new # Для безопасности работы потока end # Блокировка счета и перевод денег со сберегательного счета на текущий def transfer_from_savings(x) @1ock.synchronize { ©savings -= x ^checking += x } end # Блокировка счета и составление отчета о текущих показателях баланса def report @1ock.synchronize { "#@пап1е\пТекущий: #@сЬеск1пд\пСберегательный: #@savings" } end end 9.9.7.1. Взаимная блокировка Приступая к использованию Mutex-объектов для исключения потоков, следует проявлять осторожность, чтобы избежать взаимной блокировки. Под этим поня- тием подразумевается условие, возникающее, когда все потоки ждут получения
460 Глава 9. Платформа Ruby ресурса, удерживаемого другим потоком. Поскольку все потоки заблокированы, они не могут освободить удерживаемые ими блокировки. А поскольку они не мо- гут освободить эти блокировки, ни один другой поток не может захватить эти бло- кировки. В классическом сценарии взаимной блокировки участвуют два потока и два Mutex- объекта. Поток 1 блокирует мьютекс 1, а затем пытается заблокировать мьютекс 2. Тем временем поток 2 блокирует мьютекс 2, а затем пытается заблокировать мью- текс 1. Ни один из потоков не может захватить нужную ему блокировку, и ни один из потоков не может освободить блокировку, в которой нуждается другой поток, поэтому оба потока блокируются навечно: # Классическая взаимная блокировка: два потока и две блокировки require 'thread' m.n = Mutex.new, Mutex.new t = Thread.new { m. lock puts "Поток t заблокировал мьютекс m" sleep 1 puts "Поток t ждет возможности заблокировать мьютекс п" n.lock } s = Thread.new { n.lock puts "Поток s заблокировал мьютекс n" sleep 1 puts "Поток s ждет возможности заблокировать мьютекс m" m.lock } t.join s.join Способом избежать подобной взаимной блокировки является постоянная блоки- ровка ресурсов в одной и той же последовательности. Если второй поток блокирует m до блокировки п, то взаимной блокировки не произойдет. Следует заметить, что взаимная блокировка может возникнуть и без использования Mutex-объектов. Вызов метода join для потоков, которые вы- зывают метод Thread. stop, приведет к взаимной блокировке обоих потоков, пока какой-нибудь третий поток не сможет возобновить работу остановленного по- тока. Следует помнить, что некоторые реализации Ruby способны обнаруживать про- стые взаимные блокировки, наподобие только что рассмотренной, и прерывать работу программы с выдачей ошибки, но гарантии подобного развития событий не дается.
9.9. Потоки и параллельные вычисления 461 9.9.8. Очередь и очередь определенного размера В стандартной библиотеке thread специально для параллельного программирова- ния определяются структуры данных для организации очередей — Queue и оче- редей определенного размера — SizedQueue. Они реализуют FIFO-очереди1, обе- спечивающие безопасную работу потоков, и предназначены для осуществления модели программирования производитель-потребитель. Согласно этой модели, один поток производит какого-то рода значение и помещает его в очередь с помо- щью метода enq («enqueue» — «ставить в очередь») или его синонима push. Другой поток по мере необходимости «потребляет» это значение, извлекая его из очереди с помощью метода deq («dequeue» — «удаление из очереди»). (Методы pop и shift являются синонимами метода deq.) Ключевым свойством очереди — Queue, определяющим ее пригодность к использо- ванию в параллельном программировании, является способность метода deq бло- кироваться при пустой очереди и ожидать до тех пор, пока поток-производитель не добавит к очереди какое-нибудь значение. В классах Queue и SizedQueue реали- зован некий основной API, но в варианте SizedQueue фигурирует максимальный размер. Если очередь уже достигла своего максимального размера, то метод, до- бавляющий к ней значения, блокируется до тех пор, пока потребитель не удалит значение из очереди. Как и при работе с другими имеющимися в Ruby классами коллекций, количе- ство элементов в очереди можно определить с помощью методов size или length, а определить, пуста очередь или нет, можно с помощью метода empty?. При вызове метода SizedQueue.new следует указать максимальный размер очереди SizedQueue. После создания SizedQueue с помощью методов max и тах= можно запро- сить или изменить максимальный размер. В этой главе уже было показано, как добавлять к модулю Enumerabl е параллельный метод отображения. Теперь мы определим метод, который объединит параллель- ный метод отображения — тар с параллельным методом инъекции — Inject. Он создает поток для каждого элемента перечисляемой коллекции и применяет этот поток для использования отображаемого Proc-объекта. Значение, возвращенное этим Ргос-объектом, ставится в очередь в Queue-объекте. Один заключительный поток действует в качестве потребителя; он удаляет значения из очереди и пере- дает их по мере доступности принимающему инъекции Ргос-объекту. Назовем метод, который можно использовать для параллельного вычисления суммы квадратов значений массива, методом параллельной инъекции (concurrent injection) — conject. Следует заметить, что последовательный алгоритм для при- веденного далее простого примера сложения сумм квадратов практически всегда работал бы быстрее: а = [-2.-1.0.1.2] mapper = lambda {|х| х*х } # Вычисление квадратов Injector = lambda {(total,х| total+x } # Вычисление суммы a.conject(0, mapper, Injector) # => 10 1 FIFO — принцип «First In — First Out», то есть «Первым вошел — первым вышел». — При- меч. перев.
462 Глава 9. Платформа Ruby Для метода con ject используется следующий код, в котором следует обратить вни- мание на Queue-объект и его методы enq и deq: module Enumerable # Параллельная инъекция: предполагается использование начального значения # и двух Procs-объектов def conjectdnltlal, mapper, injector) # Использование Queue для передачи значения от отображаемых потоков # к инъекционному потоку q = Queue.new count = О each do |1tem| Thread.new do q.enq(mapper[1tem]) end count += 1 end t = Th read.new do x = Initial while(count > 0) x = 1njector[x.q.deq] count -= 1 end x end t.value end end # Сколько элементов? # Для каждого элемента # Создается новый поток # Отображение и постановка в очередь # отображенного значения # Подсчет элементов # Создание потока-инъектора # Использование указанного начального # значения # Один проход для каждого элемента # Извлечение значения и его инъекция # Обратный отсчет # Значение потока - это значения инъекций # Ожидание завершения работы потока- # инъектора и возвращение его значения 9.9.9. Переменные условий и очереди В отношении класса Queue есть одно весьма существенное замечание: метод deq может блокироваться. Обычно представляется, что блокировки случаются толь- ко с методами ввода-вывода (или при вызове в отношении потока метода join, или при вызове метода lock в отношении мьютекса). Но в многопотоковом про- граммировании порой возникает необходимость заставить поток ожидать насту- пления каких-нибудь условий (не имеющих отношения к управлению этим по- током). В случае с классом Queue условие состоит в непустом состоянии очереди: если очередь пуста, то поток-потребитель должен ждать до тех пор, пока поток- производитель не вызовет метод enq и не сделает очередь непустой. Проще всего заставить поток пребывать в ожидании, пока какой-нибудь дру- гой поток не сообщит ему, что можно продолжить работу, за счет использования
9.9. Потоки и параллельные вычисления 463 класса переменных условий — Conditionvariable. Как и класс Queue, класс Conditionvariable является частью стандартной библиотеки thread. Объект Conditionvariable создается с помощью метода Conditionvariable.new. Чтобы заста- вить поток ждать возникновения условия, используется метод wait. Чтобы воз- обновить работу одного ожидающего потока, используется метод signal. Чтобы возобновить работу всех ожидающих потоков, используется метод broadcast. При использовании переменных условий есть одна небольшая загвоздка: чтобы заста- вить все работать должным образом, ожидающий поток должен передать методу wa i t заблокированный Mutex-объект. Этот мьютекс будет временно разблокирован на время ожидания потока и опять заблокирован при возобновлении работы по- тока. Мы завершаем рассмотрение потоков вспомогательным классом, который порой может пригодиться в многопотоковых программах. Он называется Exchanger и по- зволяет двум потокам обмениваться произвольными значениями. Предположим, что есть потоки tl и t2 и Exchanger-объект е. Поток tl вызывает е.exchanged). За- тем этот метод блокируется (разумеется, с использованием Conditionvariable) до тех пор, пока t2 не вызовет е.exchanged). Этот второй поток не блокируется, он просто возвращает 1 — значение, переданное потоком tl. Теперь, когда этот вто- рой поток вызвал exchange, поток tl возобновляет работу и возвращает из вызова метода exchange значение 2. Показанная здесь реализация Exchanger представляется несколько усложненной, но она демонстрирует типичное применение класса Conditionvariable. Одной из интересных особенностей этого кода является использование двух Mutex-объектов. Один из них используется для синхронизированного доступа к методу exchange и передается методу wait, принадлежащему переменной условия. Другой мьютекс используется для определения, является ли вызывающий поток первым или вто- рым потоком, вызывающим метод exchange. Вместо использования с этим мью- тексом метода lock этот класс использует неблокирующий метод try lock. Если @first.try_lock возвращает true, то вызывающим является первый поток. В про- тивном случае им является второй поток: require 'thread' class Exchanger def initialize # Эти переменные будут содержать два обмениваемых значения @first_value = @second_value = nil # Этот мьютекс защищает доступ к методу exchange @lock = Mutex.new # Этот мьютекс позволяет определить, какой из потоков вызывает # exchange, первый или второй @first = Mutex.new # Этот объект Conditionvariable позволяет первому потоку ждать # поступления от второго потока @second = Conditionvariable.new end , продолжение тУ
464 Глава 9. Платформа Ruby # Обмен этого значения на значение, переданного другим потоком def exchangee value) @1ock.synchronize do # В конкретный момент времени этот метод может # быть вызван только одним потоком if @first.try_lock # Это первый поток @first_value = value # Сохранение аргумента первого потока # Теперь ожидание поступления от второго потока. # При этом на время ожидания мьютекс разблокируется, # чтобы второй поток тоже мог вызвать этот метод @second.wait(@lock) @first.unlock @second_value Else @second_value = value ^second.signal @first_value end end end end # Ожидание поступления от второго потока # Подготовка к следующему обмену # Возвращение значения второго потока # В противном случае - это второй поток # Сохранение второго значения # Сообщение первому потоку, что мы уже здесь # Возвращение значения первого потока
Глава 10 Среда окружения Ruby
466 Глава 10. Среда окружения Ruby В этой главе собраны все темы, связанные с программированием на Ruby, которые не были рассмотрены ранее. Большинство обсуждаемых здесь вопросов касается взаимодействия Ruby и операционной системы, под которой он запущен. По сути, некоторые рассматриваемые особенности зависят от применяемой операционной системы. Точно так же многие из рассматриваемых вопросов могут зависеть от реализации: не все Ruby-интерпретаторы реализуют их одинаково. В главе будут рассмотрены следующие темы: О аргументы командной строки Ruby-интерпретатора и переменные окруже- ния; О высокоуровневая среда выполнения: глобальные функции, переменные и кон- станты; О сокращения, используемые для сценариев текстовой обработки: глобальные функции, переменные и ключи интерпретатора, позаимствованные, как пра- вило, из языка программирования Perl, которые дают возможность создавать короткие, но мощные Ruby-программы для обработки текстовых файлов; О команды операционной системы: запуск команд оболочки и вызов исполняемых программ в используемой операционной системе. Свойства, позволяющие ис- пользовать Ruby в качестве сценарного или «связующего» языка; О безопасность: как уменьшить риск SQL-инъекций и похожих на них атак с помощью имеющегося в Ruby механизма пометок и как поместить не вызы- вающий доверия код Ruby в изолированную программную среду («sandbox») с помощью уровней выполнения SSAFE. 10.1. Вызов Ruby-интерпретатора Стандартная реализация Ruby, осуществленная на языке Си, вызывается из ко- мандной строки следующим образом: ruby [ключи] [--] программа [аргументы] Ключи представляют собой параметры (от нуля и более) командной строки, вли- яющие на работу интерпретатора. Вскоре мы рассмотрим, какие параметры могут быть применены в командной строке. Программа — это имя файла, в котором содержится запускаемая Ruby-программа. Если имя программы начинается с дефиса, то его нужно предварить символа- ми --, чтобы заставить интерпретатор рассматривать его в качестве имени про- граммы, а не в качестве ключа. Если в качестве имени программы используется одиночный дефис или программа и аргументы полностью опущены, интерпрета- тор будет считывать текст программы из стандартного ввода. И наконец, аргументы — это любое количество дополнительных лексем, присут- ствующих в командной строке. Эти лексемы становятся элементами массива ARGV. В следующих подразделах рассматриваются ключи, поддерживаемые стандартной реализацией, осуществленной на языке Си. Следует заметить, что для включения
10.1. Вызов Ruby-интерпретатора 467 любого из ключей: -W, -w, -v, -d, -I, -г и -К, можно воспользоваться переменной окружения RUBYOPT. Эти ключи будут автоматически задействованы при каждом вызове интерпретатора, как будто они были указаны в командной строке. 10.1.1. Наиболее востребованные ключи Следующие ключи можно отнести к наиболее востребованным. Предполагает- ся, что большинство Ruby-реализаций поддерживают эти ключи или предлагают свои, работающие подобным же образом: -w Этот ключ разрешает выдачу предупреждений о не рекомендуемом, или про- блемном коде и устанавливает значение SVERBOSЕ в true. Многие программисты, пишущие на Ruby, регулярно пользуются этим ключом, чтобы убедиться в чи- стоте своего кода. -е сценарий Этот ключ приводит к запуску Ruby-кода, находящегося в сценарии. Если ука- зано более одного ключа -е, то связанные с ними сценарии рассматриваются как отдельные строки кода. Также если указан один или более ключей, интерпрета- тор не загружает или не запускает программу, указанную в командной строке. Для обеспечения работы коротких, однострочных сценариев Ruby-код, указан- ный с ключом -е, может использовать рассматриваемые далее в этой главе со- кращения, применяемые для проверки соответствия регулярным выражениям. -I путь Этот ключ приводит к добавлению каталогов, указанных в пуши, в начало гло- бального массива $LOAD_PATH. Указанные каталоги включаются в путь поиска, осуществляемого методами load и require (но их включение не влияет на за- грузку программ, указанных в командной строке). В командной строке могут присутствовать несколько ключей -I, и в каждом из них может быть указан один или нескольких каталогов. Если под одним клю- чом -1 указаны несколько каталогов, то в Unix или Unix-подобной системе они должны быть отделены друг от друга символом :, а в Windows — символом ;. -г библиотека Этот ключ приводит к загрузке библиотеки перед запуском указанной про- граммы. Он работает аналогично первой строке программы: require 'библиотека' Пробел между ключом -г и именем библиотеки ставить необязательно, поэто- му он часто опускается. -rubygems Этот часто используемый параметр командной строки, по сути, ключом не яв- ляется, а просто представляет собой остроумное использование ключа -г. Он приводит к загрузке модуля по имени ubygems (без буквы г) из стандартной би- блиотеки. Удобство обеспечивается тем, что модуль ubygems просто загружает
468 Глава 10. Среда окружения Ruby настоящий модуль rubygems. Ruby 1.9 может загружать установленные gem- пакеты и без этого модуля, поэтому этот ключ востребован только в Ruby 1.8. --disable-gems Этот ключ, работающий в Ruby 1.9, предотвращает добавление каталогов, в ко- торые устанавливаются gem-пакеты, к используемому по умолчанию пути за- грузки. Если установлено большое количество gem-пакетов и запускается про- грамма, их не использующая (или программа, которая напрямую управляет своей зависимостью от пакетов с помощью метода дет), то, применяя этот ключ, можно сократить время запуска программы. -d, --debug Этот ключ приводит к установке значений глобальных переменных SDEBUG и JVERBOSE в true. При установке значений этих переменных программа или би- блиотечный код, используемый этой программой, могут выводить отладочную информацию или предпринимать какие-нибудь другие действия. -h Этот ключ приводит к отображению перечня ключей интерпретатора с после- дующим выходом. 10.1.2. Ключи, связанные с предупреждениями и выдачей информации Следующие ключи управляют типом или объемом информации, отображаемой Ruby-интерпретатором: - W, -W2, --verbose Все эти ключи являются синонимами ключа -w: они разрешают выдачу подроб- ных предупреждений и устанавливают значение JVERBOSE в true. - W0 Этот ключ подавляет выдачу всех предупреждений. - V Этот ключ приводит в выдаче номера версии Ruby. Если программа не указана, его использование приводит к выходу, а не к считыванию программы из стан- дартного ввода. Если программа указана, то она запускается в режиме, соответ- ствующем применению ключа - -verbose (или -w). --version, --copyright, --help Эти ключи приводят к выводу номера версии Ruby, информации об авторских правах или вспомогательной информации, касающейся командной строки с последующим выходом. Ключ - help является синонимом -h. Ключ - -version отличается от - v тем, что никогда не приводит к запуску указанной программы. 10.1.3. Ключи, относящиеся к кодировке Следующие ключи применяются для указания используемой по умолчанию внешней кодировки Ruby-процесса и используемой по умолчанию кодировки
10.1. Вызов Ruby-интерпретатора 469 исходных файлов, у которых в соответствующем комментарии она не указана. Если ни один из этих ключей не указан, то используемая по умолчанию внеш- няя кодировка берется на основе локальной кодировки, а для исходного файла используется кодировка ASCII (вопросы исходной кодировки и используемой по умолчанию внешней кодировки рассматриваются в разделе 2.4): -К код В Ruby 1.8 с помощью этого ключа указывается исходная кодировка сценария и устанавливается значение глобальной переменной SKCODE. В Ruby 1.9 уста- навливается используемая по умолчанию внешняя кодировка Ruby-процесса и устанавливается используемая по умолчанию исходная кодировка. В качестве значения кода используются а, А, п или N для ASCII; и или U для Юникода; е или Е для EUC-JP и s или S для SJIS. (EUC-JP и SJIS являются распространенными японскими кодировками.) -Е кодировка. --encodiпд=кодправка Эти ключи подобны ключу -К, но позволяют указывать кодировку не однобук- венной аббревиатурой, а по имени. 10.1.4. Ключи, связанные с обработкой текста Эти ключи изменяют характер работы Ruby с текстом или применяются при на- писании однострочных сценариев, запускаемых с помощью ключа -е: -О ххх В этом ключе используется цифра 0, а не буква О. Значение ххх должно быть в диапазоне от нуля и до трех восьмеричных цифр. При указании этих цифр они являются ASCII-кодом символа-разделителя входящей записи и установ- кой для значения переменной $/. Этим ключом определяется «строка» для gets и подобных ему методов. Сам по себе ключ -0 устанавливает значение $/ на код символа 0. Ключ -00 играет особую роль; он переводит Ruby в «режим абзаца», в котором строки разделяются двумя смежными символами новой строки. -а Этот ключ автоматически разбивает каждую строку ввода на поля и сохраняет поля в $F. Ключ работает только с ключами цикла -п или -р и добавляет код $F = $spl it в начало каждой итерации. Также следует обратить внимание на ключ -F. -F разделитель_поля Этот ключ устанавливает входной разделитель поля $: на разделитель_поля. Это оказывает влияние на метод split, когда он вызывается без аргументов. Обра- тите внимание также на ключ -а. Разделитель поля может быть отдельным символом или произвольным регуляр- ным выражением без ограничительных слэшей. В зависимости от исполь- зуемой оболочки, может возникнуть потребность заключения в кавычки или удваивания обратных слэшей в любом регулярном выражении, указанном в командной строке.
470 Глава 10. Среда окружения Ruby -i [расширение] Этот ключ приводит к непосредственному редактированию файлов, указанных в командной строке. Строки считываются из файлов, указанных в командной строке, а выходные данные возвращаются назад, в те же самые файлы. Если указано расширение, делается резервная копия файлов с добавлением к их име- нам этого расширения. Этот ключ делает выходной разделитель записи $\ таким же, как и входной раз- делитель записи $/ (см. ключ -0), с тем, чтобы при использовании метода print, символ конца строки автоматически добавлялся к текстовому выводу. Ключ предназначен для использования с ключом -р или с ключом -п. При использо- вании с одним из этих ключей происходит автоматический вызов метода chop для удаления входного разделителя записи из каждой строки ввода. Этот ключ приводит к запуску программы в таком режиме, будто она заключе- на в следующий цикл: while gets $F = split if $-a chop! if $-1 # Считывание строки ввода в $_ # Разбиение $_ на поля, если использован # ключ -а # Удаление конца строки из # если использован ключ -1 # Сюда помещается текст программ end Этот ключ работает в Ruby 1.9, даже при том, что глобальные функции chop! и spl it в этой версии языка уже недоступны. Этот ключ часто используется с ключом -е. Обратите внимание также на ключ -р. Этот ключ приводит к запуску программы в таком режиме, будто она вписана в следующий цикл: while gets $F = split if $-a chop! if $-1 # Считывание строки ввода в $_ # Разбиение $_ на поля, если использован ключ -а # Удаление конца строки из # если использован ключ -1 # Сюда помещается текст программ print # Вывод $_ (с добавлением $/, если использован ключ -1) end Этот ключ работает в Ruby 1.9, даже при том, что глобальные функции chop! и spl i t в этой версии языка уже недоступны. Этот ключ часто используется с ключом -е. Обратите внимание и на ключ -п. 10.1.5. Ключи разного назначения Следующие ключи не подпадают ни под одну из предыдущих категорий.
10.2. Высокоуровневое окружение 471 -с Этот ключ приводит к синтаксическому разбору программы и выводу отчета о синтаксических ошибках, но самого запуска программы не происходит. -С каталог, -X каталог Этот ключ перед запуском программы изменяет текущий каталог на каталог. -s Если задействован этот ключ, интерпретатор проводит предварительную обра- ботку любых аргументов, появляющихся после имени программы и начинаю- щихся с дефиса. Для аргументов, преподнесенных в форме -х=у, он устанавли- вает значение $х в у. Для аргументов, преподнесенных в форме -х, он устанав- ливает значение $х в true. Предобработанные аргументы удаляются из массива ARGV. -S Этот ключ приводит к поиску указанного файла программы относительно пути, заданного в переменной окружения RUBY_PATH. Если файл там найден не будет, поиск осуществляется относительно пути, указанного в переменной окруже- ния PATH. И если он все равно не будет найден, то поиск продолжится обычным путем. -Тл Этот ключ приводит к установке значения SSAFE в п или в 1, если п опущен. Более подробно этот вопрос рассмотрен в разделе 10.5. -х [катдлог] Этот ключ извлекает исходный код Ruby из файла программы путем отбрасыва- ния любых строк перед первой строкой, начинающейся с #! ruby. Для совмести- мости с ключом -X, использующем заглавную букву, этот ключ также позволяет указывать каталог. 10.2. Высокоуровневое окружение При запуске Ruby-интерпретатора определяется и становится доступным для ис- пользования в программах ряд классов, модулей, констант, а также глобальных переменных и глобальных функций. Эти предопределенные средства будут рас- смотрены в следующих далее подразделах. 10.2.1. Предопределенные модули и классы При запуске интерпретатора Ruby 1.8 определяются следующие модули: Comparable FileTest Marshal Enumerable GC Math Errno Kernel Objectspace Precision Process Signal А также следующие классы:
472 Глава 10. Среда окружения Ruby Array File Method String Bignum Fixnum Module Struct Binding Float Ni1 Cl ass Symbol Class Hash Numeric Thread Continuation ID Object ThreadGroup Data Integer Proc Time Dir MatchData Range TrueClass FalseClass MatchingData Regexp UnboundMethod Кроме этого определяются следующие классы исключений: ArgumentError EOFError Exception FloatDomainError IDError IndexError Interrupt LoadError LocalJumpError Ruby 1.9 добавляет к этим спискам следующие модули, классы и исключения: NameError NoMemoryError NoMethodError NotlmplementedError RangeError RegexpError RuntimeError ScriptError SecurityError Signal Exception StandardError SyntaxError SystemCa11 Error SystemExit SystemStackError ThreadError TypeError ZeroDivisionError BasicObject Fiber FiberError Mutex VM KeyError Stopiteration Проверить наличие в вашей реализации предопределенных модулей, классов и исключений можно с помощью следующего кода: # Вывод имен всех модулей (без классов) puts Module.constants.sort.select {|x| eval(x.to_s).instance_of? Module} # Вывод имен всех классов (без исключений) puts Module.constants.sort.select {|x| c = eval(x.to_s) c.1s_a? Class and not c.ancestors.include? Exception # Вывод имен всех исключений puts Module.constants.sort.seiect {|x| c = eval(x.to_s) c.1nstance_of? Class and c.ancestors.include? Exception 10.2.2. Высокоуровневые константы При запуске Ruby-интерпретатора определяются следующие высокоуровневые константы (в дополнение к ранее перечисленным модулям и классам). Из мо- дуля, в котором определена константа с таким же именем, можно получить до- ступ к этим высокоуровневым константам за счет явного указания префикса : .
10.2. Высокоуровневое окружение 473 Получить перечень высокоуровневых констант в вашей реализации можно с по- мощью следующего кода: ruby -е 'puts Module.constants.sort.reject}|x| eval(x.to_s).is_a? Module}’ ARGF Объект ввода-вывода (10), предоставляющий доступ к виртуальной конкатена- ции файлов, названных в ARGV, или к стандартному вводу, если массив ARGV пуст. Является синонимом для $<. ARGV Массив, содержащий параметры, указанные в командной строке. Является си- нонимом для $*. DATA Если файл программы включает в отдельной строке лексему___END__, то проис- ходит определение этой константы, чтобы она стала потоком, предоставляю- щим доступ к строкам файла, размещенным за____END_. Если файл программы не включает в себя__END_, эта константа не определяется. ENV Объект, который ведет себя как хэш и предоставляет доступ к установкам пере- менных окружения, влияющих на работу интерпретатора. FALSE Не рекомендуемый синоним для false. NIL Не рекомендуемый синоним для nil. RUBY_PATCHLEVEL Строка, показывающая степень внесенных исправлений (patchlevel) для ин- терпретатора. RUBY_PLATFDRM Строка, показывающая платформу Ruby-интерпретатора. RUBY_RELEASE_DATE Строка, показывающая дату выпуска Ruby-интерпретатора. RUBY_VERSIDN Строка, показывающая версию языка Ruby, поддерживаемую интерпретатором. STDERR Стандартный поток выходной информации об ошибках. Это исходное значе- ние переменной $stderr. STDIN Стандартный входной поток. Это исходное значение переменной $stdi п. STDDUT Стандартный выходной поток. Это исходное значение переменной $ stdout. TDPLEVEL_BINDING Binding-объект, представляющий связывания в высокоуровневом окружении. TRUE Не рекомендуемый синоним для true.
474 Глава 10. Среда окружения Ruby 10.2.3. Глобальные переменные Ruby-интерпретатор предопределяет ряд глобальных переменных, которые могут быть использованы в вашей программе. Многие из них являются в той или иной степени специализированными. Некоторые используют в именах знаки пунктуа- ции. (В модуле Engl ish. rb для знаков пунктуации определяются англоязычные альтернативы. Если нужно воспользоваться более информативно наполненны- ми альтернативными вариантами, следует включить в программу строку requi re ’English'.) Некоторые переменные служат только для чтения, и им нельзя при- сваивать значения. А некоторые переменные являются локальными по отноше- нию к потоку выполнения, поэтому каждый поток Ruby-программы может видеть разное значение переменной. И наконец, некоторые глобальные переменные ($_, $- и происходящие от них пе- ременные сравнения с шаблоном) являются локальными по отношению к методу: хотя переменная является глобально доступной, ее значение локально по отно- шению к текущему методу. Если метод устанавливает значение для одной из та- ких магических глобальных переменных, он не изменяет значение, которое может увидеть код, вызвавший этот метод. Получить полный перечень глобальных переменных, предопределенных вашим Ruby-интерпретатором, можно с помощью следующей команды: ruby -е 'puts global_variables.sort' Чтобы включить в перечень информативно наполненные имена из модуля Engl 1 sh, следует воспользоваться следующей командой: ruby -rEnglish -е 'puts global_variables.sort' В следующих далее подразделах дается описание предопределенных глобальных переменных по категориям. 10.2.3.1. Глобальные переменные, используемые при вводе Эти глобальные переменные содержат конфигурационные установки и определя- ют информационное наполнение окружения для запуска Ruby-программы, в ко- торое входят аргументы командной строки. $* Предназначенный только для чтения синоним константы ARGV. Английский си- ноним: SARGV. $$ Идентификатор (ID) текущего Ruby-процесса. Только для чтения. Английские синонимы: $PID, $PROCESS_ID. $? Состояние выхода из последнего завершенного процесса. Переменная локаль- на по отношению к вычислительному потоку и предназначена только для чте- ния. Английский синоним: $CHILD_STATUS.
10.2. Высокоуровневое окружение 475 SDEBUG, $-d Устанавливается в true, если в командной строке были установлены ключи -d или - -debug. SKCDDE, $-К В Ruby 1.8 эта переменная содержит строку, в которой указывается текущая кодировка текста. Она может принимать значения «NONE», «UTF8», «SJIS» или «EUC». Значение может быть установлено с помощью ключа интерпрета- тора -К. В Ruby 1.9 эта переменная больше не работает, при ее использовании появляется предупреждение. $LDADED_FEATURES, $" Строковый массив, указывающий на те файлы, которые были загружены. Толь- ко для чтения. $LDAD_PATH. $-1 Строковый массив, содержащий каталоги, подвергающиеся поиску при за- грузке файлов с помощью методов load и require. Эта переменная только для чтения, но содержимое массива, на который она ссылается, можно изменить, добавляя, к примеру, каталоги в конец или в начало пути. $PRDGRAM_NAME, $0 Имя файла, в котором содержится текущая выполняемая Ruby-программа. Пе- ременная будет иметь значение «-», если программа считывается из стандарт- ного ввода, или «-е», если программа была определена с помощью ключа -е. Учтите, что эта переменная отличается от переменной $FILENAME. SSAFE Текущий уровень безопасности для выполнения программы. Подробности из- ложены в разделе 10.5. Значение этой переменной может быть установлено из командной строки с помощью ключа -Т. Значение этой переменной является локальным по отношению к вычислительному потоку. SVERBDSE. $-v, $-w Приобретает значение true, если в командной строке установлен ключ -v, -w или --verbose. Значение устанавливается в ni 1, если был установлен ключ -W0. В противном случае имеет значение fal se. Для подавления всех предупреждений можно установить значение этой переменной в ni 1. 10.2.3.2. Глобальные переменные, используемые при обработке исключений Две следующие глобальные переменные применяются в выражениях rescue при выдаче исключения. $! Последний выданный объект исключения. Получить доступ к объекту исклю- чения можно также при помощи синтаксиса => при объявлении выражения rescue. Значение этой переменной является локальным по отношению к потоку выполнения. Английский синоним: $ERRDR_INFD.
476 Глава 10. Среда окружения Ruby Трассировка стека последнего исключения, эквивалент для $! .backtrace. Зна- чение этой переменной является локальным по отношению к потоку выполне- ния. Английский синоним: $ERROR_POSITION. 10.2.3.3. Глобальные переменные, используемые при обработке потоков данных и текста Следующие глобальные переменные представляют стандартные потоки ввода-вы- вода, а также являются переменными, влияющими на исходное поведение мето- дов обработки текста, определенных в классе Kernel. Примеры их использования показаны в разделе 10.3: $_ Последняя строка, считанная определенными в классе Kernel методами gets и readl 1 пе. Значение этой переменной локально по отношению к потоку вы- полнения и методу С переменной «скрытно работают многие Kernel -методы. Английский синоним: $LAST_READ_LINE. $< Синоним потока данных ARGF, предназначенный только для чтения: 10-подобный объект, представляющий доступ к виртуальной конкатенации файлов, указан- ных в командной строке, или к стандартному входу, если файлы указаны не были. Чтение из этого потока осуществляется методами, среди которых опре- деленный в классе Kernel метод gets. Нужно учесть, что этот поток данных не всегда совпадает с «stdin. Английский синоним: $DEFAULT_INPUT. «stdin Стандартный входной поток. Исходным значением этой переменной является константа STDIN. Многие Ruby-программы осуществляют чтение не из Sstdin, а из ARGF или $<. «stdout, $> Стандартный выходной поток, и информационный адресат методов распечат- ки, определенных в классе Kernel: puts, pri nt, pri ntf и т. д. Английский синоним: $DEFAULT_OUTPUT. «stderr Выходной поток для стандартной ошибки. Исходным значением этой перемен- ной является константа STDERR. «FILENAME Имя файла, который в данный момент считывается из ARGF. Является эквива- лентом ARGF. fl 1 ename. Только для чтения. $. Номер последней строки, считанной из текущего файла ввода. Является экви- валентом ARGF. 1 Ineno. Английские синонимы: SNR, $INPUT_LINE_NUMBER. $/. $-0 Разделитель входной записи (по умолчанию — символ новой строки). Методы gets и readl 1 пе по умолчанию используют это значение для определения границ
10.2. Высокоуровневое окружение 477 строки. Значение можно установить с помощью ключа интерпретатора - 0. Ан- глийские синонимы: $RS, $INPUT_RECORD_SEPARATOR. $\ Разделитель выходной записи. По умолчанию используется значение nil, но оно устанавливается в $/, когда для интерпретатора используется ключ -1. Если значение не равно nil, разделитель выходной записи выводится после каждого вызова метода print (но не puts или других методов вывода). Английские сино- нимы: $ORS, $OUTPUT_RECORD_SEPARATOR. $. Выходной разделитель между аргументами print и разделитель по умолчанию для метода Array .join. Исходное значение — nil. Английские синонимы: SOFS, $OUTPUT_FIELD_SEPARATOR. $-F Используемый методом split по умолчанию разделитель полей. Исходное зна- чение — ni 1, но значение можно указать, если воспользоваться ключом интер- претатора -F. Английские синонимы: $FS, $FIELD_SEPARATOR. $F Эта переменная определяется в том случае, если Ruby-интерпретатор вызывает- ся с ключом -а совместно с ключом -п или ключом -р. Она хранит поля текущей входной строки в таком виде, как будто они были возвращены методом split. 10.2.3.4. Глобальные переменные, используемые при проверке соответствия шаблону Следующие глобальные переменные являются локальными по отношению к по- токам выполнения и методам, а их значения устанавливаются любой операцией проверки соответствия шаблону регулярного выражения: $~ MatchData-объект, созданный последней операцией проверки соответствия ша- блону. Значение этой переменной является локальным по отношению к потоку выполнения и методу. Другие рассматриваемые здесь глобальные переменные, используемые при проверке соответствия шаблону, являются производными от этой переменной. Установка значения этой переменной на новый MatchData- объект изменяет значение других переменных. Английский синоним: $MATCH_INFO. $& Самый последний соответствовавший шаблону текст. Является эквивалентом $~[0]. Только для чтения, значение этой переменной локально по отношению к потоку выполнения и к методу и является производной от переменной Английский синоним: $МАТСН. $' Строка, предшествующая той, которая была последней соответствующей ша- блону строкой. Является эквивалентом . prejnatch. Только для чтения, ее зна- чение локально по отношению к потоку выполнения и к методу и является про- изводной от переменной Английский синоним: SPREMATCH.
478 Глава 10. Среда окружения Ruby $' Строка, следующая за той, которая была последней соответствующей шаблону строкой. Является эквивалентом $~.post_match. Только для чтения, ее значение локально по отношению к потоку выполнения и к методу и является произво- дной от переменной J-. Английский синоним: SPOSTMATCH. $+ Строка, соответствующая последней группе, успешно прошедшей последнюю проверку на соответствие шаблону. Только для чтения, ее значение локально по отношению к потоку выполнения и к методу и является производной от пере- менной Английский синоним: $LAST_PAREN_MATCH. 10.2.3.5. Глобальные переменные, используемые при работе с ключами командной строки Ruby определяет ряд глобальных переменных, соответствующих состоянию или значению ключей командной строки интерпретатора. Переменные $-0, $-F, $-1, $ - К, $-d, $-v и $-w имеют синонимы и включены в предыдущие разделы. $-а Имеет значение true, если был указан ключ интерпретатора -а; в противном случае имеет значение false. Только для чтения. $-1 Имеет значение nil, если не был указан ключ интерпретатора -1. В противном случае значение устанавливается на расширение файла резервного копирова- ния, указанное с помощью ключа -1. $-1 Имеет значение true, если был указан ключ -1. Только для чтения. $-р Имеет значение true, если был указан ключ интерпретатора -р; в противном случае имеет значение fal se. Только для чтения. $-W В Ruby 1.9 эта глобальная переменная указывает на текущий уровень подроб- ности. Его значение равно 0, если был использован ключ -W0, и 2, если был ис- пользован любой из ключей: -w, -v или - -verbose. В противном случае перемен- ная имеет значение 1. Только для чтения. 10.2.4. Предопределенные глобальные функции В модуле Kernel, который включает в себя класс Object, определяется ряд закры- тых методов экземпляра, которые служат в качестве глобальных функций. В си- лу своей закрытости они могут вызываться как функции, без явного указания объекта-получателя. А поскольку они включены в класс Object, их можно вызвать отовсюду — не важно, какое значение имеет sei f, оно будет представлять объект, и эти методы могут быть вызваны для него в неявном виде. Функции, определяе- мые в модуле Kernel, могут быть сгруппированы в несколько категорий, большин- ство из которых рассматриваются в этой и других главах книги.
10.2. Высокоуровневое окружение 479 10.2.4.1. Функции ключевых слов Следующие функции модуля Kernel ведут себя как ключевые слова, а их описание дано в разных главах этой книги: block_g1ven? Iterator? loop require callcc lambda proc throw catch load raise 10.2.4.2. Функции ввода, вывода и обработки текста В модуле Kernel определяются следующие функции, большинство из которых яв- ляются глобальными вариантами методов ввода-вывода (10). Более подробно они рассмотрены в разделе 10.3: Format print puts sprintf gets pri ntf readline p putc readlines В Ruby 1.8 (но не 1.9) в модуле Kernel также определяются следующие глобальные варианты методов обработки строки, которые в неявном виде работают с пере- менной chomp chop gsub scan sub chomp! chop! gsub! split sub! 10.2.4.3. Методы работы с операционной системой Следующие функции, определяемые в методе Kernel, позволяют Ruby-программе взаимодействовать с операционной системой. Они зависят от используемой плат- формы и рассматриваются в разделе 10.4. Следует учесть, что ' — это метод, имею- щий особое имя обратного апострофа и возвращающий текстовый вывод произ- вольной команды, используемой в оболочке операционной системы: fork select system trap exec open syscall test 10.2.4.4. Предупреждения, отказы и выходы Следующие функции, определяемые в методе Kernel, отображают предупрежде- ния, выдают исключения, вызывают выход из программы или регистрируют бло- ки кода, запускаемые при завершении работы программы. Они рассматриваются вместе с методами работы с операционной системой в разделе 10.4: Abort at_ex1t exit exit! Fall warn 10.2.4.5. Функции отражения Следующие функции модуля Kernel являются частью использующегося в Ruby API отражения, рассмотренного в главе 8:
480 Глава 10. Среда окружения Ruby binding caller eval global_variables local_variables methodjnissing removeji nstance_vari abl e set_trace_func si ngleton_method_added s1ngleton_method_removed singletonjnethodjjndefi ned trace_var untrace_var 10.2.4.6. Функции преобразования Следующие функции, определяемые в модуле Kernel, пытаются преобразовать свои аргументы в новый тип данных. Они рассматриваются в разделе 3.8.7.3: Array Float Integer String 10.2.4.7. Функции модуля Kernel различного назначения Следующие смешанные функции, определяемые в модуле Kernel, не подпадают ни под одну из предыдущих категорий: autoload rand srand autoload? sleep Методы rand и srand предназначены для генерации случайных чисел и рассма- триваются в разделе 9.3.7. Методы autoload и autoload? рассматриваются в разде- ле 7.6.3. А метод sleep рассматривается в разделах 9.9 и 10.4.4. 10.2.5. Глобальные функции, определенные пользователями Когда метод определен внутри объявления класса или модуля с помощью клю- чевого слова def и для него не указан метод-получатель, то этот метод создается как открытый метод экземпляра для sei f, где sei f — это определяемый вами класс или модуль. Использование ключевого слова def на верхнем уровне, за пределами любого класса или модуля, имеет два существенных отличия. Во-первых, высоко- уровневые методы являются методами экземпляра класса Object (даже если self не ссылается на Object). Во-вторых, высокоуровневые методы всегда закрыты. ВЫСОКОУРОВНЕВЫЙ SELF: MAIN-ОБЪЕКТ Поскольку высокоуровневые методы становятся методами экземпляра класса Object, можно предположить, что значение self будет ссылаться на Object. Но на самом деле высокоуровневые методы — случай исключительный: методы определяются в Object, но self ссылается на другой объект. Этот особый высо- коуровневый объект известен как объект «main», и больше о нем, собственно, и нечего сказать. Классом main-объекта служит Object, и у него есть синглтон- метод to_s, возвращающий строку «main».
10.3. Сокращения для удобства извлечения данных и составления отчетов 481 То обстоятельство, что высокоуровневые методы определяются в классе Object, означает, что они наследуются всеми объектами (включая Module и Class) и (если не будут переопределены) могут использоваться внутри любого определения ме- тода класса или метода экземпляра. (Чтобы в этом убедиться, можно просмотреть алгоритм разрешения имен методов, описанный в разделе 7.8.) Тот факт, что вы- сокоуровневые методы имеют статус закрытых, означает, что они могут быть вы- званы подобно функциям, без явного указания объекта-получателя. Таким образом, Ruby подражает процедурному принципу программирования в рамках своей среды, имеющей строгую объектно-ориентированную природу. 10.3. Сокращения для удобства извлечения данных и составления отчетов Ruby создавался под влиянием языка сценариев Perl, название которого пред- ставляет собой акроним для Practical Extraction and Reporting Language (язык, удобный для извлечения данных и составления отчетов). Поэтому Ruby вклю- чает ряд глобальных функций, облегчающих написание программ, извлекающих информацию из файлов и генерирующих отчеты. Согласно принципу объектно- ориентированного программирования, функции ввода-вывода являются метода- ми класса 10, а функции работы со строками — методами класса St г 1 пд. Но из прак- тических соображений полезно иметь глобальные функции, осуществляющие чтение и запись при работе с предопределенными входными и выходными пото- ками данных. Вдобавок к предоставлению этих глобальных функций Ruby идет дальше Perl и определяет для функций особое поведение: многие из них работают неявным образом со специальной, локальной по отношению к методу перемен- ной В этой переменной сохраняется последняя строка, считанная с входного потока данных. Запомнить ее помогает знак подчеркивания: он похож на строку. (Многие глобальные переменные Ruby, использующие знаки пунктуации, были унаследованы из Perl.) Вдобавок к глобальным функциям ввода и вывода есть не- сколько глобальных функций обработки строк, похожих на методы класса String, но работающих неявным образом с переменной Эти глобальные функции и переменные задуманы как сокращения для исполь- зования в коротких и простых Ruby-сценариях. Как правило, их использование в больших программах считается дурным тоном. 10.3.1. Функции ввода Глобальные функции gets, readline и readlInes в точности похожи на 10-методы с такими же именами (которые рассматриваются в разделе 9.7.3.1), но они рабо- тают неявным образом с потоком данных $< (который также доступен в виде кон- станты, известной как ARGF). Как и методы класса 10, эти глобальные функции не- явно устанавливают значение переменной
482 Глава 10. Среда окружения Ruby Поток $< ведет себя как 10-объект, но на самом деле таковым не является. (Опре- деляемый в нем метод class возвращает ссылку на Object, а его метод to_s воз- вращает «ARGF».) Дать точное определение поведению этого потока довольно сложно. Если массив ARGV пуст, то $< представляет собой то же самое, что и STDIN: стандартный поток ввода данных. Если ARGV не пуст, то Ruby предполагает, что он представляет собой список имен файлов. В этом случае $< ведет себя так, будто чтение происходит из конкатенации каждого из этих файлов. Но это не точная картина поведения $< Когда происходит первый запрос на чтение, направленный к $<, Ruby использует метод ARGV.shift для удаления из ARGV первого имени фай- ла. Он открывает этот файл и считывает его содержимое. Когда будет достигнут конец этого файла, Ruby повторяет процесс, убирая из ARGV следующее имя файла и открывая этот файл. Поток $< не сообщает о конце файла до тех пор, пока в ARGV не останется ни одного имени файла. А это означает, что Ruby-сценарии могут внести в ARGV изменения (к примеру, что- бы обработать ключи командной строки) перед началом чтения из потока данных $< Ваш сценарий после запуска может также добавить к ARGV файлы и $< будет эти файлы использовать. 10.3.2. Нерекомендуемые функции извлечения данных В Ruby 1.8 и более ранних версиях глобальные функции chomp, chomp!, chop, chop!, gsub, gsub!, scan, split, sub и sub! работают так же, как и одноименные методы, определяемые в классе String, но при этом неявно имеют дело с Более того, chomp, chop, gsub и sub присваивают свои результаты обратно переменной а это означает, что они, по сути, являются синонимами своих версий, помеченных вос- клицательными знаками. Из Ruby 1.9 эти глобальные функции были удалены, поэтому использовать их в новом коде не следует. 10.3.3. Функции составления отчетов В модуле Kernel определяется ряд глобальных функций для отсылки выходных данных на $ stdout. (Изначально эта глобальная переменная ссылается на стан- дартный выходной поток, STDOUT, используемый Ruby-процессом, но ее значение можно изменить, повлияв на поведение рассматриваемых здесь функций.) Функции puts, print, pri ntf и putc являются эквивалентами одноименных методов класса STDOUT (которые рассматриваются в разделе 9.7.4). Вспомним, что puts до- бавляет символ новой строки к своим выходным данным, если его там еще нет. С другой стороны, функция print не осуществляет автоматического добавления символа новой строки, но добавляет выходной разделитель записи $\, если зна- чение этой глобальной переменной было установлено. Глобальная функция р от- носится к тем, что не имеют аналогов в классе 10. Она предназначена для отладки,
10.4. Обращение к операционной системе 483 а ее короткое имя существенно облегчает набор. Она вызывает метод inspect от- носительно каждого из своих аргументов и передает получающиеся в результате его работы строки, методу puts. Вспомним, что метод inspect по умолчанию явля- ется эквивалентом метода to_s, но некоторые классы переопределяют его, чтобы предоставить более подходящую для разработчика выходную информацию, удоб- ную для отладки. Если затребовать библиотеку рр, то вместо функции р можно будет воспользоваться функцией рр «привлекательной распечатки» (pretty print) выходной отладочной информации. (Эта функция будет полезна при распечатке больших массивов и хэшей.) Ранее упоминавшийся метод printf предполагает использование в качестве свое- го первого аргумента форматируемую строку и подставляет значения всех своих остальных аргументов в эту строку перед тем, как вывести результат. Форматиро- вание внутри строки можно также применить и без отправки результата на $ stdout, если воспользоваться глобальной функцией spri ntf или ее синонимом format. Они работают так же, как и оператор %, определяемый в классе St г i ng. 10.3.4. Сокращения, используемые в однострочных сценариях В этой главе уже рассматривался ключ интерпретатора -е, предназначенный для запуска однострочных Ruby-сценариев (этот ключ часто используется в связке с ключами цикла - п и -р). Существует одно особое сокращение, унаследованное от языка Perl, которое применяется только в том случае, если сценарий указан с ключом -е. Если сценарий указан с ключом -е и в выражении условия появляется отдельно взятый литерал регулярного выражения (в виде составной части инструкции или модификатора if, unless, while или until), то это регулярное выражение неявным образом сравнивается со значением переменной К примеру, если нужно рас- печатать все строки файла, начинающиеся на букву А, то можно воспользоваться следующей строкой: ruby -n -е 'print if /АА/' файл_данных Если сохранить этот сценарий в файле и запустить его без ключа -е, он по-преж- нему сработает, но будет выдано предупреждение (даже без установленного клю- ча -w). Чтобы избежать выдачи предупреждения, нужно вместо того, что исполь- зовалось ранее, применить явное сравнение: print if $_ =- /ЖА/ 10.4. Обращение к операционной системе В Ruby поддерживается ряд глобальных функций для взаимодействия с опе- рационной системой с целью запуска программ, разветвления новых процессов,
484 Глава 10. Среда окружения Ruby обработки сигналов и т. д. Изначально Ruby разрабатывался для Юникс-подобных операционных систем, и многие из этих функций, зависящих от применяемой ОС, несут на себе печать этого наследия. В силу такой своей природы эти функции пе- реносятся намного хуже, чем многие другие, а некоторые из них вообще не могут быть реализованы в Windows или на другой, не имеющей отношения к Юниксу платформе. В следующих далее подразделах описаны некоторые из наиболее ча- сто используемых функций, зависящих от применяемой операционной системы. Здесь не рассматриваются такие функции, как syscall, которые являются сугубо низкоуровневыми и зависящими от применяемой платформы. 10.4.1. Вызов команд операционной системы Метод Kernel.' предполагает использование единственного строкового аргу- мента, представляющего команду оболочки операционной системы. Он запускает подоболочку и передает ей указанный текст. Возвращаемое значение является текстом, распечатываемым на стандартном устройстве вывода. Обычно этот метод вызывается с использованием специального синтаксиса; он вызывается из строко- вых литералов, окруженных обратными кавычками, или из строковых литералов, ограниченных с помощью символьной последовательности Ях (рассматриваемой в разделе 3.2.1.6). Например: os = 'uname' # Строковый литерал, он же вызов метода os = $x{uname} # Другой вариант синтаксиса цитирования os = Kernel("uname") # Явный вызов метода Этот метод не просто вызывает указанную исполняемую команду; он вызывает оболочку, а это означает возможность применения свойств этой оболочки, вроде групповых символов для имен файлов: files = 'echo *.xml' Другой способ запуска процесса и чтения его выходных данных связан с приме- нением функции Kernel open. Этот метод является вариантом метода File.open и чаще всего используется для открытия файлов. (А если воспользоваться requi re 'open-uri' и востребовать этот модуль из состава стандартной библиотеки, наш метод также может быть использован для открытия URL-адресов HTTP и FTP.) Но если первым символом указанного «имени файла» будет символ конвейера |, то вместо этого будет открыт конвейер для чтения из и (или) записи в указанную команду оболочки: pipe = open("|echo *.xml") files = pipe.readline pipe.close Если нужно вызвать команду в оболочке операционной системы, не интересуясь при этом выводимой ею информацией, то вместо этого следует воспользоваться методом Kernel. system. При передаче одиночной строки этот метод выполняет эту строку в оболочке, ждет завершения выполнения команды и возвращает true при
10.4. Обращение к операционной системе 485 успешном выполнении и fal se при отказе. Если методу system передать несколько аргументов, то первым аргументом должно быть имя программы, а все остальные должны быть аргументами командной строки. В таком случае никакие расшире- ния оболочки с этими аргументами не выполняются. Низкоуровневый способ вызова произвольных выполняемых программ связан с использованием функции ехес. Из этой функции нет возврата: она просто под- меняет текущий запущенный Ruby-процесс на выполнение указанной програм- мы. Она может пригодиться при написании Ruby-сценариев, являющихся про- стым обрамлением для запуска каких-нибудь других программ. Но чаще всего эта функция используется в сочетании с функцией fork, которая рассматривается в следующем разделе. 10.4.2. Процессы и ветвления В разделе 9.9 рассматривается Ruby API для создания многопотоковых программ. Другим подходом к достижению в Ruby параллельности вычислений является за- действование нескольких Ruby-процессов. Он реализуется с помощью функции fork или ее синонима Process. fork. Проще всего использовать эту функцию с бло- ком: fork { puts "Привет от дочернего процесса: #$$" } puts "Привет от родительского процесса: #$$" При использовании данного способа исходный Ruby-процесс продолжается на основе кода, имеющегося после блока, а новый Ruby-процесс выполняет код, име- ющийся в блоке. При вызове без блока fork ведет себя по-другому. В рамках родительского процес- са вызов fork возвращает целое число, являющееся идентификатором (ID) вновь созданного дочернего процесса. В дочернем процессе такой же вызов fork возвращает nil. Таким образом предыду- щий код может быть записан в следующем виде: pid = fork if (pid) puts "Привет от родительского процесса: #$$" puts "Создан дочерний процесс #{pid}" else puts Привет из дочернего процесса: #$$" end Одно весьма существенное различие между процессами и потоками состоит в том, что процессы не используют общего пространства памяти. Когда вызывается функция fork, новый Ruby-процесс запускается как точный дубликат родитель- ского процесса. Но любые изменения, внесенные им в состояние процесса (путем изменения или создания объектов), производятся в его собственном адресном
486 Глава 10. Среда окружения Ruby пространстве. Дочерний процесс не может изменить структуры данных родитель- ского процесса, точно так же как и родительский процесс не может вмешаться в структуры, видимые дочерним процессом. Если нужно получить средство связи между родительским и дочерним процесса- ми, следует воспользоваться методом open и передать ему «| -» в качестве первого аргумента. Тем самым будет открыт канал ко вновь созданному с помощью вет- вления Ruby-процессу. Вызов метода open передает управление связанному с ним блоку, как в родительском, так и в дочернем процессе. В дочернем процессе блок получает ni 1. А в родительском процессе блоку передается 10-объект. Чтение из этого 10-объекта возвращает данные, записанные дочерним процессом. А данные, записанные в 10-объект, становятся доступными для чтения из стан- дартного ввода дочернего процесса. Например: open(”|-”, "r+") do |child| if child # Это родительский процесс chi Id.puts("Привет дочернему процессу") # Отправка дочернему процессу response = chi Id.gets # Чтение из дочернего процесса puts "Дочерний процесс выдал: #{response}" else # Это дочерний процесс from_parent = gets # Чтение из родительского процесса STDERR.puts "Родительский процесс выдал: #{from_parent}" puts("Привет родителю!") # Отправка родительскоему процессу end end С функцией fork или методом open может найти полезное применение функция Kernel .exec. Ранее было показано, что для отправки оболочке операционной си- стемы произвольных команд, можно воспользоваться функциями ' и system. Но оба этих метода выполняются согласованно, они не возвращают управление, пока не завершится выполнение команды. Если нужно выполнить команду операци- онной системы в виде отдельного процесса, то сначала следует воспользоваться функцией fork, чтобы создать дочерний процесс, а затем в этом дочернем процессе вызвать функцию ехес, чтобы запустить команду. Вызов функции ехес никогда не приводит к возврату управления; он заменяет текущий процесс новым процес- сом. Для вызова ехес используются такие же аргументы, как и для вызова system. Если используется всего один аргумент, то он считается командой оболочки. Если используется несколько аргументов, то первый из них указывает на вызываемую исполняемую команду, а все остальные аргументы становятся для исполняемой команды значениями аргументов — «ARGV»: ореп("|-", "г") do |child| if child # Это родительский процесс files = child.readlines # Чтение выходных данных нашего # дочернего процесса
10.4. Обращение к операционной системе 487 child.close else # Это дочерний процесс exec("/bin/ls". "-1") # Чтение другой исполняемой команды end end Работа с использованием процессов является задачей низкоуровневого програм- мирования, и ее подробности выходят за рамки тематики данной книги. Если вам хочется получить дополнительные сведения, начните с использования инстру- ментального средства ri, выдающего сведения о других методах, определяемых в модуле Process. 10.4.3. Отлавливание сигналов Многие операционные системы допускают отправку запущенным процессам асин- хронных сигналов. Такое, к примеру, случается при нажатии пользователем со- четания клавиш Ctrl+C для прерывания работы программы. Многие программы оболочки отправляют сигнал по имени «SIGINT» (для прерывания работы) в ответ на использование сочетания Ctrl+C. А ответ по умолчанию на этот сигнал обычно заключается в прерывании работы программы. Ruby позволяет программам «от- лавливать» сигналы и определять для них свои собственные обработчики. Это де- лается с помощью метода Kernel .trap (или его синонима Signal .trap). К примеру, если нужно запретить пользователю использование сочетания Ctrl+C для преры- вания программы: trap "SIGINT" { puts "SIGINT игнорируется" } Вместо передачи методу trap блока, можно передать ему Ргос-объект, что будет иметь равносильное значение. Если нужно просто молча проигнорировать сигнал, то методу в качестве второго аргумента можно передать строку «IGNORE». Чтобы восстановить для сигнала поведение, предусмотренное операционной системой по умолчанию нужно в качестве второго аргумента передать строку «DEFAULT». Для таких продолжительно работающих программ, как серверные, может приго- диться определение обработчиков сигналов, заставляющих серверные программы перечитывать свои конфигурационные файлы, выкладывать статистику своего использования в файлы регистрационных журналов или, к примеру, входить в ре- жим отладки. В Юникс-подобных операционных системах для этих целей чаще всего используются сигналы SIGUSR1 и SIGUSR2. 10.4.4. Прерывание работы программ Для прерывания работы программы или осуществления действий, имеющих к нему отношение, существует ряд методов, связанных с модулем Kernel. Наиболее
488 Глава 10. Среда окружения Ruby простой в этом ряду является функция exit. Она выдает исключение SystemExit, которое, не будучи перехваченным, приводит к выходу из программы. Но перед выходом тем не менее запускаются END-блоки и любые обработчики закрытия, за- регистрированные с помощью метода Kernel. at_exi t. Чтобы осуществить немедленный выход, следует вместо этого метода восполь- зоваться методом exit!. Оба метода воспринимают целочисленный аргумент, определяющий код выхода из программы, который сообщается операционной си- стеме. В модуле Kernel для этих двух функций есть их синонимы — Process.exit и Process.exit!. Функция abort выводит определенное сообщение об ошибке на стандартный вы- ходной поток, а затем вызывает exi t(1). Метод fail является простым синонимом для метода raise, который предназначен для тех случаев, когда исключение выдается с намерением прервать работу про- граммы. Как и abort, метод fai 1 приводит к выдаче сообщения, отображаемого при выходе из программы. Например: fail "Неизвестный ключ #{switch}" Функция warn является родственной для abort и fail: она выводит предупре- дительное сообщение в стандартную ошибку (если только предупреждения не были явным образом отключены с помощью ключа -W0). Но при этом следует отметить, что эта функция не выдает исключение и не вызывает выход из про- граммы. Функция sleep является еще одной родственной функцией, которая не вызыва- ет выхода из программы. Вместо этого она просто заставляет программу (или по крайней мере текущий поток выполнения программы) сделать паузу на указанное количество секунд. 10.5. Безопасность Имеющаяся в Ruby система безопасности предоставляет механизм написания программ, работающих с сомнительными данными и ненадежным кодом. Система безопасности состоит из двух частей. Первая часть представляет собой механизм, позволяющий отличить безопасные данные от сомнительных, или помеченных данных. Вторая часть представляет собой технологию ограниченного выполнения, которая позволяет «ограничить функциональность» среды окружения Ruby и пре- дохранить Ruby-интерпретатор от выполнения потенциально опасных операций над помеченными данными. Это способствует предохранению от таких явлений, как инъекционные SQL-атаки, при которых злонамеренно составленные входные данные изменяют поведение программы. Ограниченное выполнение может пойти еще дальше, чтобы сомнительный (и, возможно, зловредный) код мог быть вы- полнен без опасений, что его работа приведет к удалению файлов, краже данных или причинению другого вреда.
10.5. Безопасность 489 10.5.1. Помеченные данные Каждый объект, имеющийся в Ruby, может быть либо помеченным, либо непо- меченным. Буквальные значения, находящиеся в исходном коде, являются непо- меченными. Значения, получаемые из внешней среды, являются помеченными. Сюда включаются строки, считанные из командной строки (ARGV) или переменные окружения (ENV), а также любые данные, считанные из файлов, сокетов или дру- гих информационных потоков. Переменная среды окружения PATH имеет особый статус: она помечается лишь в том случае, если один или более указанных в ней каталогов является общедоступным для записи. Важно отметить, что помечен- ность сродни инфекции, поэтому объекты, извлеченные из помеченных объектов, также являются помеченными. В классе Object определяются методы taint, tainted? и untaint, позволяющие ста- вить пометку на непомеченный объект, проверять помеченность объекта и сни- мать пометку с ранее помеченного объекта. Снимать пометку с помеченного объ- екта следует только в том случае, если ваш код его проверил и определил, что он безопасен, несмотря на опасность его происхождения или источника получения. 10.5.2. Ограниченное выполнение и уровни безопасности Ruby может выполнять программы с включенной проверкой безопасности. Уро- вень проверки безопасности определяется значением глобальной переменной JSAFE. Обычно по умолчанию уровень безопасности установлен в 0, но он равен 1 для тех Ruby-программ, которые запускают setuid или setgid. (Эти термины ОС Юникс относятся к программам, запущенным с привилегиями, непонятными для тех пользователей, которые их запускают.) Допустимые уровни безопасности вы- ражаются целыми числами 0,1,2,3 и 4. Для Ruby-интерпретатора уровень безопас- ности может быть установлен в явном виде с помощью ключа командной строки -Т. Уровень безопасности можно также установить за счет присваивания соответству- ющего значения переменной JSAFE. Тем не менее следует заметить, что это значение можно только увеличивать, а способов его уменьшения не существует: $SAFE=1 # Повышение уровня безопасности $SAFE=4 # Дальнейшее повышение уровня безопасности $SAFE=O # SecurityError! Этого делать нельзя Значение переменной SSAFE является локальным по отношению к потоку выпол- нения. Иными словами, значение SSAFE в потоке может изменяться, не оказывая никакого влияния на значение этой переменной для других потоков. Используя это свойство, потоки, при выполнении сомнительной программы, могут быть по- мещены в изолированную ограниченную среду: Thread.start { # Создание потока для изоляции программы SSAFE =4 # Ограниченное выполнение действительно только для этого потока # Здесь должен запускаться сомнительный код }
490 Глава 10. Среда окружения Ruby Рассмотрение вопроса имеющихся в Ruby уровней безопасности должно вестись со ссылкой на реализацию языка. Некоторые реализации могут иметь свои осо- бенности. В частности, в JRuby (на время написания данной книги) предприняты весьма скромные попытки имитировать режимы ограниченного выполнения, су- ществующие в эталонной реализации. Кроме того, следует учесть, что существующая в Ruby модель безопасности не получила такого же тщательного и длительного исследования, которому подвер- глась архитектура безопасности, имеющаяся в языке Java. В следующих далее подразделах объясняется, как в Ruby предполагается работа режима ограничен- ного выполнения, но обнаруженные на данный момент ошибки могут позволить эти ограничения обойти. По умолчанию устанавливается уровень безопасности 0. При этом данные на на- личие пометок не проверяются. 10.5.2.2. Уровень безопасности 1 На этом уровне запрещаются все потенциально опасные операции с использова- нием помеченных данных. Невозможно вычислить строку кода, если эта строка имеет пометку, невозможно затребовать (с помощью requi re) библиотеку, если она имеет пометку, невозможно открыть указанный файл, если его имя имеет пометку, и невозможно подключиться к сетевому хост-узлу, если имя хоста помечено. На- верное, этот уровень безопасности должен устанавливаться для программ, в осо- бенности для программ, имеющих отношение к сетевым серверам, которые вос- принимают произвольную входную информацию. Это поможет отловить ошибки программирования, связанные с использованием помеченных данных небезопас- ным образом. Если вы пишете библиотеку, выполняющую потенциально опасные операции, — такие как связь с сервером базы данных, — нужно проверять значение переменной $SAFE. Если оно равно или выше единицы, ваша библиотека не должна работать с помеченными объектами. Например, вы не должны отправлять SQL-запросы к базе данных, если строка, содержащая подобные запросы, является помеченной. Ограничения выполнения, связанные с уровнем безопасности 1, включают сле- дующие пункты: О переменные среды окружения RUBYLIB и RUBYOPT при запуске игнорируются; О текущий каталог (.) в $LOAD_PATH не включается; О использование ключей командной строки -е, -1, -I, -г, -s, -S и -X запрещается; О для помеченных аргументов вызов некоторых методов экземпляров и методов классов, определяемых в классах Di г, ID, Fi le и Fi leTest — запрещается; О вызов test, eval, require, load и trap с помеченными аргументами невозможен. 10.5.2.3. Уровень безопасности 2 Уровень безопасности 2 ограничивает операции над помеченными данными в том же объеме, что и уровень 1, но вдобавок к этому налагает дополнительные
10.5. Безопасность 491 ограничения на работу с файлами и процессами, независимо от наличия пометок. Вряд ли существуют какие-нибудь причины, по которым программа должна уста- навливать значение своего собственного уровня безопасности в 2, но системный администратор может выбрать режим запуска написанной вами программы с этим уровнем безопасности, гарантируя ее несостоятельность в создании или удалении каталогов, изменении прав доступа к файлам, запуске выполняемых компонентов операционной системы, загрузке кода Ruby из общедоступных каталогов и т. д. На этом уровне ограничивается работа следующих методов: Dlr.chdiг File.truncate Dlr.chroot Flle.umask Dlr.mkdlr lO.fctrl DI г.rmdlг 10.1 octi Flle.chmod Kernel .exit! Fl le.chown Kernel.fork File.fl ock Kernel.syscall Flle.Istat Kernel.trap Process.eg1d= Process.fork Process.kill Process.setpgld Process.setpriority Process.setsld Вдобавок к этому уровень безопасности 2 предохраняет вас от загрузки или вос- требования Ruby-кода, или запуска выполняемых компонентов, сохраненных в об- щедоступных каталогах. 10.5.2.4. Уровень безопасности 3 Уровень безопасности 3 включает все ограничения уровня 2 и в дополнение к это- му помечает при создании как неблагонадежные все объекты, включая литералы в исходном коде программы (но исключая предопределенные объекты в глобаль- ной среде окружения). Более того, при этом уровне безопасности запрещается использование метода untaint. Уровень безопасности 3 является промежуточным шагом по направлению к уровню 4 и используется довольно редко. 10.5.2.5. Уровень безопасности 4 Этот уровень расширяет возможности уровня безопасности 3, предотвращая лю- бые изменения непомеченных объектов (включая вызов для них метода taint). Код, запущенный на этом уровне, не может вносить изменения в глобальную сре- ду окружения, а также не может изменять любые непомеченные объекты, ранее созданные кодом, запущенным на более низком уровне безопасности. Таким обра- зом создается эффективная среда изоляции, в которой сомнительный код может запускаться без нанесения какого-нибудь вреда. (По крайней мере теоретически в будущем могут быть найдены какие-нибудь ошибки реализации или недостатки в исходной модели безопасности.) Вызов метода eval в отношении помеченных строк на уровнях 1, 2 и 3 запрещен. На уровне безопасности 4 он разрешен опять, поскольку ограничений на этом уровне вполне достаточно, для того чтобы вычисление строки не нанесло какого- нибудь вреда. Способ вычисления произвольного кода в изолированной среде уровня 4 выглядит следующим образом:
492 Глава 10. Среда окружения Ruby def safe_eval (str) Thread.start { $SAFE = 4 eval(str) }.value end # Запуск изолированного потока # Повышение уровня безопасности # Вычисление в изолированной среде # Извлечение результата Использование метода require для загрузки в код Ruby какого-нибудь другого файла при установленном уровне безопасности 4 невозможно. Можно восполь- зоваться методом load, но только в изолированной форме, с использованием true в качестве его второго аргумента, заставляя тем самым Ruby изолировать загру- женный файл в безымянном модуле, чтобы любые определенные в нем классы, модули или константы не оказывали влияния на глобальное пространство имен. Это означает, что код, запущенный на уровне безопасности 4, может загружать, но не может использовать классы и модули, определенные во внешних модулях. В изолированную среду уровня 4 можно внести дальнейшие ограничения путем помещения изолирующего потока (перед установкой значения переменной SSAFE) в ThreadGroup и вызова для этой группы метода enclose. Подробности изложены в разделе 9.9.5. Как часть созданной им изолированной среды, уровень безопас- ности 4 запрещает дополнительные операции, включая следующие: О использование requi re, не изолированного вызова 1 oad, использование auto 1 oad и include; О изменение класса Object; О изменение непомеченных классов или модулей; О использование методов метапрограммирования; О работу с вычислительными потоками, исключая текущий; О доступ к локальным данным вычислительного потока; О завершение процесса; О использование файлового ввода-вывода; О изменение переменных среды; О задание предопределенного начального значения генератору случайных чисел с помощью метода srand.
Дэвид Флэнаган, Юкихиро Мацумото Язык программирования Ruby Перевели с английского Н. Вилъчинский Заведующий редакцией Руководитель проекта Ведущий редактор Литературный редактор Художественный редактор Корректор Верстка А. Кривцов А. Юрченко Ю. Сергиенко П. Маннинен Л. Адуевская В. Нечаева Л. Родионова Подписано в печать 20.12.10. Формат 70x100/16. Усл. п. л. 39,99. Тираж 1000. Заказ 24849. ООО «Мир книг», 198206, Саикт-Петербург, Петергофское шоссе, 73, лит. А29. Налоговая льгота — общероссийский классификатор продукции ОК 005-93, том 2; 95 3005 — литература учебная. Отпечатано по технологии CtP в ОАО «Печатный двор» им. А. М. Горького. 197110, Саикт-Петербург, Чкаловский пр., 15.
сялд САНКТ-ПЕТЕРБУРГСКАЯ АНТИВИРУСНАЯ ЛАБОРАТОРИЯ ДАНИЛОВА www.SRLD.rcj 8 (812) 336-3739 Антивирусные програттные продукты Ruby предназначен для того, чтобы сделать программистов счастливыми. Юкихиро «Matz» Мацумото Эта книга — официальное руководство по динамическому языку программирования Ruby. Авторский состав воистину звездный: Дэвид Флэнаган — известнейший специалист в области программирования, автор ряда бестселлеров по JavaScript и Java; Юкихиро «Matz» Мацумото — создатель и ведущий разработчик Ruby. В книге приведено детальное описание всех аспектов языка: лексической и синтаксической структуры Ruby, разновидностей данных и элементарных выражений, определений методов, классов и модулей. Кроме того, книга содержит информацию об API-функциях платформы Ruby. Издание будет интересно опытным программистам, знакомящимся с новым для себя языком Ruby, а также тем, кто уже программирует на Ruby и хочет достичь более высокого уровня понимания и мастерства работы. Уровень пользователя: начинающий/опытный Тема: Ruby С^пптер O’REILLY’ Заказ книг: 197198, Санкт-Петербург, а/я 127 тел.: (812) 703-73-74, postbook@piter.com 61093, Харьков-93, а/я 9130 тел.: (057) 758-41-45, 751-10-02, piter@kharkov.piter.com www.piter.com — вся информация о книгах и веб-магазин