Вы находитесь на странице: 1из 342

THE RUBY WAY, SECOND EDITION ПРОГРАММИРОВАНИЕ НА ЯЗЫКЕ RUBY

HAL FULTON ХЭЛ ФУЛТОН

Addison Wesley
Upper Saddle River, NJ • Boston • Indianapolis • San Francisco
New York • Toronto • Montreal • London • Münich • Paris • Madrid
Capetown • Sydney • Tokyo • Singapore • Mexico City Москва, 200
УДК 004.438
ББК 32.973.26018.2

Фултон Х.
Ф94 Программирование на языке Ruby. – М.: ДМК Пресс, 200. – 688 с.: ил.
Содержание
ISBN 5-94074-357-9
Предисловие ......................................................... 12
Ruby – относительно новый объектно-ориентированный язык, разработан-
ный Юкихиро Мацумото в 1995 году и позаимствовавший некоторые особен- Об авторе .............................................................. 17
ности у языков LISP, Smalltalk, Perl, CLU и других. Язык активно развивается
и применяется в самых разных областях: от системного администрирования Введение .............................................................. 18
до разработки сложных динамических сайтов. О втором издании .......................................................................18
Книга является полноценным руководством по Ruby – ее можно исполь- Как организована эта книга .........................................................21
зовать и как учебник, и как справочник, и как сборник ответов на вопросы Об исходных текстах, приведенных в книге .................................23
типа «как сделать то или иное в Ruby». В ней приведено свыше 400 приме- «Путь Ruby» .................................................................................24
ров, разбитых по различным аспектам программирования, и к которым ав-
тор дает обстоятельные комментарии. Глава 1. Обзор Ruby ................................................ 29
Издание предназначено для программистов самого широкого круга и са- 1.1. Введение в объектно-ориентированное
мой разной квалификации, желающих научиться качественно и професси- программирование ..............................................................30
онально работать на Ruby. 1.2. Базовый синтаксис и семантика Ruby ...................................35
1.3. ООП в Ruby ...........................................................................48
1.4. Динамические аспекты Ruby.................................................57
1.5. Потренируйте свою интуицию: что следует запомнить .........61
1.6. Жаргон Ruby .........................................................................76
1.7. Заключение ..........................................................................79
УДК 004.438
ББК 32.973.26018.2 Глава 2. Строки ...................................................... 80
2.1. Представление обычных строк .............................................80
Все права защищены. Любая часть этой книги не может быть воспроизведена в какой 2.2. Альтернативная нотация для представления строк ...............81
бы то ни было форме и какими бы то ни было средствами без письменного разрешения
владельцев авторских прав.
2.3. Встроенные документы
Материал, изложенный в данной книге, многократно проверен. Но поскольку вероят- 2.4. Получение длины строки ......................................................83
ность технических ошибок все равно существует, издательство не может гарантировать 2.5. Построчная обработка ..........................................................83
абсолютную точность и правильность приводимых сведений. В связи с этим издательство
не несет ответственности за возможные ошибки, связанные с использованием книги.
2.6. Побайтовая обработка ..........................................................84
2.7. Специализированное сравнение строк .................................84
All rights reserved. No part of this book may be reproduced or transmitted in any form or by 2.8. Разбиение строки на лексемы ..............................................85
any means, electronic, mechanical, photocopying, recording or otherwise, without prior written 2.9. Форматирование строк ........................................................87
permission the publisher. For information on getting permission for reprints and excerpts, contact
permission@peachpit.com. RUSSIAN language edition published by DMK PUBLISHERS, 2.10. Строки в качестве объектов ввода/вывода..........................87
Copyright © 2007. 2.11. Управление регистром .......................................................88
2.12. Вычленение и замена подстрок ..........................................88
ISBN 0672328844 Copyright 200 © Pearson Education, Inc. 2.13. Подстановка в строках ........................................................90
ISBN 5-94074-357-9 © Оформление, ДМК Пресс, 200 2.14. Поиск в строке ....................................................................91
6 Содержание Содержание 7

2.15. Преобразование символов в коды ASCII и обратно .............92 3.10. Сопоставление точки символу конца строки .....................121
2.16. Явные и неявные преобразования ......................................92 3.11. Внутренние модификаторы ..............................................122
2.17. Дописывание в конец строки ..............................................94 3.12. Внутренние подвыражения ...............................................122
2.18. Удаление хвостовых символов новой строки и прочих ........94 3.13. Ruby и Oniguruma ..............................................................123
2.19. Удаление лишних пропусков ...............................................95 3.14. Примеры регулярных выражений ......................................129
2.20. Повтор строк ......................................................................96 3.15. Заключение ......................................................................133
2.21. Включение выражений в строку ..........................................96
2.22. Отложенная интерполяция ..................................................96
Глава 4. Интернационализация в Ruby .......................134
2.23. Разбор данных, разделенных запятыми ..............................97 4.1. Исторические сведения и терминология ............................135
2.24. Преобразование строки в число (десятичное или иное) ......98 4.2. Кодировки в пост-ASCII мире ..............................................139
2.25. Кодирование и декодирование строк в кодировке rot13 ......99 4.3. Справочники сообщений ....................................................150
2.26. Шифрование строк ...........................................................100 4.4. Заключение ........................................................................157
2.27. Сжатие строк ....................................................................101 Глава 5. Численные методы.....................................158
2.28. Подсчет числа символов в строке .....................................101 5.1. Представление чисел в языке Ruby.....................................158
2.29. Обращение строки............................................................102 5.2. Основные операции над числами .......................................159
2.30. Удаление дубликатов ........................................................102 5.3. Округление чисел с плавающей точкой ...............................160
2.31. Удаление заданных символов ...........................................102 5.4. Сравнение чисел с плавающей точкой ................................162
2.32. Печать специальных символов..........................................102 5.5. Форматирование чисел для вывода ....................................163
2.33. Генерирование последовательности строк .......................103
5.6. Вставка разделителей при форматировании чисел ............163
2.34. Вычисление 32-разрядного CRC .......................................103
5.7. Работа с очень большими числами .....................................164
2.35. Вычисление MD5-свертки строки .....................................104
5.8. Использование класса BigDecimal ......................................164
2.36. Вычисление расстояния Левенштейна
5.9. Работа с рациональными числами ......................................166
между двумя строками ......................................................105
5.10. Перемножение матриц .....................................................167
2.37. base64-кодирование и декодирование .............................106
5.11. Комплексные числа ..........................................................171
2.38. Кодирование и декодирование строк
5.12. Библиотека mathn .............................................................172
(uuencode/uudecode).........................................................107
5.13. Разложение на простые множители,
2.39. Замена символов табуляции пробелами
вычисление НОД и НОК .....................................................172
и сворачивание пробелов в табуляторы .............................107
5.14. Простые числа ..................................................................173
2.40. Цитирование текста ..........................................................108
5.15. Явные и неявные преобразования чисел...........................174
2.41. Заключение ......................................................................109
5.16. Приведение числовых значений .......................................175
Глава 3. Регулярные выражения ...............................110 5.17. Поразрядные операции над числами ................................176
3.1. Синтаксис регулярных выражений ......................................110 5.18. Преобразование системы счисления ................................177
3.2. Компиляция регулярных выражений ...................................112 5.19. Извлечение кубических корней,
3.3. Экранирование специальных символов ..............................113 корней четвертой степени и т. д. ........................................178
3.4. Якоря..................................................................................113 5.20. Определение порядка байтов ...........................................178
3.5. Кванторы ............................................................................114 5.21. Численное вычисление определенного интеграла ............179
3.6. Позитивное и негативное заглядывание вперед .................116 5.22. Тригонометрия в градусах, радианах и градах ..................180
3.7. Обратные ссылки................................................................117 5.23. Неэлементарная тригонометрия.......................................181
3.8. Классы символов ................................................................119 5.24. Вычисление логарифмов по произвольному основанию ...182
3.9. Обобщенные регулярные выражения .................................120 5.25. Вычисление среднего, медианы и моды набора данных ...182
8 Содержание Содержание 9

5.26. Дисперсия и стандартное отклонение ..............................183 Глава 8. Массивы, хэши


5.27. Вычисление коэффициента корреляции ...........................184 и другие перечисляемые структуры.......................219
5.28. Генерирование случайных чисел .......................................185 8.1. Массивы .............................................................................219
5.29. Кэширование функций с помощью метода memoize .........186 8.2. Хэши...................................................................................242
5.30. Заключение ......................................................................187 8.3. Перечисляемые структуры в общем ...................................252
Глава 6. Символы и диапазоны ................................188 8.4. Заключение ........................................................................259
6.1. Символы .............................................................................188 Глава 9. Более сложные структуры данных ................260
6.2. Диапазоны..........................................................................192 9.1. Множества .........................................................................260
6.3. Заключение ........................................................................200
9.2. Стеки и очереди .................................................................263
Глава 7. Дата и время .............................................202 9.3. Деревья ..............................................................................268
7.1. Определение текущего момента времени ..........................203 9.4. Графы .................................................................................274
7.2. Работа с конкретными датами (после точки отсчета) ..........203 9.5. Заключение ........................................................................280
7.3. Определение дня недели ....................................................204 Глава 10. Ввод/вывод и хранение данных ..................281
7.4. Определение даты Пасхи....................................................204
10.1. Файлы и каталоги .............................................................282
7.5. Вычисление n-ого дня недели в месяце ..............................205
10.2. Доступ к данным более высокого уровня ..........................306
7.6. Преобразование из секунд в более крупные единицы.........206
10.3. Библиотека KirbyBase .......................................................314
7.7. Вычисление промежутка времени,
прошедшего от точки отсчета ............................................207 10.4. Подключение к внешним базам данных.............................317
7.8. Високосные секунды ..........................................................207 10.5. Заключение ......................................................................329
7.9. Определение порядкового номера дня в году .....................208 Глава 11. ООП и динамические механизмы в Ruby .......330
7.10. Контроль даты и времени .................................................208 11.1. Рутинные объектно-ориентированные задачи ..................331
7.11. Определение недели в году ..............................................209 11.2. Более сложные механизмы ...............................................356
7.12. Проверка года на високосность ........................................210 11.3. Динамические механизмы ................................................375
7.13. Определение часового пояса............................................210 11.4. Заключение ......................................................................395
7.14. Манипулирование временем без даты ..............................211
7.15. Сравнение моментов времени ..........................................211 Глава 12. Графические интерфейсы для Ruby .............396
7.16. Прибавление интервала к моменту времени .....................211 12.1. Ruby/Tk.............................................................................397
7.17. Вычисление разности между двумя моментами 12.2. Ruby/GTK2 ........................................................................409
времени ............................................................................212 12.3. FXRuby (FOX) .....................................................................422
7.18. Работа с конкретными датами (до точки отсчета)..............212 12.4. QtRuby ..............................................................................436
7.19. Взаимные преобразования объектов Date, 12.5. Другие библиотеки
Time и DateTime .................................................................213 для создания графических интерфейсов ...........................446
7.20. Извлечение даты и времени из строки ..............................214 12.6. Заключение ......................................................................447
7.21. Форматирование и печать даты и времени .......................215
7.22. Преобразование часовых поясов ......................................216 Глава 13. Потоки в Ruby ..........................................448
7.23. Определение числа дней в месяце ...................................216 13.1. Создание потоков и манипулирование ими.......................449
7.24. Разбиение месяца на недели ............................................216 13.2. Синхронизация потоков ....................................................458
7.25. Заключение ......................................................................218 13.3. Заключение ......................................................................473
10 Содержание Содержание 11

Глава 14. Сценарии Глава 19. Ruby и Web-приложения ............................592


и системное администрирование ..........................474 19.1. Программирование CGI на Ruby .......................................592
14.1. Запуск внешних программ ................................................474 19.2. FastCGI .............................................................................597
14.2. Флаги и аргументы в командной строке ............................479 19.3. Ruby on Rails .....................................................................599
14.3. Библиотека Shell ...............................................................482 19.4. Разработка Web-приложений с помощью Nitro..................603
14.4. Переменные окружения ....................................................485 19.5. Введение в Wee ................................................................615
14.5. Сценарии на платформе Microsoft Windows .......................487 19.6. Разработка Web-приложений с помощью IOWA .................617
14.6. Моментальный инсталлятор для Windows .........................493 19.7. Ruby и Web-сервер ...........................................................622
14.7. Библиотеки, о которых полезно знать ...............................494 19.8. Заключение ......................................................................629
14.8. Работа с файлами, каталогами и деревьями .....................495 Глава 20. Распределенный Ruby...............................630
14.9. Различные сценарии.........................................................498
20.1. Обзор: библиотека drb ......................................................630
14.10. Заключение ....................................................................502
20.2. Пример: эмуляция биржевой ленты ..................................633
Глава 15. Ruby и форматы данных ............................503 20.3. Rinda: пространство кортежей в Ruby ...............................636
15.1. Разбор XML и REXML .........................................................503 20.4. Обнаружение сервисов в распределенном Ruby ...............640
15.2. RSS и Atom .......................................................................508 20.5. Заключение ......................................................................641
15.3. Обработка изображений с помощью RMagick ...................512 Глава 21. Инструменты разработки для Ruby ..............642
15.4. Создание документов в формате PDF
21.1. Система RubyGems ...........................................................642
с помощью библиотеки PDF::Writer .....................................521
21.2. Программа Rake ...............................................................644
15.5. Заключение ......................................................................530
21.3. Оболочка irb......................................................................647
Глава 16. Тестирование и отладка .............................531 21.4. Утилита ri ..........................................................................652
16.1. Библиотека Test::Unit ........................................................531 21.5. Поддержка со стороны редакторов ...................................653
16.2. Комплект инструментов ZenTest .......................................535 21.6. Интегрированные среды разработки ................................654
16.3. Работа с отладчиком Ruby ................................................538 21.7. Заключение ......................................................................656
16.4. Использование irb в качестве отладчика ...........................541 Глава 22. Сообщество пользователей Ruby ................657
16.5. Измерение покрытия кода ................................................542
22.1. Ресурсы в Web ..................................................................657
16.6. Измерение производительности ......................................543
22.2. Новостные группы и списки рассылки...............................658
16.7. Объекты печати ................................................................547
22.3. Блоги и онлайновые журналы ...........................................658
16.8. Заключение ......................................................................548
22.4. Запросы на изменение Ruby .............................................659
Глава 17. Создание пакетов 22.5. Каналы IRC .......................................................................659
и распространение программ ...............................550 22.6. Конференции по Ruby .......................................................659
17.1. Программа RDoc ..............................................................550 22.7. Локальные группы пользователей Ruby ............................660
17.2. Установка и подготовка пакета .........................................555 22.8. Заключение ......................................................................660
17.3. RubyForge и RAA ...............................................................559 Алфавитный указатель ...........................................662
17.4. Заключение ......................................................................560
Глава 18. Сетевое программирование .......................561
18.1. Сетевые серверы ..............................................................562
18.2. Сетевые клиенты ..............................................................572
18.3. Заключение ......................................................................591
Предисловие к первому изданию 13

По мере того как компьютеры становятся мощнее и дешевле, ситуация по-


степенно меняется. Возьмем, к примеру, структурное программирование. Маши-
не все равно, насколько хорошо структурирована программа: она просто испол-
няет ее команда за командой. Идеи структурного программирования обращены
к людям, а не к машинам. То же относится и к объектно-ориентированному про-
Предисловие граммированию.
Пришло время проектировать языки, удобные для людей!
В 1993 году я разговаривал со своим коллегой о сценарных языках, их вырази-
Предисловие ко второму изданию тельности и перспективах. Я считал, что программирование пойдет именно по это-
му пути и будет ориентироваться на человека. Но меня не вполне устраивали су-
В древнем Китае люди, в особенности философы, полагали, что под внешней обо-
ществующие языки, такие как Perl и Python. Я хотел видеть язык более мощный,
лочкой мира и любого существа скрыто нечто, чего нельзя ни объяснить, ни опи-
чем Perl, и более объектно-ориентированный, чем Python. Найти идеальный вари-
сать словами. Это нечто китайцы называли Дао, а японцы – До. На русский язык ант мне не удалось, поэтому остался один выход: изобрести свой собственный.
это слово можно перевести как Путь. Слово «до» входит в такие названия, как Ruby – не самый простой язык, но ведь и человеческая душа не проста. Ей рав-
дзюдо, кендо, карате-до и айкидо. Это не просто боевые искусства, а целая фило- но нравятся простота и сложность. Она не приемлет ни слишком примитивных, ни
софия и взгляд на жизнь. чересчур заумных вещей. Она ищет равновесия.
Так и в языке программирования Ruby есть свои философия и способ мышле- Поэтому при проектировании ориентированного на человека языка – Ruby –
ния. Этот язык заставляет думать по-новому. Он помогает программистам полу- я следовал принципу наименьшего удивления. Иными словами, работа шла под
чать удовольствие от своей работы. И не потому, что Ruby был создан в Японии, а девизом: хорошо то, что не кажется мне странным. Вот почему я чувствую себя
потому, что программирование стало важной частью существования – по крайней в своей родной стихии и испытываю радость, когда программирую на Ruby. А с мо-
мере, для некоторых людей, жизнь которых Ruby призван улучшить. мента выхода в свет первой версии в 1995 году многие программисты во всем ми-
Как всегда, описать, что такое Дао, трудно. Я чувствую это, но не могу подыс- ре разделили со мной эту радость.
кать нужных слов даже на японском, моем родном языке. А вот один смельчак по Как всегда, хочу выразить величайшую благодарность всем членам сообщест-
имени Хэл Фултон попытался, и его первая попытка (первое издание этой книги) ва, сложившегося вокруг Ruby. Они – причина его успеха.
оказалась довольно удачной. Попытка номер два увенчалась еще лучшим резуль- Также я благодарен автору этой книги, Хэлу Фултону, за то, что он показал
татом, чему немало способствовала помощь многих людей из сообщества пользо- другим Путь Ruby. В книге объясняется философия, стоящая за языком Ruby. Это
вателей Ruby. По мере того как Ruby набирает популярность (отчасти благодаря квинтэссенция моих мыслей и ощущений членов сообщества. Интересно, как Хэ-
продукту Ruby on Rails), все важнее становится овладение секретами мастерства лу удалось прочитать мои мысли и раскрыть секрет Пути Ruby!.. Я никогда не
производительного программирования на этом языке. Надеюсь, что книга, кото- встречался с ним лично, надеюсь, что скоро это все-таки произойдет.
рую вы держите в руках, поможет вам в решении этой задачи. В заключение хочу выразить надежду, что эта книга и сам язык Ruby помогут
Удачной работы! вам получить удовольствие и радость от программирования.
Юкихиро «Мац» Мацумото Юкихиро «Мац» Мацумото
Сентябрь 2001, Япония
Август 2006 года, Япония

Предисловие к первому изданию


Вскоре после того как я впервые познакомился с компьютерами в начале 1980-х
годов, меня заинтересовали языки программирования. С тех пор я буквально по-
мешался на этой теме. Думаю, причина такого интереса в том, что языки програм-
мирования – это способ выражения мыслей. Они по сути своей предназначены
для человека.
Но вопреки этому факту языки программирования почему-то всегда оказыва-
лись в большей степени машинно-ориентированными. Многие из них спроекти-
рованы с учетом удобства для компьютеров.
Благодарности за первое издание 15

подготовки PDF-файлов. Калеб Теннис (Kaleb Tennis) дал материал по Qt. Эрик
Ходел (Eric Hodel) помог в описании продуктов Rinda и Ring, а Джеймс Бритт
(James Britt) внес большой вклад в главу о разработке приложений для Web.
И еще раз выражаю искреннюю благодарность и восхищение Мацу – не толь-
ко за помощь, но и прежде всего – за создание Ruby. Domo arigato gozaimasu*!
Благодарности Еще раз хочу поблагодарить своих родителей. Они постоянно подбадрива-
ли меня и с нетерпением ждали выхода книги... Я еще сделаю из них програм-
мистов!
Благодарности за второе издание И опять – спасибо всем членам сообщества пользователей Ruby за неутоми-
мую энергию и дух коллективизма. Особенно я благодарен читателям этой книги
Здравый смысл подсказывает, что второе издание требует в два раза меньше рабо-
(обоих изданий). Я надеюсь, что вы найдете ее информативной, полезной и, быть
ты, чем первое. Но здравый смысл ошибается.
может, даже увлекательной.
Хотя значительная часть текста книги перекочевала прямиком из первого из-
дания, даже эту часть пришлось сильно править. К каждому предложению прихо-
дилось ставить вопрос: «Сохранилось ли в 2006 году то, что было верно в 2001-м?» Благодарности за первое издание
И это только начало! Создание книги – плод усилий целого коллектива. Я не понимал этого в полной
Короче говоря, я потратил много сотен часов на подготовку второго издания – мере, пока не взялся за это дело сам. Рекомендую всем пройти подобное испыта-
примерно столько же, сколько ушло на первое. И тем не менее я «всего лишь ав- ние, хотя оно и не из легких. Нет сомнений, что без помощи многих и многих лю-
тор». дей книга не увидела бы свет.
Книга появляется на свет в результате усилий многих людей. Если говорить об Прежде всего, выражаю благодарность и восхищение Мацу (Юкихиро Мацу-
издательстве, то я благодарен Дебре Вильямс Коули (Debra Williams Cauley), Сонь- мото), который создал язык Ruby. Domo arigato gozaimasu!*
лин Киу (Songlin Qiu) и Менди Фрэнк (Mandie Frank) за тяжелый труд и бесконеч- Спасибо Конраду Шнейкеру (Conrad Schneiker), который подал мне идею на-
ное терпение. Спасибо Джениль Бриз (Geneil Breeze) за неутомимое вычитывание писать эту книгу и помог выработать ее общий план. Он же оказал мне неоцени-
и выпалывание сорняков из моего английского. Есть много других сотрудников, ко- мую услугу, познакомив с языком Ruby в 1999 году.
торых я не могу назвать, поскольку их работа проходила за кулисами и я никогда с Материалом для книги меня снабжали несколько человек. Первым хочу на-
ними не встречался. звать Гая Хэрста (Guy Hurst), который написал значительную часть начальных
За техническое редактирование отвечали главным образом Шашанк Дейт глав, а также два приложения. Его помощь поистине бесценна.
(Shashank Date) и Фрэнсис Хван (Francis Hwang). Они прекрасно справились со Спасибо также другим помощникам, называя которых, я не придерживаюсь
своей задачей – если остались какие-то ошибки, это всецело моя вина. какого-то определенного порядка. Кэвин Смит (Kevin Smith) многое сделал для
Большое спасибо людям, которые предлагали объяснения, писали код приме- раздела главы 6, посвященного GTK, избавив меня от изучения сложной темы в
ров и отвечали мне на многочисленные вопросы. Это сам Мац (Юкихиро Мацу- условиях жесткого графика. Патрик Логан (Patrick Logan) пролил свет на графи-
мото), Дейв Томас (Dave Thomas), Кристиан Нойкирхен (Christian Neukirchen), ческую систему FOX GUI, описанную в той же главе. Чед Фаулер (Chad Fowler) в
Чед Фаулер (Chad Fowler), Дэниэл Бергер (Daniel Berger), Армин Рерль (Armin главе 9 углубился в тайны XML, а также помог при написании раздела о CGI.
Roehrl), Стефан Шмидль (Stefan Schmiedl), Джим Вайрих (Jim Weirich), Райан Благодарю всех, кто правил корректуру, писал рецензии и помогал иными спосо-
Дэвис (Ryan Davis), Дженни У. (Jenny W.), Джим Фриз (Jim Freeze), Лайл Джон- бами: Дона Мучоу (Don Muchow), Майка Стока (Mike Stok), Михо Огисима (Miho
сон (Lyle Johnson), Мартин Де Мелло (Martin DeMello), Март Лоуренс (Mart Ogishima) и уже упомянутых выше. Спасибо Дэвиду Эпштейну (David Eppstein),
Lawrence), Рон Джеффрис (Ron Jeffries), Тим Хантер (Tim Hunter), Чет Хенд- профессору математики, который ответил на вопросы по теории графов.
Одна из замечательных особенностей Ruby – поддержка со стороны сообщес-
риксон (Chet Hendrickson), Натаниэль Талбот (Nathaniel Talbott) и Бил Клеб (Bil
тва пользователей. В списке рассылки и в конференции многие отвечали на мои
Kleb).
вопросы, подавали идеи и всячески помогали. Опять же не придерживаясь опре-
Особая благодарность активным помощникам. Эндрю Джонсон (Andrew John-
деленного порядка, хочу упомянуть Дейва Томаса (Dave Thomas), Энди Ханта
son) здорово обогатил мои знания о регулярных выражениях. Пол Бэтли (Paul
(Andy Hunt), Хи-Соб Парка (Hee-Sob Park), Майка Уилсона (Mike Wilson), Ави
Battley) предоставил обширный материал для главы об интернационализации.
Брайанта (Avi Bryant), Ясуси Шоджи (Yasushi Shoji «Yashi»), Шуго Маэда (Shugo
Масао Муто (Masao Mutoh) помог в написании той же главы, а также снабдил
меня материалами по GTK. Остин Зиглер (Austin Ziegler) научил меня секретам * Огромное спасибо (яп.)
Maeda), Джима Вайриха (Jim Weirich) и Масаки Сукета (Masaki Suketa). Как это
ни печально, но, скорее всего, кого-то я пропустил.
Очевидно, что книга никогда не вышла бы без помощи издателя. Многие ра-
ботали над ней незримо для меня, но особенно я хотел бы поблагодарить Вильяма
Брауна (William Brown), который тесно сотрудничал со мной и постоянно поощ-
рял, и Скотта Мейера (Scott Meyer), который скрупулезно занимался объедине-
нием всех материалов. Других назвать не могу, потому что никогда о них не слы-
Об авторе
шал. Но они знают себя сами. Хэл Фултон – обладатель двух ученых степеней по информатике, полученных в
Хочу поблагодарить своих родителей, которые следили за проектом издале- Университете штата Миссисипи. Он четыре года преподавал информатику в кол-
ка, подбадривали меня и даже дали себе труд ради меня освоить азы программи- ледже, пока не переехал в Остин, штат Техас, для работы по контрактам (в ос-
рования. новном с отделением компании IBM в Остине). Более 15 лет он работал с раз-
Один мой друг как-то сказал: «Если ты написал книгу, которую никто не чита- личными версиями ОС UNIX, в том числе AIX, Solaris и Linux. С языком Ruby
ет, значит, ты не написал ее вовсе». Поэтому напоследок я хочу поблагодарить чи- впервые познакомился в 1999 году, а в 2001 приступил к работе над первым изда-
тателей. Эта книга для вас. Надеюсь, что она окажется полезной. нием этой книги – второй книги на английском языке, посвященной Ruby. Фул-
тон присутствовал на шести конференциях по Ruby и проводил презентации на
четырех из них, в частности на первой европейской конференции по языку Ruby,
состоявшейся в Карлсруэ (Германия). Сейчас Хэл работает в компании Broadwing
Communications, располагающейся в Остине, и занимается вопросами, связанны-
ми с большим хранилищем данных и относящимися к нему телекоммуникацион-
ными приложениями. В работе он использует язык C++, СУБД Oracle и, конеч-
но, Ruby.
Фултон по-прежнему постоянно присутствует в списке рассылки и в IRC-ка-
нале, посвященном Ruby, а также участвует в нескольких разрабатываемых проек-
тах на Ruby. Он член Ассоциации по вычислительной технике (ACM – Association
for Computing Machinery) и компьютерного общества IEEE (Институт инженеров
по электротехнике и электронике). В свободное от работы время увлекается музы-
кой, чтением, искусством и фотографией. Кроме всего прочего, Хэл член общества
по изучению Марса и энтузиаст космических полетов. Мечтал бы когда-нибудь
совершить такой полет. Проживает в Остине, штат Техас.
О втором издании 19

Аналогичным образом главы 18, 19 и 20 образовались в результате разделе-


ния главы 9. Приложения удалены, чтобы освободить место для основного мате-
риала.
Появились также следующие новые главы:
• глава 15, «Форматы данных в Ruby». Здесь рассматриваются форматы
Введение XML, RSS, графические файлы, создание PDF-файлов и другие вопросы;
• глава 16, «Тестирование и отладка». Речь идет о тестировании, профилиро-
вании, отладке, анализе кода и тому подобных вещах;
Путь, о котором можно поведать, – • в главе 17, «Создание пакетов и распространение программ», обсуждаются,
не постоянный Путь. в частности, инструмент setup.rb и создание пакетов в формате RubyGem;
Лао Цзы, «Дао де цзин»* • глава 21, «Инструменты разработки для Ruby», знакомит с поддержкой
Ruby в редакторах и интегрированных средах разработки (IDE), утилитой
Эта книга называется «Путь Ruby»**. Название нуждается в некотором пояснении. ri и форматом RubyGem с точки зрения пользователя;
Я ставил себе целью выразить в этой книге философию языка Ruby, насколь- • в главе 22, «Сообщество Ruby», приводятся основные Web-сайты, списки
ко это в моих силах. Ту же цель преследовали мои добровольные помощники. Ус- рассылки, форумы, конференции, IRC-каналы по Ruby и прочие дополни-
пех должно разделить между всеми, а ошибки остаются моей и только моей виной. тельные сведения.
Конечно, я не могу абсолютно точно сказать, в чем же состоит истинный дух В широком смысле все главы в этой книге «новые». Каждую из них я под-
Ruby. Эту задачу я оставляю Мацу, но подозреваю, что даже ему трудно будет об- верг тщательной ревизии, внес сотни мелких и десятки крупных изменений. Был
лечь свои ощущения в слова. исключен материал, утративший актуальность и показавшийся мне не слишком
Короче говоря, «The Ruby Way» – всего лишь книга, а Путь Ruby – удел созда- важным. Я учел изменения в самом языке Ruby, а также добавил в каждую главу
теля языка и сообщества в целом. Втиснуть его в рамки книги довольно трудно. И
новые примеры и комментарии.
все-таки в этом введении я попытаюсь хотя бы отчасти передать неуловимый дух
Возможно, вас интересует, что было добавлено в старые главы. Отвечаю: уже
Ruby. Мудрый ученик не воспримет эту попытку как окончательный вердикт!
упомянутая библиотека регулярных выражений Oniguruma; математические биб-
Не забывайте, что это второе издание. Большая часть введения сохранена, но
лиотеки и классы, включая BigDecimal, mathn и matrix; такие новые классы, как Set
впереди раздел «О втором издании», в котором описываются изменения и вновь
и DateTime.
включенный материал.
В главу 10, «Ввод/вывод и хранение данных», я добавил материал о методе
readpartial, неблокирующем вводе/выводе и классе StringIO. Также рассмотрены
О втором издании форматы CSV, YAML и KirbyBase. В ту часть главы 10, которая посвящена базам
Все меняется, и Ruby – не исключение. Я пишу это введение в 2006 году, когда с данных, включены сведения о СУБД Oracle и SQLite, интерфейсе DBI, а также об
момента выхода первого издания прошло уже почти пять лет. Настало время для объектно-реляционном отображении (Object-Relational Mappers – ORM).
обновления. Глава 11, «ООП и динамические механизмы в Ruby», пополнилась инфор-
В это издание внесено немало исправлений и добавлено много нового матери- мацией о таких недавно добавленных в язык конструкциях, как initialize_copy,
ала. Прежняя глава 4 («Простые задачи, связанные с обработкой данных») пре- const_get, const_missing и define_method. Также я рассматриваю делегирование и
вратилась в шесть глав, две из которых («Диапазоны и символы» и «Интернацио- перенаправление.
нализация в Ruby») совсем новые; в четыре остальных добавлены новые примеры Глава 12, «Графические интерфейсы для Ruby», была переработана целиком
и комментарии к ним. Сильно расширен материал о регулярных выражениях: те- (в особенности разделы, посвященные GTK и Fox). Раздел по QtRuby – новый от
перь рассматриваются не только «классические» выражения, но и новая библио- начала до конца.
тека для их поддержки, Oniguruma. В главе 14, «Сценарии и системное администрирование», теперь обсуждаются
Главы 8 и 9 раньше составляли одну главу. Она была разбита на две, поскольку моментальный инсталлятор для Windows и ряд аналогичных пакетов. Кроме то-
из-за вновь добавленного материала оказалась слишком большой. го, улучшен код примеров.
В главе 18, «Сетевое программирование», появились разделы о вложениях в
* Пер. В. Малявина. – Прим. ред.
** Оригинальное название книги. Дословный перевод «Путь Ruby», однако в русскоязычном изда- электронные письма и о взаимодействии с IMAP-сервером. Также рассматривает-
нии было выбрано название «Программирование на языке Ruby». – Прим. ред. ся библиотека OpenURI.
20 Введение Как организована эта книга 21

В главе 19, «Ruby и Web-приложения», теперь рассматриваются продукты ницы не заметите, а если заметите, то, наверное, согласитесь, что язык стал бо-
Ruby on Rails, Nitro, Wee, IOWA и другие инструменты для Web. Также уделено лее последовательным.
внимание инструментам WEBrick и в какой-то мере Mongrel. Изменилась семантика некоторых системных методов. Но несущественно.
В главу 20, «Распределенный Ruby», добавлен материал о системе Rinda – ре- Например, раньше метод Dir#chdir не принимал в качестве параметра блок, но не-
ализации пространства кортежей, написанной на Ruby. Тут же приводятся сведе- сколько лет назад это стало допустимо.
ния о родственной системе Ring. Некоторые системные методы объявлены устаревшими или переименованы.
Так ли необходимы все эти добавления? Да, уверяю вас. Метод class утратил свой псевдоним type (поскольку в Ruby мы обычно не гово-
Напомню, кстати, что «The Ruby Way» – это вторая вышедшая на английском рим о типах объектов). Метод intern получил более понятное название to_sym, а
языке книга по языку Ruby; первой была знаменитая «Мотыга», или «Programming метод Array#indices называется Array#values_at. Можно было бы продолжить, но
Ruby», Дэйва Томаса (Dave Thomas) и Энди Ханта (Andy Hunt). Моя книга была думаю, что суть вы уловили.
составлена так, чтобы не перекрывать, а дополнять свою предшественницу; это и Кроме того, было добавлено несколько новых системных методов, например
стало одной из основных причин ее популярности. Enumerable#inject, Enumerable#zip и IO#readpartial. Старая библиотека futils
Когда я приступал к работе над первым изданием, еще не было международ- теперь называется fileutils, и у нее появилось собственное пространство имен
ных конференций по Ruby. Не было сайтов RubyForge, ruby-doc.org, не было Wiki- FileUtil, тогда как раньше она добавляла методы в класс File.
страницы rubygarden.org. Вообще в Сети не было почти ничего, кроме официаль- Есть и еще много изменений. Но важно понимать, что все они вносились очень
ного сайта Ruby. В архиве приложений Ruby насчитывалось всего несколько сотен осторожно и аккуратно. Язык как был Ruby, так им и остался. Красота Ruby в не-
программ. малой степени обязана тому факту, что он изменяется медленно и обдуманно, ве-
В то время лишь немногие периодические издания (как обычные, так и онлай- домый мудростью Маца и других разработчиков.
новые) знали о существовании этого языка. Когда где-то публиковалась статья о Сегодня нет недостатка в книгах по Ruby. Публикуется больше статей, чем мы
Ruby, мы все брали ее на заметку; о ней сообщалось в списке рассылки и там же в состоянии переварить. Множатся руководства и документация в сети Web.
проводилось обсуждение. Появились новые инструменты и библиотеки. По разным причинам большая
Многих привычных сегодня инструментов и библиотек еще не существовало. их часть – это каркасы и инструменты для разработки Web-приложений, средства
Все пока было впереди: и система RDoc, и пакет REXML для анализа XML-доку- для создания сетевых дневников (блогов), разметки, а также для объектно-реля-
ментов. Математическая библиотека была куда беднее нынешней. Поддержка баз ционного отображения (ORM). Но есть и инструментарий для работы с базами
данных была фрагментарной, а драйверы ODBC и вовсе отсутствовали. Tk был данных, организации графических интерфейсов, математических расчетов, Web-
чуть ли не единственным графическим интерфейсом. Приложения для Web раз- сервисов, обработки изображений, управления версиями и т. д.
рабатывались в виде низкоуровневых CGI-сценариев. Поддержка Ruby в редакторах широко распространена и достигла немалой
Еще не появился «моментальный» инсталлятор для Windows. Пользователям
изощренности. Существуют интегрированные среды разработки (IDE), весьма
Windows приходилось выполнять компиляцию исходных текстов в среде Cygwin
полезные и зрелые; частично они перекрываются с конструкторами графических
или с помощью minigw.
интерфейсов.
Системы RubyGem не было даже в примитивной форме. Процедура поиска и
Нет сомнений и в том, что сообщество пользователей разрослось и измени-
установки приложений проводилась вручную; для решения этой задачи использо-
лось. Сегодня Ruby никак не назовешь нишевым языком: им пользуются в НА-
вались инструменты типа tar и make.
СА, Национальной администрации по океану и атмосфере (NOAA), компании
Никто слыхом не слыхал о Ruby on Rails. Никто (насколько мне известно) не
Motorola и во многих других крупных организациях. Он применяется для рабо-
употреблял термина «утипизация»*. Не было ни YAML для Ruby, ни системы Rake.
ты с графикой, доступа к базам данных, численного анализа, Web-приложений и в
В то время мы пользовались версией Ruby 1.6.4 и считали ее безмерно крутой.
других областях. Короче говоря, Ruby стал весьма популярным языком.
Но Ruby 1.8.5 (с которой я обычно работаю сейчас) еще круче!
Я работал над новой редакцией этой книги с любовью. Надеюсь, что она ока-
Был незначительно изменен синтаксис, но не настолько серьезно, чтобы об
жется вам полезной.
этом писать. По большей части речь идет о «граничных случаях», и теперь син-
таксис в этих ситуациях выглядит более разумно. Ruby всегда отличали стран-
ности в отношении к необязательности скобок; в 98% случаев вы никакой раз- Как организована эта книга
Вряд ли вы станете изучать Ruby по этой книге. В ней не так уж много вводного
* Речь идет о переводе выражения «duck typing». Его смысл и происхождение объясняются в разде-
ле 1.6. Приношу извинения ревнителям чистоты русского языка за то, что не смог удержаться от игры и учебного материала. Если вы еще ничего не знаете о Ruby, то лучше начать с ка-
слов и выдумал этот «термин» вместо «утиной типизации». – Прим. перев. кой-нибудь другой книги.
22 Введение Об исходных текстах, приведенных в книге 23

Но программисты – народ упорный, и я допускаю, что научиться Ruby только Другой каприз состоит в том, что я избегаю пользоваться обособленными вы-
по этой книге возможно. В главе 1, «Обзор Ruby», приводится краткое введение в ражениями, если у них нет побочных эффектов. В Ruby выражения – одна из ос-
язык и очень скромное руководство. нов языка, и это прекрасно; я старался извлечь из этой особенности максимум
Также в главе 1 есть довольно полный перечень «скользких мест» (который пользы. Но во фрагментах кода предпочитаю не употреблять выражения, которые
трудно поддерживать в актуальном состоянии). Для разных читателей этот пере- просто возвращают никак не используемое значение. Например, для иллюстрации
чень полезен в разной мере, поскольку что для одного интуитивно очевидно, для конкатенации строк достаточно было бы написать "abc" + "def", но я в этом слу-
другого выглядит странно. чае пишу что-то вроде str = "abc" + "def". Кому-то это покажется излишеством,
В основном эта книга призвана отвечать на вопросы типа «Как сделать?». но выглядит естественным для программиста на языке C, привыкшего к тому, что
И потому вы, вероятно, многое будете пропускать. Я почту за честь, если кто-то бывают функции типа void и не-void (а также программисту на Pascal, мысляще-
прочтет книгу от корки до корки, но не надеюсь на это. Скорее я ожидаю, что вы му в терминах процедур и функций).
будете искать в оглавлении темы, которые вас интересуют в конкретный момент. Третий каприз заключается в моем нежелании употреблять символ решетки
Впрочем, с момента выхода первого издания мне приходилось беседовать с разны- для обозначения методов экземпляра. Многие поклонники Ruby считают, что я
ми людьми, и оказалось, что многие прочли книгу целиком. Более того, несколько
проявляю излишнюю болтливость, когда пишу «метод экземпляра crypt класса
человек писали мне, что выучили по ней Ruby. Что ж, все возможно!..
String», а не просто String#crypt, но я полагаю, что так никто не запутается. (На
Некоторые рассматриваемые в книге вопросы могут показаться элементарны-
самом деле мне придется постепенно смириться с использованием такой нотации,
ми. Но ведь у разных людей и опыт разный; то, что очевидно одному, будет от-
так как ясно, что она уже никуда не исчезнет.)
кровением для другого. Я старался сделать изложение как можно более полным.
Я старался давать ссылки на внешние ресурсы там, где это уместно. Ограниче-
С другой стороны, было стремление уложиться в разумный объем (ясно, что эти
цели противоречивы). ния по времени и объему не позволили мне включить в книгу все, что я хотел бы,
Можно назвать эту книгу «справочником наоборот». Вы ищете то, что нужно, но надеюсь, что это хотя бы отчасти компенсируется указаниями на то, где найти
не по имени класса или метода, а по функции или назначению. Например, в классе недостающую информацию. Из всех источников самым главным, наверное, сле-
String есть несколько методов для манипулирования регистром букв: capitalize, дует считать архив приложений Ruby (Ruby Application Archive) в сети; вы не раз
upcase, casecmp, downcase и swapcase. В настоящем справочнике они встречались встретите ссылки на него.
бы в алфавитном порядке, а в этой книге собраны в одном месте. В начале книги принято приводить соглашения об использовании шрифтов,
Конечно, в борьбе за полноту охвата материала я иногда сворачивал на путь, применяемых для выделения кода, и о том, как отличить пример от обычного текста.
которому следуют справочные руководства. Во многих случаях я старался ком- Но я не стану оскорблять вас недоверием к вашим умственным способностям, –
пенсировать это, предлагая не совсем обычные примеры или разнообразя их по вы ведь и раньше читали техническую литературу.
сравнению со справочниками. Хочу подчеркнуть, что примерно 10% текста книги было написано другими
Я старался не перегружать код комментариями. Если не считать первой гла- людьми. И это не считая технического редактирования и корректуры!.. Вы просто
вы, то думаю, что достиг этой цели. Писатель может стать не в меру болтливым, но обязаны прочитать благодарности, приведенные в этой (и любой другой) книге.
программист-то хочет видеть код (а если не хочет, то должен хотеть). Большинство читателей пропускают их. Прошу, прочтите прямо сейчас. Это будет
Иногда примеры выглядят искусственными, за что я приношу свои извине- так же полезно, как питание овощами.
ния. Проиллюстрировать какой-то прием или принцип в отрыве от реальной за-
дачи бывает сложно. Но чем сложнее задача, чем выше ее уровень, тем большие
усилия я прилагал к подысканию реального примера. Так, если речь идет о конка-
Об исходных текстах, приведенных в книге
тенации строк, то, наверное, вы увидите безыскусный фрагмент кода с упоминани- Все сколько-нибудь значительные фрагменты кода собраны в архив, который
ем пресловутых “foo” и “bar”, но когда рассматривается тема разбора XML-доку- можно загрузить из сети. Этот архив есть на сайте www.awprofessional.com и на
мента, будет приведен куда более содержательный и реалистичный пример. моем собственном сайте (www.rubyhacker.com).
Есть в этой книге два-три каприза, в которых хочу заранее сознаться. Во-пер- Он предлагается в виде tgz-файла и в виде zip-файла. При именовании файлов
вых, я всеми силами старался избегать «уродливых» глобальных переменных типа в нем принято следующее соглашение: код, которому в тексте соответствует прону-
$_ и ей подобных, пришедших из языка Perl. Они есть в Ruby и прекрасно работа- мерованный листинг, находится в файле с таким же именем (например, listing14-
ют, даже применяются в повседневной работе всеми или большинством програм- 1.rb). Более короткие фрагменты именуются по номеру страницы, возможно, с до-
мистов. Но почти всегда от их использования можно уйти, что я и позволил себе бавленной буквой (например, p260a.rb и p260b.rb). Совсем короткие фрагменты,
чуть ли не во всех примерах. которые нельзя исполнить «вне контекста», в архиве обычно отсутствуют.
24 Введение «Путь Ruby» 25

«Путь Ruby» «совершенство достигнуто не тогда, когда нечего добавить, а тогда, когда нечего
убрать».
Что мы имеем в виду, говоря о Пути Ruby? Я полагаю, что тут есть два взаимосвя-
Но Ruby – сложный язык. Почему же я называю его простым?
занных аспекта: философия проектирования Ruby и философия использования
Если бы мы лучше понимали мироздание, то, наверное, открыли бы «закон со-
этого языка. Естественно, что дизайн и применение связаны друг с другом, будь
хранения сложности» – факт, который вмешивается в нашу жизнь подобно энтро-
то программное или аппаратное обеспечение. Иначе зачем бы существовала наука
пии, которую мы не можем преодолеть, а способны лишь рассеивать.
эргономика?.. Если я снабжаю устройство ручкой, то, наверное, предполагаю, что
кто-то за эту ручку возьмется. И в этом ключ. Нельзя избежать сложности, но можно укрыться от нее. Мы
В языке Ruby имеется не выразимое словами качество, которое делает его тем, можем убрать ее из виду! Это тот же старый добрый принцип черного ящи-
что он есть. Мы наблюдаем это качество в дизайне синтаксиса и семантики язы- ка, внутри которого решается сложная задача, хотя на поверхности лишь го-
ка, присутствует оно и в написанных на нем программах. Все же стоит попытаться лая простота.
сформулировать, в чем состоит эта отличительная особенность. Если вам еще не наскучили цитаты, то будет уместно привести слова Альбер-
Очевидно, Ruby – не просто инструмент для написания программ, но и сам по та Эйнштейна: «Все должно быть просто настолько, насколько возможно, но не
себе является программой. Почему работа программ, написанных на Ruby, должна проще».
следовать законам, отличным от тех, которым подчинена работа интерпретатора? Таким образом, на взгляд программиста, Ruby – это воплощенная простота
В конце концов, Ruby – исключительно динамичный и расширяемый язык. Мо- (хотя у человека, отвечающего за сопровождение интерпретатора, взгляд может
гут найтись причины, по которым эти два уровня где-то расходятся, вероятно, ста- быть иной). Но вместе с тем имеется пространство для компромиссов. В реаль-
раясь приспособиться к несовершенству реального мира. Но в общем случае мыс- ном мире всем нам приходится немного «прогибаться». К примеру, все сущнос-
лительные процессы могут и должны быть сходными. Интерпретатор Ruby можно ти в программе на Ruby должны были бы быть истинными объектами, однако не-
было бы написать на самом Ruby, в полном соответствии с принципом Хофштад- которые, в том числе целые числа, хранятся как непосредственные значения. Это
тера, хотя в настоящее время это еще не сделано. компромисс, знакомый всем студентам отделений информатики уже много деся-
Мы нечасто задумываемся над этимологией слова «путь», но оно употребля- тилетий: элегантность дизайна приносится в жертву практичности реализации.
ется в двух разных смыслах. Во-первых, это метод или техника, а во-вторых – до- По существу, мы променяли одну простоту на другую.
рога. Ясно, что оба значения взаимосвязаны, и, говоря «путь Ruby», я имею в ви- То, что Ларри Уолл говорил о языке Perl, остается справедливым: «Когда вы
ду и то и другое. хотите что-то выразить на маленьком языке, оно становится большим. А когда вы
Следовательно, мы говорим о мыслительном процессе, но вместе с тем и о до- пытаетесь выразить то же самое на большом языке, оно становится маленьким».
роге, по которой движемся. Даже величайшие гуру программирования не могут Это верно и в отношении английского языка. Если биолог Эрнст Хэккель смог
сказать о себе, что достигли совершенства: они лишь на пути к нему. Таких путей всего тремя словами выразить глубокую мысль «онтогенез повторяет филогенез»,
может быть несколько, но я здесь говорю только об одном. то лишь потому, что эти слова с весьма специфическим смыслом были в его рас-
Привычная мудрость гласит, что форма определяется функцией. Это верно, поряжении. Мы соглашаемся на внутреннюю сложность языка, потому что она по-
спору нет. Однако Фрэнк Ллойд Райт* (имея в виду свою собственную область зволяет избежать сложности в отдельных высказываниях.
интересов) как-то сказал: «Форма определяется функцией, которая была понята Переформулирую этот принцип по-другому: «не пишите 200 строк кода, ког-
неправильно. Форма и функция должны быть едины, сливаться в духовном еди- да достаточно 10».
нении». Я считаю само собой разумеющимся, что краткость в общем случае хороша.
Что Райт имел в виду? Я бы сказал, что на этот вопрос вы найдете ответ не в Короткий фрагмент кода занимает меньше места в мозгу программиста, его проще
книгах, а в собственном опыте. воспринять как единое целое. Благоприятным побочным эффектом следует счи-
Однако я думаю, что Райт выразил эту мысль где-то еще, разбив ее на части, тать и то, что в короткой программе будет меньше ошибок.
которые проще переварить. Он был великим поборником простоты, который од- Конечно, не нужно забывать предупреждение Эйнштейна о простоте. Если
нажды заметил: «Самые полезные инструменты архитектора – это ластик рядом с расположить краткость слишком высоко в списке приоритетов, то получится со-
чертежной доской и гвоздодер на строительной площадке». вершенно загадочный код. Согласно теории информации, сжатые данные статис-
Итак, одним из достоинств Ruby является простота. Надо ли цитировать других тически похожи на белый шум. Если вы видели код на C или APL либо регуляр-
мыслителей, высказывавшихся на эту тему? Согласно Антуану де Сент-Экзюпери, ное выражение – особенно плохо написанные, то понимаете, что я имею в виду.
* Фрэнк Ллойд Райт (1867–1959) – знаменитый архитектор и дизайнер. Одна из самых известных «Просто, но не слишком просто» – это ключевая фраза. Стремитесь к краткости,
работ – Музей Гуггенхайма в Нью-Йорке. (Прим. перев.) но не жертвуйте понятностью.
26 Введение «Путь Ruby» 27

Трюизмом следует считать мысль о том, что краткость в сочетании с понятнос- тем меньше удивления будет вызывать Ruby. И смею уверить, подражание Мацу
тью – это хорошо. Но тому есть причина, причем настолько фундаментальная, что большинству из нас пойдет только на пользу.
мы часто о ней забываем. А состоит она в том, что компьютер создан для человека, Какой бы ни была логическая конструкция системы, тренировать свою инту-
а не человек для компьютера. ицию необходимо. Каждый язык программирования – это отдельный мир со сво-
В старые добрые дни все было почти наоборот. Компьютеры стоили милли- ими допущениями, точно так же как и любой естественный язык. Когда я начал
оны долларов и пожирали многие киловатты электричества. Люди же вели себя изучать немецкий, то обнаружил, что все существительные пишутся с прописной
так, будто компьютер – божество, а программисты – его скромные жрецы. Час ма- буквы... за исключением слова deutsch (немецкий язык). Я пожаловался профес-
шинного времени стоил дороже часа личного времени. сору, подчеркивая, что ведь это же название самого языка. Он улыбнулся и отве-
Когда компьютеры стали меньше и дешевле, приобрели популярность языки тил: «Не надо с этим бороться».
высокого уровня. Они неэффективны с точки зрения машины, зато эффективны с Профессор говорил, что надо позволить немцу оставаться немцем. Продолжая
позиции человека. Ruby – всего лишь одно из последних достижений на этом пу- эту мысль, хочу дать совет всем, кто переходит на использование Ruby после
ти. Некоторые даже называют его языком сверхвысокого уровня (VHLL – Very освоения других языков. Пусть Ruby остается Ruby! Не ожидайте, что это будет
High-Level Language). Хотя этот термин еще не получил четкого определения, я Perl. Не требуйте от него поведения, характерного для языков LISP или Smalltalk.
думаю, что он оправдан. С другой стороны, у Ruby есть элементы, присущие любому из этих трех языков.
Компьютер призван быть слугой, а не хозяином, а, как сказал Мац, толковый Для начала действуйте в соответствии с априорными представлениями, но когда
слуга должен выполнять сложное задание при минимуме указаний. Так было на они оказываются неверны, не боритесь с установленными правилами (если только
протяжении всей истории информатики. Мы начали с машинного языка, перешли Мац не согласится с тем, что в них необходимо внести изменения).
к языку ассемблера, а потом добрались и до языков высокого уровня. Каждый программист сегодня знает о принципе ортогональности (хотя лучше
Мы сейчас говорим о смещении парадигмы: от машиноцентрической к чело-
было бы назвать его принципом ортогональной полноты). Вообразим пару осей,
векоцентрической. На мой взгляд, Ruby дает великолепный пример человекоцен-
по одной из которых откладываются языковые сущности, а по другой – множество
трического программирования.
атрибутов и возможностей. Когда мы говорим об ортогональности, то обычно име-
Теперь я хочу взглянуть на вопрос под несколько иным углом. В 1980 году
ем в виду, что пространство, определяемое этими осями, настолько «полно», на-
вышла чудесная книжка Джеффри Джеймса «Дао программирования» (Geoffrey
сколько это логически возможно.
James, The Tao of Programming). Каждая строчка из нее достойна цитирования, но
Одна из составных частей Пути Ruby – стремление к ортогональности. Мас-
я ограничусь лишь одной выдержкой: «Программа должна следовать “закону на-
сив в некоторых отношениях подобен хэшу, а потому и операции над ними долж-
именьшего удивления”. Что это за закон? Все просто: программа должна отвечать
пользователю так, чтобы вызывать у него как можно меньше удивления». (Конеч- ны быть похожи. До тех пор пока мы не вступаем в область, где эти сущности на-
но, если речь идет об интерпретаторе языка, то пользователем является програм- чинают отличаться друг от друга.
мист.) Мац говорит, что «естественность» важнее ортогональности. Но чтобы понять,
Не знаю, Джеймс ли придумал термин «закон наименьшего удивления», но я что естественно, а что нет, надо долго думать и писать программы.
впервые узнал его из упомянутой книги. Этот закон хорошо известен и часто ци- Ruby стремится быть дружелюбным к программисту. Например, у многих ме-
тируется в сообществе пользователей Ruby. Правда, обычно его называют «при- тодов есть синонимы; оба метода size и length возвращают число элементов в мас-
нципом наименьшего удивления» (Principle of Least Surprise, POLS). Лично я сиве. Два разных написания слова – indexes и indices – относятся к имени одного
упрямо придерживаюсь акронима LOLA – Law of Least Astonishment. и того же метода. Некоторые называют это досадным недоразумением, но я скло-
Но, как ни называй, правило остается справедливым и служит основополага- нен считать такую избыточность хорошим дизайном.
ющим принципом продолжающейся работы над языком Ruby. О нем полезно пом- Ruby стремится к последовательности и единообразию. В этом нет ничего
нить и тем, кто разрабатывает библиотеки и пользовательские интерфейсы. мистического: во всех жизненных ситуациях мы жаждем регулярности и разме-
Конечно, одна проблема остается: разные люди удивляются разным вещам; не ренности. Сложнее научиться понимать, когда этот принцип следует нарушить.
существует всеобщего согласия о том, как «должен» вести себя объект или метод. Например, в Ruby принято добавлять вопросительный знак (?) в конец имени
Но мы можем стремиться быть последовательными и находить веские обоснова- метода, ведущего себя как предикат. Это хорошо и удобно, программа становится
ния принимаемым проектным решениям, а каждый человек должен тренировать яснее, а в пространстве имен легче ориентироваться. Но менее последовательным
собственную интуицию. является аналогичное употребление восклицательного знака для обозначения по-
Кстати, Мац как-то заметил, что «принцип наименьшего удивления» должен тенциально «деструктивных» или «опасных» методов (в том смысле, что они моди-
относиться и к нему как к дизайнеру. Чем больше ваше мышление походит на его, фицируют внутреннее состояние вызывающего объекта). Непоследовательность
28 Введение
состоит в том, что не все деструктивные методы помечаются таким образом. Нуж-
но ли восстановить справедливость?
Нет, на самом деле не нужно. Некоторые методы по сути своей изменяют состо-
яние (например, методы replace и concat класса Array). Одни являются «методами
установки», которые допускают присваивание атрибуту класса; ясно, что не следу-
ет добавлять восклицательный знак к имени атрибута или к знаку равенства. Дру- Глава 1. Обзор Ruby
гие в каком-то смысле изменяют состояние объекта, например read, но это проис-
ходит так часто, что нет смысла особо отмечать данный факт. Если бы имя каждого
деструктивного метода заканчивалось символом !, то программа превратилась бы в Язык формирует способ нашего мышления
рекламную брошюру фирмы, занимающейся многоуровневым маркетингом. и определяет то, о чем мы можем размышлять.
Вы замечаете действие разнонаправленных сил, тенденцию нарушать все пра- Бенджамин Ди Уорф
вила? Тогда позвольте мне сформулировать второй закон Фултона: «У каждого
правила есть исключения, кроме второго закона Фултона». (Доля шутки тут есть, Стоит напомнить, что в новом языке программирования иногда видят панацею,
но небольшая.) особенно его адепты. Но ни один язык не сможет заменить все остальные. Не су-
В Ruby мы видим не «педантичную непротиворечивость», а строгое следова- ществует инструмента, безусловно пригодного для решения любой мыслимой за-
ние набору простых правил. Может быть, отчасти Путь Ruby состоит в том, что его дачи. Есть много предметных областей и много ограничений, налагаемых решае-
подход не является закостенелым и неподвижным. Мац как-то сказал, что при про- мыми в них задачами.
ектировании языка нужно «следовать велениям своего сердца». И еще один аспект А самое главное – есть разные пути обдумывания задач, и это следствие разно-
философии Ruby: «Не бойтесь изменений во время выполнения, не бойтесь быть го опыта и личных качеств самих программистов. Поэтому в обозримой перспек-
динамичными». Мир динамичен, так почему язык программирования должен быть тиве будут появляться все новые и новые языки. А пока есть много языков, будет
статичным? Ruby – один из самых динамичных среди существующих языков. много людей, которые их критикуют и защищают. Короче говоря, «языковым вой-
С некоторыми оговорками я бы выделил и такой аспект: «Не будьте рабом про- нам» конца не предвидится, но мы в этой книге не станем принимать в них учас-
изводительности». Если производительность оказывается недопустимо низкой, тия.
проблему придется решать, но не следует с самого начала выводить ее на первый И тем не менее в постоянном поиске новой, более удачной системы записи
план. Предпочитайте элегантность эффективности в тех случаях, когда эффектив- программ нас иногда озаряют идеи, переживающие контекст, в котором зароди-
ность не слишком критична. Впрочем, когда вы пишете библиотеку, которая бу- лись. Как Pascal многое позаимствовал у Algol, как Java выросла из C, так и каж-
дет использоваться непредвиденными способами, о производительности следует дый язык что-то берет у своих предшественников.
задуматься с самого начала. Язык – это одновременно набор инструментов и площадка для игр. У него есть
Когда я смотрю на язык Ruby, то вижу равновесие между разными проектны- практическая сторона, но он же служит и полигоном для испытания новых идей,
ми целями, вижу сложное взаимодействие, напоминающее о задаче n тел в физике. которые могут быть приняты или отвергнуты сообществом программистов.
Я могу представить себе, что он моделировался как мобил Александра Кальдера. Одна из наиболее далеко идущих идей – концепция объектно-ориентиро-
Быть может, больше всего завораживает само взаимодействие, гармония, лежащая ванного программирования (ООП). Многие скажут, что значимость ООП име-
в основе философии Ruby, а не отдельные составные части. Программисты знают, ет скорее эволюционный, нежели революционный характер, но никто не возразит
что их ремесло – не просто сплав науки и технологии, но еще и искусство. Мне не- против того, что оно оказало огромное влияние на индустрию. Двадцать пять лет
ловко говорить, что в компьютерных дисциплинах есть какой-то духовный аспект, назад объектная ориентированность представляла в основном академический ин-
но – строго между нами! – он безусловно присутствует. (Если вы не читали книгу терес; сегодня это универсально принятая парадигма.
Роберта Пирсига «Дзен и искусство ухода за мотоциклом» (Robert Pirsig, Zen and Вездесущность ООП породила много «рекламной чепухи» в индустрии. В
the Art of Motorcycle Maintenance), горячо рекомендую.) классической работе, написанной в конце 1980-х годов, Роджер Кинг отметил:
Источником Ruby стала человеческая потребность создавать полезные и кра- «Если вы хотите продать кошку специалисту по компьютерам, скажите, что она
сивые вещи. Программы, написанные на Ruby, должны проистекать из того же бо- объектно-ориентированная». Мнения по поводу того, что на самом деле представ-
говдохновенного источника. Это, на мой взгляд, и является квинтэссенцией Пу- ляет собой ООП, весьма неоднородны, и даже среди тех, кто разделяет общую точ-
ти Ruby. ку зрения, имеются разногласия относительно терминологии.
30 Обзор Ruby Введение в ООП 31

Мы не ставим себе целью поучаствовать в спорах. Мы согласны, что ООП – три объекта этого класса: fido, rover и spot. У каждой собаки могут быть такие ат-
полезный инструмент и ценная методология решения задач; мы не утверждаем, рибуты, как возраст и дата вакцинации. Предположим, однако, что нужно сохра-
что она способна излечить рак. нить еще и имя владельца всех собак. Можно, конечно, поместить его в каждый
Что касается истинной природы ООП, то у нас есть любимые определения и объект, но это пустая трата памяти, к тому же искажающая смысл дизайна. Ясно,
термины, но мы пользуемся ими лишь для эффективного общения, так что прере- что атрибут «имя владельца» принадлежит не отдельному объекту, а классу в це-
каться по поводу смысла слов не станем. лом. Такие атрибуты (синтаксис их определения в разных языках различен) назы-
Обо всем этом пришлось сказать лишь потому, что знакомство с основами ООП ваются атрибутами класса (или переменными класса).
необходимо для чтения этой книги и понимания примеров и подходов. Что бы ни Есть немало ситуаций, в которых может понадобиться переменная класса. До-
говорили о Ruby, он безусловно является объектно-ориентированным языком. пустим, например, что нужно знать, сколько всего было создано объектов неко-
торого класса. Можно было бы завести переменную класса, инициализировать ее
1.1. Введение в объектно-ориентированное нулем и увеличивать на единицу при создании каждого объекта. Эта переменная
ассоциирована именно с классом, а не с каким-то конкретным объектом. С точки
программирование зрения области видимости она не отличается от любого другого атрибута, но су-
Прежде чем начать разговор о самом языке Ruby, неплохо было бы потолковать ществует лишь одна ее копия для всего множества объектов данного класса.
об объектно-ориентированном программировании вообще. Поэтому сейчас мы Чтобы отличить атрибуты класса от обыкновенных атрибутов, последние час-
вкратце рассмотрим общие идеи, лишь слегка касаясь Ruby. то называют атрибутами объекта (или атрибутами экземпляра). Условимся, что
в этой книге под словом «атрибут» понимается атрибут экземпляра, если явно не
1.1.1. Что такое объект оговорено, что это атрибут класса.
В объектно-ориентированном программировании объект – фундаментальное по- Точно так же методы объекта служат для управления доступом к его атрибу-
нятие. Объект – это сущность, служащая контейнером для данных и управляющая там и предоставляют четко определенный интерфейс для этой цели. Но иногда же-
доступом к этим данным. С объектом связан набор атрибутов, которые в сущности лательно или даже необходимо определить метод, ассоциированный с самим клас-
представляют собой просто переменные, принадлежащие объекту. (В этой книге сом. Неудивительно, что метод класса управляет доступом к переменным класса,
мы будем без стеснения употреблять привычный термин «переменная» в приме- кроме того, выполняя действия, распространяющиеся на весь класс, а не на какой-
нении к атрибутам.) Кроме того, с объектом ассоциирован набор функций, предо- то конкретный объект. Как и в случае с атрибутами, мы будем считать, что метод
ставляющих интерфейс к функциональным возможностям объекта. Эти функции принадлежит объекту, если явно не оговорено противное.
называются методами. Стоит отметить, что в некотором смысле все методы являются методами клас-
Важно отметить, что любой объектно-ориентированный язык предоставляет са. Не нужно думать, что, создав сто объектов, мы породили сотню копий кода ме-
механизм инкапсуляции. В общепринятом смысле это означает, во-первых, что тодов! Однако правила ограничения области видимости гласят, что метод каждо-
атрибуты и методы объекта ассоциированы именно с этим объектом, а во-вторых, го объекта оперирует данными только того объекта, от имени которого вызван.
что область видимости атрибутов и методов по умолчанию ограничена самим объ- Тем самым у нас создается иллюзия, будто методы объекта ассоциированы с са-
ектом (применение принципа сокрытия информации). мими объектами.
Объект считается экземпляром класса объекта (обычно он называется просто
классом). Класс можно представлять себе как чертеж или образец, а объект – как 1.1.2. Наследование
вещь, изготовленную по этому чертежу. Также класс часто называют абстрактным Мы подходим к одной из самых сильных сторон ООП – наследованию. Наследо-
типом, то есть типом более сложным, нежели целое или строка символов. вание – это механизм, позволяющий расширять ранее определенную сущность пу-
Создание объекта (экземпляра класса) называют инстанцированием. В неко- тем добавления новых возможностей. Короче говоря, наследование – это способ
торых языках имеются явные конструкторы и деструкторы – функции, выполня- повторного использования кода. (Простой и эффективный механизм повторно-
ющие действия, необходимые соответственно для инициализации и уничтожения го использования долго был Святым Граалем в информатике. Много десятилетий
объекта. Отметим попутно, что в Ruby есть нечто, что можно назвать конструкто- назад его поиски привели к изобретению параметризованных процедур и библио-
ром, но никакого аналога деструктора не существует (благодаря наличию меха- тек. ООП – лишь одна из последних попыток реализации искомого.)
низма сборки мусора). Обычно наследование рассматривается на уровне класса. Если нам необходим
Иногда возникает ситуация, когда некоторые данные имеют широкую область какой-то класс, а в наличии имеется более общий, то можно определить свой класс
видимости, не ограниченную одним объектом, и помещать копию такого атрибута так, чтобы он наследовал поведение уже существующего. Предположим, напри-
в каждый экземпляр класса неправильно. Рассмотрим, к примеру, класс MyDogs и мер, что есть класс Polygon, описывающий выпуклые многоугольники. Тогда класс
32 Обзор Ruby Введение в ООП 33

прямоугольника Rectangle можно унаследовать от Polygon. При этом Rectangle своего родителя? Получает ли Bat две копии? Или они должны быть объединены
будет иметь все атрибуты и методы класса Polygon. Так, может уже быть написан в один атрибут, поскольку все равно заимствованы у общего предка?
метод, вычисляющий периметр путем суммирования длин всех сторон. Если все Это скорее проблемы проектировщика языка, а не программиста. В разных объ-
было реализовано правильно, этот метод автоматически будет работать и для но- ектно-ориентированных языках они решаются по-разному. Иногда вводятся пра-
вого класса; переписывать код не придется. вила, согласно которым какое-то одно определение атрибута «выигрывает». Либо
Если класс B наследует классу A, мы говорим, что B является подклассом A, а же предоставляется возможность различать одноименные атрибуты. Иногда да-
A – суперкласс B. По-другому говорят, что A – базовый или родительский класс, а же язык позволяет вводить псевдонимы или переименовывать идентификаторы.
B – производный или дочерний класс. Многими это рассматривается как аргумент против множественного наследова-
Как мы видели, производный класс может трактовать методы своего базового ния – о механизмах разрешения подобных конфликтов имен нет единого мнения,
класса как свои собственные. С другой стороны, он может переопределить метод поэтому все они «языкозависимы». В языке C++ предлагается минимальный на-
базового класса, предоставив иную его реализацию. Кроме того, в большинстве бор средств для разрешения неоднозначностей; механизмы языка Eiffel, наверное,
языков есть возможность вызвать из переопределенного метода метод базового получше, а в Perl проблема решается совсем по-другому.
класса с тем же именем. Иными словами, метод foo класса B знает, как вызвать ме- Есть и альтернатива – полностью запретить множественное наследование. Та-
тод foo класса A. (Любой язык, не предоставляющий такого механизма, можно за- кой подход принят в языках Java и Ruby. На первый взгляд, это даже не назовешь
подозрить в отсутствии истинной объектной ориентированности.) То же верно и компромиссным решением, но, вскоре мы убедимся, что все не так плохо, как ка-
в отношении атрибутов. жется. Мы познакомимся с приемлемой альтернативой традиционному множест-
Отношение между классом и его суперклассом интересно и важно, обычно венному наследованию, но сначала обсудим полиморфизм – еще одно понятие из
его называют отношением «является». Действительно, квадрат Square «являет- арсенала ООП.
ся» прямоугольником Rectangle, а прямоугольник Rectangle «является» мно-
гоугольником Polygon и т. д. Поэтому, рассматривая иерархию наследования (а 1.1.3. Полиморфизм
такие иерархии в том или ином виде присутствуют в любом объектно-ориенти- Термин «полиморфизм», наверное, вызывает самые жаркие семантические спо-
рованном языке), мы видим, что в любой ее точке специализированные сущности ры. Каждый знает, что это такое, но все понимают его по-разному. (Не так давно
«являются» подклассами более общих. Отметим, что это отношение транзитив- вопрос «Что такое полиморфизм?» стал популярным во время собеседования при
но, – если обратиться к предыдущему примеру, то квадрат «является» много- поступлении на работу. Если его зададут вам, рекомендую процитировать какого-
угольником. Однако отношение «является» не коммутативно – каждый прямо- нибудь эксперта, например Бертрана Мейера или Бьерна Страуструпа; если собе-
угольник есть многоугольник, но не каждый многоугольник – прямоугольник. седник не согласится, то пусть он спорит с классиком, а не с вами.)
Это подводит нас к теме множественного наследования. Можно предста- Буквально слово «полиморфизм» означает «способность принимать разные
вить себе класс, который наследует нескольким классам. Например, классы Dog формы или обличья». В самом широком смысле так называют ситуацию, когда
(Собака) и Cat (Кошка) могут наследовать классу Mammal (Млекопитающее), а различные объекты по-разному отвечают на одно и то же сообщение или вызов
Sparrow (Воробей) и Raven (Ворон) – классу WingedCreature (Крылатое). Но как метода.
быть с классом Bat (ЛетучаяМышь)? Он с равным успехом может наследовать Дамиан Конвей (Damian Conway) в книге «Object-Oriented Perl» проводит
и Mammal, и WingedCreature! Это хорошо согласуется с нашим жизненным опы- смысловое различие между двумя видами полиморфизма. Первый, наследствен-
том, ведь многие вещи можно отнести не к одной категории, а сразу к несколь- ный полиморфизм, – то, что имеет в виду большинство программистов, говорящих
ким, не вложенным друг в друга. о полиморфизме.
Множественное наследование, вероятно, наиболее противоречивая часть ООП. Если некоторый класс наследует своему суперклассу, то по определению все
Некоторые указывают на потенциальные неоднозначности, требующие разреше- методы суперкласса присутствуют также и в подклассе. Таким образом, цепочка
ния. Например, если в обоих классах Mammal и WingedCreature имеется атрибут size наследования представляет собой линейную иерархию классов, отвечающих на
(размер) или метод eat (есть), то какой из них имеется в виду, когда мы обращаем- одни и те же методы. Нужно, конечно, помнить, что в любом подклассе метод мо-
ся к нему из объекта класса Bat? С этой трудностью тесно связана проблема ром- жет быть переопределен; именно это и составляет сильную сторону наследования.
бовидного наследования; она называется так из-за формы диаграммы наследова- При вызове метода объекта обычно отвечает либо метод, унаследованный от су-
ния, возникающей, когда оба суперкласса наследуют одному классу. Представьте перкласса, либо более специализированный вариант этого метода, созданный в
себе, что классы Mammal и WingedCreature наследуют общему предку Organism (Ор- интересах именно данного подкласса.
ганизм); тогда иерархия наследования от Organism к Bat будет иметь форму ром- В языках со статической типизацией, например в C++, наследственный поли-
ба. Но как быть с атрибутами, которые оба промежуточных класса наследуют от морфизм гарантирует совместимость типов вниз по цепочке наследования (но не
34 Обзор Ruby Базовый синтаксис и семантика Ruby 35

в обратном направлении). Скажем, если B наследует A, то указатель на объект клас- рассматриваются как экземпляры. В таких языках, как Java, C++ и Eiffel, дело об-
са A может указывать и на объект класса B; обратное же неверно. Совместимость стоит иначе. В них примитивные типы (особенно константы) не являются насто-
типов – существенная черта ООП в подобных языках, можно даже сказать, что ящими объектами, хотя иногда могут рассматриваться как таковые с помощью
полиморфизм ей и исчерпывается. Но, конечно же, полиморфизм можно реализо- «классов-оберток». Вероятно, есть языки, которые более радикально объектно
вать и в отсутствие статической типизации (как в Ruby). ориентированы, чем Ruby, но их немного.
Второй вид полиморфизма, упомянутый Конвеем, – это интерфейсный поли- Большинство объектно-ориентированных языков статично; методы и атри-
морфизм. Для него не требуется наличия отношения наследования между клас- буты, принадлежащие классу, глобальные переменные и иерархия наследования
сами; нужно лишь, чтобы в интерфейсах объектов были методы с одним и тем же определяются во время компиляции. Быть может, самый сложный концептуаль-
именем. Такие объекты можно трактовать как принадлежащие одному виду, и по- ный переход заключается в том, что в Ruby все это происходит динамически. И
тому мы имеем некую разновидность полиморфизма (хотя в большинстве работ определения, и даже порядок наследования можно задавать во время исполнения.
он так не называется). Честно говоря, каждое объявление или определение исполняется во время рабо-
Читатели, знакомые с языком Java, понимают, что в нем реализованы оба ви- ты программы. Помимо прочих достоинств, это позволяет избавиться от условной
да полиморфизма. Класс в Java может расширять другой класс, наследуя ему с по- компиляции, и во многих случаях получается более эффективный код.
мощью ключевого слова extends, а может с помощью ключевого слова implements На этом мы завершаем беглую экскурсию в мир ООП. Мы старались последо-
реализовывать интерфейс, за счет чего приобретает заранее известный набор ме- вательно применять введенные здесь термины на протяжении всей книги. Перей-
тодов (которые необходимо переопределить). Такой синтаксис позволяет интер- дем теперь к краткому обзору самого языка Ruby.
претатору Java во время компиляции определить, можно ли вызывать данный ме-
тод для конкретного объекта.
Ruby поддерживает интерфейсный полиморфизм, но по-другому. Он позволя-
1.2. Базовый синтаксис и семантика Ruby
ет определять модули, методы которых допускается «подмешивать» к существую- Выше мы отметили, что Ruby – настоящий динамический объектно-ориентиро-
щим классам. Но обычно модули так не используются. Модуль состоит из методов ванный язык.
и констант, которые можно использовать так, будто они являются частью класса Прежде чем переходить к обзору синтаксиса и семантики, упомянем некото-
или объекта. Когда модуль подмешивается с помощью предложения include, мы рые другие его особенности.
получаем ограниченную форму множественного наследования. (По словам проек- Ruby – прагматичный (agile) язык. Он пластичен и поощряет частую перера-
тировщика языка Юкихиро Мацумото, это можно рассматривать как одиночное ботку (рефакторинг), которая выполняется без особого труда.
наследование с разделением реализации.) Таким образом удается сохранить пре- Ruby – интерпретируемый язык. Разумеется, в будущем ради повышения про-
имущества множественного наследования, не страдая от его недостатков. изводительности могут появиться и компиляторы Ruby, но мы считаем, что у ин-
терпретатора много достоинств. Он не только позволяет быстро создавать прото-
1.1.4. Еще немного терминов типы, но и сокращает весь цикл разработки.
В языках, подобных C++, существует понятие абстрактного класса. Такому клас- Ruby ориентирован на выражения. Зачем писать предложение, когда выраже-
су разрешается наследовать, но создать его экземпляр невозможно. В более дина- ния достаточно? Это означает, в частности, что программа становится более ком-
мичном языке Ruby такого понятия нет, но если программист пожелает, то может пактной, поскольку общие части выносятся в отдельное выражение и повторения
смоделировать его, потребовав, чтобы все методы были переопределены в произ- удается избежать.
водных классах. Полезно это или нет, оставляем на усмотрение читателя. Ruby – язык сверхвысокого уровня (VHLL). Один из принципов, положен-
Создатель языка C++ Бьерн Страуструп определяет также понятие конкретно- ных в основу его проектирования, заключается в том, что компьютер должен ра-
го типа. Это класс, существующий только для удобства. Он спроектирован не для ботать для человека, а не наоборот. Под «плотностью» Ruby понимают тот факт,
наследования; более того, ожидается, что ему никто никогда наследовать не будет. что сложные, запутанные операции можно записать гораздо проще, чем в языках
Другими словами, преимущества ООП в этом случае сводятся только к инкапсу- более низкого уровня.
ляции. Ruby не поддерживает такой конструкции синтаксически (как и C++), но Начнем мы с рассмотрения общего духа языка и некоторых применяемых в
по природе своей прекрасно приспособлен для создания подобных классов. нем терминов. Затем вкратце обсудим природу программ на Ruby, а потом уже пе-
Считается, что некоторые языки поддерживают более «чистую» модель ООП, рейдем к примерам.
чем другие. (К ним мы применяем термин «радикально объектно-ориентирован- Прежде всего отметим, что программа на Ruby состоит из отдельных строк, –
ный».) Это означает, что любая сущность в языке является объектом, даже прими- как в C, но не как в «древних» языках наподобие Фортрана. В одной строке мо-
тивные типы представлены полноценными классами, а переменные и константы жет быть сколько угодно лексем, лишь бы они правильно отделялись пропусками.
36 Обзор Ruby Базовый синтаксис и семантика Ruby 37

В одной строке может быть несколько предложений, разделенных точками с запя- 1.2.2. Комментарии и встроенная документация
той; только в этом случае точка с запятой и необходима. Логическая строка может Комментарии в Ruby начинаются со знака решетки (#), находящегося вне строки
быть разбита на несколько физических при условии, что все, кроме последней, за- или символьной константы, и продолжаются до конца строки:
канчиваются обратной косой чертой или лексическому анализатору дан знак, что
x = y + 5 # Это комментарий.
предложение еще не закончено. Таким знаком может, например, быть запятая в # Это тоже комментарий.
конце строки. print "# А это не комментарий."
Главной программы как таковой (функции main) не существует; исполнение
Предполагается, что встроенная документация будет извлечена из програм-
происходит сверху вниз. В более сложных программах в начале текста могут рас-
мы каким-нибудь внешним инструментом. С точки зрения интерпретатора это
полагаться многочисленные определения, за которыми следует (концептуально)
главная программа. Но даже в этом случае программа исполняется сверху вниз, обычный комментарий. Весь текст, расположенный между строками, которые на-
так как в Ruby все определения исполняются. чинаются с лексем =begin и =end (включительно), игнорируется интерпретатором
(этим лексемам не должны предшествовать пробелы).
1.2.1. Ключевые слова и идентификаторы =begin
Ключевые (или зарезервированные) слова в Ruby обычно не применяются ни для Назначение этой программы –
каких иных целей. Вот их полный перечень: излечить рак
BEGIN END alias and begin и установить мир во всем мире.
break case class def defined? =end
do else elsif end ensure
false for if in module
1.2.3. Константы, переменные и типы
next nil not or redo В Ruby переменные не имеют типа, однако объекты, на которые переменные ссы-
rescue retry return self super лаются, тип имеют. Простейшие типы – это символ, число и строка.
then true undef unless until Числовые константы интуитивно наиболее понятны, равно как и строки. В
when while yield общем случае строка, заключенная в двойные кавычки, допускает интерполяцию
Имена переменных и других идентификаторов обычно начинаются с буквы выражений, а заключенная в одиночные кавычки интерпретируется почти бук-
или специального модификатора. Основные правила таковы: вально – в ней распознается только экранированная обратная косая черта.
• имена локальных переменных (и таких псевдопеременных, как self и nil) Ниже показана «интерполяция» переменных и выражений в строку, заклю-
начинаются со строчной буквы или знака подчеркивания _; ченную в двойные кавычки:
• имена глобальных переменных начинаются со знака доллара $; a = 3
• имена переменных экземпляра (принадлежащих объекту) начинаются со b = 79
знака «собачки» @; puts "#{a} умноженное на #{b} = #{a*b}" # 3 умноженное на 79 = 237
• имена переменных класса (принадлежащих классу) предваряются двумя Более подробная информация о литералах (числах, строках, регулярных вы-
знаками @ (@@); ражениях и т. п.) приведена в следующих главах.
• имена констант начинаются с прописной буквы; Стоит упомянуть особую разновидность строк, которая полезна прежде всего
• в именах идентификаторов знак подчеркивания _ можно использовать на- в небольших сценариях, применяемых для объединения более крупных программ.
равне со строчными буквами; Строка, выводимая программой, посылается операционной системе в качестве
• имена специальных переменных, начинающиеся со знака доллара (напри- подлежащей исполнению команды, а затем результат выполненной команды под-
мер, $1 и $/), здесь не рассматриваются. ставляется обратно в строку. В простейшей форме для этого применяются строки,
Примеры: заключенные в обратные кавычки. В более сложном варианте используется син-
• локальные переменные alpha, _ident, some_var; таксическая конструкция %x:
• псевдопеременные self, nil, __FILE__; 'whoami'
• константы K6chip, Length, LENGTH; 'ls -l'
• переменные экземпляра @foobar, @thx1138, @NOT_CONST; %x[grep -i meta *.html | wc -l]
• переменные класса @@phydeaux, @@my_var, @@NOT_CONST; Регулярные выражения в Ruby похожи на символьные строки, но использу-
• глобальные переменные $beta, $B12vitamin, $NOT_CONST. ются по-другому. Обычно в качестве ограничителя выступает символ косой черты.
38 Обзор Ruby Базовый синтаксис и семантика Ruby 39

Синтаксис регулярных выражений в Ruby и Perl имеет много общего. Подроб- К содержимому хэша-переменной доступ осуществляется так же, как для мас-
нее о регулярных выражениях см. главу 3. сивов, – с помощью квадратных скобок:
Массивы в Ruby – очень мощная конструкция; они могут содержать данные print phone_numbers["Jenny"]
любого типа. Более того, в одном массиве можно хранить данные разных типов. plurals["octopus"] = "octopi"
В главе 8 мы увидим, что все массивы – это экземпляры класса Array, а потому к Однако следует подчеркнуть, что у массивов и хэшей много методов, именно
ним применимы разнообразные методы. Массив-константа заключается в квад- они и делают эти контейнеры полезными. Ниже, в разделе «ООП в Ruby», мы рас-
ратные скобки. Примеры: кроем эту тему более подробно.
[1, 2, 3]
[1, 2, "застегни мне молнию на сапоге"] 1.2.4. Операторы и приоритеты
[1, 2, [3,4], 5] Познакомившись с основными типами данных, перейдем к операторам в языке
["alpha", "beta", "gamma", "delta"] Ruby. В приведенном ниже списке они представлены в порядке убывания приори-
Во втором примере показан массив, содержащий целые числа и строки. В тре- тета:
тьем примере мы видим вложенный массив, а в четвертом – массив строк. Как и
:: Разрешение области видимости
в большинстве других языков, нумерация элементов массива начинается с нуля.
[] Взятие индекса
Так, в последнем из показанных выше примеров элемент "gamma" имеет индекс 2.
** Возведение в степень
Все массивы динамические, задавать размер при создании не нужно.
+ - ! ~ Унарный плюс/минус, НЕ ...
Поскольку массивы строк встречаются очень часто (а набирать их неудобно),
* / % Умножение, деление ...
для них предусмотрен специальный синтаксис:
+ - Сложение/вычитание
%w[alpha beta gamma delta]
<< >> Логические сдвиги ...
%w(Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec)
%w/am is are was were be being been/ & Поразрядное И
| ^ Поразрядное ИЛИ, исключающее ИЛИ
Здесь не нужны ни кавычки, ни запятые; элементы разделяются пробелами.
> >= < <= Сравнение
Если встречаются элементы, содержащие внутренние пробелы, такой синтаксис,
== === <=> != =~ !~ Равенство, неравенство ...
конечно, неприменим.
&& Логическое И
Для доступа к конкретному элементу массива по индексу применяются квад-
|| Логическое ИЛИ
ратные скобки. Результирующее выражение можно получить или выполнить для
.. ... Операторы диапазона
него присваивание:
= (also +=, -=, ...) Присваивание
val = myarray[0]
?: Тернарный выбор
print stats[j]
x[i] = x[i+1]
not Логическое отрицание
and or Логическое И, ИЛИ
Еще одна «могучая» конструкция в Ruby – это хэш. Его также называют ассо-
циативным массивом или словарем. Хэш – это множество пар данных; обыкновен- Некоторые из перечисленных символов служат сразу нескольким целям. На-
но он применяется в качестве справочной таблицы или как обобщенный массив, пример, оператор << обозначает поразрядный сдвиг влево, но также применяется
в котором индекс не обязан быть целым числом. Все хэши являются экземпляра- для добавления в конец (массива, строки и т. д.) и как маркер встроенного докумен-
ми класса Hash. та. Аналогично знак + означает сложение чисел и конкатенацию строк. Ниже мы
Хэш-константа, как правило, заключается в фигурные скобки, а ключи отделя- увидим, что многие операторы – это просто сокращенная запись вызова методов.
ются от значений символом =>. Ключ можно считать индексом для доступа к ассо- Итак, мы определили большую часть типов данных и многие из возможных
циированному с ним значению. На типы ключей и значений не налагается ника- над ними операций. Прежде чем двигаться дальше, приведем пример программы.
ких ограничений. Примеры:
1.2.5. Пример программы
{1=>1, 2=>4, 3=>9, 4=>16, 5=>25, 6=>36}
{"cat"=>"cats", "ox"=>"oxen", "bacterium"=>"bacteria"} В любом руководстве первой всегда приводят программу, печатающую строку
{"водород"=>1, "гелий"=>2, "углерод"=>12} Hello, world!, но мы рассмотрим что-нибудь более содержательное. Вот неболь-
{"нечетные"=>[1,3,5,7], "четные"=>[2,4,6,8]} шая интерактивная консольная программа, позволяющая переводить температу-
{"foo"=>123, [4,5,6]=>"my array", "867-5309"=>"Jenny"} ру из шкалы Фаренгейта в шкалу Цельсия и наоборот.
40 Обзор Ruby Базовый синтаксис и семантика Ruby 41

print "Введите температуру и шкалу (C or F): " str. При вызовах методов в Ruby обычно можно опускать скобки: print "foo" и
str = gets print("foo") – одно и то же.
exit if str.nil? or str.empty? В переменной str хранится символьная строка, но могли бы храниться данные
str.chomp!
любого другого типа. В Ruby данные имеют тип, а переменные – нет. Переменная
temp, scale = str.split(" ")
начинает существовать, как только интерпретатор распознает присваивание ей;
abort "#{temp} недопустимое число." if temp !~ /-?\d+/ никаких предварительных объявлений не существует.
Метод exit завершает программу. В той же строке мы видим управляющую
temp = temp.to_f конструкцию, которая называется «модификатор if». Он аналогичен предложе-
case scale нию if, существующему в большинстве языков, только располагается после дей-
when "C", "c" ствия. Для модификатора if нельзя задать ветвь else, и он не требует закрытия.
f = 1.8*temp + 32 Что касается условия, мы проверяем две вещи: имеет ли переменная str значение
when "F", "f" (то есть не равна nil) и не является ли она пустой строкой. Если встретится конец
c = (5.0/9.0)*(temp-32)
файла, то будет истинно первое условие; если же пользователь нажмет клавишу
else
abort "Необходимо задать C или F."
Enter, не введя никаких данных, – второе.
end Это предложение можно было бы записать и по-другому:
exit if not str or not str[0]
if f.nil? Эти проверки работают потому, что переменная может иметь значение nil, а
print "#{c} градусов C\n" nil в Ruby в логическом контексте вычисляется как «ложно». На самом деле как
else
«ложно» вычисляются nil и false, а все остальное – как «истинно». Это означает,
print "#{f} градусов F\n"
end кстати, что пустая строка "" и число 0 – не «ложно».
В следующем предложении над строкой выполняется операция chomp! (для
Ниже приведены примеры прогона этой программы. Показано, как она пере-
удаления хвостового символа новой строки). Восклицательный знак в конце пре-
водит градусы Фаренгейта в градусы Цельсия и наоборот, а также как обрабаты-
дупреждает, что операция изменяет значение самой строки, а не возвращает но-
вает неправильно заданную шкалу или число: вую. Восклицательный знак применяется во многих подобных ситуациях как на-
Введите температуру и шкалу (C or F): 98.6 F поминание программисту о том, что у метода есть побочное действие или что он
37.0 градусов C
более «опасен», чем аналогичный метод без восклицательного знака. Так, метод
chomp возвращает такой же результат, но не модифицирует значение строки, для
Введите температуру и шкалу (C or F): 100 C
212.0 градусов F которой вызван.
В следующем предложении мы видим пример множественного присваивания.
Введите температуру и шкалу (C or F): 92 G Метод split разбивает строку на куски по пробелам и возвращает массив. Двум
Необходимо задать C или F. переменным в левой части оператора присваиваются значения первых двух эле-
ментов массива в правой части.
Введите температуру и шкалу (C or F): junk F В следующем предложении if с помощью простого регулярного выражения
junk недопустимое число. выясняется, введено ли допустимое число. Если строка не соответствует образцу,
Теперь рассмотрим, как эта программа работает. Все начинается с предложе- который состоит из необязательного знака «минус» и одной или более цифр, то
ния print, которое есть не что иное, как вызов метода print из модуля Kernel. Дан- число считается недопустимым и программа завершается. Отметим, что предло-
ный метод выполняет печать на стандартный вывод. Это самый простой способ ос- жение if оканчивается ключевым словом end. Хотя в данном случае это не нужно,
тавить курсор в конце строки. мы могли бы включить перед end ветвь else. Ключевое слово then необязательно;
Далее мы вызываем метод gets (прочитать строку из стандартного ввода) и в этой книге мы стараемся не употреблять его.
присваиваем полученное значение переменной str. Для удаления хвостового сим- Метод to_f преобразует строку в число с плавающей точкой. Это число запи-
вола новой строки вызывается метод chomp!. сывается в ту же переменную temp, в которой раньше хранилась строка.
Обратите внимание, что print и gets, которые выглядят как «свободные» Предложение case выбирает одну из трех ветвей: пользователь указал C, F или
функции, на самом деле являются методами класса Object (который, вероятно, какое-то другое значение в качестве шкалы. В первых двух случаях выполняется
наследует Kernel). Точно так же chomp! – метод, вызываемый от имени объекта вычисление, в третьем мы печатаем сообщение об ошибке и выходим.
42 Обзор Ruby Базовый синтаксис и семантика Ruby 43

Кстати, предложение case в Ruby позволяет гораздо больше, чем показано в then можно опускать во всех случаях, кроме последнего (предназначенного для
примере. Нет никаких ограничений на типы данных, а все выражения могут быть использования в выражениях). Также заметьте, что в модификаторах (третья стро-
произвольно сложными, в том числе диапазонами или регулярными выражениями. ка) ветви else быть не может.
В самом вычислении нет ничего интересного. Но обратите внимание, что пе- Предложение case в Ruby позволяет больше, чем в других языках. В его вет-
ременные c и f впервые встречаются внутри ветвей case. В Ruby нет никаких объ- вях можно проверять различные условия, а не только сравнивать на равенство.
явлений – переменная начинает существовать только в результате присваивания. Так, например, разрешено сопоставление с регулярным выражением. Проверки в
А это означает, что после выхода из case лишь одна из переменных c и f будет предложении case эквивалентны оператору ветвящегося равенства (===), поведе-
иметь действительное значение. ние которого зависит от объекта. Рассмотрим пример:
Мы воспользовались этим фактом, чтобы понять, какая ветвь исполнялась, и case "Это строка символов."
в зависимости от этого вывести то или другое сообщение. Сравнение f с nil по- when "одно значение"
puts "Ветвь 1"
зволяет узнать, есть ли у переменной осмысленное значение. Этот прием приме-
when "другое значение"
нен только для демонстрации возможности: ясно, что при желании можно было puts "Ветвь 2"
бы поместить печать прямо внутрь предложения case. when /симв/
Внимательный читатель заметит, что мы пользовались только «локальными» puts "Ветвь 3"
переменными. Это может показаться странным, так как, на первый взгляд, их об- else
ластью видимости является вся программа. На самом деле они локальны относи- puts "Ветвь 4"
тельно верхнего уровня программы. Глобальными они кажутся лишь потому, что end
в этой простой программе нет контекстов более низкого уровня. Но если бы мы Этот код напечатает Ветвь 3. Почему? Сначала проверяемое выражение срав-
объявили какие-нибудь классы или методы, то в них переменные верхнего уров- нивается на равенство с двумя строками: "одно значение" и "другое значение". Эта
ня были бы не видны. проверка завершается неудачно, поэтому мы переходим к третьей ветви. Там нахо-
дится образец, с которым сопоставляется выражение. Поскольку оно соответству-
1.2.6. Циклы и ветвление ет образцу, то выполняется предложение print. В ветви else обрабатывается слу-
Потратим немного времени на изучение управляющих конструкций. Мы уже ви- чай, когда ни одна из предшествующих проверок не прошла.
дели простое предложение if и модификатор if. Существуют также парные струк- Если проверяемое выражение – целое число, то его можно сравнивать с цело-
туры, в которых используется ключевое слово unless (в них также может присут- численным диапазоном (например, 3..8); тогда проверяется, что число попадает в
ствовать необязательная ветвь else), а равно применяемые в выражениях формы диапазон. В любом случае выполняется код в первой подошедшей ветви.
if и unless. Все они сведены в таблицу 1.1. В Ruby имеется богатый набор циклических конструкций. К примеру, while
и until – циклы с предварительной проверкой условия, и оба работают привыч-
Таблица 1.1. Условные предложения ным образом: в первом случае задается условие продолжения цикла, а во втором –
условие завершения. Есть также их формы с модификатором, как для предло-
Формы с if Формы с unless
жений if и unless. Кроме того, в модуле Kernel есть метод loop (по умолчанию
if x < 5 then unless x >= 5 then бесконечный цикл), а в некоторых классах реализованы итераторы.
statement1 statement1 В примерах из таблицы 1.2 предполагается, что где-то определен такой мас-
end end сив list:
if x < 5 then unless x < 5 then
statement1 statement2 list = %w[alpha bravo charlie delta echo];
else else В цикле этот массив обходится и печатается каждый его элемент.
statement2 statement1
end end Таблица 1.2. Циклы
statement1 if y == 3 statement1 unless y != 3
# Цикл 1 (while) # Цикл 2 (until)
x = if a>0 then b else c end x = unless a<=0 then c else b end
i=0 i=0
while i < list.size do until i == list.size do
print "#{list[i]} " print "#{list[i]} "
Здесь формы с ключевыми словами if и unless, расположенные в одной стро- i += 1 i += 1
ке, выполняют в точности одинаковые функции. Обратите внимание, что слово end end
44 Обзор Ruby Базовый синтаксис и семантика Ruby 45

Таблица 1.2. Циклы В циклах 7 и 8 используется тот факт, что у массива есть числовой индекс.
Итератор times исполняется заданное число раз, а итератор upto увеличивает свой
# Цикл 3 (for) # Цикл 4 (итератор 'each') параметр до заданного значения. И тот, и другой для данной ситуации приспособ-
for x in list do list.each do |x| лены плохо.
print "#{x} " print "#{x} " Цикл 9 – это вариант цикла for, предназначенный специально для работы со
end end
значениями индекса при помощи указания диапазона. В цикле 10 мы пробегаем
# Цикл 5 (метод 'loop') # Цикл 6 (метод 'loop')
весь диапазон индексов массива с помощью итератора each_index.
i=0 i=0
n=list.size-1 n=list.size-1 В предыдущих примерах мы уделили недостаточно внимания вариантам цик-
loop do loop do лов while и loop с модификаторами. Они довольно часто используются из-за крат-
print "#{list[i]} " print "#{list[i]} " кости. Вот еще два примера, в которых делается одно и то же:
i += 1 i += 1 perform_task() until finished
break if i > n break unless i <= n
end end perform_task() while not finished
# Цикл 7 (итератор 'times') # Цикл 8 (итератор 'upto') Также из таблицы 1.2 осталось неясным, что циклы не всегда выполняются от
n=list.size n=list.size-1
начала до конца. Число итераций не всегда предсказуемо. Нужны дополнитель-
n.times do |i| 0.upto(n) do |i|
print "#{list[i]} " print "#{list[i]} " ные средства управления циклами.
end end Первое из них – ключевое слово break, встречающееся в циклах 5 и 6. Оно по-
# Цикл 9 (for) # Цикл 10 ('each_index') зволяет досрочно выйти из цикла; в случае вложенных циклов происходит выход
n=list.size-1 list.each_index do |xt из самого внутреннего. Для программистов на C это интуитивно очевидно.
for print "#{list[x]} " Ключевое слово retry применяется в двух случаях: в контексте итератора и в
i in 0..n do end контексте блока begin-end (обработка исключений). В теле итератора (или цикла
print "#{list[i]} " for) оно заставляет итератор заново выполнить инициализацию, то есть повторно
end вычислить переданные ему аргументы. Отметим, что к циклам общего вида это не
относится.
Ключевое слово redo – обобщение retry на циклы общего вида. Оно работает
Рассмотрим эти примеры более подробно. Циклы 1 и 2 – «стандартные» формы в циклах while и until, как retry в итераторах.
циклов while и until; ведут они себя практически одинаково, только условия про- Ключевое слово next осуществляет переход на конец самого внутреннего цик-
тивоположны. Циклы 3 и 4 – варианты предыдущих с проверкой условия в конце, ла и возобновляет исполнение с этой точки. Работает для любого цикла и итера-
а не в начале итерации. Отметим, что использование слов begin и end в этом кон- тора.
тексте – просто грязный трюк; на самом деле это был бы блок begin/end (приме- Как мы только что видели, итератор – важное понятие в Ruby. Но следует от-
няемый для обработки исключений), за которым следует модификатор while или метить, что язык позволяет определять и пользовательские итераторы, не ограни-
until. Однако для тех, кто желает написать цикл с проверкой в конце, разницы нет. чиваясь встроенными.
На мой взгляд, конструкции 3 и 4 – самый «правильный» способ кодирова- Стандартный итератор для любого объекта называется each. Это существенно
ния циклов. Они заметно проще всех остальных: нет ни явной инициализации, ни отчасти из-за того, что позволяет использовать цикл for. Но итераторам можно да-
явной проверки или инкремента. Это возможно потому, что массив «знает» свой вать и другие имена и применять для разных целей.
размер, а стандартный итератор each (цикл 6) обрабатывает такие детали автома- В качестве примера рассмотрим многоцелевой итератор, который имитиру-
тически. На самом деле в цикле 5 производится неявное обращение к этому ите- ет цикл с проверкой условия в конце (как в конструкции do-while в C или repeat-
ратору, поскольку цикл for работает с любым объектом, для которого определен until в Pascal):
итератор each. Цикл for – лишь сокращенная запись для вызова each; часто такие def repeat(condition)
сокращения называют «синтаксической глазурью», имея в виду, что это не более yield
чем удобная альтернативная форма другой синтаксической конструкции. retry if not condition
В циклах 5 и 6 используется конструкция loop. Выше мы уже отмечали, что end
хотя loop выглядит как ключевое слово, на самом деле это метод модуля Kernel, а В этом примере ключевое слово yield служит для вызова блока, который зада-
вовсе не управляющая конструкция. ется при таком вызове итератора:
46 Обзор Ruby Базовый синтаксис и семантика Ruby 47

j=0 begin
repeat (j >= 10) do # Ничего полезного.
j+=1 # ...
puts j end
end Просто перехватывать ошибки не очень осмысленно. Но у блока может быть
С помощью yield можно также передать параметры, которые будут подставле- один или несколько обработчиков rescue. Если произойдет ошибка в любой точ-
ны в список параметров блока (между вертикальными черточками). В следующем ке программы между begin и rescue, то управление сразу будет передано в подхо-
искусственном примере итератор всего лишь генерирует целые числа от 1 до 10, а дящий обработчик rescue.
вызов итератора порождает кубические степени этих чисел: begin
def my_sequence x = Math.sqrt(y/z)
for i in 1..10 do # ...
yield i rescue ArgumentError
end puts "Ошибка при извлечении квадратного корня."
end rescue ZeroDivisionError
puts "Попытка деления на нуль."
my_sequence {|x| puts x**3 } end
Отметим, что вместо фигурных скобок, в которые заключен блок, можно на- Того же эффекта можно достичь следующим образом:
писать ключевые слова do и end. Различия между этими формами есть, но доволь- begin
но тонкие. x = Math.sqrt(y/z)
# ...
1.2.7. Исключения rescue => err
Как и многие другие современные языки, Ruby поддерживает исключения. puts err
Исключения – это механизм обработки ошибок, имеющий существенные пре- end
имущества по сравнения с прежними подходами. Нам удается избежать возвра- Здесь в переменной err хранится объект-исключение; при выводе ее на пе-
та кодов ошибок и запутанной логики их анализа, а код, который обнаружива- чать объект будет преобразован в осмысленную символьную строку. Отметим, что
ет ошибку, можно отделить от кода, который ее обрабатывает (чаще всего они так коль скоро тип ошибки не указан, то этот обработчик rescue будет перехватывать
или иначе разделены). все исключения, производные от класса StandardError. В конструкции rescue =>
Предложение raise возбуждает исключение. Отметим, что raise – не зарезер- variable можно перед символом => дополнительно указать тип ошибки.
вированное слово, а метод модуля Kernel. (У него есть синоним fail.) Если типы ошибок указаны, то может случиться так, что тип реально возник-
raise # Пример 1 шего исключения не совпадает ни с одним из них. На этот случай после всех обра-
raise "Произошла ошибка" # Пример 2 ботчиков rescue разрешается поместить ветвь else.
raise ArgumentError # Пример 3
begin
raise ArgumentError, "Неверные данные" # Пример 4
# Код, в котором может возникнуть ошибка...
raise ArgumentError.new("Неверные данные ") # Пример 5
rescue Type1
raise ArgumentError, " Неверные данные ", caller[0] # Пример 6
# ...
В первом примере повторно возбуждается последнее встретившееся исключе- rescue Type2
ние. В примере 2 создается исключение RuntimeError (подразумеваемый тип), ко- # ...
торому передается сообщение "Произошла ошибка". else
В примере 3 возбуждается исключение типа ArgumentError, а в примере 4 та- # Прочие исключения...
кое же исключение, но с сообщением "Неверные данные". Пример 5 – просто дру- end
гая запись примера 4. Наконец, в примере 6 еще добавляется трассировочная ин- Часто мы хотим каким-то образом восстановиться после ошибки. В этом по-
формация вида "filename:line" или "filename:line:in 'method'" (хранящаяся в может ключевое слово retry (внутри тела обработчика rescue). Оно позволяет
массиве caller). повторно войти в блок begin и попытаться еще раз выполнить операцию:
А как обрабатываются исключения в Ruby? Для этой цели служит блок begin- begin
end. В простейшей форме внутри него нет ничего, кроме кода: # Код, в котором может возникнуть ошибка...
48 Обзор Ruby ООП в Ruby 49

rescue 1.3.1. Объекты


# Пытаемся восстановиться...
В Ruby все числа, строки, массивы, регулярные выражения и многие другие сущ-
retry # Попробуем еще раз.
end
ности фактически являются объектами. Работа программы состоит в вызове ме-
тодов разных объектов:
Наконец, иногда необходим код, который «подчищает» что-то после выполне-
3.succ # 4
ния блока begin-end. В этом случае можно добавить часть ensure:
"abc".upcase # "ABC"
begin [2,1,5,3,4].sort # [1,2,3,4,5]
# Код, в котором может возникнуть ошибка... someObject.someMethod # какой-то результат
rescue
# Обработка исключений.
В Ruby каждый объект представляет собой экземпляр какого-то класса. Класс
ensure содержит реализацию методов:
# Этот код выполняется в любом случае. "abc".class # String
end "abc".class.class # Class
Код, помещенный внутрь части ensure, выполняется при любом способе выхо- Помимо инкапсуляции собственных атрибутов и операций объект в Ruby име-
да из блока begin-end – вне зависимости от того, произошло исключение или нет. ет уникальный идентификатор:
Исключения можно перехватывать еще двумя способами. Во-первых, сущест- "abc".object_id # 53744407
вует форма rescue в виде модификатора: Этот идентификатор объекта обычно не представляет интереса для програм-
x = a/b rescue puts("Деление на нуль!") миста.
Кроме того, тело определения метода представляет собой неявный блок begin-
end; слово begin опущено, а все тело метода подготовлено к обработке исключения
1.3.2. Встроенные классы
и завершается словом end: Свыше 30 классов уже встроено в Ruby. Как и во многих других объектно-ори-
ентированных языках, в нем не допускается множественное наследование, но это
def some_method
# Код... еще не означает, что язык стал менее выразительным. Современные языки часто
rescue построены согласно модели одиночного наследования. Ruby поддерживает моду-
# Восстановление после ошибки... ли и классы-примеси, которые мы обсудим в следующей главе. Также реализова-
end ны идентификаторы объектов, что позволяет строить устойчивые, распределенные
На этом мы завершаем как обсуждение обработки исключений, так и рассмот- и перемещаемые объекты.
рение основ синтаксиса и семантики в целом. Для создания объекта существующего класса обычно используется метод new:
У Ruby есть многочисленные аспекты, которых мы не коснулись. Оставшая- myFile = File.new("textfile.txt","w")
ся часть главы посвящена более развитым возможностям языка, в том числе рас- myString = String.new("Это строковый объект")
смотрению ряда практических приемов, которые помогут программисту среднего Однако не всегда его обязательно вызывать явно. В частности, при создании
уровня научиться «думать на Ruby». объекта String можно и не упоминать этот метод:
yourString = "Это тоже строковый объект"
1.3. ООП в Ruby aNumber = 5 # и здесь метод new не нужен

В языке Ruby есть все элементы, которые принято ассоциировать с объектно-ори- Ссылки на объекты хранятся в переменных. Выше уже отмечалось, что сами
ентированными языками: объекты с инкапсуляцией и сокрытием данных, методы переменные не имеют типа и не являются объектами – они лишь ссылаются на
с полиморфизмом и переопределением, классы с иерархией и наследованием. Но объекты.
Ruby идет дальше, добавляя ограниченные возможности создания метаклассов, x = "abc"
синглетные методы, модули и классы-примеси. Из этого правила есть исключение: небольшие неизменяемые объекты некото-
Похожие идеи, только под иными именами, встречаются и в других объектно- рых встроенных классов, например Fixnum, непосредственно копируются в пере-
ориентированных языках, но одно и то же название может скрывать тонкие раз- менные, которые на них ссылаются. (Размер этих объектов не превышает размера
личия. В этом разделе мы уточним, что в Ruby понимается под каждым из элемен- указателя, поэтому хранить их таким образом более эффективно.) В таком случае
тов ООП. во время присваивания делается копия объекта, а куча не используется.
50 Обзор Ruby ООП в Ruby 51

При присваивании переменных ссылки на объекты обобществляются. Термины «модуль» и «примесь» – почти синонимы. Модуль представляет со-
y = "abc" бой набор методов и констант, внешних по отношению к программе на Ruby. Его
x = y можно использовать просто для управления пространством имен, но основное
x # "abc" применение модулей связано с «подмешиванием» его возможностей в класс (с по-
После выполнения присваивания x = y и x, и y ссылаются на один и тот же объ- мощью директивы include). В таком случае он используется как класс-примесь.
ект: Этот термин очевидно заимствован из языка Python. Стоит отметить, что в не-
x.object_id # 53732208 которых вариантах LISP такой механизм существует уже больше двадцати лет.
y.object_id # 53732208 Не путайте описанное выше употребление термина «модуль» с другим значе-
нием, которое часто придается ему в информатике. Модуль в Ruby – это не внеш-
Если объект изменяемый, то модификация, примененная к одной переменной,
ний исходный текст и не двоичный файл (хотя может храниться и в том, и в другом
отражается и на другой:
виде). Это объектно-ориентированная абстракция, в чем-то похожая на класс.
x.gsub!(/a/,"x")
Примером использования модуля для управления пространством имен слу-
y # "xbc"
жит модуль Math. Так, чтобы получить определение числа π, необязательно вклю-
Однако новое присваивание любой из этих переменных не влияет на другую: чать модуль Math с помощью предложения include; достаточно просто написать
# Продолжение предыдущего примера Math::PI.
x = "abc" Примесь дает способ получить преимущества множественного наследования,
не отягощенные характерными для него проблемами. Можно считать, что это огра-
y # по-прежнему равно "xbc"
ниченная форма множественного наследования, но создатель языка Мац называ-
Изменяемый объект можно сделать неизменяемым, вызвав метод freeze: ет его одиночным наследованием с разделением реализации.
x.freeze Отметим, что предложение include включает имена из указанного простран-
x.gsub!(/b/,"y") # Ошибка! ства имен (модуля) в текущее. Метод extend добавляет объекту функции из моду-
Символ в Ruby ссылается на переменную по имени, а не по ссылке. Во многих ля. В случае применения include методы модуля становятся доступны как методы
случаях он может вообще не ссылаться на идентификатор, а вести себя как некая экземпляра, а в случае extend – как методы класса.
разновидность неизменяемой строки. Символ можно преобразовать в строку с по- Необходимо оговориться, что операции load и require не имеют ничего обще-
мощью метода to_s. го с модулями: они относятся к исходным и двоичным файлам (загружаемым ди-
Hearts = :Hearts # Это один из способов присвоить намически или статически). Операция load читает файл и вставляет его в теку-
Clubs = :Clubs # уникальное значение константе, щую точку исходного текста, так что начиная с этой точки становятся видимы все
Diamonds = :Diamonds # некий аналог перечисления определения, находящиеся во внешнем файле. Операция require аналогична load,
Spades = :Spades # в языках Pascal или C. но не загружает файл, если он уже был загружен ранее.
Программисты, только начинающие осваивать Ruby, особенно имеющие опыт
puts Hearts.to_s # Печатается "Hearts" работы с языком C, могут поначалу путать операции require и include, которые
Продемонстрированный выше фокус с «перечислением» был более осмыслен никак не связаны между собой. Вы еще поймаете себя на том, что сначала вызыва-
на ранних этапах развития Ruby, когда еще не было класса Symbol, а наличие дво- ете require, а потом include для того, чтобы воспользоваться каким-то внешним
еточия перед идентификатором превращало его в целое число. Если вы пользуе- модулем.
тесь таким трюком, не предполагайте, что фактическое значение символа будет не-
изменным или предсказуемым – просто используйте его как константу, значение
1.3.4. Создание классов
которой неважно. В Ruby есть множество встроенных классов, и вы сами можете определять новые.
Для определения нового класса применяется такая конструкция:
1.3.3. Модули и классы-примеси class ClassName
Многие встроенные методы наследуются от классов-предков. Особо стоит от- # ...
метить методы модуля Kernel, подмешиваемые к суперклассу Object. Посколь- end
ку класс Object повсеместно доступен, то и добавленные в него из Kernel мето- Само имя класса – это глобальная константа, поэтому оно должно начинать-
ды также доступны в любой точке программы. Эти методы играют важную роль ся с прописной буквы. Определение класса может содержать константы, перемен-
в Ruby. ные класса, методы класса, переменные экземпляра и методы экземпляра. Данные
52 Обзор Ruby ООП в Ruby 53

уровня класса доступны всем объектам этого класса, тогда как данные уровня эк- @@count += 1
земпляра доступны только одному объекту. @myvar = 10
Попутное замечание: строго говоря, классы в Ruby не имеют имен. «Имя» end
класса – это всего лишь константа, ссылающаяся на объект типа Class (посколь-
def MyClass.getcount # метод класса
ку в Ruby Class – это класс). Ясно, что на один и тот же класс могут ссылаться
@@count # переменная класса
несколько констант, и их можно присваивать переменным точно так же, как мы
end
поступаем с любыми другими объектами (поскольку в Ruby Class – это объект).
Если вы немного запутались, не расстраивайтесь. Удобства ради новичок может def getcount # экземпляр возвращает переменную класса!
считать, что в Ruby имя класса – то же самое, что в C++. @@count # переменная класса
Вот как определяется простой класс: end
class Friend
@@myname = "Эндрю" # переменная класса def getmyvar # метод экземпляра
@myvar # переменная экземпляра
def initialize(name, sex, phone) end
@name, @sex, @phone = name, sex, phone
# Это переменные экземпляра def setmyvar(val) # метод экземпляра устанавливает @myvar
end @myvar = val
end
def hello # метод экземпляра def myvar=(val) # другой способ установить @myvar
puts "Привет, я #{@name}." @myvar = val
end end
end
def Friend.our_common_friend # метод класса
puts "Все мы друзья #{@@myname}." foo = MyClass.new # @myvar равно 10
end foo.setmyvar 20 # @myvar равно 20
foo.myvar = 30 # @myvar равно 30
end
f1 = Friend.new("Сюзанна","F","555-0123") Здесь мы видим, что getmyvar возвращает значение переменной @myvar, а
f2 = Friend.new("Том","M","555-4567") setmyvar устанавливает его. (Многие программисты говорят о методах чтения и
установки). Все это работает, но не является характерным способом действий в
f1.hello # Привет, я Сюзанна. Ruby. Метод myvar= похож на перегруженный оператор присваивания (хотя, стро-
f2.hello # Привет, я Том. го говоря, таковым не является); это более удачная альтернатива setmyvar, но есть
Friend.our_common_friend # Все мы друзья Эндрю. способ еще лучше.
Поскольку данные уровня класса доступны во всем классе, их можно ини- Класс Module содержит методы attr, attr_accessor, attr_reader и attr_writer.
циализировать в момент определения класса. Если определен метод с именем Ими можно пользоваться (передавая символы в качестве параметров) для авто-
initialize, то гарантируется, что он будет вызван сразу после выделения памя- матизации управления доступом к данным экземпляра. Например, все три метода
ти для объекта. Этот метод похож на традиционный конструктор, но не выполня- getmyvar, setmyvar и myvar= можно заменить одной строкой в определении класса:
ет выделения памяти. Память выделяется методом new, а освобождается неявно attr_accessor :myvar
сборщиком мусора.
При этом создается метод myvar, который возвращает значение @myvar, и метод
Теперь взгляните на следующий фрагмент, обращая особое внимание на мето-
myvar=, который позволяет изменить значение той же переменной. Методы attr_
ды getmyvar, setmyvar и myvar=:
reader и attr_writer создают соответственно версии методов доступа к атрибуту
class MyClass
для чтения и для изменения.
NAME = "Class Name" # константа класса Внутри методов экземпляра, определенных в классе, можно при необходимос-
@@count = 0 # инициализировать переменную класса ти пользоваться переменной self. Это просто ссылка на объект, от имени которо-
def initialize # вызывается после выделения памяти для объекта го вызван метод экземпляра.
54 Обзор Ruby ООП в Ruby 55

Для управления видимостью методов класса можно пользоваться модифика- По умолчанию все определенные в классе методы открыты. Исключение со-
торами private, protected и public. (Переменные экземпляра всегда закрыты, об- ставляет лишь initialize. Методы, определенные на верхнем уровне программы,
ращаться к ним извне класса можно только с помощью методов доступа.) Каждый тоже по умолчанию открыты. Если они объявлены закрытыми, то могут вызывать-
модификатор принимает в качестве параметра символ, например :foo, а если он ся только в функциональной форме (как, например, методы, определенные в клас-
опущен, то действие модификатора распространяется на все последующие опре- се Object).
деления в классе. Пример: Классы в Ruby сами являются объектами – экземплярами метакласса Class.
class MyClass Классы в этом языке всегда конкретны, абстрактных классов не существует. Од-
нако теоретически можно реализовать и абстрактные классы, если вам это для че-
def method1 го-то понадобится.
# ... Класс Object является корнем иерархии. Он предоставляет все методы, опре-
end деленные во встроенном модуле Kernel.
def method2
Чтобы создать класс, наследующий другому классу, нужно поступить следу-
# ...
ющим образом:
end
class MyClass < OtherClass
def method3 # ...
# ... end
end Помимо использования встроенных методов, вполне естественно определить
и собственные либо переопределить унаследованные. Если определяемый метод
private :method1 имеет то же имя, что и существующий, то старый метод замещается. Если новый
public
метод должен обратиться к замещенному им «родительскому» методу (так быва-
:method2 ет часто), можно воспользоваться ключевым словом super.
protected :method3 Перегрузка операторов, строго говоря, не является неотъемлемой особеннос-
тью ООП, но этот механизм знаком программистам на C++ и некоторых других
private языках. Поскольку большинство операторов в Ruby так или иначе являются ме-
тодами, то не должен вызывать удивления тот факт, что их можно переопределять
def my_method или определять в пользовательских классах. Переопределять семантику операто-
# ... ра в существующем классе редко имеет смысл, зато в новых классах определение
end
операторов – обычное дело.
def another_method
Можно создавать синонимы методов. Для этого внутри определения класса
# ... предоставляется такой синтаксис:
end alias newname oldname
Число параметров будет таким же, как для старого имени, и вызываться метод-
end
синоним будет точно так же. Обратите внимание на отсутствие запятой; alias –
В этом классе метод method1 закрытый, method2 открытый, а method3 защищен- это не имя метода, а ключевое слово. Существует метод с именем alias_method,
ный. Поскольку далее вызывается метод private без параметров, то методы my_ который ведет себя аналогично, но в случае его применения параметры должны
method и another_method будут закрытыми. разделяться запятыми, как и для любого другого метода.
Уровень доступа public не нуждается в объяснениях, он не налагает никаких
ограничений ни на доступ к методу, ни на его видимость. Уровень private озна- 1.3.5. Методы и атрибуты
чает, что метод доступен исключительно внутри класса или его подклассов и мо- Как мы уже видели, методы обычно используются в сочетании с простыми экзем-
жет вызываться только в «функциональной форме» от имени self, причем вы- плярами классов и переменными, причем вызывающий объект отделяется от име-
зывающий объект может указываться явно или подразумеваться неявно. Уровень ни метода точкой (receiver.method). Если имя метода является знаком препина-
protected означает, что метод вызывается только внутри класса, но, в отличие от ния, то точка опускается. У методов могут быть аргументы:
закрытого метода, не обязательно от имени self. Time.mktime(2000, "Aug", 24, 16, 0)
56 Обзор Ruby Динамические аспекты Ruby 57

Поскольку каждое выражение возвращает значение, то вызовы методов мо- Вот пример определения синглетного метода для строкового объекта:
гут сцепляться: str = "Hello, world!"
3.succ.to_s str2 = "Goodbye!"
/(x.z).*?(x.z).*?/.match("x1z_1a3_x2z_1b3_").to_a[1..3]
3+2.succ def str.spell
self.split(/./).join("-")
Отметим, что могут возникать проблемы, если выражение, являющееся ре- end
зультатом сцепления, имеет тип, который не поддерживает конкретный метод.
Точнее, при определенных условиях некоторые методы возвращают nil, а вы- str.spell # "H-e-l-l-o-,- -w-o-r-l-d-!"
зов любого метода от имени такого объекта приведет к ошибке. (Конечно, nil – str2.spell # Ошибка!
полноценный объект, но он не обладает теми же методами, что и, например, мас- Имейте в виду, что метод определяется для объекта, а не для переменной.
сив.) Теоретически с помощью синглетных методов можно было бы создать систему
Некоторым методам можно передавать блоки. Это верно для всех итерато- объектов на базе прототипов. Это менее распространенная форма ООП без клас-
ров – как встроенных, так и определенных пользователем. Блок обычно заключа- сов*. Основной структурный механизм в ней состоит в конструировании нового
ется в операторные скобки do-end или в фигурные скобки. Но он не рассматрива- объекта путем использования существующего в качестве образца; новый объект
ется так же, как предшествующие ему параметры, если таковые существуют. Вот ведет себя как старый за исключением тех особенностей, которые были переопре-
пример вызова метода File.open: делены. Тем самым можно строить системы на основе прототипов, а не наследова-
my_array.each do |x| ния. Хотя у нас нет опыта в этой области, мы полагаем, что создание такой систе-
some_action мы позволило бы полнее раскрыть возможности Ruby.
end

File.open(filename) { |f| some_action } 1.4. Динамические аспекты Ruby


Именованные параметры будут поддерживаться в последующих версиях Ruby, Ruby – динамический язык в том смысле, что объекты и классы можно изменять
но на момент работы над этой книгой еще не поддерживались. В языке Python они во время выполнения. Ruby позволяет конструировать и интерпретировать фраг-
называются ключевыми аргументами, сама идея восходит еще к языку Ada. менты кода в ходе выполнения статически написанной программы. В нем есть
Методы могут принимать переменное число аргументов: хитроумный API отражения, с помощью которого программа может получать ин-
receiver.method(arg1, *more_args) формацию о себе самой. Это позволяет сравнительно легко создавать отладчики,
профилировщики и другие подобные инструменты, а также применять нетриви-
В данном случае вызванный метод трактует more_args как массив и обращает-
альные способы кодирования.
ся с ним, как с любым другим массивом. На самом деле звездочка в списке фор-
Наверное, это самая трудная тема для программиста, приступающего к изуче-
мальных параметров (перед последним или единственным параметром) может
нию Ruby. В данном разделе мы вкратце рассмотрим некоторые следствия, выте-
«свернуть» последовательность фактических параметров в массив:
кающие из динамической природы языка.
def mymethod(a, b, *c)
print a, b 1.4.1. Кодирование во время выполнения
c.each do |x| print x end Мы уже упоминали директивы load и require. Важно понимать, что это не встро-
end
енные предложения и не управляющие конструкции; на самом деле это методы.
mymethod(1,2,3,4,5,6,7)
Поэтому их можно вызывать, передавая переменные или выражения как парамет-
ры, в том числе условно. Сравните с директивой #include в языках C и C++, кото-
# a=1, b=2, c=[3,4,5,6,7] рая обрабатывается во время компиляции.
Код можно строить и интерпретировать по частям. В качестве несколько ис-
В Ruby есть возможность определять методы на уровне объекта (а не клас-
кусственного примера рассмотрим приведенный ниже метод calculate и вызыва-
са). Такие методы называются синглетными; они принадлежат одному-единствен-
ющий его код:
ному объекту и не оказывают влияния ни на класс, ни на его суперклассы. Такая
возможность может быть полезна, например, при разработке графических интер- def calculate(op1, operator, op2)
string = op1.to_s + operator + op2.to_s
фейсов пользователя: чтобы определить действие кнопки, вы задаете синглетный
метод для данной и только данной кнопки. * Типичный пример – язык JavaScript (Прим. перев.)
58 Обзор Ruby Динамические аспекты Ruby 59

# Предполагается, что operator - строка; построим длинную action2


# строку, состоящую из оператора и операндов. end
eval(string) # Вычисляем и возвращаем значение. else
end def my_action
default_action
@alpha = 25 end
@beta = 12 end
Таким способом мы достигаем желаемого результата, но условие вычисляет-
puts calculate(2, "+", 2) # Печатается 4
ся только один раз. Когда программа вызовет метод my_action, он уже будет пра-
puts calculate(5, "*", "@alpha") # Печатается 125
puts calculate("@beta", "**", 3) # Печатается 1728
вильно определен.
Вот та же идея, доведенная чуть ли не до абсурда: программа запрашивает у 1.4.2. Отражение
пользователя имя метода и одну строку кода. Затем этот метод определяется и вы- В языках Smalltalk, LISP и Java реализована (с разной степенью полноты) идея
зывается: рефлексивного программирования – активная среда может опрашивать структуру
puts "Имя метода: " объектов и расширять либо модифицировать их во время выполнения.
meth_name = gets В языке Ruby имеется развитая поддержка отражения, но все же он не заходит
puts "Строка кода: " так далеко, как Smalltalk, где даже управляющие конструкции являются объекта-
code = gets ми. В Ruby управляющие конструкции и блоки не представляют собой объекты.
(Объект Proc можно использовать для того, чтобы представить блок в виде объек-
string = %[def #{meth_name}\n #{code}\n end] # Строим строку.
eval(string) # Определяем метод. та, но управляющие конструкции объектами не бывают никогда.)
eval(meth_name) # Вызываем метод. Для определения того, используется ли идентификатор с данным именем, служит
ключевое слово defined? (обратите внимание на вопросительный знак в конце слова):
Зачастую необходимо написать программу, которая могла бы работать на раз-
ных платформах или при разных условиях, но при этом сохранить общий набор if defined? some_var
puts "some_var = #{some_var}"
исходных текстов. Для этого в языке C применяются директивы #ifdef, но в Ruby
else
все определения исполняются. Не существует такого понятия, как «этап компи- puts "Переменная some_var неизвестна."
ляции»; все конструкции динамические, а не статические. Поэтому для принятия end
решения такого рода мы можем просто вычислить условие во время выполнения:
Аналогично метод respond_to? выясняет, может ли объект отвечать на вызов
if platform == Windows указанного метода (то есть определен ли данный метод для данного объекта). Ме-
action1
тод respond_to? определен в классе Object.
elsif platform == Linux
action2
В Ruby запрос информации о типе во время выполнения поддерживается очень
else полно. Тип или класс объекта можно определить, воспользовавшись методом type
default_action (из класса Object). Метод is_a? сообщает, принадлежит ли объект некоторому
end классу (включая и его суперклассы); синонимом служит имя kind_of?. Например:
Конечно, за такое кодирование приходится расплачиваться некоторым сниже- puts "abc".class "" # Печатается String
нием производительности, поскольку иногда условие вычисляется много раз. Но puts 345.class # Печатается Fixnum
rover = Dog.new
рассмотрим следующий пример, который делает практически то же самое, однако
весь платформенно-зависимый код помещен в один метод, имя которого от плат- print rover.class # Печатается Dog
формы не зависит:
if platform == Windows if rover.is_a? Dog
def my_action puts "Конечно, является."
action1 end
end
elsif platform == Linux if rover.kind_of? Dog
def my_action puts "Да, все еще собака."
60 Обзор Ruby Потренируйте свою интуицию 61

end весомое преимущество. В таких языках, как C++, за выделение и освобождение па-
мяти отвечает программист. В более поздних языках, например Java, память осво-
if rover.is_a? Animal бождается сборщиком мусора (когда объект покидает область видимости).
puts "Да, он к тому же и животное."
Явное управление памятью может приводить к двум видам ошибок. Если
end
освобождается память, занятая объектом, на который еще есть ссылки, то при по-
Можно получить полный список всех методов, которые можно вызвать для следующем доступе к нему объект может оказаться в противоречивом состоянии.
данного объекта. Для этого предназначен метод methods из класса Object. Имеются Так называемые висячие указатели трудно отлаживать, поскольку вызванные ими
также его варианты private_instance_methods, public_instance_methods и т. д. ошибки часто проявляются далеко от места возникновения. Утечка памяти име-
Аналогично можно узнать, какие переменные класса или экземпляра ассоции- ет место, когда не освобождается объект, на который больше никто не ссылает-
рованы с данным объектом. По самой природе ООП в перечни методов и перемен- ся. В этом случае программа потребляет все больше и больше памяти и в конеч-
ных включаются те, что определены как в классе самого объекта, так и во всех его ном счете аварийно завершается; такие ошибки искать тоже трудно. В языке Ruby
суперклассах. В классе Module имеется метод constants, позволяющий получить для отслеживания неиспользуемых объектов и освобождения занятой ими памя-
список всех констант, определенных в модуле. ти применяется механизм сборки мусора. Для тех, кто в этом разбирается, отме-
В классе Module есть метод ancestors, возвращающий список модулей, вклю- тим, что в Ruby используется алгоритм пометки и удаления, а не подсчета ссылок
ченных в данный модуль. В этот список входит и сам данный модуль, то есть спи- (у последнего возникают трудности при обработке рекурсивных структур).
сок, возвращаемый вызовом Mod.ancestors, содержит по крайней мере элемент Сборка мусора влечет за собой некоторое снижение производительности. Мо-
Mod. В этот список входят не только родительские классы (отобранные в силу на- дуль GC предоставляет ограниченные средства управления, позволяющие про-
следования), но и «родительские» модули (отобранные в силу включения). граммисту настроить его работу в соответствии с нуждами конкретной программы.
В классе Object есть метод superclass, который возвращает суперкласс объек- Можно также определить чистильщика (finalizer) объекта, но это уже тема для
та или nil. Не имеет суперкласса лишь класс Object, и, значит, только для него мо- «продвинутых» (см. раздел 11.3.14).
жет быть возвращен nil.
Модуль ObjectSpace применяется для получения доступа к любому «живому»
объекту. Метод _idtoref преобразует идентификатор объекта в ссылку на него; 1.5. Потренируйте свою интуицию:
можно считать, что это операция, обратная той, что выполняет двоеточие в начале что следует запомнить
имени. В модуле ObjectSpace есть также итератор each_object, который перебира-
Надо честно признаться: «все становится интуитивно ясным после того, как пой-
ет все существующие в данный момент объекты, включая и те, о которых иным об-
мешь». Эта истина и составляет суть данного раздела, поскольку в Ruby немало
разом узнать невозможно. (Напомним, что некоторые неизменяемые объекты не-
особенностей, отличающих его от всего, к чему привык программист на одном из
большого размера, например принадлежащие классам Fixnum, NilClass, TrueClass
традиционных языков.
и FalseClass, не хранятся в куче из соображений оптимизации.)
Кто-то из читателей решит, что не нужно зря тратить время на повторение из-
1.4.3. Отсутствующие методы вестного. Если вы из их числа, можете пропустить разделы, содержание которых
При вызове метода (myobject.mymethod) Ruby ищет поименованный метод в сле- кажется вам очевидным. Программисты имеют неодинаковый опыт; искушенные
дующем порядке: пользователи C и Smalltalk воспримут Ruby совсем по-разному. Впрочем, мы на-
деемся, что внимательное прочтение последующих разделов поможет многим чи-
1. Синглетные методы, определенные для объекта myobject.
тателям разобраться в том, что же такое Путь Ruby.
2. Методы, определенные в классе объекта myobject.
3. Методы, определенные в предках класса объекта myobject. 1.5.1. Синтаксис
Синтаксический анализатор Ruby сложен и склонен прощать многие огрехи. Он
Если найти метод mymethod не удается, Ruby ищет метод с именем method_missing.
пытается понять, что хотел сказать программист, а не навязывать ему жесткие пра-
Если он определен, то ему передается имя отсутствующего метода (в виде символа)
вила. Но к такому поведению надо еще привыкнуть. Вот перечень того, что следу-
и все переданные ему параметры. Этот механизм можно применять для динамичес-
ет знать о синтаксисе Ruby.
кой обработки неизвестных сообщений, посланных во время выполнения.
• Скобки при вызове методов, как правило, можно опускать. Все следующие
1.4.4. Сборка мусора вызовы допустимы:
Управлять памятью на низком уровне трудно и чревато ошибками, особенно в таком foobar
динамичном окружении, какое создает Ruby. Наличие механизма сборки мусора – foobar()
62 Обзор Ruby Потренируйте свою интуицию 63

foobar(a,b,c) • Ключевое слово then (в предложениях if и case) необязательно. Если вам


foobar a, b, c кажется, что с ним программа понятнее, включайте его в код. То же отно-
• Коль скоро скобки необязательны, что означает такая запись: x y z? Ока- сится к слову do в циклах while и until.
зывается, вот что: «Вызвать метод y, передав ему параметр z, а результат пе- • Вопросительный и восклицательный знаки не являются частью идентифи-
редать в виде параметра методу x.» Иными словами, x(y(z)). катора, который модифицируют, – их следует рассматривать как суффик-
Это поведение в будущем изменится. См. обсуждение поэтического режи- сы. Таким образом, хотя идентификаторы chop и chop! считаются различ-
ма в разделе 1.6 ниже. ными, использовать восклицательный знак в любом другом месте имени не
• Попробуем передать методу хэш: разрешается. Аналогично в Ruby есть конструкция defined?, но defined –
my_method {a=>1, b=>2} ключевое слово.
Это приведет к синтаксической ошибке, поскольку левая фигурная скоб- • Внутри строки символ решетки # – признак начала выражения. Значит, в
ка воспринимается как начало блока. В данном случае скобки необходимы: некоторых случаях его следует экранировать обратной косой чертой, но
my_method({a=>1, b=>2}) лишь тогда, когда сразу за ним идет символ {, $ или @.
• Предположим теперь, что хэш – единственный (или последний) параметр • Поскольку вопросительный знак можно добавлять в конец идентифика-
метода. Ruby снисходительно разрешает опускать фигурные скобки: тора, то следует аккуратно расставлять пробелы в тернарном операторе.
my_method(a=>1, b=>2)
Пусть, например, имеется переменная my_flag, которая может принимать
значения true или false. Тогда первое из следующих предложений пра-
Кто-то увидит здесь вызов метода с именованными параметрами. Это обман- вильно, а второе содержит синтаксическую ошибку:
чивое впечатление, хотя никто не запрещает применять подобную конструкцию и
x = my_flag? 23 : 45 # Правильно.
в таком смысле.
x = my_flag? 23 : 45 # Синтаксическая ошибка.
• Есть и другие случаи, когда пропуски имеют некоторое значение. Напри-
мер, на первый взгляд все четыре выражения ниже означают одно и то же: • Концевой маркер для встроенной документации не следует считать лексе-
мой. Он помечает строку целиком, поэтому все находящиеся в той же стро-
x = y + z
x = y+z ке символы не являются частью текста программы, а принадлежат встроен-
x = y+ z ному документу.
x = y +z • В Ruby нет произвольных блоков, то есть нельзя начать блок в любом мес-
Но фактически эквивалентны лишь первые три. В четвертом же случае ана- те, как в C. Блоки разрешены только там, где они нужны, – например, могут
лизатор считает, что вызван метод y с параметром +z! И выдаст сообщение присоединяться к итератору. Исключение составляет блок begin-end, кото-
об ошибке, так как метода с именем y не существует. Мораль: пользуйтесь рый можно употреблять практически везде.
пробелами разумно. • Не забывайте, что ключевые слова BEGIN и END не имеют ничего общего с
• Аналогично x = y*z – это умножение y на z, тогда как x = y *z – вызов ме- begin и end.
тода y, которому в качестве параметра передается расширение массива z. • При статической конкатенации строк приоритет конкатенации ниже, чем у
• В именах идентификаторов знак подчеркивания _ считается строчной бук- вызова метода. Например:
вой. Следовательно, имя идентификатора может начинаться с этого знака, str = "Первая " 'second'.center(20) # Примеры 1 and 2
но такой идентификатор не будет считаться константой, даже если следую- str = "Вторая " + 'second'.center(20) # дают одно и то же.
щая буква прописная. str = "Первая вторая".center(20) # Примеры 3 and 4
• В линейной последовательности вложенных предложений if применяется str = ("Первая " + 'вторая').center(20) # дают одно и то же.
ключевое слово elsif, а не else if или elif, как в некоторых других язы- • В Ruby есть несколько псевдопеременных, которые выглядят как локаль-
ках. ные переменные, но применяются для особых целей. Это self, nil, true,
• Ключевые слова в Ruby нельзя назвать по-настоящему зарезервированны- false, __FILE__ и __LINE__.
ми. Если метод вызывается от имени некоторого объекта (и в других слу-
чаях, когда не возникает неоднозначности), имя метода может совпадать 1.5.2. Перспективы программирования
с ключевым словом. Но поступайте так с осторожностью, не забывая, что Наверное, каждый, кто знает Ruby (сегодня), в прошлом изучал или пользовался
программу будут читать люди. другими языками. Это, с одной стороны, облегчает изучение Ruby, так как многие
64 Обзор Ruby Потренируйте свою интуицию 65

средства похожи на аналогичные средства в других языках. С другой стороны, у • ARGV[0] – первый аргумент в командной строке; они нумеруются начиная с
программиста может возникнуть ложное чувство уверенности при взгляде на зна- нуля. Это не имя файла или сценария, предшествующего параметрам, как
комые конструкции Ruby. Он может прийти к неверным выводам, основанным на argv[0] в языке C.
прошлом опыте; можно назвать это явление «багажом эксперта». • Большинство операторов в Ruby на самом деле является методами; их запись
Немало специалистов переходит на Ruby со Smalltalk, Perl, C/C++ и других в виде «знаков препинания» – не более чем удобство. Первое исключение из
языков. Ожидания этих людей сильно различаются, но так или иначе присутству- этого правила – набор операторов составного присваивания (+=, -= и т. д.).
ют. Поэтому рассмотрим некоторые вещи, на которых многие спотыкаются. Второе исключение – операторы =, .., ..., !, not, &&, and, ||, or, !=, !~.
• Символ в Ruby представляется целым числом. Это не самостоятельный тип, • Как и в большинстве современных языков программирования (хотя и не во
как в Pascal, и не эквивалент строки длиной 1. В ближайшем будущем по- всех), булевские операции закорачиваются, то есть вычисление булевского
ложение изменится и символьная константа станет строкой, но на момент выражения заканчивается, как только его значение истинности становит-
написания данной книги этого еще не произошло. Рассмотрим следующий ся известным. В последовательности операций or вычисление заканчивает-
фрагмент: ся, когда получено первое значение true, а в последовательности операций
x = "Hello" and – когда получено первое значение false.
y = ?A
puts "x[0] = #{x[0]}" # Печатается x[0] = 72 • Префикс @@ применяется для переменных класса (то есть ассоциированных
puts "y = #{y}" # Печатается y = 65 с классом в целом, а не с отдельным экземпляром).
if y == "A" # Печатается no • loop – не ключевое слово. Это метод модуля Kernel, а не управляющая кон-
puts "yes" струкция.
else
puts "no" • Кому-то синтаксис unless-else может показаться интуитивно неочевид-
end ным. Поскольку unless – противоположность if, то ветвь else выполняет-
ся, когда условие истинно.
• Не существует булевского типа. TrueClass и FalseClass – это два разных
класса, а единственными их экземплярами являются объекты true и false. • Простой тип Fixnum передается как непосредственное значение и, стало
быть, не может быть изменен внутри метода. То же относится к значениям
• Многие операторы в Ruby напоминают операторы в языке C. Два заметных
true, false и nil.
исключения – операторы инкремента и декремента (++ и --). Их в Ruby нет
ни в «пост», ни в «пред» форме. • Не путайте операторы && и || с операторами & и |. Те и другие используют-
ся в языке C; первые два предназначены для логических операций, послед-
• Известно, что в разных языках оператор деления по модулю работает по-
ние два – для поразрядных.
разному для отрицательных чисел. Не вдаваясь в споры о том, что правиль-
но, проиллюстрируем поведение в Ruby: • Операторы and и or имеют более низкий приоритет, чем && и ||. Взгляните
puts (5 % 3) # Печатается 2
на следующий фрагмент:
puts (-5 % 3) # Печатается 1 a = true
puts (5 % -3) # Печатается -1 b = false
puts (-5 % -3) # Печатается -2 c = true
d = true
• Некоторые привыкли думать, что «ложь» можно представлять нулем, пус- a1 = a && b or c && d # Операции && выполняются первыми.
той строкой, нулевым символом и т. п. Но в Ruby все это равно «истине». a2 = a && (b or c) && d # Операция or выполняется первой.
На самом деле истиной будет все кроме объектов false и nil. puts a1 # Печатается false
• В Ruby переменные не принадлежат никакому классу: класс есть только у puts a2 # Печатается true
значений. • Не забывайте, что «оператор» присваивания имеет более высокий прио-
• Переменные в Ruby не объявляются, однако считается хорошим тоном при- ритет, чем операторы and и or! (это относится и к составным операторам
сваивать переменной начальное значение nil. Разумеется, при этом с пере- присваивания: +=, -= и пр.). Например, код x = y or z выглядит как обычное
менной не ассоциируется никакой тип и даже не происходит истинной ини- предложение присваивания, но на самом деле это обособленное выраже-
циализации, но анализатор знает, что данное имя принадлежит переменной, ние (эквивалент (x=y) or z). Вероятно, программист имел в виду следую-
а не методу. щее: x = (y or z).
66 Обзор Ruby Потренируйте свою интуицию 67

y = false кажутся близкими родственниками. Точного аналога предложению case в Ruby нет
z = true ни в каком другом знакомом мне языке, поэтому оно заслуживает особого внимания.
Выше мы уже рассматривали синтаксис этого предложения, а теперь сосредо-
x = y or z # Оператор = выполняется РАНЬШЕ or!
точимся на его семантике.
puts x # Печатается false
• Для начала рассмотрим тривиальный пример. Выражение expression срав-
(x = y) or z # Строка 5: то же, что и выше. нивается со значением value, и, если они совпадают, выполняется некото-
puts x # Печатается fals рое действие. Ничего удивительного.
case expression
x = (y or z) # Оператор or вычисляется сначала.
when value
puts x # Печатается true
некоторое действие
• Не путайте атрибуты объектов с локальными переменными. Если вы при- end
выкли к C++ или Java, можете забыть об этом! Переменная @my_var в кон- В Ruby для этой цели есть специальный оператор === (называется операто-
тексте класса – это переменная экземпляра (или атрибут), но my_var в том ром отношения). Иногда его еще называют (не совсем правильно) операто-
же контексте – локальная переменная. ром ветвящегося равенства. Неправильность в том, что он не всегда отно-
• Во многих языках, и в Ruby в том числе, есть цикл for. Рано или поздно воз- сится именно к проверке на равенство.
никает вопрос, можно ли модифицировать индексную переменную. В не- • Предыдущее предложение можно записать и так:
которых языках эту управляющую переменную запрещено изменять вовсе if value === expression
(выводится предупреждение либо сообщение об ошибке на этапе компиля- некоторое действие
ции или выполнения); в других это допустимо, хотя и приводит к изменению end
поведения цикла. В Ruby принят третий подход. Переменная, управляющая
циклом for, считается обычной переменной, которую можно изменять в лю- • Не путайте оператор отношения с оператором проверки на равенство (==).
бой момент, но это изменение не оказывает влияния на поведение цикла! Они принципиально различны, хотя во многих случаях ведут себя оди-
Цикл for присваивает этой переменной последовательные значения, что бы наково. Оператор отношения определен по-разному в разных классах, а
с ней ни происходило внутри тела цикла. Например, следующий цикл будет для данного класса его поведение может зависеть от типа переданного опе-
выполнен ровно 10 раз и напечатает значения от 1 до 10: ранда.
for var in 1..10 • Не думайте, что проверяемое выражение – это объект, которому сравнива-
puts "var = #{var}" емое значение передается в качестве параметра. На самом деле как раз на-
if var > 5 оборот (мы это только что видели).
var = var + 2
end
• Это подводит нас к наблюдению, что x === y означает вовсе не то же самое,
end что y === x! Иногда результат совпадает, но в общем случае оператор отно-
шения не коммутативен. (Именно поэтому нам не нравится термин «опера-
• Имена переменных не всегда легко «на глаз» отличить от имен методов. тор ветвящегося равенства» – ведь проверка на равенство всегда коммута-
Как решает этот вопрос анализатор? Правило такое: если анализатор ви- тивна.) Если перевернуть исходный пример, окажется, что следующий код
дит, что идентификатору присваивается значение до его использования, то ведет себя иначе:
он считается переменной; в противном случае это имя метода. (Отметим,
case value
что операция присваивания может и не выполняться: достаточно того, что when expression
интерпретатор ее видел.) некоторое действие
end
1.5.3. Предложение case в Ruby
Во всех современных языках есть та или иная форма многопутевого ветвления. В • В качестве примера рассмотрим строку str и образец (регулярное выраже-
C/C++ и Java это предложение switch, а в Pascal – предложение case. Служат они ние) pat, с которым эта строка успешно сопоставляется.
одной и той же цели и функционируют примерно одинаково. Выражение str =~ pat истинно, как в языке Perl. Поскольку Ruby опре-
Предложение case в Ruby похоже, но при ближайшем рассмотрении оказы- деляет противоположную семантику оператора =~ в классе Regexp, можно
вается настолько уникальным, что варианты ветвления, принятые в C и в Pascal, также сказать, что выражение pat =~ str истинно. Следуя той же логике,
68 Обзор Ruby Потренируйте свою интуицию 69

мы обнаруживаем, что истинно и pat === str (исходя из того, как определен выражения в какой-то ветви оказалось успешным, то следующие ветви не
оператор === в классе Regexp). Однако выражение str === pat истинным не вычисляются. Поэтому не стоит помещать в ветви case выражения, в кото-
является. А значит, фрагмент рых вызываются методы с побочными эффектами. (Впрочем, такие вызовы
case "Hello" в любом случае сомнительны). Имейте также в виду, что такое поведение
when /Hell/ может замаскировать ошибки, которые произошли бы во время выполне-
puts "Есть соответствие." ния, если бы выражение вычислялось. Например:
else case x
puts "Нет соответствия." when 1..10
end puts "Первая ветвь"
делает не то же самое, что фрагмент when foobar() # Возможен побочный эффект?
case /Hell/ puts "Вторая ветвь"
when "Hello" when 5/0 # Деление на нуль!
puts "Есть соответствие." puts "Третья ветвь"
else else
puts "Нет соответствия." puts "Четвертая ветвь"
end end

Если это вас смущает, просто постарайтесь запомнить. А если не смущает, тем Если x находится в диапазоне от 1 до 10, то метод foobar() не вызывается, а вы-
лучше! ражение 5 / 0 (которое, естественно, привело бы к ошибке) не вычисляется.
• Программисты, привыкшие к C, могут быть озадачены отсутствием предложе- 1.5.4. Рубизмы и идиомы
ний break в ветвях case. Такое использование break в Ruby необязательно (и не- Материал в этом разделе во многом пересекается с изложенным выше. Но не за-
допустимо). Связано это с тем, что «проваливание» редко бывает желательно думывайтесь особо, почему мы решили разбить его именно таким образом. Прос-
при многопутевом ветвлении. В конце каждой ветви when имеется неявный пе- то многие вещи трудно точно классифицировать и организовать единственно пра-
реход на конец предложения case. В этом отношении Ruby напоминает Pascal. вильным образом. Мы ставили себе задачу представить информацию в удобном
• Значения в каждой ветви case могут быть произвольными. На типы никаких для усвоения виде.
ограничений не налагается. Они не обязаны быть константами; допускаются Ruby проектировался как непротиворечивый и ортогональный язык. Но вмес-
и переменные, и сложные выражения. Кроме того, в ветви может проверять- те с тем это сложный язык, в котором есть свои идиомы и странности. Некоторые
ся попадание в диапазон. из них мы обсудим ниже.
• В ветвях case могут находиться пустые действия (пустые предложения). • С помощью ключевого слова alias можно давать глобальным переменным
Значения в разных ветвях не обязательно должны быть уникальными – до- и методам альтернативные имена (синонимы).
пускаются перекрытия, например: • Пронумерованные глобальные переменные $1, $2, $3 и т. д. не могут иметь
case x синонимов.
when 0 • Мы не рекомендуем использовать «специальные переменные» $=, $_, $/ и
when 1..5
им подобные. Иногда они позволяют написать более компактный код, но
puts "Вторая ветвь"
when 5..10
при этом он не становится более понятным. Поэтому в данной книге мы
puts "Третья ветвь" прибегаем к ним очень редко, что и вам рекомендуем.
else • Не путайте операторы диапазона .. и ... – первый включает верхнюю гра-
puts "Четвертая ветвь" ницу, второй исключает. Так, диапазон 5..10 включает число 10, а диапазон
end 5...10 – нет.
Если x принимает значение 0, ничего не делается. Для значения 5 печатает- • С диапазонами связана одна мелкая деталь, которая может вызвать путани-
ся строка «Вторая ветвь» – несмотря на то что 5 удовлетворяет и условию цу. Если дан диапазон m..n, то метод end вернет конечную его точку n, равно
в третьей ветви. как и его синоним last. Но те же методы возвращают значение n и для диа-
• Перекрытие ветвей допускается потому, что они вычисляются в строгом пазона m...n, хотя n не включается в него. Чтобы различить эти две ситуа-
порядке и выполняется закорачивание. Иными словами, если вычисление ции, предоставляется метод end_excluded?.
70 Обзор Ruby Потренируйте свою интуицию 71

• Не путайте диапазоны с массивами. Следующие два присваивания абсо- понятно для программистов, если только они не привыкли к языку, в ко-
лютно различны: тором переменные автоматически инициализируются нулем или пустым
x = 1..5 значением.
x = [1, 2, 3, 4, 5] • Такое поведение можно в некотором смысле обойти. Можно определить
Однако есть удобный метод to_a для преобразования диапазона в массив. операторы для объекта nil, так что в случае, когда начальное значение пере-
(Во многих других типах тоже есть такой метод.) менной равно nil, мы получим желаемый результат. Так, метод nil.+, при-
• Часто бывает необходимо присвоить переменной значение лишь в том слу- веденный ниже, позволит инициализировать объект типа String или Fixnum,
чае, когда у нее еще нет никакого значения. Поскольку «неприсвоенная» для чего достаточно вернуть аргумент other. Таким образом, nil + other бу-
переменная имеет значение nil, можно решить эту задачу так: x = x || 5 дет равно other.
или сокращенно x ||= 5. Имейте в виду, что значение false, а равно и nil, def nil.+(other)
будет при этом перезаписано. other
• В большинстве языков для обмена значений двух переменных нужна до- end
полнительная временная переменная. В Ruby наличие механизма множес- Мы привели этот код для иллюстрации возможностей Ruby, но стоит ли
твенного присваивания делает ее излишней: выражение x, y = y, x обме- поступать так на практике, оставляем на усмотрение читателя.
нивает значения x и y. • Уместно будет напомнить, что Class – это объект, а Object – это класс. Мы
• Четко отличайте класс от экземпляра. Например, у переменной класса попытаемся прояснить этот вопрос в следующей главе, а пока просто по-
@@foobar областью видимости является весь класс, а переменная экземпля- вторяйте это как мантру.
ра @foobar заново создается в каждом объекте класса. • Некоторые операторы нельзя перегружать, потому что они встроены в сам
• Аналогично метод класса ассоциирован с тем классом, в котором опреде- язык, а не реализованы в виде методов. К таковым относятся =, .., ..., and,
лен; он не принадлежит никакому конкретному объекту и не может вызы- or, not, &&, ||, !, != и !~. Кроме того, нельзя перегружать составные операто-
ваться от имени объекта. При вызове метода класса указывается имя клас- ры присваивания (+=, -= и т. д.). Это не методы и, пожалуй, даже не вполне
са, а при вызове метода экземпляра – имя объекта. операторы.
• В публикациях, посвященных Ruby, часто для обозначения метода экзем- • Имейте в виду, что хотя оператор присваивания перегружать нельзя, тем не
пляра применяют решеточную нотацию. Например, мы пишем File.chmod, менее возможно написать метод экземпляра с именем foo= (тогда станет до-
чтобы обозначить метод chmod класса File, и File#chmod для обозначения пустимым предложение x.foo = 5). Можете рассматривать знак равенства
метода экземпляра с таким же именем. Эта нотация не является частью как суффикс.
синтаксиса Ruby. Мы старались не пользоваться ей в этой книге.
• Напомним: «голый» оператор разрешения области видимости подразумева-
• В Ruby константы не являются истинно неизменными. Их нельзя изменять ет наличие Object перед собой, то есть ::Foo – то же самое, что Object::Foo.
в теле методов экземпляра, но из других мест это вполне возможно.
• Как уже говорилось, fail – синоним raise.
• Ключевое слово yield пришло из языка CLU и некоторым программис-
• Напомним, что определения в Ruby исполняются. Вследствие динамичес-
там может быть непонятно. Оно используется внутри итератора, чтобы пе-
кой природы языка можно, например, определить два метода совершенно
редать управление блоку, с которым итератор был вызван. В данном слу-
чае yield не означает, что нужно получить результат или вернуть значение. по-разному в зависимости от значения признака, проверяемого во время
Скорее, речь идет о том, чтобы уступить процессор для работы. выполнения.
• Составные операторы присваивания +=, -= и пр. – это не методы (собствен- • Напомним, что конструкция for (for x in a) на самом деле вызывает ите-
но, это даже не операторы). Это всего лишь «синтаксическая глазурь» или ратор each. Любой класс, в котором такой итератор определен, можно об-
сокращенная форма записи более длинной формы. Поэтому x += y значит ходить в цикле for.
в точности то же самое, что x = x + y. Если оператор + перегружен, то опе- • Не забывайте, что метод, определенный на верхнем уровне, добавляется в
ратор += «автомагически» учитывает новую семантику. модуль Kernel и, следовательно, становится членом класса Object.
• Из-за того, как определены составные операторы присваивания, их нельзя • Методы установки (например, foo=) должны вызываться от имени объекта,
использовать для инициализации переменных. Если первое обращение к иначе анализатор решит, что речь идет о присваивании переменной с таким
переменной x выглядит как x += 1, возникнет ошибка. Это интуитивно именем.
72 Обзор Ruby Потренируйте свою интуицию 73

• Напомним, что ключевое слово retry можно использовать в итераторах, но end


не в циклах общего вида. В контексте итератора оно заставляет заново ини- end
циализировать все параметры и возобновить текущую итерацию с начала. Это необязательно и в некоторых случаях даже нежелательно.
• Ключевое слово retry применяется также при обработке исключений. Не • Помните, что строки (strings) в некотором смысле двулики: их можно рас-
путайте два этих вида использования. сматривать как последовательность символов или как последовательность
• Метод объекта initialize всегда является закрытым. строчек (lines). Кому-то покажется удивительным, что итератор each опе-
• Когда итератор заканчивается левой фигурной скобкой (или словом end) и рирует строками (здесь под «строкой» понимается группа символов, завер-
возвращает значение, это значение можно использовать для вызова после- шающаяся разделителем записей, который по умолчанию равен символу
дующих методов, например: новой строки). У each есть синоним each_line. Если вы хотите перебирать
squares = [1,2,3,4,5].collect do |x| x**2 end.reverse символы, можете воспользоваться итератором each_byte. Итератор sort
# squares теперь равно [25,16,9,4,1] также оперирует строками. Для строк (strings) не существует итератора
each_index из-за возникающей неоднозначности. Действительно, хотим ли
• В конце программы на Ruby часто можно встретить идиому мы обрабатывать строку посимвольно или построчно? Все это со временем
if $0 == __FILE__ войдет в привычку.
Таким образом проверяется, исполняется ли файл как автономный кусок • Замыкание (closure) запоминает контекст, в котором было создано. Один
кода (true) или как дополнительный, например библиотека (false). Ти- из способов создать замыкание – использование объекта Proc. Например:
пичное применение – поместить некую «главную программу» (обычно с def power(exponent)
тестовым кодом) в конец библиотеки. proc {|base| base**exponent}
• Обычное наследование (порождение подкласса) обозначается символом <: end
class Dog < Animal
# ... square = power(2)
end cube = power(3)
Однако для создания синглетного класса (анонимного класса, который рас- a = square.call(11) # Результат равен 121.
ширяет единственный экземпляр) применяется символ <<: b = square.call(5) # Результат равен 25.
class << platypus c = cube.call(6) # Результат равен 216.
# ... d = cube.call(8) # Результат равен 512.
end
Обратите внимание, что замыкание «знает» значение показателя степени,
• При передаче блока итератору есть тонкое различие между фигурными скоб- переданное ему в момент создания.
ками ({}) и операторными скобками do-end. Связано оно с приоритетом: • Однако помните: в замыкании используется переменная, определенная во
mymethod param1, foobar do ... end внешней области видимости (что вполне допустимо). Это свойство может
# Здесь do-end связано с mymethod. оказаться полезным, но приведем пример неправильного использования:
mymethod param1, foobar { ... } $exponent = 0
# А здесь {} связано с именем foobar, предполагается, что это метод.
def power
• Традиционно в Ruby однострочные блоки заключают в фигурные скобки, а proc {|base| base**$exponent}
многострочные – в скобки do-end, например: end
my_array.each { |x| puts x }
$exponent = 2
my_array.each do |x| square = power
print x
if x % 2 == 0 $exponent = 3
puts " четно." cube = power
else
puts " нечетно." a = square.call(11) # Неверно! Результат равен 1331.
74 Обзор Ruby Потренируйте свою интуицию 75

b = square.call(5) # Неверно! Результат равен 125. Переменная экземпляра класса @y в предыдущем примере – в действитель-
ности атрибут объекта класса Myclass, являющегося экземпляром класса
# Оба результата неверны, поскольку используется ТЕКУЩЕЕ Class. (Напомним, что Class – это объект, а Object – это класс.) На пере-
# значение $exponent. Так было бы даже в том случае, когда
менные экземпляра класса нельзя ссылаться из методов экземпляра и, во-
# используется локальная переменная, покинувшая область
# видимости (например, с помощью define_method).
обще говоря, они не очень полезны.
• attr, attr_reader, attr_writer и attr_accessor – сокращенная запись для
c = cube.call(6) # Результат равен 216. определения методов чтения и установки атрибутов. В качестве аргумен-
d = cube.call(8) # Результат равен 512. тов они принимают символы (экземпляры класса Symbol).
• Напоследок рассмотрим несколько искусственный пример. Внутри блока • Присваивание переменной, имя которой содержит оператор разрешения
итератора times создается новый контекст, так что x – локальная перемен- области видимости, недопустимо. Например, Math::PI = 3.2 – ошибка.
ная. Переменная closure уже определена на верхнем уровне, поэтому для
блока она не будет локальной. 1.5.5. Ориентация на выражения и прочие вопросы
closure = nil # Определим замыкание, чтобы его имя было известно. В Ruby выражения важны почти так же, как предложения. Для программиста на C
1.times do # Создаем новый контекст. это звучит знакомо, а для программиста на Pascal – откровенная нелепость. Но
x = 5 # Переменная x локальная в этом блоке. Ruby ориентирован на выражения даже в большей степени, чем C.
closure = Proc.new { puts "В замыкании, x = #{x}" } Заодно в этом разделе мы остановимся на паре мелких вопросов, касающихся
end регулярных выражений; считайте это небольшим бонусом.
• В Ruby любое присваивание возвращает то же значение, которое стоит в
x = 1
правой части. Поэтому иногда мы можем немного сократить код, как пока-
# Определяем x на верхнем уровне. зано ниже, но будьте осторожны, имея дело с объектами! Не забывайте, что
это почти всегда ссылки.
closure.call # Печатается: В замыкании, x = 5 x = y = z = 0 # Все переменные сейчас равны 0.
Обратите внимание, что переменная x, которой присвоено значение 1, – это
новая переменная, определенная на верхнем уровне. Она не совпадает с од- a = b = c = [] # Опасно! a, b и c ссылаются
ноименной переменной, определенной внутри блока. Замыкание печата- # на ОДИН И ТОТ ЖЕ пустой массив.
x = 5
ет 5, так как запоминает контекст своего создания, в котором была опре-
y = x += 2 # Сейчас x и y равны 7.
делена переменная x со значением 5.
• Переменные с именами, начинающимися с одного символа @, определен- Напомним однако, что значения типа Fixnum и им подобные хранятся не-
ные внутри класса, – это, вообще говоря, переменные экземпляра. Однако посредственно, а не как ссылки на объекты.
если они определены вне любого метода, то становятся переменными эк- • Многие управляющие конструкции возвращают значения, в частности if,
земпляра класса. (Это несколько противоречит общепринятой терминоло- unless и case. Следующий код корректен; он показывает, что при принятии
гии ООП, в которой «экземпляр класса» – то же самое, что и «экземпляр» решения ветви могут быть выражениями, а не полноценными предложени-
или «объект».) Пример: ями.
class Myclass a = 5
x = if a < 8 then 6 else 7 end # x равно 6.
@x = 1 # Переменная экземпляра класса.
@y = 2 # Еще одна. y = if a < 8 # y тоже равно 6;
6 # предложение if может располагаться
def mymethod else # на одной строке
@x = 3 # Переменная экземпляра. 7 # или на нескольких.
# Заметим, что в этой точке @y недоступна. end
end
# unless тоже работает; z присваивается значение 4.
end z = unless x == y then 3 else 4 end
76 Обзор Ruby Жаргон Ruby 77

t = case a # t получает В Ruby термин «атрибут» носит неофициальный характер. Можно считать,
when 0..3 # значение что атрибут – это переменная экземпляра, которая раскрывается внешнему миру
"low" # medium. с помощью одного из методов семейства attr. Но тут нет полной определенности:
when 4..6 могут существовать методы foo и foo=, не соответствующие переменной @foo, как
"medium" можно было бы ожидать. И, конечно, не все переменные экземпляра считаются ат-
else
рибутами. Как обычно, нужно придерживаться здравого смысла.
"high"
end
Атрибуты в Ruby можно подразделить на методы чтения (reader) и установки
(writer). Метод доступа, или акцессор (accessor), является одновременно методом
Здесь мы сделали такие отступы, будто case является присваиванием. Мы чтения и установки. Это согласуется с названием метода attr_accessor, но проти-
воспринимаем такую запись спокойно, хотя вам она может не понравиться. воречит принятой в других сообществах семантике, согласно которой акцессор да-
• Отметим, что циклы while и until, напротив, не возвращают никаких по- ет доступ только для чтения.
лезных значений; обычно их значением является nil: Оператор === имеется только в Ruby (насколько мне известно). Обыкновен-
i = 0 но он называется оператором ветвящегося равенства (case equality operator), пос-
x = while (i < 5) # x равно nil. кольку неявно используется в предложениях case. Но это название, как я уже гово-
puts i+=1 рил, не вполне точно, потому что речь идет не только о «равенстве». В данной книге
end я часто употребляю термин «оператор отношения» (relationship operator). Изоб-
рел его не я, но проследить происхождение мне не удалось, к тому же он употреб-
• Тернарный оператор можно использовать как в предложениях, так и в вы-
ляется нечасто. Жаргонное название – «оператор тройного равенства» (threequal
ражениях. В силу синтаксических причин (или ограничений анализатора)
operator) или просто «три равно».
скобки здесь обязательны:
Оператор <=>, наверное, лучше всего называть оператором сравнения. На жарго-
x = 6 не его называют космическим оператором (spaceship operator), поскольку он напо-
y = x == 5 ? 0 : 1 # y равно 1. минает летающую тарелку – так ее изображали в старых видеоиграх.
x == 5 ? puts("Привет") : puts("Пока") # Печатается: "Пока"
Термин «поэтический режим» (poetry mode) подчеркивает, что можно опус-
• Предложение return в конце метода можно опускать. Метод всегда возвра- кать ненужные знаки препинания и лексемы (насмешливый намек на отношение
щает значение последнего вычисленного выражения, в каком бы месте это поэтов к пунктуации на протяжении последних шестидесяти лет). Поэтический
вычисление ни происходило. режим также часто означает «опускание скобок при вызове метода».
• Когда итератор вызывается с блоком, последнее выражение, вычисленное some_method(1, 2, 3) # Избыточные скобки.
в блоке, возвращается в качестве значения блока. Если при этом в теле ите- some_method 1, 2, 3 # "Поэтический режим".
ратора есть предложение x = yield, то x будет присвоено это значение. Но мне этот принцип представляется более общим. Например, когда хэш пере-
дается в качестве последнего или единственного параметра, можно опускать фи-
• Регулярные выражения. Напомним, что после регулярного выражения мож-
гурные скобки. В конце строки можно не ставить точку с запятой (а потому никто
но написать модификатор многострочности /m, и в этом случае точка (.) бу-
этого и не делает). В большинстве случаев разрешается опускать ключевое слово
дет сопоставляться с символом новой строки.
then в предложениях if и case.
• Регулярные выражения. Опасайтесь соответствий нулевой длины. Если Некоторые программисты заходят еще дальше, опуская скобки даже в опреде-
все элементы регулярного выражения необязательны, то такому образцу лении методов, но большинство так не поступает:
будет соответствовать «ничто», причем соответствие всегда будет найде- def my_method(a, b, c) # Можно и так: def my_method a, b, c
но в начале строки. Это типичная ошибка, особенно часто ее допускают # ...
новички. end
Стоит отметить, что в некоторых случаях сложность грамматики Ruby приво-
дит к сбоям анализатора. Во вложенных вызовах методов скобки для ясности луч-
1.6. Жаргон Ruby ше оставлять. Иногда в текущей версии Ruby выводятся предупреждения:
Заново начинать учить английский для освоения Ruby необязательно. Но нужно def alpha(x)
знать кое-какие жаргонные выражения, обычные в сообществе. Некоторые из них x*2
имеют другой смысл, чем принято в компьютерном мире. Им и посвящен настоя- end
щий раздел. def beta(y)
78 Обзор Ruby Заключение 79

y*3 Кто-то предложил использовать термин eigenclass (класс в себе) – производ-


end ное от немецкого слова eigen (свой собственный), коррелирующее с термином
«собственное значение» (eigenvalue), применяемым в математике и физике. Ос-
gamma = 5
троумно, но в сообществе не прижилось и некоторым активно не нравится.
delta = alpha beta gamma # 30 -- то же, что alpha(beta(gamma))
# Выдается предупреждение: Вернемся к предыдущему примеру. Поскольку метод hyphenate не существует
# warning: parenthesize argument(s) for future version ни в каком-либо другом объекте, ни в классе, это синглетный метод данного объ-
# предупреждение: заключайте аргумент(ы) в скобки для совместимости с екта. Это не вызывает неоднозначности. Иногда сам объект называется синглет-
# с будущими версиями ным, поскольку он единственный в своем роде – больше ни у кого такого метода
Термин duck typing («утиная типизация» или просто «утипизация»), насколь- нет.
ко я знаю, принадлежит Дейву Томасу (Dave Thomas) и восходит к поговорке: «ес- Однако вспомним, что в Ruby сам класс является объектом. Поэтому мы мо-
ли кто-то выглядит как утка, ковыляет как утка и крякает как утка, то, наверное, жем добавить метод в синглетный класс класса, и этот метод будет уникален для
это и есть утка». Точный смысл термина «утипизация» – тема для дискуссий, но объекта, который – по чистой случайности – оказался классом. Пример:
мне кажется, что это намек на тенденцию Ruby заботиться не столько о точном class MyClass
классе объекта, сколько о том, какие методы для него можно вызывать и какие опе- class << self # Альтернатива: def self.hello
рации над ним можно выполнять. В Ruby мы редко пользуемся методом is_a? или def hello # или: def MyClass.hello
kind_of, зато гораздо чаще прибегаем к методу respond_to?. Обычное дело – прос- puts "Привет от #{self}!"
то передать объект методу, зная, что при неправильном использовании будет воз- end
буждено исключение. Так оно рано или поздно и случается. end
end
Унарную звездочку, которая служит для расширения массива, можно было бы
назвать оператором расширения массива, но не думаю, что кто-нибудь слышал та- Поэтому необязательно создавать объект класса MyClass для вызова этого ме-
кое выражение. В хакерских кругах ходят словечки «звездочка» (star) и «расплю- тода.
щивание» (splat), а также производные определения – «расплющенный» (splatted) MyClass.hello # Привет от MyClass!
и «сплющенный» (unsplatted). Дэвид Алан Блэк придумал остроумное название Впрочем, вы, наверное, заметили, что это не что иное, как метод класса в Ruby.
«унарный оператор линеаризации» (unary unarray operator)*. Иными словами, метод класса – синглетный метод объекта-класса. Можно также
Термин синглет (singleton) многие считают перегруженным. Это вполне обычное сказать, что это синглетный метод, определенный для объекта, который случайно
английское слово, означающее вещь, существующую в единственном экземпляре. По- оказался классом.
ка мы используем его в качестве модификатора, никакой путаницы не возникает. Осталась еще парочка терминов. Переменная класса – это, разумеется, то, имя
Но Singleton (Одиночка) – это еще и хорошо известный паттерн проектирова- чего начинается с двух символов @. Возможно, название неудачно из-за нетриви-
ния, относящийся к классу, для которого может существовать лишь один объект. ального поведения относительно наследования. Переменная экземпляра класса –
В Ruby для реализации этого паттерна имеется библиотека singleton. нечто совсем иное. Это обычная переменная экземпляра; только объект, которому
Синглетный класс (singleton class) в Ruby – подобная классу сущность, мето- она принадлежит, является классом. Дополнительную информацию по этой теме
ды которой хранятся на уровне объекта, а не класса. Пожалуй, это не «настоящий
вы найдете в главе 11.
класс», потому что его нельзя инстанцировать. Ниже приведен пример открытия
синглетного класса для строкового объекта с последующим добавлением метода:
str = "hello" 1.7. Заключение
class << str # Альтернатива: На этом завершается наш обзор объектно-ориентированного программирования
def hyphenated # def str.hyphenated и краткая экскурсия по языку Ruby. В последующих главах изложенный матери-
self.split("").join("-") ал будет раскрыт более полно.
end
Хотя в мои намерения не входило «учить Ruby» новичков, не исключено, что
end
даже начинающие программисты на Ruby почерпнут что-то полезное из этой гла-
str.hyphenated # "h-e-l-l-o" вы. Как бы то ни было, последующие главы будут полезны «рубистам» начально-
го и среднего уровня. Надеюсь, что даже опытные программисты на Ruby найдут
* Прошу читателя извинить меня за это нагромождение выдуманных слов, но жаргон вряд ли следу- для себя что-то новенькое.
ет переводить академически правильным языком. Хочется надеяться, что это все же лучше, чем каль-
ки английских слов. (Прим. перев.)
Альтернативная нотация для представления строк 81

Простейшая строка в Ruby заключается в одиночные кавычки. Такие строки


воспринимаются буквально; в качестве управляющих символов в них распозна-
ются только экранированная одиночная кавычка (\’) и экранированный символ
обратной косой черты (\\):
s1 = 'Это строка' # Это строка.
Глава 2. Строки s2 = 'Г-жа О\'Лири'
s3 = 'Смотри в C:\\TEMP'
# Г-жа О'Лири.
# Смотри в C:\TEMP.
Строки, заключенные в двойные кавычки, обладают большей гибкостью. В них
Когда-то элементарными кирпичиками мироздания считались атомы, допустимо много других управляющих последовательностей, в частности для пред-
потом протоны, потом кварки. Теперь таковыми считаются струны*. ставления символов забоя, табуляции, возврата каретки и перевода строки. Можно
Дэвид Гросс, профессор теоретической физики, также включать произвольные символы, представленные восьмеричными цифрами:
Принстонский университет s1 = "Это знак табуляции: (\t)"
s2 = "Несколько символов забоя: xyz\b\b\b"
s3 = "Это тоже знак табуляции: \011"
В начале 1980-х годов один профессор информатики, начиная первую лекцию по s4 = "А это символы подачи звукового сигнала: \a \007"
структурам данных, не представился студентам, не сказал, как называется курс, не
Внутри строки, заключенной в двойные кавычки, могут встречаться даже вы-
рассказал о его программе и не порекомендовал никаких учебников – а вместо это-
ражения (см. раздел 2.21).
го сходу спросил: «Какой тип данных самый важный?»
Было высказано несколько предположений. Когда профессор услышал слово
«указатели», он выразил удовлетворение, но все-таки не согласился со студентом, 2.2. Альтернативная нотация для представления строк
а высказал свое мнение: «Самым важным является тип символ». Иногда встречаются строки, в которых много метасимволов, например одиночных
У него были на то основания. Компьютерам предназначено быть нашими слу- и двойных кавычек и т. д. В этом случае можно воспользоваться конструкциями %q
гами, а не хозяевами, а человеку понятны только символьные данные. (Есть, ко- и %Q. Вслед за ними должна идти строка, обрамленная с обеих сторон символами-
нечно, люди, которые без труда читают и двоичные данные, но о них мы сейчас го- ограничителями; лично я предпочитаю квадратные скобки ([ ]).
ворить не будем.) Символы, а стало быть, и строки, позволяют человеку общаться При этом %q ведет себя как одиночные кавычки, а %Q – как двойные.
с компьютером. Любую информацию, в том числе и текст на естественном языке, S1 = %q[Как сказал Магритт, "Ceci n'est pas une pipe."]
можно закодировать в виде строк. s2 = %q[Это не табуляция: (\t)] # Равнозначно 'Это не табуляция: \t'
Как и в других языках, строка в Ruby – просто последовательность символов. s3 = %Q[А это табуляция: (\t)] # Равнозначно "А это табуляция: \t"
Подобно другим сущностям, строки являются полноценными объектами. В про- В обоих вариантах можно применять и другие ограничители, помимо квадрат-
граммах приходится выполнять разнообразные операции над строками: конкате- ных скобок: круглые, фигурные, угловые скобки.
нировать, выделять лексемы, анализировать, производить поиск и замену и т. д. s1 = %q(Билл сказал: "Боб сказал: 'This is a string.'")
Язык Ruby позволяет все это делать без труда. s2 = %q{Другая строка.}
Почти всюду в этой главе предполагается, что байт – это символ. Но при ра- s3 = %q<В этой строке есть специальные символы '"[](){}.>
боте в многоязычной среде это не совсем так. Вопросы интернационализации об- Допустимы также непарные ограничители. В этом качестве может выступать
суждаются в главе 4. любой символ, кроме букв, цифр и пропусков (пробелов и им подобных), кото-
рый имеет визуальное представление и не относится к числу перечисленных вы-
2.1. Представление обычных строк ше парных ограничителей.
Строка в Ruby – это последовательность 8-битовых байтов. Она не завершается s1 = %q:"Я думаю, что это сделала корова г-жи О'Лири," сказал он.:
нулевым символом, как в C, и, следовательно, может содержать такие символы. В s2 = %q*\r – это control-M, а \n - это control-J.*
строке могут быть символы с кодами больше 0xFF, но такие строки имеют смысл,
лишь если выбран некоторый набор символов (кодировка). Дополнительную ин- 2.3. Встроенные документы
формацию о кодировках вы найдете в главе 4.
Для представления длинной строки, занимающей несколько строк в тексте, мож-
* В английском языке словом «string» обозначается как «строка», так и «струна» (Прим. перев.) но, конечно, воспользоваться обычными строками в кавычках:
82 Строки Построчная обработка 83

str = "Три девицы под окном arr = self.split("\n") # Разбить на строки.


Пряли поздно вечерком..." arr.map! {|x| x.sub!(/\s*\|/,"")} # Удалить начальные символы.
Но тогда отступ окажется частью строки. str = arr.join("\n") # Объединить в одну строку.
self.replace(str) # Подменить исходную строку.
Можно вместо этого воспользоваться встроенным документом, изначально
end
предназначенным для многострочных фрагментов. (Идея и сам термин заимство- end
ваны из более старых языков.) Синтаксически он начинается с двух знаков <<, за
которыми следует концевой маркер, нуль или более строк текста и в завершение Для ясности я включил подробные комментарии. В этом коде применяются кон-
тот же самый концевой маркер в отдельной строке: струкции, которые будут рассмотрены ниже – как в этой, так и в последующих главах.
Используется этот метод так:
str = <<EOF
Три девицы под окном str = <<end.margin
Пряли поздно вечерком... |Этот встроенный документ имеет "левое поле"
EOF |на уровне вертикальной черты в каждой строке.
|
Но следите внимательно, чтобы после завершающего концевого маркера не | Можно включать цитаты,
было пробелов. В текущей версии Ruby маркер в такой ситуации не распознается. | делать выступы и т. д.
Встроенные документы могут быть вложенными. В примере ниже показано, end
как передать методу три представленных таким образом строки:
В качестве концевого маркера естественно употребить слово end. (Впрочем,
some_method(<<str1, <<str2, <<str3)
это дело вкуса. Выглядит такой маркер как зарезервированное слово end, но на са-
первый кусок
текста... мом деле этот выбор ничуть не хуже любого другого.) Каждая строка начинается
str1 с символа вертикальной черты, но эти символы потом отбрасываются вместе с на-
второй кусок... чальными пробелами.
str2
третий кусок
текста. 2.4. Получение длины строки
str3 Для получения длины строки служит метод length. У него есть синоним size.
По умолчанию встроенный документ ведет себя как строка в двойных кавыч- str1 = "Карл"
ках, то есть внутри него интерпретируются управляющие последовательности и x = str1.length # 4
интерполируются выражения. Но если концевой маркер заключен в одиночные str2 = "Дойль"
кавычки, то и весь документ ведет себя как строка в одиночных кавычках: x = str2.size # 5
str = <<'EOF'
Это не знак табуляции: \t
а это не символ новой строки: \n
2.5. Построчная обработка
EOF Строка в Ruby может содержать символы новой строки. Например, можно прочи-
Если концевому маркеру встроенного документа предшествует дефис, то мар- тать в память файл и сохранить его в виде одной строки. Применяемый по умол-
кер может начинаться с красной строки. При этом удаляются только пробелы из чанию итератор each в этом случае перебирает отдельные строки:
той строки, на которой расположен сам маркер, но не из предшествующих ей строк str = "Когда-то\nдавным-давно...\nКонец\n"
документа. num = 0
str.each do |line|
str = <<-EOF
num += 1
Каждая из этих строк
print "Строка #{num}: #{line}"
начинается с пары
end
пробелов.
EOF Выполнение этого кода дает следующий результат:
Опишу стиль, который нравится лично мне. Предположим, что определен та- Строка 1: Когда-то
кой метод margin: Строка 2: давным-давно...
Строка 3: Конец
class String
def margin Альтернативно можно было бы воспользоваться методом each_with_index.
84 Строки Разбиение строки на лексемы 85

2.6. Побайтовая обработка end


title1 = "Calling All Cars"
Поскольку на момент написания этой книги язык Ruby еще не поддерживал ин- title2 = "The Call of the Wild"
тернационализацию в полной мере, то символ и байт – по существу одно и то же. # При стандартном сравнении было бы напечатано "yes".
Для последовательной обработки символов пользуйтесь итератором each_byte: if title1 < title2
str = "ABC" puts "yes"
str.each_byte {|char| print char, " " } else
# Результат: 65 66 67. puts "no" # А теперь печатается "no".
end
В текущей версии Ruby строку можно преобразовать в массив односимволь-
ных строк с помощью метода scan, которому передается простое регулярное выра- Обратите внимание, что мы «сохранили» старый метод <=> с помощью ключе-
жение, соответствующее одному символу: вого слова alias и в конце вызвали его. Если бы мы вместо этого воспользовались
str = "ABC" методом <, то был бы вызван новый метод <=>, что привело бы к бесконечной ре-
chars = str.scan(/./) курсии и в конечном счете к аварийному завершению программы.
chars.each {|char| print char, " " } Отметим также, что оператор == не вызывает метод <=> (принадлежащий клас-
# Результат: A B C. су-примеси Comparable). Это означает, что для специализированной проверки строк
на равенство пришлось бы отдельно переопределить метод ==. Но в рассмотренном
2.7. Специализированное сравнение строк случае в этом нет необходимости.
Допустим, что мы хотим сравнивать строки без учета регистра. Для этого есть
В язык Ruby уже встроен механизм сравнения строк: строки сравниваются в при-
встроенный метод casecmp; надо лишь добиться, чтобы он вызывался при сравне-
вычном лексикографическом порядке (то есть на основе упорядочения, присуще-
нии. Вот как это можно сделать:
го данному набору символов). Но при желании можно задать собственные прави-
ла сравнения любой сложности. class String
def <=>(other)
Предположим, например, что мы хотим игнорировать английские артикли a,
casecmp(other)
an и the, если они встречаются в начале строки, а также не обращать внимания на
end
большинство знаков препинания. Для этого следует переопределить встроенный end
метод <=> (он вызывается из методов <, <=, > и >=). В листинге 2.1 показано, как это
Есть и более простой способ:
сделать.
class String
alis <=> casecmp(other)
Листинг 2.1. Специализированное сравнение строк
end
class String
alias old_compare <=> Но это не все. Надо еще переопределить оператор ==, чтобы он вел себя точ-
def <=>(other) но так же:
a = self.dup class String
b = other.dup def ==(other)
# Удалить знаки препинания. casecmp(other) == 0
a.gsub!(/[\,\.\?\!\:\;]/, "") end
b.gsub!(/[\,\.\?\!\:\;]/, "") end
# Удалить артикли из начала строки.
Теперь все строки будут сравниваться без учета регистра. И при всех операци-
a.gsub!(/^(a |an |the )/i, "")
b.gsub!(/^(a |an |the )/i, "") ях сортировки, которые определены в терминах метода <=>, регистр тоже не будет
# Удалить начальные и хвостовые пробелы. учитываться.
a.strip!
b.strip!
# Вызвать старый метод <=>.
2.8. Разбиение строки на лексемы
a.old_compare(b) Метод split разбивает строку на части и возвращает массив лексем. Ему переда-
end ются два параметра: разделитель и максимальное число полей (целое).
86 Строки Строки в качестве объектов ввода/вывода 87

По умолчанию разделителем является пробел, а точнее, значение специальной sep = ss.scan(/\W+/) # Получить следующий фрагмент,
переменной $; или ее англоязычного эквивалента $FIELD_SEPARATOR. Если же пер- # не являющийся словом.
вым параметром задана некоторая строка, то она и будет использоваться в качес- break if sep.nil?
end
тве разделителя лексем.
s1 = "Была темная грозовая ночь."
words = s1.split # ["Была", "темная", "грозовая", "ночь] 2.9. Форматирование строк
s2 = "яблоки, груши, персики"
list = s2.split(", ") # ["яблоки", "груши", "персики"]
В Ruby, как и в языке C, для этой цели предназначен метод sprintf. Он принимает
s3 = "львы и тигры и медведи" строку и список выражений, а возвращает строку. Набор спецификаторов в фор-
zoo = s3.split(/ и /) # ["львы", "тигры", "медведи"] матной строке мало чем отличается от принятого в функции sprintf (или printf)
из библиотеки C.
Второй параметр ограничивает число возвращаемых полей, при этом действу-
ют следующие правила: name = "Боб"
age = 28
1. Если параметр опущен, то пустые поля в конце отбрасываются. str = sprintf("Привет, %s... Похоже, тебе %d лет.", name, age)
2. Если параметр – положительное число, то будет возвращено не более ука- Спрашивается, зачем нужен этот метод, если можно просто интерполиро-
занного числа полей (если необходимо, весь «хвост» строки помещается в вать значения в строку с помощью конструкции #{expr}? А затем, что sprintf по-
последнее поле). Пустые поля в конце сохраняются. зволяет выполнить дополнительное форматирование – например, задать макси-
3. Если параметр – отрицательное число, то количество возвращаемых полей мальную ширину поля или максимальное число цифр после запятой, добавить
не ограничено, а пустые поля в конце сохраняются. или подавить начальные нули, выровнять строки текста по левой или правой
Ниже приведены примеры: границе и т. д.
str = "alpha,beta,gamma,," str = sprintf("%-20s %3d", name, age)
list1 = str.split(",") # ["alpha","beta","gamma"] В классе String есть еще метод %, который делает почти то же самое. Он при-
list2 = str.split(",",2) # ["alpha", "beta,gamma,,"] нимает одно значение или массив значений любых типов:
list3 = str.split(",",4) # ["alpha", "beta", "gamma", ","] str = "%-20s %3d" % [name, age] # То же, что и выше
list4 = str.split(",",8) # ["alpha", "beta", "gamma", "", ""]
list5 = str.split(",",-1) # ["alpha", "beta", "gamma", "", ""] Имеются также методы ljust, rjust и center; они принимают длину резуль-
тирующей строки и дополняют ее до указанной длины пробелами, если это необ-
Для сопоставления строки с регулярным выражением или с другой строкой
ходимо.
служит метод scan:
str = "Моби Дик"
str = "I am a leaf on the wind..."
s1 = str.ljust(12) # "Моби Дик"
# Строка интерпретируется буквально, а не как регулярное выражение.
s2 = str.center(12) # " Моби Дик "
arr = str.scan("a") # ["a","a","a"]
s3 = str.rjust(12) # " Моби Дик"
# При сопоставлении с регулярным выражением возвращаются все соответствия.
arr = str.scan(/\w+/) # ["I", "am", "a", "leaf", "on", "the", Можно задать и второй параметр, который интерпретируется как строка за-
"wind"] полнения (при необходимости она будет урезана):
# Можно задать блок. str = "Капитан Ахав"
str.scan(/\w+/) {|x| puts x } s1 = str.ljust(20,"+") # "Капитан Ахав++++++++"
Класс StringScanner из стандартной библиотеки отличается тем, что сохраня- s2 = str.center(20,"-") # "——Капитан Ахав——"
ет состояние сканирования, а не выполняет все за один раз: s3 = str.rjust(20,"123") # "12312312Капитан Ахав"
require 'strscan'
str = "Смотри, как я парю!" 2.10. Строки в качестве объектов ввода/вывода
ss = StringScanner.new(str)
loop do Помимо методов sprintf и scanf, есть еще один способ имитировать ввод/вывод
word = ss.scan(/\w+/) # Получать по одному слову. в строку: класс StringIO.
break if word.nil? Из-за сходства с объектом IO мы рассмотрим его в главе, посвященной вводу/
puts word выводу (см. раздел 10.1.24).
88 Строки Вычленение и замена подстрок 89

2.11. Управление регистром пара объектов класса Fixnum, диапазон, регулярное выражение или строка. Ниже
мы рассмотрим все варианты.
В классе String есть множество методов управления регистром. В этом разделе мы
Если задана пара объектов класса Fixnum, то они трактуются как смещение от
приведем их краткий обзор.
начала строки и длина, а возвращается соответствующая подстрока.
Метод downcase переводит символы всей строки в нижний регистр, а метод
str = "Шалтай-Болтай"
upcase – в верхний:
sub1 = str[7,4] # "Болт"
s1 = "Бостонское чаепитие" sub2 = str[7,99] # "Болтай" (выход за границу строки допускается)
s2 = s1.downcase # "бостонское чаепитие" sub3 = str[10,-4] # nil (отрицательная длина)
s3 = s2.upcase # "БОСТОНСКОЕ ЧАЕПИТИЕ"
Важно помнить, что это именно смещение и длина (число символов), а не на-
Метод capitalize представляет первый символ строки в верхнем регистре, а чальное и конечное смещение.
все остальные – в нижнем: Если индекс отрицателен, то отсчет ведется от конца строки. В этом случае ин-
s4 = s1.capitalize # "Бостонское чаепитие" декс начинается с единицы, а не с нуля. Но при нахождении подстроки указанной
s5 = s2.capitalize # "Бостонское чаепитие" длины все равно берутся символы правее, а не левее начального:
s6 = s3.capitalize # "Бостонское чаепитие"
str1 = "Алиса"
Метод swapcase изменяет регистр каждой буквы на противоположный: sub1 = str1[-3,3] # "иса"
s7 = "ЭТО БЫВШИЙ попугай." str2 = "В Зазеркалье"
s8 = s7.swapcase # "это бывший ПОПУГАЙ." sub3 = str2[-8,6] # "зеркал"
Начиная с версии 1.8, в язык Ruby включен метод casecmp, который работает Можно задавать диапазон. Он интерпретируется как диапазон позиций внут-
аналогично стандартному методу <=>, но игнорирует регистр: ри строки. Диапазон может включать отрицательные числа, но в любом случае ниж-
n1 = "abc".casecmp("xyz") # -1 няя граница не должна быть больше верхней. Если диапазон «инвертированный»
n2 = "abc".casecmp("XYZ") # -1 или нижняя граница оказывается вне строки, возвращается nil:
n3 = "ABC".casecmp("xyz") # -1 str = "Уинстон Черчилль"
n4 = "ABC".casecmp("abc") # 0 sub1 = str[8..13] # "Черчил"
n5 = "xyz".casecmp("abc") # 1 sub2 = str[-4..-1] # "илль"
У каждого из перечисленных методов имеется аналог, осуществляющий моди- sub3 = str[-1..-4] # nil
sub4 = str[25..30] # nil
фикацию «на месте» (upcase!, downcase!, capitalize!, swapcase!).
Не существует встроенных методов, позволяющих узнать регистр буквы, но Если задано регулярное выражение, то возвращается строка, соответствующая
это легко сделать с помощью регулярных выражений: образцу. Если соответствия нет, возвращается nil:
if string =~ /[a-z]/ str = "Alistair Cooke"
puts "строка содержит символы в нижнем регистре" sub1 = str[/l..t/] # "list"
end sub2 = str[/s.*r/] # "stair"
if string =~ /[A-Z]/ sub3 = str[/foo/] # nil
puts "строка содержит символы в верхнем регистре" Если задана строка, то она и возвращается, если встречается в качестве под-
end строки в исходной строке; в противном случае возвращается nil:
if string =~ /[A-Z]/ and string =~ /a-z/
puts "строка содержит символы в разных регистрах" str = "theater"
end sub1 = str["heat"] # "heat"
if string[0..0] =~ /[A-Z]/ sub2 = str["eat"] # "eat"
puts "строка начинается с прописной буквы" sub3 = str["ate"] # "ate"
end sub4 = str["beat"] # nil
sub5 = str["cheat"] # nil
Отметим, что все эти методы не учитывают местные особенности (locale).
Наконец, в тривиальном случае, когда в качестве индекса задано одно число
Fixnum, возвращается ASCII-код символа в соответствующей позиции (или nil,
2.12. Вычленение и замена подстрок если индекс выходит за границы строки):
В Ruby к подстрокам можно обращаться разными способами. Обычно применя- str = "Aaron Burr"
ются квадратные скобки, как для массивов, но внутри скобок может находиться ch1 = str[0] # 65
90 Строки Поиск в строке 91

ch1 = str[1] # 97 Метод Regexp.last_match эквивалентен действию специальной переменной $&


ch3 = str[99] # nil (она же $MATCH).
Важно понимать, что все описанные выше способы могут использоваться не
только для доступа к подстроке, но и для ее замены: 2.14. Поиск в строке
str1 = "Шалтай-Болтай" Помимо различных способов доступа к подстрокам, есть и другие методы поиска
str1[7,3] = "Хва" # "Шалтай-Хватай"
str2 = "Алиса"
в строке. Метод index возвращает начальную позицию заданной подстроки, сим-
str2[-3,3] = "ександра" # "Александра" вола или регулярного выражения. Если подстрока не найдена, возвращается nil:
str3 = "В Зазеркалье" str = "Albert Einstein"
str3[-9,9] = "стеколье" # "В Застеколье" pos1 = str.index(?E) # 7
str4 = "Уинстон Черчилль" pos2 = str.index("bert") # 2
str4[8..11] = "Х" # "Уинстон Хилль" pos3 = str.index(/in/) # 8
str5 = "Alistair Cooke" pos4 = str.index(?W) # nil
str5[/e$/] ="ie Monster" # "Alistair Cookie Monster" pos5 = str.index("bart") # nil
str6 = "theater" pos6 = str.index(/wein/) # nil
str6["er"] = "re" # "theatre" Метод rindex начинает поиск с конца строки. Но номера позиций отсчитыва-
str7 = "Aaron Burr" ются тем не менее от начала:
str7[0] = 66 # "Baron Burr"
str = "Albert Einstein"
Присваивание выражения, равного nil, не оказывает никакого действия. pos1 = str.rindex(?E) # 7
pos2 = str.rindex("bert") # 2
2.13. Подстановка в строках pos3 = str.rindex(/in/)
pos4 = str.rindex(?W)
#
#
13 (найдено самое правое соответствие)
nil
Мы уже видели, как выполняются простые подстановки. Методы sub и gsub предо- pos5 = str.rindex("bart") # nil
ставляют более развитые средства, основанные на сопоставлении с образцом. Име- pos6 = str.rindex(/wein/) # nil
ются также варианты sub! и gsub!, позволяющие выполнить подстановку «на месте». Метод include? сообщает, встречается ли в данной строке указанная подстро-
Метод sub заменяет первое вхождение строки, соответствующей образцу, дру- ка или один символ:
гой строкой или результатом вычисления блока: str1 = "mathematics"
s1 = "spam, spam, and eggs" flag1 = str1.include? ?e # true
s2 = s1.sub(/spam/,"bacon") # "bacon, spam, and eggs" flag2 = str1.include? "math" # true
s3 = s2.sub(/(\w+), (\w+),/,'\2, \1,') # "spam, bacon, and eggs" str2 = "Daylight Saving Time"
s4 = "Don't forget the spam." flag3 = str2.include? ?s # false
s5 = s4.sub(/spam/) { |m| m.reverse } # "Don't forget the maps." flag4 = str2.include? "Savings" # false
s4.sub!(/spam/) { |m| m.reverse } Метод scan многократно просматривает строку в поисках указанного образца.
# s4 теперь равно "Don't forget the maps."
Будучи вызван внутри блока, он возвращает массив. Если образец содержит не-
Как видите, в подставляемой строке могут встречаться специальные символы сколько (заключенных в скобки) групп, то массив окажется вложенным:
\1, \2 и т. д. Но такие специальные переменные, как $& (или ее англоязычная вер- str1 = "abracadabra"
сия $MATCH), не допускаются. sub1 = str1.scan(/a./)
Если употребляется форма с блоком, то допустимы и специальные перемен- # sub1 теперь равно ["ab","ac","ad","ab"]
ные. Если вам нужно лишь получить сопоставленную с образцом строку, то она бу- str2 = "Acapulco, Mexico"
дет передана в блок как параметр. Если эта строка вообще не нужна, то параметр, sub2 = str2.scan(/(.)(c.)/)
конечно, можно опустить. # sub2 теперь равно [ ["A","ca"], ["l","co"], ["i","co"] ]
Метод gsub (глобальная подстановка) отличается от sub лишь тем, что заменя- Если при вызове задан блок, то метод поочередно передает этому блоку най-
ются все вхождения, а не только первое: денные значения:
s5 = "alfalfa abracadabra" str3 = "Kobayashi"
s6 = s5.gsub(/a[bl]/,"xx") # "xxfxxfa xxracadxxra" str3.scan(/[^aeiou]+[aeiou]/) do |x|
s5.gsub!(/[lfdbr]/) { |m| m.upcase + "-" } print "Слог: #{x}\n"
# s5 теперь равно "aL-F-aL-F-a aB-R-acaD-aB-R-a" end
92 Строки Явные и неявные преобразования 93

Этот код выводит такой результат: поскольку оператор + ожидает, что второй операнд – тоже строка. Но так как в
Слог: Ko классе Pathname есть метод to_str, то он вызывается. Класс Pathname «маскирует-
Слог: ba ся» под строку, то есть может быть неявно преобразован в String.
Слог: ya На практике методы to_s и to_str обычно возвращают одно и то же значение,
Слог: shi но это необязательно. Неявное преобразование должно давать «истинное строко-
вое значение» объекта, а явное можно расценивать как «принудительное» преоб-
2.15. Преобразование символов в коды ASCII и обратно разование.
В Ruby символ представляется целым числом. Это поведение изменится в вер- Метод puts обращается к методу to_s объекта, чтобы получить его строковое
сии 2.0, а возможно и раньше. В будущем предполагается хранить символы в ви- представление. Можно считать, что это неявный вызов явного преобразования. То
де односимвольных строк. же самое справедливо в отношении интерполяции строк. Вот пример:
str = "Martin" class Helium
print str[0] # 77 def to_s
"He"
Если в конец строки дописывается объект типа Fixnum, то он предварительно end
преобразуется в символ: def to_str
str2 = str << 111 # "Martino" "гелий"
end
end
2.16. Явные и неявные преобразования
На первый взгляд, методы to_s и to_str могут вызвать недоумение. Ведь оба пре- e = Helium.new
образуют объект в строковое представление, так? print "Элемент "
Но есть и различия. Во-первых, любой объект в принципе можно как-то пре- puts e # Элемент He.
образовать в строку, поэтому почти все системные классы обладают методом to_s. puts "Элемент " + e # Элемент гелий.
puts "Элемент #{e}" # Элемент He.
Однако метод to_str в системных классах не реализуется никогда.
Как правило, метод to_str применяется для объектов, очень похожих на стро- Как видите, разумное определение этих методов в собственном классе может
ки, способных «замаскироваться» под строку. В общем, можете считать, что метод несколько повысить гибкость применения. Но что сказать об идентификации объ-
to_s – это явное преобразование, а метод to_str – неявное. ектов, переданных методам вашего класса?
Я уже сказал, что ни в одном системном классе не определен метод to_str (по Предположим, например, что вы написали метод, который ожидает в качест-
крайней мере, мне о таких классах неизвестно). Но иногда они вызывают to_str ве параметра объект String. Вопреки философии «утипизации», так делают час-
(если такой метод существует в соответствующем классе). то, и это вполне оправдано. Например, предполагается, что первый параметр ме-
Первое, что приходит на ум, – подкласс класса String; но на самом деле объ- тода File.new – строка.
ект любого класса, производного от String, уже является строкой, так что опреде- Решить эту проблему просто. Если вы ожидаете на входе строку, проверьте,
лять метод to_str излишне. имеет ли объект метод to_str, и при необходимости вызывайте его.
А вот пример из реальной жизни. Класс Pathname определен для удобства ра- def set_title(title)
боты с путями в файловой системе (например, конкатенации). Но путь естествен- if title.respond_to? :to_str
но отображается на строку (хотя и не наследует классу String). title = title.to_str
end
require 'pathname'
# ...
path = Pathname.new("/tmp/myfile")
end
name = path.to_s # "/tmp/myfile"
name = path.to_str # "/tmp/myfile" (Ну и что?) Ну а если объект не отвечает на вызов метода to_str? Есть несколько вариан-
# Вот где это оказывается полезно... тов действий. Можно принудительно вызвать метод to_s; можно проверить, при-
heading = "Имя файла равно " + path надлежит ли объект классу String или его подклассу; можно, наконец, продолжать
puts heading # " Имя файла равно /tmp/myfile" работать, понимая, что при попытке выполнить операцию, которую объект не под-
В этом фрагменте мы просто дописали путь в конец обычной строки " Имя держивает, мы получим исключение ArgumentError.
файла равно". Обычно такая операция приводит к ошибке во время выполнения, Короткий путь к цели выглядит так:
94 Строки Удаление лишних пропусков 95

title = title.to_str rescue title разные операционные системы неодинаково трактуют понятие «новой строки».
Он опирается на тот факт, что при отсутствии реализации метода to_str воз- В UNIX-подобных системах новая строка представляется символом \n. А в DOS
никнет исключение. Разумеется, модификаторы rescue могут быть вложенными: и Windows для этой цели используется пара символов \r\n.
title = title.to_str rescue title.to_s rescue title str = gets.chop # Прочитать, удалить символ новой строки.
# Обрабатывается маловероятный случай, когда отсутствует даже метод to_s. s2 = "Some string\n" # "Some string" (нет символа новой строки).
С помощью неявного преобразования можно было бы сделать строки и числа s3 = s2.chop! # s2 теперь тоже равно "Some string".
s4 = "Other string\r\n"
практически эквивалентными:
s4.chop! # "Other string" (нет символа новой строки).
class Fixnum
def to_str Обратите внимание, что при вызове варианта chop! операнд-источник моди-
self.to_s фицируется.
end Важно еще отметить, что последний символ удаляется, даже если это не сим-
end вол новой строки:
str = "abcxyz"
str = "Число равно " + 345 # Число равно 345. s1 = str.chop # "abcxy"
Но я не рекомендую так поступать: «много хорошо тоже нехорошо». В Ruby, Поскольку символ новой строки присутствует не всегда, иногда удобнее при-
как и в большинстве языков, строки и числа – разные сущности. Мне кажется, что менять метод chomp:
ясности ради преобразования, как правило, должны быть явными. str = "abcxyz"
И еще: в методе to_str нет ничего волшебного. Предполагается, что он возвра- str2 = "123\n"
щает строку, но если вы пишете такой метод сами, ответственность за то, что он str3 = "123\r"
действительно так и поступает, ложится на вас. str4 = "123\r\n"
s1 = str.chomp # "abcxyz"
s2 = str2.chomp # "123"
2.17. Дописывание в конец строки # Если установлен стандартный разделитель записей, то удаляется не только
Для конкатенации строк применяется оператор <<. Он «каскадный», то есть позво- # \n, но также \r и \r\n.
ляет выполнять подряд несколько операций над одним и тем же операндом-при- s3 = str3.chomp # "123"
емником. s4 = str4.chomp # "123"
str = "A" Как и следовало ожидать, имеется также метод chomp! для замены «на месте».
str << [1,2,3].to_s << " " << (3.14).to_s Если методу chomp передана строка-параметр, то удаляются перечисленные в
# str теперь равно "A123 3.14". ней символы, а не подразумеваемый по умолчанию разделитель записей. Кстати,
Если число типа Fixnum принадлежит диапазону 0..255, то оно будет преобра- если разделитель записей встречается в середине строки, то он не удаляется:
зовано в символ: str1 = "abcxyz"
str2 = "abcxyz"
str = "Marlow"
s1 = str1.chomp("yz") # "abcx"
str << 101 << ", Christopher"
s2 = str2.chomp("x") # "abcxyz"
# str теперь равно "Marlowe, Christopher".

2.18. Удаление хвостовых символов 2.19. Удаление лишних пропусков


Метод strip удаляет пропуски в начале и в конце строки, а вариант strip! делает
новой строки и прочих то же самое «на месте».
Часто бывает необходимо удалить лишние символы в конце строки. Типичный str1 = "\t \nabc \t\n"
пример – удаление символа новой строки после чтения строки из внешнего ис- str2 = str1.strip # "abc"
точника. str3 = str1.strip! # "abc"
Метод chop удаляет последний символ строки (обычно это символ новой # str1 теперь тоже равно "abc".
строки). Если перед символом новой строки находится символ перевода карет- Под пропусками, как обычно, понимаются пробелы, символы табуляции и пе-
ки (\r), он тоже удаляется. Причина такого поведения заключается в том, что рехода на новую строку.
96 Строки Разбор данных, разделенных запятыми 97

Чтобы удалить пропуски только в начале или только в конце строки, приме- Другое, более громоздкое решение состоит в том, чтобы сохранить строку, за-
няйте методы lstrip и rstrip: ключенную в одиночные кавычки, потом «обернуть» ее двойными кавычками и
str = " abc " вычислить:
s2 = str.lstrip # "abc " str = '#{name} – мое имя, а #{nation} – моя родина'
s3 = str.rstrip # " abc" name, nation = "Стивен Дедал", "Ирландия"
Имеются также варианты lstrip! и rstrip! для удаления «на месте». s1 = eval('"' + str + '"')
# Стивен Дедал – мое имя, а Ирландия – моя родина.

2.20. Повтор строк Можно также передать eval другую функцию привязки:
bind = proc do
В Ruby оператор (или метод) умножения перегружен так, что в применении к
name,nation = "Гулливер Фойл", "Земля"
строкам выполняет операцию повторения. Если строку умножить на n, то полу- binding
чится строка, состоящая из n конкатенированных копий исходной: end.call # Надуманный пример; возвращает привязанный контекст блока
etc = "Etc. "*3 # "Etc. Etc. Etc. " s2 = eval('"' + str + '"',bind)
ruler = "+" + ("."*4+"5"+"."*4+"+")*3 # Гулливер Фойл – мое имя, а Земля – моя родина.
# "+....5....+....5....+....5....+"
У техники работы с eval есть свои «причуды». Например, будьте осторожны,
вставляя управляющие последовательности, скажем \n.
2.21. Включение выражений в строку
Это легко позволяет сделать синтаксическая конструкция #{}. Нет нужды думать 2.23. Разбор данных, разделенных запятыми
о преобразовании, добавлении и конкатенации; нужно лишь интерполировать пе-
Данные, разделенные запятыми, часто встречаются при программировании.
ременную или другое выражение в любое место строки:
Это в некотором роде «наибольший общий делитель» всех форматов обмена
puts "#{temp_f} по Фаренгейту равно #{temp_c} по Цельсию"
puts "Значение определителя равно #{b*b - 4*a*c}."
данными. Например, так передаются данные между несовместимыми СУБД
puts "#{word} это #{word.reverse} наоборот." или приложениями, которые не поддерживают никакого другого общего фор-
мата.
Внутри фигурных скобок могут находиться даже полные предложения. При
Будем предполагать, что данные представляют собой строки и числа, а все
этом возвращается результат вычисления последнего выражения.
строки заключены в кавычки. Еще предположим, что все символы должным обра-
str = "Ответ равен #{ def factorial(n)
зом экранированы (например, запятые и кавычки внутри строки).
n==0 ? 1 : n*factorial(n-1)
end Задача оказывается простой, поскольку такой формат данных подозрительно
answer = factorial(3) * 7}, естественно." напоминает встроенные в Ruby массивы данных разных типов. Достаточно заклю-
# Ответ равен 42, естественно. чить все выражение в квадратные скобки, чтобы получить массив.
При интерполяции глобальных переменных, а также переменных класса и эк- string = gets.chop!
земпляра фигурные скобки можно опускать: # Предположим, что прочитана такая строка:
# "Doe, John", 35, 225, "5'10\"", "555-0123"
print "$gvar = #$gvar и ivar = #@ivar."
data = eval("[" + string + "]") # Преобразовать в массив.
Интерполяция не производится внутри строк, заключенных в одиночные ка- data.each {|x| puts "Значение = #{x}"}
вычки (поскольку их значение не интерпретируется), но применима к заключен-
Этот код выводит такой результат:
ным в двойные кавычки встроенным документам и к регулярным выражениям.
Значение = Doe, John
Значение = 35
2.22. Отложенная интерполяция Значение = 225
Иногда желательно отложить интерполяцию значений в строку. Идеального спо- Значение = 5' 10"
Значение = 555-0123
соба решить эту задачу не существует, но можно воспользоваться блоком:
str = proc {|x,y,z| "Числа равны #{x}, #{y} и #{z}" } Более общее решение дает стандартная библиотека CSV. Есть также усовер-
s1 = str.call(3,4,5) # Числа равны 3, 4 и 5. шенствованный инструмент под названием FasterCSV. Поищите его в сети, он не
s2 = str.call(7,8,9) # Числа равны 7, 8 и 9. входит в стандартный дистрибутив Ruby.
98 Строки Кодирование и декодирование строк в кодировке rot13 99

2.24. Преобразование строки в число y = "0111".to_i


z = "0x111".to_i
# 0
# 0
(десятичное или иное) Однако у метода to_i есть необязательный второй параметр для указания ос-
Есть два основных способа преобразовать строку в число: методы Integer и Float нования. Обычно применяют только четыре основания: 2, 8, 10 (по умолчанию)
модуля Kernel и методы to_i и to_f класса String. (Имена, начинающиеся с про- и 16. Впрочем, префиксы не распознаются даже при определении основания.
писной буквы, например Integer, обычно резервируются для специальных функ- x = "111".to_i(2) # 7
ций преобразования.) y = "111".to_i(8) # Восьмеричное - возвращает 73.
Простой случай тривиален, следующие два предложения эквивалентны: z = "111".to_i(16) # Шестнадцатеричное - возвращает 291.
x = "123".to_i # 123
x = "0b111".to_i # 0
y = Integer("123") # 123
y = "0111".to_i # 0
Но если в строке хранится не число, то поведение этих методов различается: z = "0x111".to_i # 0
x = "junk".to_i # Молча возвращает 0. Из-за «стандартного» поведения этих методов цифры, недопустимые при дан-
y = Integer("junk") # Ошибка.
ном основании, обрабатываются по-разному:
Метод to_i прекращает преобразование, как только встречает первый символ, x = "12389".to_i(8) # 123 (8 игнорируется).
не являющийся цифрой, а метод Integer в этом случае возбуждает исключение: y = Integer("012389") # Ошибка (8 недопустима).
x = "123junk".to_i # 123 Хотя полезность этого и сомнительна, метод to_i понимает основания вплоть
y = Integer("123junk") # Ошибка. до 36, когда в представлении числа допустимы все буквы латинского алфавита.
Оба метода допускают наличие пропусков в начале и в конце строки: (Возможно, это напомнило вам о base64-кодировании; дополнительную информа-
x = " 123 ".to_i # 123 цию по этому поводу вы найдете в разделе 2.37.)
y = Integer(" 123 ") # 123 x = "123".to_i(5) # 66
Преобразование строки в число с плавающей точкой работает аналогично: y = "ruby".to_i(36) # 1299022
x = "3.1416".to_f # 3.1416 Для преобразования символьной строки в число можно также воспользовать-
y = Float("2.718") # 2.718 ся методом scanf из стандартной библиотеки, которая добавляет его в модуль
Оба метода понимают научную нотацию: Kernel, а также классы IO и String:
x = Float("6.02e23") # 6.02e23 str = "234 234 234"
y = "2.9979246e5".to_f # 299792.46 x, y, z = str.scanf("%d %o %x") # 234, 156, 564
Методы to_i и Integer также по-разному относятся к системе счисления. По Метод scanf реализует всю имеющую смысл функциональность стандартных
умолчанию, естественно, подразумевается система по основанию 10, но другие то- функций scanf, sscanf и fscanf из библиотеки языка C. Но строки, представляю-
же допускаются (это справедливо и для чисел с плавающей точкой). щие двоичные числа, он не обрабатывает.
Говоря о преобразовании из одной системы счисления в другую, мы всегда
имеем в виду строки. Ведь целое число неизменно хранится в двоичном виде. 2.25. Кодирование и декодирование строк
Следовательно, преобразование системы счисления – это всегда преобразова-
ние одной строки в другую. Здесь мы рассмотрим преобразование из строки (об- в кодировке rot13
ратное преобразование рассматривается в разделах 5.18 и 5.5). Rot13 – наверное, самый слабый из известных человечеству шифров. Исторически
Числу в тексте программы может предшествовать префикс, обозначающий ос- он просто препятствовал «случайному» прочтению текста. Он часто встречается в
нование системы счисления. Префикс 0b обозначает двоичное число, 0 – восьме- конференциях Usenet; например, так можно закодировать потенциально обидную
ричное, а 0x – шестнадцатеричное. шутку или сценарий фильма «Звездные войны. Эпизод 13» накануне премьеры.
Метод Integer такие префиксы понимает, а метод to_i – нет: Принцип кодирования состоит в смещении символов относительно начала ал-
x = Integer("0b111") # Двоичное - возвращает 7. фавита (латинского) на 13: A превращается в N, B – в O и т. д. Строчные буквы
y = Integer("0111") # Восьмеричное - возвращает 73. смещаются на ту же величину; цифры, знаки препинания и прочие символы игно-
z = Integer("0x111") # Шестнадцатеричное - возвращает 291. рируются. Поскольку 13 – это ровно половина от 26 (число букв в латинском ал-
фавите), то функция является обратной самой себе, то есть ее повторное примене-
x = "0b111".to_i # 0 ние восстанавливает исходный текст.
100 Строки Подсчет числа символов в строке 101

Ниже приведена реализация этого метода, добавленного в класс String, ника- 2.27. Сжатие строк
ких особых комментариев она не требует:
Для сжатия строк и файлов применяется библиотека Zlib.
class String Зачем может понадобиться сжимать строки? Возможно, чтобы ускорить ввод/
вывод из базы данных, оптимизировать использование сети или усложнить рас-
def rot13
познавание строк.
self.tr("A-Ma-mN-Zn-z","N-Zn-zA-Ma-m")
end
В классах Deflate и Inflate имеются методы класса deflate и inflate соответ-
ственно. У метода deflate (он выполняет сжатие) есть дополнительный параметр,
end задающий режим сжатия. Он определяет компромисс между качеством сжатия и
скоростью. Если значение равно BEST_COMPRESSION, то строка сжимается макси-
joke = "Y2K bug" мально, но это занимает сравнительно много времени. Значение BEST_SPEED зада-
joke13 = joke.rot13 # "L2X oht" ет максимальную скорость, но при этом строка сжимается хуже. Подразумеваемое
по умолчанию значение DEFAULT_COMPRESSION выбирает компромиссный режим.
episode2 = "Fcbvyre: Naanxva qbrfa'g trg xvyyrq." require 'zlib'
puts episode2.rot13 include Zlib

long_string = ("abcde"*71 + "defghi"*79 + "ghijkl"*113)*371


2.26. Шифрование строк # long_string состоит из 559097 символов.

Иногда нежелательно, чтобы строки можно было легко распознать. Например, па- s1 = Deflate.deflate(long_string,BEST_SPEED) # 4188 символов.
роли не следует хранить в открытом виде, какими бы ограничительными ни были s3 = Deflate.deflate(long_string) # 3568 символов
права доступа к файлу. s2 = Deflate.deflate(long_string,BEST_COMPRESSION) # 2120 символов
В стандартном методе crypt применяется стандартная функция с тем же име- Неформальные эксперименты показывают, что скорость отличается примерно
нем для шифрования строки по алгоритму DES. Она принимает в качестве пара- в два раза, а плотность сжатия – в обратной пропорции на ту же величину. И ско-
метра «затравку» (ее назначение то же, что у затравки генератора случайных чи- рость, и плотность сильно зависят от состава строки. Разумеется, на скорость вли-
сел). На платформах, отличных от UNIX, параметр может быть иным. яет и имеющееся оборудование.
Ниже показано тривиальное приложение, которое запрашивает пароль, знако- Имейте в виду, что существует пороговое значение длины строки. Если строка
мый любителям Толкиена: короче, то сжимать ее практически бесполезно (если только вы не хотите сделать
coded = "hfCghHIE5LAM." ее нечитаемой). В этом случае неизбежные накладные расходы могут даже привес-
ти к тому, что сжатая строка окажется длиннее исходной.
puts "Говори, друг, и жми Enter!"
2.28. Подсчет числа символов в строке
print "Пароль: "
Метод count подсчитывает число вхождений в строку символов из заданного на-
password = gets.chop
бора:
if password.crypt("hf") == coded s1 = "abracadabra"
puts "Добро пожаловать!" a = s1.count("c") # 1
else b = s1.count("bdr") # 5
puts "Кто ты, орк?" Строковый параметр ведет себя как простое регулярное выражение. Если он
end начинается с символа ^, то берется дополнение к списку:
Стоит отметить, что на такое шифрование не стоит полагаться в серверных c = s1.count("^a") # 6
Web-приложениях, поскольку пароль, введенный в поле формы, все равно переда-
ется по сети в открытом виде. В таких случаях проще всего воспользоваться про- d = s1.count("^bdr") # 6
токолом SSL (Secure Sockets Layer). Разумеется, никто не запрещает пользоваться Дефис обозначает диапазон символов:
шифрованием на сервере, но по другой причине – чтобы защитить пароль в храни- e = s1.count("a-d") # 9
лище, а не во время передачи по сети. f = s1.count("^a-d") # 2
102 Строки Вычисление 32-разрядного CRC 103

2.29. Обращение строки s1 =


puts
"Внимание" << 7 << 7 << 7
s1.dump
# Добавлено три символа ASCII BEL.
# Печатается: Внимание\007\007\007
Для обращения строки служит метод reverse (или его вариант для обращения «на s2 = "abc\t\tdef\tghi\n\n"
месте» reverse!): puts s2.dump # Печатается: abc\t\tdef\tghi\n\n
s1 = "Star Trek" s3 = "Двойная кавычка: \""
s2 = s1.reverse # "kerT ratS" puts s3.dump # Печатается: Двойная кавычка: \"
s1.reverse! # s1 теперь равно "kerT ratS" При стандартном значении переменной $KCODE метод dump дает такой же эф-
Пусть требуется обратить порядок слов (а не символов). Тогда можно снача- фект, как вызов метода inspect для строки. Переменная $KCODE рассматривается
ла воспользоваться методом String#split, который вернет массив слов. В классе в главе 4.
Array тоже есть метод reverse, поэтому можно обратить массив, а затем с помощью
метода join объединить слова в новую строку: 2.33. Генерирование последовательности строк
phrase = "Now here's a sentence" Изредка бывает необходимо получить «следующую» строку. Так, следующей для
phrase.split(" ").reverse.join(" ") строки "aaa" будет строка "aab" (затем "aac", "aad" и так далее).
# "sentence a here's Now"
В Ruby для этой цели есть метод succ:
droid = "R2D2"
2.30. Удаление дубликатов improved = droid.succ # "R2D3"
Цепочки повторяющихся символов можно сжать до одного методом squeeze: pill = "Vitamin B"
pill2 = pill.succ # "Vitamin C"
s1 = "bookkeeper"
s2 = s1.squeeze # "bokeper" Не рекомендуется применять этот метод, если точно не известно, что началь-
s3 = "Hello..." ное значение предсказуемо и разумно. Если начать с какой-нибудь экзотической
s4 = s3.squeeze # "Helo." строки, то рано или поздно вы получите странный результат.
Если указан параметр, то будут удаляться только дубликаты заданных в нем Существует также метод upto, который в цикле вызывает succ, пока не будет
символов: достигнуто конечное значение:
s5 = s3.squeeze(".") # "Hello." "Files, A".upto "Files, X" do |letter|
puts "Opening: #{letter}"
Этот параметр подчиняется тем же правилам, что и параметр метода count (см. end
раздел 2.28), то есть допускаются дефис и символ ^ .
Имеется также метод squeeze!. # Выводится 24 строки.
Еще раз подчеркнем, что эта возможность используется редко, да и то на ваш страх
2.31. Удаление заданных символов и риск. Кстати, метода, возвращающего «предшествующую» строку, не существует.
Метод delete удаляет из строки те символы, которые включены в список, передан-
ный в качестве параметра: 2.34. Вычисление 32-разрядного CRC
s1 = "To be, or not to be" Контрольный код циклической избыточности (Cyclic Redundancy Checksum,
s2 = s1.delete("b") # "To e, or not to e" CRC) – хорошо известный способ получить «сигнатуру» файла или произволь-
s3 = "Veni, vidi, vici!" ного массива байтов. CRC обладает тем свойством, что вероятность получения
s4 = s3.delete(",!") # "Veni vidi vici" одинакового кода для разных входных данных равна 1 / 2**N, где N – число битов
Этот параметр подчиняется тем же правилам, что и параметр метода count (см. результата (чаще всего 32).
раздел 2.28), то есть допускаются символы - (дефис) и ^ (каре). Вычислить его позволяет библиотека zlib, написанная Уэно Кацухиро (Ueno Ka-
Имеется также метод delete!. tsuhiro). Метод crc32 вычисляет CRC для строки, переданной в качестве параметра.
require 'zlib'
2.32. Печать специальных символов include Zlib
crc = crc32("Hello") # 4157704578
Метод dump позволяет получить графическое представление символов, которые crc = crc32(" world!",crc) # 461707669
обычно не печатаются вовсе или вызывают побочные эффекты: crc = crc32("Hello world!") # 461707669 (то же, что и выше)
104 Строки Вычисление расстояния Левенштейна 105

В качестве необязательного второго параметра можно передать ранее вычис- Короче говоря, для получения MD5-свертки нужно написать:
ленный CRC. Результат получится такой, как если бы конкатенировать обе строки require 'md5'
и вычислить CRC для объединения. Это полезно, например, когда нужно вычис- m = MD5.new("Секретные данные").hexdigest
лить CRC файла настолько большого, что прочитать его можно только по частям.
2.36. Вычисление расстояния Левенштейна
2.35. Вычисление MD5-свертки строки между двумя строками
Алгоритм MD5 вырабатывает 128-разрядный цифровой отпечаток или дайджест Расстояние между строками важно знать в индуктивном обучении (искусствен-
сообщения произвольной длины. Это разновидность свертки, то есть функция ный интеллект), криптографии, исследовании структуры белков и других областях.
шифрования односторонняя, так что восстановить исходное сообщение по дай- Расстоянием Левенштейна называется минимальное число элементарных мо-
джесту невозможно. Для Ruby имеется расширение, реализующее MD5; интере- дификаций, которым нужно подвергнуть одну строку, чтобы преобразовать ее в
сующиеся могут найти его в каталоге ext/md5 стандартного дистрибутива. другую. Элементарными модификациями называются следующие операции: del
Для создания нового объекта MD5 есть два эквивалентных метода класса: new (удаление одного символа), ins (замена символа) и sub (замена символа). Замену
и md5: можно также считать комбинацией удаления и вставки (indel).
require 'md5' Существуют разные подходы к решению этой задачи, но не будем вдаваться в
hash = MD5.md5 технические детали. Достаточно знать, что реализация на Ruby (см. листинг 2.2)
hash = MD5.new позволяет задавать дополнительные параметры, определяющие стоимость всех
Есть также четыре метода экземпляра: clone, digest, hexdigest и update. Ме- трех операций модификации. По умолчанию за базовую принимается стоимость
тод clone просто копирует существующий объект, а метод update добавляет новые одной операции indel (стоимость вставки = стоимость удаления).
данные к объекту:
Листинг 2.2. Расстояние Левенштейна
hash.update("Дополнительная информация...")
class String
Можно создать объект и передать ему данные за одну операцию:
secret = MD5.new("Секретные данные") def levenshtein(other, ins=2, del=2, sub=1)
# ins, del, sub – взвешенные стоимости.
Если задан строковый аргумент, он добавляется к объекту путем обращения к
методу update. Повторные обращения эквивалентны одному вызову с конкатени- return nil if self.nil?
рованными аргументами: return nil if other.nil?
# Эти два предложения: dm = [] # Матрица расстояний.
cryptic.update("Данные...")
cryptic.update(" еще данные.") # Инициализировать первую строку.
# ...эквивалентны одному такому: dm[0] = (0..self.length).collect { |i| i * ins }
cryptic.update("Данные... еще данные.") fill = [0] * (self.length - 1)

Метод digest возвращает 16-байтовую двоичную строку, содержащую 128-раз- # Инициализировать первую колонку.
рядный дайджест. for i in 1..other.length
Но наиболее полезен метод hexdigest, который возвращает дайджест в виде dm[i] = [i * del, fill.flatten]
строки в коде ASCII, состоящей из 32 шестнадцатеричных символов, соответству- end
ющих 16 байтам. Он эквивалентен следующему коду:
# Заполнить матрицу.
def hexdigest for i in 1..other.length
ret = '' for j in 1..self.length
digest.each_byte {|i| ret << sprintf('%02x', i) } # Главное сравнение.
ret dm[i][j] = [
end dm[i-1][j-1] +
(self[j-1] == other[i-1] ? 0 : sub),
secret.hexdigest # "b30e77a94604b78bd7a7e64ad500f3c2" dm[i][j-1] + ins,
106 Строки Замена символов табуляции пробелами 107

dm[i-1][j] + del Простейший способ осуществить base64-кодирование и декодирование – вос-


].min пользоваться встроенными возможностями Ruby. В классе Array есть метод pack,
end который возвращает строку в кодировке base64 (если передать ему параметр "m").
end
А в классе String есть метод unpack, который декодирует такую строку:
# Последнее значение в матрице и есть str = "\007\007\002\abdce"
# расстояние Левенштейна между строками. new_string = [str].pack("m") # "BwcCB2JkY2U="
dm[other.length][self.length] original = new_string.unpack("m") # ["\a\a\002\abdce"]
end Отметим, что метод unpack возвращает массив.
end
2.38. Кодирование и декодирование строк
s1 = "ACUGAUGUGA" (uuencode/uudecode)
s2 = "AUGGAA"
Префикс uu в этих именах означает UNIX-to-UNIX. Утилиты uuencode и uudecode –
d1 = s1.levenshtein(s2) # 9
s3 = "pennsylvania" это проверенный временем способ обмена данными в текстовой форме (аналогич-
s4 = "pencilvaneya" ный base64).
d2 = s3.levenshtein(s4) # 7 str = "\007\007\002\abdce"
s5 = "abcd"
s6 = "abcd" new_string = [str].pack("u") # '(!P<"!V)D8V4''
d3 = s5.levenshtein(s6) # 0 original = new_string.unpack("u") # ["\a\a\002\abdce"]
Определив расстояние Левенштейна, мы можем написать метод similar?, вы- Отметим, что метод unpack возвращает массив.
числяющий меру схожести строк. Например:
class String
2.39. Замена символов табуляции пробелами
def similar?(other, thresh=2)
if self.levenshtein(other) < thresh
и сворачивание пробелов в табуляторы
true Бывает, что имеется строка с символами табуляции, а мы хотели бы преобразо-
else вать их в пробелы (или наоборот). Ниже показаны два метода, реализующих эти
false операции:
end
class String
end
def detab(ts=8)
end
str = self.dup
while (leftmost = str.index("\t")) != nil
if "polarity".similar?("hilarity") space = " "*(ts-(leftmost%ts))
puts "Электричество – забавная штука!" str[leftmost]=space
end end
str
Разумеется, можно было бы передать методу similar? три взвешенные стои- end
мости, которые он в свою очередь передал бы методу levenshtein. Но для просто-
ты мы не стали этого делать. def entab(ts=8)
str = self.detab
2.37. base64-кодирование и декодирование areas = str.length/ts
newstr = ""
Алгоритм base64 часто применяется для преобразования двоичных данных в тек- for a in 0..areas
стовую форму, не содержащую специальных символов. Например, в конференци- temp = str[a*ts..a*ts+ts-1]
ях так обмениваются исполняемыми файлами. if temp.size==ts
108 Строки Заключение 109

if temp =~ / +/ word = words.shift


match=Regexp.last_match[0] break if not word
endmatch = Regexp.new(match+"$") if out[line].length + word.length > max
if match.length>1 out[line].squeeze!(" ")
temp.sub!(endmatch,"\t") line += 1
end out[line] = ""
end end
end out[line] << word + " "
newstr += temp end
end
newstr out.each {|line| puts line} # Печатает 24 очень коротких строки.
end
Библиотека Format решает как эту, так и много других схожих задач. Поищи-
end те ее в сети.

foo = "Это всего лишь тест. " 2.41. Заключение


Мы обсудили основы представления строк (заключенных в одиночные или двой-
puts foo ные кавычки). Поговорили о том, как интерполировать выражения в строку в
puts foo.entab(4) двойных кавычках; узнали, что в таких строках допустимы некоторые специаль-
puts foo.entab(4).dump ные символы, представленные управляющими последовательностями. Кроме то-
го, мы познакомились с конструкциями %q и %Q, которые позволяют нам по свое-
Отметим, что этот код не распознает символы забоя. му вкусу выбирать ограничители. Наконец, рассмотрели синтаксис встроенных
документов, унаследованных из старых продуктов, в том числе командных интер-
2.40. Цитирование текста претаторов в UNIX.
Иногда бывает необходимо напечатать длинные строки текста, задав ширину по- В этой главе были продемонстрированы все наиболее важные операции, кото-
ля. Приведенный ниже код решает эту задачу, разбивая текст по границам слов и рые программисты обычно выполняют над строками: конкатенация, поиск, извле-
учитывая символы табуляции (но символы забоя не учитываются, а табуляция не чение подстрок, разбиение на лексемы и т. д. Мы видели, как можно кодировать
сохраняется): строки (например, по алгоритму base64) и сжимать их.
Пришло время перейти к тесно связанной со строками теме – регулярным вы-
str = <<-EOF
ражениям. Регулярные выражения – это мощное средства сопоставления строк с
When in the Course of human events it becomes necessary
for one people to dissolve the political bands which have
образцами. Мы рассмотрим их в следующей главе.
connected them with another, and to assume among the powers
of the earth the separate and equal station to which the Laws
of Nature and of Nature's God entitle them, a decent respect
for the opinions of mankind requires that they should declare
the causes which impel them to the separation.
EOF

max = 20

line = 0
out = [""]

input = str.gsub(/\n/," ")


words = input.split(" ")
while input != ""
Синтаксис регулярных выражений 111

Таблица 3.2. Модификаторы регулярных выражений

Модификатор Назначение
I Игнорировать регистр
O Выполнять подстановку выражения только один раз
Глава 3. Регулярные выражения M Многострочный режим (точка сопоставляется с символом новой строки)
X Обобщенное регулярное выражение (допускаются пробелы и коммента-
рии)
Я провела бы его по лабиринту, где тропы орнаментом украшены...
Эми Лоуэлл
Дополнительные примеры будут рассмотрены в главе 4.
Чтобы завершить введение в регулярные выражение, в таблице 3.3 мы приво-
Мощь регулярных выражений как инструмента программирования часто недооце- дим наиболее употребительные символы и обозначения.
нивается. Первые теоретические исследования на эту тему датируются сороковыми
годами прошлого века, в вычислительные системы они проникли в 1960-х годах, а Таблица 3.3. Общеупотребительные обозначения в регулярных выражениях
затем были включены в различные инструментальные средства операционной сис-
Обозначение Пояснение
темы UNIX. В 1990-х годах популярность языка Perl привела к тому, что регуляр-
ные выражения вошли в обиход, перестав быть уделом бородатых гуру. ^ Начало строки текста (line) или строки символов (string)
Красота регулярных выражений заключается в том, что почти весь наш опыт $ Конец строки текста или строки символов
можно выразить в терминах образцов. А если имеется образец, то можно провести . Любой символ, кроме символа новой строки (если не установлен
сопоставление с ним, можно найти то, что ему соответствует, и заменить найден- многострочный режим)
ное чем-то другим по своему выбору. \w Символ – часть слова (цифра, буква или знак подчеркивания)
Во время работы над данной книгой язык Ruby находился в переходном состо- \W Символ, не являющийся частью слова
янии. Старая библиотека регулярных выражений заменялась новой под названи- \s Пропуск (пробел, знак табуляции, символ новой строки и т. д.)
ем Oniguruma. Этой библиотеке посвящен раздел 3.13 данной главы. Что касается \S Символ, не являющийся пропуском
интернационализации, то это тема главы 4. \d Цифра (то же, что [0-9])
\D Не цифра
\A Начало строки символов (string)
3.1. Синтаксис регулярных выражений \Z Конец строки символов или позиция перед конечным символом новой
Обычно регулярное выражение ограничено с двух сторон символами косой чер- строки
ты. Применяется также форма %r. В таблице 3.1 приведены примеры простых ре- \z Конец строки символов (string)
гулярных выражений: \b Граница слова (только вне квадратных скобок [ ])
Таблица 3.1. Простые регулярные выражения \B Не граница слова
\b Забой (только внутри квадратных скобок [ ])
Регулярное Пояснение [] Произвольный набор символов
выражение
* 0 или более повторений предыдущего подвыражения
/Ruby/ Соответствует одному слову Ruby
*? 0 или более повторений предыдущего подвыражения (нежадный
/[Rr]uby/ Соответствует Ruby или ruby алгоритм)
/^abc/ Соответствует abc в начале строки + 1 или более повторений предыдущего подвыражения
%r(xyz$) Соответствует xyz в конце строки +? 1 или более повторений предыдущего подвыражения (нежадный
%r|[0-9]*| Соответствует любой последовательности из нуля или более цифр алгоритм)
{m,n} От m до n вхождений предыдущего подвыражения
Сразу после регулярного выражения можно поместить однобуквенный моди- {m,n}? От m до n вхождений предыдущего подвыражения (нежадный алгоритм)
фикатор. В таблице 3.2 приведены наиболее часто употребляемые модификаторы. ? 0 или 1 повторений предыдущего подвыражения
112 Регулярные выражения Якоря 113

Таблица 3.3. Общеупотребительные обозначения в регулярных выражениях "S" или "s" означает Shift-JIS
"U" или "u" означает UTF-8
Обозначение Пояснение Литеральное регулярное выражение можно задавать и не вызывая метод new
| Альтернативы или compile. Достаточно заключить его в ограничители (символы косой черты).
(?= ) Позитивное заглядывание вперед pat1 = /^foo.*/
(?! ) Негативное заглядывание вперед pat2 = /bar$/i
() Группировка подвыражений Более подробная информация приводится в главе 4.
(?> ) Вложенное подвыражение
(?: ) Несохраняющая группировка подвыражений 3.3. Экранирование специальных символов
(?imx-imx) Включить/выключить режимы, начиная с этого места
Метод класса Regexp.escape экранирует все специальные символы, встречающи-
(?imx-imx: Включить/выключить режимы для этого выражения
expr)
еся в регулярном выражении. К их числу относятся звездочка, вопросительный
знак и квадратные скобки.
(?# ) Комментарий
str1 = "[*?]"
str2 = Regexp.escape(str1) # "\[\*\?\]"
Умение работать с регулярными выражениями – большой плюс для современ- Синонимом является метод Regexp.quote.
ного программиста. Полное рассмотрение этой темы выходит далеко за рамки на-
стоящей книги, но, если вам интересно, можете обратиться к книге Jeffrey Friedl,
Mastering Regular Expressions*.
3.4. Якоря
Дополнительный материал вы также найдете в разделе 3.13. Якорь – это специальное выражение, соответствующее позиции в строке, а не кон-
кретному символу или последовательности символов. Позже мы увидим, что это
простой частный случай утверждения нулевой длины, то есть соответствия, кото-
3.2. Компиляция регулярных выражений рое не продвигает просмотр исходной строки ни на одну позицию.
Для компиляции регулярных выражений предназначен метод Regexp.compile (си- Наиболее употребительные якоря уже были представлены в начале главы. Про-
ноним Regexp.new). Первый параметр обязателен, он может быть строкой или ре- стейшими из них являются ^ и $, которые соответствуют началу и концу строки
гулярным выражением. (Отметим, что если этот параметр является регулярным символов.
выражением с дополнительными флагами, то флаги не будут перенесены в новое string = "abcXdefXghi"
откомпилированное выражение.) /def/ =~ string # 4
/abc/ =~ string # 0
pat1 = Regexp.compile("^foo.*") # /^foo.*/
/ghi/ =~ string # 8
pat2 = Regexp.compile(/bar$/i) # /bar/ (i не переносится) /^def/ =~ string # nil
Если второй параметр задан, обычно это поразрядное объединение (ИЛИ) ка- /def$/ =~ string # nil
ких-либо из следующих констант: Regexp::EXTENDED, Regexp::IGNORECASE, Regexp:: /^abc/ =~ string # 0
/ghi$/ =~ string # 8
MULTILINE. При этом любое отличное от nil значение приведет к тому, что регу-
лярное выражение не будет различать регистры; мы рекомендуем опускать вто- Впрочем, я немного уклонился от истины. Эти якоря на самом деле соответ-
рой параметр. ствуют началу и концу не строки символов (string), а строки текста (line). Вот что
options = Regexp::MULTILINE || Regexp::IGNORECASE
произойдет, если те же самые образцы применить к строке, внутри которой есть
pat3 = Regexp.compile("^foo", options) символы новой строки:
pat4 = Regexp.compile(/bar/, Regexp::IGNORECASE) string = "abc\ndef\nghi"
/def/ =~ string # 4
Третий параметр, если он задан, включает поддержку многобайтных символов. /abc/ =~ string # 0
Он может принимать одно из четырех значений: /ghi/ =~ string # 8
"N" или "n" означает отсутствие поддержки /^def/ =~ string # 4
"E" или "e" означает EUC /def$/ =~ string # 4
/^abc/ =~ string # 0
* Дж. Фридл. Регулярные выражения. – Питер, 2003 (Прим. перев.) /ghi$/ =~ string # 8
114 Регулярные выражения Кванторы 115

Однако имеются якоря \A и \Z, которые соответствуют именно началу и кон- pattern = /Huzzah(!+)?/ # Скобки здесь обязательны.
цу самой строки символов. pattern =~ "Huzzah" # 0
string = "abc\ndef\nghi" pattern =~ "Huzzah!!!!" # 0
/\Adef/ =~ string # nil Но есть и способ лучше. Требуемое поведение описывается квантором *.
/def\Z/ =~ string # nil pattern = /Huzzah!*/ # * применяется только к символу !
/\Aabc/ =~ string # 0 pattern =~ "Huzzah" # 0
/ghi\Z/ =~ string # 8
pattern =~ "Huzzah!!!!" # 0
Якорь \z отличается от \Z тем, что последний устанавливает соответствие пе-
Как распознать американский номер социального страхования? С помощью
ред конечным символом новой строки, а первый должен соответствовать явно.
такого образца:
string = "abc\ndef\nghi"
str2 << "\n" ssn = "987-65-4320"
/ghi\Z/ =~ string # 8 pattern = /\d\d\d-\d\d-\d\d\d\d/
/\Aabc/ =~ str2 # 8 pattern =~ ssn # 0
/ghi\z/ =~ string # 8 Но это не вполне понятно. Лучше явно сказать, сколько цифр должно быть в
/ghi\z/ =~ str2 # nil каждой группе. Это можно сделать, указав число повторений в фигурных скобках:
Можно также устанавливать соответствие на границе слова с помощью якоря pattern = /\d{3}-\d{2}-\d{4}/
\b или с позицией, которая не находится на границе слова (\B). Примеры исполь-
Необязательно, что такой образец будет короче, но он более понятен читате-
зования метода gsub показывают, как эти якоря работают:
лю программы.
str = "this is a test" Можно также использовать диапазоны, границы которых разделены запятой.
str.gsub(/\b/,"|") # "|this| |is| |a| |test|"
Предположим, что номер телефона в Элбонии состоит из двух частей: в первой
str.gsub(/\B/,"-") # "t-h-i-s i-s a t-e-s-t"
может быть от трех до пяти цифр, а во второй – от трех до семи. Вот как выглядит
Не существует способа отличить начало слова от конца. соответствующий образец:
elbonian_phone = /\d{3,5}-\d{3,7}/
3.5. Кванторы Нижняя и верхняя границы диапазона необязательны (но хотя бы одна долж-
Немалая часть аппарата регулярных выражений связана с обработкой необяза- на быть задана):
тельных элементов и повторений. Элемент, за которым следует вопросительный /x{5}/ # Соответствует 5 x.
знак, необязателен; он может присутствовать или отсутствовать, а общее соот- /x{5,7}/ # Соответствует 5-7 x.
ветствие зависит от прочих частей регулярного выражения. (Этот квантор имеет /x{,8}/ # Соответствует не более 8 x.
смысл применять только к подвыражению ненулевой длины, но не к якорям.) /x{3,}/ # Соответствует по меньшей мере 3 x.
pattern = /ax?b/ Ясно, что кванторы ?, + и * можно переписать и так:
pat2 = /a[xy]?b/
pattern =~ "ab" # 0 /x?/ # То же, что /x{0,1}/
pattern =~ "acb" # nil /x*/ # То же, что /x{0,}
pattern =~ "axb" # 0 /x+/ # То же, что /x{1,}
pat2 =~ "ayb" # 0 Фразеология, применяемая при описании регулярных выражений, изобилу-
pat2 =~ "acb" # nil ет яркими терминами: жадный (greedy), неохотный (reluctant), ленивый (lazy) и
Элементы часто повторяются неопределенное число раз (для формулировки собственнический (possessive). Самым важным является различие между жадны-
этого условия служит квантор +). Например, следующий образец соответствует ми и нежадными выражениями.
любому положительному числу: Рассмотрим следующий фрагмент кода. На первый взгляд, это регулярное вы-
pattern = /[0-9]+/ ражение должно сопоставляться со строкой "Where the", но на самом деле ему со-
pattern =~ "1" # 0 ответствует более длинная подстрока "Where the sea meets the":
pattern =~ "2345678" # 0
str = "Where the sea meets the moon-blanch'd land,"
Еще один типичный случай – образец, повторяющийся нуль или более раз. Конеч- match = /.*the/.match(str)
но, это условие можно выразить с помощью кванторов + и ?. Вот, например, как сказать, p match[0] # Вывести полученное соответствие:
что после строки Huzzah должно быть нуль или более восклицательных знаков: # "Where the sea meets the"
116 Регулярные выражения Обратные ссылки 117

Причина состоит в том, что оператор * выполняет жадное сопоставление, то 3.7. Обратные ссылки
есть продвигается так далеко по строке, как только можно, в поисках самого длин-
Каждая заключенная в круглые скобки часть регулярного выражения является от-
ного соответствия. Чтобы излечить его от жадности, нужно добавить вопроситель-
дельным соответствием. Они нумеруются, и есть несколько способов сослаться на
ный знак:
такие части по номерам. Сначала рассмотрим традиционный «некрасивый» способ.
str = "Where the sea meets the moon-blanch'd land," Сослаться на группы можно с помощью глобальных переменных $1, $2 и т. д:
match = /.*?the/.match(str)
str = "a123b45c678"
p match[0] # Вывести полученное соответствие:
if /(a\d+)(b\d+)(c\d+)/ =~ str
# "Where the"
puts "Частичные соответствия: '#$1', '#$2', '#$3'"
Итак, оператор * жадный, если за ним не стоит ?. То же самое относится к кван- # Печатается: Частичные соответствия: 'a123', 'b45', 'c768'
торам + и {m,n} и даже к самому квантору ?. end
Я не сумел найти разумных примеров применения конструкций {m,n}? и ??. Эти переменные нельзя использовать в подставляемой строке в методах sub
Если вам о них известно, пожалуйста, поделитесь со мной своим опытом. и gsub:
Дополнительная информация о кванторах содержится в разделе 3.13. str = "a123b45c678"
str.sub(/(a\d+)(b\d+)(c\d+)/, "1st=#$1, 2nd=#$2, 3rd=#$3")
3.6. Позитивное и негативное заглядывание вперед # "1st=, 2nd=, 3rd="
Почему такая конструкция не работает? Потому что аргументы sub вычисля-
Понятно, что регулярное выражение сопоставляется со строкой линейно (осу-
ются перед вызовом sub. Вот эквивалентный код:
ществляя при необходимости возвраты). Поэтому существует понятие «текуще-
го положения» в строке, это аналог указателя файла или курсора. str = "a123b45c678"
s2 = "1st=#$1, 2nd=#$2, 3rd=#$3"
Термин «заглядывание» означает попытку сопоставить часть строки, находя- reg = /(a\d+)(b\d+)(c\d+)/
щуюся дальше текущего положения. Это утверждение нулевой длины, поскольку str.sub(reg,s2)
даже если соответствие будет найдено, никакого продвижения по строке не про- # "1st=, 2nd=, 3rd="
изойдет (то есть текущее положение не изменится). Отсюда совершенно понятно, что значения $1, $2, $3 никак не связаны с сопо-
В следующем примере строка "New World" будет сопоставлена, если за ней сле- ставлением, которое делается внутри вызова sub.
дует одна из строк "Symphony" или "Dictionary". Однако третье слово не будет час- В такой ситуации на помощь приходят специальные коды \1, \2 и т. д.:
тью соответствия. str = "a123b45c678"
s1 = "New World Dictionary" str.sub(/(a\d+)(b\d+)(c\d+)/, '1st=\1, 2nd=\2, 3rd=\3')
s2 = "New World Symphony" # "1st=a123, 2nd=b45, 3rd=c768"
s3 = "New World Order"
Обратите внимание на одиночные (твердые) кавычки в предыдущем приме-
reg = /New World(?= Dictionary| Symphony)/
ре. Если бы мы воспользовались двойными (мягкими) кавычками, не приняв ни-
m1 = reg.match(s1) каких мер предосторожности, то элементы, которым предшествует обратная косая
m.to_a[0] # "New World" черта, были бы интерпретированы как восьмеричные числа:
m2 = reg.match(s2) str = "a123b45c678"
m.to_a[0] # "New World" str.sub(/(a\d+)(b\d+)(c\d+)/, "1st=\1, 2nd=\2, 3rd=\3")
m3 = reg.match(s3) # nil # "1st=\001, 2nd=\002, 3rd=\003"
Вот пример негативного заглядывания: Обойти эту неприятность можно за счет двойного экранирования:
reg2 = /New World(?! Symphony)/ str = "a123b45c678"
m1 = reg.match(s1) str.sub(/(a\d+)(b\d+)(c\d+)/, "1st=\\1, 2nd=\\2, 3rd=\\3")
m.to_a[0] # "New World" # "1st=a123, 2nd=b45, 3rd=c678"
m2 = reg.match(s2) Допустима и блочная форма подстановки, в которой можно использовать гло-
m.to_a[0] # nil бальные переменные:
m3 = reg.match(s3) # "New World"
str = "a123b45c678"
В данном случае строка "New World" подходит, только если за ней не следует str.sub(/(a\d+)(b\d+)(c\d+)/) { "1st=#$1, 2nd=#$2, 3rd=#$3" }
строка "Symphony". # "1st=a123, 2nd=b45, 3rd=c678"
118 Регулярные выражения Классы символов 119

При таком применении блока числа с обратной косой чертой нельзя исполь- # "beta "
зовать ни в двойных, ни в одиночных кавычках. Если вы немного поразмыслите, p1 = refs.begin(1) # 6
то поймете, что это разумно. p2 = refs.end(1) # 11
# "gamma "
Упомяну попутно о том, что существуют незапоминаемые группы (noncapturing
p3 = refs.begin(2) # 11
groups). Иногда при составлении регулярного выражения нужно сгруппировать p4 = refs.end(2) # 17
символы, но чему будет соответствовать в конечном счете такая группа, несущест- # "delta "
венно. На этот случай и предусмотрены незапоминаемые группы, описываемые p5 = refs.begin(3) # 17
синтаксической конструкцией (?:...): p6 = refs.end(3) # 23
str = "a123b45c678" # "beta gamma delta"
str.sub(/(a\d+)(?:b\d+)(c\d+)/, "1st=\\1, 2nd=\\2, 3rd=\\3") p7 = refs.begin(0) # 6
# "1st=a123, 2nd=c678, 3rd=" p8 = refs.end(0) # 23

В предыдущем примере вторая группа не запоминается, поэтому та группа, ко- Аналогично метод offset возвращает массив из двух чисел: смещение начала
торая должна была бы быть третьей, становится второй. и смещение конца соответствия. Продолжим предыдущий пример:
Лично мне не нравится ни одна из двух нотаций (\1 и $1). Иногда они удоб- range0 = refs.offset(0) # [6,23]
ны, но никогда не бывают необходимы. Все можно сделать «красивее», в объект- range1 = refs.offset(1) # [6,11]
но-ориентированной манере. range2 = refs.offset(2) # [11,17]
range3 = refs.offset(3) # [17,23]
Метод класса Regexp.last_match возвращает объект класса MatchData (как и
метод экземпляра match). У этого объекта есть методы экземпляра, с помощью ко- Части строки, которые находятся перед сопоставленной подстроки и после нее,
торых программист может получить обратные ссылки. можно получить методами pre_match и post_match соответственно. В том же коде:
Обращаться к объекту MatchData можно с помощью квадратных скобок, как ес- before = refs.pre_match # "alpha "
ли бы это был массив соответствий. Специальный элемент с индексом 0 содержит after = refs.post_match # "epsilon"
текст всей сопоставляемой строки, а элемент с индексом n ссылается на n-ую за-
помненную группу: 3.8. Классы символов
pat = /(.+[aiu])(.+[aiu])(.+[aiu])(.+[aiu])/i Классы символов – это просто форма перечисления (указание альтернатив), в ко-
# В этом образце есть четыре одинаковых группы. тором каждая группа состоит из одного символа. В простейшем случае список воз-
refs = pat.match("Fujiyama")
можных символов заключается в квадратные скобки:
# refs is now: ["Fujiyama","Fu","ji","ya","ma"]
x = refs[1] /[aeiou]/ # Соответствует любой из букв a, e, i, o, u; эквивалентно
y = refs[2..3] # /(a|e|i|o|u)/, только группа не запоминается.
refs.to_a.each {|x| print "#{x}\n"} Внутри класса символов управляющие последовательности типа \n по-прежне-
Отметим, что объект refs – не настоящий массив. Поэтому, если мы хотим об- му распознаются, но такие метасимволы, как . и ?, не имеют специального смысла:
ращаться с ним как с таковым, применяя итератор each, следует сначала преобра- /[.\n?]/ # Сопоставляется с точкой, символом новой строки,
зовать его в массив с помощью метода to_a (как показано в примере). # вопросительным знаком.
Есть и другие способы нахождения сопоставленной подстроки внутри исход- Символ каре (^) внутри класса символов имеет специальный смысл, если нахо-
ной строки. Методы begin и end возвращают смещения начала и конца соответ- дится в начале; в этом случае он формирует дополнение к списку символов:
ствия. (Важно понимать, что смещение конца – это индекс символа, следующего [^aeiou] # Любой символ, КРОМЕ a, e, i, o, u.
за найденным соответствием.) Дефис внутри класса символов обозначает диапазон (в лексикографическом
str = "alpha beta gamma delta epsilon" порядке):
# 0....5....0....5....0....5....
/[a-mA-M]/ # Любой символ из первой половины алфавита.
# (для удобства подсчета)
/[^a-mA-M]/ # Любой ДРУГОЙ символ, а также цифры и символы, отличные
# от букв и цифр.
pat = /(b[^ ]+ )(g[^ ]+ )(d[^ ]+ )/
# Три слова, каждое из которых представляет собой отдельное соответствие. Дефис в начале или в конце класса символов, а также каре в середине те-
refs = pat.match(str) ряют специальный смысл и интерпретируются буквально. То же относится
120 Регулярные выражения Сопоставление точки символу конца строки 121

к левой квадратной скобке, но правая квадратная скобка, очевидно, должна эк- Чтобы привести несколько искусственный пример умеренно сложного регу-
ранироваться: лярного выражения, предположим, что имеется такой список адресов:
/[-^[\]]/ # Сопоставляется с дефисом, каре и правой квадратной скобкой. addresses =
[ "409 W Jackson Ave", "No. 27 Grande Place",
Регулярные выражения в Ruby могут содержать ссылки на именованные клас-
"16000 Pennsylvania Avenue", "2367 St. George St.",
сы символов вида [[:name:]]. Так, [[:digit:]] означает то же самое, что образец "22 Rue Morgue", "33 Rue St. Denis",
[0-9]. Во многих случаях такая запись оказывается короче или, по крайней мере, "44 Rue Zeeday", "55 Santa Monica Blvd.",
понятнее. "123 Main St., Apt. 234", "123 Main St., #234",
Есть еще такие именованные классы: [[:print:]] (символы, имеющие графи- "345 Euneva Avenue, Suite 23", "678 Euneva Ave, Suite A"]
ческое начертание) и [[:alpha:]] (буквы): Здесь каждый адрес состоит из трех частей: номер дома, название улицы и не-
s1 = "abc\007def" обязательный номер квартиры. Я предполагаю, что перед числом может быть
/[[:print:]]*/.match(s1) необязательная строка No., а точку в ней можно опускать. Еще предположим, что
m1 = Regexp::last_match[0] # "abc" название улицы может включать символы, обычно входящие в состав слова, а так-
же апостроф, дефис и точку. Наконец, если адрес содержит необязательный номер
s2 = "1234def" квартиры, то ему должны предшествовать запятая и одна из строк Apt., Suite или
/[[:digit:]]*/.match(s2) # (знак номера).
m2 = Regexp::last_match[0] # "1234" Вот какое регулярное выражение я составил для разбора адреса. Обратите
внимание, насколько подробно оно прокомментировано (может быть, даже из-
/[[:digit:]]+[[:alpha:]]/.match(s2) лишне подробно):
m3 = Regexp::last_match[0] # "1234d"
regex = / ^ # Начало строки.
Каре перед именем класса символов формирует его дополнение: ((No\.?)\s+)? # Необязательно: No[.]
/[[:^alpha:]]/ # Все символы, кроме букв. \d+ \s+ # Цифры и пробелы.
((\w|[.'-])+ # Название улицы... может
Для многих классов имеется также сокращенная нотация. Наиболее распро- \s* # состоять из нескольких слов.
странены сокращения \d (любая цифра), \w (любой символ, входящий в состав )+
«слова») и \s (пропуски – пробел, знак табуляции или новой строки): (,\s* # Необязательно: запятая и т. д.
str1 = "Wolf 359" (Apt\.?|Suite|\#) # Apt[.], Suite, #
/\w+/.match(str1) # Соответствует "Wolf" (то же, что /[a-zA-Z_0-9]+/) \s+ # Пробелы.
/\w+ \d+/.match(str1) # Соответствует "Wolf 359" (\d+|[A-Z]) # Цифры или одна буква.
/\w+ \w+/.match(str1) # Соответствует "Wolf 359" )?
/\s+/.match(str1) # Соответствует " " $ # Конец строки.
/x
«Дополнительные» формы обычно записываются в виде прописной буквы:
Идея понятна. Когда сложность регулярного выражения достигает некоего по-
/\W/ # Любой символ, не входящий в состав слова. рога (какого именно – дело вкуса), делайте его обобщенным, чтобы можно было
/\D/ # Все кроме цифр.
добавить форматирование и комментарии.
/\S/ # Все кроме пропусков.
Возможно, вы заметили, что я пользовался обычными комментариями Ruby
Дополнительная информация, относящаяся только к Oniguruma, приводится (# ...), а не специальными, применяемыми в регулярных выражениях ((?#...)).
в разделе 3.13. Почему? Просто потому, что это разрешено! Специальный комментарий необхо-
дим только тогда, когда его следует закончить раньше конца строки (например, ес-
3.9. Обобщенные регулярные выражения ли в той же строке за комментарием продолжается регулярное выражение).
Регулярные выражения, особенно длинные, часто выглядят загадочно. Модифи-
катор x позволяет записывать регулярное выражение на нескольких строках. При 3.10. Сопоставление точки символу конца строки
этом пробелы и символы новой строки игнорируются, так что можно делать для Обычно точка соответствует любому символу, кроме конца строки. Если задан мо-
наглядности отступы. Заодно разрешается оставлять комментарии, хотя это воз- дификатор многострочности m, точка будет сопоставляться и с этим символом. Дру-
можно даже в простых регулярных выражениях. гой способ – задать флаг Regexp::MULTILINE при создании регулярного выражения:
122 Регулярные выражения Ruby и Oniguruma 123

str = "Rubies are red\nAnd violets are blue.\n" re1 =~ str # 0


pat1 = /red./ re2 =~ str # nil
pat2 = /red./m re1.match(str).to_a # ["abccccdef", "abccc"]
re2.match(str).to_a # []
str =~ pat1 # nil В предыдущем примере подвыражение abc* выражения re2 поглощает все
str =~ pat2 # 11
вхождения буквы c и (в соответствии с собственническим инстинктом) не отдает
Этот режим не оказывает влияния на то, где устанавливается соответствие их назад, препятствуя возврату.
якорям (^, $, \A, \Z). Изменяется только способ сопоставления с точкой.
3.13. Ruby и Oniguruma
3.11. Внутренние модификаторы Новая библиотека регулярных выражений в Ruby называется Oniguruma. Это япон-
Обычно модификаторы (например, i или m) задаются после регулярного выраже- ское слово означает что-то вроде «колесо духов». (Те, кто не владеет японским,
ния. Но что если мы хотим применить модификатор только к части выражения? часто пишут его неправильно; имейте в виду, что тут не обойтись без «guru»!)
Существует специальная нотация для включения и выключения модифика- Новая библиотека превосходит старую в нескольких отношениях. Прежде все-
торов. Заключенный в круглые скобки вопросительный знак, за которым следует го, она лучше работает с иноязычными строками, а также добавляет кое-какие ин-
один или несколько модификаторов, «включает» их до конца регулярного выра- тересные возможности к регулярным выражениям. Наконец, лицензия на ее ис-
жения. А если некоторым модификаторам предшествует минус, то соответствую- пользование мягче, чем на использование Ruby в целом. Когда писалась эта книга,
щие режимы «выключаются»: Oniguruma еще не была полностью интегрирована в Ruby.
/abc(?i)def/ # Соответствует abcdef, abcDEF, abcDef, ..., В следующем разделе мы расскажем, как определить, присутствует ли библи-
# но не ABCdef. отека Oniguruma. А затем покажем, как можно ее собрать, если она не включена в
/ab(?i)cd(?-i)ef/ # Соответствует abcdef, abCDef, abcDef, ..., дистрибутив.
# но не ABcdef или abcdEF.
/(?imx).*/ # То же, что /.*/imx 3.13.1. Проверка наличия Oniguruma
/abc(?i-m).*/m # Для последней части регулярного выражения включить Если вас интересует библиотека Oniguruma, то первым делом нужно выяснить,
# распознавание регистра, выключить многострочный
есть ли она в вашем экземпляре Ruby. В версиях 1.8.4 и младше ее, скорее всего,
# режим.
нет. Стандартно она включается в дистрибутив версии 1.9.
При желании можно поставить перед подвыражением двоеточие, и тогда за- Вот как можно без труда выяснить, присутствует ли Oniguruma, проверив три
данные модификаторы будут действовать только для этого подвыражения: условия. Во-первых, как я сказал, она стандартно поставляется в версии 1.9 и стар-
/ab(?i:cd)ef/ # То же, что /ab(?i)cd(?-i)ef/ ше. В последних версиях обеих библиотек для работы с регулярными выражения-
По техническим причинам использовать таким образом модификатор o нельзя. ми определена строковая константа Regexp::ENGINE. Если она содержит подстроку
Модификатор x – можно, но я не знаю, кому бы это могло понадобиться. Oniguruma, то у вас новая библиотека. И последний шаг: если вы все еще не знае-
те, с какой библиотекой работаете, можно попытаться вычислить регулярное вы-
ражение, записанное в «новом» синтаксисе. Если при этом возникнет исключение
3.12. Внутренние подвыражения SyntaxError, значит, у вас старая библиотека; в противном случае – новая.
Для указания подвыражений применяется нотация ?>: def oniguruma?
re = /(?>abc)(?>def)/ # То же, что /abcdef/ return true if RUBY_VERSION >= "1.9.0"
re.match("abcdef").to_a # ["abcdef"]
Отметим, что наличие подвыражения еще не означает группировки. С по- if defined?(Regexp::ENGINE) # Константа ENGINE определена?
мощью дополнительных скобок их, конечно, можно превратить в запоминаемые if Regexp::ENGINE.include?('Oniguruma')
return true # Какая-то версия Oniguruma.
группы.
else
Еще обратим внимание на то, что эта конструкция собственническая, то есть return false # Старая библиотека.
жадная и при этом не допускает возврата в подвыражение. end
str = "abccccdef" end
re1 = /(abc*)cdef/
re2 = /(?>abc*)cdef/ eval("/(?<!a)b/") # Новый синтаксис.
124 Регулярные выражения Ruby и Oniguruma 125

return true # Сработало: новая библиотека. 3.13.3. Некоторые новые возможности Oniguruma
rescue SyntaxError # Не сработало: старая библиотека.
Oniguruma добавляет много новых возможностей к механизму работы с регуляр-
return false
end ными выражениями в Ruby. Из самых простых отметим дополнительную управ-
ляющую последовательность для указания класса символов. Если \d и \D соот-
puts oniguruma? ветствуют десятичным цифрам и не цифрам, то \h и \H являются аналогами для
шестнадцатеричных цифр:
3.13.2. Сборка Oniguruma "abc" =~ /\h+/ # 0
Если в вашу версию библиотека Oniguruma не включена, можете самостоятельно "DEF" =~ /\h+/ # 0
откомпилировать Ruby и скомпоновать с недостающей библиотекой. Ниже при- "abc" =~ /\H+/ # nil
ведены соответствующие инструкции. Эта процедура должна работать начиная с
Добавилось возможностей у классов символов в квадратных скобках. Для ор-
версии 1.6.8 (хотя она уже совсем старенькая).
ганизации вложенных классов можно применять оператор &&. Вот как можно за-
Получить исходный текст Oniguruma можно из архива приложений Ruby RAA
писать регулярное выражение, соответствующее любой букве, кроме гласных a, e,
(http://raa.ruby-lang.org/) или найти в другом месте. Исходные тексты Ruby, ес-
тественно, находятся на официальном сайте. i, o, u:
Если вы работаете на платформе UNIX (в том числе в среде Cygwin в Windows reg1 = /[a-z&&[^aeiou]]/ # Задает пересечение.
или Mac OS/X), выполните следующие действия: А следующее выражение соответствует всему алфавиту, кроме букв от m до p:
1. gunzip oniguruma.tar.gz reg2 = /[a-z&&[^m-p]]/
2. tar xvf oniguruma.tar Поскольку такие выражения выглядят не очень понятно, рекомендую пользо-
3. cd oniguruma ваться этим средством осмотрительно.
4. ./configure with-rubydir=<ruby-source-dir> Другие возможности Oniguruma, например оглядывание назад и именованные
5. Одно из следующих: соответствия, будут рассмотрены ниже. Все связанное с интернационализацией
make 16 # Для Ruby 1.6.8 отложим до главы 4.
make 18 # Для Ruby 1.8.0/1.8.1
3.13.4. Позитивное и негативное оглядывание назад
6. cd ruby-source-dir
Если заглядывания вперед вам недостаточно, то Oniguruma предлагает еще и огля-
7. ./configure
дывание назад, позволяющее определить, предшествует ли текущему положению
8. make clean
заданный образец.
9. make
Как и многое другое в регулярных выражениях, эту возможность довольно труд-
10. make test # Простой тест интерпретатора Ruby. но понять и обосновать. Спасибо Эндрю Джексону за следующий пример.
11. cd ../oniguruma # Укажите путь к библиотеке. Предположим, что вам нужно проанализировать некоторую генетическую по-
12. make rtest следовательность (молекула ДНК состоит из четырех основных белков, которые
Или: обозначаются A, C, G и T.) Допустим, что мы ищем все неперекрывающиеся це-
make rtest RUBYDIR=ruby-install-dir почки нуклеотидов (длины 4), следующие за T. Нельзя просто попытаться найти T
Если же вы работаете на платформе Win32, скажем в Windows XP, то потребу- и взять следующие четыре символа, поскольку T может быть последним символом
ются Visual C++ и исполняемый файл patch.exe. Выполните следующие действия: в предыдущем соответствии.
1. Распакуйте архив любой имеющейся у вас программой. gene = 'GATTACAAACTGCCTGACATACGAA'
2. copy win32\Makefile Makefile seqs = gene.scan(/T(\w{4})/)
# seqs равно: [["TACA"], ["GCCT"], ["ACGA"]]
2. Одно из следующих:
Но в этом коде мы пропустили цепочку GACA, которая следует за GCCT. Позитив-
nmake 16 RUBYDIR=ruby-source-dir # для Ruby 1.6.8
ное оглядывание назад позволит найти все нужные цепочки:
nmake 18 RUBYDIR=ruby-source-dir # для Ruby 1.8.0/1.8.1
gene = 'GATTACAAACTGCCTGACATACGAA'
4. Следуйте инструкции в файле ruby-source-dir\win32\README.win32. seqs = gene.scan(/(?<=T)(\w{4})/)
При возникновении ошибок обратитесь в список рассылки или конференцию. # seqs равно: [["TACA"], ["GCCT"], ["GACA"], ["ACGA"]]
126 Регулярные выражения Ruby и Oniguruma 127

Следующий пример – небольшая модификация примера, предложенного 3.13.6. Именованные соответствия


К. Косако (K. Kosako). Предположим, что есть текст в формате XML (или HTML), Специальной формой подвыражения является именованное выражение, которое
и мы хотим перевести в верхний регистр весь текст вне тегов (то есть cdata). Вот позволяет присвоить образцу имя (а не просто порядковый номер).
как можно сделать это с помощью оглядывания назад: Синтаксически это выглядит так: (?<name>expr), где name – имя, начинающе-
text = <<-EOF еся с буквы (как идентификаторы в Ruby). Обратите внимание на сходство этой
<body> <h1>This is a heading</h1> конструкции с неименованным атомарным подвыражением.
<p> This is a paragraph with some Для чего может понадобиться именованное выражение? Например, для того,
<i>italics</i> and some <b>boldface</b>
чтобы сослаться на него внутри обратной ссылки. Ниже приведен пример просто-
in it...</p>
</body> го регулярного выражения для сопоставления с повторяющимся словом (см. так-
EOF же раздел 3.14.6):
re1 = /\s+(\w+)\s+\1\s+/
pattern = /(?:^| # Начало или... str = "Now is the the time for all..."
(?<=>) # текст после '>' re1.match(str).to_a # ["the the","the"]
) Здесь мы запомнили слово, а затем сослались на него по номеру \1. Пример-
([^<]*) # И все символы, кроме '<' (запомнены).
но так же можно пользоваться ссылками на именованные выражения. При первом
/x
обнаружении подвыражения ему присваивается имя, а в обратной ссылке упо-
puts text.gsub(pattern) {|s| s.upcase } требляется символ \k, за которым следует это имя (всегда в угловых скобках):
re2 = /\s+(?<anyword>\w+)\s+\k<anyword>\s+/
# Вывод: Второй вариант длиннее, зато понятнее. (Имейте в виду, что в одном и том же
# <body> <h1>THIS IS A HEADING</h1>
регулярном выражении нельзя использовать и именованные, и нумерованные об-
# <p>THIS IS A PARAGRAPH WITH SOME
ратные ссылки.) Если нравится, пользуйтесь!
# <i>ITALICS</i> AND SOME <b>BOLDFACE</b>
# IN IT...</p> В Ruby уже давно можно включать обратные ссылки в строки, передаваемые
# </body> методам sub и gsub. Раньше с этой целью допускалось лишь использование нуме-
рованных ссылок, но в самых последних версиях именованные тоже разрешены:
3.13.5. Еще о кванторах str = "I breathe when I sleep"
Мы уже встречались с атомарными подвыражениями в «классической» библиоте-
ке регулярных выражений в Ruby. Они выделяются с помощью нотации (?>...) и # Нумерованные соответствия...
r1 = /I (\w+) when I (\w+)/
являются «собственническими» в том смысле, что жадные и не допускают возвра-
s1 = str.sub(r1,'I \2 when I \1')
та внутрь подвыражения.
Oniguruma предлагает еще один способ выразить собственническую при- # Именованные соответствия...
роду – с помощью квантора +. Он отличается от метасимвола + в смысле «один r1 = /I (?<verb1>\w+) when I (?<verb2>\w+)/
или более» и даже может использоваться с ним совместно. (На самом деле это s2 = str.sub(r2,'I \k<verb2> when I \k<verb1>')
«вторичный» квантор, как и ?, который можно употреблять в таких контек-
стах, как ??, +? и *?.) puts s1 # I sleep when I breathe
puts s2 # I sleep when I breathe
Применение + к повторяющемуся образцу эквивалентно заключению его в
скобки как независимого подвыражения, например: Еще одно возможное применение именованных выражений – повторное упо-
r1 = /x*+/ # То же, что /(?>x*)/ требление выражения. В таком случае перед именем ставится символ \g (а не \k).
r2 = /x++/ # То же, что /(?>x+)/ Определим, например, образец spaces так, чтобы можно было использовать его
r3 = /x?+/ # То же, что /(?>x?)/ многократно. Тогда последнее выражение примет вид:
По техническим причинам Ruby не считает конструкцию {n,m}+ собственни- re3 = /(?<spaces>\s+)(?<anyword>\w+)\g<spaces>\k<anyword>\g<spaces>/
ческой. Обратите внимание, что этот образец многократно употребляется с помощью
Понятно, что новый квантор – не более чем удобное обозначение, никакой но- маркера \g. Особенно удобна такая возможность в рекурсивных регулярных вы-
вой функциональности он не несет. ражениях, но это тема следующего раздела.
128 Регулярные выражения Примеры регулярных выражений 129

Нотацией \g<1> можно пользоваться и тогда, когда именованных подвыраже- Ошибка объясняется наличием рекурсивного обращения в начале каждой аль-
ний нет. Тогда запомненное ранее подвыражение вызывается по номеру, а не по тернативы. Немного подумав, вы поймете, что это приведет к бесконечному воз-
имени. врату.
И последнее замечание об именованных соответствиях. В самых последних вер-
сиях Ruby имя (в виде строки или символа) может передаваться методу MatchData в 3.14. Примеры регулярных выражений
качестве индекса, например:
В этом разделе мы приведем краткий перечень регулярных выражений, которые
str = "My hovercraft is full of eels"
могут оказаться полезны на практике или просто послужат учебными примера-
reg = /My (?<noun>\w+) is (?<predicate>.*)/
m = reg.match(str) ми. Для простоты примеров ни одно выражение не зависит от наличия Oniguruma.
puts m[:noun] # hovercraft
3.14.1. Сопоставление с IP-адресом
puts m["predicate"] # full of eels
puts m[1] # то же, что m[:noun] или m["noun"] Пусть мы хотим понять, содержит ли строка допустимый IPv4-адрес. Стандарт-
но он записывается в точечно-десятичной нотации, то есть в виде четырех деся-
Как видите, обычные индексы тоже не запрещены. Обсуждается возможность
тичных чисел, разделенных точками, причем каждое число должно находиться в
добавить в объект MatchData и синглетные методы.
диапазоне от 0 до 255.
puts m.noun Приведенный ниже образец решает эту задачу (за немногими исключениями
puts m.predicate
типа «127.1»). Для удобства восприятия мы разобьем его на части. Отметим, что
Но во время работы над книгой это еще не было реализовано. символ \d дважды экранирован, чтобы косая черта не передавалась из строки в ре-
гулярное выражение (чуть ниже мы решим и эту проблему).
3.13.7. Рекурсия в регулярных выражениях
num = "(\\d|[01]?\\d\\d|2[0-4]\\d|25[0-5])"
Возможность повторно обращаться к подвыражению позволяет создавать ре-
pat = "^(#{num}\.){3}#{num}$"
курсивные регулярные выражения. Например, данный код находит любое вложен- ip_pat = Regexp.new(pat)
ное выражение с правильно расставленными скобками (спасибо Эндрю Джексону):
str = "a * ((b-c)/(d-e) - f) * g" ip1 = "9.53.97.102"

reg = /(? # Начало именованного выражения. if ip1 =~ ip_pat # Печатается: "да"


\( # Открывающая круглая скобка. puts "да"
(?: # Незапоминаемая группа. else
(?> # Сопоставление с собственническим выражением: puts "нет"
\\[()] # экранированная скобка end
| # ЛИБО
Надо признать, что в определении переменной num слишком много символов
[^()] # вообще не скобка.
) # Конец собственнического выражения. обратной косой черты. Определим ее в виде регулярного выражения, а не строки:
| # ЛИБО num = /(\d|[01]?\d\d|2[0-4]\d|25[0-5])/
\g # Вложенная группа в скобках (рекурсивный вызов). Когда одно регулярное выражение интерполируется в другое, вызывается метод
)* # Незапоминаемая группа повторяется нуль или
to_s, который сохраняет всю информацию из исходного регулярного выражения.
# более раз.
\) # Закрывающая круглая скобка. num.to_s # "(?-mix:(\\d|[01]?\\d\\d|2[0-4]\\d|25[0-5]))"
) # Конец именованного выражения. Иногда для встраивания удобно использовать регулярное выражение, а не стро-
/x ку. Хорошее эвристическое правило: интерполируйте регулярные выражения, если
m = reg.match(str).to_a # ["((b-c)/(d-e) - f)", "((b-c)/(d-e) - f)"]
нет веских причин интерполировать строки.
Отметим, что левосторонняя рекурсия запрещена. Следующий пример допустим: IPv6-адреса пока не очень широко распространены, но для полноты рассмот-
str = "bbbaccc" рим и их. Они записываются в виде восьми шестнадцатеричных чисел, разделен-
re1 = /(?<foo>a|b\g<foo>c)/ ных двоеточиями, с подавлением начальных нулей.
re1.match(str).to_a # ["bbbaccc","bbbaccc"] num = /[0-9A-Fa-f]{0,4}/
А такой – нет: pat = /^(#{num}:){7}#{num}$/
re2 = /(?<foo>a|\g<foo>c)/ # Синтаксическая ошибка! ipv6_pat = Regexp.new(pat)
130 Регулярные выражения Примеры регулярных выражений 131

v6ip = "abcd::1324:ea54::dead::beef" Почему такое выражение не годится? Взгляните на этот пример и поймете:
rom1.to_s # "(?-mix:m{0,3})"
if v6ip =~ ipv6_pat # Печатается: "да"
puts "да" Обратите внимание, что метод to_s запоминает флаги для каждого выраже-
else ния; тем самым флаг всего выражения перекрывается.
puts "нет"
end 3.14.4. Сопоставление с числовыми константами
Сопоставление с простым целым десятичным числом – самое простое. Число со-
3.14.2. Сопоставление с парой «ключ-значение» стоит из необязательного знака и последовательности цифр (правда, Ruby позво-
Иногда приходится работать со строками вида «ключ=значение» (например, при ляет использовать знак подчеркивания в качестве разделителя цифр). Отметим,
разборе конфигурационного файла приложения). что первая цифра не должна быть нулем, иначе число будет интерпретироваться
Следующий код извлекает ключ и значение. Предполагается, что ключ состо- как восьмеричное.
ит из одного слова, значение продолжается до конца строки, а знак равенства мо- int_pat = /^[+-]?[1-9][\d_]*$/
жет быть окружен пробелами: Целые константы в других системах счисления обрабатываются аналогично.
pat = /(\w+)\s*=\s*(.*?)$/ Образцы для шестнадцатеричных и двоичных чисел сделаны не чувствительными
str = "color = blue" к регистру, так как они содержат букву:
matches = pat.match(str) hex_pat = /^[+-]?0x[\da-f_]+$/i
oct_pat = /^[+-]?0[0-7_]+$/
puts matches[1] # "color" bin_pat = /^[+-]?0b[01_]+$/i
puts matches[2] # "blue" Сопоставить число с плавающей точкой в обычной нотации несколько слож-
нее. Последовательности цифр по обе стороны десятичной точки необязательны,
3.14.3. Сопоставление с числами, записанными римскими цифрами но хотя бы одна цифра должна быть:
Следующее довольно сложное регулярное выражение сопоставляется с любым
float_pat = /^(\d[\d_]*)*\.[\d_]*$/
правильно записанным римскими цифрами числом (до 3999 включительно). Как
и раньше, для удобства восприятия образец разбит на части: Образец для чисел, записанных в научной нотации, основан на предыдущем:
rom1 = /m{0,3}/i sci_pat = /^(\d[\d_]*)?\.[\d_]*(e[+-]?)?(_*\d[\d_]*)$/i
rom2 = /(d?c{0,3}|c[dm])/i Эти образцы могут оказаться полезны, если вы хотите убедиться, что строка
rom3 = /(l?x{0,3}|x[lc])/i содержит число, перед тем как пытаться преобразовать ее.
rom4 = /(v?i{0,3}|i[vx])/i
roman = /^#{rom1}#{rom2}#{rom3}#{rom4}$/ 3.14.5. Сопоставление с датой и временем
Пусть надо выделить дату и время, записанные в формате mm/dd/yy hh:mm:ss. Вот
year1985 = "MCMLXXXV"
первая попытка: datetime = /(\d\d)\/(\d\d)\/(\d\d) (\d\d): (\d\d):(\d\d)/.
if year1985 =~ roman # Печатается: "да" Но такой образец распознает некоторые некорректные даты и отвергает пра-
puts "да" вильные. Следующий вариант более избирателен. Обратите внимание, как мы
else строим его путем интерполяции мелких регулярных выражений в более крупное:
puts "нет" mo = /(0?[1-9]|1[0-2])/ # От 01 до 09 или от 1 до 9 или 10-12.
end
dd = /([0-2]?[1-9]|[1-3][01])/ # 1-9 или 01-09 или 11-19 и т. д.
Возможно, у вас появилось искушение поставить в конец всего выражения мо- yy = /(\d\d)/ # 00-99
дификатор i, чтобы сопоставлялись и строчные буквы: hh = /([01]?[1-9]|[12][0-4])/ # 1-9 или 00-09 или...
# Это не работает! mi = /([0-5]\d)/ # 00-59, обе цифры должны присутствовать.
ss = /([0-6]\d)?/ # Разрешены еще и доли секунды ;-)
rom1 = /m{0,3}/
rom2 = /(d?c{0,3}|c[dm])/ date = /(#{mo}\/#{dd}\/#{yy})/
rom3 = /(l?x{0,3}|x[lc])/ time = /(#{hh}:#{mi}:#{ss})/
rom4 = /(v?i{0,3}|i[vx])/
roman = /^#{rom1}#{rom2}#{rom3}#{rom4}$/i datetime = /(#{date} #{time})/
132 Регулярные выражения Заключение 133

Вот как можно вызвать это регулярное выражение из метода String#scan, что- package = "mylib-1.8.12"
бы получить массив соответствий: matches = package.match(/(.*)-(\d+)\.(\d+)\.(\d+)/)
name, major, minor, tiny = matches[1..-1]
str="Recorded on 11/18/07 20:31:00"
str.scan(datetime)
3.14.9. Еще несколько образцов
# [["11/18/07 20:31:00", "11/18/07", "11", "18", "00",
# "20:31:00", "20", "31", ":00"]] Завершим наш список несколькими выражениями из категории «разное». Как
обычно, почти все эти задачи можно решить несколькими способами.
Разумеется, все это можно было сделать с помощью одного большого регуляр-
Пусть нужно распознать двузначный почтовый код американского штата.
ного выражения:
Проще всего, конечно, взять выражение /[A-Z]{2}/. Но оно сопоставляется с та-
datetime = %r{(
(0?[1-9]|1[0-2])/ # mo: от 01 до 09 или от 1 до 9 или 10-12.
кими строками, как XX или ZZ, которые допустимы, но бессмысленны. Следующий
([0-2]?[1-9]|[1-3][01])/ # dd: 1-9 или 01-09 или 11-19 и т. д. образец распознает все стандартные аббревиатуры, общим числом 51 (50 штатов
(\d\d) [ ] # yy: 00-99 и DC – округ Колумбия):
([01]?[1-9]|[12][0-4]): # hh: 1-9 или 00-09 или... state = /^A[LKZR] | C[AOT] | D[EC] | FL | GA | HI | I[DLNA] |
([0-5]\d): # mm: 00-59, обе цифры должны присутствовать. K[SY] | LA | M[EDAINSOT] | N[EVHJMYCD] | O[HKR] |
(([0-6]\d))? # ss: разрешены еще и доли секунды ;-) PA | RI | S[CD] | T[NX] | UT | V[TA] | W[AVIY]$/x
)}x
Для ясности я воспользовался обобщенным регулярным выражением (моди-
Обратите внимание на конструкцию %r{}, позволяющую не экранировать сим- фикатор x). Пробелы и символы новой строки в нем игнорируются.
волы обратной косой черты.
Продолжая эту тему, приведем регулярное выражение для распознавания поч-
3.14.6. Обнаружение повторяющихся слов в тексте тового индекса США (он может состоять из пяти или девяти цифр):
В этом разделе мы реализуем детектор повторяющихся слов. Повторение одного zip = /^\d{5}(-\d{4})?$/
и того же слова два раза подряд – типичная опечатка. Следующий код распозна- Якоря (в этом и других выражениях) призваны лишь гарантировать, что ни
ет такие ситуации: до, ни после сопоставленной строки никаких лишних символов нет. Отметим, что
double_re = /\b(['A-Z]+) +\1\b/i это выражение не отбрасывает несуществующие индексы, поэтому оно не так по-
лезно, как предыдущее.
str="There's there's the the pattern." Следующее регулярное выражение распознает номер телефона в формате NANP
str.scan(double_re) # [["There's"],["the"]]
(североамериканский план нумерации). Есть три способа записи такого номера:
Обратите внимание на модификатор i в конце выражения, он позволяет про- phone = /^((\(\d{3}\) |\d{3}-)\d{3}-\d{4}|\d{3}\.\d{3}\.\d{4})$/
водить сопоставление без учета регистра. Каждой группе соответствует массив, "(512) 555-1234" =~ phone # true
поэтому в результате получается массив массивов. "512.555.1234" =~ phone # true
"512-555-1234" =~ phone # true
3.14.7. Поиск слов, целиком набранных прописными буквами "(512)-555-1234" =~ phone # false
Мы упростили пример, предположив, что в тексте нет чисел, подчерков и т. д. "512-555.1234" =~ phone # false
allcaps = /\b[A-Z]+\b/ Распознавание денежной суммы в долларах также не составит труда:
string = "This is ALL CAPS" dollar = /^\$\d+(\.\d\d)?$/
string[allcaps] # "ALL" Ясно, что слева от десятичной точки должна быть хотя бы одна цифра, а после
Suppose you want to extract every word in all-caps: знака доллара не должно быть пробелов. Отметим еще, что если вы хотите только
string.scan(allcaps) # ["ALL", "CAPS"] выделить, а не проконтролировать суммы в долларах, то якоря следовало бы уда-
При желании можно было бы обобщить эту идею на идентификаторы Ruby и лить, а центы сделать необязательными.
аналогичные вещи.

3.14.8. Сопоставление с номером версии 3.15. Заключение


Принято присваивать библиотекам и приложениям номера версий, состоящие из трех На этом мы заканчиваем обсуждение регулярных выражений в Ruby. Ознакомив-
чисел, разделенных точками. Следующее регулярное выражение сопоставляется с по- шись со строками и регулярными выражениями, мы можем перейти к теме интер-
добной строкой, выделяя попутно имя пакета и отдельные части номера версии: национализации, где пригодится рассмотренный выше материал.
Исторические сведения и терминология 135

def shorten(str)
(str[0..0] + str[1..-2].length.to_s + str[-1..-1]).upcase
end

shorten("internationalization") # I18N
shorten("multilingualization") # M17N
Глава 4. Интернационализация в Ruby shorten("localization") # L10N
Термины I18N и M17N – практически синонимы; еще говорят «глобализа-
ция», но это слово имеет и другой смысл. Термин L10N более широкий: он подразу-
Посему дано ему имя: Вавилон, ибо там смешал Господь мевает полную поддержку местной культуры и соглашений (например, символов
язык всей земли, и оттуда рассеял их Господь по всей земле. обозначения валюты, способов форматирования даты и времени, использования
Бытие, 11:9 точки или запятой для отделения целой и дробной частей в десятичном числе и
многое другое).
Мы уже говорили, что тип символа, наверное, самый важный из всех. Но что та- Начнем с терминологии, поскольку в этой области широко используется жар-
кое символьные данные? Какие символы? Из какого алфавита? Какой язык? Ка- гон. Заодно совершим небольшой экскурс в историю, так как текущее состояние
кие культурные особенности? дел становится понятным, лишь если рассмотреть его в контексте медленной эво-
В прошлом в вычислительной технике и информатике применялся исключи- люции. Уроки истории будут сведены к минимуму.
тельно английский язык. Традиция восходит, вероятно, еще к Чарльзу Беббиджу.
Это не так уж и плохо, ведь надо же с чего-то начинать, а алфавит из 26 букв без 4.1. Исторические сведения и терминология
диакритических знаков – неплохое начало. В «недобрые старые дни» становления компьютерных технологий, примерно сов-
Но теперь компьютеры распространились повсеместно. Наверное, в каждой падающие по времени с периодом использования перфокарт, существовало мно-
стране есть хотя бы несколько компьютеров и тот или иной вид доступа в сеть. Ес- жество наборов символов. К счастью, с появлением кода ASCII в 1970-х годах эти
тественно, любой человек предпочитает читать Web-страницы, электронную поч- дни миновали безвозвратно.
ту и прочие данные на своем родном языке, а не только на английском. Аббревиатура ASCII означает American Standard Code for Information Inter-
Человеческие языки поразительно разнообразны. Некоторые являются поч- change (Американский стандартный код обмена информацией). Это был большой
ти фонетическими, к другим это определение применимо с большой натяжкой. шаг вперед, однако ключевое слово здесь «американский». Код проектировался
В одних есть настоящий алфавит, другие же предлагают набор из нескольких ты- даже без учета европейских языков, не говоря уже об азиатских.
сяч символов, ведущих происхождение от пиктограмм. В некоторых языках не Но в нем были и огрехи. Набор символов ASCII состоит из 128 символов (он
один алфавит, а несколько. На каких-то языках пишут сверху вниз, на других – 7-разрядный). Но как можно так расточительно относиться к дополнительному
справа налево. Некоторые алфавиты очень просты, в других ряд букв украшен биту? Возникла естественная идея расширить набор ASCII, воспользовавшись ко-
приводящими в трепет точечками, черточками, кружочками, галочками и штри- дами от 128 до 255 для других целей. Беда в том, что эта идея была реализована
хами... Есть языки, где при определенных условиях соседние буквы комбиниру- многократно и по-разному компанией IBM и другими. Не было общепринятого
ются; иногда это обязательно, а иногда и нет. В каких-то языках предусмотрено соглашения о том, какому символу соответствует, например, код 221.
различение строчных и прописных букв, но большинство таких различий не уста- Недостатки такого подхода очевидны. Даже если отправитель и получатель
навливает. договорятся об используемом наборе символов, все равно они не смогут общать-
За 25 лет мы прошли длинный путь. Мы худо-бедно научились приводить в ся на нескольких языках: для всех сразу не хватит символов. Если вы хотите пи-
порядок хаотическое нагромождение символов и языков. сать по-немецки, но вставить в текст несколько цитат на греческом или иврите, то,
Если вам часто приходится иметь дело с программами, спроектированными скорее всего, ничего не получится. И эта схема не позволила даже приблизиться к
для работы в различной языковой среде, то вы знаете, что такое интернациона- решению проблем, связанных с азиатскими языками, например китайским, япон-
лизация. Это способность программы поддерживать более одного естественного ским и корейским.
языка. Было два основных способа решить эту задачу. Первый – использовать гораздо
С интернационализацией тесно связаны мультиязычность и локализация. По- более обширный набор символов, например представляя каждый символ 16 би-
чему-то принято сокращать эти слова, удаляя средние буквы и подставляя вместо тами (так называемые широкие символы). Второй – обратиться к многобайтовым
них число, равное количеству удаленных букв: кодировкам переменной длины. При такой схеме одни символы представляются
136 Интернационализация в Ruby Исторические сведения и терминология 137

единственным байтом, другие – двумя, а третьи – тремя или даже большим числом. • Байт – это просто восемь битов (хотя когда-то даже это было неверно). По
При этом, очевидно, возникает масса вопросов. В частности, любая строка должна традиции многие считают, что байт соответствует одному символу. Ясно,
однозначно декодироваться. Первый байт многобайтового символа мог бы прина- что в контексте I18N это не так.
длежать специальному классу, а потому мы сумели бы понять, что следует ожидать • Кодовая позиция – один элемент воображаемой таблицы, с помощью кото-
дополнительный байт; но как быть со вторым и последующими? Разрешено ли им рой представляется набор символов. Хотя это и не совсем верно, можете
перекрываться с набором однобайтовых символов? Могут ли определенные сим- считать, что кодовые позиции взаимно однозначно отображаются на сим-
волы выступать в роли второго и третьего байта или это следует запретить? Смо- волы. Точнее будет сказать, что иногда для уникального указания символа
жем ли мы перейти в середину строки и при этом не запутаться? Сможем ли про- требуется несколько кодовых позиций.
сматривать строку в обратном направлении? Для разных кодировок были приняты • Глиф (печатный знак) – визуальное представление кодовой позиции. Хотя
различные проектные решения. интуитивно это и не совсем очевидно, символ и его визуальное представле-
В конечном счете родилась идея кодировки Unicode. Считайте, что это «всемир- ние – разные вещи. (Я могу открыть текстовый редактор и набрать пропис-
ный набор символов». Увы, на практике все не так просто. ную A десятком разных шрифтов, но все это будет один и тот же символ A.)
Возможно, вы слышали, что Unicode был (или остается) ограничен 65536 сим- • Понятие графемы близко к глифу, но о графемах мы говорим в контексте
волами (именно столько различных комбинаций можно представить 16 битами). языка, а не программного обеспечения. Графема может быть комбинацией
Распространенное заблуждение!.. При проектировании Unicode такие ограниче- (простой или не очень) двух и более глифов. Так пользователь воспринима-
ния не закладывались. С самого начала было ясно, что во многих случаях это бу- ет символ в контексте своего родного языка. Разница настолько тонкая, что
дет многобайтовая схема. Количество представимых с помощью Unicode симво- большинство программистов могут о ней никогда не задумываться.
лов практически безгранично, и это хорошо, так как 65000 никогда не хватит для
всех языков мира. Что же тогда такое символ? Даже в мире Unicode нет четкого понимания этого
Говоря об интернационализации, нужно прежде всего понимать, что интер- предмета, поскольку языки ведут себя по-разному, а программисты мыслят иначе,
чем прочие люди. Будем говорить, что символ – это абстракция написания знака,
претация строки не является внутренне присущей самой строке. Это заблуждение
который визуально может быть представлен одним или несколькими способами.
проистекает из уже неактуального представления, будто существует лишь один
Перейдем к конкретике. Сначала я хочу познакомить вас с нотацией. Тради-
способ хранения строки.
ционно кодовые позиции Unicode записываются как U+, а затем четыре или бо-
Подчеркну, это исключительно важное положение. Внутренне строка – всего
лее шестнадцатеричных цифр в верхнем регистре. То, что мы называем латинской
лишь последовательность байтов. Представьте себе, что в памяти машины хранит-
буквой A, можно представить в виде U+0041.
ся один байт в кодировке ASCII. Если это буква, которую мы называем «пропис-
Теперь возьмем букву é (строчная e с акутом). Ее можно представить в Unicode
ная латинская A», то реально хранится число 65.
двумя способами. Во-первых, это одна кодовая позиция U+00E9 (СТРОЧНАЯ ЛА-
Почему мы считаем, что 65 – это A? Потому что так мы договорились исполь-
ТИНСКАЯ E С АКУТОМ). С другой стороны, это сочетание двух кодовых пози-
зовать (интерпретировать) это значение. Если мы складываем его с другим чис-
ций: строчная e + диакритический знак акут – U+0065 и U+0301. Иными словами,
лом, то оно используется (интерпретируется) как число. А если отправляем его на
СТРОЧНАЯ ЛАТИНСКАЯ E, за которой следует АКУТ.
терминал по последовательной линии связи – значит, интерпретируем как ASCII-
Обе формы одинаково правильны. Более короткая называется монолитной
символ.
(precomposed) формой. Однако имейте в виду, что не для каждого языка имеют-
Если можно по-разному интерпретировать одиночный байт, то почему же не-
ся монолитные варианты, поэтому не всегда можно свести подобный символ к од-
льзя так сделать для последовательности байтов? На самом деле, чтобы получи-
ной кодовой позиции.
лась осмысленная строка, предполагаемая схема интерпретации (или кодировка)
Я назвал Unicode кодировкой, но это не вполне верно. Unicode отображает
должна быть известна заранее. Кодировка – это просто соответствие между двоич-
символы на кодовые позиции; существуют разные способы отобразить кодовые
ными числами и символами. И снова не все так просто.
позиции на двоичное представление. По существу, Unicode – это семейство коди-
Поскольку Ruby появился в Японии, он прекрасно справляется с двумя раз-
ровок.
личными японскими кодировками (и ASCII). Не буду тратить время на рассказ о
Возьмем, к примеру, строку "Matz". Она состоит из четырех кодовых позиций
поддержке японского языка; если вы японец, то в вашем распоряжении сколько
Unicode:
угодно книг по Ruby на этом языке. А для всех остальных наиболее распростра-
ненной кодировкой является Unicode. О ней мы и будем говорить в этой главе. "Matz" # U+004d U+0061 U+0074 U+007a
Но перед тем как перейти к деталям, познакомимся с некоторыми терминами. Естественнее всего сохранить их в виде простой последовательности байтов.
Называть вещи полезными именами – одна из основ мудрости! 00 4d 00 61 00 74 00 7a
138 Интернационализация в Ruby Кодировки в пост-ASCII мире 139

Такая кодировка называется UCS-2 (два байта) или UTF-16 (16 битов). Отме- 4.2. Кодировки в пост-ASCII мире
тим, что эта кодировка имеет две разновидности: тупоконечную (big-endian) и ост-
«Век ASCII» прошел, хотя не все еще осознали этот факт. Многие допущения, ко-
роконечную (little-endian) – в зависимости от того, старший или младший байт
торые программисты делали в прошлом, уже несправедливы. Нам необходимо но-
хранится первым.
вое мышление.
Заметим, однако, что каждый второй байт в этой последовательности нулевой.
Есть две идеи, которые, на мой взгляд, являются основополагающими, почти
Это не просто совпадение, английский язык редко выходит за пределы кодовой
аксиомами. Во-первых, строка не имеет внутренней интерпретации. Она должна
позиции U+00FF. Так разбрасываться памятью расточительно.
И это наблюдение подводит нас к идее кодировки UTF-8. В ней «традицион- интерпретироваться в соответствии с некоторым внешним стандартом. Во-вто-
ные» символы представлены одним байтом, а остальные – несколькими. Вот как рых, байт и символ – не одно и то же; символ может состоять из одного или не-
записывается та же строка в кодировке UTF-8: скольких байтов. Есть и другие уроки, но это самое важное.
Эти факты оказывают на программирование тонкое влияние. Рассмотрим сна-
4d 61 74 7a
чала, как следует работать с символьными строками по-современному.
Мы всего лишь избавились от нулей. Однако более важен тот факт, что мы по-
лучили обычную кодировку ASCII. Так и задумано: «простой ASCII» можно счи- 4.2.1. Библиотека jcode и переменная $KCODE
тать собственным подмножеством UTF-8. Чтобы использовать в Ruby разные наборы символов, вы должны знать о глобаль-
Отсюда, в частности, следует, что при интерпретации текста в кодировке UTF-8 ной переменной $KCODE, от значения которой зависит поведение многих системных
как ASCII-текста он выглядит «как обычно» (особенно если это преимуществен- методов, манипулирующих строками. (Кстати говоря, буква K – напоминание о
но англоязычный текст). Иногда вы видите, что браузер или другое приложение кандзи, одной из иероглифических азбук в японском языке.) Эта переменная при-
отображает английский текст правильно, но местами появляются «крокозябры». нимает одно из пяти стандартных значений, каждое из которых представлено од-
Это, скорее всего, означает, что программа сделала неверные предположения об ной буквой, неважно – строчной или прописной (ASCII и NONE – одно и то же).
используемой кодировке. a ASCII
Итак, можно сказать, что UTF-8 экономит память. Конечно, я снова станов- n NONE (ASCII)
люсь на англоцентрическую точку зрения (по крайней мере, ASCII-центричес- e EUC
кую). Если текст в основном состоит из ASCII-символов, то да, память экономит- s SJIS
ся, но для других языков, например греческого или русского, размер строк даже u UTF-8
увеличится. Для ясности можно пользоваться и полными названиями (например, $KCODE =
Еще одно очевидное достоинство UTF-8 – «обратная совместимость» с коди- "UTF-8"). Важен только первый символ.
ровкой ASCII, которая, по-видимому, все еще остается самой распространенной О кодировке ASCII мы уже знаем. EUC и Shift-JIS (SJIS) нам малоинтересны.
однобайтовой кодировкой в мире. Наконец, у UTF-8 есть некоторые особенности, Мы сосредоточимся на значении UTF-8.
делающие ее удобной для программистов. Установив значение $KCODE, вы задаром получаете весьма богатую функцио-
Во-первых, байты, входящие в состав многобайтовых символов, тщательно по- нальность. Например, метод inspect (он автоматически вызывается при обраще-
добраны. Нулевой байт (ASCII 0) никогда не встречается в качестве n-ого байта нии к методу p для печати объекта в читаемом виде) обычно учитывает текущее
в последовательности (где n > 1); то же самое справедливо для таких распростра- значение $KCODE.
ненных символов, как косая черта (обычно используется для разделения компо- $KCODE = "n"
нентов пути к файлу). На самом деле никакой байт из диапазона 0x00-0x7F не мо-
жет быть частью никакого другого символа. # Для справки: французское слово " p e"
Второй байт многобайтового символа однозначно определяет, сколько байтов # обозначает разновидность меча (sword).
за ним следует. Этот второй байт всегда выбирается из диапазона от 0xC0 до 0xFD,
а следующие за ним – из диапазона от 0x80 до 0xBF. Таким образом, схема кодиро- eacute = ""
eacute << 0303 << 0251 # U+00E9
вания свободна от состояния и позволяет восстанавливать пропущенные или ис-
sword = eacute + "p" + eacute + "e"
каженные байты. p eacute # "\303\251"
UTF-8 – одна из самых распространенных и гибких кодировок в мире. Она
применяется с начала 1990-х годов и является кодировкой по умолчанию для p sword # "\303\251p\303\251e"
XML-документов. В этой главе мы будем иметь дело главным образом именно с
UTF-8. $KCODE = "u"
140 Интернационализация в Ruby Кодировки в пост-ASCII мире 141

p eacute # " " это ошибкой, поскольку получить представление слова с первой прописной бук-
p sword # " p e" вой довольно трудно; такая задача просто не решается в схеме интернационализа-
Регулярные выражения в режиме UTF-8 тоже становятся несколько «умнее». ции Ruby. Считайте, что это нереализованное поведение.)
$KCODE = "n" $KCODE = "u"
letters = sword.scan(/(.)/) sword.upcase # " P E"
# [["\303"], ["\251"], ["p"], ["\303"], ["\251"], ["e"]] sword.capitalize # " p e"
puts letters.size # 6 Если вы не пользуетесь монолитной формой, то в некоторых случаях метод
может сработать, поскольку латинские буквы отделены от диакритических зна-
$KCODE = "u"
ков. Но в общем случае работать не будет – в частности, для турецкого, немецко-
letters = sword.scan(/(.)/) го, голландского и любого другого языка с нестандартными правилами преобра-
# [[" "], ["p"], [" "], ["e"]] зования регистра.
puts letters.size # 4 Возможно, вы думаете, что неакцентированные символы в некотором смысле
Библиотека jcode предоставляет также несколько полезных методов, напри- эквивалентны своим акцентированным вариантам. Это почти всегда не так. Здесь
мер jlength и each_char. Рекомендую включать эту библиотеку с помощью дирек- мы имеем дело с разными символами. Убедимся в этом на примере метода count:
тивы require всякий раз, как вы работаете с кодировкой UTF-8. $KCODE = "u"
В следующем разделе мы снова рассмотрим некоторые типичные операции со sword.count("e") # 1 (не 3)
строками и регулярными выражениями. Заодно поближе познакомимся с jcode. Но для составных (не монолитных) символов верно прямо противоположное.
В этом случае латинская буква рспознается.
4.2.2. Возвращаясь к строкам и регулярным выражениям Метод count возвращает сбивающий с толку результат, когда ему передается
При работе с UTF-8 некоторые операции ничем не отличаются. Например, конка- многобайтовый символ. Метод jcount ведет себя в этом случае правильно:
тенация строк выполняется так же, как и раньше: $KCODE = "u"
" p" + " e" # " p e" sword.count("e ") # 5 (не 3)
" p" << " e" # " p e" sword.jcount("e ") # 3
Поскольку UTF-8 не имеет состояния, то для проверки вхождения подстроки Существует вспомогательный метод mbchar? , который определяет, есть ли в
тоже ничего специально делать не нужно: строке многобайтовые символы.
" p e ".include?(" ") # true $KCODE = "u"
Однако при написании интернациональной программы некоторые типичные sword.mbchar? # 0 (смещение первого многобайтового символа)
допущения все же придется переосмыслить. Ясно, что символ больше не эквива- "foo".mbchar? # nil
лентен байту. При подсчете символов или байтов надо думать о том, что именно В библиотеке jcode переопределены также методы chop, delete, squeeze, succ,
мы хотим сосчитать и для чего. То же относится к числу итераций. tr и tr_s. Применяя их в режиме UTF-8, помните, что вы работаете с версиями,
По общепринятому соглашению, кодовую позицию часто представляют себе «знающими о многобайтовости». При попытке манипулировать многобайтовыми
как «программистский символ». Это еще одна полуправда, но иногда она оказы- строками без библиотеки jcode вы можете получить странные или ошибочные ре-
вается полезной. зультаты.
Метод jlength возвращает число кодовых позиций в строке, а не байтов. Если Можно побайтно просматривать строку, как обычно, с помощью итератора
нужно получить число байтов, пользуйтесь методом length. each_byte. А можно просматривать посимвольно с помощью итератора each_char.
$KCODE = "u" Второй способ имеет дело с односимвольными строками, первый (в текущей вер-
require 'jcode' сии Ruby) – с однобайтными целыми.
Разумеется, мы в очередной раз приравниваем кодовую позицию к символу.
sword = " p e " Несмотря на название, метод each_char на самом деле перебирает кодовые пози-
sword.jlength # 4 ции, а не символы.
sword.length # 6
$KCODE = "u"
Такие методы, как upcase и capitalize, обычно неправильно работают со спе- sword.each_byte {|x| puts x } # Шесть строк с целыми числами.
циальными символами. Это ограничение текущей версии Ruby. (Не стоит считать sword.each_char {|x| puts x } # Четыре строки со строками.
142 Интернационализация в Ruby Кодировки в пост-ASCII мире 143

Если вы запутались, не переживайте. Все мы через это проходили. Я попытал- def reveal_non_ascii(str)
ся свести все вышесказанное в таблицу 4.1. str.unpack('U*').map do |cp|
if cp < 0x80
Таблица 4.1. Составные и монолитные формы
cp.chr
else
Монолитная форма “ ”
'(U+%04X)'% cp
Название символа Глиф Кодовая Байты
позиция UTF-8 Примечания end
end.join
Строчная латинская e é U+00E9 0xC3 Один символ, одна кодовая end
с акутом 0xA9 позиция, один байт
У метода String#unpack есть «близкий родственник» Array#pack, выполняю-
Составная форма “ ”
щий обратную операцию:
Название символа Глиф Кодовая Байты
позиция UTF-8 Примечания [233, 112, 233, 101].pack('U*') # " p e "
Строчная латинская e e U+0065 0x65 Один символ, две кодовых Мы можем воспользоваться им, чтобы вставить Unicode-символы, которые
Модифицирующий акут ´ U+0301 0xCC позиции (два «программистских трудно ввести с клавиатуры:
0x81 символа»), три байта UTF-8
eacute = [0xE9].pack('U')
cafe = "caf#{eacute}" # "caf "
Что еще надо учитывать при работе с интернациональными строками? Квад- Регулярным выражениям тоже известно о многобайтовых символах, особенно
ратные скобки по-прежнему относятся к байтам, а не к символам. Но при желании если вы пользуетесь библиотекой Oniguruma (мы рассматривали ее в главе 3). На-
это можно изменить. Ниже приведена одна из возможных реализаций (не особен- пример, образец /./ сопоставляется с одним многобайтовым символом.
но эффективная, зато понятная): Модификатор u извещает регулярное выражение о том, что мы работаем с ко-
class String дировкой UTF-8. Если $KCODE равно "u", то модификатор можно не задавать, од-
нако это и не повредит. (К тому же такая избыточность может быть полезна, если
def [](index) код является частью большой программы, а какое значение переменной $KCODE в
self.scan(/./)[index] ней установлено, вам неизвестно.)
end Даже без Oniguruma регулярные выражения распознают, относится ли данный
def []=(index,value)
многобайтовый символ к категории тех, что могут входить в состав слова:
$KCODE = "u"
arr = self.scan(/./)
arr[index] = value sword =~ /\w/ # 0
self.replace(arr.join) sword =~ /\W/ # nil
value При наличии Oniguruma последовательности, начинающиеся с символа об-
end ратной косой черты (\w, \s и т. п.) распознают и более широкие диапазоны кодо-
вых точек: слова, пропуски и т. д.
end Регулярные выражения позволяют безопасно выполнять простые манипуля-
Конечно, здесь не реализована значительная часть функциональности настоя- ции со строками. Мы и так можем без труда усекать строки. Следующий код воз-
щего метода [ ], который понимает диапазоны, регулярные выражения и т. д. Если вращает не более 20 символов из строки ascii_string:
вам все это нужно, придется запрограммировать самостоятельно. ascii_string[0,20]
У метода unpack есть параметры, помогающие манипулировать Unicode-стро- Однако, поскольку кодовая позиция Unicode может занимать более одного бай-
ками. Указав в форматной строке параметр U*, мы можем преобразовать строку в та, такую технику нельзя безопасно применять к строке в кодировке UTF-8. Есть
кодировке UTF-8 в массив кодовых позиций (U без звездочки преобразует только
риск, что в конце строки окажется недопустимая последовательность байтов. Кро-
первую кодовую позицию):
ме того, это не слишком полезно, так как мы не можем заранее сказать, сколько в ре-
codepoints = sword.unpack('U*') # [233, 112, 233, 101] зультате получится кодовых позиций. На помощь приходят регулярные выражения:
Вот несколько более полезный пример, в котором все кодовые позиции в стро- def truncate(str, max_length)
ке, отличные от ASCII (то есть начиная с U+0080), преобразуются к виду U+XXXX, ко- str[/.{0,#{max_length}}/m]
торый мы обсуждали выше: end
144 Интернационализация в Ruby Кодировки в пост-ASCII мире 145

4.2.3. Распознавание кодировки 4. СТРОЧНАЯ ЛАТИНСКАЯ БУКВА O С ТРЕМОЙ + ЛИГАТУРА ДВОЙ-


Распознать, в какой кодировке записана данная строка, довольно сложно. Много- НОЕ F + n + e + n
байтовые кодировки обладают отличительными признаками, по которым их мож- Трема – это две точки над буквой (в немецком языке называется «умляут»).
но опознать, но с однобайтовыми – а именно они применяются в западных язы- Нормализацией называется процедура приведения разных представлений сим-
ках – дело обстоит куда хуже. Для решения можно применить статистические вола к стандартной форме. Можно быть уверенным, что после нормализации дан-
методы, но эта тема выходит за рамки данной книги (к тому же результат в общем ный символ закодирован вполне определенным образом. Каким именно, зависит
случае получается не слишком надежным). от того, чего мы хотим достичь. В приложении 15 к стандарту Unicode перечисле-
К счастью, обычно перед нами стоит более простая задача – выяснить, запи- ны четыре формы нормализации:
сана ли строка в кодировке UTF-8. На этот вопрос можно дать достаточно надеж- 1. Форма D (каноническая декомпозиция).
ный ответ. Приведем один способ (основанный на том, что метод unpack возбуж- 2. Форма C (каноническая декомпозиция с последующей канонической ком-
дает исключение, если ему передана некорректная строка): позицией).
class String 3. Форма KD (совместимая декомпозиция).
def utf8?
unpack('U*') rescue return false 4. Форма KC (совместимая декомпозиция с последующей канонической ком-
true позицией).
end Иногда можно встретить аббревиатуры NKFC (Normalization Form KC) и т. д.
end Точные правила, сформулированные в стандарте, довольно сложны; в них про-
ведено различие между «канонической эквивалентностью» и «совместимой экви-
4.2.4. Нормализация Unicode-строк валентностью». (Корейский и японский языки требуют особого рассмотрения, но
До сих пор мы пользовались монолитными символами, в которых базовый символ мы не станем тратить на это время.) В таблице 4.2 показано, как форма нормали-
и диакритический знак объединены в одну кодовую позицию. Но, вообще говоря, зации влияет на приведенные выше строки.
в Unicode символы и диакритические знаки представлены отдельно. Вместо того
чтобы хранить букву é в кодовой позиции СТРОЧНАЯ ЛАТИНСКАЯ БУКВА E Таблица 4.2. Нормализованные формы в Unicode
С АКУТОМ, можно было бы представить ее в составной форме как СТРОЧНУЮ
Исходная NFD NFC NFKD NFKC
ЛАТИНСКУЮ БУКВУ E и МОДИФИЦИРУЮЩИЙ АКУТ.
Для чего это может понадобиться? Для обеспечения дополнительной гибкос- o+¨+f+f+n+e+n o+¨+f+f+n+e+n ö+f+f+n+e+n o+¨+f+f+n+e+n ö+f+f+n+e+n
ти и возможности применять диакритические знаки к любому символу, а не огра- ö+f+f+n+e+n o+¨+f+f+n+e+n ö+f+f+n+e+n o+¨+f+f+n+e+n ö+f+f+n+e+n
ничивать себя комбинациями, которые предусмотрел проектировщик кодировки. o+¨+ff+n+e+n o+¨+ff+n+e+n ö+ff+n+e+n o+¨+f+f+n+e+n ö+f+f+n+e+n
На самом деле в шрифты включены глифы для наиболее распространенных ком- ö+ff+n+e+n o+¨+ff+n+e+n ö+ff+n+e+n o+¨+f+f+n+e+n ö+f+f+n+e+n
бинаций символа и диакритического знака, но отображение символа и его коди-
рование – вещи разные. Формы C и D обратимы, KC и KD – нет. С другой стороны, потеря некоторых
При проектировании Unicode приходилось учитывать такие вещи, как эф- данных в формах KC и KD – свидетельство того, что все четыре строки двоично
фективность и совместимость с существующими национальными кодировками. эквивалентны. Какая форма лучше всего подходит, зависит от приложения. Мы
Иногда это приводит к избыточности; например, в Unicode имеются кодовые по- еще вернемся к этой теме в следующем разделе.
зиции как для составных форм, так и для многих уже применяющихся монолит- Для Ruby есть библиотека, позволяющая выполнить описанные нормализации,
ных форм. хотя в стандартный дистрибутив она не входит. Вы можете скачать ее со страницы
Рассмотрим, к примеру, немецкое слово «öffnen» (открывать). Даже если за- http://www.yoshidam.net/Ruby.html и установить командой gem install unicode.
быть о регистре, его можно закодировать четырьмя способами: Если библиотека unicode установлена, то для выполнения любой нормализа-
1. o + МОДИФИЦИРУЮЩАЯ ТРЕМА (U+0308) + f + f + n + e + n ции достаточно вызвать один из методов Unicode.normalize_X:
2. СТРОЧНАЯ ЛАТИНСКАЯ БУКВА O С ТРЕМОЙ (U+00F6) + f + f + n + require 'unicode'
sword_kd = Unicode.normalize_KD(sword)
e+n
sword_kd.scan(/./) # ["e", "'", "p", "e", "'", "e"]
3. o + МОДИФИЦИРУЮЩАЯ ТРЕМА + ЛИГАТУРА ДВОЙНОЕ F sword_kc = Unicode.normalize_KC(sword)
(U+FB00) + n + e + n sword_kc.scan(/./) # [" ", "p", " ", "e"]
146 Интернационализация в Ruby Кодировки в пост-ASCII мире 147

4.2.5. Упорядочение строк Затем создадим хэшированную таблицу, чтобы установить соответствие меж-
Обычно, хотя и не всегда, строки упорядочиваются по алфавиту или сходным об- ду исходными и трансформированными строками, и воспользуемся ей для сорти-
разом. Упорядочение тесно связано с нормализацией: в обоих случаях применяют- ровки исходных строк. Наличие такой таблицы позволяет провести трансформа-
ся одни и те же идеи и библиотеки. цию только один раз.
Предположим, например, что мы хотим отсортировать такой массив строк: def collate(array)
eacute = [0x00E9].pack('U') transformations = array.inject({}) do |hash, item|
acute = [0x0301].pack('U') hash[item] = yield item
array = ["epicurian", "#{eacute}p#{eacute}e", "e#{acute}lan"] hash
# ["epicurian", " p e ", " lan"] end
array.sort_by {|x| transformations[x] }
Что произойдет, если передать этот массив методу Array#sort? end
array.sort # ["epicurian", " lan", " p e "]
Не годится!.. Попытаемся понять, почему так получилось. Сортируемые стро- collate(array) {|a| transform(a) } # [" lan", " p e", "epicurian"]
ки Ruby сравнивает побайтно. Чтобы убедиться в этом, достаточно взглянуть на Уже лучше, но мы еще не учли прописные буквы и эквивалентность символов.
первые несколько байтов каждой строки: Возьмем для примера немецкий язык.
array.map {|item| "#{item}: #{item.unpack('C*')[0,3].join(',')}" } На самом деле в немецком языке есть несколько способов упорядочения; мы
# ["epicurian: 101,112,105", " p e : 195,169,112", остановимся на стандарте DIN-2 (как в телефонном справочнике). Согласно это-
# "e'lan: 101,204,129"] му стандарту, символ ß (эсцет) эквивалентен ss, а умляут эквивалентен букве e (то
Тут возникают две трудности. Во-первых, символы UTF-8, не имеющие ана- есть ö – то же самое, что oe и т. д.).
лога в кодировке ASCII, начинаются с байта, имеющего большое числовое значе- Наш метод трансформации должен учитывать эти детали. Снова начнем с де-
ние, а стало быть, после сортировки неизбежно окажутся после ASCII-символов. композиции составных символов. Например, модифицирующая трема (умляут)
Во-вторых, составные латинские символы оказываются раньше монолитных из- представляется кодовой позицией U+0308. За основу мы возьмем метод преобразо-
за первого ASCII-байта. вания регистра, имеющийся в Ruby, но несколько дополним его. Вот как выглядит
В системные библиотеки обычно включают функции сортировки, которые сравни- теперь код трансформации:
вают строки в соответствии с правилами конкретного языка. В библиотеке, поставля- def transform_de(str)
емой вместе с компилятором языка C, для этого служат функции strxfrm и strcoll. decomposed = Unicode.normalize_KD(str).downcase
Имейте в виду, что проблема возникает даже в случае кодировки ASCII. При decomposed.gsub!(' ', 'ss')
сортировке ASCII-строк в Ruby производится прямое лексикографическое срав- decomposed.gsub([0x0308].pack('U'), 'e')
нение, однако в реальной жизни (например, если мы хотим отсортировать по на- end
званиям книги из библиотеки Конгресса США) есть много правил, которые не учи-
тываются при таком упрощенном подходе. array = ["Stra e", " ffnen"]
array.map {|x| transform_de(x) } # ["strasse", "oeffnen"]
Для упорядочения строк можно создать промежуточные строки и отсортиро-
вать именно их. Как конкретно это сделать, зависит от предъявляемых требований Не для всех языков годится такой прямолинейный подход. Например, в испан-
и языка; универсального алгоритма не существует. ском между буквами n и o есть еще буква ñ. Однако, если каким-то образом сдви-
Предположим, что список обрабатывается согласно правилам английского язы- нуть оставшиеся буквы, то мы справимся и с этой проблемой. В листинге 4.1 для
ка, причем диакритические знаки игнорируются. Первым делом нужно определить упрощения обработки нормализация применена к монолитным символам. Кроме
методику трансформации. Мы приведем все символы к составному виду, а затем того, мы облегчили себе жизнь, игнорируя различия между буквами с диакрити-
исключим диакритические знаки, оставив только базовые символы. Для модифи- ческими знаками и без них.
цирующих диакритических знаков в Unicode выделен диапазон от U+0300 to U+036F:
def transform(str) Листинг 4.1. Упорядочение строк в испанском языке
Unicode.normalize_KD(str).unpack('U*').select{ |cp| def map_table(list)
cp < 0x0300 || cp > 0x036F table = {}
}.pack('U*') list.each_with_index do |item, i|
end item.split(',').each do |subitem|
array.map{|x| transform(x) } # ["epicurian", "epee", "elan"] table[Unicode.normalize_KC(subitem)] = (?a + i).chr
148 Интернационализация в Ruby Кодировки в пост-ASCII мире 149

end зависят от платформы, но наиболее распространенные стандартизованы и имеют-


end ся везде. Если установлена пакетная утилита iconv, то перечень распознаваемых
table кодировок можно получить с помощью команды iconv –l.
end Помимо названия кодировки, iconv принимает еще флаги, управляющие ее
поведением. Они указываются в конце строки, содержащей целевую кодировку.
ES_SORT = map_table(%w(
Обычно iconv возбуждает исключение, если получает недопустимые вход-
a,A, , b,B c,C d,D e,E, , f,F g,G h,H i,I, , j,J k,K l,L m,M
n,N , o,O, , p,P q,Q r,R s,S t,T u,U, , v,V w,W x,X y,Y z,Z ные данные или почему-либо не может представить их в целевой кодировке. Флаг
)) //IGNORE подавляет исключение.
broken_utf8_string = "hello\xfe"
def transform_es(str) converter = Iconv.new('ISO-8859-15', 'UTF-8')
array = Unicode.normalize_KC(str).scan(/./u) # будет возбуждено исключение Iconv::IllegalSequence
array.map {|c| ES_SORT[c] || c}.join converter.iconv(broken_utf8_string)
end
converter = Iconv.new('ISO-8859-15//IGNORE', 'UTF-8')
array = %w[ ste estoy a o apogeo amor] converter.iconv(broken_utf8_string) # "hello"
array.map {|a| transform_es(a) } Этот же флаг позволяет очистить строку от неверных данных:
# ["etue", "etupz", "aop", "aqpgep", "amps"] broken_sword = " p e \xfe"
converter = Iconv.new('UTF-8//IGNORE', 'UTF-8')
collate(array) {|a| transform_es(a) } converter.iconv(broken_sword) # " p e"
# ["amor", "a o", "apogeo", " ste", "estoy"]
Иногда некоторые символы нельзя представить в целевой кодировке. Обычно
В реальности упорядочение немного сложнее, чем показано в примерах вы-
в этом случае возбуждается исключение. Флаг //TRANSLIT говорит iconv, что нуж-
ше; обычно требуется до трех уровней обработки. На первом уровне сравнивают-
но вместо этого попытаться подобрать приблизительные эквиваленты.
ся только базовые символы без учета диакритических знаков и регистра, на вто-
converter = Iconv.new('ASCII', 'UTF-8')
ром учитываются диакритические знаки, а на третьем – регистр. Второй и третий
converter.iconv(sword) # Возбуждается Iconv::IllegalSequence.
уровень необходимы лишь в том случае, когда на предыдущих уровнях строки converter = Iconv.new('ASCII//IGNORE', 'UTF-8')
совпали. Кроме того, в некоторых языках последовательности, состоящие из не- converter.iconv(sword) # "pe"
скольких символов, сортируются как единая семантическая единица (например, converter = Iconv.new('ASCII//TRANSLIT', 'UTF-8')
в хорватском lj расположено между l и m). Поэтому разработка языковозависимо- converter.iconv(sword) # "'ep'ee"
го или обобщенного алгоритма сортировки – задача нетривиальная: необходимо Этим свойством можно воспользоваться, чтобы получить URL, содержащий
хорошо разбираться в конкретном языке. Невозможно изобрести по-настоящему только ASCII-символы:
универсальный алгоритм сортировки, который давал бы правильные результаты str = "Stra e p e "
для всех языков, хотя попытки в этом направлении производились. converter = Iconv.new('ASCII//TRANSLIT', 'UTF-8')
converter.iconv(sword).gsub(/ /, '-').gsub(/[^a-z\-]/in).downcase
4.2.6. Преобразование из одной кодировки в другую # "strasse-epee"
В стандартной библиотеке Ruby имеется интерфейс к библиотеке iconv для пре-
Однако работать это будет лишь в отношении латиницы.
образования из одной кодировки символов в другую. Она должна работать на всех
В листинге 4.2 приведен реальный пример совместного применения библиотек
платформах, в том числе и в Windows (если дистрибутив устанавливался момен-
iconv и open-uri для скачивания Web-страницы и перекодирования ее в UTF-8.
тальным инсталлятором).
Чтобы преобразовать строку из UTF-8 в ISO-8859-15, библиотека iconv ис-
Листинг 4.2. Перекодирование Web-страницы в кодировку UTF-8
пользуется следующим образом:
require 'open-uri'
require 'iconv'
require 'iconv'
converter = Iconv.new('ISO-8859-15', 'UTF-8')
sword_iso = converter.iconv(sword)
def get_web_page_as_utf8(url)
Важно помнить, что сначала указывается целевая кодировка, а потом исход- open(url) do |io|
ная (как при присваивании). Количество и названия поддерживаемых кодировок source = io.read
150 Интернационализация в Ruby Справочники сообщений 151

type, *parameters = io.content_type_parse языково-зависимые строки от остальной программы. Тогда для того, чтобы про-
# Не перекодировать, если не (X)HTML грамма «заговорила» на другом языке, достаточно всего лишь подменить спра-
unless type =~ %r!^(?:text/html|application/xhtml+xml)$! вочник.
return source
«Наилучший» способ реализовать эту идею в Ruby – воспользоваться библио-
end
# Сначала проверяем заголовки, присланные сервером: текой Ruby-GetText-Package. Я буду называть ее просто gettext, поскольку имен-
if pair = parameters.assoc('charset') но так называется содержащий ее файл (не путайте с утилитой gettext!). Эту ве-
encoding = pair.last ликолепную библиотеку написал Масао Муто (Masao Mutoh), он же очень помог
# Затем анализируем HTML: при написании данного раздела.
elsif source =~ /\]*?charset=([^\s'"]+)/i Библиотека представляет собой реализацию на Ruby (не обертку) набора ути-
encoding = $1 лит gettext из проекта GNU (самый известный продукт в этой области). Ее офи-
# Если не удалось определить, предполагаем кодировку по умолчанию,
циальный сайт находится по адресу http://gettext.rubyforge.org/, а утилиты GNU
# определенную в стандарте HTTP.
else можно найти на сайте http://www.gnu.org/software/gettext/.
encoding = 'ISO-8859-1'
end
4.3.1. Исторические сведения и терминология
converter = Iconv.new('UTF-8//IGNORE', encoding) Библиотека gettext на самом деле, как мы увидим, состоит из нескольких библи-
return converter.iconv(source) отек. Для доступа к основным функциям нужно включить предложение require
end 'gettext', а для получения разного рода дополнительных средств (в частности,
end работы со справочниками сообщений) – предложение require 'gettext/utils'.
Это еще не все системные вопросы, связанные с преобразованием кодировок. Главная причина, по которой мы используем справочники сообщений, – это,
Предположим, что в операционной системе, где установлен Ruby, определена ло- конечно, перевод сообщений на другие языки. С их помощью мы также обраба-
каль, отличная от UTF-8, или Ruby общается с ОС не в UTF-8 (так, например, обсто- тываем случаи, когда формы единственного и множественного числа различают-
ит дело в дистрибутиве для Win32). Тогда возникают дополнительные сложности. ся (один файл, два файла). Кстати, эти правила очень сильно зависят от конкрет-
Например, Windows поддерживает Unicode в именах файлов и на системном ного языка.
уровне работает исключительно в Unicode. Но в настоящее время Ruby взаимодей- Обычно у каждой библиотеки и приложения имеется собственный справоч-
ствует с Windows при помощи устаревших кодовых страниц. Для англоязычного ник сообщений. Следовательно, в дистрибутив можно включать набор переведен-
и большинства других западных изданий это страница 1252 (или WINDOWS-1252). ных на разные языки справочников.
Внутри программы можно пользоваться и кодировкой UTF-8, но все имена Учитываются переменные окружения LANG и GETTEXT_PATH. Их назначение мы
файлов придется перевести в кодировку, заданную кодовой страницей. Iconv по- рассмотрим ниже.
может это сделать, но важно не забывать, что кодовая страница позволяет описать Для сопровождения справочника сообщений есть две основных операции (они
только малое подмножество всех символов, имеющихся в Unicode. выполняются вне вашей программы): извлечь сообщения из исходного текста Ruby-
Кроме того, это означает, что пока Ruby для Windows не может открывать фай- программы для формирования начального справочника и включить новые сообще-
лы, имена которых нельзя описать с помощью кодовой страницы. Это ограниче- ния из исходного текста в существующий справочник (слияние). Операции извле-
ние не относится к Mac OS X, Linux и другим системам с локалью UTF-8. чения и слияния мы рассмотрим в разделе 4.3.3.

4.3.2.Приступаем к работе со справочниками сообщений


4.3. Справочники сообщений Возможно, библиотека gettext на вашем компьютере уже установлена. Если нет,
проще всего выполнить команду gem install gettext.
Ложбан не зависит от национальных особенностей. Для разработки вам понадобятся утилиты GNU. Если вы работаете в системе
Его словарь был создан алгоритмически на основе шести UNIX, то, скорее всего, они уже установлены. В случае платформы Win32 можно
наиболее распространенных в мире разговорных языков: установить Glade/GTK+ для Windows; заодно вы получите и утилиты GNU. В лю-
китайского, хинди, английского, русского, испанского и арабского. бом случае необходимы они только на этапе разработки, а не во время выполнения.
Nick Nicholas, John Cowan. What is Lojban? Если у вас нет программы rake, установите ее из gem-пакета. Это дополнитель-
ное удобство.
Справочник сообщений – это набор сообщений на одном языке. Данное поня- Коль скоро среда настроена и все установлено, можно приступать к работе со
тие неотъемлемо от концепции локализации (L10N). Идея в том, чтобы отделить справочниками. Но сначала познакомимся с терминологией.
152 Интернационализация в Ruby Справочники сообщений 153

• PO-файл – это переносимый объектный файл. Так называется текстовое Отметим также, что параметры отделены от текста сообщения, поэтому при
(понятное человеку) представление справочника сообщений. У каждо- необходимости могут подставляться в другом порядке. Ведь иногда при переводе
го такого файла есть вариант для различных поддерживаемых локалей. на другой язык приходится переставлять слова.
POT-файл – это шаблон. Тот же метод можно вызвать и короче:
• MO-файл – это переносимый двоичный файл справочника. Он создается puts _("Name: %s, Age: %d") % [@name, @age]
из PO-файла. Библиотека для Ruby умеет читать только MO-файлы, но не Однако мы рекомендуем более длинную запись. Она понятнее и дает больше
PO-файлы. информации переводчику.
• Текстовый домен – это, по существу, просто базовое имя MO-файла. Он ас- Метод n_ предназначен для обработки единственного и множественного чис-
социирован с приложением (привязан к нему). ла. Значение параметра @children_num – индекс, говорящий о том, какую из зара-
нее заданных строк использовать. (Правило Plural-Forms, о котором я скоро рас-
4.3.3. Локализация простого приложения скажу, определяет порядок вычисления индекса.)
В следующем примере определяется класс Person, после чего с ним выполняются Отметим, что сообщения по умолчанию обязаны быть англоязычными (даже
различные действия. Метод show выводит локализованные сообщения: если родной язык программиста не английский). Нравится вам это или нет, но ан-
require 'gettext' глийский ближе всего к универсальному языку с точки зрения большинства пе-
реводчиков.
class Person Я сказал, что нам пригодится программа rake. Создадим файл Rakefile (в ка-
include GetText талоге myapp) для сопровождения справочников сообщений. Он будет выполнять
две основные операции: обновлять PO-файлы и создавать MO-файлы.
def initialize(name, age, children_num)
require 'gettext/utils'
@name, @age, @children_num = name, age, children_num
bindtextdomain("myapp")
desc "Update pot/po files."
end
task :updatepo do
GetText.update_pofiles("myapp", ["person.rb"], "myapp 1.0.0")
def show
end
puts _("Information") desc "Create mo-files"
puts _("Name: %{name}, Age: %{age}") % {:name => @name, :age => @age} task :makemo do
puts n_("%{name} has a child.", "%{name} has %{num} children.", GetText.create_mofiles
@children_num) % {:name => @name, :num => @children_num} end
end
end Здесь мы воспользовались библиотекой gettext/utils, в которой имеются
функции для работы со справочниками сообщения. Метод update_pofiles созда-
john = Person.new("John", 25, 1) ет начальный файл myapp/po/myapp.pot на основе исходного текста person.rb. При
john.show втором (и всех последующих) вызовах эта функция выполнит обновление, или
linda = Person.new("Linda", 30, 3) слияние файла myapp/po/myapp.pot и всех файлов вида myapp/po/#{lang}/myapp.po.
linda.show Второй параметр – массив целевых файлов. Обычно он задается примерно так:
Предположим, что этот код сохранен в файле myapp/person.rb. Как вы скоро GetText.update_pofiles("myapp",
увидите, иерархия каталогов имеет значение. Вызов метода bindtextdomain связы- Dir.glob("{lib,bin}/**/*.{rb,rhtml}"),
вает текстовый домен "myapp" с объектом Person во время выполнения. "myapp 1.0.0")
В методе show есть три обращения к библиотеке gettext. Вызываемый метод Вызов метода GetText.create_mofiles создает необходимые подкаталоги в ка-
называется _ (одно подчеркивание), чтобы не отвлекать внимание. талоге data/locale/ и генерирует MO-файлы из PO-файлов.
Первое обращение просто выводит локализованное сообщение, соответству- Итак, выполнив команду rake updatepo, мы создадим каталог myapp/po, а в нем
ющее строке "Information". Второе демонстрирует локализованное сообщение с файл myapp.pot.
двумя параметрами. В хэше задается список значений, подставляемых в строку. Теперь отредактируем заголовок файла po/myapp.pot. Он содержит описание
Интерполировать их напрямую нельзя, потому что это вступало бы в противоре- приложения (название, имя автора, адрес электронной почты, условия лицензи-
чие с основной целью: хранить в справочнике небольшое число сообщений. рования и т. д.).
154 Интернационализация в Ruby Справочники сообщений 155

# Пример приложения. (Осмысленное название) # Форма множественного числа.


# Copyright (C) 2006 Foo Bar (Автор приложения) "Plural-Forms: nplurals=2; plural=(n != 1);\n"
# Файл распространяется по лицензии XXX. (Лицензия)
# #: person.rb:12
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR. (Информация о переводчике) msgid "Information"
# msgstr "Jouhou"
#, fuzzy
msgid "" #: person.rb:13
msgstr "" msgid "Name: %{name}, Age: %{age}"
"Project-Id-Version: myapp 1.0.0\n" (ID и версия проекта) msgstr "Namae: %{name}, Nenrei: %{age}"
#...
Что такое маркер fuzzy? Так отмечается тот факт, что какая-то часть не пере- #: person.rb:14
msgid "%{name} has a child."
ведена или перевод вызывает сомнения. Все автоматически сгенерированные со-
msgid_plural "%{name} has %{num} children."
общения помечаются таким образом, чтобы человек знал, что их нужно проверить
msgstr[0] "%{name} ha hitori kodomo ga imasu."
и изменить. msgstr[1] "%{name} ha %{num} nin no kodomo ga imasu."
Файл myapp.pot нужно разослать переводчикам. (Конечно, вы можете перевес-
Тегом msgid помечается исходное сообщение, а тегом msgstr – переведен-
ти его и самостоятельно.)
ное. При наличии строки msgid_plural необходимо включить отдельные строки
Предположим, что вы переводите на японский язык. На машине установле-
msgstr[i] в соответствии с правилом Plural-Forms. Индекс i вычисляется на осно-
на локаль ja_JP.UTF-8, что означает «Япония (ja), японский язык (JP), кодиров-
ве выражения Plural-Forms. В данном случае при num != 1 используется msgstr[1]
ка UTF-8».
(сообщение с существительным во множественном числе).
Для начала скопируем файл myapp.pot в myapp.po. При наличии набора GNU-
утилит gettext лучше воспользоваться командой msginit, а не просто cp. Эта ути- Истоки синтаксиса правила Plural-Forms следует искать в языке C. Как ви-
лита учитывает переменные окружения и правильно устанавливает некоторые пе- дим, он опирается на тот факт, что булевские выражения в C возвращают 0 или 1.
ременные в заголовке. В UNIX она вызывается следующим образом: Имейте в виду, что формы единственного и множественного числа в боль-
шой степени зависят от языка. Во многих языках есть несколько форм мно-
LANG=ja_JP.UTF-8 msginit -i myapp.pot -o myapp.po
жественного числа. Например, в польском слово «файл» в единственном чис-
Затем отредактируйте файл myapp.po, как показано в листинге 4.3. Редактиро- ле записывается как «plik». Если количество экземпляров заканчивается на 2,
вать необходимо в той кодировке, которая указана в строке Content-Type. 3 и 4, то во множественном числе пишется «pliki», а во всех остальных случа-
ях – «plików».
Листинг 4.3. Файл myapp.po после редактирования Поэтому для польского языка правило Plural-Forms выглядит так:
# Пример приложения. Plural-Forms: nplurals=3; \
# Copyright (C) 2006 Foo Bar plural=n==1 ? 0 : \
# Файл распространяется по лицензии XXX. n%10>=2 && n%10<=4 && (n%100=20) ? 1 : 2;
#
# Ваше имя <yourname@foo.com>, 2006. (Вся информация о переводчике) Заголовок файла – не пустая формальность. Особенно важны разделы Content-
# (Удалите строку 'fuzzy') Type и Plural-Forms. При пользовании утилитой msginit они вставляются автома-
msgid "" тически, в противном случае необходимо добавить их вручную.
msgstr "" Закончив работу, переводчик посылает файлы обратно разработчику (или вы
"Project-Id-Version: myapp 1.0.0\n" сами возвращаетесь к роли разработчика).
"POT-Creation-Date: 2006-05-22 23:27+0900\n" Файлы myapp.po, полученные от переводчиков, помещаются в соответствую-
"PO-Revision-Date: 2006-05-23 14:39+0900\n" щие каталоги (внутри каталога myapp/po). Например, французскую версию следо-
# Информация о текущем переводчике.
вало бы поместить в каталог myapp/po/fr/myapp.po, немецкую – в каталог myapp/
"Last-Translator: Your Name <foo@bar.com>\n"
"Language-Team: Japanese\n" (Ваш язык) po/de/myapp.po и т. д.
"MIME-Version: 1.0\n" Затем выполните команду rake makemo. Она преобразует PO-файлы в MO-фай-
"Content-Type: text/plain; charset=UTF-8\n" (Кодировка файла) лы. Сгенерированные MO-файлы будут помещены в каталог myapp/data/locale/
"Content-Transfer-Encoding: 8bit\n" (в котором есть подкаталоги для каждого языка).
156 Интернационализация в Ruby Заключение 157

Таким образом, окончательная структура дерева каталогов выглядит так:


myapp/
4.4. Заключение
В этой главе мы рассмотрели один из самых сложных аспектов программирова-
Rakefile
ния – проблему интернационализации кода. При этом нам понадобился материал
person.rb
po/
из двух предыдущих глав, так как интернационализация тесно связана со строка-
myapp.pot ми и регулярными выражениями.
de/myapp.po Мы видели, что в Ruby некоторые задачи решаются просто благодаря нали-
fr/myapp.po чию библиотеки jcode и сопутствующих инструментов. Заодно мы познакоми-
ja/myapp.po лись с наборами символов вообще и с набором Unicode в частности.
: Мы узнали, что регулярные выражения в общем случае лучше поддержива-
data/ ют Unicode, чем средства работы со строками, а также рассмотрели методы pack и
locale/ unpack с точки зрения полезности для манипулирования Unicode-строками.
de/LC_MESSAGES/myapp.mo Наконец, мы довольно подробно остановились на справочниках сообщений.
fr/LC_MESSAGES/myapp.mo Мы поняли, для чего они нужны, как их создавать и поддерживать.
ja/LC_MESSAGES/myapp.mo Детально рассмотрев вопрос о строках и регулярных выражениях, вернемся на
:
главную дорогу. Глава 5 посвящена численному анализу в языке Ruby.
Перевод закончен, можно протестировать пример. Но предварительно cледу-
ет указать, где искать MO-файлы и для какой локали проводится тестирование.
Установим переменные окружения GETTEXT_PATH и LANG, запустим программу и по-
смотрим, что она выведет.
export GETTEXT_PATH="data/locale"
export LANG="ja_JP.UTF-8"
ruby person.rb
Программа выводит локализованные сообщения в соответствии со значени-
ем переменной LANG.

4.3.4. Прочие замечания


Если вы распространяете вместе со своей программой справочники сообщений, то
лучше собрать пакет с помощью системы RubyGems или библиотеки setup.rb. До-
полнительную информацию по этому поводу вы найдете в разделе 17.2.
При установке пакета, собранного RubyGems, справочники сообщений копи-
руются в каталоги вида:
(gem-packages-installed-dir)/myapp-x.x.x/data/locale/
Такие каталоги уже включены в путь поиска для библиотеки gettext, поэтому
ваша программа будет локализована даже без явной установки переменной окру-
жения GETTEXT_PATH.
В случае сборки пакета с помощью библиотеки setup.rb справочники сообще-
ний помещаются в каталог (system-dir)/share/locale/. И в этом случае локали-
зация достигается без установки переменной GETTEXT_PATH.
Напомним, что описанная библиотека не является оберткой набора утилит
gettext от GNU. Однако файлы сообщений совместимы, поэтому при желании вы
можете пользоваться средствами сопровождения GNU. Понятно, что во время вы-
полнения программы все эти инструменты не нужны, то есть пользователь не обя-
зан их устанавливать на свой компьютер.
Основные операции над числами 159

Целые числа можно представлять и в других системах счисления (по основа-


нию 2, 8 и 16). Для этого в начале ставятся префиксы 0b, 0 и 0x соответственно.
0b10010110 # Двоичное.
0b1211 # Ошибка!
01234 # Восьмеричное (основание 8).
Глава 5. Численные методы 01823
0xdeadbeef
#
#
Ошибка!
Шестнадцатеричное (основание 16).
0xDEADBEEF # То же самое.
0xdeadpork # Ошибка!
Дважды [члены Парламента] задавали мне вопрос: «А скажите,
мистер Бэббидж, если вы заложите в эту машину неверные числа, то В числах с плавающей точкой десятичная точка должна присутствовать, а по-
получите правильный результат?» Не могу даже представить себе, на- казатель степени, возможно со знаком, необязателен:
сколько извращенно должен мыслить человек, задающий такие вопросы. 3.14 # Число пи, округленное до сотых.
Чарльз Бэббидж -0.628 # -2*pi, поделенное на 10, округленное до тысячных.
6.02e23 # Число Авогадро.
6.626068e-34 # Постоянная Планка.
Числа – самый первичный тип данных, естественный для любого компьютера.
В классе Float есть константы, определяющие минимальные и максимальные
Придется сильно постараться, чтобы найти такую область знания, в которой нет
значения чисел с плавающей точкой. Они машиннозависимы. Вот некоторые на-
места числам. Будь вы бухгалтером или конструктором воздухоплавательных ап-
иболее важные:
паратов, без чисел вам не обойтись. В этой главе мы обсудим различные способы
Float::MIN # 2.2250738585072e-308 (на конкретной машине)
обработки, преобразования и анализа числовых данных.
Float::MAX # 1.79769313486232e+308
Как и всякий современный язык, Ruby прекрасно умеет работать с любыми
Float::EPSILON # 2.22044604925031e-16
числами – как целыми, так и с плавающей точкой. В нем есть полный набор ожи-
даемых математических операторов и функций, а вместе с тем и кое-какие прият-
ные сюрпризы: классы Bignum, BigDecimal и Rational. 5.2. Основные операции над числами
Помимо средств для манипуляции числами, имеющихся в системной и стан- Обычные операции сложения, вычитания, умножения и деления в Ruby, как и во
дартной библиотеках, мы рассмотрим более специфические темы (тригонометрия, всех распространенных языках программирования, обозначаются операторами +,
математический анализ и статистика). Примеры приведены не только для справ- -, *, /. Операторы в большинстве своем реализованы в виде методов (и потому мо-
ки, но и как образцы кода на языке Ruby, иллюстрирующие принципы, изложен- гут быть переопределены).
ные в других частях книги. Возведение в степень обозначается оператором **, как в языках BASIC и FOR-
TRAN. Эта операция подчиняется обычным математическим правилам.
5.1. Представление чисел в языке Ruby a = 64**2 # 4096
b = 64**0.5 # 8.0
Если вы знакомы с любым другим языком программирования, то представление c = 64**0 # 1
чисел в Ruby не вызовет у вас никакого удивления. Объект класса Fixnum может d = 64**-1 # 0.015625
представлять число со знаком или без знака:
При делении одного целого числа на другое дробная часть результата отбрасы-
237 # Число без знака (положительное).
вается. Это не ошибка, так и задумано. Если вы хотите получить результат с пла-
+237 # То же, что и выше.
-237 # Отрицательное число.
вающей точкой, позаботьтесь о том, чтобы хотя бы один из операндов был числом
с плавающей точкой.
Если число длинное, то между любыми цифрами можно вставлять знак под-
3 / 3 # 3
черкивания. Это сделано исключительно для удобства, на значении константы ни-
5 / 3 # 1
как не сказывается. Обычно подчерки вставляются в те же места, где бухгалтеры 3 / 4 # 0
вставляют пробелы: 3.0 / 4 # 0.75
1048576 # Число в обычной записи. 3 / 4.0 # 0.75
1_048_576 # То же самое значение. 3.0 / 4.0 # 0.75
160 Численные методы Округление чисел с плавающей точкой 161

Если вы работаете с переменными и сомневаетесь относительно их типа, вос- fraction = self - whole
пользуйтесь приведением типа к Float или методом to_f: if fraction == 0.5
z = x.to_f / y if (whole % 2) == 0
z = Float(x) / y whole
else
См. также раздел 5.17 «Поразрядные операции над числами». whole+1
end
5.3. Округление чисел с плавающей точкой else
self.round
end
Кирк: Какие, вы говорите, у нас шансы выбраться отсюда? end
Спок: Трудно сказать точно, капитан. Приблизительно 7824.7 к одному.
Стар Трек, «Миссия милосердия» end

a = (33.4).round2 # 33
Метод round округляет число с плавающей точкой до целого: b = (33.5).round2 # 34
pi = 3.14159 c = (33.6).round2 # 34
new_pi = pi.round # 3 d = (34.4).round2 # 34
temp = -47.6 e = (34.5).round2 # 34
temp2 = temp.round # -48 f = (34.6).round2 # 35
Иногда бывает нужно округлить не до целого, а до заданного числа знаков пос- Видно, что round2 отличается от round только в том случае, когда дробная часть
ле запятой. В таком случае можно воспользоваться функциями sprintf (которая
в точности равна 0.5. Отметим, кстати, что число 0.5 можно точно представить в
умеет округлять) и eval:
двоичном виде. Не так очевидно, что этот метод правильно работает и для отрица-
pi = 3.1415926535
тельных чисел (попробуйте!). Отметим еще, что скобки в данном случае необяза-
pi6 = eval(sprintf("%8.6f",pi)) # 3.141593
pi5 = eval(sprintf("%8.5f",pi)) # 3.14159 тельны и включены в запись только для удобства восприятия.
pi4 = eval(sprintf("%8.4f",pi)) # 3.1416 Ну а если мы хотим округлять до заданного числа знаков после запятой, но
Это не слишком красиво. Поэтому инкапсулируем оба вызова функций в ме- при этом использовать метод «округления до четного»? Тогда нужно добавить в
тод, который добавим в класс Float: класс Float также метод roundf2:
class Float class Float

def roundf(places) # Определение round2 такое же, как и выше.


temp = self.to_s.length def roundf2(places)
sprintf("%#{temp}.#{places}f",self).to_f shift = 10**places
end (self * shift).round2 / shift.to_f
end
end
Иногда требуется округлять до целого по-другому. Традиционное округление end
n+0.5 с избытком со временем приводит к небольшим ошибкам; ведь n+0.5 все-та-
a = 6.125
ки ближе к n+1, чем к n. Есть другое соглашение: округлять до ближайшего четно-
b = 6.135
го числа, если дробная часть равна 0.5. Для реализации такого правила можно бы-
x = a.roundf2(a) # 6.12
ло бы расширить класс Float, добавив в него метод round2:
y = b.roundf2(b) # 6.13
class Float
У методов roundf и roundf2 есть ограничение: большое число с плавающей точ-
def round2 кой может стать непредставимым при умножении на большую степень 10. На этот
whole = self.floor случай следовало бы предусмотреть проверку ошибок.
162 Численные методы Вставка разделителей при форматировании чисел 163

5.4. Сравнение чисел с плавающей точкой def equals?(x, tolerance=EPSILON)


(self-x).abs < tolerance
Печально, но факт: в компьютере числа с плавающей точкой представляются не- end
точно. В идеальном мире следующий код напечатал бы «да», но на всех машинах,
где мы его запускали, печатается «нет»: end
x = 1000001.0/0.003
y = 0.003*x flag1 = (3.1416).equals? Math::PI # false
if y == 1000001.0 flag2 = (3.1416).equals?(Math::PI, 0.001) # true
puts "да"
else Можно также ввести совершенно новый оператор для приближенного сравне-
puts "нет" ния, назвав его, например, =~.
end Имейте в виду, что это нельзя назвать настоящим решением. При последо-
Объясняется это тем, что для хранения числа с плавающей точкой выделено вательных вычислениях погрешность накапливается. Если вам совершенно не-
конечное число битов, а с помощью любого, сколь угодно большого, но конечного обходимы числа с плавающей точкой, смиритесь с неточностями (см. также раз-
числа битов нельзя представить периодическую десятичную дробь с бесконечным делы 5.8 и 5.9).
числом знаков после запятой.
Из-за этой неустранимой неточности при сравнении чисел с плавающей точкой 5.5. Форматирование чисел для вывода
мы можем оказаться в ситуации (продемонстрированной выше), когда с практичес-
Для вывода числа в заданном формате применяется метод printf из модуля Kernel.
кой точки зрения два числа равны, но аппаратура упрямо считает их различными.
Он практически не отличается от одноименной функции в стандартной библиоте-
Ниже показан простой способ выполнения сравнения с «поправкой», когда чис-
ке C. Дополнительную информацию см. в документации по методу printf.
ла считаются равными, если отличаются не более чем на величину, задаваемую про-
граммистом: x = 345.6789
i = 123
class Float
printf("x = %6.2f\n", x) # x = 345.68
printf("x = %9.2e\n", x) # x = 3.457e+02
EPSILON = 1e-6 # 0.000001
printf("i = %5d\n", i) # i = 123
printf("i = %05d\n", i) # i = 00123
def ==(x)
printf("i = %-5d\n", i) # i = 123
(self-x).abs < EPSILON
end Чтобы сохранить результат в строке, а не печатать его немедленно, восполь-
зуйтесь методом sprintf. При следующем обращении возвращается строка:
end str = sprintf("%5.1f",x) # "345.7"
Наконец, в классе String есть метод %, решающий ту же задачу. Слева от знака
x = 1000001.0/0.003 % должна стоять форматная строка, а справа – единственный аргумент (или мас-
y = 0.003*x сив значений), результатом является строка.
if y == 1.0 # Пользуемся новым оператором ==. # Порядок вызова: 'формат % значение'
puts "да" # Теперь печатается "да". str = "%5.1f" % x # "345.7"
else
str = "%6.2f, %05d" % [x,i] # "345.68, 00123"
puts "нет"
end
В зависимости от ситуации может понадобиться задавать разные погрешнос- 5.6. Вставка разделителей при форматировании чисел
ти. Для этого определим в классе Float новый метод equals?. (При таком выборе Возможно, есть и более удачные способы достичь цели, но приведенный ниже код
имени мы избежим конфликта со стандартными методами equal? и eql?; послед- работает. Мы инвертируем строку, чтобы было удобнее выполнять глобальную за-
ний, кстати, вообще не следует переопределять). мену, а в конце инвертируем ее еще раз:
class Float def commas(x)
str = x.to_s.reverse
EPSILON = 1e-6 str.gsub!(/([0-9]{3})/,"\\1,")
164 Численные методы Использование класса BigDecimal 165

str.gsub(/,$/,"").reverse В подобной ситуации на помощь приходит класс BigDecimal. Однако в случае


end бесконечных периодических дробей проблема остается. Другой подход обсужда-
ется в разделе 5.9 «Работа с рациональными числами».
puts commas(123) # "123"
Объект BigDecimal инициализируется строкой. (Объекта типа Float было бы
puts commas(1234) # "1,234"
puts commas(12345) # "12,435" недостаточно, поскольку погрешность вкралась бы еще до начала конструирова-
puts commas(123456) # "123,456" ния BigDecimal.) Метод BigDecimal эквивалентен BigDecimal.new; это еще один
puts commas(1234567) # "1,234,567" особый случай, когда имя метода начинается с прописной буквы. Поддерживают-
ся обычные математические операции, например + и *. Отметим, что метод to_s
может принимать в качестве параметра форматную строку. Дополнительную ин-
5.7. Работа с очень большими числами формацию вы найдете на сайте ruby-doc.org.
require 'bigdecimal'
Управлять массами все равно что управлять немногими:
дело в частях и в числе. x = BigDecimal("3.2")
Сунь-Цзы* y = BigDecimal("2.0")
z = BigDecimal("1.2")

При необходимости Ruby позволяет работать с произвольно большими целыми if (x - y) == z


числами. Переход от Fixnum к Bignum производится автоматически, прозрачно для puts "равны" # Печатается "равны"!
программиста. В следующем разделе результат оказывается настолько большим, else
что преобразуется из объекта Fixnum в Bignum: puts "не равны"
num1 = 1000000 # Один миллион (10**6) end
num2 = num1*num1 # Один триллион (10**12)
puts num1 # 1000000 a = x*y*z
puts num1.class # Fixnum a.to_s # "0.768E1" (по умолчанию: научная нотация)
puts num2 # 1000000000000 a.to_s("F") # "7.68" (обычная запись)
puts num2.class # Bignum Если необходимо, можно задать число значащих цифр. Метод precs возвраща-
Размер Fixnum зависит от машинной архитектуры. Вычисления с объектами ет эту информацию в виде массива, содержащего два числа: количество использо-
Bignum ограничены только объемом памяти и быстродействием процессора. Ко- ванных байтов и максимальное число значащих цифр.
нечно, они потребляют больше памяти и выполняются несколько медленнее, тем x = BigDecimal("1.234",10)
не менее операции над очень большими целыми (сотни знаков) реальны. y = BigDecimal("1.234",15)
x.precs # [8, 16]
y.precs # [8, 20]
5.8. Использование класса BigDecimal
В каждый момент число использованных байтов может оказаться меньше мак-
Стандартная библиотека bigdecimal позволяет работать с дробями, имеющими
симального. Максимум может также оказаться больше запрошенного вами (по-
много значащих цифр. Число хранится как массив цифр, а не преобразуется в дво-
скольку BigDecimal пытается оптимизировать использование внутренней памяти).
ичное представление. Тем самым достижима произвольная точность, естественно,
У обычных операций (сложение, вычитание, умножение и деление) есть вари-
ценой замедления работы.
анты, принимающие в качестве дополнительного параметра число значащих цифр.
Чтобы оценить преимущества, рассмотрим следующий простой фрагмент ко-
Если результат содержит больше значащих цифр, чем указано, производится округ-
да, в котором используются числа с плавающей точкой:
ление до заданного числа знаков.
if (3.2 - 2.0) == 1.2
a = BigDecimal("1.23456")
puts "равны"
b = BigDecimal("2.45678")
else
puts "не равны" # Печатается "не равны"!
end # В комментариях "BigDecimal:objectid" опущено.
c = a+b # <'0.369134E1',12(20)>
* Трактат «Искусство войны». c2 = a.add(b,4) # <'0.3691E1',8(20)>
166 Численные методы Перемножение матриц 167

d = a-b # <'-0.122222E1',12(20)> Вернемся к примеру, на котором мы демонстрировали неточность операций


d2 = a.sub(b,4) # <'-0.1222E1',8(20)> над числами с плавающей точкой (см. раздел 5.4). Ниже мы выполняем те же дей-
ствия над рациональными, а не вещественными числами и получаем «математи-
e = a*b # <'0.3033042316 8E1',16(36)>
чески ожидаемый» результат:
e2 = a.mult(b,4) # <'0.3033E1',8(36)>
x = Rational(1000001,1)/Rational(3,1000)
f = a/b # <'0.5025114173 8372992290 7221E0',24(32)> y = Rational(3,1000)*x
f2 = a.div(b,4) # <'0.5025E0',4(16)> if y == 1000001.0
puts "да" # Теперь получаем "да"!
В классе BigDecimal определено и много других функций, например floor, abs
else
и т. д. Как и следовало ожидать, имеются операторы % и **, а также операторы срав- puts "нет"
нения, к примеру <. Оператор == не умеет округлять свои операнды – эта обязан- end
ность возлагается на программиста.
Конечно, не любая операция дает рациональное же число в качестве резуль-
В модуле BigMath определены константы E и PI с произвольной точностью. (На
тата:
самом деле это методы, а не константы.) Там же определены функции sin, cos, exp
и пр.; все они принимают число значащих цифр в качестве параметра. x = Rational(9,16) # Rational(9, 16)
Math.sqrt(x) # 0.75
Следующие подбиблиотеки являются дополнениями к BigDecimal.
x**0.5 # 0.75
bigdecimal/math Модуль BigMath x**Rational(1,2) # 0.75
bigdecimal/jacobian Методы для вычисления матрицы Якоби Однако библиотека mathn в какой-то мере изменяет это поведение (см. раздел
bigdecimal/ludcmp Модуль LUSolve, разложение матрицы в произве-
5.12).
дение верхнетреугольной и нижнетреугольной
bigdecimal/newton Методы nlsolve и norm
В настоящей главе эти подбиблиотеки не описываются. Для получения допол- 5.10. Перемножение матриц
нительной информации обратитесь к сайту ruby-doc.org или любому подробному Стандартная библиотека matrix предназначена для выполнения операций над чис-
справочному руководству. ловыми матрицами. В ней определено два класса: Matrix и Vector.
Следует также знать о прекрасной библиотеке NArray, которую написал Маса-
хиро Танака (Masahiro Tanaka) – ее можно найти на сайте www.rubyforge.org. Хо-
5.9. Работа с рациональными числами тя эта библиотека не относится к числу стандартных, она широко известна и очень
Класс Rational позволяет (во многих случаях) производить операции с дробями полезна. Если вы предъявляете повышенные требования к быстродействию, нуж-
с «бесконечной» точностью, но лишь если это настоящие рациональные числа (то даетесь в особом представлении данных или желаете выполнять быстрое преобра-
есть частное от деления двух целых чисел). К иррациональным числам, например зование Фурье, обязательно ознакомьтесь с этим пакетом. Впрочем, для типичных
π или e, он неприменим. применений стандартной библиотеки matrix должно хватить, поэтому именно ее
Для создания рационального числа мы вызываем специальный метод Rational мы и рассмотрим.
(еще один из немногих методов, имя которого начинается с прописной буквы; обыч- Чтобы создать матрицу, мы, конечно же, обращаемся к методу класса. Сделать
но такие методы служат для преобразования данных или инициализации).
это можно несколькими способами. Самый простой – вызвать метод Matrix.[]
r = Rational(1,2) # 1/2 или 0.5 и перечислить строки в виде массивов. Ниже мы записали вызов на нескольких
s = Rational(1,3) # 1/3 или 0.3333...
строчках, но, разумеется, это необязательно:
t = Rational(1,7) # 1/7 или 0.14...
u = Rational(6,2) # "то же самое, что" 3.0 m = Matrix[[1,2,3],
z = Rational(1,0) # Ошибка! [4,5,6],
[7,8,9]]
Результатом операции над двумя рациональными числами, как правило, сно-
ва является рациональное число. Вместо этого можно вызвать метод rows, передав ему массив массивов (в таком
r+t # Rational(9, 14)
случае «дополнительные» скобки необходимы). Необязательный параметр copy,
r-t # Rational(5, 14) по умолчанию равный true, указывает, надо ли скопировать переданные массивы
r*s # Rational(1, 6) или просто сохранить на них ссылки. Оставляйте значение true, если нужно защи-
r/s # Rational(3, 2) тить исходные массивы от изменения, и задавайте false, если это несущественно.
168 Численные методы Перемножение матриц 169

row1 = [2,3] Индексация начинается с 0, как и для массивов в Ruby. Возможно, это про-
row2 = [4,5] тиворечит вашему опыту работы с матрицами, но индексация с 1 в качестве аль-
m1 = Matrix.rows([row1,row2]) # copy=true тернативы не предусмотрена. Можно реализовать эту возможность самостоя-
m2 = Matrix.rows([row1,row2],false) # Не копировать.
тельно:
row1[1] = 99 # Теперь изменим row1.
p m1 # Matrix[[2, 3], [4, 5]] # Наивный подход... не поступайте так!
p m2 # Matrix[[2, 99], [4, 5]] class Matrix
alias bracket []
Можно задать матрицу и путем перечисления столбцов, если воспользовать-
ся методом columns. Ему параметр copy не передается, потому что столбцы в лю- def [](i,j)
бом случае расщепляются, так как во внутреннем представлении матрица хранит- bracket(i-1,j-1)
ся построчно: end
m1 = Matrix.rows([[1,2],[3,4]]) end
m2 = Matrix.columns([[1,3],[2,4]]) # m1 == m2
Предполагается, что все матрицы прямоугольные, но это не проверяется. Если m = Matrix[[1,2,3],[4,5,6],[7,8,9]]
p m[2,2] # 5
вы создадите матрицу, в которой отдельные строки или столбцы длиннее либо ко-
роче остальных, то можете получить неверные или неожиданные результаты. На первый взгляд, этот код должен работать. Большинство операций над мат-
Некоторые специальные матрицы, особенно квадратные, конструируются про- рицами даже будет давать правильный результат при такой индексации. Так в
ще. Так, тождественную матрицу конструирует метод identity (или его синонимы чем же проблема? В том, что мы не знаем деталей внутренней реализации клас-
I и unit): са Matrix. Если в нем для доступа к элементам матрицы всегда используется соб-
im1 = Matrix.identity(3) # Matrix[[1,0,0],[0,1,0],[0,0,1]] ственный метод [ ], то все будет хорошо. Но если где-нибудь имеются прямые
im2 = Matrix.I(3) # То же самое. обращения к внутреннему массиву или применяются иные оптимизированные ре-
im3 = Matrix.unit(3) # То же самое. шения, то возникнет ошибка. Поэтому, решившись на такой трюк, вы должны тща-
Более общий метод scalar строит диагональную матрицу, в которой все эле- тельно протестировать новое поведение.
менты на диагонали одинаковы, но не обязательно равны 1: К тому же необходимо изменить методы row и vector. В них индексы тоже на-
чинаются с 0, но метод [ ] не вызывается. Я не проверял, что еще придется моди-
sm = Matrix.scalar(3,8) # Matrix[[8,0,0],[0,8,0],[0,0,8]]
фицировать.
Еще более общим является метод diagonal, который формирует диагональ- Иногда необходимо узнать размерность или форму матрицы. Для этого есть
ную матрицу с произвольными элементами (ясно, что параметр, задающий раз- разные методы, например row_size и column_size.
мерность, в этом случае не нужен). Метод row_size возвращает число строк в матрице. Что касается метода column_
dm = Matrix.diagonal(2,3,7) # Matrix[[2,0,0],[0,3,0],[0,0,7]] size, тут есть одна тонкость: он проверяет лишь размер первой строки. Если по ка-
Метод zero создает нулевую матрицу заданной размерности (все элементы ким-либо причинам матрица не прямоугольная, то полученное значение бессмыс-
равны 0): ленно. Кроме того, поскольку метод square? (проверяющий, является ли матрица
zm = Matrix.zero(3) # Matrix[[0,0,0],[0,0,0],[0,0,0]] квадратной) обращается к row_size и column_size, его результат тоже нельзя счи-
тать стопроцентно надежным.
Понятно, что методы identity, scalar, diagonal и zero создают квадратные
матрицы. m1 = Matrix[[1,2,3],[4,5,6],[7,8,9]]
m2 = Matrix[[1,2,3],[4,5,6],[7,8]]
Чтобы создать матрицу размерности 1xN или Nx1, воспользуйтесь методом
m1.row_size # 3
row_vector или column_vector соответственно.
m1.column_size # 3
a = Matrix.row_vector(2,4,6,8) # Matrix[[2,4,6,8]] m2.row_size # 3
b = Matrix.column_vector(6,7,8,9) # Matrix[[6],[7],[8],[9]] m2.column_size # 3 (неправильно)
К отдельным элементам матрицы можно обращаться, указывая индексы в квад- m1.square? # true
ратных скобках (оба индекса заключаются в одну пару скобок). Отметим, что не су- m2.square? # true (неправильно)
ществует метода [ ]=. По той же причине, по которой его нет в классе Fixnum: матри- Решить эту мелкую проблему можно, например, определив метод rectangular?.
цы – неизменяемые объекты (такое решение было принято автором библиотеки). class Matrix
m = Matrix[[1,2,3],[4,5,6]] def rectangular?
puts m[1,2] # 6 arr = to_a
170 Численные методы Комплексные числа 171

first = arr[0].size v3 = v1 + v2 # Vector[6,8,10]


arr[1..-1].all? {|x| x.size == first } v4 = v1*v2.covector # Matrix[[8,10,12],[12,15,18],[16,20,24]]
end v5 = v1*5 # Vector[10,15,20]
end Имеется метод inner_product (скалярное произведение):
Можно, конечно, модифицировать метод square?, так чтобы сначала он прове- v1 = Vector[2,3,4]
рял, является ли матрица прямоугольной. В таком случае нужно будет изменить v2 = Vector[4,5,6]
метод column_size, чтобы он возвращал nil для непрямоугольной матрицы. x = v1.inner_product(v2) # 47
Для вырезания части матрицы имеется несколько методов. Метод row_vectors Дополнительную информацию о классах Matrix и Vector можно найти в лю-
возвращает массив объектов класса Vector, представляющих строки (см. обсужде- бом справочном руководстве, например воспользовавшись командной утилитой
ние класса Vector ниже.) Метод column_vectors работает аналогично, но для столб- ri, или на сайте ruby-doc.org.
цов. Наконец, метод minor возвращает матрицу меньшего размера; его параметрами
являются либо четыре числа (нижняя и верхняя границы номеров строк и столб-
цов), либо два диапазона. 5.11. Комплексные числа
m = Matrix[[1,2,3,4],[5,6,7,8],[6,7,8,9]] Стандартная библиотека complex предназначена для работы с комплексными чис-
лами в Ruby. Большая ее часть не требует пояснений.
rows = m.row_vectors # Три объекта Vector. Для создания комплексного числа применяется следующая несколько необыч-
cols = m.column_vectors # Четыре объекта Vector. ная нотация:
m2 = m.minor(1,2,1,2) # Matrix[[6,7,],[7,8]] z = Complex(3,5) # 3+5i
m3 = m.minor(0..1,1..3) # Matrix[[[2,3,4],[6,7,8]]
Необычно в ней то, что имя метода совпадает с именем класса. В данном случае
К матрицам применимы обычные операции: сложение, вычитание, умножение наличие скобок указывает на то, что это вызов метода, а не ссылка на константу.
и деление. Для выполнения некоторых из них должны соблюдаться ограничения Вообще говоря, имена методов не похожи на константы, и я не рекомендую начи-
на размеры матриц-операндов; в противном случае будет возбуждено исключение нать имена методов с прописной буквы, разве что в подобных специальных слу-
(например, при попытке перемножить матрицы размерностей 3x3 и 4x4). чаях. (Отметим, что имеются также методы Integer и Float; вообще, имена, начи-
Поддерживаются стандартные преобразования: inverse (обращение), transpose нающиеся с прописной буквы, зарезервированы для методов, которые выполняют
(транспонирование) и determinant (вычисление определителя). Для целочислен- преобразование данных и аналогичные действия.)
ных матриц определитель лучше вычислять с помощью библиотеки mathn (раз- Метод im преобразует вещественное число в мнимое (по существу, умножая
дел 5.12). его на i). Поэтому представлять комплексные числа можно и с помощью более
Класс Vector – это, по существу, частный случай одномерной матрицы. Его привычной нотации:
объект можно создать с помощью методов [ ] или elements; в первом случае пара-
a = 3.im # 3i
метром является развернутый массив, а во втором – обычный массив и необяза-
b = 5 - 2.im # 5-2i
тельный параметр copy (по умолчанию равный true).
Если вас больше интересуют полярные координаты, то можно обратиться к
arr = [2,3,4,5]
v1 = Vector[*arr] # Vector[2,3,4,5] методу polar:
v2 = Vector.elements(arr) # Vector[2,3,4,5] z = Complex.polar(5,Math::PI/2.0) # Радиус, угол.
v3 = Vector.elements(arr,false) # Vector[2,3,4,5] В классе Complex имеется также константа I, которая представляет число i –
arr[2] = 7 # Теперь v3 – Vector[2,3,7,5]. квадратный корень из минус единицы:
Метод covector преобразует вектор длины N в матрицу размерности Nx1 (вы- z1 = Complex(3,5)
полняя попутно транспонирование). z2 = 3 + 5*Complex::I # z2 == z1
v = Vector[2,3,4] После загрузки библиотеки complex некоторые стандартные математические
m = v.covector # Matrix[[2,3,4]] функции изменяют свое поведение. Тригонометрические функции – sin, sinh, tan
Поддерживается сложение и вычитание векторов одинаковой длины. Вектор и tanh (а также некоторые другие, например, exp и log) начинают принимать еще
можно умножать на матрицу и на скаляр. Все эти операции подчиняются обыч- и комплексные аргументы. Некоторые функции, например sqrt, даже возвращают
ным математическим правилам. комплексные числа в качестве результата.
v1 = Vector[2,3,4] x = Math.sqrt(Complex(3,5)) # Приближенно Complex(2.1013, 1.1897)
v2 = Vector[4,5,6] y = Math.sqrt(-1) # Complex(0,1)
172 Численные методы Простые числа 173

Дополнительную информацию ищите в любой полной документации, в част- factors = 126.prime_division # [[2,1], [3,2], [7,1]]
ности на сайте ruby-doc.org. # То есть 2**1 * 3**2 * 7**1
Имеется также метод класса Integer.from_prime_division, который восста-
5.12. Библиотека mathn навливает исходное число из его сомножителей. Это именно метод класса, потому
что выступает в роли «конструктора» целого числа.
В программах, выполняющих большой объем математических вычислений, очень
factors = [[2,1],[3,1],[7,1]]
пригодится замечательная библиотека mathn, которую написал Кейдзу Исидзука
num = Integer.from_prime_division(factors) # 42
(Keiju Ishitsuka). В ней есть целый ряд удобных методов и классов; кроме того, она
унифицирует все классы Ruby для работы с числами так, что они начинают хоро- Ниже показано, как разложение на простые множители можно использовать
шо работать совместно. для отыскания наименьшего общего кратного (НОК) двух чисел:
Простейший способ воспользоваться этой библиотекой – включить ее с по- require 'mathn'
мощью директивы require и забыть. Поскольку она сама включает библиотеки
complex, rational и matrix (в таком порядке), то вы можете этого не делать. class Integer
def lcm(other)
В общем случае библиотека mathn пытается вернуть «разумные» результа-
pf1 = self.prime_division.flatten
ты вычислений. Например, при извлечении квадратного корня из Rational будет pf2 = other.prime_division.flatten
возвращен новый объект Rational, если это возможно; в противном случае Float. h1 = Hash[*pf1]
В таблице 5.1 приведены некоторые последствия загрузки этой библиотеки. h2 = Hash[*pf2]
Таблица 5.1. Результаты вычислений в случае отсутствия hash = h2.merge(h1) {|key,old,new| [old,new].max }
и наличия библиотеки mathn Integer.from_prime_division(hash.to_a)
end
Выражение Без mathn С mathn end
Math.sqrt(Rational(9,16)) 0.75 Rational(3,4)
1/2 0 Rational(1,2) p 15.lcm(150) # 150
p 2.lcm(3) # 6
Matrix.identity(3)/3 Matrix[[0,0,0], Matrix[[1/3,0,0],
p 4.lcm(12) # 12
[0,0,0],[0,0,0]] [0,1/3,0],[0,0,1/3]]
p 200.lcm(30) # 600
Math.sqrt(64/25) 1.4142... Rational(8,5)
Rational(1,10).inspect Rational(1,10) 1/10
5.14. Простые числа
В библиотеке mathn есть класс для порождения простых чисел. Итератор each воз-
Библиотека mathn добавляет методы ** и power2 в класс Rational. Она изменя-
вращает последовательные простые числа в бесконечном цикле. Метод succ по-
ет поведение метода Math.sqrt и добавляет метод Math.rsqrt, умеющий работать с
рождает следующее простое число. Вот, например, два способа получить первые
рациональными числами.
Дополнительная информация приводится в разделах 5.13 и 5.14. 100 простых чисел:
require 'mathn'

5.13. Разложение на простые множители, list = []


вычисление НОД и НОК gen = Prime.new
gen.each do |prime|
В библиотеке mathn определены также некоторые новые методы в классе Integer. list << prime
Так, метод gcd2 служит для нахождения наибольшего общего делителя (НОД) break if list.size == 100
объекта, от имени которого он вызван, и другого числа. end
n = 36.gcd2(120) # 12
k = 237.gcd2(79) # 79 # или:

Метод prime_division выполняет разложение на простые множители. Резуль- list = []


тат возвращается в виде массива массивов, в котором каждый вложенный массив со- gen = Prime.new
держит простое число и показатель степени, с которым оно входит в произведение: 100.times { list << gen.succ }
174 Численные методы Приведение числовых значений 175

В следующем фрагменте проверяется, является ли данное число простым. От- Желая явно преобразовать объект класса MyClass в целое число, мы вызовем
метим, что если число велико, а машина медленная, то на выполнение может уй- метод to_i:
ти заметное время: m = MyClass.new
require 'mathn' x = m.to_i # 3
Но при передаче объекта MyClass какой-нибудь функции, ожидающей целое
class Integer число, будет неявно вызван метод to_int. Предположим, к примеру, что мы хотим
def prime? создать массив с известным начальным числом элементов. Метод Array.new может
max = Math.sqrt(self).ceil
принять целое, но что если вместо этого ему будет передан объект MyClass?
max -= 1 if max % 2 == 0
pgen = Prime.new m = MyClass.new
pgen.each do |factor| a = Array.new(m) # [nil,nil,nil,nil,nil]
return false if self % factor == 0 Как видите, метод new оказался достаточно «умным», чтобы вызвать to_int и
return true if factor > max затем создать массив из пяти элементов.
end Дополнительную информацию о поведении в другом контексте (строковом)
end вы найдете в разделе 2.16. См. также раздел 5.16.
end

31.prime? # true 5.16. Приведение числовых значений


237.prime? # false Приведение можно считать еще одним видом неявного преобразования. Если не-
1500450271.prime? # true которому методу (например, +) передается аргумент, которого он не понимает, он
пытается привести объект, от имени которого вызван, и аргумент к совместимым
5.15. Явные и неявные преобразования чисел типам, а затем сложить их. Принцип использования метода coerce в вашем соб-
Программисты, только начинающие изучать Ruby, часто удивляются, зачем нуж- ственном классе понятен из следующего примера:
ны два метода to_i и to_int (и аналогичные им to_f и to_flt). В общем случае ме- class MyNumberSystem
тод с коротким именем применяется для явных преобразований, а метод с длин-
def +(other)
ным именем – для неявных. if other.kind_of?(MyNumberSystem)
Что это означает? Во-первых, в большинстве классов определены явные кон- result = some_calculation_between_self_and_other
верторы, но нет неявных. Насколько мне известно, методы to_int и to_flt не опре- MyNumberSystem.new(result)
делены ни в одном из системных классов. else
Во-вторых, в своих собственных классах вы, скорее всего, будете определять n1, n2 = other.coerce(self)
неявные конверторы, но не станете вызывать их вручную (если только не заняты n1 + n2
end
написанием «клиентского» кода или библиотеки, которая пытается не конфлик-
end
товать с внешним миром).
Следующий пример, конечно, надуманный. В нем определен класс MyClass, ко- end
торый возвращает константы из методов to_i и to_int. Такое поведение лишено Метод coerce возвращает массив из двух элементов, содержащий аргумент и
смысла, зато иллюстрирует идею: вызывающий объект, приведенные к совместимым типам.
class MyClass В данном примере мы полагаемся на то, что приведение выполнит тип аргу-
мента. Но если мы хотим быть законопослушными гражданами, то должны реа-
def to_i лизовать приведение в своем классе, сделав его пригодным для работы с другими
3
типами чисел. Для этого нужно знать, с какими типами мы в состоянии работать
end
def to_int
непосредственно, и при необходимости выполнять приведение к одному из этих
5 типов. Если мы не можем сделать это самостоятельно, то должны обратиться за
end помощью к родительскому классу.
def coerce(other)
end if other.kind_of?(Float)
176 Численные методы Преобразование системы счисления 177

return other, self.to_f Имеются операторы сдвига влево и вправо (<< и >> соответственно). Это логи-
elsif other.kind_of?(Integer) ческие операторы сдвига, они не затрагивают знаковый бит (хотя оператор >> рас-
return other, self.to_i пространяет его).
else
super x = 8
end y = -8
end
a = x >> 2 # 2
Разумеется, это будет работать только, если наш объект реализует методы to_ b = y >> 2 # -2
i и to_f. c = x << 2 # 32
Метод coerce можно применить для реализации автоматического преобразо- d = y << 2 # -32
вания строк в числа, как в языке Perl: Конечно, если сдвиг настолько велик, что дает нулевое значение, то знаковый
class String бит теряется, поскольку -0 и 0 – одно и то же.
Квадратные скобки позволяют трактовать числа как битовые массивы. Бит с
def coerce(n)
номером 0 всегда является младшим, вне зависимости от порядка битов в конкрет-
if self['.']
[n, Float(self)] ной машинной архитектуре.
else x = 5 # То же, что 0b0101
[n, Integer(self)] a = x[0] # 1
end b = x[1] # 0
end c = x[2] # 1
d = x[3] # 0
end # И так далее # 0
Присваивать новые значения отдельным битам с помощью такой нотации не-
x = 1 + "23" # 24
возможно (поскольку Fixnum хранится как непосредственное значение, а не как
y = 23 * "1.23" # 28.29
ссылка на объект). Но можно имитировать это действие путем сдвига 1 влево на
Мы не настаиваем на таком решении. Но рекомендуем реализовывать coerce нужное число позиций с последующим выполнением операции ИЛИ или И.
при создании любого класса для работы с числовыми данными. # Выполнить присваивание x[3] = 1 нельзя,
# но можно поступить так:
5.17. Поразрядные операции над числами x
#
|= (1<<3)
Выполнить присваивание x[4] = 0 нельзя,
Иногда требуется работать с двоичным представлением объекта Fixnum. На при- # но можно поступить так:
кладном уровне такая необходимость возникает нечасто, но все-таки возникает. x &= ~(1<<4)
Ruby обладает всеми средствами для таких операций. Для удобства числовые
константы можно записывать в двоичном, восьмеричном или шестнадцатеричном
виде. Поразрядным операциям И, ИЛИ, ИСКЛЮЧАЮЩЕЕ ИЛИ и НЕ соответ-
5.18. Преобразование системы счисления
ствуют операторы &, |, ^ и ~. Ясно, что любое целое число можно представить в любой системе счисления, по-
x = 0377 # Восьмеричное (десятичное 255)
скольку хранятся эти числа в двоичном виде. Мы знаем, что Ruby умеет работать
y = 0b00100110 # Двоичное (десятичное 38) с целыми константами, записанными в любой из четырех наиболее популярных
z = 0xBEEF # Шестнадцатеричное (десятичное 48879) систем. Следовательно, разговор о преобразовании системы счисления может вес-
тись только применительно к числам, записанным в виде строк.
a = x | z # 48895 (поразрядное ИЛИ) Вопрос о преобразовании строки в целое рассмотрен в разделе 2.24.
b = x & z # 239 (поразрядное И) Для преобразования числа в строку проще всего воспользоваться методом
c = x ^ z # 48656 (поразрядное ИСКЛЮЧАЮЩЕЕ ИЛИ) to_s, которому можно еще передать основание системы счисления. По умолча-
d = ~ y # -39 (отрицание или дополнение до 1) нию оно равно 10, но в принципе может быть любым вплоть до 36 (когда задей-
Метод экземпляра size позволяет узнать размер слова для той машины, на ко- ствованы все буквы латинского алфавита).
торой исполняется программа. 237.to_s(2) # "11101101"
size # Для конкретной машины возвращает 4. 237.to_s(5) # "1422"
178 Численные методы Численное вычисление определенного интеграла 179

237.to_s(8) # "355" Хотите верьте, хотите нет, но решение не произвольно. Существуют убе-
237.to_s # "237" дительные аргументы в пользу обеих точек зрения (обсуждать их здесь мы не
237.to_s(16) # "ed" будем).
237.to_s(30) # "7r"
Вот уже больше двадцати лет, как для описания противоположных позиций
Другой способ – обратиться к методу % класса String: применяются термины «остроконечный» (little-endian) и «тупоконечный» (big-
hex = "%x" % 1234 # "4d2" endian). Кажется, впервые их употребил Дэнни Коэн (Danny Cohen); см. его клас-
oct = "%o" % 1234 # "2322" сическую статью “On Holy Wars and a Plea for Peace” (IEEE Computer, October
bin = "%b" % 1234 # "10011010010" 1981). Взяты они из романа Джонатана Свифта «Путешествия Гулливера».
Метод sprintf тоже годится: Обычно нам безразличен порядок байтов в конкретной машинной архитекту-
str = sprintf(str,"Nietzsche is %x\n",57005) ре. Но как быть, если все-таки его нужно знать?
# str теперь равно: "Nietzsche is dead\n" Можно воспользоваться показанным ниже методом. Он возвращает одну из
Если нужно сразу же вывести преобразованное в строку значение, то подой- строк LITTLE, BIG или OTHER. Решение основано на том факте, что директива l вы-
дет и метод printf. полняет упаковку в машинном формате, а директива N распаковывает в сетевом
порядке байтов (по определению тупоконечном).
5.19. Извлечение кубических корней, def endianness
num=0x12345678
корней четвертой степени и т. д. little = "78563412"
big = "12345678"
В Ruby встроена функция извлечения квадратного корня (Math.sqrt), поскольку native = [num].pack('l')
именно она применяется чаще всего. А если надо извлечь корень более высокой сте- netunpack = native.unpack('N')[0]
пени? Если вы еще не забыли математику, то эта задача не вызовет затруднений. str = "%8x" % netunpack
Можно, например, воспользоваться логарифмами. Напомним, что e в степе- case str
ни x – обратная функция к натуральному логарифму x и что умножение чисел when little
эквивалентно сложению их логарифмов. "LITTLE"
when big
x = 531441
"BIG"
cuberoot = Math.exp(Math.log(x)/3.0) # 81.0
else
fourthroot = Math.exp(Math.log(x)/4.0) # 27.0
"OTHER"
Но можно просто использовать дробные показатели степени (оператор возве- end
дения в степень принимает в качестве аргумента произвольное целое число или end
число с плавающей точкой).
include Math puts endianness # В данном случае печатается "LITTLE"
y = 4096 Этот прием может оказаться удобным, если, например, вы работаете с двоич-
cuberoot = y**(1.0/3.0) # 16.0 ными данными (скажем, отсканированным изображением), импортированными
fourthroot = y**(1.0/4.0) # 8.0 из другой системы.
fourthroot = sqrt(sqrt(y)) # 8.0 (то же самое)
twelfthroot = y**(1.0/12.0) # 2.0
Отметим, что во всех примерах мы пользовались при делении числами с пла-
5.21. Численное вычисление определенного интеграла
вающей точкой (чтобы избежать отбрасывания дробной части).
Я очень хорошо владею дифференциальным и интегральным исчислением...
5.20. Определение порядка байтов У. С. Джильберт, «Пираты Пензанса», акт 1
Интересно, что производители компьютеров никак не могут договориться, в каком
порядке лучше хранить двоичные байты. Следует ли размещать старший бит по Для приближенного вычисления определенного интеграла имеется проверенная
большему или по меньшему адресу? При передаче сообщения по проводам нужно временем техника. Любой студент, изучавший математический анализ, вспомнит,
сначала посылать старший или младший бит? что она называется суммой Римана.
180 Численные методы Неэлементарная тригонометрия 181

Приведенный ниже метод integrate принимает начальное и конечное значения Дуговой градус, которым мы пользуемся в повседневной жизни, – пережиток
зависимой переменной, а также приращение. Четвертый параметр (который на самом древневавилонской системы счисления по основанию 60: в ней окружность делится
деле параметром не является) – это блок. В блоке должно вычисляться значение функ- на 360 градусов. Менее известна псевдометрическая единица измерения град, опре-
ции от переданной в него зависимой переменной (здесь слово «переменная» употреб- деленная так, что прямой угол составляет 100 град (а вся окружность – 400 град).
ляется в математическом, а не программистском смысле). Необязательно отдельно При вычислении тригонометрических функций в языках программирования
определять функцию, которая вызывается в блоке, но для ясности мы это сделаем. по умолчанию чаще всего используются радианы, и Ruby в этом отношении не ис-
def integrate(x0, x1, dx=(x1-x0)/1000.0) ключение. Но мы покажем, как производить вычисления и в градусах, и в градах
x = x0 для тех читателей, которые по образованию не инженеры, а по происхождению не
sum = 0 древние вавилоняне.
loop do Поскольку число любых угловых единиц в окружности – константа, можно лег-
y = yield(x) ко переходить от одних единиц к другим. Мы определим соответствующие констан-
sum += dx * y
ты и будем пользоваться ими в коде. Для удобства поместим их в модуль Math.
x += dx
break if x > x1 module Math
end
sum RAD2DEG = 360.0/(2.0*PI) # Радианы в градусы.
end RAD2GRAD = 400.0/(2.0*PI) # Радианы в грады.

def f(x) end


x**2 Теперь можно определить и новые тригонометрические функции. Посколь-
end ку мы всегда преобразуем в радианы, то будем делить на определенные выше ко-
z = integrate(0.0,5.0) {|x| f(x) }
эффициенты. Можно было бы поместить определения функций в тот же модуль
Math, но мы этого делать не стали.
puts z, "\n" # 41.7291875 def sin_d(theta)
Здесь мы опираемся на тот факт, что блок возвращает значение, которое может Math.sin (theta/Math::RAD2DEG)
end
быть получено с помощью yield. Кроме того, сделаны некоторые допущения. Во-
первых, мы предполагаем, что x0 меньше x1 (в противном случае получится беско- def sin_g(theta)
нечный цикл). Читатель сам легко устранит подобные огрехи. Во-вторых, мы счи- Math.sin (theta/Math::RAD2GRAD)
таем, что функцию можно вычислить в любой точке заданной области. Если это не end
так, мы получим хаотическое поведение. (Впрочем, подобные функции все равно, Функции cos и tan можно было бы определить аналогично.
как правило, не интегрируемы – по крайней мере, на указанном интервале. В ка- С функцией atan2 дело обстоит несколько сложнее. Она принимает два аргу-
честве примера возьмите функцию f(x)=x/(x-3) в точке x=3.) мента (длины противолежащей и прилежащей сторон прямоугольного треуголь-
Призвав на помощь полузабытые знания об интегральном исчислении, мы мог- ника). Поэтому мы преобразуем результат, а не аргумент:
ли бы вычислить, что в данном случае результат равен примерно 41.666 (5 в кубе,
def atan2_d(y,x)
поделенное на 3). Почему же ответ не так точен, как хотелось бы? Из-за выбран-
Math.atan2(y,x)/Math::RAD2DEG
ного размера приращения; чем меньше величина dx, тем точнее результат (ценой end
увеличения времени вычисления).
Напоследок отметим, что подобная методика более полезна для действитель- def atan2_g(y,x)
но сложных функций, а не таких простых, как f(x) = x**2. Math.atan2(y,x)/Math::RAD2GRAD
end
5.22. Тригонометрия в градусах, радианах и градах
При измерении дуг математической, а заодно и «естественной» единицей изме- 5.23. Неэлементарная тригонометрия
рения является радиан. По определению, угол в один радиан соответствует длине В ранних версиях Ruby не было функций arcsin и arccos. Равно как и гиперболи-
дуги, равной радиусу окружности. Немного поразмыслив, легко понять, что угол ческих функций sinh, cosh и tanh. Их определения были приведены в первом изда-
2π радиан соответствует всей окружности. нии этой книги, но сейчас они являются стандартной частью модуля Math.
182 Численные методы Дисперсия и стандартное отклонение 183

5.24. Вычисление логарифмов def gmean(x)


prod=1.0
по произвольному основанию x.each {|v| prod *= v}
prod**(1.0/x.size)
Чаще всего мы пользуемся натуральными логарифмами (по основанию e, часто end
натуральный логарифм обозначается как ln), иногда также десятичными (по ос-
нованию 10). Эти функции реализованы в методах Math.log и Math.log10 соответ- data = [1.1, 2.3, 3.3, 1.2, 4.5, 2.1, 6.6]
ственно.
В информатике, а в особенности в таких ее областях, как кодирование и тео- am = mean(data) # 3.014285714
hm = hmean(data) # 2.101997946
рия информации, обычно применяются логарифмы по основанию 2. Например, gm = gmean(data) # 2.508411474
так вычисляется минимальное число битов, необходимых для представления чис-
Медианой набора данных называется значение, которое оказывается прибли-
ла. Определим функцию с именем log2:
зительно в середине отсортированного набора (ниже приведен код для вычисления
def log2(x)
медианы). Примерно половина элементов набора меньше медианы, а другая по-
Math.log(x)/Math.log(2)
end
ловина – больше. Ясно, что такая статистика показательна не для всякого набора.
def median(x)
Ясно, что обратной к ней является функция 2**x (как обращением ln x служит sorted = x.sort
Math::E**x или Math.exp(x)). mid = x.size/2
Эта идея обобщается на любое основание. В том маловероятном случае, если sorted[mid]
вам понадобится логарифм по основанию 7, можно поступить так: end
def log7(x)
data = [7,7,7,4,4,5,4,5,7,2,2,3,3,7,3,4]
Math.log(x)/Math.log(7)
puts median(data) # 4
end
Мода набора данных – это наиболее часто встречающееся в нем значение. Если
На практике знаменатель нужно вычислить один раз и сохранить в виде кон-
такое значение единственно, набор называется унимодальным, в противном слу-
станты.
чае – мультимодальным. Мультимодальные наборы более сложны, здесь мы их
рассматривать не будем. Интересующийся читатель может обобщить и улучшить
5.25. Вычисление среднего, медианы приведенный ниже код:
и моды набора данных def mode(x)
f = {} # Таблица частот.
Пусть дан массив x, вычислим среднее значение по всем элементам массива. На са- fmax = 0 # Максимальная частота.
мом деле есть три общеупотребительные разновидности среднего значения. Сред- m = nil # Мода.
нее арифметическое – это то, что мы называем средним в обыденной жизни. Сред- x.each do |v|
нее гармоническое – это число элементов, поделенное на сумму обратных к ним. f[v] ||= 0
f[v] += 1
И, наконец, среднее геометрическое – это корень n-ой степени из произведения n fmax,m = f[v], v if f[v] > fmax
значений. Вот эти определения, воплощенные в коде: end
def mean(x) return m
sum=0 end
x.each {|v| sum += v}
sum/x.size data = [7,7,7,4,4,5,4,5,7,2,2,3,3,7,3,4]
end puts mode(data) # 7

def hmean(x)
sum=0
5.26. Дисперсия и стандартное отклонение
x.each {|v| sum += (1.0/v)} Дисперсия – это мера «разброса» значений из набора. (Здесь мы не различаем сме-
x.size/sum щенные и несмещенные оценки.) Стандартное отклонение, которое обычно обо-
end значается буквой σ, равно квадратному корню из дисперсии.
184 Численные методы Генерирование случайных чисел 185

data = [2, 3, 2, 2, 3, 4, 5, 5, 4, 3, 4, 1, 2] Приведенная ниже версия отличается лишь тем, что работает с одним масси-
вом, каждый элемент которого – массив, содержащий пару (x, y):
def variance(x)
def correlate2(v)
m = mean(x)
sum = 0.0
sum = 0.0
v.each do |a|
x.each {|v| sum += (v-m)**2 }
sum += a[0]*a[1]
sum/x.size
end
end
xymean = sum/v.size.to_f
x = v.collect {|a| a[0]}
def sigma(x)
y = v.collect {|a| a[1]}
Math.sqrt(variance(x))
xmean = mean(x)
end
ymean = mean(y)
sx = sigma(x)
puts variance(data) # 1.461538462
sy = sigma(y)
puts sigma(data) # 1.20894105
(xymean-(xmean*ymean))/(sx*sy)
Отметим, что функция variance вызывает определенную выше функцию mean. end

5.27. Вычисление коэффициента корреляции d = [[1,6.1], [2.1,3.1], [3.9,5.0], [4.8,6.2]]

Коэффициент корреляции – одна из самых простых и полезных статистических c4 = correlate2(d) # 0.2277822492


мер. Он измеряет «линейность» набора, состоящего из пар (x, y), и изменяется И, наконец, в последнем варианте предполагается, что пары (x, y) хранятся в
от -1.0 (полная отрицательная корреляция) до +1.0 (полная положительная кор- хэше. Код основан на предыдущем примере:
реляция). def correlate_h(h)
Для вычисления воспользуемся функциями mean и sigma (стандартное откло- correlate2(h.to_a)
нение), которые были определены в разделах 5.25 и 5.26. О смысле этого показате- end
ля можно прочитать в любом учебнике по математической статистике.
В следующем коде предполагается, что есть два массива чисел одинакового e = { 1 => 6.1, 2.1 => 3.1, 3.9 => 5.0, 4.8 => 6.2}
размера:
c5 = correlate_h(e) # 0.2277822492
def correlate(x,y)
sum = 0.0
x.each_index do |i| 5.28. Генерирование случайных чисел
sum += x[i]*y[i]
end Если вас устраивают псевдослучайные числа, вам повезло. Именно они предостав-
xymean = sum/x.size.to_f ляются в большинстве языков, включая и Ruby.
xmean = mean(x) Метод rand из модуля Kernel возвращает псевдослучайное число x с плаваю-
ymean = mean(y) щей точкой, отвечающее условиям x >= 0.0 и x < 1.0. Например (вы можете полу-
sx = sigma(x) чить совсем другое число):
sy = sigma(y) a = rand # 0.6279091137
(xymean-(xmean*ymean))/(sx*sy)
end Если при вызове задается целочисленный параметр max, то возвращается це-
лое число из диапазона 0...max (верхняя граница не включена). Например:
a = [3, 6, 9, 12, 15, 18, 21] n = rand(10) # 7
b = [1.1, 2.1, 3.4, 4.8, 5.6]
Чтобы «затравить» генератор случайных чисел (задать начальное значение –
c = [1.9, 1.0, 3.9, 3.1, 6.9]
seed), применяется метод srand из модуля Kernel, который принимает один чис-
c1 = correlate(a,a) # 1.0 ловой параметр. Если не передавать никакого значения, то метод srand самосто-
c2 = correlate(a,a.reverse) # -1.0 ятельно изготовит затравку, учитывая (среди прочего) текущее время. Если же
c3 = correlate(b,c) # 0.8221970228 параметр передан, то именно он и становится затравкой. Это бывает полезно при
186 Численные методы Заключение 187

тестировании, когда для воспроизводимости результатов многократно вызывае- g1 = zeta(0.8,0.1,0.1)


мая программа должна получать одну и ту же последовательность псевдослучай-
ных чисел. memoize(:zeta) # Сохранить таблицу в памяти.
g2 = zeta(0.8,0.1,0.1)
srand(5)
i, j, k = rand(100), rand(100), rand(100) memoize(:zeta,"z.cache") # Сохранить таблицу на диске.
# 26, 45, 56 g3 = zeta(0.8,0.1,0.1)
srand(5) Обратите внимание, что можно задать имя файла. Это может несколько замед-
l, m, n = rand(100), rand(100), rand(100) лить работу, зато экономится память, и таким образом мы можем сохранить запом-
# 26, 45, 56 ненные результаты и воспользоваться ими при следующих вызовах программы.
В ходе неформального тестирования мы вызывали функцию 50000 раз в цик-
5.29. Кэширование функций с помощью метода memoize ле. Оказалось, что g2 вычисляется примерно в 1100 раз быстрее, чем g1, а g3 – при-
мерно в 700 раз. На вашей машине может получиться иной результат.
Пусть имеется вычислительно сложная математическая функция, которую нуж- Отметим еще, что библиотека memoize предназначена не только для матема-
но многократно вызывать по ходу работы программы. Если быстродействие кри- тических функций. Ее можно использовать для запоминания результатов работы
тично и при этом можно пожертвовать небольшим количеством памяти, то имеет любого вычислительно сложного метода.
смысл сохранить результаты вычисления функции в таблице и обращаться к ней
во время выполнения. (Тут неявно предполагается, что функция будет часто вы-
зываться с одними и теми же параметрами, то есть получается, что мы «выбрасы- 5.30. Заключение
ваем» результат дорогостоящего вычисления и снова повторяем его позже.) Такая В этой главе были рассмотрены различные представления чисел, в том числе
техника иногда называется запоминанием (memoizing), отсюда и название библи- целых (в разных системах счисления) и с плавающей точкой. Мы видели, какие
отеки memoize. трудности возникают при работе с числами с плавающей точкой и как можно час-
Эта библиотека не входит в стандартный дистрибутив, поэтому придется уста- тично обойти эти трудности, применяя рациональные числа. Мы познакомились с
новить ее вручную. явными и неявными преобразованиями, а также с приведениями типов.
В следующем примере демонстрируется сложная функция zeta. Она применя- Также мы изучили разнообразные способы манипулирования числами, век-
ется при решении одной задачи из области популяционной генетики, но вдавать- торами и матрицами. Был приведен обзор стандартных библиотек, полезных для
ся в объяснения мы не станем. численного анализа, в частности библиотеки mathn.
require 'memoize' Пойдем дальше. В следующей главе мы обсудим два очень характерных для
include Memoize Ruby типа данных: символы и диапазоны.

def zeta(x,y,z)
lim = 0.0001
gen = 0
loop do
gen += 1
p,q = x + y/2.0, z + y/2.0
x1, y1, z1 = p*p*1.0, 2*p*q*1.0, q*q*0.9
sum = x1 + y1 + z1
x1 /= sum
y1 /= sum
z1 /= sum
delta = [[x1,x],[y1,y],[z1,z]]
break if delta.all? {|a,b| (a-b).abs < lim }
x,y,z = x1,y1,z1
end
gen
end
Символы 189

По словам Джима Вайриха, символ – это «объект, у которого есть имя». Остин
Зиглер предпочитает говорить об «объекте, который сам является именем». Как
бы то ни было, существует взаимно однозначное соответствие между символами
и именами. К чему можно применить имена? Например, к переменным, методам и
произвольным константам.
Глава 6. Символы и диапазоны Типичное применение символов – для представления имени переменной или
метода. Например, чтобы добавить в класс атрибут, допускающий чтение и изме-
нение, можно поступить следующим образом:
Я слышу и забываю. Я вижу и запоминаю. Я делаю и понимаю. class SomeClass
Конфуций attr_accessor :whatever
end
Символы и диапазоны – объекты, весьма характерные для языка Ruby. Они рас- То же самое можно выразить иначе:
сматриваются в одной главе не потому, что тесно связаны между собой, а потому, class SomeClass
что сказать о них можно не так уж много. def whatever
Концепцию символа в Ruby понять непросто. Они напоминают «атомы» в язы- @whatever
ке Lisp. Вместо того чтобы давать длинное и сложное определение, я расскажу о end
том, что можно делать с символами и как они применяются. В конце концов, на def whatever=(val)
вопрос «что такое число» можно дать очень глубокомысленный ответ, но нам нуж- @whatever = val
но всего лишь знать, как манипулировать числами. end
Диапазоны проще. Это всего лишь представление множества, заданного конеч- end
ными точками. Аналогичные конструкции есть в языках Pascal, PHP и даже SQL Другими словами, символ :whatever говорит методу attr_accessor, что мето-
Познакомимся с символами и диапазонами поближе, чтобы понять, как они дам чтения и установки (а равно и самой переменной экземпляра) следует присво-
практически используются в программах на Ruby. ить имена, определяемые указанным символом.
Но почему не воспользоваться просто строкой? Вообще-то можно. Многие,
6.1. Символы даже большинство системных методов, ожидающих символ в качестве параметра,
соглашаются и на строку.
Символ в Ruby – это экземпляр класса Symbol. Синтаксически он обычно обозна-
attr_reader :alpha
чается двоеточием (:), за которым следует идентификатор.
attr_reader "beta" # Так тоже можно.
Символ похож на строку, он тоже соответствует последовательности символов.
Отличие от строки состоит в том, что у каждого символа есть только один экзем- На самом деле символ «похож» на строку в том смысле, что ему соответству-
пляр (как и в случае с объектами Fixnum). Следовательно, имеет место проблема ет последовательность символов. Поэтому некоторые говорят, что «символ – это
потребления памяти или производительности, о которой нужно помнить. Напри- просто неизменяемая строка». Но класс Symbol не наследует классу String, а ти-
мер, в нижеприведенном коде строка "foo" представлена в памяти тремя различ- пичные операции над строками необязательно применимы к символам.
ными объектами, а символ :foo – одним, на который есть несколько ссылок: Также неправильно думать, что символы напрямую соответствуют идентифи-
array = ["foo", "foo", "foo", :foo, :foo, :foo] каторам. Из-за этого непонимания некоторые говорят о «таблице символов» (как
Некоторых смущает двоеточие перед именем символа. Не волнуйтесь, это всего если бы речь шла об ассемблированном объектном коде). В действительности это
лишь синтаксическое соглашение. У строк, массивов и хэшей есть начальный и ко- представление бессмысленно; хотя символы и хранятся в какой-то внутренней
нечный ограничители, а у символов – только начальный. Считайте, что это унар- таблице (а как же иначе?), Ruby не дает к ней доступа, поэтому программистам все
ный, а не бинарный ограничитель. На первый взгляд синтаксис кажется стран- равно, существует она или нет.
ным, но ничего таинственного в нем нет. Более того, символы даже не всегда выглядят как идентификаторы. Обычно
Стоит отметить, что в старых версиях Ruby (до 1.6) символьные константы не это так, что бы под этим ни понимать, но символ может содержать и знаки препи-
были полноценными объектами, поскольку преобразовывались в Fixnum и в таком нания, если заключен в кавычки. Все показанные ниже символы допустимы:
виде хранились. Внутреннее представление осталось таким же; символу ставит- sym1 = :"This is a symbol"
ся в соответствие число, и хранится он как непосредственное значение. Само чис- sym2 = :"This is, too!"
ло можно получить, вызвав метод to_i, но в этом редко возникает необходимость. sym3 = :")(*&^%$" # И даже такой.
190 Символы и диапазоны Символы 191

Можно даже использовать символы для определения переменных и методов эк- Можно ли сказать, что это «лучше», чем механизм исключений? Необязатель-
земпляра, но тогда для ссылки на них пришлось бы применять такие методы, как но. Но такую методику стоит иметь в виду, особенно когда приходится обрабаты-
send и instance_variable_get. Вообще говоря, такая практика не рекомендуется. вать «граничные случаи», которые не считаются ошибками.
6.1.1. Символы как перечисления 6.1.3. Символы, переменные и методы
В языке Pascal и в поздних версиях C есть понятие перечисляемого типа. В Ruby Наверное, чаще всего символы применяются для определения атрибутов класса:
ничего подобного быть не может, ведь никакого контроля типов не производится. class MyClass
Но символы часто используются как мнемонические имена; стороны света можно attr_reader :alpha, :beta
было бы представить как :north, :south, :east и :west. attr_writer :gamma, :delta
Быть может, немного понятнее хранить их в виде констант: attr_accessor :epsilon
North, South, East, West = :north, :south, :east, :west # ...
end
Если бы это были строки, а не символы, то определение их в виде констант
могло бы сэкономить память, но каждый символ все равно существует в объ- Имейте в виду, что в этом фрагменте на самом деле исполняется некий код.
ектном пространстве в единственном экземпляре. (Символы, подобно объектам Например, attr_accessor использует имя символа для определения имени пере-
Fixnum, хранятся как непосредственные значения.) менной экземпляра, а также методов для ее чтения и изменения. Это не означает,
что всегда имеется точное соответствие между символом и именем переменной эк-
6.1.2. Символы как метазначения земпляра. Например, обращаясь к методу instance_variable_set, мы должны за-
Мы нередко пользуемся исключениями, чтобы уйти от кодов возврата. Но никто дать точное имя переменной, включая и знак @:
не мешает возвращать коды ошибки, если вам так хочется. К тому же в Ruby ме- sym1 = :@foo
тод может возвращать более одного значения. sym2 = :foo
В таком механизме часто возникает необходимость. Когда-то символ NUL ко- instance_variable_set(sym1,"str") # Правильно.
да ASCII вообще не считался символом. В языке C есть понятие нулевого указате- instance_variable_set(sym2,"str") # Ошибка.
ля (NULL), в Pascal есть указатель nil, в SQL NULL означает отсутствие какого бы Короче говоря, символ, передаваемый методам из семейства attr, – всего лишь
то ни было значения. В Ruby, конечно, тоже есть свой nil. аргумент, а сами эти методы создают требуемые переменные и методы экземпля-
Проблема в том, что такие метазначения часто путают с действительными зна- ра, основываясь на значении символа. (В конец имени метода изменения добавля-
чениями. В наши дни все считают NUL настоящим символом кода ASCII. И в Ruby ется знак равенства, а в начало имени переменной экземпляра – знак @.) Бывают
нельзя сказать, что nil не является объектом; его можно хранить, над ним можно также случаи, когда символ должен точно соответствовать идентификатору, на ко-
выполнять какие-то операции. Поэтому не вполне понятно, как интерпретировать торый ссылается.
ситуацию, когда hash[key] возвращает nil: то ли указанный ключ вообще не най- В большинстве случаев (если не во всех!) методы, ожидающие на входе сим-
ден, то ли с ним ассоциировано значение nil. вол, принимают также строку. Обратное не всегда верно.
Идея в том, что иногда символы могут выступать в роли подходящих метазна-
чений. Представьте метод, который получает строку из сети (возможно, по прото- 6.1.4. Преобразование строки в символ и обратно
колу HTTP или иным способом). При желании можно было бы вернуть нестроко- Строки и символы можно преобразовывать друг в друга с помощью методов to_
вое значение как индикатор исключительной ситуации. str и to_sym:
str = get_string a = "foobar"
case str b = :foobar
when String a == b.to_str # true
# Нормальная обработка. b == a.to_sym # true
when :eof
# Конец файла, закрытие сокета и т. п. Для метапрограммирования иногда бывает полезен такой метод:
when :error class Symbol
# Ошибка сети или ввода/вывода. def +(other)
when :timeout (self.to_s + other.to_s).to_sym
# Ответ не получен вовремя. end
end end
192 Символы и диапазоны Диапазоны 193

Он позволяет конкатенировать символы (или дописывать строку в конец сим- Оператор .. включает конечную точку, а оператор ... не включает. (Если это
вола). Ниже приведен пример использования; мы принимаем на входе символ и для вас неочевидно, просто запомните.) Таким образом, диапазоны digits и scale2
пытаемся определить, представляет ли он какой-нибудь метод доступа (то есть су- из предыдущего примера одинаковы.
ществует ли метод чтения или установки атрибута с таким именем): Но диапазоны могут состоять не только из целых чисел – более того, не только
class Object из чисел. Началом и концом диапазона в Ruby может быть любой объект. Однако,
def accessor?(sym) как мы вскоре увидим, не все диапазоны осмыслены или полезны.
return (self.respond_to?(sym) and self.respond_to?(sym+"=")) Основные операции над диапазоном – обход, преобразование в массив, а так-
end же выяснение, попадает ли некоторый объект в данный диапазон. Рассмотрим раз-
end
нообразные варианты этих и других операций.
Упомяну также о более изощренном способе применения символов. Иногда при
выполнении операции map нужно указать сложный блок. Однако во многих случа- 6.2.1. Открытые и замкнутые диапазоны
ях мы просто вызываем некоторый метод для каждого элемента массива или набора: Диапазон называется замкнутым, если включает конечную точку, и открытым –
list = words.map {|x| x.capitalize } в противном случае:
Не кажется ли вам, что для такой простой задачи слишком много знаков пре- r1 = 3..6 # Замкнутый.
пинания? Давайте вместо этого определим метод to_proc в классе Symbol. Он будет r2 = 3...6 # Открытый.
a1 = r1.to_a # [3,4,5,6]
приводить любой символ к типу объекта proc. Но какой именно объект proc следует
a2 = r2.to_a # [3,4,5]
вернуть? Очевидно, соответствующий самому символу в контексте объекта; иными
словами, такой, который пошлет сам символ в виде сообщения объекту. Нельзя сконструировать диапазон, который не включал бы начальную точку.
def to_proc
Можно считать это ограничением языка.
proc {|obj, *args| obj.send(self, *args) }
6.2.2. Нахождение границ диапазона
end
Кстати, этот код заимствован из проекта Гэвина Синклера (Gavin Sinclair) «Рас- Методы first и last возвращают соответственно левую и правую границу диапа-
ширения Ruby». Имея такой метод, мы можем следующим образом переписать пер- зона. У них есть синонимы begin и end (это еще и ключевые слова, но интерпрети-
воначальный код: руются как вызов метода, если явно указан вызывающий объект).
list = words.map(&:capitalize) r1 = 3..6
r2 = 3...6
Стоит потратить немного времени и разобраться, как это работает. Метод map r1a, r1b = r1.first, r1.last # 3, 6
обычно принимает только блок (никаких других параметров). Наличие знака & (ам- r1c, r1d = r1.begin, r1.end # 3, 6
персанд) позволяет передать объект proc вместо явно указанного блока. Поскольку r2a, r2b = r1.begin, r1.end # 3, 6
мы применяем амперсанд к объекту, не являющемуся proc, то интерпретатор пы- Метод exclude_end? сообщает, включена ли в диапазон конечная точка:
тается вызвать метод to_proc этого объекта. Получающийся в результате объект
r1.exclude_end? # false
proc подставляется вместо явного блока, чтобы метод map вызывал его для каждо- r2.exclude_end? # true
го элемента массива. А зачем передавать self в виде сообщения элементу массива?
Затем, что объект proc является замыканием и, следовательно, помнит контекст, в 6.2.3. Обход диапазона
котором был создан. А в момент создания self был ссылкой на символ, для кото- Обычно диапазон можно обойти. Для этого класс, которому принадлежат грани-
рого вызывался метод to_proc. цы диапазона, должен предоставлять осмысленный метод succ (следующий).
(3..6).each {|x| puts x } # Печатаются четыре строки
6.2. Диапазоны # (скобки обязательны).
Понятие диапазона интуитивно понятно, но и у него имеются некоторые неоче- Пока все хорошо. И тем не менее будьте очень осторожны при работе со стро-
видные особенности и способы применения. Одним из самых простых является ковыми диапазонами! В классе String имеется метод succ, но он не слишком поле-
числовой диапазон: зен. Пользоваться этой возможностью следует только при строго контролируемых
digits = 0..9 условиях, поскольку метод succ определен не вполне корректно. (В определении
scale1 = 0..10 используется, скорее, «интуитивно очевидный», нежели лексикографический по-
scale2 = 0...10 рядок, поэтому существуют строки, для которых «следующая» не имеет смысла.)
194 Символы и диапазоны Диапазоны 195

r1 = "7".."9" r = 3..12
r2 = "7".."10" arr = r.to_a # [3,4,5,6,7,8,9,10,11,12]
r1.each {|x| puts x } # Печатаются три строки.
Ясно, что для диапазонов чисел типа Float такой подход не работает. Со стро-
r2.each {|x| puts x } # Ничего не печатается!
ковыми диапазонами иногда будет работать, но лучше этого не делать, поскольку
Предыдущие примеры похожи, но ведут себя по-разному. Отчасти причина в результат не всегда очевиден или осмыслен.
том, что границы второго диапазона – строки разной длины. Мы ожидаем, что в
диапазон входят строки "7", "8", "9" и "10", но что происходит на самом деле? 6.2.6. Обратные диапазоны
При обходе диапазона r2 мы начинаем со значения "7" и входим в цикл, кото- Имеет ли смысл говорить об обратном диапазоне? И да, и нет. Следующий диапа-
рый завершается, когда текущее значение окажется больше правой границы. Но зон допустим:
ведь "7" и "10" – не числа, а строки, и сравниваются они как строки, то есть лекси- r = 6..3
кографически. Поэтому левая граница оказывается больше правой, и цикл не вы- x = r.begin # 6
полняется ни разу. y = r.end # 3
А что сказать по поводу диапазонов чисел с плавающей точкой? Такой диапа- flag = r.end_excluded? # false
зон можно сконструировать и, конечно, проверить, попадает ли в него конкрет- Как видите, мы можем определить обе границы и узнать, что правая граница
ное число. Это полезно. Но обойти такой диапазон нельзя, так как метод succ от- включена. Но этим перечень возможных операций практически исчерпывается.
сутствует.
arr = r.to_a # []
fr = 2.0..2.2 r.each {|x| p x } # Ни одной итерации.
fr.each {|x| puts x } # Ошибка! y = 5
Почему для чисел с плавающей точкой нет метода succ? Теоретически можно r.include?(y) # false (для любого значения y)
было бы увеличивать число на некоторое приращение. Но величина такого прира- Означает ли это, что обратные диапазоны всегда бесполезны? Вовсе нет. В не-
щения сильно зависела бы от конкретной машины, при этом даже для обхода «не- которых случаях разумно инкапсулировать границы в один объект.
большого» диапазона понадобилось бы гигантское число итераций, а полезность На самом деле массивы и строки часто принимают обратные диапазоны в ка-
такой операции весьма сомнительна. честве индексов, поскольку индексация для них начинается