Программирование на языке Ruby, часть 2

Автор Hiran Ramankutty

Перевод Андрей Киселев

В предыдущем выпуске, мы рассмотрели основы языка программирования Ruby. Теперь пришла пора расширить наши познания.

Регулярные выражения

Регулярные выражения в Ruby заключаются между парой символов '/' (слэш), как в Perl или awk. Они поражают своей выразительностью и мощью, когда приходится иметь дело с шаблонами (например, поиск по шаблону).

print "abcdef" =~ /de/,"\n"
print "aaaaaa" =~ /d/,"\n"
^D
3
nil

Где `=~' -- это оператор поиска по шаблону. Он возвращает позицию в строке, где было найдено совпадение с шаблоном, или nil, если поиск не увенчался успехом. Синтаксис регулярных выражений весьма специфичен, взгляните ниже:

   
          [ ]     диапазон. (т.е., запись [a-z] означает любой символ в диапазоне от a до z)
          \w      буква или цифра. то же самое, что и [0-9A-Za-z_]
          \W      символ, не являющийся ни буквой ни цифрой
          \s      пробел. то же самое, что и [ \t\n\r\f]
          \S      не пробельный символ.
          \d      цифра. то же самое, что и [0-9].
          \D      символ, не являющийся цифрой.
          \b      граница слова.
          \B      не граница слова.
          *       повторяется 0 или большее число раз
          +       повторяется 1 или большее число раз
          {m,n}   повторяется не менее n и не более m раз
          ?       1 или 0 раз
          |       логическое "или"
          ( )     группировка

Например, выражение `^f[a-z]+' имеет смысл "символ f, за которым следует хотя бы один символ из диапазона от `a' до `z'". А теперь представим, что вам необходимо найти строку, которая подходит под описание, хотя бы такое: "Начинается с прописной буквы f, за которой следует одна заглавная буква и далее, до конца строки, возможно, следуют только прописные буквы". На языке C вам наверняка потребуется написать с дюжину строк, чтобы выполнить такой поиск. Разве я не прав? В Ruby же вы просто проверяете строку на совпадение с шаблоном /^f[A-Z](^[a-z])*$/. Возможность поиска строк по заданному шаблону часто используется в среде UNIX, типичный пример -- `grep'. Познакомимся с регулярными выражениями поближе. Рассмотрим следующую программу:

 # Сохраните этот код в файле regx.rb
 st = "\033[7m"
 en = "\033[m"
     
 while TRUE
        print "str> "
        STDOUT.flush
        str = gets
        break if not str
        str.chop!
        print "pat> "
        STDOUT.flush
        re = gets
        break if not re
        re.chop!
        str.gsub! re, "#{st}\\&#{en}"
        print str, "\n"
end
print "\n"
# А теперь дайте команду: ruby regx.rb

Программа предложит ввести две строки. Первая -- строка, по которой будет осуществляться поиск, вторая -- регулярное выражение. После чего будет выполнен поиск в строке по шаблону и найденная часть будет подсвечена. Хочу уточнить, что программа использует escape-последовательности для управления параметрами отображения, поэтому корректное отображение результата гарантировано только на ANSI-совместимых терминалах.

str>foobar
pat>^fo+
foobar
~~~

Для этого примера будут подсвечены первые три символа (foo). Символы ``~~~'' я использовал для выделения найденной подстроки на случай, если вы читаете эту статью с помощью текстового браузера. Давайте немного поэкспериментируем.

str>asd987wonew06521
pat>\d
asd987wonew06521
   ~~~     ~~~~~
str>foozboozer
pat>f.*z
foozboozer
~~~~~~~~

Обратите внимание на то, что в последнем примере будет выделена подстрока foozbooz, а не fooz. Это потому, что в качестве результата поиска по шаблону принимается самый длинный вариант. Следующий пример на первый взгляд выглядит более сложным:

str> Wed Feb  7 08:58:04 JST 1996
pat> [0-9]+:[0-9]+(:[0-9]+)?
Wed Feb  7 08:58:04 JST 1996
           ~~~~~~~~

Теперь попробуем написать шаблон для поиска шестнадцатеричных (HEX) чисел. (например, таких как 0x123af00c или 0xbc13590ae).

def chab(s)   # "поиск шестнадцатеричного числа, заключенного в угловые скобки"
        (s =~ /<0(x|X)(\d|[a-f]|[A-F])+>/) != nil
end
print chab "Здесь нет числа."
print "\n",chab "Может здесь? {0x35}" # используются не те скобки, которые ожидаются
print "\n",chab "Или здесь? <0x38z7e>" # Это не HEX-число
print "\n",chab "Вот оно: <0xfc0004>."
print "\n"
^D
false
false
false
true

Итераторы

Под итератором понимается код (объект), выполняющий некоторую последовательность действий несколько раз. Взгляните на следующий пример, написанный на языке C:

char *str;
for (str = "abcdefg"; *str != '\0'; str++) {
  /* обработка очередного символа */
}

Обратите внимание на степень абстракции, которую предоставляет синтаксис цикла for(...). Но не смотря на это программист все равно должен знать внутреннее представления данных, поскольку в данном случае выполнение цикла прекращается при встрече "пустого" (NULL) символа -- завершающего символа для строк.

Наличие итераторов - одна из особенностей , которая отличает языки высокого уровня. Взгляните на пример сценария командной оболочки (/bin/sh):

for i in *.[ch]; do
  # ...  некоторые действия, выполняемые для каждого файла
done

Эта конструкция обрабатывает все *.c и *.h файлы в текущем каталоге, причем командная оболочка сама беспокоится о выборе файла с подходящим расширением одного за другим. Разве это не более высокий уровень программирования чем C? Что вы об этом думаете?

Для встроенных типов данных создать итератор достаточно просто, но в ООП (Объектно Ориентированное Программирование прим. перев.) это может стать серьезной проблемой, поскольку пользователи могут определять свои собственные типы данных.

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

Взгляните на следующий пример:

"abc".each_byte{|c| printf "%c\n", c}
^D
a
b
c

Где each_byte является итератором для каждого символа в строке. Локальная переменная `c' служит здесь для приема очередного символа. C-подобный код этого итератора может быть записан так:

s="abc"
i=0
while i < s.length
        printf "%c\n",s[i]
        i+=1
end
^D
a
b
c

... однако итератор each_byte концептуально более прост и код, использующий итератор, останется работоспособным, даже если реализация класса строк претерпит существенные изменения. Замечательная особенность итераторов -- устойчивость к ошибкам, которые могут возникнуть из-за таких изменений, и я думаю, что это может служить характеристикой хорошего кода.

Еще один итератор для строк.

"a\nb\nc\n".each_line{|l| print l}
^D
a
b
c

Такая утомительная задача, как выделение отдельных элементов строки, разделенных некоторым символом, очень просто решается с помощью итераторов.

Попробуем переписать этот пример с помощью оператора for.

for l in "a\nb\nc\n"
        print l
end
^D
a
b
c

Оператор for выполняет итерации способом, аналогичным итератору строки. И выполняет те же действия, что и each_line в предыдущем примере

Цикл может быть прерван и перезапущен оператором `retry'. См. пример ниже.

c = 0
for i in 0..4
        print i
     if i==2 and c==0
                c = 1
          print "\n"
          retry
     end
end
^D
012
01234

Итератору можно передать даже блок исполняемого кода, который может быть исполнен с помощью оператора yield. Ниже показан пример итератора repeat, который выполняет вывод на экран заданное число раз.

def repeat(num)
        while 0 < num
                yield 
                num-=1
        end
end
repeat(4) {print "hello world\n"}
^D
hello world
hello world
hello world
hello world

Если вам что-то непонятно -- попробуйте вставить в пример вывод значения переменной num до и после оператора yield.

С помощью оператора retry можно попробовать определить while-подобный итератор (см. пример ниже), но на практике такой прием не пользуется популярностью из-за снижения скорости исполнения.

def MYWHILE(cond)
        return if not cond
        yield
        retry
end
i = 0
MYWHILE(i<3) {print i,"\n" ;i+=1}
^D
0
1
2

Надеюсь, что к настоящему моменту у вас сложилось достаточно четкое представление об итераторах. Конечно, существуют некоторые ограничения, но тем не менее, при создании новых типов данных очень удобно включать в их реализацию и соответствующие итераторы. В этом случае примеры итераторов `repeat()' и `MYWHILE()' не представляют особого интереса. К итераторам мы еще вернемся, после того как рассмотрим понятие класса.

Объектно-ориентированный образ мышления

"Объектно-ориентированный" -- очень броское выражение. Ruby претендует на звание объектно-ориентированного языка программирования, но что же все таки означает понятие "объектно-ориентированный"?

Можно дать разные ответы на этот вопрос, но все они, вероятно, сводятся к одному и тому же. Однако, не будем торопиться с выводами и для начала рассмотрим традиционную парадигму программирования.

В традиционном представлении проблема программирования заключается в создании структур данных и процедур, обрабатывающих эти данные. В рамках такого представления данные являются "инертными", "пассивными" и "беспомощными" элементами программы, поскольку они полностью отданы на милость процедурам, которые в свою очередь "активны", "логичны" и "всемогущи".

Главная проблема такого подхода в том, что программы пишут люди, которые не в состоянии удерживать в памяти все до мельчайших подробностей. По мере развития проекта количество процедур увеличивается, вспоминать что где и как работает становится все труднее и труднее. Повышается вероятность появления опечаток, приводящих к трудноуловимым ошибкам. "Всплывают" сложные и непредсказуемые взаимодействия. Работа по управлению таким проектом начинает напоминать попытку пронести рассерженного кальмара мимо лица не дав ему коснуться своими щупальцами. Существуют определенные рекомендации по уменьшению этих отрицательных моментов, но есть лучшее решение, представляющее собой совершенно иной подход к программированию.

При объектно-ориентированном подходе значительная часть действий делегируется самим данным, таким образом понятие "данные" можно перевести из разряда "пассивных" элементов программы в разряд "активных". Или другими словами

Воспринимая классы как "механизмы" ("машины"), мы ничего не можем сказать об их внутреннем устройстве, которое может быть очень простым или наоборот очень сложным. Всю работу мы выполняем "щелкая переключателями" и "читая показания приборов". Мы позволяем себе "вскрывать" их только в исключительных случаях -- когда абсолютно уверены в необходимости что-то подправить. После того как "машина" будет готова, мы можем смело забыть о том как она работает

Вам может показаться, что мы делаем лишнюю работу, на самом деле мы выполняем работу по предотвращению ошибок.

Рассмотрим простой пример из реальной жизни, но тем не менее прекрасно иллюстрирующий некоторые понятия. В моем автомобиле имеется одометр, измеряющий расстояние, пройденное с момента последнего нажатия на кнопку сброса. Как можно представить себе модель этого прибора на языке программирования? На языке C его можно представить как переменную, возможно типа float. Управляющая программа должна увеличивать эту переменную на некую величину и обнулять ее, когда это необходимо. Что тут может быть неправильно? В программе может крыться ошибка, в результате которой, значение переменной будет меняться непредсказуемым образом. Кто писал на C -- тот знает, что на поиски таких ошибок уходит масса времени. А когда причина обнаруживается, то она может оказаться простой до нелепости. (Такие моменты обычно сопровождаются звонким хлопком по лбу!)

При объектно-ориентированном подходе та же проблема решается несколько иначе. Программист уже не задается вопросом: "Какой из имеющихся типов данных более подходит для решения задачи?". Его скорее интересует вопрос: "Как работает моделируемый объект?". Т.е. подход совершенно иной. При таком подходе нам необходимо потратить немного времени для того чтобы решить -- что такое одометр и как он взаимодействует с окружающим миром. Пусть в нашем представлении это будет небольшой механизм, который способен измерять и отображать пройденное расстояние, обнулять его и ничего больше.

Мы не предусматриваем возможность занесения в прибор произвольного значения. Почему? Потому, что это расстояние фактически не было пройдено и мы это прекрасно понимаем. Функциональность прибора мы уже описали выше и это описание предусматривает все, что нам нужно. Поэтому, сразу же предусмотрим выдачу сообщения об ошибке, если где-то в программе будет сделана попытка записать в одометр "левое" значение (скажем -- задание температуры для установки климат-контроля салона), во время выполнения программы (или на этапе компиляции, это зависит от типа используемого языка программирования) о том, что не допустимо заносить в одометр произвольные значения. Сообщение не обязательно должно быть очень подробным, но достаточным для того, чтобы понять в чем дело. Конечно -- это не устраняет самой ошибки, но дает возможность быстро локализовать ее. Это один способ из множества, с помощью которого объектно-ориентированное программирование позволяет сэкономить время, затрачиваемое на поиск ошибок.

Поднявшись на более высокий уровень абстракции, можно сказать, что нам нужен не один конкретный прибор, а скорее фабрика по их производству, потому что создать такую фабрику так же просто, как и отдельно взятый прибор. Фабрика выпускает приборы имея единственный шаблон. Такой шаблон называется классом, а сам прибор -- объектом. Большинство объектно-ориентированных языков программирования требуют, чтобы определение класса предшествовало созданию и использованию объектов этого класса, но в Ruby это не так.

Особо хочу подчеркнуть, что использование объектно-ориентированного языка не вынуждает следовать объектно-ориентированному стилю программирования. На любом языке можно написать запутанный, сырой, плохо продуманный, неустойчивый, содержащий ошибки код. Язык Ruby (в противоположность другим, особенно C++) делает объектно-ориентированный стиль программирования естественной привычкой так, что даже когда вам приходится работать над маленькими проектами, у вас не возникает потребности писать уродливый код, чтобы сэкономить усилия. Следующей нашей темой будут "органы управления" (методы объектов), а затем мы перейдем к "фабрикам" (классам). Оставайтесь с нами!


Hiran Ramankutty

Я -- студент последнего курса Правительственного Колледжа Компьютерных Наук в городе Трикур (Trichur). Кроме Linux, я с большим удовольствием занимаюсь изучением физики.
Copyright (C) 2002, Hiran Ramankutty. Copying license http://www.linuxgazette.com/copying.html
Published in Issue 83 of Linux Gazette, October 2002

Вернуться на главную страницу