[lg81]: Программирование на языке Ruby, часть 1

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

Автор: Hiran Ramankutty

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

Введение

Ruby представляет собой объектно-ориентированный язык программирования интерпретирующего типа. Он был создан программистом из Японии -- Юкихиро Матсумото (Yukihiro Matsumoto), где этот язык пользуется большей популярностью нежели Python или Perl! Первая часть из серии является введением в основы языка Ruby. Более глубокий материал будет даваться в последующих частях.

Я не собираюсь превозносить язык Ruby и доказывать его преимущества перед другими языками программирования. Большинство из нас прекрасно понимают, что каждый язык программирования имеет свои неповторимые черты. И ваш выбор скорее будет зависеть от ваших личных предпочтений и доступности стандартных библиотек, нежели от неких таинственных технических проблем.

Требования

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

Hello World

Начнем с обычной в таких случаях программы `Hello, World'

% cat > hello.rb
print "Hello World\n"
^D
% ruby hello.rb
Hello World
%

Переменные

Имена переменных должны начинаться с символа:

                $                       глобальная переменная
                @                       переменная экземпляра (поле объекта, данные-член)
                a-z или '_'             локальная переменная
                A-Z                     константа

Кроме того существуют еще две псевдопеременные, которые являются исключением из вышеуказанного правила - это `self' и `nil'.

  • self -- представляет собой ссылку на текущий исполняемый объект
  • nil -- не имеет значения, FALSE или значение неинициализированной переменной

Обе они именуются как локальные переменные, но таковыми не являются! Реальный смысл этих переменных станет вам понятен немного позже.

Глобальные переменные

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

 % ruby
 print $foo,"\n"
 $foo = 5
 print $foo,"\n"
 ^D
 %
Результат работы интерпретатора:
nil
5
Эта особенность дает возможность `связывать' процедуры с глобальными переменными, т.е. при изменении значения переменной `связанная' процедура будет вызываться автоматически. Но об этом позже!

Существует ряд специальных предопределенных глобальных переменных, являющихся по сути системными переменными интерпретатора Ruby (доступны только для чтения). Некоторые из них приведены ниже.

  • $! последнее сообщение об ошибке
  • $@ место возникновения ошибки
  • $_ последняя строка, считанная вызовом gets
  • $. номер последней строки, прочитанной интерпретатором
  • $& Найденная подстрока в последнем поиске по шаблону.
  • $~ результат последнего поиска по шаблону в виде массива подвыражений
  • $n N-ное подвыражение из массива - результата последнего поиска по шаблону (то же самое, что и $~[n])
  • $= флаг чувствительности к регистру
  • $0 имя файла программы
  • $* аргументы командной строки
  • $$ ID процесса интерпретатора
  • $? статус завершения последнего дочернего процесса

Локальные переменные

Имена локальных переменных должны начинаться с прописных (маленьких) символов латинского алфавита или с символа '_'. В отличие от глобальных переменных и ссылок на переменные, локальные переменные не принимают значение 'nil', например, при выполнении следующего кода:

% ruby
print foo
^D
Вы получите сообщение об ошибке:
      "undefined local variable or method 'foo' for #(object...)".

Область видимости локальных переменных ограничивается одной из следующих разновидностей блоков программы

  • proc {....}
  • loop {....}
  • def .... end
  • class .... end
  • module .... end
  • вся программа (если не выполняется ни один из вышеуказанных случаев)
Локальная переменная, инициализированная в каком либо блоке (или процедуре), после выхода за пределы блока становится неопределенной. Например:
def foo(n)
        k = 2 * n
        print "\n",k,"\n"
        print defined? k,"\n"
end

foo 3
print defined? k,"\n"
^D
Вывод интерпретатора:

6
local-variable
nil

В этом примере оператор `defined?' используется для проверки существования передаваемого ему аргумента. Результаты работы оператора "local-variable" и "nil" понятны и так и в комментариях не нуждаются.

Константы

Имена констант начинаются с заглавных (больших) символов латинского алфавита. Но, как правило, программирующие на Ruby, дают константам имена, состоящие только из заглавных символов. Так, например и 'Foo', и 'FOO' являются константами. Как и в случае с локальными переменными, константы инициализируются оператором присваивания, а обращение к неинициализированной константе, равно как и попытка изменения инициализированной константы, вызывает сообщение об ошибке. Проверьте это самостоятельно.

Строки

Строки в Ruby ограничиваются либо одинарными ('...'), либо двойными кавычками ("..."). Однако такие строки имеют различия между собой. При использовании двойных кавычек допускается вставлять в строки управляющие escape-последовательности, начинающиеся с символа '\', а также вычислять выражения с помощью конструкции #{}. Например:

print "\n"
print '\n'
print "\001","\n"
print '\001',"\n"
print "abcd #{5*3} efg","\n"
var = " abc "
print "1234#{var}567","\n"
^D

\n
\001
abcd 15 efg
1234abc567

Дополнительную информацию по строкам вы получите в разделе, посвященном массивам, поскольку массивы и строки во многом похожи друг на друга.

Массивы

Для определения массивов используются квадратные скобки '[]'. Массивы в Ruby являются гетерогенными (т.е. могут содержать данные различных типов).

a = [1,2,"3"]
print a,"\n"
^D

123

Однако, если вы попытаетесь заставить интерпретатор подсчитать сумму элементов массива, показанного выше, то он выдаст сообщение об ошибке:

         Error!!String cannot be coerced into Fixnum
Дело в том, что элемент массива `3' хранится в виде строки. Если вычисление суммы элементов массива задать следующим образом:
a = [1,2,"3"]
b = a[0] + a[1] + a[2].to_i
print b,"\n"
^D

6
То интерпретатор выполнит его без ошибки. Добавление '.to_i' к 3-му элементу массива (т.е. a[2].to_i) заставляет интерпретатор выполнить преобразование содержимого элемента массива a[2] в целое со знаком. Ради эксперимента можете попробовать подставить '.to_s'.

К массивам можно применять операции конкатенации (слияния) и повторения.

a = [1,2,"3"]
print a + ["foo","bar"]
print a * 2
^D
Результат
123foobar
123123

Можно получить "срез" массива:

a = [1,2,"3","foo","bar"]
print a[0],"\n"
print a[0,2],"\n"
print a[0..3],"\n"
print a[-2..2],"\n"
print a[-3..-1],"\n"

Массивы и строки взаимно конвертируемы. Массив можно преобразовать в строку с помощью метода 'join', а строку -- в массив с помощью метода 'split'.

a = [1,2,3]
print a[2],"\n"
a = a.join(":")
print a[2],"\n"
print a,"\n"
a = a.split(":")
print a[2],"\n"
print a,"\n"
^D

Ассоциативные массивы (хэши (англ. hash) или словари)-- еще один немаловажный вид представления данных. Хэши содержат пары ``ключ'' и ``значение'', см. пример ниже:

h = {1 => 2, "2" => "4"}
print hash,"\n"
print hash[1],"\n"
print hash["2"],"\n"
print hash[5],"\n"
^D
Я, надеюсь, результаты убедительны!

Управляющие конструкции

If - else

Попробуем написать функцию вычисления факториала. Математическое определение факториала:

      n! = 1                    (если n==0)
      n! = n * (n-1)!           (иначе)
На Ruby вычисление факториала может быть реализовано следующим образом:
def fact(n)
        if n == 0
                1
        else
                n * fact(n-1)
        end
end
print fact 4,"\n"
В результате получится 24.

Из-за наличия ключевого слова `end' Ruby иногда называют 'Алголо-подобным' языком. В данном примере рекурсивной функции вы можете заметить отсутствие оператора возврата результата (return). Естественно, использование этого оператора не возбраняется, но в данной ситуации он является излишним, поскольку в качестве возвращаемого значения принимается результат вычисления последнего выражения.

Цикл for

for i in 0..4
        body-of-for
end

Где i -- переменная цикла, а 0..4 -- диапазон значений. Для строковых переменных, заголовок цикла for можно записать так:

for i in "abc"

Цикл while

Попробуйте запустить этот пример

i=0
while i < 10
        print i+=1,"\n"
end

Case

Оператор выбора case используется для проверки набора условий. В результате работы следующего примера

i = 7
case i
when 2..5
        print "i in 2 to 5\n"
when 6..10
        print "i in 6 to 10\n"
end
^D

вы получите

i in 6 to 10

2..5 -- означает диапазон значений от 2 до 5 включительно. Здесь проверяется - попадает ли значение переменной i в заданный диапазон.

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

case 'abcdef'
when 'aaa','bbb'
        print 'contains aaa or bbb \n"
when /def/
        print "contains def \n"
end
^D
contains def

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

Дополнительные управляющие конструкции

Оператор case, из примера, приведенного выше, проверяет попадание значения переменной i в диапазон 2..5. Эта же проверка может быть записана несколько иначе, например:

(2..5) === i

Оператор отношения '===' используется для проверки выполнения нескольких условий одновременно и интерпретируется подобно ключевому слову when.

Попробуйте самостоятельно реализовать функцию, подобную isalnum(),isalpha(),isnum() и т.п. с помощью оператора '===' внутри конструкции if.

При использовании конструкций if и while для проверки отдельных значений, код может быть несколько сокращен

i = 7
print "contained in 5..10\n" if (5..10) === i
print i-=1,"\n" while i > 0
^D
contained in 5..10
6
5
4
3
2
1
0

Иногда может возникнуть необходимость в инвертировании проверяемого условия. Так, условие "если" (if) при инвертировании приобретает смысл "если не", условие while -- в условие until и т.д.

Существует четыре различных варианта управления исполнением цикла. Первый, аналогичный C -- break, означает выход за тело цикла. Второй -- next, переход в начало следующей итерации (аналог continue в C). Третий -- redo, перезапускает текущую итерацию. Следующий C-код иллюстрирует механизм работы этих трех методов (break, next и redo):

while(condition) {
  label_redo:
        goto label_next;                /* ruby's "next" */
        goto label_break;               /* ruby's "break"*/
        goto label_redo;                /* ruby's "redo" */
        ...
        ...
  label_next:
}
label_break:
...

И четвертый вариант -- оператор return. Фактически, этот оператор приводит не только к прерыванию цикла, но и к завершению работы метода (функции), который содержит прерываемый цикл.

Заключение

Мы рассмотрели некоторые элементарные особенности языка программирования Ruby в том объеме, который позволит вам написать свои первые программы. В своих следующих статьях, посвященных этому "драгоценному камню" (слово Ruby может быть переведено на русский язык как "рубин", прим. перев.), я буду делиться своим опытом , приобретенным при его изучении. До свидания!

Hiran Ramankutty

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


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

Программирование на языке 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
Вернуться на главную страницу


[lg86]: Программирование на языке Ruby, часть 3

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

Автор: Hiran Ramankutty
Перевод: Андрей Киселев

В первой части мы рассмотрели основные синтаксические конструкции языка программирования Ruby. Вторая часть была посвящена итераторам и основным принципам объектно-ориентированного программирования. В этой части мы поговорим об объектно-ориентированном программировании более детально.

Методы

Метод -- это некоторая функция, описывающая реакцию объекта на поступивший запрос. Взгляните на пример вызова метода, приведенный ниже:

print "asdfgh".length
^D
6

Из примера видно, что для строкового объекта вызывается метод с именем `length'.

Немного усложним:

foo = "abc"
print foo.length,"\n"
foo = [1, 2]
print foo.length,"\n"
^D
3
2

Из этого примера видно, что решение о том какой метод вызвать принимается во время исполнения и, что выбор этот зависит от содержимого переменной. (в первом случае переменная foo представляется как строковая переменная, во втором -- как массив прим. перев.).

У читателя может возникнуть вопрос: "Как же так? Ведь длина строки и длина массива считаются по разному?". Не стоит беспокоиться. К счастью, Ruby автоматически выбирает соответствующий ситуации метод. Эта особенность в объектно-ориентированном программировании называется полиморфизмом.

Нет никакой необходимости знать о том как работает тот или иной метод, но каждый должен знать о том какие методы имеются у объекта. При вызове неизвестного объекту метода, возникает ошибка. Например, попробуйте вызвать метод "length"для объекта "foo", присвоив ему предварительно значение "5".

Я уже упоминал о специальной псевдопеременной self. Она определяет объект, чей метод был вызван. Однако указание этой переменной очень часто опускается, например:

self.method_name(arguments...)

может быть сокращено до

method_name(arguments...)

Эти два вызова идентичны друг другу, просто второй способ является сокращенным вариантом первого.

Классы

Реальный мир состоит из объектов, которые могут быть классифицированы. Например, годовалый малыш, увидев собаку, думает о ней как "гав-гав". В терминах объектно-ориентированного программирования, "гав-гав" -- это класс, а объект, принадлежащий классу -- экземпляр класса.

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

class Dog
        def bark
                print "Гав гав\n"
        end
end
^D

Описание класса должно начинаться с ключевого слова class и заканчиваться ключевым словом end. Ключевое слово def в данном контексте начинает определение метода класса.

Итак, мы описали класс с именем `Dog', а теперь создадим объект.

tommy = Dog.new
tommy.bark
^D
Гав гав

Этот код создает новый экземпляр класса Dog и связывает его с переменной tommy. Метод `new' любого класса создает новый экземпляр этого класса. Теперь переменная tommy обладает свойствами класса Dog и может "прогавкать" (метод `bark').

Наследование

Вы когда нибудь задавались вопросом -- как разные люди классифицируют объекты? Например, как люди видят собаку? Математик может описать собаку как комбинацию чисел и геометрических фигур. Физик -- как результат работы естественных и искусственных сил. Моя сестра (биолог) может представить собаку как разновидность отряда псовых домашних (canine domesticus). Для нее собака -- это разновидность псовых, псовые -- разновидность млекопитающих, а млекопитающие -- всегда животные.

Отсюда видно, что классификация объектов имеет форму иерархии, хотя и не всегда. Посмотрим, как это можно реализовать в Ruby.

class Animal
        def breath
                print "Вдох и выдох\n"
        end
end
class Cat<Animal
        def bark
                print "мяу\n"
        end
end
tama = Cat.new
tama.breath
tama.bark
^D
Вдох и выдох
мяу

Из примера видно, что класс Cat не имеет определения метода breath (рус. - "дыхание", прим. перев.), но этот метод наследуется от родительского класса Animal. И имеет дополнительный специфичный метод `bark' (рус. - "лаять", здесь следует читать как "звук, издаваемый животным", прим. перев.).

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

При создании нового класса, наследующего черты родительского класса, нам необходимо будет только определить методы, описывающие различия между потомком и родителем. Это одно из достоинств объектно-ориентированного программирования, которое иногда называют "дифференциальным программированием".

Переопределение методов

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

class Human
        def print_id
                print "Я - человек.\n"
        end
        def train_toll(age)
                print "Детский билет.\n" if age < 12
        end
end
Human.new.print_id
class Student1<Human
        def print_id
                print "Я - студент.\n"
        end
end
Student1.new.print_id
class Student2<Human
        def print_id
                super
                print "И студент.\n"
        end
end
Student2.new.print_id
^D
Я - человек.
Я - студент.
Я - человек.
И студент.

В дочернем классе, переопределяющем метод родительского класса, можно вызвать оригинальный метод родителя с помощью ключевого слова `super'. Попробуйте запустить этот код:

class Student3<Human
        def train_toll(age)
                super(11) # принудительно снижаем возраст
        end
end
Student3.new.train_toll(25)
^D
Детский билет.

Надеюсь, этих простых примеров достаточно для того, чтобы понять принципы наследования и переопределения методов.

Еще о методах

Возможность вызова метода может быть ограничена. Для функции (определяемой на верхнем уровне) смотрите ниже:

def sqr(x)
        x * x
end
print sqr(5)
^D
25
Если определение функции находится за пределами описания класса, то она добавляется как метод класса Object. Класс Object является базовым для всех остальных классов -- в Ruby все классы являются потомками класса Object. Таким образом, метод `sqr' может вызываться из других классов.

Теперь, когда любой класс может вызвать метод `sqr', давайте попробуем вызвать его с помощью псевдопеременной `self':

print self.sqr(5)
^D
ERR: private method `sqr' called for (Object)

Как показано выше, вызов функции с помощью `self' приводит к выдаче сообщения об ошибке. Это сообщение недостаточно понятно (интуитивно), что оно означает?

Суть в том, что функции, определенные за пределами какого либо класса должны вызываться как простые функции, а не как методы. Посмотрите какое сообщение об ошибке получается при вызове не определенного метода.

Методы, определенные как простые функции, должны вызываться как простые функции, подобно функциям C++, даже в пределах класса.

Область видимости метода может быть ограничена ключевыми словами "public" и "private", где public -- определяет общедоступные методы класса, а private -- скрытые, которые могут быть вызваны только из других методов класса.

class Test
        def bar
                print "bar -< "
                foo
        end
        private
        def foo
                print "foo\n"
        end
end
temp = Test.new
temp.bar
temp.foo
^D
bar -< foo
ERR: private method `foo' called for (Test)

Из этого примера все должно быть понятно.

Единичные (Singleton) методы

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

class SingletonTest
        def size
                print "25\n"
        end
end
t1=SingletonTest.new
t2=SingletonTest.new
def t2.size
        print "10\n"
end
t1.size
t2.size
^D
25
10

Где t1 и t2 -- принадлежат одному и тому же классу и тем не менее, для экземпляра t2 переопределяется метод `size', обеспечивая его индивидуальность. Такой специфичный метод называется единичный метод (singleton method).

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

Модули

Модули в Ruby очень похожи на классы, но используются для группировки родственных классов в одном месте. Вот три основных отличия модулей от классов:

  1. Модули не имеют экземпляров модулей.
  2. Модули не могут иметь дочерние модули.
  3. Модули определяются конструкцией module ... end.

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

print Math::PI,"\n"
print Math.sqrt(2),"\n"
^D
3.141592654
1.414213562

Оператор `::' указывает на то, что константа определена в соответствующем модуле или классе. Чтобы обращаться к методам или константам напрямую, следует использовать директиву `include'

include Math
print sqrt(2),"\n"
print PI,"\n"
^D
1.414213562
3.141592654

Другая причина -- "смешение" (`mixin') модулей. Смысл этого термина достаточно сложен для понимания, поэтому остановимся на нем подробнее.

Некоторые объектно-ориентированные языки допускают наследование от нескольких родительских классов, такая возможность называется множественным наследованием. Ruby преднамеренно лишен этой возможности. Вместо этого он допускает смешение (mixin) с модулем.

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

Смешение модулей можно наблюдать в стандартной библиотеке, где в результате "смешения" модулей с классом, имеющим метод `each', возвращающий каждый элемент, последний получает в свое распоряжение методы `sort', `find' и т.п..

Между множественным наследованием и "смешением" существуют следующие отличия:

  • Модуль не может иметь экземпляр модуля, этой особенностью обладают только абстрактные классы.
  • Модуль хранит взаимоотношения экземпляров в виде дерева.

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

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

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

Процедурные объекты

Представьте себе, что вы пишите программу, которая обрабатывает некоторые сигналы. Знакомые с этим поймут всю прелесть передачи процедуры в виде аргумента методу (который занимается обработкой сигналов).

Процедурные объекты создаются с помощью встроенного метода proc. Исполняемый код записывается внутри фигурных скобок. Вызов на исполнение производится методом call процедурного объекта. Смотрите ниже:

obj = proc{print "Hello world\n"}
obj.call
^D
Hello world

C-программисты обнаружат сходство между процедурными объектами и указателями на функции.

Заключение

Эта часть является заключительной в серии. Цель серии была -- дать начальные сведения о программировании на языке Ruby. Я сейчас слишком занят своим дипломным проектом, чтобы выделить время на более глубокое изучение Ruby. Но в будущем, как только позволит время, я это продолжу.

Успехов в труде...

Hiran Ramankutty

Я -- студент последнего курса Правительственного Колледжа Компьютерных Наук в городе Трикур (Trichur), Индия. Кроме Linux, я с большим удовольствием занимаюсь изучением физики.

Copyright (C) 2003, Hiran Ramankutty. Copying license http://www.linuxgazette.com/copying.html
Published in Issue 86 of Linux Gazette, January 2003
Вернуться на главную страницу


Ruby

Ruby -- Руководство пользователя

Содержание

Ruby -- "простой объектно- ориентированный язык". Сначала это может показаться несколько странным, но он был спроектирован таким образом, чтобы программы на Ruby было легко и читать и писать. Это Руководство пользователя поможет вам научиться запускать и использовать Ruby, а также даст вам понимание природы Ruby, которое вы можете и не получить, прочитав Reference Manual.

Содержание

1. Что такое Ruby? | 15. Управление доступом
2. Поехали ! | 16. Singleton-методы
3. Простые примеры | 17. Модули
4. Строки | 18. Процедурные объекты
5. Регулярные выражения | 19. Переменные
6. Массивы | 20. Глобальные переменные
7. Снова к простым примерам | 21. Переменные экземпляра класса
8. Управляющие структуры | 22. Локальные переменные
9. Итераторы | 23. Константы класса
10. Объектно-ориентированное мышление | 24. Обработка исключительных ситуаций: rescue
11. Методы | 25. Обработка исключительных ситуаций: ensure
12. Классы | 26. Аксессоры
13. Наследование | 27. Инициализация объектов
14. Переопределение объектов | 28. Фишки

История документа

  • Оригинальная версия на японском языке -- matz.
  • Первоначальный перевод на английский -- GOTO Kentaro & Julian Fondren.
  • Дальнейшие перевод и сопровождение -- Mark Slagell. Date stamp для данной версии -- 20010216.
  • Akinori Musha, Laurent Julliard, Manpreet Singh и Robert Gustavsson помогли с отслеживанием ошибок и улучшением форматирования и представления. Спасибо!
  • Перевод на русский -- Alexander Miatchkov (Александр Мячков) с разрешения Mark Slagell.

Ruby -- руководство пользователя

Что такое Ruby?

Ruby -- это "интерпретируемый скриптовый язык для простого и быстрого объектно-ориентированного программирования". Что бы это значило?

интерпретируемый скриптовый язык:

  • Возможность прямого осуществления системных вызовов
  • мощная поддержка операций со строками и регулярными выражениями
  • немедленная обратная связь во время разработки

простое и быстрое программирование:

  • отсутствие необходимости объявления переменных
  • переменные не типизированы
  • простой и последовательный синтакс
  • автоматическое управление памятью

объектно - ориентированное программирование:

  • все является объектом
  • классы, наследование, методы и т.д.
  • singleton- методы
  • mixin при помощи модулей
  • итераторы и скобочные операции

а также:

  • целые числа с различной разрядностью
  • модель обработки исключительных ситуаций
  • динамическая загрузка
  • потоки

Если Вы не особо знакомы с представленными выше концепиями - не переживайте и читайте дальше. Мантра Ruby -- быстро и просто.

Ruby -- руководство пользователя

Поехали!

Для начала вы захотите проверить, установлен ли у вас Ruby. После символа подсказки в командной строке (он обозначен здесь как "%", так что % набирать не надо), введите

% ruby -v

(-v дает указание интерепретатору вывести его версию), затем нажмите кнопку Enter. Если Ruby установлен, вы увидите нечто похожее на:

% ruby -v
ruby 1.6.1 (2000-09-27) [i586-linux]

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

Теперь давайте поиграем с Ruby. Вы можете ввести код программы прямо в командной строке, используя параметр -e:

% ruby -e 'print "hello world\n"'
hello world

Более обычным является сохранение программы в файле.

% cat > test.rb
print "hello world\n"
^D
% cat test.rb
print "hello world\n"
% ruby test.rb
hello world

^D -- это комбинация control-D. вышеприведенное верно для UNIX. При использовании DOS, попробуйте так:

C:\ruby> copy con: test.rb
print "hello world\n"
^Z
C:\ruby> type test.rb
print "hello world\n"
C:\ruby> ruby test.rb
hello world

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

Некоторые на удивление сложные и полезные вещи могут быть сделаны с помощью миниатюрных программ, умещающихся в командной строке. Например, эта программа заменяет строку foo строкой bar во всех исходниках на C и в хидер-файлах в текущем рабочем каталоге, делая резервные копии первоначальных файлов в файлах с добавлением ".bak" в конце названия:

% ruby -i.bak -pe 'sub "foo", "bar"' *.[ch]

А эта программа работает как команда cat в UNIX (но работает медленнее, чем cat):

% ruby -pe 0 file

Ruby -- руководство пользователя

Простые примеры

Давайте напишем функцию для вычисления факториала. Математическое определение факториала от n следующее:

n! = 1                (n==0)
   = n * (n-1)!       (иначе)

На Ruby это может быть записано как:

def fact(n)
  if n == 0
    1
  else
    n * fact(n-1)
  end
end

Вы можете заметить повторение в появлении end. Ruby из-за этого называют "Algol-like". (На самом деле, синтакс Ruby более повторяет синтакс языка под названием Eiffel.) Вы также можете заметить отсутствие оператора return. В нем нет необходимости, поскольку функция в Ruby возвращает последнее вычисленное в ней значение. Использование оператора return допустимо, но необходимым не является.

Теперь давайте опробуем нашу функцию вычисления факториала. Добавление одной строчки кода дает нам рабочую программу:

Программа для нахождения факториала от числа
# запишите ее в файл fact.rb

def fact(n)
  if n == 0
    1
  else
    n * fact(n-1)
  end
end

print fact(ARGV[0].to_i), "\n"

ARGV здесь - это массив, содержащий аргументы из командной строки, а to_i преобразует строку символов в численное представление.

% ruby fact.rb 1
1
% ruby fact.rb 5
120

Будет ли программа работать для аргумента, равного 40? Это вызвало бы переполнение при вычислениях на калькуляторе...

% ruby fact.rb 40
815915283247897734345611269596115894272000000000

Работает. На самом деле, Ruby может справляться с любыми целыми числами, дозволенными размерами памяти вашей машины. Так что и 400! может быть посчитан:

% ruby fact.rb 400
64034522846623895262347970319503005850702583026002959458684
44594280239716918683143627847864746326467629435057503585681
08482981628835174352289619886468029979373416541508381624264
61942352307046244325015114448670890662773914918117331955996
44070954967134529047702032243491121079759328079510154537266
72516278778900093497637657103263503315339653498683868313393
52024373788157786791506311858702618270169819740062983025308
59129834616227230455833952075961150530223608681043329725519
48526744322324386699484224042325998055516106359423769613992
31917134063858996537970147827206606320217379472010321356624
61380907794230459736069956759583609615871512991382228657857
95493616176544804532220078258184008484364155912294542753848
03558374518022675900061399560145595206127211192918105032491
00800000000000000000000000000000000000000000000000000000000
0000000000000000000000000000000000000000000

Мы не можем навскидку проверить правильность результата, но, должно быть, он правильный. :-)

Цикл ввод/обработка

Когда Вы запускаете Ruby без аргументов, он считывает команды со стандартного устройства ввода и исполняет их по окончании ввода:

% ruby
print "hello world\n"
print "good-bye world\n"
^D
hello world
good-bye world

Ruby также поставляется с программой под названием eval.rb, которая позволяет вводить код на Ruby с клавиатуре в интерактивном цикле, показывая по ходу дела результаты его обработки. Она будет широко использоваться до конца тьюториала.

Если у Вас ANSI-совместимый терминал (это наверняка так и есть, если вы работаете на каком-либо из клонов UNIX, в DOS вам потребуются установленные драйвера ANSI.SYS или ANSI.COM), вам стоит использовать расширенный eval.rb, который добавляет визуальную помощь в расстановке отступов, выдает предупреждения, обеспечивает подсветку синтаксиса. В ином случае ищите в каталоге sample в каталоге установкии дистрибутива версию без поддержки ANSI. Вот короткий пример сессии работы с eval.rb:

% ruby eval.rb
ruby> print "Hello, world.\n"
Hello, world.
   nil
ruby> exit

hello world выводится функцией print. Следующая строка, в данном случае nil, сообщает о результатах выполнения последней команды; Ruby не делает различий между оператором and выражением, так что обработка куска кода просто означает то же самое, что и его исполнение. В данном случае, nil показывает, что print не вернул никакого определенного значения. Заметьте, что из интерепретатора можно выйти при помощи команды exit, хотя ^D тоже работает.

Далее в этом руководстве "ruby>" означает приглашение для ввода нашей маленькой полезной программки eval.rb.

Ruby -- руководство пользователя

Строки

Ruby работает как с числами, так и со строками. Строка может быть заключена в двойные ("...") или одинарные ('...') кавычки.

ruby> "abc"
   "abc"
ruby> 'abc'
   "abc"

Двойные и одинарные кавички в некоторых случаях производят разный эффект. Строка в двойных кавычках разрешает экранирование символов после лидирующего обратного слэша, а также вычисление вложенных выражений при использовании #{}. Строки в одинарных кавычках не производят такой интерпретации; что видим, то и получаем. Примеры:

ruby> print "a\nb\nc","\n"
a
b
c
   nil
ruby> print 'a\nb\nc',"\n"
a\nb\nc
   nil
ruby> "\n"
   "\n"
ruby> '\n'
   "\\n"
ruby> "\001"
   "\001"
ruby> '\001'
   "\\001"
ruby> "abcd #{5*3} efg"
   "abcd 15 efg"
ruby> var = " abc "
   " abc "
ruby> "1234#{var}5678"
   "1234 abc 5678"

Оперирование со строками в Ruby более умное и интуитивное, чем в С. Например, вы можете соединять строки при помощи +, или повторять строку несколько раз при помощи *:

ruby> "foo" + "bar"
   "foobar"
ruby> "foo" * 2
   "foofoo"

Объединение строк на С гораздо более неудобное, поскольку оно требует явного управления памятью:

char *s = malloc(strlen(s1)+strlen(s2)+1);
strcpy(s, s1);
strcat(s, s2);
/* ... */
free(s);

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

Вот еще некоторые примеры того, что можно делать со строками.

Объединение:

ruby> word = "fo" + "o"
   "foo"

Повторение:

ruby> word = word * 2
   "foofoo"

Вычленение символов (Отметьте, что символы в Ruby являются целыми числами)

ruby> word[0]
   102            # 102 is ASCII code of `f'
ruby> word[-1]
   111            # 111 is ASCII code of `o'

(Отрицательный индекс начинает отсчет не с начала, а с конца строки)

Выделение подстрок:

ruby> herb = "parsley"
   "parsley"
ruby> herb[0,1]
   "p"
ruby> herb[-2,2]
   "ey"
ruby> herb[0..3]
   "pars"
ruby> herb[-5..-2]
   "rsle"

Проверка на равенство:

ruby> "foo" == "foo"
   true
ruby> "foo" == "bar"
   false

Замечание: в Ruby 1.0 приведенные выше результаты представлены в верхнем регистре, т.е. TRUE.

А теперь попробуем некоторые из этих свойств в работе. Это будет пазл "Угадай слово", хотя паззл, возможно, звучит чересчур величественно для нашей поделки ;-)

# сохраните в файл guess.rb
words = ['foobar', 'baz', 'quux']
secret = words[rand(3)]

print "guess? "
while guess = STDIN.gets
  guess.chop!
  if guess == secret
    print "You win!\n"
    break
  else
    print "Sorry, you lose.\n"
  end
  print "guess? "
end
print "The word was ", secret, ".\n"

Не обращайте пока что слишком много внимания на детали этой программы. Вот как выглядит вывод пазла:

% ruby guess.rb
guess? foobar
Sorry, you lose.
guess? quux
Sorry, you lose.
guess? ^D
The word was baz.

(С учетом вероятности успеха 1/3, могло бы быть и лучше)

Ruby -- руководство пользователя

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

Давайте-ка скомпонуем более интересную программу. На этот раз мы будем проверять, удовлетворяет ли строка некоторому набору свойств, который мы будем называть шаблоном.

Вот некоторые из символов и их сочетаний, которые имеют специальное значение при использовании в шаблонах:

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

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

Пусть, например, мы хотим проверить, удовлетворяет ли строка следующему описанию: "Начинается с f в нижнем регистре, сразу за ней следует Буква в верхнем регистре, потом что угодно, при условии, что буквы в нижнем регистре отсутствуют." Если Вы опытный программист на С, то Вы уже, вероятно, написали в своей голове примерно дюжину строк кода, так?? Поверьте, это едва ли поможет Вам. Но в Ruby Вам нужен всего лишь один запрос на соответствие строки регулярному выражению /^f[A-Z][^a-z]*$/.

А как насчет "Содержит шестнадцатеричное число, заключенное в угловые скобки?" Пожалуйста:

ruby> def chab (s)   # "contains hex in angle brackets"
    |    (s =~ /<0[Xx][\dA-Fa-f]+>/) != nil
    | end
  nil
ruby> chab "Not this one."
  false
ruby> chab "Maybe this? {0x35}"    # wrong kind of brackets
  false
ruby> chab "Or this? <0x38z7e>"    # bogus hex digit
  false
ruby> chab "Okay, this: <0xfc0004>."
  true

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

Вот маленькая программа, чтобы помочь Вам поэкспериментировать с регулярными выражениями. Запишите ее как regx.rb и запустите командой "ruby regx.rb" в командной строке.

# Необходим терминал с поддержкой ANSI !

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"

Нужно ввести 2 строки - в первой должна содержаться строка, а во второй регулярное выражение, на соответствие которому и проверяется строка. После проверки строка выводится на экран, причем все ее совпавшие части печатаются в инверсном режиме. Не забивайте себе голову деталями сейчас, скоро будет произведен анализ этого кода.

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

ТО, что выше наисано красным, на экране выглядит как инвертированное. "~~~~~" для тех, кто пользуется браузером в текстовом режиме.

Давайте попробуем другие сочетания.

str> abc012dbcd555
pat> \d
abc012dbcd555
   ~~~    ~~~

Если то, что выводит программа, для Вас является неожиданностью, то посмотрите внимательно на таблицу вверху этой страницы: \d не имееет никакого отношения к символу d, вместо этого он означает одиночную цифру.

А что, если имеется несколько способов удовлетворить условиям, накладываемым шаблоном?

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
           ~~~~~~~~

"=~" это оператор выполнения сравнения по шаблону; он возвращает позицию в строке, начиная с которой найдено совпадение, или nil в случае, если таковое не найдено.

ruby> "abcdef" =~ /d/
   3
ruby> "aaaaaa" =~ /d/
   nil

Ruby -- руководство пользователя

Массивы

Вы можете создать массив, перечислив несколько его элементов в квадратных скобках ([]) и разделяя их запятой. Массивы в Ruby могут включать в себя элементы разных типов.

ruby> ary = [1, 2, "3"]
   [1, 2, "3"]

Массвы могут быть повторены или объединены точно так же, как и строки.

ruby> ary + ["foo", "bar"]
   [1, 2, "3", "foo", "bar"]
ruby> ary * 2
   [1, 2, "3", 1, 2, "3"]

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

ruby> ary[0]
   1
ruby> ary[0,2]
   [1, 2]
ruby> ary[0..1]
   [1, 2]
ruby> ary[-2]
   2
ruby> ary[-2,2]
   [2, "3"]
ruby> ary[-2..-1]
   [2, "3"]

(Отрицательные индексы обозначают относительное смещение считая с не с начала массива, а с конца.)

Массивы могут быть преобразованы в строки и из строк, используя join и split соответственно:

ruby> str = ary.join(":")
   "1:2:3"
ruby> str.split(":")
   ["1", "2", "3"]

Хешы

Ассоциативный массив имеет элементы, доступ к которым осуществляется не по порядковому номену (индексу), а по ключу keys, который может быть значением любого типа. Такие массывы иногда называют хеш или словарь; в мире Ruby предпочитают название хэш. Хеш может быть создан путем перечисления пар в круглых скобках ({}). Для поиска чего-либо в хеше используется ключ, так как если бы вы использовали индекс для поиска в в массиве.

ruby> h = {1 => 2, "2" => "4"}
   {1=>2, "2"=>"4"}
ruby> h[1]
   2
ruby> h["2"]
   "4"
ruby> h[5]
   nil
ruby> h[5] = 10     # добавляем значение
   10
ruby> h
   {5=>10, 1=>2, "2"=>"4"}
ruby> h[1] = nil    # удаление значения
   nil
ruby> h[1]
   nil
ruby> h
   {5=>10, "2"=>"4"}

Ruby -- руководство пользователя

Снова к простым примерам

Давайте теперь рассмотрим код некоторых примеров, показанных выше. Для ссылок мы во всех скриптах будем использовать номера строк.

Факториал

Этот скрипт был представлен в главе "Простые примеры".

01  def fact(n)
02    if n == 0
03      1
04    else
05      n * fact(n-1)
06    end
07  end
08  print fact(ARGV[0].to_i), "\n"

Поскольку это первое рассмотрение, мы проанализируем отдельно каждую строку.

01  def fact(n)

В первой строке def - это оператор, используемый для определения функции (или, точнее, метода; о том, что такое метод, мы расскажем в последующих главах). Здесь он определяет, что функции fact передается один аргумент, обозначенный как n.

02    if n == 0

if предназначено для проверки условия. Если условие выполняется, то просчитывается следующая строка; иначе выполняется то, что следует за else.

03      1

Значение, возвращаемое if, равно 1, если условие выполняется.

04    else

Если условие не выполняется, то вычисляется блок кода отсюда до end.

05      n * fact(n-1)

Если условие ны выполняется, то результатом if будет значение, равное n умноженное на fact(n-1).

06    end

Первое "end" закрывает оператор if.

07  end

Второе "end" закрывает оператор def.

08  print fact(ARGV[0].to_i), "\n"

В этой строке происходит вызов функции fact() с передачей в нее значения, определенного в командной строке, затем выводится результат.

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

Гммм... А что произойдет, если мы скормим программе в качестве параметра отрицательное число? Видите проблему? А сможете исправить?

Строки

Теперь мы рассмотрим программу-пазл из главы, посвященной строкам.

01  words = ['foobar', 'baz', 'quux']
02  secret = words[rand(3)]
03
04  print "guess? "
05  while guess = STDIN.gets
06    guess.chop!
07    if guess == secret
08      print "you win\n"
09      break
10    else
11      print "you lose.\n"
12    end
13    print "guess? "
14  end
15  print "the word is ", secret, ".\n"

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

rand(3) в строке 2 возвращает случайную величину в диапазоне от 0 до 2 включительно. Это случайное значение используется для определения одного из элементов массива words.

В строке 5 мы считываем одну строку из со стандартного ввода при помощи метода STDIN.gets. Если получаем EOF (end of file - конец файла) во время считывания, gets возвращает nil. Таким образом, выполнение блока внутри while продолжается до тех пор, пока не появится ^D (или ^Z в DOS), означающее конец ввода.

guess.chop! в строке 6 удаляет последний символ из guess; в нашем случае это всегда будет символ newline (новая строка).

В строке 15 мы выводим задуманное слово. Мы записали это в виде оператора print с тремя аргументами (которые выводятся друг за другом), но было бы так же эффективно записать его с одним аргументом, написав вывод secret в виде #{secret} с целью ясно обозначить то, что это переменная, вместо которой должно быть подставленоо ее значение, а не просто строка символов для вывода:

print "the word is #{secret}.\n"

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

В конце концов рассмотрим программу из главы regular expressions.

01  st = "\033[7m"
02  en = "\033[m"
03
04  while TRUE
05    print "str> "
06    STDOUT.flush
07    str = gets
08    break if not str
09    str.chop!
10    print "pat> "
11    STDOUT.flush
12    re = gets
13    break if not re
14    re.chop!
15    str.gsub! re, "#{st}\\&#{en}"
16    print str, "\n"
17  end
18  print "\n"

В строке 4 вместо условия в while жестко зашито значение true, так что плучается нечто, выглядящее как бесконечный цикл. Тем не менее, мы вставляем оператор break в 8 и 13 строки для обеспечения возможности выхода из цикла. Эти два break также являются примерами модификаторов оператора if. "if-модификатор" позволяет выполнение кода слева от себя тогда и только тогда, когда заданное условие выполняется.

Добавление по поводу chop! (см. строки 9 и 14). В Ruby мы обычно добавляем '!' или '?' в конце названий определенных методов. Восклицательный знак (!, иногда поизносимый как "bang!") означает нечто потенциально деструктивное, то есть нечто, способное изменить значение того, над чем оно работает. chop! прямо воздействует на исходную строку, но chop без восклицательного знака работает с копией. Ниже проиллюстрировано это различие.

ruby> s1 = "forth"
  "forth"
ruby> s1.chop!       # Изменяется s1.
  "fort"
ruby> s2 = s1.chop   # Измененная копия перекидывается s2,
  "for"
ruby> s1             # ... без воздействия на s1.
  "fort"

Позднее Вы столкнетесь с именами методов, оканчивающимися на вопросительный знак (?, иногда произносимый как "huh?"); это означает предикатный метод, который может возвратить true либо false.

Строке 15 необходимо уделить особое внимание. Во-первых, отметьте наличие еще одного так называемого деструктивного метода gsub!. Он изменяет в str все подходящие под шаблон re последовательности (sub означает замена - substitute, а лидирующее g означает глобальное, т.е., заменяяются все подходящие части строки, а не только первую совпавшую). Чем дальше, тем лучше; но чем же мы заменяем совпавшие части текста? st и en были определены ANSI-последовательности для инвертирования текста и приведения его к обычному виду соответственно. В строке 15 они заключены в #{} для обеспечения того, что они будут интерпретированны именно таким образом (и мы не видим неправильно выведенные имена переменных). Между ними мы видим "\\&". Небольшая хитрость. Поскольку строка, в которой производится замена, прописана в двойных кавычках, то пара обратных слешей будет интерпретироваться как один обратный слеш; то, что gsub! получит на самом деле, будет выглядеть как "\&", и это оказывается особым кодом, который ссылается на первое, попавшее под шаблон. Таким образом, новая строка при печати будет выглядеть точно так же, как и старая, за исключением того, что учаски, совпавшие с шаблоном, будут выведены в инверсном виде.

Ruby -- руководство пользователя

Управляющие структуры

В этой главе более конкретно рассматриваются управляющие структуры Ruby.

case

Мы используем оператор case для проверки последовательности условий. Это примерно похоже на оператор switch в C и Java, но по сравнению с ним, как мы увидим далее, более мощный.

ruby> i=8
ruby> case i
    | when 1, 2..5
    |   print "1..5\n"
    | when 6..10
    |   print "6..10\n"
    | end
6..10
   nil

2..5 - это выражение, которое означает диапазон между 2 и 5 включительно. Следующее выражение означает проверку на вхождение значения i в диапазон:

(2..5) === i

case внутри использует оператор отношения === для проверки нескольких условий за раз. Придерживаясь объектно-ориентированной природы Ruby, === интерпретируется соответственно объекту, находящемуся в условии when. Например, следующий код проверяет строки на равенство в первом when, и соответствие регулярному выражению во втором when.

ruby> case 'abcdef'
    | when 'aaa', 'bbb'
    |   print "aaa or bbb\n"
    | when /def/
    |   print "includes /def/\n"
    | end
includes /def/
   nil

while

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

while - это как бы повторяющийся if. Мы использовали его в программе-пазле, угадываюшем слова и в программе (смотри предыдущую главу); там этот оператор выглядел как while условие ... end с блоком кода, выполнявшимся до тех пор, пока условие выполнялось (т.е. результатом выполнения условия было true). Но while и if также может быть легко применено и для отдельного выражения:

ruby> i = 0
  0
ruby> print "It's zero.\n" if i==0
It's zero.
  nil
ruby> print "It's negative.\n" if i<0
  nil
ruby> print "#{i+=1}\n" while i<3
1
2
3
   nil

Иногда желательно инвертировать результат выполнения проверки условия . unless - это обратное для if, а until - обратное для while. Мы оставляем Вам проверку того, как это работает. Поэкспериментируйте.

Существует 4 способа прервать выполнение цикла внутри него. Первый, break выполняет, как и в C, полный выход из цикла. Второй, next выполняет переход на начало следующей итерации (так же, как и continue в С). Третий - в Ruby есть оператор redo, который повторно выполняет текущую итерацию. Приведенный ниже код на С иллюстрирует действие операторов break, next, и redo:

while (condition) {
 label_redo:
   goto label_next        /* next */
   goto label_break       /* break */
   goto label_redo        /* redo */
   ;
   ;
 label_next:
}
label_break:
;

Четвертым способом выбраться из цикла является return. При выполнении return произходит не только выход из тела цикла, но также и возврат из метода, содержащего данный цикл. Если задан аргумент, то он будет его значение будет передано вызывающему методу, иначе возвращается nil.

for

Программистам на С уже, наверное, интересно, как же делаются циклы с "for". В Ruby for несколько более интересен, чем можно было бы предположить. Нижеприведенный цикл выполняется для каждого элемента из коллекции:

for elt in collection
  ...
end

Коллекция может быть представлена как диапазон значений (это как раз то, что подразумевает большиство людей, говоря о циклах "for"):

ruby> for num in (4..6)
    |    print num,"\n"
    | end
4
5
6
   4..6

Это также может быть некоторыми другими выдами коллекций, такими как массив:

ruby> for elt in [100,-9.6,"pickle"]
    |    print "#{elt}\t(#{elt.type})\n"
    | end
100    (Fixnum)
-9.6   (Float)
pickle (String)
   [100, -9.6, "pickle"]

Но мы збегаем несколько вперед. for, на самом деле, является другим способом записи each, который - так получилось - будет нашим первым примером использования итераторов. Две нижеприведенных примера являются эквивалентными:

#  If you're used to C or Java, you might prefer this.
for i in collection
  ...
end

#  A Smalltalk programmer might prefer this.
collection.each {|i|
  ...
}

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

Ruby -- руководство пользователя

Итераторы

Итераторы не являются чем-то, свойственным только Ruby. Они общеупотребительны в объектно-ориентированных языках программирования. Они также есть в Lisp, хотя там они не называются итераторами. Как бы то ни было, концепция итераторов не является близкой множеству людей, так что давайте рассмотрим ее более детально.

Глагол iterate (повторять) означает "проделывать одно и то же много (ну, несколько) раз, так что итератор (iterator) - это нечто, делающее одно и то же много раз.

Когда мы программируем, то нуждаемся в использовании циклов во множестве различных ситуаций. В С мы кодируем их, используя for или while. Например,

char *str;
for (str = "abcdefg"; *str != '\0'; str++) {
  /* process a character here */
}

Синтакс оператора C for(...) обеспечивает абстракцию, помогающую в создании циклов, но для проверки *str на null необходимо знание программистом деталей о внутреннем представлении строки. Это дает представление о С как о низкоуровневом языке программирования. Высокоуровневые языки отличает более гибкая поддержка ими итераторов. Посмотрите на скрипт sh :

#!/bin/sh

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

Обрабатываются все исходники на С и хидеры в текущем каталоге , а shell заботится о деталях получения и подстановки имен файлов один за одним. Думаю, это работает на несколько более высоком уровне, чем С, а?

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

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

В Ruby для типа String имеется несколько полезных итераторов:

ruby> "abc".each_byte{|c| printf "<%c>", c}; print "\n"
<a><b><c>
   nil

each_byte - итератор для каждого символа в строке. Каждый символ подставляется в локальную переменную c. Это может быть представлено как нечто, весьма напоминающее код на С...

ruby> s="abc";i=0
   0
ruby> while i<s.length
    |    printf "<%c>", s[i]; i+=1
    | end; print "\n"
<a><b><c>
   nil

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

Вот еще итератор для String - each_line.

ruby> "a\nb\nc\n".each_line{|l| print l}
a
b
c
   nil

С задачами, которые потребовали бы больших усилий при программировании на С (поиск разделителей строк, выделение подстрок, и т.д.), легко справиться, используя итераторы.

Оператор for, использованный в предыдущей главе, делает итерации тем же путем, что и итератор each. each для String делает то же самое, что и each_line, так что давайте перепишем предыдущий пример с for:

ruby> for l in "a\nb\nc\n"
    |   print l
    | end
a
b
c
   nil

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

ruby> c=0
   0
ruby> for i in 0..4
    |   print i
    |   if i == 2 and c == 0
    |     c = 1
    |     print "\n"
    |     retry
    |   end
    | end; print "\n"
012
01234
   nil

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

ruby> def repeat(num)
    |   while num > 0
    |     yield
    |     num -= 1
    |   end
    | end
   nil
ruby> repeat(3) { print "foo\n" }
foo
foo
foo
   nil

При помощи retry, можно определить итератор, который будет работать так же, как и while, хотя это слишком медленно, чтобы быть практичным.

ruby> def WHILE(cond)
    |   return if not cond
    |   yield
    |   retry
    | end
   nil
ruby> i=0; WHILE(i<3) { print i; i+=1 }
012   nil

Понятно, что такое итератор? Есть некоторые ограничения, но Вы можете написать собственные итераторы; фактически, когда Вы создаете новый тип данных, то часто удобно определить подходящий для него итератор. В этом смысле приведенные выше примере не слишком полезны. Мы сможем говорить о практическом применении итераторов после того, как будем иметь лучшее представление о том, что же такое классы.

Ruby -- руководство пользователя

Объектно-ориентированный подход

"Объектно-ориентированный" - какое же это заразное выражение. Назовите что-то "объектно-ориентированным" - и Ваша фраза будет оччень умно. О Ruby говорят как об объектно-ориентированном языке; а что же точно означает "Объектно-ориентированный"?

Есть множество вариантов ответа на этот вопрос, но все они, вероятно, сводятся к одному и тому же. Чем слихком быстро их просуммировать, давайте немного поразмышляем о парадигме традиционного программирования

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

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

Что нам дает объектно-ориентированное программирование - оно позволяет перенести повторяющуюся логику и механизмы взаимодействия на сами данные; оно меняет концепцию даннык с пассивной на активную. Иначе говоря,

  • Мы более не рассматриваем данные как некоторую коробку с откытой крышкой, которая позволяет нам заглянуть вовнутрь и пожевать содержимое.
  • ы начинаем рассматривать данные как некий закрытый механизм с четко определенными рычажками и индикаторами.

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

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

Давайте нанем с примера; он слишком прост, чтобы быть практичным, однако способен, по крайней мере, частично проиллюстрировать саму концепцию. В Вашей машине есть одометр. Его работа заключается, среди прочего, в том, чтобы подсчитывать расстояние, пройденное с момента последнего нажатаи я его кнопки сброса. Как это можно смоделировать на языке программирования? На С одоиметр можно представить просто числовой переменной, возможно, с типом float. Такая программа будет работать с этой переменной увеличивая ее значение маленькими шагами, сбрасывая ее значение в ноль при необходимости. Что же здесь не так? Из-за ошибки в программе этой переменной может быть присвоено неверное значение просто по воле случая. Любой, кто программировал на С, знает как это часами или целыми днями выслеживал подобные ошибки, которые при обнаружении оказываются абсурдно элементарными. (Момент обнаружения бага обычно прослеживается по звуку шлепка по лбу.)

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

Мы не даем возможности установить произвольное значение - почему? А потому что, как все мы знаем, одометр не работает таким образом. Есть только определенные действия, которые разрешено проводить с одометром. Таким образом, если что-то в программе пытается установить некоторое посторонне значение (скажем, целевую температуру системы климат-контроля машины) в одометр, немедленно станет ясно, что происходит что-то не то. Во время выполнения программы (или, возможно, во время компиляции, - зависит от вида языка) нам будет указано, что недопустимо присваивать объекту Tripmeter произвольные значения. Сообщение может быть не настолько ясным, но достаточно близким к этому. Это не предотвращает ошибки, не так ли? Но это быстро укажет нам направление поиска причины. Это только один из способов, которым ОО программирование может предотвратить кучу впустую потраченного времени.

Мы обычно поднимаемся на уровень абстракции выше чем здесь, Потому как оказывается удобным построить factory, которая производит подобные устройства, чем создавать их поодиночке. Маловероятно, что мы прямо будем создавать объект одометр; вместо этого мы предпочтем, чтобы нужное их количество было построено по пределенному шаблону. Шаблон (или, если Вам больше нравится, tripmeter factory) соотносится с тем, что называют "классом", а отдельно взятый одометр соотносится с "объектом". В большинстве ОО языков требуется, чтобы класс был определен до того, как мы можем получить объект нового типа, но не в Ruby.

Здесь ничего не сказано о том, что использование ОО языка вынуждает создавать хороший ОО дизайн. В самом деле, на любом языке можно написать насквозь нечитаемую, неаккуратную, плохо продуманную, глючную и неустойчивую в работе программу. Что Ruby дает Вам (особо по сравнению с С++), так это то что, когда Вы прорабатываюте детали, то не чувствуется желания ухудшить стиль для сохранения усилий. По ходу повествования мы будем обсуждать как же Ruby идет к этой прекрасной цели. Следующей темой будет "кнопочки и рычажки" (методы объектов), от которых мы перейдем к "фабрикам" (классам). Вы еще здесь?

Ruby -- руководство пользователя

Методы

Что есть метод? В ОО программировании мы не думаем опрямом изменении данных объекта снаружи; вместо этого объекты сами знают о как с ними нужно работать (когда их об этом попросят в соответствующей форме.) Вы можете сказать, что мы шлем сообщения объекту, и эти сообщения в общем случае, вызывают некоторые действия или осмысленный отклик. Это должно происходить независимо от нашего представления или щаботы о том, как на самом деле действует объект. Действия, которые нам разрешено запрашивать у объекта (или, что равнозначно, сообщения, которые он понимает) являются методами объекта

В Ruby мы производим вызов метода, используя точечную нотацию (так же, как в С++ или Java). Имена объектов, чьи методы мы вызываем, располагаются справа от точки.

ruby> "abcdef".length
   6

Интуитивно, этот строковый объект запрашивается о его длине.. Технически, мы вызываем метод length объекта "abcdef".

Другие объекты могут иметь несколько иное представление о length, или вообще никакого. Решение, как отреагировать на сообщение, принимается на лету, во время исполнения программы, и принимаемые действия могут меняться в зависимости от того, на что ссылается переменная.

ruby> foo = "abc"
   "abc"
ruby> foo.length
   3
ruby> foo = ["abcde", "fghij"]
   ["abcde", "fghij"]
ruby> foo.length
   2

Что мы имеем в виду под длиной может варьироваться в зависимости от того, о каком объекте мы говорим. В первый раз, когда мы запрашиваем foo о его длине в предыдущем примере, он ссылается на простую строку, и здесь может быть только один осмысленный ответ. Во второй раз foo ссылается на массив, и есть причины думать, что результатом будет 2, 5, or 10; но наиболее подходящим ответом будет, конечно, 2 (другие результаты также могут быть, по желанию, получены) .

ruby> foo[0].length
   5
ruby> foo[0].length + foo[1].length
   10

Необходимо отметить, что массив что-то знает о том, что значит быть массивом. Данные в Ruby несут такое знание, так что при необходимости запрос на них автоматически может быть удовлетворен несколькими способами. Это освобождает программиста от бремени запоминания множества специфичных имен функций, поскольку относительно небольшое число названий методов, соотносящихся с концепцией того, что мы знаем, как выразить на естественном языке, может быть применено к различным тиам данных, и результатом будет то, что мы предполагаем. Это свойство ОО языков программирования (которое, IMHO, Java использует не лучшим образом) называется полиморфизм.

Когда объект получает сообщение, которое он не понимает, "возбуждается" ошибка:

ruby> foo = 5
   5
ruby> foo.length
ERR: (eval):1: undefined method `length' for 5(Fixnum)

Таким образом, необходимо знать, какие методы принимаются объектом, несмотря на то, что нам не нужно знать, как они обрабатываются.

Если методу передаются аргументы, они обычно заключаются в скобки,

object.method(arg1, arg2)

но их можно опустить, если этим мы не вносим неоднозначность.

object.method arg1, arg2

В Ruby существует особая переменная self; она ссылается на вызывающий метод объект. Такое происходит настолько часто, что для удобства "self." может быть опущен, когда объект вызывает собственные методы:

self.method_name(args...)

есть то же самое, что и

method_name(args...)

Как можно подумать, вызовы функций являются просто укороченной записью вызова методов объекта self. Это делает Ruby тем, что называется чисто объектно-ориентированным языком. Все же функциональные методы ведут себя подобно функциям в других языках программирования для тех, кто не хочет понимать, что вызовы функций также на самом деле являются методами объектов Ruby. Мы можем говорить о функциях как если бы они не были методами объектов, если нам уж так этого хочется.

Ruby -- руководство пользователя

Классы

Реальный мир полон объектов, и мы можем их классифицировать. Например, при виде собаки младенец, вероятно, скажет "bow-wow" независимо от породы; и мы в самом деле можем рассматривать окружающий мир в данных категориях.

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

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

ruby> class Dog
    |   def speak
    |     print "Bow Wow\n"
    |   end
    | end
   nil

В Ruby определением класса заключаеься в ограничители class и end. def внутри этого раздела открывает определение метода класса, которое, как мы рассмотрели ранее, определяет некоторое специфическое поведение объектов данного класса.

Теперь, после определения класса Dog, мы можем использовать его для создания объекта "собака".

ruby> pochi = Dog.new
   #<Dog:0xbcb90>

Мы создали новый объект класса Dog, и дали ему имя pochi. Метод new любого класса создает объект данного класса. Поскольку pochi принадлежит классу Dog в соответствии с нашим определением класса он имеет все свойства, которые мы определили для класса Dog. Т.к. наша идея Dog-ности была очень простой, есть только одно действие, которое pochi можо попросить выполнить.

ruby> pochi.speak
Bow Wow
   nil

Создание нового объекта класса иногда называется инстанциированием этого класса. нам нужно получить собаку, прежде чем мы будем иметь удовольствие поговорить с ней; мы не можем просто попросить class Dog погавкать для нас.

ruby> Dog.speak
ERR: (eval):1: undefined method `speak' for Dog:class

В этом не больше смысла, чем пытаться съесть концепцию бутерброда.

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

ruby> (Dog.new).speak   # or more commonly, Dog.new.speak
Bow Wow
   nil

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

Ruby -- руководство пользователя

Наследование

Наша классаификация объектов в повседеневной жизни естественно иерархична. Мы знаем, что все кошки есть млекопитающие, и все млекопитающие есть животные. Более мелкие классы наследуют характеристики более крупых, к которым они принадлежат.

Мы можем отразить эту концепцию в Ruby:

ruby> class Mammal
    |   def breathe
    |     print "inhale and exhale\n"
    |   end
    | end
   nil
ruby> class Cat<Mammal
    |   def speak
    |     print "Meow\n"
    |   end
    | end
   nil

Хотя мы явно не определили, как именно Cat должен дышать (breathe), каждый кот наследует поведение класса Mammal, поскольку Cat был определен как подкласс Mammal. (В ОО терминологии меньший класс является подклассом а больший (более общий) класс является суперклассом.) Следовательно, с точки зрения программиста, кошки (cats) бесплатно получают возможность дышать (breathe); после добавления метода speak method, наши кошки могут и дышать (breathe) и говорить (speak).

ruby> tama = Cat.new
   #<Cat:0xbd80e8>
ruby> tama.breathe
inhale and exhale
   nil
ruby> tama.speak
Meow
   nil

Бывают ситуации, когда некоторые свойства суперкласса не должны наследоваться определенным подклассом. Хотя обычно птицы знают как летать, пингвины являются нелетающим подклассом птиц.

ruby> class Bird
    |   def preen
    |     print "I am cleaning my feathers."
    |   end
    |   def fly
    |     print "I am flying."
    |   end
    | end
   nil
ruby> class Penguin<Bird
    |   def fly
    |     fail "Sorry. I'd rather swim."
    |   end
    | end
   nil

Вместо того, чтобы полностью определять все свойства каждого новото класса, мы можем просто добавить или переопределить их в соответствии с различиями между подклассом и суперклассом. Это использование наследования иногда называют differential programming. Это одно из преимуществ объектно-ориентированного программирования.

Ruby -- руководство пользователя

Переопределение методов

В подклассе мы можем переопределить поведение метода суперкласса его переопределением.

ruby> class Human
    |   def identify
    |     print "I'm a person.\n"
    |   end
    |   def train_toll(age)
    |     if age < 12
    |       print "Reduced fare.\n";
    |     else
    |       print "Normal fare.\n";
    |     end
    |   end
    | end
   nil
ruby> Human.new.identify
I'm a person.
   nil
ruby> class Student1<Human
    |   def identify
    |     print "I'm a student.\n"
    |   end
    | end
   nil
ruby> Student1.new.identify
I'm a student.
   nil

Предположим, мы хотим расширить метод identify суперкласса вместо его полной замены. . Для этого используется super.

ruby> class Student2<Human
    |   def identify
    |     super
    |     print "I'm a student too.\n"
    |   end
    | end
   nil
ruby> Student2.new.identify
I'm a human.
I'm a student too.
   nil

super позволяет передавать аргументы первоначальному методу. Иногда говорят, что существуют два типа людей...

ruby> class Dishonest<Human
    |   def train_toll(age)
    |     super(11) # we want a cheap fare.
    |   end
    | end
   nil
ruby> Dishonest.new.train_toll(25)
Reduced fare.
   nil

ruby> class Honest<Human
    |   def train_toll(age)
    |     super(age) # pass the argument we were given
    |   end
    | end
   nil
ruby> Honest.new.train_toll(25)
Normal fare.
   nil

Ruby -- руководство пользователя

Управление доступом

Ранее мы говорили, что в Ruby нет функций, только методы Тем не менее, есть более чем один вид методов. Earlier, we said that ruby has no functions, only methods. However there is more than one kind of method. В этой главе мы введем понятие управления доступом.

Посмотрим, что случится, если мы определим метод "top level", а не внутри определения некоторого класса. Можно представить данный метод аналогом функции в более традиционном языке программирования, таком как С.

ruby> def square(n)
    |   n * n
    | end
   nil
ruby> square(5)
   25

Кажется, что наш метод не принадлежит никакому классу, однако Ruby включает его в определение базового класса Object, который является суперклассом для любого другого класса. В результате любой объект в состоянии использовать данный метод. Все правильно, но есть некоторый подвох: это private (приватный) метод для любого класса. Ниже мы рассмотрим, что это означает, но одним из следствий является то, что он может быть вызван только как функция, как в примере:

ruby> class Foo
    |   def fourth_power_of (x)
    |     square(x) * square(x)
    |   end
    | end
  nil
ruby> Foo.new.fourth_power_of 10
  10000

Мы не можем явно применить метод к объекту:

ruby> "fish".square(5)
ERR: (eval):1: private method `square' called for "fish":String

Это достаточно разумно для сохранения чистой ОО природы Ruby (функции - это все еще методы объектов, но получателем вызовов явно является self) при обеспечении возможности вызова функций, написанных на традиционных языках программирования.

Общепринятым подходом в ОО программирование, как мы обращали внимание ранее, является разделение спецификации и реализации, или что метод объекта должен выполнять и как он на внутри это делает. Внутренне поведение объекта в общем случае должно быть скрыто от его пользователей; их должно волновать лишь что они передают и что получают обратно, надеясь, что объект знает, как что делать внутри. Как таковое, часто бывает полезно иметь в классах методы, невидимые снаружи, но которые используются внутри (и могут быть изменены программистом при необходимости, без влияния на видение объектов данного класса конечными пользователями). В приведенном ниже тривиальном примере смотрите на engine как на невидимого внутреннего работягу.

ruby> class Test
    |   def times_two(a)
    |     print a," times two is ",engine(a),"\n"
    |   end
    |   def engine(b)
    |     b*2
    |   end
    |   private:engine  # this hides engine from users
    | end
   Test
ruby> test = Test.new
   #<Test:0x4017181c>
ruby> test.engine(6)
ERR: (eval):1: private method `engine' called for #<Test:0x4017181c>
ruby> test.times_two(6)
6 times two is 12.
   nil

Можно предположить, что test.engine(6) вернет значение 12, но, как мы знаем, engine недоступен пользователю объекта Test. только другие методы Test, такие как times_two, имеют право использовать engine. Нам необходимо использовать общедоступный интерфейс, который состоит из метода times_two. Программист, который разрабатывает этот класс, может свободно изменить engine (здесь, например, написав вместо b*2 строку b+b, предполагая большую производительность нового варианта) не воздействуя на взаимодействие пользователя с объектами класса Test objects. Безусловно, данный пример слишком прост чтобы быть полезным; преимущества управления доступом становятся яснее только когда мы начинаем создавать более сложные и интересные классы.

Ruby -- руководство пользователя

Singleton-методы

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

ruby> class SingletonTest
    |   def size
    |     print "25\n"
    |   end
    | end
   nil
ruby> test1 = SingletonTest.new
   #<SingletonTest:0xbc468>
ruby> test2 = SingletonTest.new
   #<SingletonTest:0xbae20>
ruby> def test2.size
    |   print "10\n"
    | end
   nil
ruby> test1.size
25
   nil
ruby> test2.size
10
   nil

В этом примере, test1 и test2 принадлежат одному и тому же классу, но для test2 переопределен метод size, так что он ведет себя по-другому. Метод, определенный только для единичного объекта, называется singleton-методом.

Singleton-методы часто используются для элементов (GUI), где при нажатии различных кнопок должны производиться различные действия.

Singleton-методы не являются характерными только для Ruby, они также присутствуют и в CLOS, Dylan, и т.д. Также в некоторых языках, например Self и NewtonScript, существуют только singleton-методы. Их иногда называют prototype-based языки.

Ruby -- руководство пользователя

Модули

Модули в Ruby аналогичны классам, за исключением:

  • модуль не может иметь представителей.
  • модуль не может иметь подклассов.
  • модуль определяется конструкцией module ... end.

На самом деле... класс Module модуля является суперклассом класса Class класса. Понятно? Нет? Едем дальше.

Существуют два типа модулей. Один предназначен для централизованного хранения методов и констант. Модуль Math в стандартной библиотеке Ruby выступает в данной роли:

ruby> Math.sqrt(2)
   1.41421
ruby> Math::PI
   3.14159

Оператор :: указывает интерпретатору Ruby местоположение константы (возможно, некоторые модули кроме Math определяют значение PI несколько иначе). Если мы хотим прямо ссылаться на методы или константы модуля без использования ::, мы можем включить (include) нужный модуль:

ruby> include Math
   Object
ruby> sqrt(2)
   1.41421
ruby> PI
   3.14159

Другим способом использования модулей является миксин (mixin). Некоторые ОО языки программирования, включая С++, допускают множественное наследование , то есть наследование от более чем одного суперкласса. Примером множественного наследования из реальной жизни может служить будильник; можно считать, что будильник принадлежит к классу часы и также к классу жужжалка.

Ruby умышленно не разрешает множественное наследование, но техника mixin-ов является хорошей альтернативой. Помните, что модули не могут быть инстанциированы и от них нельзя образовать подклассы; но если мы включаем (include) модуль в определение класса, его методы добавляются, или подмешиваются ("mixed in") в класс.

Миксины можно рассматривать с позиций того, какие частные свойства мы хотим получить. Например, если у класса есть работающий метод each, включая в него модуль Enumerable из стандартной библиотеки бесплатно дает нам методы sort и find.

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

Ruby -- руководство пользователя

Процедурные объекты

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

Новый процедурный объект создается используя proc:

ruby> quux = proc {
    |   print "QUUXQUUXQUUX!!!\n"
    | }
   #<Proc:0x4017357c>

Здесь то, на что ссылается quux является объектом, и, как большинство объектов, имеет некоторое поведение, которое может быть запущено. В частности, мы можем попросить его выполниться через его метод call:

ruby> quux.call
QUUXQUUXQUUX!!!
   nil

Итак, в итоге, может ли quux быть использован в качестве аргумента? Конечно.

ruby> def run( p )
    |   print "About to call a procedure...\n"
    |   p.call
    |   print "There: finished.\n"
    | end
   nil
ruby> run quux
About to call a procedure...
QUUXQUUXQUUX!!!
There: finished.
   nil

Метод trap дает возможность задать реакцию по нашему выбору на любой системный сигнал.

ruby> inthandler = proc{ print "^C was pressed.\n" }
   #<Proc:0x401730a4>
ruby> trap "SIGINT", inthandler
   #<Proc:0x401735e0>

В нормальных условиях нажатие ^C заставляет интерпретатор завершить выполнение. Теперь же печатается сообщение и интерпретатор продолжает выполнение, так что вы не теряете выполняемую работу. (Но вы не застряли в нем вечно - можно завершить его набрав exit или нажав ^D.)

Последнее замечание перед переходом к другой теме: нет необходимости назначать процедурному объекту имя перед привязкой его к сигналу. Эквивалентный анонимный процедурный объект будет выглядеть как:

ruby> trap "SIGINT", proc{ print "^C was pressed.\n" }
   nil

или, еще более компактно,

ruby> trap "SIGINT", 'print "^C was pressed.\n"'
   nil

Краткая форма записи дает некоторое удобство и читабельность, когда Вы пишете маленькие анонимные процедуры.

Ruby -- руководство пользователя

Переменные

В Ruby существуют три вид переменных, один вид констант и точно две псевдо-переменные. Переменные и константы не имеют типа. Хотя у нетипизированных переменных есть некоторые недостатки, они имеют больше преимуществ и хорошо вписываются в философию Ruby быстро и просто.

В большинстве языков переменные должны быть объявлены для указания их типа, изменяемости (т.е. являются ли они константами), и облати действия; Поскольку тип не задается, а остальное видно из имени переменной, объявление переменных в Ruby не нужно.

Первая буква идентификатора дает нам возможность навскидку определить категорию:

$ | глобальная переменная
@ | переменная в классе
[a-z] or _ | локальная переменная
[A-Z] | константа

Единственным исключением являются псевдо-переменные в Ruby: self, который всегда ссылается на выполняемый в данное время объект, и nil, неопределенное значение, присваиваемое неинициализированным переменным. У обоих имена, подходящие под категорию локальных, но self является глобальной переменной, поддерживаемой интерпретатором, а nil на самом дле константа. Так как есть только два исключения, они не портят картину в целом.

Вы не можете присвоить значение self или nil.main, поскольку self ссылается на объект верхнего уровня:

ruby> self
   main
ruby> nil
   nil

Ruby -- руководство пользователя

Глобальные переменные

Имя глобальной переменной начинается с $. оно может быть использовано в любом месте программы. До инициализации глобальная переменная содержит специальное значение nil.

ruby> $foo
   nil
ruby> $foo = 5
   5
ruby> $foo
   5

Необходимо соблюдать умеренность в использовании глобальных переменных. они опасны, поскольку их значение может быть изменено отовсюду. Чрезмерное их использование может сделать трудной локализацию ошибок; это также является показателем непродуманности дизайна Вашей программы. Если уж без использования глобальной переменной не удается обойтись, убедитесь в том, что Вы даете ей самоописательное имя, вероятность повторного неумышленного использования которого в дальнейшем низка (присвоение глобальной переменной имени типа $foo вероятно будет плохой идеей).

Одним хорошим свойством глобальной переменной является то, что его можно трассировать; Вы можете определить процедуру, которая будет вызываться при изменении значения переменной.

ruby> trace_var :$x, proc{print "$x is now ", $x, "\n"}
   nil
ruby> $x = 5
$x is now 5
   5

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

Ниже приведена подборка особых переменных, чье имя состоит из знака доллара ($) с последующим одним символом. Например, $$ содержит id процесса интерпретатора Ruby, является read-only. Здесь приведены основные системные переменные и их значение (смотрите детали в справочном руководстве по Ruby ):

$! | последнее сообщение об ошибке
$@ | местоположение ошибки
$_ | последняя строка, прочитанная gets
$. | номер последней строки, прочитанной интерпретатором
$& | последняя строка, обработанная regexp
$~ | последнее совпадение с regexp, как массив подвыражений
$n | nth-е подвыражение в последнем совпадении(то же самое, что и $~[n])
$= | флаг зависимости от регистра символов
$/ | разделитель во входных строках
$\ | разделитель в выходных строках
$0 | имя файла со скриптом Ruby
$* | аргументы командной строки
$$ | ID процесса интерпретатора
$? | статус завершения последнего дочернего процесса

Выше $_ и $~ имеют локальную область действия. Судя по именам, они должны быть глобальными, но в таком виде они более полезны и есть исторические причины для использования этих имен.

Ruby -- руководство пользователя

Переменные экземпляра класса

Переменные экземпляра класса имеют имена, начинающиеся с @, и область их действия ограничена тем, на что можно сослаться, используя self. Два разных объекта, даже если они являются объектами одного и того же класса, могут иметь разные значения в своих переменные экземпляра класса (@-переменные). Снаружи объекта его @-переменные не могут быть не только изменены, но даже и просмотрены (т.е. в Ruby @-переменные никогда не являются public), за исключением случаев, когда это явно определено программистом. Как и в случае глобальных переменных, @-переменные содержат заначение nil если они не были инициализированы.

@-переменные в Ruby не нуждаются в декларировании. Это подразумевает гибкость структуры объекта. Фактически, каждая @-переменная динамически добавляется в объект при первой ссылке на нее.

ruby> class InstTest
    |   def set_foo(n)
    |     @foo = n
    |   end
    |   def set_bar(n)
    |     @bar = n
    |   end
    | end
   nil
ruby> i = InstTest.new
   #<InstTest:0x83678>
ruby> i.set_foo(2)
   2
ruby> i
   #<InstTest:0x83678 @foo=2>
ruby> i.set_bar(4)
   4
ruby> i
   #<InstTest:0x83678 @foo=2, @bar=4>

Обратите внимание, что i не сообщает о значении @bar пока не выполнен метод set_bar.

Ruby -- руководство пользователя

Локальные переменные

Имя локальной переменной должно начинаться с латинской буквы в нижнем регистре или символа подчеркивания (_). Локальная переменная, в отличие от глобальной или @-переменной не содержит значение nil до инициализации:

ruby> $foo
   nil
ruby> @foo
   nil
ruby> foo
ERR: (eval):1: undefined local variable or method `foo' for main(Object)

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

В общем случае, область видимости локальной переменной одна из:

  • proc{ ... }
  • loop{ ... }
  • def ... end
  • class ... end
  • module ... end
  • тело программы (если ни один из приведенных выше случаев не подходит)

В следующем примере defined? - это оператор, проверяющий был ли определен идентификатор. Он возвращает описание идентификатора, если он был определен, или nil в противном случае. Как вы видите, область действия bar локализована в цикле; при завершении цикла bar становится неопределен.

ruby> foo = 44; print foo, "\n"; defined? foo
44
   "local-variable"
ruby> loop{bar=45; print bar, "\n"; break}; defined? bar
45
   nil

Процедурные объекты, живущие в той же области действия, что и локальные переменные, делят общие локальные переменные, принадлежащие это области. Итак, локальная переменная bar принадлежит main и процедурным объектам p1 и p2:

ruby> bar=0
   0
ruby> p1 = proc{|n| bar=n}
   #<Proc:0x8deb0>
ruby> p2 = proc{bar}
   #<Proc:0x8dce8>
ruby> p1.call(5)
   5
ruby> bar
   5
ruby> p2.call
   5

Заметьте, что "bar=0" вначале не может быть опущена; это присваивание гарантирует, что bar охватывается в p1 и p2. Иначе p1 и p2 определят свои собственные локадьные переменные bar, и вызов p2 приведет к ошибке "undefined local variable or method".

Мощь процедурных объектов следует из их способности быть переданными в качестве аргументов: разделяемые локальные переменные остаются действующими даже если они передаются из своей наальной области действия.

ruby> def box
    |   contents = 15
    |   get = proc{contents}
    |   set = proc{|n| contents = n}
    |   return get, set
    | end
   nil
ruby> reader, writer = box
   [#<Proc:0x40170fc0>, #<Proc:0x40170fac>]
ruby> reader.call
   15
ruby> writer.call(2)
   2
ruby> reader.call
   2

Ruby особо сообразителен в том, что касается области действия. Из наших примеров очевидно, что переменная contents разделяется между reader и writer. Но мы можем создать множественные пары reader-writer используя box как определено выше; каждая пара делит общую переменную contents, и эти пары не пересекаются друг с другом.

ruby> reader_1, writer_1 = box
   [#<Proc:0x40172820>, #<Proc:0x4017280c>]
ruby> reader_2, writer_2 = box
   [#<Proc:0x40172668>, #<Proc:0x40172654>]
ruby> writer_1.call(99)
   99
ruby> reader_1.call
   99
ruby> reader_2.call
   15

Ruby -- руководство пользователя

Константы класса

Имя константы начинается с латинской буквы верхнего регистра. значение ей должно присваиваться максимум один раз. В текущей реализации Ruby повторное присваивание значения константе вызывает предупреждение, но не ошибку (не-ANSI версия eval.rb не показывает предупреждения):

ruby>fluid=30
   30
ruby>fluid=31
   31
ruby>Solid=32
   32
ruby>Solid=33
   (eval):1: warning: already initialized constant Solid
   33

Константы могут быть определены внутри классов, но в отличие от @-переменных, они доступны за пределами класса.

ruby> class ConstClass
    |   C1=101
    |   C2=102
    |   C3=103
    |   def show
    |     print C1," ",C2," ",C3,"\n"
    |   end
    | end
   nil
ruby> C1
ERR: (eval):1: uninitialized constant C1
ruby> ConstClass::C1
   101
ruby> ConstClass.new.show
101 102 103
   nil

Константы также могут быть определены в модулях.

ruby> module ConstModule
    |   C1=101
    |   C2=102
    |   C3=103
    |   def showConstants
    |     print C1," ",C2," ",C3,"\n"
    |   end
    | end
   nil
ruby> C1
ERR: (eval):1: uninitialized constant C1
ruby> include ConstModule
   Object
ruby> C1
   101
ruby> showConstants
101 102 103
   nil
ruby> C1=99  # not really a good idea
   99
ruby> C1
   99
ruby> ConstModule::C1  # the module's constant is undisturbed ...
   101
ruby> ConstModule::C1=99
ERR: (eval):1: compile error
(eval):1: parse error
ConstModule::C1=99
                ^
ruby> ConstModule::C1  # ... regardless of how we tamper with it.
   101

Ruby -- руководство пользователя

Обработка исключительных ситуаций: rescue

При выполнении программы могут возникать непредвиденные проблемы. Файл, который нужно прочитать, может не существовать; диск может оказаться полным в момент записи данных; пользователь может некорректно ввести данные.

ruby> file = open("some_file")
ERR: (eval):1:in `open': No such file or directory - some_file

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

FILE *file = fopen("some_file", "r");
if (file == NULL) {
  fprintf( stderr, "File doesn't exist.\n" );
  exit(1);
}
bytes_read = fread( buf, 1, bytes_desired, file );
if (bytes_read != bytes_desired ) {
  /* do more error handling here ... */
}
...

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

В Ruby, как в большинстве современных языков, мы обрабатываем исключения для блоков кода раздельно, таким образом работая эффективно с подобными сюрпризами и не перегружая напрасно программиста или того человека, который позднее будет читать этот код. Блок кода, помеченный begin до тех пор, пока не происходит исключительная ситуация, которая передает управление блоку, отвечающему за ее обработку (начинается с rescue). Если исключения не происходит, то rescue-блок не выполняется. Следующий метод возвращает первую строку из текстового файла или nil, если происходит исключение:

def first_line( filename )
  begin
    file = open("some_file")
    info = file.gets
    file.close
    info  # Last thing evaluated is the return value
  rescue
    nil   # Can't read the file? then don't return a string
  end
end

Иногда бывает нужна возможность обойтись с проблемой творчески. Здесь, если требуемый файл недоступен, мы пробуем использовать стандартный поток ввода:

begin
  file = open("some_file")
rescue
  file = STDIN
end

begin
  # ... process the input ...
rescue
  # ... and deal with any other exceptions here.
end

retry может быть использован внутри rescue для запуска повторного выполнения блока с begin. Теперь можно переписать предыдущий пример немного компактнее:

fname = "some_file"
begin
  file = open(fname)
  # ... process the input ...
rescue
  fname = "STDIN"
  retry
end

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

Любая библиотека Ruby возбуждает исключения при возникновении любой ошибки, и вы также можете явно возбуждать ошибки в своем коде. Для этого используйте raise. Этот оператор принимает один аргумент, который представляет из себя строку, описывающую исключение. Аргумент не обязателен, но опускать его не стоит. Его можно получить позже из специальной глобальной переменной $!.

ruby> raise "test error"
   test error
ruby> begin
    |   raise "test2"
    | rescue
    |   print "An error occurred: ",$!, "\n"
    | end
An error occurred: test2
   nil

Ruby -- руководство пользоватуля

Обработка исключительных ситуаций: ensure

Иногда бывает нужна очистка по завершении работы некоторого метода. Возможно, должен быть закрыт открытый файл, буферизованные данные должны быть сброшены, и т.п., etc. Если бы существовала только одна точка выхода из метода, мы могли бы уверенно поместить код очистки в одно место и быть уверены, что он будет выполнен; однако возврат из метода может происходить из нескольких точек, или наша очистка может быть пропущена из-за возникновения исключения.

begin
  file = open("/tmp/some_file", "w")
  # ... write to the file ...
  file.close
end

В этом примере, если ошибка возникает во время записи файла, файл останется открытым. Также не хочется обращаться к подобной избыточности:

begin
  file = open("/tmp/some_file", "w")
  # ... write to the file ...
  file.close
rescue
  file.close
  fail # raise an exception
end

Это неуклюже, к тому же становится неуправляемым при усложнении кода, поскольку необходимо реагировать на каждые return и break.

По этой причине вводится новое ключевое слово в схему "begin...rescue...end" -- ensure. Блок ensure выполняется независимо от успешности выполнения блока begin.

begin
  file = open("/tmp/some_file", "w")
  # ... write to the file ...
rescue
  # ... handle the exceptions ...
ensure
  file.close   # ... and this always happens.
end

Возможно использование ensure без rescue, или наоборот, но если они использованы вместе в одном блоке begin...end , rescue должен предшествовать ensure.

Ruby -- руководство пользователя

Аксессоры

Что такое аксессор?

Мы кратко обсудили @-переменные в предшествующих главах, но мало поэкспериментировали с ними. @-переменные объекта являются его атрибутами, тем, что обычно отличает его от других объектов этого же класса. Важно иметь возможность задавать и считывать значения атрибутов; для этого необходимо написание методов, называемых аксессорами атрибутов. Через мгновение мы увидим, что не всегда нужно явное задание аксессоров, но давайте сейчас полностью пройдем по этому пути. Есть два вида аксессоров - writers и readers.

ruby> class Fruit
    |   def set_kind(k)  # a writer
    |     @kind = k
    |   end
    |   def get_kind     # a reader
    |     @kind
    |   end
    | end
   nil
ruby> f1 = Fruit.new
   #<Fruit:0xfd7e7c8c>
ruby> f1.set_kind("peach")  # use the writer
   "peach"
ruby> f1.get_kind           # use the reader
   "peach"
ruby> f1                    # inspect the object
   #<Fruit:0xfd7e7c8c @kind="peach">

Достаточно просто; мы можем сохранять и считывать информацию о том, на фрукт какого вида мы смотрим. Но имена наших методов слегка многословны. Пример далее имеет более ясный и привычный вид:

ruby> class Fruit
    |   def kind=(k)
    |     @kind = k
    |   end
    |   def kind
    |     @kind
    |   end
    | end
   nil
ruby> f2 = Fruit.new
   #<Fruit:0xfd7e7c8c>
ruby> f2.kind = "banana"
   "banana"
ruby> f2.kind
   "banana"

Метод inspect

Краткое отступление. Вы уже заметили, что когда мы прямо рассматриваем объект, нам показывают нечто загадочное, типа #<anObject:0x83678>. Это просто действие по умолчанию, и мы свободно можем изменить его. Все, что нужно сделать - это добавить метод inspect в определение класса. Он должен возвращать строковое значение, описывающее объект подходящим способом, включая состояние всех или некоторых @-переменные.

ruby> class Fruit
    |   def inspect
    |     "a fruit of the " + @kind + " variety"
    |   end
    | end
   nil
ruby> f2
   "a fruit of the banana variety"

Ему соответствует метод to_s (перевести в строку), который используется для вывода на печать объекта. В общем случае, можно представлять себе inspect как средство, используемое при написании и отладке программ, а to_s как способ обогащения вывоа программы. eval.rb использует inspect для вывода результатов. Вы можете использовать метод p для выполнения простого вывода при отладке программы.

# Эти две строки эквивалентны:
p anObject
print anObject.inspect, "\n"

Легкий способ создания аксессоров

Поскольку многие @-переменные нуждаются в применении аксессоров, Ruby предоставляет удобные сокращения для стандартных форм.

Сокращение | Эффект (значение)
attr_reader :v | def v; @v; end
attr_writer :v | def v=(value); @v=value; end
attr_accessor :v | attr_reader :v; attr_writer :v
attr_accessor :v, :w | attr_accessor :v; attr_accessor :w

Давайте воспользуемся этими преимуществами и добавим в пример информацию о свежести. Сперва мы затребуем автоматическую генерацию reader-а и writer-а, а затем включим новую информацию в inspect:

ruby> class Fruit
    |   attr_accessor :condition
    |   def inspect
    |     "a " + @condition + @kind"
    |   end
    | end
   nil
ruby> f2.condition = "ripe"
   "ripe"
ruby> f2
   "a ripe banana"

More fun with fruit

Никто не ест гнилые фрукты - так пусть время делает свое черное дело.

ruby> class Fruit
    |   def time_passes
    |     @condition = "rotting"
    |   end
    | end
   nil
ruby> f2
   "a ripe banana"
ruby> f2.time_passes
   "rotting"
ruby> f2
   "a rotting banana"

Но играя с нашим классом, мы внесли некоторую проблему. Что будет, если мы попытаемся сейчас создать третий фрукт? Вспомните, @-переменные не существуют до тех пор, пока им не присваивается какое-либо значение.

ruby> f3 = Fruit.new
ERR: failed to convert nil into String

Это метод inspect жалуется здесь, и на то есть причина. Мы попросили его дать отчет о типе и состоянии фрукта, но до сих пор f3 не был присвоен не один атрибут. Если бы мы пожелали, то можно было бы переписать метод inspect таким образом, чтобы он проверял определенность @-переменных используя метод defined? и давал по ним отчет только при положительном исходе, но, наверное, так будет не учень хорошо; т.к. каждый фрукт имеет вид и состояние, то желательно было бы каким-либо образом обеспечить их задание. Это и будет темой следующей главы.

Ruby -- руководство пользователя

Инициализация объектов

В нашем классе Fruit из предыдущей главы есть @-переменные: первая описывает вид фрукта, вторая - его состояние. Только после написания своего специализированного метода inspect для этого класса, мы осознали, что для любого фрукта отсутствие заданных характеристик бессмысленно. К счастью, в Ruby есть способ гарантировать, что @-переменные всегда будут инициализированы.

Метод initialize

Когда Ruby создает новый объект, он ищет в описании класса метод initialize и вызывает его. Таким образов, простая вещь, которую мы можем проделать,- это использовать initialize для задания всем @-переменным значений "по-умолчанию", так что теперь "методу inspect всегда есть что сказать". ;-)

ruby> class Fruit
    |   def initialize
    |     @kind = "apple"
    |     @condition = "ripe"
    |   end
    | end
   nil
ruby> f4 = Fruit.new
   "a ripe apple"

От предположения к требованию

Бывает, что задавать значения "по-умолчанию" бессмысленно. Существует ли в природе такая вещь, как "фрукт по умолчанию"? наверное, будет предпочтительно, чтобы вид каждого фрукта задавался во время его создания. для этого необходимо ввести формальный аргумент в метод initialize . Мы не будем здесь вдаваться в причину почему, но аргументы, которые мы задаем в вызове метода new на самом деле передаются в метод initialize.

ruby> class Fruit
    |   def initialize( k )
    |     @kind = k
    |     @condition = "ripe"
    |   end
    | end
   nil
ruby> f5 = Fruit.new "mango"
   "a ripe mango"
ruby> f6 = Fruit.new
ERR: (eval):1:in `initialize': wrong # of arguments(0 for 1)

Гибкая инициализация

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

ruby> class Fruit
    |   def initialize( k="apple" )
    |     @kind = k
    |     @condition = "ripe"
    |   end
    | end
   nil
ruby> f5 = Fruit.new "mango"
   "a ripe mango"
ruby> f6 = Fruit.new
   "a ripe apple"

Вы можете использовать значения "по-умолчанию" для любых методов, а не только для initialize. Список аргументов должен быть построен так, чтобы аргументы, которые могут иметь значения "по-умолчанию", шли последними.

Иногда полезно дать возможность инициализировать объект несколькими способами. Хотя это и выходит за рамки данного руководства, Ruby поддерживает reflection объектов и список аргументов переменной длины, которые вместе дают возможность эффективной перезагрузки методов.

Ruby -- руководство пользователя

Фишки

Эта глава посвящена некоторым практическим приемам.

Разделители выражений

В некоторых языках необходимо соблюдение некоторых правил пунктуации; часто используется точка с запятой (;), для обозначения окончания выражения в программе. Ruby вместо этого следует соглашениям, используемым в шеллах, таких как sh или csh. Множественные выражения в одной строке должны разделяться точкой с запятой, но это не обязательно в конце строки; перевод строки трактуется как точка с запятой. Если линия заканчивается обратным слэшем (\), перевод строки игнорируется; это позволяет разнести длинный оператор на несколько строк.

Комментарии

Зачем писать комментарии? Хотя хорошо написанный код имеет тенденцию к самодокументированию, часто бывает полезно делать заметки на полях; будет ошибкой думать, что другие смогут немедленно сказать что делает Ваш код лишь мельком взглянув на него (как это можете Вы). Кроме того, вы можете оказаться в роли этого другого человека всего лишь несколько дней спустя; кто из нас не возвращался спустя некоторое время к старой программе чтобы что-то дописать или исправить ее и не говорил "Я знаю, что это писал я. Но, черт побери, что вот это значит?"

Некоторые опытные программисты достаточно правильно заметят, что противоречивые и устаревшие комментарии могут быть хуже, чем их отсутствие. Безусловно, комментарии не могут быть заменой для читабельного кода; если Ваш код неясен, вероятно, он также содержит ошибки. Может оказаться, что, пока Вы изучаете Ruby, Вы больше испытываете нужду в комментариях, а затем все меньше и меньше, по мере того, как Вы учитесь все лучше выражать свои идеи с помощью простого, элегантного, читабельного кода.

Ruby следует общепринятому в скриптах правилу, по которому начало комментария выделяется знаком фунта (#). Все, что следует за ним, интерпретатором игнорируется.

Также, для облегчения написания больших комментариев, интерпретатор Ruby также игнорирует все, что заключено между строкой, начинающейся с "=begin" и заканчивающейся на "=end".

#!/usr/local/bin/ruby

=begin
**********************************************************************
  This is a comment block, something you write for the benefit of
  human readers (including yourself).  The interpreter ignores it.
  There is no need for a '#' at the start of every line.
**********************************************************************
=end

Упорядочиваем код

Интерпретатор Ruby обрабатывает код по мере его чтения. Нет ничего похожего на фазу компиляции; если что-то еще не было прочитано, значит, это просто неопределено.

# this results in an "undefined method" error:

print successor(3),"\n"

def successor(x)
  x + 1
end

Это не заставляет Вас, как может показаться на первый взгляд, располагать код в стиле "сверху-вниз". Когда интерпретатор натыкается на определение метода, он может благополучно проглотить неопределенные ссылки, но к тому времени, когда метод будет вызван, все они должны быть определены:

# Conversion of fahrenheit to celsius, broken
# down into two steps.

def f_to_c(f)
  scale (f - 32.0)  # This is a forward reference, but it's okay.
end

def scale(x)
  x * 5.0 / 9.0
end

printf "%.1f is a comfortable temperature.\n", f_to_c( 72.3 )

Хотя это может показаться менее удобным, чем то, к чему Вы, может быть, привыкли в Perl или Java, это накладывает меньшие ограничения, чем программирование на С без прототипов (что вынуждает Вас постоянно поддерживать частичную упорядоченность ссылок). Размещение кода верхнего уровня внизу исходника работает всегда. Но даже это не так досадно, как может сперва показаться. Разумный и безболезненный способ обеспечить нужное Вам поведение - определить функцию main в начале файла, а затем вызвать ее снизу.

#!/usr/local/bin/ruby

def main
  # Put your top level code here...
end

# ... put all your support code here, organized as you see fit ...

main # ... and start execution here.

Также может помочь то, что в Ruby есть утилиты, разбивающие сложную программу на читабельные, с возможностью повторного использования, логически взаимосвязанные куски. Мы уже встречались с использованием include для доступа к модулям. Вы также сочтете полезным использование средств load и require. load работает как если бы файл, на который он ссылается, был скопирован и помещен в текст программы вместо него (несто похожее на директиву препроцессора #include в C). require - несколько более сложная вещь, код загружается только один раз и только тогда, когда он реально нужен. Есть и другие отличия между load и require; их можно посмотреть в справочном руководстве или в FAQ.

Се ля ви...

Этого тьюториала должно быть достаточно, чтобы помочь вам начать писать программы на Ruby. Если возникают вопросы, Вы всегда можете порыться в справочном руководстве чтобы "углубить и расширить" ;-) свои познания . FAQ и справочник по библиотеке также являются важными ресурсами.

Удачи, и счастливого кодирования!

Одностраничная версия руководства пользователя Ruby
Fri Feb 16 13:56:18 CST 2001 (Eng)
Mon Sep 10 16:16:48 CST 2001 (Rus)


Ruby News | Чужая документация | Ruby – простой и мощный объектно-ориентированный язык программирования, созданный Юкихиро Мацумото (Yukihiro Matsumoto) также известный под ником Мац (matz)

Здравствуйте, Гость !

Russian Ruby
Ruby News
"Узнал сам -- поделись с сообществом!"
Главная страница :: Добавить новость :: Контакт с автором
Навигация
· Главная
· Регистрация
· Добавить новость
· Контакт с автором
· Наша документация
· Опросы
· Список пользователей
· Форум RubyNews
· Чужая документация

Наш опрос
Ruby сложный язык?

Да
Нет
Возможно
Затрудняюсь ответить

Результаты
Другие опросы

Ответов: 349
Комментариев: 6

Ссылки
Сайты о Ruby (ru)
· Блог Д.Сабанина
· ruby-forum.ru
· ~roganova
· ruby.h1.ru
· rubysun

Документация (ru)
· Введение в ООП (МГИУ)
· Язык программирования Ruby (МГИУ)
· Руководство пользователя
· Курс Ruby
· Список рассылки
· Архив сообщений

Official sites (en)
· Ruby homepage
· Ruby Central
· RubyGarden
· Ruby Change Request
· ruby-forum.org

Documentation (en)
· RDoc 1.9 online
· Ruby Doc
· Ruby Class and Library Reference
· ruby-talk
· RubyGarden Wiki
· Programming Ruby
· Learning Ruby
· lrz-s diary
· Ruby on Windows
· rubyXML
· RubyConf
· Ruby FAQ
· Ruby Cookbook [1]
· Ruby Cookbook [2]
· PLEAC-Ruby
· RubyUsersGroups
· Ruby Bag Tracking System
· Ruby Output
· Ruby Hotlinks
· Perl vs Ruby
· RubyHacker

Programms (en)
· Ruby App Arhive (RAA)
· YARAA (mirror RAA)
· RubyForge
· Ruby RPMs for Linux
· Debian packages for Ruby
· RubyUnit
· Ruby Documentation generator

Друзья
Студенческий форум Московского Электронно - Технологического Техникума

Русскоязычный форум разработчиков на языке Ruby

Чужая документация

Язык Ruby

Курс лекций по предмету:

«Программирование на языке высокого уровня»

для студентов МПЭК специальности 2201

Содержание

Содержание. II

Лекция 1. Знакомство с языком Ruby. IV

Основные возможности Ruby: IV

Термины объектно-ориентированной концепции. IV

Первая программа на Ruby. V

Простейшие типы данных в Ruby. VI

Переменные. VII

Префиксы имен переменных. VII

Область видимости локальных переменных. VII

Разновидности блоков. VII

Список ресурсов для самостоятельного изучения: VII

Лекция 2. Числа. VIII

Основные формы записи чисел. VIII

Методы работы с числами. IX

Методы преобразования типов. X

Модуль Math. X

Методы модуля Math. XI

Получение случайного числа. XI

Лекция 3. Строки. XII

Таблица escape-последовательностей. XII

Альтернативные способы задания строк. XII

Методы преобразования типов. XIII

Методы работы с числовыми данными. XIII

Методы поиска в строках. XIV

Лекция 3.5. Строки. Продолжение. XIV

Методы модификации строк. XIV

Методы поиска и замены.. XV

Метод eval XVI

Лекция 4. Промежутки времени XVI

Методы класса Time. XVI

Метод mktime. XVII

Метод strftime. XVII

Таблица макрокоманд (макросов) XVII

Лекция 5. Массивы.. XVIII

Методы получения/удаления/добавления элементов. XIX

Методы преобразования массива. XXI

Лекция 5.5. Итераторы.. XXI

Методы числовых характеристик. XXI

Методы логических характеристик. XXII

Методы преобразования типов. XXII

Лекция 6. Вывод данных. XXII

Метод print XXII

Метод puts. XXIII

Метод p. XXIII

Метод sprintf XXIII

Правило построения команды форматирования. XXIII

Метод printf XXIV

Лекция 7. Логический тип и операторы ветвления. XXIV

Стандартные логические операторы.. XXIV

Методы сравнения. XXV

Логические методы.. XXV

Операторы ветвления. XXV

Оператор if XXV

Оператор case-when. XXVI

Тернарный оператор. XXVII

Лекция 7.5. Циклы.. XXVII

Цикл loop. XXVII

Цикл while. XXVII

Цикл until XXVIII

Лекция 8. Итераторы.. XXVIII

Итератор upto. XXVIII

Итератор downto. XXIX

Итератор step. XXIX

Итератор times. XXIX

Итераторы each и for XXX

Конструкции управления циклами и итераторами. XXX

Лекция 1. Знакомство с языком Ruby

Ruby – простой и мощный объектно-ориентированный язык программирования интерпретируемого типа. Он создан 23 февраля 1993 года японским программистом Юкихиро Мацумото (Yukihiro Matsumoto) также известным под ником Мац (matz). На данный момент он является самым популярным языком в Японии.

Основные возможности Ruby:

1. Простой и чистый синтаксис.

2. Основные объектно-ориентированные понятия: классы, методы, объекты и т.д.

3. Специальные ОО понятия: mix-in’ы, синглитон (singleton) методы и т.д.

4. Переопределение операторов.

5. Обработка исключений.

6. Итераторы, замыкания и l-функции.

7. Сборщик мусора.

8. Высокая переносимость и платформонезависимость (даже выше, чем у Java).

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

1. Быстрый цикл разработки (редактирование – запуск - редактирование).

2. Использование интерпретатора.

3. Изначальная объектно-ориентированность.

4. Нетипизированные переменные (не требуют объявления).

5. Использование и результат в различных операционных системах принципиально не отличаются.

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

Термины объектно-ориентированной концепции

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

Опр. Объект – экземпляр класса.

Опр. Класс – функциональность и данные (методы и атрибуты), которыми может обладать объект данного класса.

Опр. Метод – функция, описывающая реакцию объекта на поступивший запрос (задает поведение объекта).

Опр. Атрибуты – данные, которыми может обладать объект (или класс).

Все объекты некоторого класса обладают одними и теми же методами и атрибутами. Хотя Ruby и позволяет изменить это правило, будем считать, что это именно так (см. описание singleton-методов). Для того, чтобы применить метод к некоторому объекту используется оператор вызова метода, обозначаемый символом . (точка), т.е. после указания объекта ставится точка, а затем указывается применяемый метод. Пример:

obj.method

Первая программа на Ruby

По давней программерской традиции наша первая программа на языке Ruby будет печатать фразу «Hello, World!»:

puts(“Hello, World!”)

Результатом выполнения данной программы будет вывод на экран следующей строки:

Hello, World!

Опр. Комментарии – часть текста программы, которая игнорируется при обработке.

Ruby имеет два вида комментариев: блочные и однострочные. Все, что располагается между строками =begin и =end, считается блочным комментарием. Пример:

=begin

Это

комментарий

=end

Однострочные комментарии начинаются с символа # и заканчиваются концом строки. Пример:

# Это тоже комментарий

Программа на языке Ruby, часто называемая скриптом есть последовательность инструкций (утверждений, предложений). Каждая инструкция по умолчанию заканчивается концом строки. Если в строке следует разместить несколько инструкций, то их следует разделить символом ; (точка с запятой). Пример:

puts(“Это текст, ”) ; puts(“в Ruby.”)

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

puts \

(“И мы обязательно этому научимся!”)

Познакомимся с простейшими встроенными классами, без которых не обойтись при изучении любого языка.

Простейшие типы данных в Ruby

Числа

Иерархия наследования классов чисел

числа делятся на два подкласса: дробные и целые. Целые в свою очередь делятся на те, которые превышают 32 бита и те, которые их не превышают.

Строки

Экземпляры класса String.

Массивы

Экземпляры класса Array.

Моменты времени

Экземпляры класса Time.

Логический (булевский) тип

Экземпляры классов TrueClass и FalseClass.Содержат единственных представителей true и false соответственно.

«Ничего»

Экземпляры класса NilClass. Cодержит единственного представителя nil. Любая необъявленная переменная равняется nil.

Для того, чтобы проверить принадлежность классу некоторого объекта необходимо применить метод .class (или .type) к этому объекту. В результате мы получим строку, которую можно будет вывести на экран при помощи метода puts. Пример:

puts( obj.class )

puts( obj.type )

Переменные

Как и в любом другом языке программирования в Ruby есть переменные, которые ссылаются на некоторый объект. Будем считать, что переменная хранит значение, т.к. различие между ссылкой и хранением в рамках Ruby для нас практически неразличима.

Префиксы имен переменных

Метасимвол

Вид переменной

Пример

$

глобальная переменная

$value

@

переменная экземпляра

@value

@@

переменная класса

@@value

a-z_

локальная переменная

_value, value

A-Z

локальная константа

Value, VALUE

Область видимости локальных переменных

Опр. Область видимости – блок программы, в котором определена и используется некоторая локальная переменная.

Разновидности блоков

- lambda{…} – l-функция

- loop{…} – бесконечный цикл (а также все циклы и блоки итераторов)

- def … end – метод

- class … end – класс

- module … end – модуль

- вся программа (если не выполняется ни один из указанных случаев)

Локальная переменная, инициализированная в каком либо блоке, после выхода за пределы блока становится неопределенной (и жертвой сборщика мусора).

Список ресурсов для самостоятельного изучения:

1. http://ruby-lang.org/ - домашняя страничка Ruby (англ).

2. http://ruby-doc.org/ - сайт с очень интересными статьями по Ruby (англ).

3. http://rubysun.by.ru/ - содержит коллекцию ссылок на ресурсы по Ruby (рус).

4. http://pleac.sourceforge.net/pleac_ruby/t1.html - примеры выполнения различных трюков в Ruby (англ).

5. Любая литература по Perl, т.к. многие вещи в Ruby и Perl схожи.

Лекция 2. Числа

На прошлой лекции мы рассматривали основные типы (классы) данных в Ruby. Теперь рассмотрим их более подробно. Первым из рассматриваемых типов данных будут числа.

Основные формы записи чисел

Число
Описание

Fixnum

123_456

подчеркивание игнорируется

-567

отрицательное число

0xbad

шестнадцатеричное число

0377

восьмеричное

-0b101010

отрицательное двоичное

0b0101_0101

подчеркивание игнорируется

Bignum

123_456_789_123_456

подчеркивание игнорируется

-123_456_789_123_456

отрицательное

Float

-12.34

отрицательное дробное

.1234е2

экспоненциальная форма для числа 12.34

1234е-2

экспоненциальная форма для числа 12.34

При записи целых чисел сначала указывается его знак (знак + обычно не пишется). Далее идет основание системы счисления, в которой задается число (если оно отлично от 10): 0 - для восьмеричной, 0x - для шестнадцатеричной, 0b - для двоичной. Затем идет последовательность цифр, выражающих число в данной системе счисления. При записи больших чисел можно использовать символ подчеркивания, который игнорируется при обработке.

Дробные числа задаются только в десятичной системе счисления, при этом для отделения дробной части используется символ . (точка). Для задания дробных чисел может быть применена и экспоненциальная форма записи: два различных представления -.1234е2 и 1234е-2 задают одно и тоже число 12.34.

Для вычисления арифметических операций применяются следующие операторы (методы):

Оператор | Операция
Numeric
+ | сложение
- | вычитание
* | умножение
/ | деление
% | остаток от деления
** | возведение в степень
Fixnum
& | побитовое «и»
| | побитовое «или»
^ | побитовое «исключающее или»
<< | побитовый сдвиг влево
>> | побитовый сдвиг вправо
~ | побитовая инверсия

Порядок вычисления определяется стандартными приоритетами для арифметических операций. Для изменения приоритета следует применять круглые скобки. Пример:

2 + 2 * 2 # 6

(2 + 2) * 2 # 8

Если все аргументы арифметического выражения целые числа, то результат будет целым, если хотя бы одно дробное, то результат будет дробным. Пример:

5/8 # 0

10/8 # 1

5.0/8 # 0.625

Методы работы с числами

Метод | Операция
ceil | наименьшее целое не меньшее, чем данное.
 | 12.34.ceil # 13 -12.34.ceil # -12
floor | наибольшее целое, не больше данного.
 | 12.34.floor # 12 -12.34.floor # -13
round | округление по правилам
 | 12.34.round # 12 12.56.round # 13
abs | модуль числа.
 | –12.34.abs # 12.34

Для практического использования понадобятся только два метода: round и abs.

Стоит заметить, что в вышеуказанных примерах первая точка – это разделитель дробной и целой части, а вторая оператор вызова метода.

Помимо вышеуказанных операций для чисел в Ruby существуют методы преобразования типов.

Методы преобразования типов

Метод | Операция
to_f | преобразовать в дробное.
 | -1234.to_f # -1234.0
to_i | преобразовать в целое
 | -12.56.to_i # -12 -12.34.to_i.abs # 12

Для того чтобы расширить круг наших задач рассмотрим модуль Math, а попутно и работу с модулями.

Опр. Модуль – аналог библиотеки в директивных и множественного наследования в ОО языках программирования.

Для более простого запоминания модули легче воспринимать как библиотеки расширения.

Модуль Math

Содержит множество математических функций и две математические константы E и PI (соответственно значение e и p) Для того, чтобы воспользоваться любой из функций модуля необходимо указать имя модуля и через точку имя функции (метода). Пример:

Math.sqrt(2) # 1.4142135623731

Если необходимо использовать одну из констант модуля, то необходимо указать имя модуля и через два двоеточия имя константы. Пример:

Math::PI # 3.14159265358979

Math::E # 2.71828182845905

Если в программе необходимы активные математические вычисления, то написание префикса Math неприемлемо, т.к. ухудшается читаемость программы. К счастью есть возможность подключения модуля. Если необходимо убрать имя модуля из вызова методов и констант, то следует этот модуль подключить. Подключение модуля производит метод include. Пример:

include Math

sqrt( 2 ) # 1.4142135623731

PI # 3.14159265358979

А теперь можно приступить к основной части данной темы – перечислению методов модуля Math.

Методы модуля Math

Метод | Описание
cos(x), sin(x), tan(x) | вычисляет косинус, синус или тангенс от угла x, заданного в радианах
 | Math.cos( 0 ) # 1.0 Math.sin( Math::PI / 2 ) # 1.0 Math.tan( Math::PI/4 ) # 1
atan2(y,x) | возвращает в радианах арктангенс отношения y к x
 | Math.atan2( 1.0, 1.0 ) # 0.785398163397448
exp(x) | возвращает число e возведенное в степень x
 | Math.exp(1) # 2.71828182845905
log(x) | натуральный логарифм числа x
 | Math.log(10) # 2.30258509299405
log10(x) | логарифм числа x по основанию 10
 | Math.log(10) # 1.0
sqrt(x) | квадратный корень числа x
 | Math.sqrt(2) # 1.4142135623731

Наиболее интересной функцией в любом языке программирования является генератор случайных чисел.

Получение случайного числа

Метод | Описание
rand(max) rand | метод получения очередной величины из псевдослучайной последовательности. rand генерирует дробное число в промежутке от 0 до 1, а rand( max ) – целое число в промежутке от 0 до max
 | rand(10) # 3 rand(1000) # 323 rand # 0.7408769294
srand(n) srand | метод изменения псевдослучайной последовательности.
 | srand 1234 rand # 0.7408769294 srand 1234 rand # 0.7408769294 srand rand # 0.2145348572

Рассмотрим в качестве примера работы с числами задачи округления числа до десятых долей и изменения порядка цифр в числе (любой системы счисления). Пример:

a = 12.34 ; (a*10).round.to_f / 10 # округление до десятых

a = 0123 # следует получить 0231

a/8%8*8**2 + a%8*8 + a/8**2 # 0231

Лекция 3. Строки

Другим столь же распространенным классом является класс String. Строки в Ruby ограничиваются апострофами ('...'), либо кавычками ("..."). Пример:

puts ‘строка’ # строка

puts “строка” # строка

Однако такие строки имеют различия между собой. При использовании кавычек допускается вставлять в строки, управляющие escape-последовательности, начинающиеся с символа '\', а также вычислять выражения с помощью конструкции #{}. Пример:

puts "a \t b" # a b

puts 'a \t b' # a \t b

puts "3*5+8 = #{3*5+8}" # 3*5+8 = 23

puts '3*5+8 = #{3*5+8}' # 3*5+8 = #{3*5+8}

puts "5/8 = #{5/8}" # 5/8 = 0

puts "5/8 = #{5/8.to_f}" # 5/8 = 0.625

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

Таблица escape-последовательностей

Конструкция | Описание
\n | Перевод строку
\t | Табуляция
\b | Возврат в предыдущую позицию с удалением символа.
\a | Звуковой сигнал
\\ | Обратная косая черта

Помимо основных способов задания строк в Ruby существуют альтернативные способы, наследуемые из языка Perl.

Альтернативные способы задания строк

Строки в апострофах | Строки в кавычках
%q/hello/ %q(hello) %q!hello! | %Q{hello} %<hello> %*hello*

Для того, чтобы задать строку альтернативным способом надо сначала поставить знак %, а затем идентификатор вида строки: q для строки в апострофах, а Q для строки в кавычках (обычно Q не пишется). Далее следует парный (скобки и т.д.) или одинарный символ границы строки. Если в строку следует вставить символ границы строки, то следует его экранировать символом \ (обратная косая черта). Пример:

%q(hel\)lo) # ‘hel)lo’

%!hel\!lo! # “hel!lo”

Также как и числа, строки обладают методами преобразования типов.

Методы преобразования типов

Метод | Операция
to_i | преобразовать в целое
 | "-12.34".to_i # -12 "+12:34".to_i # 12
to_f | преобразовать в дробное.
 | "12.34".to_f # 12.34 "12qq34".to_f # 12.0

При преобразовании к целому числу отбрасывается оставшаяся часть строки, как только встречается символ, отличный от цифры (исключение - знак плюс или минус в начале строки). Аналогичные правила применяются и при преобразовании к дробному числу. Единственным отличием является то, что первая найденная точка рассматривается как разделитель целой и дробной части.

Кстати, для преобразования любого числа в строку используется метод to_s.

Для того, чтобы получить строку, содержащую символ с заданным ASCII кодом, используется метод chr. Данный метод может быть применен только к положительному числу, меньше 255. Пример:

67.chr # "С"

Класс String предоставляет большое число методов для работы со строками. Ниже будут приведены несколько таблиц с методами класса String разбитых по виду работы со строкой.

Методы работы с числовыми данными

Метод | Описание
* | Повтор строки
 | “ab”*3 # ”ababab”
[] | Возвращает ASCII-код символа, находящегося на указанной позиции (начиная с 0)
 | “abc”[0] # 97 “abc”[0].chr” # “a”
size | Возвращает длину строки
 | “12345678”.size # 8
hex, oct | Преобразование строки с числом в шестнадцатеричной и восьмеричной системе счисления в число в десятичной системе счисления.
 | "-377".oct # -255 "bad".oct #0 "0377bad".oct # 255 “0xa".hex # 10 "-1234".hex # -4660 "bad".hex # 2989

Методы поиска в строках

Метод | Описание
[нач..кон] | Ищет подстроку, заключенную в указанном диапазоне (включая концы для .. и не включая для …)
 | a = “abcdef” a[0..3] # “abcd” a[0…3] # “abc”
[нач, дл] | Ищет подстроку, начинающуюся с указанной позиции и имеющую заданную длину. Отрицательное начало высчитывается с конца строки.
 | a = “abcdef” a[0,3] # ”abc” a[-3,3] # “def”
index | Ищет номер позиции, с которой начинается указанная подстрока (первое вхождение). Опционально задается номер позиции, с которой начинается поиск.
 | a = “abcdabcd” a.index(“cd”) # 2 a.index(“cd”,3) # 6

Лекция 3.5. Строки. Продолжение

Методы модификации строк

Метод | Описание
+ | Конкатенация (сцепление или сложение) строк.
 | “мол” + “око” # “молоко”
chomp | Удаляет символ перевода строки (если он есть) из конца строки.
 | “ab\ncdef\n”.chomp # “ab\ncdef”
capitalize | Делает первый символ строки заглавным.
 | “abc”.capitslize # “Abc”
upcase | Делает все символы строки заглавными
 | “abc”.upcase # “ABC”
downcase | Делает все символы строки строчными
 | “SoNaR”.downcase # “sonar”
delete | Удаляет указанные символы из строки, может указываться диапазон изменения символов.
 | a = "abcdef" a.delete('ea') # “bcdf” a.delete('a-c') # “def”
reverse | Возвращает строку, содержащую символы в обратном порядке.
 | "abcdef".reverse # "fedcba"
strip | Удаляет пробелы в начале и конце строки.
 | " 123 ".strip # “123”
squeeze | Оставляет в группе повторяющихся символов только один. Допускается задание списка символов, на которые распространяется данное действие.
 | a = "22---23**" a.squeeze # “2-23*” a.squeeze('*-') # “22-23*”
ljust, rjust, center | Дополняет строку пробелами до указанной длины, выравнивая соответственно по левому, по правому краю или по центру.
 | a = “123” a.ljust(8) # “123 ” a.rjust(8) # “ 123” a.center(8) # “ 123 ”

Методы поиска и замены

Метод | Описание
sub | Производит замену первого вхождения подстроки в строке.
 | “hello”.sub(“l”,”*”) # “he*lo”
gsub | Производит замену всех вхождений подстроки в строке.
 | “hello”.gsub(“l”,”*”) # “he**o
tr | Заменяет все найденные вхождения символов на заданные
 | "22+33=55".tr('25','47') # "44+33=77"

Разумеется, что это лишь малая часть тех методов, которыми обладает класс String, но этих методов достаточно для рассмотрения большого числа алгоритмов работы со строками.

«Методы модификации строк», а также «Методы поиска и замены» обладают методами-двойниками, отличающихся от обычных именем (имя заканчивается на восклицательный знак) и тем, что они модифицируют исходную строку. Следует использовать их с осторожностью, т.к. они возвращают nil тогда, когда строка не изменилась из-за вызова данных методов. Пример:

“hello”.sub(“a”,”*”) # “hello”

“hello”.sub!(“a”,”*”) # nil

Метод eval

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

eval("2**10") # 1024

eval('"мол".size * "око".size') # 9

Лекция 4. Промежутки времени

Экземпляр класса Time в языке Ruby содержит информацию о дате, времени и временной зоне. Для создания объекта этого класса, содержащего текущие дату и время, используется метод now. Такие объекты могут являться аргументами операций + и -. Пример:

Time.now # текущее время

Time.now-60 # отняли 60 секунд

Time.now+60 # добавили 60 секунд

Методы класса Time

Метод | Описание
sec | Получить число секунд
min | Получить число минут
hour | Получить число часов
day | Получить день месяца
mon | Получить номер месяца
year | Получить год
wday | Получить номер дня недели
yday | Получить номер дня в году
zone | Получить информацию о временной зоне
to_i | Получить число секунд, прошедших с 01.01.1970 года

Метод mktime

Если необходимо создать некоторую дату отличную от текущей, то необходимо использовать метод mktime, а не now для создания объекта Time. Формат вызова данного метода следующий:

Time.mktime( year, [month, day, hour, min, sec, usec]) -> aTime

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

Time.mktime(2000,"jan",1,20,15,1)\ #Sat Jan 01 20:15:01 CST 2000

Как видно из записи данный метод (также как и now) создает новый объект Time. Такие методы будем называть конструкторами.

Опр. Конструктор – метод, который создает новый объект.

Метод strftime

Если необходимо сформировать строку с информацией о дате или времени, то целесообразнее использовать метод strftime. Он получает строку с макрокомандами, а возвращает строку с данными, подставляя на место макрокоманд соответствующие данные. Пример:

t = Time.now

t.strftime("Напечатано %m/%d/%Y") \

#"Напечатано 11/25/2001"

t.strftime("%I:%M%p") # “11:50PM"

Таблица макрокоманд (макросов)

Макрос | Описание
%a | Короткое имя дня недели (“Wed”)
%A | Полное имя дня недели (“Wednesday”)
%b | Короткое имя месяца (“Jan”)
%B | Полное имя месяца (“January”)
%c | Время и дата (“09/10/03 20:18:24”)
%d | День месяца (01..31)
%H | Число часов в 24-часовом формате (00..23)
%I | Число часов в 12-часовом формате (01..12)
%j | День в году (001..366)
%m | Месяц в году (01..12)
%M | Число минут в часе (00..59)
%p | Временной разделитель (“AM” или “PM”)
%S | Число секунд в минуте (00..60)
%U | Номер недели в году начиная с 1-го воскресенья (00..53)
%W | Номер недели в году начиная с 1-го понедельника (00..53)
%w | Номер дня недели (0 – воскресенье, 0..6)
%x | Дата с годом без века (09/10/03)
%X | Время (20:38:02)
%y | Год без века (00..99)
%Y | Год с веком (2003)
%Z | Имя временной зоны
%% | Символ %

В языке Ruby имеется метод sleep, который заставляет программу "заснуть" на число секунд, указанное в качестве аргумента метода. Пример:

puts "Через #{365-Time.now.yday} дней – сессия!"

puts Time.now

sleep(10)

puts Time.now

Лекция 5. Массивы

На данный момент нами рассмотрено уже три простейших типа данных: числа, строки и моменты времени. Сейчас мы приступим к изучению интереснейшего типа данных – массивам.

Опр. Массив – структурированный тип данных, который используется для описания упорядоченной совокупности элементов, имеющей общее имя.

Массивы в Ruby динамические (дополняемые), гетерогенные (содержат элементы любого типа) и являются объектами класса Array.

Для простейшего определения массивов используются квадратные скобки ([…]). Элементы массива перечисляются через запятую. Пример:

a = [ 1, Time.now, ”5” ] # массив из трех элементов

Для доступа к элементу массива используется метод квадратные скобки [] с указанием внутри них номера элемента. Нумерация элементов в массивах начинается с нуля.

a = [ 1, Time.now, ”5” ]

a[ 2 ] # “5”

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

Array.new( 3 ) # [ nil, nil, nil ]

По умолчанию, конструктор заполняет все ячейки нового массива объектом nil. Если необходимо создать массив с элементами другого класса, то необходимо указать требуемый класс в виде второго параметра. Пример:

Array.new( 3, Time ) # [Time, Time, Time]

Array.new( 3, Time ) # [ 0, 0, 0 ]

Для заполнения массива используется метод []= с номером элемента массива внутри []. Пример:

a = Array.new( 3 )

a[ 2 ] = 5

a # [ nil, nil, 5 ]

Если необходимо создать массив размерностью больше единицы, то необходимо создать массив массивов и обращаться к ним последовательно (номер массива, а потом номер элемента). Пример:

a = Array.new( 2 ).map{ Array.new( 2 ) } # двумерный массив

a[0][1] = “3”

a[0] # [ nil, “3” ]

a # [ [ nil, “3” ], [ nil, nil ] ]

Как и в строках, в массивах существует альтернативный способ создания (пришел из Perl). Но используется он только для задания массива строк. Пример:

%w(раз два три) # ["раз", "два", "три"]

Если одна из строк содержит пробел, то его следует экранировать символом \. Пример:

%w(раз\ два три) # ["раз два", "три"]

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

Методы получения/удаления/добавления элементов

Метод | Описание
[ ] at | Получение элемента с указанным индексом; если аргумент является отрицательным числом, то индекс отсчитывается с конца массива.
 | a = [1, 2, 3, 4] a[0] # 1 a.at(0) # 1 a[-2] # 3
[нач..кон] | Получение подмассива заключенного в указанном диапазоне (включая концы для .. и не включая для …)
 | a = [1, 2, 3, 4] a[0..2] # [1, 2, 3] a[0…2] # [1, 2]
[нач, дл] | Получение подмассива, начинающегося с указанной позиции и имеющего заданную длину. Отрицательное начало высчитывается с конца строки.
 | a = [1, 2, 3, 4] a[0, 2] # [1, 2] a[-2, 2] # [3, 4]
[ ]= | Занесение элемента в указанную позицию массива. Допускается отрицательный индекс.
 | a = [1, 2, 3, 4] a[0] = 5 a # [5, 2, 3, 4] a[-2] = 6 a # [5, 2, 6, 4]
index | Получение позиции первого вхождения аргумента (nil если не найден)
 | a = [ 1, 2, 3 ]; a.index( 2 ) # 1 a.index( 6 ) # nil
<< push | Добавление элемента в конец массива
 | [1, 2] << "c" << [3, 4] # [1, 2, "c", [3, 4]] [1, 2].push(3, 4) # [1, 2, 3, 4]
pop | Удаление последнего элемента массива. Возвращает удаленный элемент
 | a = [ 1, 2, 3 ]; a.pop # 3 a # [1, 2]
unshift | Добавление элемента в начало массива со сдвигом остальных
 | [1, 2 ].unshift( 3 ) # [ 3, 1, 2 ]
shift | Удаление первого элемента массива со сдвигом остальных. Возвращает удаленный элемент
 | a = [ 3, 1, 2 ]; a.shift # 3 a# [ 1, 2 ]
first | Возвращает первый элемент массива
 | [ 3, 1, 2 ].first # “a”
last | Возвращает последний элемент массива
 | [ 3, 1, 2 ].last # “c”

Методы преобразования массива

Метод | Описание
+ | Добавление одного массива к другому
 | [1, 2] + [3, 4] # [1, 2, 3, 4]
* | Повторение
 | [1, 2] * 2 # [1, 2, 1, 2]
| | Объединение множеств
 | [1, 2, 3] | [1, 2, 5] # [1, 2, 3, 5]
& | Пересечение множеств
 | [1, 2, 3] & [1, 2, 5] # [1, 2]
- | Разность множеств
 | [1, 2, 3] - [1, 2, 5] # [3]
compact | Удаление всех объектов nil из массива
 | [1, nil, 3, nil, 5].compact # [1, 3, 5]
flatten | Получение одномерного массива из многомерного
 | [1, 2, “3”, [4, 5]].flatten # [1, 2, “3”, 4, 5]
reverse | Инвертирование массива
 | [1, 2, “3”, [4, 5]].reverse # [[4, 5], ”3”, 2, 1]
clear | Удаление всех элементов массива
 | [1, 2, “3”, [4,5]].clear # []
sort | Сортировка массива
 | a = [3, 1, 2] a.sort # [1, 2, 3] a.sort {|x,y| y <=> x } # [3, 2, 1]

В качестве домашнего задания следует рассмотреть задачу перемешивания массива с использованием методов sort и rand.

Лекция 5.5. Итераторы

Опр. Итератор – метод, последовательно возвращающий в свое тело элементы из некоторого множества.

Применительно к массивам, это метод, последовательно возвращающий в свое тело элементы массива.

Метод | Описание
each | Вызов блока операторов по одному разу для каждого элемента массива
 | %w(a b c).each {|str| print str, "--" } # a--b--c--
map | Тот же each, только результат блока записывается в текущий элемент массива.
 | %w(a b c).map {|str| str+"!"} # ["a!","b!","c!"]

Методы числовых характеристик

Метод | Описание
size | Определение количества элементов массива
 | [ 1, nil, 3, nil, 5 ].size # 5
nitems | Определение количества элементов массива, отличных от nil
 | [ 1, nil, 3, nil, 5 ].nitems # 3

Методы логических характеристик

Опр. Логический метод – метод, который возвращает true или false (возможны и другие варианты).

Имена логических методов в Ruby заканчиваются на вопросительный знак.

Метод | Описание
include? | Проверка на наличие элемента в массиве
 | a = [1, nil, 3, nil, 5] a.include?( 3 ) # true a.include?( 6 ) # false
empty? | Проверка на отсутствие элементов массива
 | [].empty? # true [1].empty? # false

Методы преобразования типов

Метод | Описание
join | Создание единой строки из массива строк. Опциональнально указывается разделитель
 | a = [ "a", "b", "c" ] a.join # "abc" a.join("-") # "a-b-c"
split | Создание массива из строки с разделителями. Опционально указывается разделитель
 | a = "a-b c” a.split(“-”) # [ "a", "b c" ] a.split(“”) # [ "a", ”-”, "b", ” ”, "c" ] a.split # [ "a-b", "c" ]

Лекция 6. Вывод данных

Метод print

Метод print просто выводит информацию на экран. Все последующие методы вывода на экран строятся на его базе. Пример:

print "Hello world!\n" # Hello world!

Как видно из примера, необходимо специально указывать символ перевода строки, иначе весь вывод на экран будет в одну строку.

Метод puts

Метод puts отличается от print тем, что прикрепляет к выводимой строке символ перевода строки. Пример:

puts("Hello world!") # Hello world!

print("Hello world!\n") # Hello world!

Метод p

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

a = 5

puts a.inspect # 5

p a # 5

Метод sprintf

Метод sprintf – совсем не метод вывода на экран. Он формирует строку (используя команды форматирования), которая используется методом printf. Созданную строку можно вывести на экран или использовать в вычислениях. Пример:

sprintf("%i*%i+%i = %i\n”, 3, 5, 8, 3*5+8) # “3*5+8 = 23”

Правило построения команды форматирования

0. символ %

1. флаги (в любом порядке)

Флаг | Описание | Пример
пробел | если первый печатаемый символ не знак, то числу должен предшествовать пробел | “ 12”
+ | всегда печатать число со знаком | “+12”
- | преобразованный аргумент должен быть прижат к левому краю поля | “12 “
0 | число дополняется ведущими нулями до всей ширины поля | “012”

2. число, задающее минимальную ширину поля

3. точка, отделяющая ширину от точности

4. число, задающее точность

5. код форматирования

Код | Описание
b | Целое число в двоичном формате
c | Односимвольная строка
i | Десятичное целое со знаком
e | Экспоненциальное представление дробного числа
E | Эквивалент e, но используется заглавная буква E
f | Обычная форма дробного числа
g | Наикратчайшее из e и f
G | Наикратчайшее из E и f
o | Целое число в восьмеричном формате
s | Строка
u | Десятичное целое без знака
x | Шестнадцатеричное целое (строчные буквы)
X | Шестнадцатеричное целое (заглавные буквы)

Пример:

sprintf(“%06.2f + %.2f ”, Math::PI, Math::E ) \

# “003.14 + 2.72”

Метод printf

Метод printf выводит посредством метода print строку, обработанную методом sprintf. Пример:

a = "%i*%i+%i = %i\n”

printf( a, 3, 5, 8, 3*5+8) # 3*5+8 = 23

print (sprintf( a, 3, 5, 8, 3*5+8 )) # 3*5+8 = 23

Лекция 7. Логический тип и операторы ветвления

Логический тип в Ruby представлен:

1. логическими методами;

2. логическими объектами (true и false);

3. логическими операторами;

4. методами сравнения.

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

Следует помнить, что false и nil трактуются как ложные (в логическом контексте). Все остальное, что вычисляется, является истинным.

Стандартные логические операторы

Оператор |  | Описание | Пример
Литера | Символ |  | Литера | Символ
or | || | дизъюнкция, лог. «или» | a or b | a || b
and | && | конъюнкция, лог. «и» | a and b | a && b
not | ! | отрицание, лог. «не» | not a | !a

Необходимо заметить, что операторы, записанные литерами, имеют меньший приоритет, чем символьные операторы.

Методы сравнения

Метод | Описание
== | сравнение
< | меньше
<= | меньше или равно
> | больше
>= | больше или равно
!= | не равно

Логические методы

Логические методы принадлежат классу, в котором они объявлены. Для примера рассмотрим метод, который позволяет определить существует ли его аргумент. Это метод defined?, который возвращает nil, если аргумент не определен, а иначе – его описание. Пример:

defined?( 1 ) # “expression”

defined?( a ) # nil

defined?( printf ) # “method”

Описание остальных логических методов следует искать в описании классов, которым они принадлежат.

Операторы ветвления

Как и любой другой язык программирования высокого уровня Ruby имеет стандартный набор операторов ветвления. Это операторы if, case-when и тернарный оператор. Все эти операторы могут использоваться в правой части оператора присваивания. Пример:

a = if b<10 then 5

else 6

end

Оператор if

Общая форма оператора if:

if <лог. выр.> [then <оператор>]

<тело>

[ elsif <лог. выр.> [then <оператор>]

<тело> ]*

[ else

<тело> ]

end

Здесь <лог. выр.> должно принадлежать логическому типу. Запись [then <оператор>] означает, что этот оператор является необязательным в общем случае, но обязателен, если в строке после него идет оператор. Можно использовать любое количество блоков elsif и только один блок else. Блоки elsif и else являются необязательными и могут пропускаться. Оператор end в конце всех блоков обязателен. Пример:

a= if b < 10 then 1

elsif b < 20 then 2

elsif b < 30 then 3

else 0

end

Помимо основной формы записи оператора if существует и сокращенная. Она используется, когда необходим только блок if, а его тело состоит из одного оператора. Сокращенная форма оператора if:

<оператор> if <лог. выр.>

Эта запись воспринимается легче, чем обычная, но использовать ее можно только в частных случаях. Пример:

a = 10 if b<10

Оператор case-when

Оператор case-when используется тогда, когда необходимо оперировать большим количеством альтернатив. Общая форма оператора case-when:

case <выражение>

when <тест1> [then <оператор>]

<тело>

[ when <тестN> [then <оператор>]

<тело>]*

[else

<тело> ]

end

Оператор [then <оператор>] используется точно также как и в операторе if. Блок else является необязательным. Оператор end в конце всех блоков обязателен. Здесь <выражение> последовательно сравнивается с каждым из тестов посредством оператора сравнения, после чего выполняется соответствующее тело. Оператор case-when возвращает последнее вычисленное выражение или nil (если такого нет). Это удобно, для присваивания значений в зависимости от условия. Пример:

a = case b

when 0..9 then 1

when 10..99 then 2

else 0

end

Тернарный оператор

Язык Ruby позаимствовал у языка Си этот изящный оператор. На самом деле это более компактная запись оператора if-then-else. Общая форма записи тернарного оператора:

<лог.выр.>?<выр.истины>:<выр.лжи>

Если <лог.выр.> истинно, то выполняется <выр.истины>, иначе <выр.лжи>. Пример:

a = ( b < 10 ) ? ( 1 ) : ( 0 )

Лекция 7.5. Циклы

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

Цикл loop

Как всегда начнем наш рассказ с самого простейшего. И как всегда на базе этого простейшего будет строиться все остальное. В данном случае начнем с вечного цикла. Цикл loop выполняет операторы внутри своего тела до тех пор, пока не будет принудительно прерван. Его общий вид:

loop [do]

<тело>

end

Пример:

loop do

puts(“Ждите зачета…”)

end

Данный пример будет выполняться до тех пор, пока программа не будет прервана. Мы будем моделировать данный бесконечный цикл на базе оставшихся двух.

Цикл while

Его общий вид:

while <лог. выр.> [do]

<тело>

end

Выполняется до тех пор, пока истинно его <лог. выр.>. Пример:

while( true ) do

puts(“Ждите зачета …”)

end

Вышеуказанный пример показывает, как свести цикл while к вечному циклу (loop).

Помимо обычной формы записи цикла существует и сокращенная постфиксная форма:

<оператор> while <лог. выр.>

Она используется тогда, когда необходимо поместить в цикл один оператор. Пример:

puts(“Ждите зачета …”) while( true )

Цикл until

Его общий вид:

until <лог. выр.> [do]

<тело>

end

Выполняется до тех пор, пока его <лог. выр.> ложно. Пример:

until( false ) do

puts(“Ждите зачета …”)

end

Помимо обычной формы записи цикла существует и сокращенная постфиксная форма:

<оператор> until <лог. выр.>

Она используется тогда, когда необходимо поместить в цикл один оператор. Пример:

puts(“Ждите зачета …”) until( false )

Лекция 8. Итераторы

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

Итератор upto

Итератор upto уникален тем, что он есть и у строк, и у целых чисел. Его общий вид:

<начало>.upto(<конец>){ [|<счетчик>|]

<тело>

}

Заметим, что <счетчик> итератора является необязательным. Значения итератора передаются в блок итератора посредством <счетчика>. Причем это применимо ко всем итераторам (кроме for).

Итератор step последовательно возвращает в тело итератора значения из промежутка <начало>..<конец>. Пример:

3.upto( 5 ){

print “1”

} # 111

‘aa’.upto(‘ac’){ |str|

print( str )

} # aaabac

Итератор downto

Итератор downto является темной стороной итератора upto. Его общий вид:

<конец>.downto(<начало>){ [|<счетчик>|]

<тело>

}

Он последовательно возвращает в тело итератора в обратном порядке целые числа в промежутке <начало>..<конец>. Пример:

5.downto( 3 ){ |i|

print i

} # 543

Итератор step

Все предыдущие итераторы на каждом своем шаге прибавляли единицу. Данный итератор позволяет явно задать размер приращения. Его общий вид:

<начало>.step( <конец>, <шаг>){ [|<счетчик>|]

<тело>

}

Он последовательно возвращает в тело итератора целые числа из промежутка <начало>..<конец> с шагом <шаг>. Пример:

1.step( 10, 2 ){ |i|

print i

} # 13579

Итератор times

Этот итератор принадлежит целым числам. Общий вид итератора:

<конец>.times{ [|<счетчик>|]

<тело>

}

Он последовательно возвращает в тело итератора значения из промежутка 0…<конец>. Пример:

0.upto( 2 ){ |i|

print( i.to_s )

} # 012

2.times{ |i|

print( i.to_s )

} # 01

Итераторы each и for

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

1. Массивы

2. Строки

3. Диапазоны (0..2 или 0…3)

В массивах он перечисляет элементы, в строках он перечисляет строки разделенные символом перевода строки, а в диапазонах он перечисляет элементы, входящие в данный диапазон значений. Пример:

[1, 2, 3].each{ |i| print i } # 123

“1\n2\n3”.each{ |str| print str } # 123

(1..3).each{ |i| print i } # 123

Но и это еще не все. В языке Ruby существует однозначное соответствие между итератором each и итератором for. Формат записи итератора for следующий:

for <счетчик> in <множество> {

<тело>

}

Итератор for является иной записью итератора each. Поэтому все типы данных обладающие итератором each получают бесплатно итератор for. Пример:

[1, 2, 3].each{ |i| print i } # 123

for i in [1, 2, 3] { print i } # 123

Конструкции управления циклами и итераторами

В Ruby имеются четыре конструкции, задаваемые ключевыми словами break, redo, next и retry, которые изменяют обычный порядок выполнения циклов и итераторов.

Конструкция | Описание
break | Немедленно прекращает выполнение цикла и управление передается на следующую за циклом (итератором) команду
 | for i in 1 .. 5 { print i break if i == 3 print "*" } # 1*2*3
redo | Повторяет тело цикла с начала, не пересчитывая условие выполнения цикла (не переходя к следующему элементу в случае итератора)
 | for i in 1 .. 5 { print i redo if i == 3 print "*" } # 1*2*333333…333 <зациклился>
next | Пропускает часть тела цикла, следующую за ним, и переходит к следующей итерации
 | for i in 1 .. 5 { print i next if i == 3 print "*" } # 1*2*34*5
retry | Начинает выполнение цикла с самого начала
 | for i in 1 .. 5 { print i retry if i == 3 print "*" } # 1*2*31*2*31*2*…31*2*31*2* <зациклился>
Сейчас на сайте
Гостей: 1
Пользователей: 0
Всего: 1

Авторизация
Логин
Пароль
Запомнить меня

Новости RubyForge
· rb-itunes, a cross-platform iTunes API:Status update - 0.1.4 in SVN
· scRUBYt! 0.2.8 is out
· matlab-ruby 1.0.0
· Active Warehouse:ActiveWarehouse ETL 0.7.2
· Ruby-on-Rest 0.0.3
· RMagick OS X installer 0.3.0
· Sequel 0.0.16
· Composite Primary Keys for ActiveRecords:Composite Primary Key support for Oracle, Postgres, Sqlite
· le bliki - First alpha version (without any interest)
· Google GData API access via Ruby wrapper

Читать далее...

Новости RAA
· sys-uname
· ruport-util
· ruby-terminfo
· swin
· rupircd
· flickraw
· win32-file
· rb-gsl
· sisu
· ruby-odbc

Читать далее...

Новости Ruby-Doc
· Ruby-doc is being remodeled

Читать далее...

Новости rubyXML
· LibXML-Ruby 0.3.6
· Libxml API Docs updated
· Moved to a New Server
· Yet Another Java Bridge
· RubyStuff Shop is Open

Читать далее...

Новости RubyGardenWiki
· GregFox
· ProgrammingRubyTwo/Ex...
· WindowsInstaller
· HamptonRoadsGroup
· RubyProjects
· HomePage
· NorthernVirginia
· StephanKaemper
· CzechGroup
· PostHelloWorld

Читать далее...

Главная страница :: Добавить новость :: Контакт с автором
Экспорт [image] новостей

Сайт сделан при поддержке LinuxSoft.ru.


Ruby — Викиучебник

Ruby

Материал из Викиучебника.

Перейти к: навигация, поиск

Человек создан для творчества, и я всегда знал, что люблю творить. Увы, я обделён талантом художника или музыканта. Зато умею писать программы.

Я хочу, чтобы компьютер был моим слугой, а не господином, поэтому я должен уметь быстро и эффективно объяснить ему, что делать.

Йокихиро Мацумото

Программы должны быть написаны так, чтобы их могли читать люди, и лишь иногда так, чтобы их могли выполнять машины.

Абельсон и Сассман

Ру́би — интерпретируемый язык программирования высокого уровня. Обладает независимой от операционной системы реализацией многопоточности, строгой динамической типизацией, «сборщиком мусора» и многими другими возможностями, поддерживающими много разных парадигм программирования, прежде всего объектную. Руби был задуман в 1993 году (24 февраля) японцем Юкихиро Мацумото (по прозвищу Мац), стремившимся создать язык, совмещающий все качества других языков, способствующие облегчению труда программиста.

Этот учебник намерен осветить все тонкости программирования на Ruby. Повествование идёт «по спирали»: начиная с самых общих понятий, темы рассматриваются по нескольку раз с нарастающей подробностью.

Учебник пока неполон. Читайте иноязычную литературу по Ruby и с новыми знаниями присоединяйтесь к написанию. Да-да, каждый может участвовать: научитесь работать в вики-среде, с остающимися вопросами выступайте на странице обсуждения учебника.

Содержание

  • 1 Основные свойства Ruby
  • 2 Начало работы
    • 2.1 Первая программа
    • 2.2 Полигон для испытания программ
      • 2.2.1 fxri: полигон и справочник
      • 2.2.2 Полигон в сети
    • 2.3 Комментарии и демонстрация результата
    • 2.4 Вывод на экран
    • 2.5 Переменные
      • 2.5.1 Переменные указывают на объект
  • 3 Типы данных
    • 3.1 Числа
    • 3.2 Логический тип
      • 3.2.1 Логические операции
      • 3.2.2 Методы сравнения
    • 3.3 Массивы
    • 3.4 Строки
    • 3.5 Ассоциативные массивы
    • 3.6 Диапазоны значений
    • 3.7 Классы и объекты
  • 4 Подробнее о числах
    • 4.1 Целые числа
    • 4.2 Дробные числа
    • 4.3 Семейный портрет чисел
    • 4.4 Арифметические операции
      • 4.4.1 Поразрядная арифметика
      • 4.4.2 Операции с присвоением
    • 4.5 Методы явного преобразования типов
    • 4.6 Случайное число
    • 4.7 Хитрости
    • 4.8 Задачи
  • 5 Подробнее о массивах
    • 5.1 Способы создания массива
    • 5.2 Диапазоны
    • 5.3 О двумерных массивах
    • 5.4 Методы работы с массивами
      • 5.4.1 Получение размера массива
      • 5.4.2 Поиск максимального/минимального элемента
      • 5.4.3 Упорядочение
      • 5.4.4 Обращение массива
      • 5.4.5 Сложение/вычитание массивов
      • 5.4.6 Объединение и пересечение массивов (как множеств)
      • 5.4.7 Удаление дубликатов
      • 5.4.8 Сплющивание массивов
      • 5.4.9 Транспонирование двумерного массива
      • 5.4.10 Размножение массивов
      • 5.4.11 Функциональность стека
      • 5.4.12 Функциональность очереди и списка
      • 5.4.13 Мнемограмма для методов стека/очереди/списка
      • 5.4.14 Создание своих классов, работающих как массивы
    • 5.5 Логические методы
      • 5.5.1 Есть элемент в массиве?
      • 5.5.2 Массив пустой?
    • 5.6 Итераторы
      • 5.6.1 Изменение всех элементов массива
      • 5.6.2 Отбор элементов по признаку
      • 5.6.3 Суммирование/произведение/агрегация элементов
      • 5.6.4 Разбиение надвое
    • 5.7 Логические итераторы
      • 5.7.1 Все ли элементы удовлетворяют условию?
      • 5.7.2 Хотя бы один элемент удовлетворяет условию?
    • 5.8 Хитрости
    • 5.9 Задачи про массивы
      • 5.9.1 Одномерные
        • 5.9.1.1 Одномерные целочисленные
      • 5.9.2 Двумерные
        • 5.9.2.1 Двумерные целочисленные
  • 6 Подробнее об ассоциативных массивах
    • 6.1 Что используется в качестве ключей?
    • 6.2 Способы создания ассоциативного массива
      • 6.2.1 Из одномерного массива
      • 6.2.2 Из двумерного массива
      • 6.2.3 Нет данных
      • 6.2.4 Известен только тип значений
      • 6.2.5 Всё известно и дано
    • 6.3 Методы работы с ассоциативными массивами
      • 6.3.1 Получение массива значений и массива ключей
      • 6.3.2 Замена ключей на значения
      • 6.3.3 Обновление пары
      • 6.3.4 Размер ассоциативного массива
      • 6.3.5 Удаление пары по ключу
      • 6.3.6 Удаление произвольной пары
      • 6.3.7 Преобразовать в индексный массив
      • 6.3.8 Упорядочение хеша
      • 6.3.9 Поиск максимальной/минимальной пары
    • 6.4 Логические методы
      • 6.4.1 Хеш пустой?
      • 6.4.2 Есть такой ключ?
      • 6.4.3 Есть такое значение?
    • 6.5 Итераторы
    • 6.6 Хитрости
    • 6.7 Задачи
  • 7 Подробнее о строках
    • 7.1 Способы создания строки
      • 7.1.1 Для чего нужны работящие и ленивые строки?
    • 7.2 Методы работы со строками
      • 7.2.1 Арифметика строк
      • 7.2.2 Преобразование в массив или путешествие туда и обратно
      • 7.2.3 Длина строки
      • 7.2.4 Получение подстрок
      • 7.2.5 Строка-перевертыш
      • 7.2.6 Меняю шило на мыло!
      • 7.2.7 Сканируем текст на оШЫбки
      • 7.2.8 Правила работы со строками
        • 7.2.8.1 Символьный класс
        • 7.2.8.2 Квантификатор
        • 7.2.8.3 Альтернатива
        • 7.2.8.4 Группировка
        • 7.2.8.5 Фиксирующая директива
        • 7.2.8.6 Модификатор
      • 7.2.9 Правильное разбиение
      • 7.2.10 Правильная замена
      • 7.2.11 Правильный поиск
      • 7.2.12 Жадность
    • 7.3 Хитрости
      • 7.3.1 Перенос по словам
      • 7.3.2 Методы преобразования к строке
    • 7.4 Задачи
  • 8 Подробнее о методах
    • 8.1 Создание метода
      • 8.1.1 Указание значений по умолчанию
      • 8.1.2 Методы с восклицательным и вопросительным знаком
      • 8.1.3 Методы присвоения
      • 8.1.4 Операторы
      • 8.1.5 "Поглощение" аргументов метода
    • 8.2 Подробнее о блоках
      • 8.2.1 Зачем они нужны
      • 8.2.2 Как создать блок
      • 8.2.3 Блоки принимают аргументы
      • 8.2.4 Свои методы с блоками
      • 8.2.5 Некоторые применения блоков
    • 8.3 Методы, которых не было
      • 8.3.1 Ширяевский .size
      • 8.3.2 Случайное число из диапазона
    • 8.4 Способы расширения библиотеки методов
      • 8.4.1 Как добавить метод к массиву/строке/венику?
      • 8.4.2 Программист-разрушитель
    • 8.5 Как написать свой итератор?
    • 8.6 Как написать свой класс?
    • 8.7 Наследовать или перемешать?
    • 8.8 Как сделать свою библиотеку методов?
  • 9 Матрицы и вектора
    • 9.1 Как создать новую матрицу?
    • 9.2 Как послать в матрицу 2-мерный массив?
    • 9.3 Как изменить элемент матрицы?
      • 9.3.1 Исправляем оплошность разработчиков
    • 9.4 Методы работы с матрицами
    • 9.5 Зачем нужны вектора?
    • 9.6 Хитрости
    • 9.7 Задачи
      • 9.7.1 Решение систем линейных уравнений методом Гаусса
  • 10 Работа с файлами
    • 10.1 Чтение при помощи класса IO
    • 10.2 Перенаправление потока
    • 10.3 Универсальный способ работы с файлами
    • 10.4 Хитрости
    • 10.5 Задачи
  • 11 Сети
    • 11.1 Как написать троян?
      • 11.1.1 Построение серверной части
      • 11.1.2 Построение клиентской части
    • 11.2 Как создать сетевой блокнот?
      • 11.2.1 Первое приближение
      • 11.2.2 Добавляем ERB
      • 11.2.3 Выносим ERB-шаблон во внешний файл
      • 11.2.4 ERB-шаблон превращается в ERB-сервлет
    • 11.3 Гнезда, которые свили не птицы
    • 11.4 Как пропинговать компьютер в сети?
    • 11.5 Простейший датчик работы службы
    • 11.6 Датчик обновления сайта
    • 11.7 Приемо-передача файлов
    • 11.8 Как скачать HTML-страницу
      • 11.8.1 Запрос заголовка
      • 11.8.2 Работа через прокси
    • 11.9 Вам письмо или You have got mail
    • 11.10 Запускаем свой веб-сервер
      • 11.10.1 Сервлетки в рубиновом соусе
  • 12 Приложения
  • 13 Дальнейшее чтение
    • 13.1 Русскоязычные ресурсы
    • 13.2 Русскоязычная литература
    • 13.3 Иноязычная литература
    • 13.4 Полезные ссылки

[править] Основные свойства Ruby

Интерпретируемый язык:

  • Возможность прямых системных вызовов
  • Мощная поддержка операций со строками и регулярными выражениями (правилами)
  • Мгновенное проявление изменений во время разработки
  • Отсутствие стадии компиляции

Простое и быстрое программирование:

  • Не надо объявлять переменные
  • Переменные не типизированы
  • Простой и последовательный синтаксис
  • Автоматическое управление оперативной памятью.

Объектно-ориентированное программирование:

  • Всё есть объект. Даже имя класса есть экземпляр класса Class
  • Классы, методы, наследование, полиморфизм, инкапсуляция и т. д.
  • Методы-одиночки
  • Примеси при помощи модулей (возможность расширить класс без наследования)
  • Итераторы и блоки
Удобства:
  • Неограниченный диапазон значений целых чисел
  • Модель обработки исключений
  • Все операторы возвращают значения, даже управляющие структуры
  • Динамическая загрузка
  • Механизм перехвата исключений
  • Поддержка потоков; как собственных, так и систем семейства Unix
Недостатки:
  • Неуправляемость некоторых процессов (таких, как выделение памяти), невозможность задания низкоуровневых структур данных или подпрограмм
  • Невозможность компиляции и сопутствующей ей оптимизации программы
    • Открытость исходного кода даже в готовой программе (есть средство упаковки исходного кода в .exe-файл под Windows)
  • Следствие двух первых недостатков — весьма низкая скорость запуска и выполнения программ

[править] Начало работы

От читателя требуется общее знание компьютеров, включая навыки работы с файловой системой и текстовыми файлами. Прежде чем начать программировать на Руби, нужно установить интерпретатор и обустроить для себя удобную среду для создания программ. Для испытания кода из учебника можно воспользоваться сетевым интерактивным интерпретатором.

Об установке и настройке рабочей среды Руби читайте приложение → /Установка .

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

Чтобы запустить программу с именем TecToBa9_nporpamma.rb (программы на Руби обычно имеют расширение .rb), в Unix-e нужно вызвать консоль и набрать:

% ruby TecToBa9_nporpamma.rb

Чтобы запустить программу из Windows нужно дважды кликнуть на файл с программой. Чтобы окно не исчезало и были видны результаты надо перед местами выхода из программы поставить команду ожидания ввода

STDIN.getc
☞
Именам программ в Windows принято давать расширение .rbw, что позволяет использовать Windows-версию интерпретатора rubyw, который надоедающее окошко просто не вызывает. Интерпретатор ruby предназначен для работы в консоли. Для интерпретатора rubyw вставлять код STDIN.getc перед местами выхода бесполезно

[править] Первая программа

Откройте редактор и напишите программу. По традиции первая программа должна просто выводить приветствие миру:

puts "Привет Мир!"

Сохраните её в файл npuBeT_mup.rb и запустите. На экране будет красоваться надпись:

Привет Мир!

На этом традиционную часть можно считать выполненной.

[править] Полигон для испытания программ

Чтобы посмотреть результат выполнения большинства примеров из учебника, не обязательно создавать файл. Достаточно использовать полигон для испытания программ на Руби irb, который входит во все дистрибутивы и сам написан на Руби. Запускается он командой

% irb

Полигон — это рубиговорящая командная строка. Она показывает вам результат выполнения каждой строки кода:

% irb(main):001:0> [1,2,3,4]
% => [1, 2, 3, 4]

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

% irb(main):002:0> "text".methods
% => ["respond_to?", "%", "index", "select", ...]

Или посчитаем:

% irb(main):002:0> 14 * 5
% => 70

Для выхода с полигона — набрать exit.

По причине того, что интерактивный терминал (полигон) поддерживает автозаполнение при введении табуляции, исходные тексты программ на Ruby принято «отбивать» пробелами (чтобы текст программы можно было просто скопировать в терминал и увидеть, как он работает). Если в irb скопировать программу, «отбитую» табуляциями, интерактивный терминал будет прерывать каждую строчку ввода чтобы предложить варианты автозаполнения.

[править] fxri: полигон и справочник

Внешний вид программы fxri

В последних версиях дистрибутива «Установка за один щелчок» для Windows появилась утилита fxri. Это оконная кроссплатформенная программа, вобравшая в себя функционал ri и irb. Буковки fx в начале означают использование библиотеки FXRuby. Последние две буквы ri означают Ruby Informer (Информатор о классах и методах в языке Руби).

У fxri три окна: в левом перечислены все методы c описаниями. Верхнее правое окно рассказывает о выбранном методе. Нижнее левое окно реализует irb, описанную выше.

[править] Полигон в сети

Иногда интерпретатора Руби нет под рукой, приходится использовать сетевой полигон. Он позволяет выполнять код на Руби прямо из браузера.

[править] Комментарии и демонстрация результата

Комментарием называется часть программного кода, пропускаемая при обработке (интерпретации или компиляции).

В Ruby знаком начала комментария служит #. Все, что между ним и концом строки пропускается. Пример:

puts 2+2          # это комментарий
puts "Привет!"    # тоже комментарий

Результат иллюстрируемого кода будет располагаться после последовательности #->. Пример:

puts 2+2          #-> 4
puts "Привет"     #-> Привет

[править] Вывод на экран

В Ruby есть много методов вывода: print, printf, p, puts, .display и другие. Но мы использовать будем два:

  • метод puts. После вывода строки осуществляет переход на новую. Если приходится выводить объект, не являющийся строкой, то puts вызывает метод .to_s для преобразования его к строке;
  • метод p. Имеет самое короткое название, из-за чего часто используется для отладки. Так же, как и puts, делает перевод на новую строку после вывода. Перед выводом любого объекта (в том числе и строки) на экран, вызывает метод .inspect.

Примеры вывода на экран:

puts [1,2,3,4]      #-> 1\n2\n3\n4
                    # \n означает перевод строки-возврат каретки
p [1,2,3,4]         #-> [1,2,3,4]
puts "npuBeT!"      #-> npuBeT!
p "npuBeT!"         #-> "npuBeT!"
puts 5              #-> 5
p 5                 #-> 5

Как видно из примера, результаты во время вывода строк и массивов существенно различаются. Если вас не смущают кавычки в результате вывода, то смело используйте p, если смущают, то puts.

☞
Иногда возникает ситуация, когда при попытке вывода на экран русскоязычной строки при помощи метода p выводится непонятный код вида:
"\323\367\350\362\345 \320\363\341\350!"

Чтобы избежать таких ситуаций следует поместить в начало программы следующий программный код:

$KCODE = "utf-8"
Кодировка utf-8 используется в SciTE. При использовании других редакторов, может потребоваться смена кодировки на соответствующую (зависит от редактора и операционной системы)

[править] Переменные

Переменные используются, чтобы сохранить промежуточный результат вычислений. Имя переменной в Ruby должно:

  • начинаться со строчной буквы или знака подчеркивания;
  • состоять из латинских букв, цифр и знака подчеркивания.

Примеры переменных:

maccuB
nepemeHHa9
gpyra9_nepemeHHa9
HenpaBuJIbHa9_nepemeHHa9    # неправильное имя. Начинается с прописной
3JIemeHT                    # неправильное имя. Начинается с цифры
_3JIemeHT                   # а вот как можно
___                         # немного странное, но корректное имя переменной

Переменная может иметь имя НЕ только латинское но и, например, русское. Для того, чтоб использовать такую возможность нужно, чтоб весь текст программы был написан в кодировке UTF-8, а интерпретатор запускался с параметром -KU

Присвоение делается знаком равенства (=), вот так:

maccuB = [1,2,3,4]
nepemeHHa9 = maccuB + [1,2]
gpyra9_nepemeHHa9 = nepemeHHa9 - maccuB

То, что справа от =, прежде чем стать значением переменной, обычно полностью вычисляется. Наша nepemeHHa9 будет содержать массив [1, 2, 3, 4, 1, 2], ибо именно таков результат действия maccuB + [1, 2]. Плюс (+) с массивами поступает именно так: прицепляет второй массив в хвост первого.

[править] Переменные указывают на объект

Фокус-покус:

nogpyra = "Даша"
k_HAM_B_rocTu_ugeT = nogpyra
puts nogpyra                     #-> "Даша",  разумеется
k_HAM_B_rocTu_ugeT[0] = "М" # меняем первую (номер ноль) букву у переменной-строки
puts nogpyra              #-> "Маша"
                              # На первый взгляд, странно и неожиданно

Значение, возвращаемое первой переменной изменилось потому, что

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

Сами данные (объект) лежат где-то в другом месте. В виду этого естественно, что напрямую изменяя сам объект, указуемый переменной, все другие переменные, указывающие на этот объект, будут возвращать то изменённое значение (также будут изменяться).

Чтобы nogpyra наша осталась "Даша", надо в переменную занести её клон:

nogpyra = "Даша"
k_HAM_B_rocTu_ugeT = nogpyra.clone
k_HAM_B_rocTu_ugeT[0] = "М"
# Но изменили мы лишь клон. Дома в сохранности сидит настоящая:
puts nogpyra #-> "Даша"
☞
Можно создавать копии объектов ещё методом .dup. Разницу между ними Вы потом поймёте

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

#...
k_HAM_B_rocTu_ugeT = "Аристарх"; # Создаётся новый объект, переменная переводится на него
p nogpyra #-> "Даша"

[править] Типы данных

Данные любого типа в Ruby суть объекты тех или иных классов. Самые используемые встроенные типы данных: Fixnum (целые числа, меньшие 231), Bignum (целые числа, большие 231), Float (числа с плавающей точкой), Array (массивы), String (строки), Hash (ассоциативные массивы). Естественно, что это только базовые типы, но их вполне хватает для широкого спектра задач.

Все абстрактные типы данных
Все абстрактные типы данных

[править] Числа

Числа в Ruby выглядят так:

5     # целое число
-12   # отрицательное целое число
4.5   # число с плавающей точкой
076   # восьмеричное число
0b010 # двоичное число
0x89  # шестнадцатиричное число

Не будем пока углубляться и мельком взглянем на другие типы данных.

[править] Логический тип

[править] Логические операции

название операции | символ операции | литерное обозначение
логическое "или" | || | or
логическое "и" | && | and
логическое "не" | ! | not
логическое "исключающее или" | ^ |

Логический (булев) тип — это вариация на тему «да» или «нет». В Ruby он представлен двумя предопределенными переменными true («истина» или «да») и false («ложь» или «нет»). Появляется логический тип в результате логических операций или вызова логических методов.

Чаще всего логический тип возникает как результат сравнения.

[править] Методы сравнения

название метода | символ
равно | ==
не равно | !=
меньше | <
больше | >
меньше или равно | <=
больше или равно | >=
Часто молодые программисты, когда надо написать «меньше или равно», пишут знак =< вместо <=. Запомнить правильное написание можно вслух проговорив «меньше или равно» и в этом же порядке писать < и =
  • Традиционно имена логических методов заканчиваются на ? (знак вопроса).
  • В качестве false может выступать nil, а в качестве true — любой объект.
  • nil — это символ пустоты.

[править] Массивы

Разработчики Руби решили не реализовывать особых классов для динамических массивов, списков, стеков и тому подобного. Они все это реализовали в массивах — структурах данных типа (или класса, — в Руби всё равно) Array. Сделано это путем добавления специальных методов; например, методы .push и .pop для стека. Особенности массивов в Руби:

  • Нет ограничений (это общий принцип языка). Массивы могут быть сколь угодно длинными.
  • Динамичность: размер массива легко меняется.
  • Гетерогенность: один массив может хранить данные разных типов.
  • Библиотека итераторов на каждый случай жизни. Эта возможность позволяет не использовать циклов для обработки данных в массивах, а следовательно избегать множества ошибок, связанных с неосторожным обращением с циклами. Итераторы реализуются на высочайшем уровне.
  • Много других методов. Все элементарные задачи для массивов решаются вызовом нужного метода.

Массив лучше всего вообразить как гусеницу и притом поезд с лапками-запятыми вместо колёс.

[1, 0, 740, 14, 88]       # целочисленный массив
['a',"й", "6",'Br', "Это массив строк, о них Вы скоро узнаете"]
[[1,2],[3,4]]     # двумерный целочисленный массив.
["1й элемент смешанного массива" , "7.343" , [4, "вепрь"] ,
   [3, 67, 4326, 12, 3781357, 84221, "строка делает этот подмассив смешанным, но это не беда"]]
maccuB=["Этот массив пойдёт в переменную maccuB", "Як-цуп-цоп, парви каридула"]

Ползёт он всегда влево; на левом же конце его локомотив — первый элемент. Первый потому, что элементы упорядочены. Если знаете порядковый номер элемента, то легко получить его значение:

maccuB[1] #=> "Як-цуп-цоп, парви каридула"

В мире поездов-гусениц счёт вагонов начинается не с локомотива, а со следующего за ним вагона. Локомотив — это как бы нулевой вагон.

maccuB[0] #=> "Этот массив пойдёт в переменную maccuB"

Массивы кажутся странными с лапками, но частенько бывают полезны при написании компьютерных программ. Возможно, вы даже поймёте, почему он ползёт всегда влево.

[править] Строки

Строки — это ряды букв и других символов. В Ruby строки используют наработки языка Перл. Вот небольшой список их возможностей:

  • Нет ограничений. Длина строки может достигать поистине фантастических размеров.
  • Динамичность. Строки можно расширять или уменьшать (для этого есть методы + и []).
  • Любой объект преобразуется к строке (методы .inspect и .to_s есть у любого объекта).
  • Строка обладает обширной библиотекой методов, которые работают с правилами (это методы .gsub, .match, .scan, .split).
☞
Правила — это новое название регулярных выражений. В текущей версии Ruby они называются регулярными выражениями, но давайте смотреть в будущее. Perl 6 уже не имеет понятия «регулярное выражение», заменив его «правилами» и «грамматиками»;
  • Можно вставлять произвольный код на языке Ruby в строку. После выполнения код заместится результатом.

Строки начинаются и заканчиваются " (лапками) или ' (типографским апострофом). Пример:

"мама мыла раму"        # строка в надёжных лапках
'рама сопротивлялась'   # строка в апострофах

Строки подобны массивам символов, поэтому их часто преобразуют к массивам, чтобы использовать богатый набор методов, а потом результат делают строкой.

[править] Ассоциативные массивы

Ассоциативные массивы подобны массивам упорядоченных пар. Работают они подобно словарям: фигурная скобка символизирует боковой вид на открытую книгу, а стрелка => покажет читателю связь каждого одного с чем-то другим. Вторая фигурная скобка говорит, что пора закрывать книгу.

{ "мама" => "мыла раму", 807 => "Это число улыбается!"}

Но можно и без фигурных скобок, одной стрелкой:

 "npeBeg" => "MegBeg"

Например:
puts maccuB["мама"] #-> мыла раму
puts maccuB["807"] #-> nil
puts maccuB[807] #-> Это число улыбается!
puts maccuB[1] #-> nil
puts maccuB["npeBeg"]#-> MegBeg

Ассоциативные массивы оставляют возможность хранения данных разного типа только в ассоциативном виде.

[править] Диапазоны значений

Чтобы было удобней получать подмассив или подстроку, был введен простенький тип данных — диапазон (класс Range). Диапазон формируется тремя элементами: начало, конец и тип протяженности (символ .. или ...). Начало и конец должны быть одного типа данных (одного класса) и быть перечислимыми, что значит, иметь метод .succ. Пример диапазонов:

'a'..'z'
'a'...'z'  # то же, что и 'a'..'y'
1..100
1...100    # то же, что и 1..99

Две точки напомнят вам о мотоцикле, на котором Диапазон проезжает от первого указанного объекта к его .succ («сакк» — так на Руби называется следующий за данным объект), и так до тех пор, пока не доберётся до последнего. Три точки ещё проще: мотоцикл, дорогу которому перегородил шлагбаум, как раз перед последним элементом.

☞
Объекты, которые имеют метод .succ называют последовательными. Это значит, что по текущему элементу можно достоверно определить следующий (используя метод .succ)

[править] Классы и объекты

Самодельные и неабстрактные, составные типы данных называются классами. Если это вам новость, то почитайте викиучебник об объектно-ориентированном программировании или статью в Википедии. Вообще, в Руби всё в конечном счёте принадлежит классу Object.

cTpoka = "Aз есмь строка"
cTpoka.class              #-> объект класса String
cTpoka.class.superclass   #-> подкласса класса Object

Классы можно определять и создавать по ним объекты. Внутри класса может быть много всего интересного, и у него может быть фамильное дерево (именно, ибо множественное наследование в Руби не позволено). И ещё много всего интересного можно сделать с классами и объектами. Но об этом позже.

[править] Подробнее о числах

Изначально числа представлены тремя типами: два целых типа (классы Fixnum и Bignum) и один дробный (класс Float). Возможно подключение дополнительных типов, — например, комплексных и рациональных чисел, но пока ограничимся тремя.

[править] Целые числа

Целые числа в Ruby не ограничены по величине, то есть могут хранить сколь угодно большие значения. Для обеспечения такого волшебного свойства было создано два класса. Один из них хранит числа меньшие 231 (по модулю), а второй — всё, что больше. По сути, для больших чисел создается массив из маленьких, а раз массив не имеет ограничений по длине, то и число получается неограниченным по значению.

Как только число типа Fixnum становится больше 231 (по модулю), то оно преобразовывается к классу Bignum. Если число типа Bignum становится меньше 231, то оно преобразовывается к типу Fixnum.

При записи целых чисел сначала указывается знак числа (знак + обычно не пишется). Далее идет основание системы счисления, в которой задается число (если оно отлично от десятичной): 0 — для восьмеричной, 0x — для шестнадцатеричной, 0b — для двоичной. Затем идет последовательность цифр, выражающих число в данной системе счисления. При записи чисел можно использовать символ подчеркивания, который игнорируется при обработке. Чтобы закрепить вышесказанное, посмотрим примеры целых чисел:

# тип Fixnum
123_456     # подчеркивание игнорируется
-567         # отрицательное число
0xbad       # шестнадцатеричное число
0377        # восьмеричное
-0b101010   # отрицательное двоичное
0b0101_0101 # подчеркивание игнорируется

# тип Bignum
123_456_789_123_456    # подчеркивание игнорируется
-123_456_789_123_456   # отрицательное
07777777777777777777   # восьмеричное большое

Как видно из примеров, маленькие целые (Fixnum) и большие целые (Bignum) отличаются только значением.

[править] Дробные числа

Дробные числа задаются только в десятичной системе счисления, при этом для отделения дробной части используется символ . (точка). Для задания дробных чисел может быть применена и экспоненциальная форма записи: два различных представления 0.1234e2 и 1234e-2 задают одно и тоже число 12.34.

# тип Float
-12.34     # отрицательное дробное
0.1234е2   # экспоненциальная форма для числа 12.34
1234е-2    # экспоненциальная форма для числа 12.34

Следует упомянуть, что дробные числа имеют фиксированный диапазон значений в отличие от целых чисел. Этот недостаток легко устраняется подключением библиотеки mathn (подключаются рациональные и комплексные числа).

[править] Семейный портрет чисел

Числовые типы данных
Числовые типы данных

В отличие от большинства элементарных типов данных, числа обладают своей иерархией. Все числа в Руби наследованы от класса Numeric (Числовой). Поэтому, если хотите добавить новый метод ко всем числам, то нужно расширять именно этот класс. Далее идет деление чисел: Integer (целое), Float (дробное) и Complex (комлексное). При желании можно добавить и Rational (рациональное), но на данном семейном портрете оно отсутствует.

От класса Integer наследуются два класса: Fixnum (целое маленькое) и Bignum (целое большое). К первому относятся все числа, по модулю меньшие 231 , а ко второму — все остальные.

  • Fixnum автоматически становится Bignum по превышении 231 по модулю. И наоборот, при падении ниже, Bignum преобразуется в Fixnum.
  • Из отрицательного числа можно получить корень, когда подключена библиотка mathn. Он будет типа Complex (комплексное).
  • Как только число типа Complex (комплексное) лишается мнимой части, то оно становится либо Integer (Fixnum или Bignum), либо Float (в зависимости от типа действительной части). Если подключена библиотка mathn, получится число типа Rational (рациональное).
  • Если в результате арифметических действий в числе типа Rational знаменатель приравнивается 1, то оно преобразуется к числу Integer (Fixnum или Bignum).

[править] Арифметические операции

Арифметические операции в Ruby точно такие же, как и в математике: сложение (+), вычитание (-), умножение (*), деление (/), получение остатка от деления (%), возведение в степень (**).

6 + 4     #-> 10
6 - 4     #-> 2
6 * 4     #-> 24
6 / 4     #-> 1
6 % 4     #-> 2
6 ** 4    #-> 1296

Эти операции используются как дробными, так и целыми числами (а также рациональными дробями и комплексными).

Порядок вычисления обычный, как учили в школе. Для изменения приоритета применяются круглые скобки:

2 + 2 * 2     #-> 6
(2 + 2) * 2   #-> 8

Первое, что бросается в глаза — результат арифметической операции двух целых чисел всегда будет целым. Особенно это видно при делении:

1/3  #-> 0
2/3  #-> 0
3/3  #-> 1

Если все аргументы арифметического выражения целые числа, то результат будет целым, если хотя бы одно дробное, то результат будет дробным.

Одна вторая в Ruby ноль,
А три вторые — единица.
Запомнить надо эту соль,
Чтоб результату не дивиться.

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

6.0 + 4     #-> 10.0
6 - 4.0     #-> 2.0
6.0 * 4.0   #-> 24.0
6.0 / 4     #-> 1.5 (одно из чисел дробное, значит результат дробный)
6.0 % 4     #-> 2.0
6 ** 4.0    #-> 1296.0

Лучше проверить эти сведения самостоятельно. Для этого даже не обязательно устанавливать интерпретатор Ruby. Достаточно лишь зайти на сайт try ruby!.

[править] Поразрядная арифметика

Знак операции | Название
& | побитовое «и»
| | побитовое «или»
^ | побитовое «исключающее или»
<< | побитовый сдвиг влево
>> | побитовый сдвиг вправо
~ | побитовая инверсия

Операции побитовой арифметики заимствованы из языка Си. На этот раз без всяких экзотических особенностей.

6 & 4     #-> 4
6 | 4     #-> 6
6 ^ 4     #-> 2
6 << 4    #-> 96
6 >> 4    #-> 0 (чересчур намного сдвинули)
~4        #-> -5 (операция только над одним аргументом)

Здесь, вроде, всё понятно и без дополнительных пояснений. А если не понятно, то справочник по языку Си поможет.

[править] Операции с присвоением

Часто можно встретить выражения вида:

ogHo_4ucJIo += gpyroe_4ucJIo

Это выполнение операции сразу с присваиванием. Вышеуказанная запись равнозначна следующей:

ogHo_4ucJIo = ogHo_4ucJIo + gpyroe_4ucJIo

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

cTpoka = "едем"
cTpoka += ", "
cTpoka *= 3
cTpoka    #-> "едем, едем, едем, "

maccuB = [1,2,3]
maccuB += [4,5]
maccuB    #-> [1,2,3,4,5]
☞
При определении метода +, метод += вы получаете в подарок. Это правило касается всех бинарных операций, обозначаемых значками.

[править] Методы явного преобразования типов

Метод | Операция
to_f | преобразовать в дробное
to_i | преобразовать в целое
to_s | преобразовать в строку
to_a | преобразовать в массив

Методы преобразования типов в Ruby традиционно начинаются с приставки to_. Последующая буква — это сокращение от названия класса, в который происходит преобразование (f — Float — дробное, i — Integer — целое, s — String — строка, a — Array — массив). Посмотрим их действие на примере:

7.to_f    #-> 7.0
7.9.to_i  #-> 7
7.to_s    #-> "7"
7.to_a    #-> [7]

[править] Случайное число

Часто требуется получить случайное число. Пример:

rand(100)  # 86
rand       #-> 0.599794231588021

В первом случае, метод rand возвращает целое число в диапазоне от 0 до 99 (на 1 меньше 100). Во втором случае, метод rand возвращает дробное число в диапазоне от 0.0 до 1.0 включительно. Различие в результате обусловлено передаваемым параметром:

  • если передается параметр (в данном случае 100), то генерируется целое случайное число (в диапазоне 0...N, где N — передаваемый агрумент);
  • если параметр отсутствует, то генерируется дробное число в диапазоне от 0.0 до 1.0.

Есть способ предсказать весь ряд «случайных» чисел. Делается это при помощи метода srand. Ему передается целое число (идентификатор «случайной» последовательности). После этого, весь случайный ряд можно предугадать. Проведем эксперимент: берусь угадать массив, который будет сгенерен следующей программой.

srand 123
Array.new(5){ rand(100) }   #-> [69, 71, 28, 42, 22]

Если вы выполните данную программу у себя дома, то получите тот же самый массив. 123 — номер «случайной» последовательности. Измените его и массив изменится!

Если вызвать srand без параметра или не вызывать его вообще, то номер «случайной» последовательности выбирается случайным образом.

[править] Хитрости

Задача: выдать целое число в двоичной системе счисления.

ucxogHoe_4ucJIo = 1234
puts sprintf("%b",ucxogHoe_4ucJIo) # метод sprintf заимствован из Си
puts ucxogHoe_4ucJIo.to_s(2)       # современный метод - означает "по основанию",
                                   # аргументом может служить не только 8 и 16, но и 5, 30...

Поменять порядок цифр данного числа на обратный:

ucxogHoe_4ucJIo = 1234
puts ucxogHoe_4ucJIo.to_s.reverse  # метод reverse переворачивает строку

Получить значение N-го двоичного разряда данного целого числа:

ucxogHoe_4ucJIo, N = 1234, 5
puts ucxogHoe_4ucJIo[ N ]

Поменять целочисленные значения двух переменных без использования третьей переменной:

ucxogHoe_nepBoe, ucxogHoe_BTopoe = 134, 234
ucxogHoe_nepBoe, ucxogHoe_BTopoe = ucxogHoe_BTopoe, ucxogHoe_nepBoe
puts ucxogHoe_nepBoe, ucxogHoe_BTopoe

Округлить дробное число до двух разрядов:

gpo6Hoe_4ucJIo = 3.1415926535
puts ( gpo6Hoe_4ucJIo * 100 ).to_i.to_f / 100
puts sprintf( "%.2f", gpo6Hoe_4ucJIo ).to_f    # полуСишный способ =)

[править] Задачи

  1. Сколько часов в году?
  2. Сколько минут в неделе?
  3. Сколько вам лет в секундах?
  4. Сколько шоколада вы съели за свою жизнь? (Осторожно, может не хватить ресурсов компьютера для вычисления.)
  5. Чтобы пронумеровать страницы книги, печатник использовал 1890 цифр. Сколько страниц в книге? Какова ее тематика?

[править] Подробнее о массивах

Массивы — это тип данных, с которым вам придется работать постоянно. Облик большинства программ зависит именно от правильного (читай «изящного») использования массивов.

[править] Способы создания массива

Массив создается как минимум тремя способами. Первый способ:

[1,2,3,4,5,6]

Вы просто перечисляете элементы массива через запятую, а границы массива обозначаете квадратными скобками. С таким методом создания массива мы уже встречались. А теперь попробуем второй способ, через вызов метода .new класса Array. От слов перейдем к делу:

Array.new( 6 ){ |index| index + 1 }   #-> [1,2,3,4,5,6]

Параметром метода .new является количество элементов будущего массива (в данном случае это число 6). В фигурных скобках указано, как мы будем заполнять массив. В данном случае, значение элемента массива будет больше на единицу его индекса. Третий способ заключается в создании объекта типа Range (диапазон) и вызове метода .to_a:

(1..6).to_a    #-> [1,2,3,4,5,6]

Есть еще тысяча и один способ, но эти три используются чаще всего.

[править] Диапазоны

Методом to_a очень удобно создавать из диапазона массив, содержащий упорядоченные элементы данного диапазона.

(1..10).to_a      #->  [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
('a'..'d').to_a   #->  ['a', 'b', 'c', 'd']

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

[ 'a', 'b', 'c', 'd', 'e']

Традиционно нумерация массива начинается с нуля и возрастает по одному:

[ 'a'0, 'b'1, 'c'2, 'd'3, 'e'4 ]

Такая нумерация называется в Ruby положительной индексацией. Хм, — скажете вы, — а есть еще и отрицательная? Да, есть!

[ 'a'  , 'b'  , 'c'  , 'd'  , 'e'   ]

Плюсы расставлены лишь для красоты. Но вернемся к отрицательной индексации. Каков ее смысл? Чтобы его пояснить, давайте решим задачку: дан массив, требуется получить предпоследний элемент.

maccuB = [ 'a', 'b', 'c', 'd', 'e']
maccuB[ maccuB.size - 2 ] #-> 'd'

В данном случае мы использовали метод .size, который возвращает размер массива. Разработчики заметили, что вызов maccuB.size приходится писать довольно часто, и решили от него избавиться. Вот что получилось:

maccuB = [ 'a', 'b', 'c', 'd', 'e']
maccuB[ -2 ]     #-> 'd'

Индекс -2 значит «второй с конца элемент массива». Вот так и появилась отрицательная индексация. Теперь давайте разберемся с диапазонами. Оказывается, в них тоже можно использовать отрицательную индексацию. Вот как можно получить все элементы массива кроме первого и последнего:

maccuB = [ 'a', 'b', 'c', 'd', 'e']
maccuB[ 1..-2 ]  #-> ['b', 'c', 'd']

Или так:

maccuB = [ 'a', 'b', 'c', 'd', 'e']
maccuB[ 1...-1 ]  #-> ['b', 'c', 'd']

Второй вариант с тремя точками, что автоматически приближает правую границу диапазона на одну позицию влево.

[править] О двумерных массивах

Для Ruby двумерный массив — это не более чем массив, содержащий одномерные массивы. Вот несколько примеров двумерных массивов:

[[1],[2,3],[4]]                                # разная длина элементов-массивов
[[1,2],[3,4]]                                        # одинаковая длинна
[["прива","Привет"],["пока","Всего хорошего"]]
[["прива","Привет"],[1,["пока","Всего хорошего"]]]   # гибрид двух-трех-мерного массива
  • Двумерность массива средствами языка не отслеживается. Вполне могут возникнуть гибриды разномерных массивов.
  • Подмассивы внутри двумерного массива могут иметь произвольную длину.
  • Элементы из двумерного массива достаются последовательно: сначала элемент-массив, потом элемент.

[править] Методы работы с массивами

Разнообразие и полезность методов у массивов создаёт впечатление, что все сложные алгоритмы уже реализованы. Это не так, программистам Руби дана действительно обширная библиотека методов. Здесь мы рассмотрим лишь самые употребимые; остальные ищите в справочнике.

[править] Получение размера массива

В Ruby массивы динамические: в каждый конкретный момент времени неизвестно сколько в нем элементов. Чтобы не плодить тайн подобного рода и был реализован метод .size:

[1,'считайте',3,'количество',5,6,'зяпятых',2,5].size   #-> 9

Мы явно указали массив, но на его месте могла стоять переменная:

maccuB = [1,'считайте',3,'количество',5,6,'запятых',2,5]
maccuB.size               #-> 9
☞
Замечено, что количество запятых в объявлении одномерного массива на единицу меньше его размера. Поэтому, если подсчитать запятые и добавить единицу, то получается размер массива
☞
Метод .size есть у многих классов. Например, у ассоциативных массивов и строк.

[править] Поиск максимального/минимального элемента

Вспомните сколько усилий вам приходилось прилагать, чтобы найти максимальный элемент? А сколько раз вы повторяли этот кусок кода в своих программах? Ну а в Ruby поиск максимального элемента осуществляется при помощи метода .max, а в более сложных случаях при помощи метода .max_by (начиная с версии 1.9). Вот как это выглядит:

['у','попа','была','собака'].max                          #-> 'у'       максимальный по значению
['у','попа','была','собака'].max_by{ |elem| elem.size }   #-> 'собака'  максимальный по размеру строки

Методы .min и .min_by работают аналогично:

['у','попа','была','собака'].min                         #-> 'была'   минимальный по значению
['у','попа','была','собака'].min_by{ |elem| elem.size }  #-> 'y'        минимальный по размеру строки

Ну как? А в Руби эти методы уже давно.

[править] Упорядочение

Не буду травить душу долгими байками. Чтобы упорядочить массив, нужно вызвать метод .sort или .sort_by (начиная с версии 1.8).

['у','попа','была','собака'].sort
   #-> ['была','попа','собака','у'] сортировка по значению
['у','попа','была','собака'].sort_by{ |elem| elem.size }
   #-> ['у','попа','была','собака'] сортировка по размеру строки

Остается только добавить, что массивы упорядочиваются по возрастанию. Если вам надо по убыванию, то придется писать собственный метод сортировки пузырьком. Шутка! По правде же, есть много способов выстроить массив по убыванию. Пока мы будем использовать метод .reverse, обращающий массив.

[править] Обращение массива

Обращение массива — это изменение порядка элементов на обратный, то есть первый элемент становится последним, второй элемент предпоследним и так далее.

Для обращения массива существует метод .reverse. Применим его к предыдущим примерам, чтобы получить сортировку по убыванию:

['у','попа','была','собака'].sort.reverse                         #-> ['у','собака','попа','была']
['у','попа','была','собака'].sort_by{ |elem| elem.size }.reverse  #-> ['собака','была','попа','у']

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

[править] Сложение/вычитание массивов

Для сложения массивов, строк и чисел используется метод +:

[1,2,3,4] + [5,6,7] + [8,9]     #-> [1,2,3,4,5,6,7,8,9]

Плюс берёт массив справа и, будто это железнодорожный состав, прицепляет его к хвосту первого массива. Это называется конкатенацией.

Вычитаются массивы методом -, но происходит это сложнее, чем расцепление вагонов:

[ 1, 1, 2, 2, 3, 3, 3, 4, 5 ] - [ 1, 2, 4 ]  #-> [3, 3, 3, 5]

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

[править] Объединение и пересечение массивов (как множеств)

Очень часто приходится решать задачи, в которых нужно оперировать множествами. У массивов припасено для этих целей два метода: | (объединение) и & (пересечение).

Рассмотрим объединение множеств в действии:

[1,2,3,4,5,5,6] | [0,1,2,3,4,5,7]    #-> [1,2,3,4,5,6,0,7]

Объединение получается вот так. Сначала массивы сцепляются:

[1,2,3,4,5,5,6,0,1,2,3,4,5,7]

Затем, начиная с первого вагона, инспектор идёт от вагона к вагону, удаляя элементы, которые уже встречались. После зачистки получается настоящее логическое объединение.

На деле это выглядит так:

[1,2,3,4,5,5,6,0,1,2,3,4,5,7]

Зачеркнутые числа — это удаленные элементы (дубликаты). Переходим от слов к делу:

[1,2,3,4,5,5,6] & [0,2,1,3,5,4,7]    #-> [1,2,3,4,5]

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

[1,2,3,4,5,5,6] & [0,2,1,3,5,4,7]

Всё просто. Важно лишь помнить, что | и & не изменяют ни первый, ни второй исходные массивы. Они через описанные процедуры создают новый массив. Чтобы тот не уехал от вас, нужно присвоить его; но не себе, а переменной, приготовленной слева от =.

В итоге пересечения или объединения множеств получается массив, не содержащий дубликатов.

[править] Удаление дубликатов

Для удаления дубликатов (повторяющихся элементов массива) в Ruby используется метод .uniq:

[1,2,3,4,5,5,6,0,1,2,3,4,5,7].uniq   #-> [1,2,3,4,5,6,0,7]

Процесс зачистки массива от дубликатов такой же, как и в объединении.

[1,2,3,4,5,5,6,0,1,2,3,4,5,7]

Поэтому объединение массивов можно записать как ( nepBblu_maccuB + BTopou_maccuB ).uniq. Но проще, конечно, объединять палкой.

[править] Сплющивание массивов

Метод .flatten делает из многомерного массива простой, длинный одномерный массив. Он как бы расплющивает его. Например, чтобы найти в двумерном массиве наибольший элемент, мы сперва расплющим массив, а потом найдём максимум методом .max:

maccuB = [[1,2],[3,4]]
maccuB.flatten.max    #-> 4

Расплющивание происходит в несколько этапов. Сначала происходит удаление всех квадратных скобок.

[[1,2],[3,4]]

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

[1,2,3,4]

Вот и все! У нас они разбежаться не успели. Повторите данное упражнение на других массивах (двумерных, трехмерных и т.д).

[править] Транспонирование двумерного массива

Задача: дан двумерный массив. Вывести одномерный массив с максимумами каждого из столбцов. Хм... посмотрим сперва, как эта задача решается для строчек, а не столбцов:

maccuB_2D = [[1,2],[3,4]]
maccuB_2D.map{ |maccuB| maccuB.max } #-> [2,4]

Чтобы решить задачу в первоначальном варианте, нам надо лишь предварительно транспонировать массив (поменять местами строки и столбцы):

maccuB_2D = [[1,2],[3,4]]
maccuB_2D.transpose.map{ |maccuB| maccuB.max } #-> [3,4]

Метод .transpose как раз и занимается транспонированием. Это позволяет с легкостью решать задачи про столбцы приемами, схожими с задачами про строки.

[править] Размножение массивов

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

["много", "денег", "прячет", "теща"] * 2
    #-> ["много", "денег", "прячет", "теща", "много", "денег", "прячет", "теща"]

Того же самого эффекта можно добиться сцепив массив необходимое количество раз:

maccuB = ["много", "денег", "прячет", "теща"]
maccuB + maccuB #-> ["много", "денег", "прячет", "теща", "много", "денег", "прячет", "теща"]

Заметили, что есть некоторая параллель с целыми числами? Умножение можно заменить сложением и наоборот!

[править] Функциональность стека

Часто и во многих алгоритмах надо добавить элемент в конец массива:

maccuB = [1,2,3,4,5]
maccuB[ maccuB.size ] = 6
maccuB  #-> [1,2,3,4,5,6]

И если уж добавили, то надо как-то его и удалить. Делается это примерно так:

maccuB = [1,2,3,4,5,6]
maccuB[0...-1] #-> [1,2,3,4,5]

Но как всегда, эти задачи возникали слишком часто и их решили реализовать в виде методов. Методы назвали .push («пихнуть» в конец массива) и .pop («выпихнуть» элемент из массива):

maccuB = [1,2,3,4,5]
maccuB.push( 6 )
maccuB      #-> [1,2,3,4,5,6]
maccuB.pop  #-> 6
maccuB      #-> [1,2,3,4,5]

[править] Функциональность очереди и списка

Чтобы можно было использовать массив в качестве очереди и/или списка, потребуется сделать всего лишь пару методов. Первый из них добавляет элемент в начало массива, а второй удаляет элемент из начала. Давайте посмотрим, как это делается универсальными методами [], []= и +:

maccuB = [1,2,3,4,5]

# добавим элемент в начало массива
maccuB = [6] + maccuB
maccuB     #-> [6,1,2,3,4,5]
maccuB[0]  #-> 6

# удалим элемент из начала массива
maccuB = maccuB[1..-1]
maccuB     #-> [1,2,3,4,5]

Теперь посмотрим, какие методы реализуют точно такую же функциональность:

maccuB = [1,2,3,4,5]

# добавляем элемент в начало массива
maccuB.unshift( 6 ) #-> [6,1,2,3,4,5]

# удаляем из начала массива
maccuB.shift        #-> 6
maccuB              #-> [1,2,3,4,5]

Удаляющий метод — .shift («сдвинуть»), а метод, добавляющий элемент в начало массива, называется .unshift (непереводимое слово, стремящееся означать нечто противоположное слову «сдвинуть»).

[править] Мнемограмма для методов стека/очереди/списка

Мнемограмма для методов .shift, .unshift, .pop и .push:

.unshift( 0 )             .push( 6 )
              [1,2,3,4,5]
.shift                    .pop

Методы с параметром (сверху) добавляют элемент в массив, а методы без параметра (снизу) — удаляют. По желанию можно дорисовать стрелочки.

Метод .shift сдвигает влево,
Метод .pop — направо.
Метод .push в конец пихает,
А .unshift — в начало.

[править] Создание своих классов, работающих как массивы

Если потребуется написание своего класса, который работает, как описанные массивы, то возникают некоторые тонкости. Дело в том, что реализация всех описанных методов займет жуткое количество времени и сил. На самом деле, для реализации большинства описанных методов, достаточно реализовать .each

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

Просто реализуйте .each и включите в ваш класс mixin Enumerable. В нем находится реализация методов, таких как .inject, .each_with_index и т.п.

[править] Логические методы

Логический метод — это метод, результатом которого является логическое выражение (true или false).

По японской традиции, имена логических методов принято заканчивать ? (вопросительным знаком). Это позволяет также получить список логических методов, вызываемых в данном случае: просто отобрать из всех имеющихся методов те, что кончаются на ?. Делается это при помощи небольшой вспомогательной программы:

maccuB = [1,2,2,3]
puts maccuB.methods.grep(/\?$/)

Для удобства, можно упорядочить полученный список:

maccuB = [1,2,2,3]
puts maccuB.methods.grep(/\?$/).sort

[править] Есть элемент в массиве?

Как узнать, есть ли некоторый элемент в массиве? Попробуем решить эту задачу при помощи метода .size и итератора .find_all:

maccuB = [1,2,3,4,5,6,7]
uckomoe = 5                # число, которое мы будем искать
maccuB.find_all{ |elem| elem == uckomoe }.size != 0  #-> true
                                                     # это значит, что такое число есть

Использование связки из трех методов (!=, .find_all и .size) для такой задачи — возмутительно! Разработчики не могли с этим долго мириться и реализовали метод специально для этой задачи. Имя ему — .include?. Перепишем нашу задачу, но на этот раз будем использовать правильный метод:

maccuB = [1,2,3,4,5,6,7]
uckomoe = 5                # число, которое мы будем искать
maccuB.include?( uckomoe ) #-> true
                           # что бы это значило?

Мутный горизонт скрывает берег,
Ветер мокр, холоден и лют.
Есть ли в озере акулы, я проверю
Методом логическим .include?

o3epo = ["правый берег", "ветер", "вода", "вода", "вода", "окунь", "вода", "вода", "левый берег"]
o3epo.include?("акула") #-> false

Опытным путем мы доказали, что акулы в озере не водятся.

[править] Массив пустой?

Если вы хотите задать массиву вопрос «ты пуст ли?», но боитесь обидеть, то можете пойти окружным путем. Например, спросить у него: ты равен пустому массиву?

nycTou_maccuB = []
noJIHbIu_maccuB = [1,2,2,3]

nycTou_maccuB == []   #-> true
noJIHbIu_maccuB == [] #-> false

Еще можно задать вопрос: твой размер равен нулю?

nycTou_maccuB = []
noJIHbIu_maccuB = [1,2,2,3]

nycTou_maccuB.size == 0   #-> true
noJIHbIu_maccuB.size == 0 #-> false

Но наш вам совет: не стоит искать обходных путей. Спросите его напрямую: .empty? («пуст?»):

nycTou_maccuB = []
noJIHbIu_maccuB = [1,2,2,3]

nycTou_maccuB.empty?     #-> true
noJIHbIu_maccuB.empty?   #-> false

[править] Итераторы

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

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

  • не зацикливаются: счётчик цикла нельзя докручивать;
  • выполняются заведомое число раз;
  • их много, и каждый делает своё дело.
Имя им — итераторы.

[править] Изменение всех элементов массива

Изменить все элементы массива можно по-всякому. Начнём с обнуления:

maccuB = ['шифровка','Штирлица','в','Центр','секретно']
maccuB.map{ 0 }             #-> [0,0,0,0,0]

Используется итератор .map, за которым следует блок, — кусочек кода, схваченный лапками-фигурными скобками. .map последовательно проходит maccuB и выполняет блок заново для каждого элемента. То, что выходит из блока, итератор .map делает очередным элементом нового массива.

Можно дать элементу .map иное задание. Для этого зажимаем в фигурные скобы блока иной код:

maccuB = [1,2,3,4,5]
maccuB.map{ |elem|  elem**2 }  #-> [1,4,9,16,25]

Прежде, чем блоку выдать квадрат очередного элемента, ему нужно знать этот элемент. Итератор .map даёт ему значение элемента, словно фотографию, обрамлённую слева и справа вертикальными чертами |. Чтобы блок смог взять эту фотографию, обязательно дать ей имя. В нашем случае это elem, но подходят и такие названия:

  • _3JleMeHt
  • x
  • y
  • a_He_x0tuTe_li_4awe4Ku_4a9_c_LuMoHom
Но недопустимы названия вроде BoT_Takoro или 3Takoro. Правила именования тут такие же, как и для обычных переменных.

Вы уже, наверное, хорошо поняли, что в итераторах массивы обрабатываются по очереди; двери вагона расходятся, появляется элемент. Блок даёт ему прозвище, выполняет код в лапках-фигурных скобках. Затем переходит к следующему вагону, и там всё сначала.

Но ещё важно помнить, что элементы не уходят из первого вагона: блок лишь осматривает каждый элемент, берёт его значение, но не меняет. Всё, что получается в результате работы блока, садится в очередной вагон другого поезда.

То имя, что показывается в раздвижных дверях, — это не сам элемент, это лишь его копия. Фотография. Голограмма. Это даже не другая переменная, это не переменная вообще. Бессмысленно присваивать новое значение фотографии:

maccuB = [1,2,3,4,5]
maccuB.map{ |elem|  elem = elem**2 } # присваивание не имеет смысла: elem несёт лишь значение элемента, не являясь тем

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

maccuB = [1,2,3,4,5]
maccuB.map{ |elem| elem**2 }  #-> [1,4,9,16,25]
maccuB       #-> [1,2,3,4,5] - неизменный первый поезд.

Можно присвоить его новой переменной maccuB_c_kBagpaTaMu. А можно заместить им существующую переменную maccuB:

maccuB = [1,2,3,4,5]
maccuB = maccuB.map{ |elem| elem**2 }  #-> [1,4,9,16,25]
maccuB       #-> [1,4,9,16,25]

Это общее явление Руби: методы (здесь — итераторы) не меняют объект (массив), с которым работают. Они лишь выдают результат, который потом можно использовать как аргумент или присвоить переменной.

Явление было воспето в фольклоре:

Метод .map все изменяет,
Как кто пожелает
И обижается на тех,
Кто результат не сохраняет.

[править] Отбор элементов по признаку

Вот как итератор .find_all выберет из массива все чётные элементы:

maccuB = [1,2,3,4,5]
maccuB.find_all{ |elem| elem%2==0 }  #-> [2,4]

elem%2==0 — это вопрос "чётен ли elem?". Ответом, как всегда, будет true или false. Ведь "чётность" — это равенство нулю (==0) остатка деления (%) на 2.

Кстати, равенство нулю можно проверять и при помощи метода .zero?. А чётность тоже можно проверить разными способами:

(elem%2).zero?

(elem&1).zero?

(elem[0]).zero? # Этот вариант круче всех

Если на вопрос, заданный в блоке, ответ true, то |elem| (значение очередного элемента исходного массива), заносится в новый массив, который в итоге будет выводом из итератора .find_all.

Выражение в блоке для .find_all должно быть логическим, то есть принимать значение true или false.

Если нужно элементы
По условию искать,
То полезнее .find_all
Метода вам не сыскать!

Мысль: красивее делить лишь последнюю цифру(так в третьем классе учат четность определять :) ). Тем более, когда нужно определить четность factorial(100) :). В любом случае базовый класс нужно расширить чем-то вроде метода .chetnoe?.(с)Вовчик

[править] Суммирование/произведение/агрегация элементов

Очень часто возникает задача найти сумму/произведение всех элементов массива. Для этих целей (начиная с версии 1.8) традиционно используется итератор .inject. Для демонстрации его работы, давайте найдем сумму элементов массива:

maccuB = [1,2,3,4,5]
maccuB.inject( 0 ){ |result, elem|   result + elem } #-> 15

Рассмотрим все по порядку. Начнем с нуля. Его следует расшифровывать как result = 0 перед началом работы итератора, то есть это начальное значение переменной result (переменной промежуточного результата).

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

После объявления описан алгоритм работы итератора. В данном случае ему предписано каждый элемент массива складывать с промежуточной суммой: result + elem.

  • Присвоение переменной (которая описывает промежуточную сумму или текущий элемент) внутри блока указывает на непонимание работы итератора .inject (и многих других). Вам должно быть стыдно!
  • Результат последнего вычисления автоматически запишется в переменную с промежуточным результатом.

Учитывая эти два замечания, напишем код, который является неправильным:

maccuB = [1,2,3,4,5]
maccuB.inject( 0 ){ |result, elem| result = result + elem } #-> 15
☞
Имена переменных result и elem созданы богатым воображением автора. В ваших программах они могут называться иначе.

Изменим имена переменных:

maccuB = [1,2,3,4,5]
maccuB.inject( 0 ){ |pe3yJibTaT, nepemeHHa9| pe3yJibTaT + nepemeHHa9 } #-> 15

Невероятно, но от изменения имен переменных, результат остается прежним. Помните это!

Для полноты картины решим еще одну задачку. На этот раз будем искать произведение всех элементов массива:

maccuB = [1,2,3,4,5]
maccuB.inject( 1 ){ |pe3yJibTaT, nepemeHHa9| pe3yJibTaT * nepemeHHa9 } #-> 120

Чтобы закрепить материал, решите задачу: найти произведение всех положительных элементов массива. Подсказка: используйте метод .find_all.

Элементов надо кучу
Перемножить иль сложить?
Есть .inject на этот случай,
Его бы вам употребить.

[править] Разбиение надвое

Итератор .partition делит массив на две части по некоторому бинарному признаку (чётности, положительности, наличию высшего образования и тому подобным). Вот как разделить массив на две части по признаку кратности трём:

maccuB = [1,2,3,4,5,6,7,8,9]
maccuB.partition{ |elem| (elem%3).zero? } #-> [ [3,6,9], [1,2,4,5,7,8] ]

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

Есть интересная хитрость, позволяющая разместить массив, полученный .partition, в две разные переменные:

maccuB = [1,2,3,4,5,6,7,8,9]
oguH, gBa = maccuB.partition{ |elem| (elem%3).zero? }
oguH   #-> [3,6,9]
gBa    #-> [1,2,4,5,7,8]

Этот метод называется «присвоение списков» и широко используется в ситуациях, когда из метода надо вернуть более одного значения. Но об этом позднее.

Меняйте названия переменных хотя бы для того, чтобы не распознали вашего списывания.

[править] Логические итераторы

В версии 1.8 появилось несколько логических методов: .all? и .any?. Они положили начало такому классу методов, как логические итераторы.

Логический итератор — это итератор (метод, обрабатывающий все элементы последовательности), возвращающий значение логического типа — true или false.

Конечно же, идея логических итераторов долгое время летала в ноосфере. Существовали итераторы, которые являлись условно-логическими: они возвращали nil в случае неудачи и какой-либо объект — в случае удачи. В логическом контексте поведение таких итераторов можно было посчитать логическим (false -> nil, а true -> число/строка/любой_объект). Примером условно-логического итератора служит метод .detect.

[править] Все ли элементы удовлетворяют условию?

В математической логике такой «итератор» называется квантором всеобщности, обозначается символом [image]. На языке Ruby он называется .all?. По сложившейся традиции, давайте посмотрим, как решалась бы эта задача до версии 1.8, то есть до появления логических итераторов:

maccuB = [1,2,2,3]
maccuB.inject( true ){ |result, elem|
    result && ( elem > 2 )
} #-> false
☞
В примере используется так называемый механизм презумпции виновности. Переменной result присваивается значение true. Логическое умножение изменяет значение переменной result на false.

Данная программа проверяет, все ли элементы maccuBа больше двух. Давайте решим эту же задачу при помощи новоявленного логического итератора:

maccuB = [1,2,2,3]
maccuB.all?{ |elem| elem > 2 } #-> false

Несмотря на то, что код получился короче, результат остался прежним: утверждение, что все элементы массива больше двух, ложно.

[править] Хотя бы один элемент удовлетворяет условию?

Вслед за квантором всеобщности (он же — логический итератор .all?), из математической логики был перенесен и квантор существования — [image]. На языке Ruby он называется .any?. Чтобы оценить его по достоинству, посмотрим решение задачи без его участия. Проверим, содержит ли maccuB хотя бы один элемент больший двух:

maccuB = [1,2,2,3]
maccuB.inject( false ){ |result, elem|
    result || ( elem > 2 )
} #-> true
☞
В данном примере используется так называемый механизм презумпции невиновности. Переменной result присваивается значение false. В результате логического сложения, происходит изменение значения переменной result на true.

Теперь, тоже самое, но через логический итератор .any?:

maccuB = [1,2,2,3]
maccuB.any?{ |elem| elem > 2 } #-> true

Естественно, что с появлением логических итераторов, реализация задач математической логики (в рамках языка Ruby) стала удобней.

[править] Хитрости

Вот так можно сгенерировать «хороший пароль» — произвольную последовательность из чисел или латинских букв, общей длиной в 8 символов.

cumBoJIbI = ['a'..'z','A'..'Z','0'..'9'].map{ |range| range.to_a }.flatten
puts (0...8).map{ cumBoJIbI[ rand( cumBoJIbI.size ) ] }.join

Перемешать упорядоченный массив:

maccuB = [1,2,3,4,5,6,7]
maccuB.sort_by{ rand }    #-> перемешанный массив

Выстроить элементы массива по убыванию без использования .reverse:

maccuB = [2,1,3,5,6,7,4]
maccuB.sort{ |x,y| y <=> x }    #-> [7,6,5,4,3,2,1]

[править] Задачи про массивы

☞
Помимо нижеследующих задач, советуем вам взглянуть на задачи из нашего сборника задач

[править] Одномерные

  1. Вывести индексы массива в том порядке, в котором соотвествующие им элементы образуют возрастающую последовательность.
  2. В численном массиве найти сумму отрицательных элементов.
  3. Найти все индексы, по которым располагается максимальный элемент.
  4. В массиве переставить в начало элементы, стоящие на четной позиции, а в конец -- стоящие на нечётной.

[править] Одномерные целочисленные
  1. Найти все элементы, большие среднего арифметического элементов.
  2. К четным элементам прибавить первый элемент, а к нечетным — последний. Первый и последний элемент не изменять.
  3. Заменить все положительные элементы на значение минимального.
  4. Найти произведение всех четных эелементов массива.
  5. Найти количество минимальных элементов.
  6. Вывести индексы элементов, меньших своего левого соседа.

[Пример решения [1]]

[править] Двумерные

  1. Поменять первый и последний столбец массива местами.
  2. Упорядочить N-ый столбец.
  3. Упорядочить строки, содержащие максимальный элемент.
  4. Упорядочить строки, если они отсортированы и перемешать, если они отсортированы.
  5. Упорядочить строки массива по значению элемента главной диагонали в каждой из строк (в исходном массиве).
  6. Найти номера строк, элементы которых упорядочены по возрастанию.

[Частичное решение [2]]

[править] Двумерные целочисленные
  1. Найти максимальный элемент для каждого столбца, а затем получить произведение этих элементов.
  2. Найти минимум в двумерном массиве.
  3. Найти произведение положительных элементов.
  4. Найти сумму положительных элементов, больших К.
  5. Вычислить сумму и среднее арифметическое элементов главной диагонали.
  6. Найти номера строк, все элементы которых — нули.

[править] Подробнее об ассоциативных массивах

Различают два типа массивов: индексные, у которых в качестве индекса только целое число и ассоциативные, где индексом может быть любой объект.

☞
Индексные массивы чаще всего называют просто «массивами», а ассоциативные массивы — «хешами» или «словарями».

Хеши можно представить как массив пар: ключ => значение. Но в отличие от массива, хеш неупорядочен: нельзя заранее сказать, какая пара будет первой, а какая последней. Правда, удобство использования массива это шибко не умаляет. Более того, поскольку в Ruby переменные не типизированы и методам с похожей функциональностью дают похожие имена, то использование хеша чаще всего равносильно использованию массива.

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

Давайте создадим хеш, где в качестве ключа будем использовать целое число:

xew = {5=>3,1=>6,3=>2}
xew[ 5 ]  #-> 3
xew[ 2 ]  #-> nil      это значит, что объект отсутствует
xew[ 3 ]  #-> 2

А вот так будет выглядеть та же самая программа, если мы будем использовать массив:

maccuB = [nil, 6, nil, 2, nil, 3]
maccuB[ 5 ]  #-> 3
maccuB[ 2 ]  #-> nil
maccuB[ 3 ]  #-> 2
Первый случай применимости хеша: если в массиве намечаются обширные незаполненные (то есть заполненные nil) области, то целесообразнее использовать хеш с целочисленным индексом.

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

Продолжим поиски случаев применимости хеша и, на этот раз подсчитаем, сколько раз каждое число повторяется в данном целочисленном массиве. Решение массивом:

maccuB = [1,2,1,2,3,2,1,2,4,5]
maccuB.uniq.map{ |i| [i, maccuB.find_all{ |j| j == i }.size ] }
  #-> [[1, 3], [2, 4], [3, 1], [4, 1], [5, 1]]

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

Теперь рассмотрим решение этой же задачи, но с применением хеша:

maccuB = [1,2,1,2,3,2,1,2,4,5]
maccuB.inject( Hash.new{ 0 } ){ |result, i|
    result[i] += 1
    result
} #-> {5=>1, 1=>3, 2=>4, 3=>1, 4=>1}

Удалось избавиться от лишних методов и обойтись лишь одной "пробежкой" итератора по массиву.

  • Начальный хеш был создан хитроумной комбинацией Hash.new{ 0 }, что в переводе на русский означает примерно следующее: "создадим пустой хеш, в котором любому несуществующему ключу будет соответствовать 0". Это нужно, чтобы суммирование (метод +) не выдавало ошибку вида: "не могу сложить nil и число типа Fixnum". В качестве упражнения, предлагаю вам заменить комбинацию Hash.new{ 0 } на {} и посмотреть, чем это чревато.
  • зачем нужно дописывать result? Дело в том, что комбинация result[i]+=i имеет в качестве результата целое число (учитывая, что массив целочисленный), а не хеш. Следовательно, параметру result автоматически будет присвоено целое число (см. описание итератора .inject). На следующей итерации мы будем обращаться к result, как к хешу, хотя там уже будет храниться число. Хорошо, если программа выдаст ошибку, а если нет? Проверьте это самостоятельно.

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

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

Теперь представим, что мы работаем системными администраторами. У нас есть список DNS-имен и IP-адреса. Каждому DNS-имени соответствует только один IP-адрес. Как нам это соответствие записать в виде программы? Попробуем это сделать при помощи массива:

maccuB = [['comp1.mydomen.ru','192.168.0.3'],
   ['comp2.mydomen.ru','192.168.0.1'],['comp3.mydomen.ru','192.168.0.2']]

Все бы ничего, но чтобы найти IP-адрес по DNS имени, придётся перелопатить весь массив в поиске нужного DNS:

dns_name = 'comp1.mydomen.ru'
maccuB.find_all{ |key, value| key == dns_name }[0][-1]
   #-> "192.168.0.3"

В данном примере было использовано два интересных приема:

  • Если в двумерном массиве заранее известное количество столбцов (в нашем случае — два), то каждому из столбцов (в рамках любого итератора) можно дать свое имя (в нашем случае: key и value). Если бы мы такого имени не давали, то вышеописанное решение выглядело бы так:
maccuB.find_all{ |array| array[0] == dns_name }[0][-1]
   #-> "192.168.0.3"
Без именования столбцов, внутри итератора вы будете работать с массивом (в двумерном массиве каждый элемент — массив, а любой итератор "пробегает" массив поэлементно). Это высказывание действительно, когда "пробежка" осуществляется по двумерному массиву.
  • метод .find_all возвращает двумерный массив примерно следующего вида: [ ["comp1.mydomen.ru", "192.168.0.3"] ], чтобы получить строку "192.168.0.3" необходимо избавиться от двумерности. Делается это при помощи метода [], который вызывается два раза (понижает размерность c двух до нуля). Метод [0] возвращает в результате — ["comp1.mydomen.ru", "192.168.0.3"], а метод [-1] — "192.168.0.3". Как раз это нам и было нужно.

Теперь ту же самую задачу решим, используя хеш:

xew = {'comp1.mydomen.ru' => '192.168.0.3',
   'comp2.mydomen.ru' => '192.168.0.1', 'comp3.mydomen.ru' => '192.168.0.2'}
xew[ 'comp1.mydomen.ru' ]   #-> "192.168.0.3"

Нет ни одного итератора и следовательно, не сделано ни одной "пробежки" по массиву.

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

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

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

maccuB = [1,2,1,2,3,2,1,2,4,5]
maccuB.inject( {} ){ |result, i| result.update( { i=>1 } ){ |key,old,new| old+new } }
   #-> {5=>1, 1=>3, 2=>4, 3=>1, 4=>1}

Описание метода .update будет дано ниже. На данном этапе, попытайтесь угадать принцип работы метода .update.

[править] Что используется в качестве ключей?

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

Если состояние объектов-ключей изменилось, то хешу необходимо вызвать метод .rehash.
maccuB_1 = [ "а", "б" ]
maccuB_2 = [ "в", "г" ]
xew = { maccuB_1 => 100, maccuB_2 => 300 }
xew[ maccuB_1 ]        #-> 100
maccuB_1[0] = "я"
xew[ maccuB_1 ]        #-> nil
xew.rehash             #-> {["я", "б"]=>100, ["в", "г"]=>300}
xew[ maccuB_1 ]        #-> 100

В данном примере ключами хеша (xew) являются два массива (maccuB_1 и maccuB_2). Одному из них (maccuB_1) мы изменили нулевой элемент (с "а" на "я"). После этого доступ к значению был потерян. После выполнения метода .rehash все встало на свои места.

☞
Как Руби отслеживает изменение ключа в ассоциативном массиве? Очень просто: с помощью метода .hash, который генерирует "контрольную сумму" обьекта в виде целого числа. Например: [1,2,3].hash #-> 25

[править] Способы создания ассоциативного массива

При создании ассоциативного массива важно ответить на несколько вопросов:

  • Какие данные имеются?
  • Какого типа эти данные?
  • Что будет ключом, а что значением?

Ответы определят способ создания хеша.

[править] Из одномерного массива

Положим, что у нас в наличии индексный массив, где ключ и значение записаны последовательно. Тогда мы можем использовать связку методов * и Hash[]:

maccuB = [1, 4, 5, 3, 2, 2]
Hash[ *maccuB ]          #-> {5=>3, 1=>4, 2=>2}

Элементы, стоящие на нечетной позиции (в данном случае: 1, 5 и 2) стали ключами, а элементы, стоящие на четной позиции (то есть: 4, 3 и 2), стали значениями.

[править] Из двумерного массива

Если повезло меньше и нам достался двумерный массив с элементами вида [["ключ_1","значение_1"],["ключ_2","значение_2"],["ключ_3","значение_3"], ... ], то его надо сплющить (.flatten) и тем задача будет сведена к предыдущей:

maccuB = [[1,4],[5,3],[2,2]]
Hash[ *maccuB.flatten ]   #-> {5=>3, 1=>4, 2=>2}

Каждый нулевой элемент подмассива станет ключом, а каждый первый — значением.

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

[["ключ_1","ключ_2","ключ_3", ... ], ["значение_1","значение_2","значение_3", ... ]]

Вспоминаем методы работы с массивами. Там был метод .transpose (транспонирование массива), вызов которого сведет задачу к предыдущей.

maccuB = [[1,5,2],[4,3,2]]
Hash[ *maccuB.transpose.flatten ]   #-> {5=>3, 1=>4, 2=>2}

[править] Нет данных

Если нет данных, то лучше записать хеш как пару фигурных скобок:

xew = {}
xew[1] = 4
xew[5] = 3
xew[2] = 2
xew        #-> {5=>3, 1=>4, 2=>2}

И уже по ходу дела разобраться, что к чему.

[править] Известен только тип значений

Сведения о типе значений использовать следует так: создать хеш, в котором будет определен элемент по умолчанию. Элементом по умолчанию должен быть нулевой элемент соотвествующего типа, то есть для строки это будет пустая строка ( "" ), для массива — пустой массив ( [] ), а для числа — нуль (0 или 0.0). Это делается, чтобы к пустому элементу можно было что-то добавить и при этом не получить ошибку.

xew = Hash.new( "" )
xew[ "песенка про зайцев" ] += "В темносинем лесу"
xew[ "песенка про зайцев" ] += ", где трепещут осины"
xew  #-> { "песенка про зайцев"=>"В темносинем лесу, где трепещут осины" }

Или ещё пример:

xew = Hash.new( 0 )
xew[ "зарплата" ] += 60
xew[ "зарплата" ] *= 21
xew  #-> {"зарплата"=>1260}

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

[править] Всё известно и дано

Если вам изначально известны все ключи и значения, то и записывайте их сразу в виде хеша:

{ "март" => 400, "январь" => 350, "февраль" => 200 }
Не изобретайте велосипед и поступайте как можно проще.

[править] Методы работы с ассоциативными массивами

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

[править] Получение массива значений и массива ключей

Для получения отдельно массива ключей или значений существуют методы .keys и .values.

{1 => 4, 5 => 3, 2 => 2}.keys    #-> [1, 2, 5]
{1 => 4, 5 => 3, 2 => 2}.values  #-> [4, 3, 2]
Ассоциативные массивы в Ruby неупорядоченны: массивы могут иметь любой порядок элементов.

[править] Замена ключей на значения

Чтобы поменять местами ключи и значения ассоциативного массива, следует применять метод .invert. Этот метод возвращает асоциативный массив с ключами, замененными значениями, и значениями, замененными ключами.

xew = {"первый ключ" => 4, "второй ключ" => 5}
xew.invert #-> {4 => "первый ключ", 5 => "второй ключ"}
Поскольку ключи в ассоциативных массивах уникальны, то ключи с одинаковыми значениями будут отброшены.
xew = {"первый ключ" => 10, "второй ключ" => 10}
xew.invert #-> {10 => "второй ключ"}

[править] Обновление пары

Что вы делаете, если хотите обновить какую-то программу или игру? Правильно, устанавливаете апдейт. А вот чтобы обновить значение в ассоциативном массиве используется метод .update. Пример использования этого метода в боевых условиях мы уже приводили в начале раздела. Если вы помните, то мы считали сколько раз повторяется каждое число. Наверняка, вы немного подзабыли его решение (у программистов есть привычка не помнить константы). Позволю себе его вам напомнить:

maccuB = [1,2,1,2,3,2,1,2,4,5]
maccuB.inject( {} ){ |result, i| result.update( { i=>1 } ){ |key,old,new| old+new } }
   #-> {5=>1, 1=>3, 2=>4, 3=>1, 4=>1}

Страшноватая запись. Поэтому будем разбирать ее по частям.

result.update( { i=>1 } ){ |key,old,new| old+new }

Сразу после названия метода (в нашем случае .update) идет передача параметра. Страшная запись { i => 1 } — это не что иное, как еще один хеш. Ключ его хранится в переменной i (счетчик итератора .inject), а в качестве значения выбрана единица. Зачем? Расскажу чуть позже.

☞
Не обязательно писать именно { i=>1 }. Можно "сократить" фигурные скобки и записать i=>1
Счетчик итератора — это переменная в которую итератор записывает текущий элемент последовательности.

Здесь вроде бы все понятно. Запись стала менее страшной, но все равно вызывает дрожь. Будем это исправлять!

result.update( { i=>1 } ){ |key,old,new| old+new }

Раньше мы не встречались с такой записью. Но ничего страшного в ней нет. Это что-то типа поля боя. Нам выдали вооружение и необходимо провести некий маневр. В нашем случае, арсенал у нас внушительный: key, old и new. Бой начинается при некоторых условиях. Наш бой начнется, когда при добавлении очередной пары (переданной в предыдущей части страшной записи) обнаружится, что такой ключ уже есть в хеше. Нам предлагается описать наши действия именно в таком случае. Что же это за действия?

result.update( { i=>1 } ){ |key,old,new| old+new }

Всего лишь сложение old и new. Ничего не говорит? Тогда расскажу, что значат переменные key, old и new. В переменную key передается значение текущего ключа, в old — старое значение ключа (old по английски значит "старый"), а в переменную new — добавляемое значение ключа (new по английски значит "новый").

Теперь переведем запись old+new на русский: в случае обнаружения ключа в хеше, нам необходимо сложить старое значение с новым. Если помните, то новое значение равняется единице, то есть в случае когда ключ, хранимый в i уже есть в хеше result, то к старому значениею просто добавляется единица. Вот и все... а вы боялись.

☞
Рекомендуется перечитать данную главу еще раз, так как вы ее немного не поняли

Интересно, сколько читателей сможет прочитать эту строку и не зациклиться на предыдущей?

[править] Размер ассоциативного массива

Ну вот, с новичками мы познакомились, теперь можно переходить к старым знакомым. Помните, как мы находили размер массива? Вот и с хешами точно также:

xew = {5=>1, 1=>3, 2=>4, 3=>1, 4=>1}
xew.size  #-> 5
☞
Стоит уточнить, что если в индексных массивах под размером понимается количество элементов, то в ассоциативноя массиве это количество пар вида ключ => значение. В остальном же это наш старый добрый .size

[править] Удаление пары по ключу

О том, как добавлять элементы в массив мы знаем, а вот про удаление — нет! Необходимо это исправить. Чем мы сейчас и займемся.

xew = {5=>1, 1=>3, 2=>4, 3=>1, 4=>1}
xew.delete( 5 )  #-> 1
xew              #-> {1=>3, 2=>4, 3=>1, 4=>1}
xew.delete( 5 )  #-> nil

Как вы, наверно, уже догадались, удалением пары по ключу занимается метод .delete. Ему передается ключ от пары, которую следует удалить.

☞
  • Метод .delete возвращает значение, которое соответствовало ключу в удаляемой паре
  • Если в хеше отсутствует пара с передаваемым ключем, то метод .delete возвращает nil
  • Напоминаем, что nil — это символ пустоты

[править] Удаление произвольной пары

Многие программисты удивляются, когда узнают, что ассоциативные массивы имеют метод .shift. Связано это удивление с тем, что у индексных массивов он удаляет первый элемент, возвращая его во время удаления. А вот как понять, какая пара является первой? И что такое первый в неупорядоченной последовательности пар?

☞
Ответ кроется в отсутствии метода-напарника .pop, так как если нельзя удалить последний элемент, то под .shift понимается удаление произвольной пары. Вот такое вот нехитрое доказательство

Давайте посмотрим его в действии:

xew = {5=>3,1=>6,3=>2}
xew.shift     #-> [5,3]
xew           #-> {1=>6, 3=>2}

Обратите внимание, что метод .shift возвращает удаляемую пару в виде индексного массива [ ключ, значение ].

Не стоит обольщаться по поводу того, что метод .shift возвращает первую пару. Помните, что ассоциативные массивы — неупорядоченны
Однажды нерадивому студенту был задан вопрос: как упорядоченны ключи в ассоциативном массиве? На что он дал радостный ответ «по возрастанию» и получил заслуженного «гуся». Не повторяйте его ошибку! Помните, что пары в ассоциативных массивах неупорядоченны.

[править] Преобразовать в индексный массив

Чуть ранее уже говорилось, что в большинстве случаев индексные массивы удобней ассоциативных.

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

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

Чтобы преобразовать ассоциативный массив в индексный, надо использовать метод to_a. Его используют все, кто не может запомнить методов работы с хешами.

xew = {"гаечный ключ" => 10, "разводной ключ" => 22}
xew.to_a  #-> [ ["гаечный ключ", 10], [разводной ключ", 22 ] ]

Способ преобразования таков. Сперва пара и преобразуется в массив:

{["гаечный ключ" => 10], ["разводной ключ" => 22]}

Затем «стрелку» заменяем на запятую

{["гаечный ключ", 10], ["разводной ключ", 22]}

и фигурные скобки выпрямляем, так что теперь их можно заправить в стаплер.

[["гаечный ключ", 10], ["разводной ключ", 22]]

[править] Упорядочение хеша

Да, множество пар в хеше неупорядоченно. Но это можно исправить, разве что результат потом будет не хешем, а двумерным массивом.

xew = {"гаечный ключ" => 4, "разводной ключ" => 10}
xew.sort #-> [ ["гаечный ключ", 4], ["разводной ключ", 10] ]
☞
Сначала хеш упорядочивается по ключам, а потом, в случаях равнозначных ключей, — по значениям.

В методе .sort_by передаются два значения:

xew = {"гаечный ключ" => 4, "разводной ключ" => 10}
xew.sort_by{ |key,value| value } #-> [ ["гаечный ключ", 4], ["разводной ключ", 10] ]

Здесь мы упорядочили хеш по значению.

[править] Поиск максимальной/минимальной пары

Максимальная пара в хеше ищется точно также, как и максимальный элемент в массиве

xew = {"гаечный ключ" => 10, "разводной ключ" => 22}
xew.max   #-> ["разводной ключ", 22]
xew.min   #-> ["гаечный ключ", 10]

но с небольшими особенностями:

  • результат поиска — массив из двух элментов вида [ключ, значение]
  • сначала поиск происходит по ключу, а в случае совпадения ключей — по значению

Несколько больше возможностей приобрели методы .max_by и min_by

но, к сожалению, у меня не установлена версия 1.9 и нет способа проверить возможность развертки массива во время передачи в блок. Дальнейшая информация может быть неверной
xew = {"гаечный ключ" => 10, "разводной ключ" => 22}
xew.max_by{ |key,value| value }  #-> ["разводной ключ", 22]
xew.min_by{ |array| array[0] }   #-> ["гаечный ключ", 10]

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

[править] Логические методы

Работа логических методов похожа на допрос с пристрастием. Помните, как в детективах во время теста на детекторе лжи, главный герой восклицал: "Отвечать только да или нет!" Если перевести это на язык Ruby, то это будет звучать примерно так: "Отвечать только true или false!"

В детективах набор вопросов стандартен:

  • знали ли вы мистера Х?
  • вы были на месте преступления?
  • убивали ли мистера Х
  • ...

На Ruby примерно тоже самое:

  • ты пустой?
  • есть ли такой элемент?
  • ты массив?
  • уверен, что не строка?

Но давайте рассмотрим их подробней.

[править] Хеш пустой?

Зададим вопрос "Хеш пустой?", но используя известный нам лексикон. Для начала спросим "Пустой хеш тебе не брат-близнец?"

nycTou_xew = {}
noJIHbIu_xew = { "гаечный" => 20, "замочный" => "английский", "разводной" => 34 }

nycTou_xew == {}   #-> true
noJIHbIu_xew == {} #-> false

Можно спросить по другому — "Размер у тебя не нулевой?"

nycTou_xew = {}
noJIHbIu_xew = { "гаечный" => 20, "замочный" => "английский", "разводной" => 34 }

nycTou_xew.size.zero?    #-> true
noJIHbIu_xew.size.zero?  #-> false

Но давайте будем задавать правильные вопросы

nycTou_xew = {}
noJIHbIu_xew = { "гаечный" => 20, "замочный" => "английский", "разводной" => 34 }

nycTou_xew.empty?    #-> true
noJIHbIu_xew.empty?  #-> false

а то еще примут нас за приезжих...

☞
Обратите внимание, что метод .empty? полностью повторяет такой же метод у индексных массивов

[править] Есть такой ключ?

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

kapmaH = { "гаечный" => 20, "замочный" => "английский", "разводной" => 34 }
kapmaH.keys.include?( "гаечный" )    #-> true

В данном примере у нас нашелся в kapmaH`e нашелся "гаечный" ключ.

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

kapmaH = { "гаечный" => 20, "замочный" => "английский", "разводной" => 34 }
kapmaH.key?( "гаечный" )            #-> true

или в стиле индексных массивов

kapmaH = { "гаечный" => 20, "замочный" => "английский", "разводной" => 34 }
kapmaH.include?( "гаечный" )        #-> true

Это несколько сократит первоначальное предложение, но тогда можно перепутать хеш с массивом.

☞
Этот же вопрос можно задать методами: .member? и .has_key?

[править] Есть такое значение?

Давайте подумаем, как задать вопрос "Есть такое значение?" хешу. Скорее всего, мы опять зададим вопрос в два этапа: "какие значения есть?" и "есть ли среди них нужное нам?"

kapmaH = { "гаечный" => 20, "замочный" => "английский", "разводной" => 34 }
kapmaH.values.include?( "гаечный" )    #-> false - ой, забыл сменить
kapmaH.values.include?( "английский" ) #-> true

Но аборигены говорят иначе и задают вопрос напрямую

kapmaH = { "гаечный" => 20, "замочный" => "английский", "разводной" => 34 }
kapmaH.value?( "английский" ) #-> true
☞
Задать вопрос "Есть такое значение?" можно не только при помощи метода .value?, но и при помощи более длинного .has_value?

[править] Итераторы

У ассоциативных массивов есть следующие итераторы:

  • .find_all — поиск всех элементов, которые удовлетворяют логическому условию
  • .map — изменение всех элементов по некоторому алгоритму
  • .inject — сложение, перемножение и агрегация элементов массива

Набор итераторов точно такой же, как и у индексных массивов — сказывается их родство. Вот только ведут себя они несколько иначе:

  • результатом является двумерный массив (как после метода .to_a)
  • в качестве счетчика (переменной в фотографии) передается массив вида [ключ, значение]
  • можно развернуть массив вида [ключ, значение] в две переменные
  • в итераторе .inject развернуть массив не получится

Рассматривать заново работу каждого итератора в отдельности скучно. Поэтому мы будем рассматривать работу всех итераторов сразу.

xew = {"гаечный ключ" => 4, "разводной ключ" => 10}

xew.find_all{ |array| array[1] < 5 }
  #-> [ ["гаечный ключ", 4] ]

xew.map { |array| "#{array[0]} на #{array[1]}" }
  #-> ["гаечный ключ на 4", "разводной ключ на 10"]

xew.inject(0){ |result, array| result + array[1] }
  #-> 14

Обратите внимание на то, что в качестве счетчика передается массив из двух элементов. В наших примерах счетчик итератора мы назвали array. В своих программах вы вольны называть его как угодно.

☞
Есть подозрение, что перед работой любого из итераторов вызывается метод .to_a. Уж больно работа итераторов в хешах напоминает работу с двумерным массивом

Теперь посмотрим, как можно развернуть array в две переменные. Делается это простой заменой array на key, value

xew = {"гаечный ключ" => 4, "разводной ключ" => 10}

xew.find_all{ |key, value| value < 5 }
  #-> [ ["гаечный ключ", 4] ]

xew.map { |key, value| "#{key} на #{value}" }
  #-> ["гаечный ключ на 4", "разводной ключ на 10"]

xew.inject(0){ |result, key, value| result + value }
  #-> Ошибка в методе '+' невозможно сложить nil и число типа Fixnum

Обратите внимание, что развертка массива прошла успешно только в первых двух итераторах. В третьем возникла ошибка. Давайте выясним, откуда там взялся nil. Дело в том, что развернуть массив не удалось и теперь он стал называться не array, а key. Переменная value осталась "не у дел" и ей присвоилось значение nil. Чтобы это исправить, достаточно поставить круглые скобки:

xew.inject(0){ |result, (key, value)| result + value }
  #-> 14

☞
Итератор .map, вызванный без аргументов, аналогичен методу .to_a: просто раскладывает хеш в двумерный массив.

[править] Хитрости

Одному программисту надоело писать xew["key"] и он захотел сделать так, чтобы можно было написать xew.key.

class Hash
    def method_missing( id )
        self[ id.id2name ]
    end
end

xew = {"hello" => "привет","bye" => "пока"}
xew.hello  #-> "привет"
xew.bye    #-> "пока"

Естественно, что ключи в таком хеше могут содержать только латиницу, нижнее подчеркивание и цифры (везде, кроме первого символа). Иначе, говоря удовлетворять всем требованиям, которые мы предъявляем к именам методов и именам переменных.

[править] Задачи

  1. Дан массив слов. Необходимо подсчитать сколько раз встречается каждое слово в массиве.

[править] Подробнее о строках

☞
Строка — это упорядоченная последовательность символов, которая располагается между ограничительными символами

Строковый тип является самым популярным в любом языке программирования. Ведь без него невозможно написать любую программу (особенно учитывая, что любая программа — это строка). При выводе на экран или записи в файл, любой тип данных преобразовывается к строке (явно или неявно). Это значит, что в конечном итоге все сводится к строковому типу. Кстати, и ввод данных тоже осуществляется в виде строки (и только потом преобразовывается в другие типы).

Студенты 4-го курса МЭТТ ГАИ поступили на подготовительные курсы в МГИУ. Там им начали преподавать основы программирования на Ruby. И одна из заданных им задач была: "Дано число, необходимо поменять порядок цифр на обратный". Задача сложная, но наши студенты об этом не знали и решили ее преобразованием к строке: ucxogHoe.to_s.reverse. Преподаватели были поражены и впредь запретили им использовать преобразования к строке в своих программах. И все потому, что это сильно упрощало решение и давало студентам огромное преимущество перед остальными слушателями курсов.

Язык Ruby унаследовал работу со строками из языка Perl (признанного лидера по работе со строками). В частности такой мощный инструмент как «правила» (rules).

☞
Раньше «правило» называлось «регулярным выражением», но в виду того, что на данный момент механизм «правил» вышел далеко за пределы регулярной грамматики, то решено его было переименовать (дабы не смущать умы образованной общественности)

Но наследование не подразумевает бездумного копирования. В частности, «правила», в рамках Ruby, получили объектно-ориентированную реализацию, что позволяет применять к ним различные методы. Помимо «правил», присутствует великое множество методов работы со строками. Причем некоторые из них являются нашими старыми знакомыми (+, *, [] и т.д.). Работают они несколько иначе, но некоторая параллель с массивами все же присутствует. Следует упомянуть два очень интересных момента:

  • Cтроки – это универсальный тип данных, т.к. в строку можно преобразовать любой другой тип данных. А также, строку можно преобразовать в любой другой тип данных (ведь изначально любой код программы – это строка).
  • Cтроки очень удобно преобразовывать в массив и обратно (методы .join и .split). Поэтому работа со строками практически такая же удобная, как и с массивами.
Если работа со строками обходится без преобразования в массив, то программа либо очень простая, либо бесполезная

[править] Способы создания строки

Строка создается при помощи ограничительных символов. Для этих целей чаще всего используются " (лапка) и ' (типографский апостроф/минута/одиночная кавычка). Их смысл различен. Строка в минутах гарантирует, что в ней будет содержаться текст такой же, как в коде программы, без изменений. Строка в лапках будет проходить предварительное преобразование. Будут раскрыты конструкции «вставка» и «специальный символ».

Зажатые в лапках работать хотят,
а строки в минутах лениво сопят.

Давайте будем называть строки в минутах «ленивыми», а строки в лапках — «работящими».

☞
«Вставка» — это хитрая конструкция, которая вставляется между ограничительными символами (внутрь строки). Она состоит из комбинации решетки и двух ушек ( #{ 'здесь был Вася' } ). Внутри нее можно писать не только 'Здесь был Вася', но и любой программный код. Результат программного кода будет преобразован к строке и вставлен вместо «вставки»

«Вставка» жизнью заправляет:
Код программный выполняет,
Тихо результат считает,
Вместо «вставки» подставляет.

«Вставка» работает только в момент создания строки. После создания придется придумывать другие способы подстановки данных в строки

Специальный символ начинается с знака \ (обратная косая черта). Самые популярные из них: \n (переход на новую строку), \t (табуляция), \\ (обратная косая черта) и \" (двойная кавычка).

Хотя специальный символ и пишется, как два знака, но на деле это всего один символ. Доказать это можно выполненением простенького кода: "\n".size #-> 1

[править] Для чего нужны работящие и ленивые строки?

Скорее всего вы будете редко вспоминать про то, что существуют работящие и ленивые строки. Тем более, что это различие действительно только на момент создания строки. Рядовой программист пользуется либо работящими, либо ленивыми строками. Давайте посмотрим, как выглядит код программиста, который использует только ленивые строки:

moe_4ucJIo = 18
mou_maccuB = [1,2,3,4]
puts 'Мое число = ' + moe_4ucJIo.to_s + ', а мой массив длины ' + mou_maccuB.size.to_s

Обратите внимание, что перед сцеплением (умные дяди называют это конкатенацией) необходимо все данные преобразовывать к строке методом .to_s. «Вставка» позволяет этого избежать. Вот как будет выглядеть та же самая программа с использованием «вставки»:

moe_4ucJIo = 18
mou_maccuB = [1,2,3,4]
puts "Мое число = #{moe_4ucJIo}, а мой массив длины #{mou_maccuB.size}"

Программа стала не только меньше, но и лучше читаться. Исчезли многочисленные сцепления.

☞
Если внутри «вставки» надо создать строку, то экранировать кавычки не стоит. Внутренности вставки не являются частью строки, а значит «живут» по своим законам.
moe_4ucJIo = 18
mou_maccuB = [1,2,3,4]
puts "Повторенье -- мать ученья. Мой массив = #{mou_maccuB.join(\",\")}"

Программа вызовет ошибку, т.к. внутри «вставки» было использовано экранирование кавычек. Правильный пример будет выглядеть так:

moe_4ucJIo = 18
mou_maccuB = [1,2,3,4]
puts "Повторенье -- мать ученья. Мой массив = #{mou_maccuB.join(",")}"
Необходимости в экранировании строк внутри «вставки» — нет!

[править] Методы работы со строками

Методы строк умеют:

  • преобразовывать входные данные в красивый вид
  • красиво оформить выходные данные
  • дезертировать в массивы

Допустим, вы нашли максимальный элемент массива. И вам надо вывести результат на экран. Вы можете поступить вот так:

maccuB = [4, 4, 2, 5, 2, 7]
puts maccuB.max   #-> 7

Несмотря на правильность решения, вывод результата выглядит некрасиво. Гораздо профессиональней будет написать вот такую программу:

maccuB = [4, 4, 2, 5, 2, 7]
puts "Максимальный элемент maccuB'a = #{maccuB.max}"
   #-> Максимальный элемент maccuB'a = 7

Заметили, насколько привлекательней стал вывод результата, когда мы задействовали строки?

☞
Заниматься оформлением выходных данных стоит только непосредственно перед выводом результата. В остальное время, использование оформительских элементов будет только мешать

Всем известно, что числа внутри компьютера хранятся в двоичной системе. Но вот парадокс: получить двоичное представление числа иногда очень сложно. В частности, для того, чтобы получить двоичную запись числа необходимо создать строку и записывать результат в нее. Это значит, что результатом преобразования в другую систему счисления (из десятичной) будет строка. Давайте посмотрим, как эта задача решается:

ucxogHoe_4ucJIo = 123
puts "В двоичном виде -> %b" % ucxogHoe_4ucJIo
   #-> В двоичном виде -> 1111011

Мы задействовали метод % (аналог sprintf в Си), который осуществляет форматирование строки. Эту же задачу можно решить несколько иначе.

ucxogHoe_4ucJIo = 123
puts "В двоичном виде -> #{ ucxogHoe_4ucJIo.to_s(2) }"
   #-> В двоичном виде -> 1111011

Со вторым вариантом вы могли уже встречаться в разделе, посвященном числам (подраздел Хитрости).

☞
Методы sprintf и printf в Ruby также присутствуют, но по непонятным причинам используются крайне редко. Чаще всего они заменяются на методы % и puts

[править] Арифметика строк

Кто бы мог подумать, но строки можно складывать, умножать и если постараться, то и делить. Естественно, что это не арифметические операции чисел, а методы со своим особенным поведением. Рассмотрим сложение, которое в строках работает как сцепление (конкатенация):

cynpyr = "Саша"
cynpyra = "Маша"
cynpyr + " плюс " + cynpyra + " = семья!"
   #-> "Саша плюс Маша = семья!"

Вот такое вот романтическое сцепление сердец!

Переходим к умножению. Умножают строки только на целое число. Строка, которую умножают просто копируется указанное число раз. Давайте напишем речь для кукушки, чтобы она не сбилась и не накукукала нам слишком мало.

ocTaJIocb_JIeT = 100
"Ky-ky! " * ocTaJIocb_JIeT
    #-> "Ky-ky! Ky-ky! Ky-ky! Ky-ky! Ky-ky! Ky-ky! Ky-ky! … Ky-ky! Ky-ky! "

Теперь за свое будущее можно не беспокоиться! Хотя нам может попасться неграмотная кукушка…

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

pe4b_kykywku = "Ky-ky! Ky-ky! Ky-ky! Ky-ky! Ky-ky! Ky-ky! Ky-ky! ... Ky-ky! Ky-ky! "
( pe4b_kykywku / " " ).size   #-> 100
☞
  • В нашем примере метод .size считает число элементов массива, который получился в результате деления
  • Скобки нужны для того, чтобы вызвать .size от результата деления, а не от " "

Деление в примере работает также как и метод .split, о котором речь пойдет чуть позже. А чтобы оно заработало у вас, необходимо добавить небольшой код в начало программы:

class String
    alias / :split
end

Этот код как раз и говорит о том, что деление и .split — одно и тоже.

☞
Деление стало нужно, когда метод * для массивов получил возможность работать, как .join (преобразовать массив в строку, расположив элементы через разделитель). В виду того, что .join и .split работают вместе точно также, как умножение и деление, то появилась идея заставить работать деление как .split

[править] Преобразование в массив или путешествие туда и обратно

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

"Ky-ky".split('-')   #-> ["Ky","ky"]
"Ky-ky".split('y')   #-> ["K","-k"]
Обратите внимание, что разделитель из результата исчезает

За преобразование строки в массив отвечает метод .split. Ему передается разделитель, по которому будет происходить деление строки на элементы. В нашем случае это '-'. Теперь вернем все на место. Для этого мы будем использовать метод .join из арсенала массивов:

["Ky","ky"].join('-') #-> "Ky-ky"
["Ky","ky"].join(',') #-> "Ky,ky"
["Ky","ky"].join      #-> "Kyky"
["Ky","ky"].to_s      #-> "Kyky"
☞
  • Разделитель будет вставлен между элементами исходного массива
  • Метод .join вызванный без разделителя дает такой же результат, как и метод .to_s

Чуть ранее мы упоминали, что можно использовать умножение вместо .join. Давайте посмотрим, как это выглядит:

["Ky","ky"] * '-'  #-> "Ky-ky"
["Ky","ky"] * 3    #-> ["Ky","ky","Ky","ky","Ky","ky"]
Если умножать массив на целое число, то будет размножение массива (прямо как умножение в строках), а не преобразование в строку

[править] Длина строки

Длина строки ищется точно также, как и длина массива или хеша, то есть методом .size:

"Во дворе дрова, а в дровах трава!".size #-> 33
"Три".size    #-> 3
Следует помнить, что пробел является символом, хотя и не отображается на экране
Ограничительные лапки или апострофы в количество символов не входят! Они предназаначены лишь для того, чтобы Ruby понял, где начинается и заканчивается строка

[править] Получение подстрок

Получение подстрок работает точно также, как и получение подмассива. С тем лишь отличием, что нумерация идет не по элементам, а по символам. Это логично, особенно, если учесть, что для строки элементом является символ.

cTpoka = "Во дворе дрова, а в дровах трава!"
cTpoka[27..-1]  #-> "трава!"
cTpoka[27...-1] #-> "трава"
cTpoka[9...14]  #-> "дрова"
cTpoka[9..13]   #-> "дрова"
☞
Отрицательная нумерация тоже работает, то есть последний элемент имеет индекс -1, предпоследний -2 и так далее
Помните, что три точки в диапазоне дают подстроку без крайнего правого элемента. Кто-то шлагбаумом балуется!

Чтобы получить один единственный символ, необходимо получить подстроку длиной 1.

cTpoka = "Во дворе дрова, а в дровах трава!"
cTpoka[3..3] #-> "д"
cTpoka[3]    #-> 228
Для получения подстроки или символа из строки необходимо всегда указывать диапазон. Даже если в диапазоне всего один элемент

Если мы указываем не диапазон, а число, то метод [] выдает нам не символ, а его целочисленный код.

cTpoka = "Во дворе дрова, а в дровах трава!"
cTpoka[3]     #-> 228
cTpoka[3].chr #-> "д"
☞
Для преобразования целочисленного кода обратно в символ, используется метод .chr

[править] Строка-перевертыш

Иногда хочется перевернуть строку задом наперед. Причины могут быть разные. Например, вы ищите полиндром (число, которое можно перевернуть без ущерба для его значения). Занимается этим благородным делом метод .reverse. Давайте сформируем сообщение для Иных по другую сторону Сумрака:

"Ночной дозор! Всем выйти из Сумрака!".reverse
    #-> "!акармуС зи итйыв месВ !розод йончоН"

Вот примерно такой бред надо прокричать, чтобы вас поняли. Кстати, попробуйте быстро проговорить полученный текст.

☞
Попробуйте применить метод .reverse к строке "Улыбок тебе дед Макар"
Не стоит путать метод .reverse для массивов с методом .reverse для строк. В массивах меняется порядок элементов, а в строках — символов

[править] Меняю шило на мыло!

Для того, чтобы заменить "шило" на "мыло" используется не газета "Из рук в руки", а методы .sub и .gsub:

"шило в мешке не утаишь".sub("шило","мыло")    #-> "мыло в мешке не утаишь"

Естественно, что менять можно не только шило и мыло, но и другие данные. Например, возраст. Девушка Ирина утверждает, что ей 18, но я-то знаю, что ей 26. Давайте восстановим истину (в ущерб своему здоровью):

"Ирине 18 лет".sub("18","26")   #-> "Ирине 26 лет"

Заметили, что мы используем только метод .sub? Давайте теперь рассмотрим работу метода .gsub и его отличие от .sub. На этот раз мы будем исправлять текст, авторы которого забыли про правило "ЖИ, ШИ — пиши через И"

cTpoka = "жыло-было шыбко шыпящее жывотное"
cTpoka.sub("жы","жи")   #-> "жило-было шыбко шыпящее жывотное"
cTpoka.gsub("жы","жи")  #-> "жило-было шыбко шыпяшее животное"
cTpoka.gsub("жы","жи").gsub("шы","ши")
   #-> "жило-было шибко шипящее животное"
Метод .sub производит только одну замену, а .gsub — все возможные

Одну ошибку лишь исправил
Ленивый метод .sub
Все остальные устранил
Трудолюбивый .gsub

☞
  • Название метода .sub произошло от английского substring (подстрока)
  • Название метода .gsub корнями уходит в язык Perl. В нем, для осуществления всевозможных замен, использовалась конструкция s///g, где модификатор /g означал все возможные замены (от английского global — всеобщий, глобальный)

[править] Сканируем текст на оШЫбки

Давайте, найдем и посчитаем оШЫбки. Искать мы будем методом .scan

cTpoka = "жыло-было шыбко шыпящее жывотное"
cTpoka.scan("шы")       #-> ["шы","шы"]
cTpoka.scan("шы").size  #-> 2
cTpoka.scan("жы").size  #-> 2
cTpoka.scan("жы").size + cTpoka.scan("шы").size  #-> 4
☞
Метод .scan находит все указанные подстроки и возвращает их в виде массива строк. В данном примере, метод .size считает количество элементов массива, возвращаемого .scan

Ужас, в одном предложении целых четыре ошибки. Будем отчислять!

Нашел оШЫбку метод .scan,
В массив ее запомнил.
Учителям он свыше дан!
Зачем его я вспомнил?!

[править] Правила работы со строками

Правила — это образцы к которым можно примерять строки. Правила обладают своим собственным языком, который позволяет описывать одну, две, сотню и вообще любое количество строк. Это своеобразная упаковка для множества строк в одну компактную запись.

Правила в Ruby ограничиваются символами / (наклонная черта). Примеры правил:

/(жы|шы)/
/\w+@[\w\.]+\w+/i

Страшно? А мне нет. На самом деле работа с правилами очень проста. Главное привыкнуть и попрактиковаться.

Правила состоят из:

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

[править] Символьный класс

Символьный класс — просто конечный набор символов. Он ограничивается квадратными скобками и содержит перечисление символов, которые можно вместо него подставить. Заменяется он всего на один символ, входящий в этот класс. Примеры символьных классов:

/[абвгде]/      #-> простое перечисление символов
/[а-яА-Я]/      #-> все русские буквы
/[0-9a-z]/      #-> цифры и строчная латиница
/[^0-9]/        #-> все символы, кроме цифр
☞
  • Можно использовать - (дефис) для указания диапазонов символов
  • Если первый символ класса (идущий сразу после открывающейся квардратной скобки) ^ (крышка), то это означает символ, который отсутствует в данном классе
  • Некоторые популярные классы имеют короткую запись

Краткие записи популярных символьных классов

Короткая запись | Полная запись | Описание
\s | [\f\t\n\r ] | пробельный символ
\S | [^\f\t\n\r ] | любой символ, кроме пробельного
\d | [0-9] | цифра
\D | [^0-9] | любой символ, кроме цифры
\w | [a-zA-Z0-9] | латиница или цифра
\W | [^a-zA-Z0-9] | любой символ, кроме латиницы или цифры
. | [^\n\r] | любой символ, кроме перевода строки
☞
Взгляните на примеры правил! Правда, они стали понятней? По крайней мере второе...

[править] Квантификатор

Квантификатор показывает сколько раз может повторяться предыдущий символ (группа, альтернатива или еще какая нечисть). Квантификатор ограничивается парой ушек (фигурными скобками). Примеры квантификаторов:

/\w{3}/          #-> три латинских буквы или цифры
/\d{1,3}/        #-> одна, две или три цифры
/[а-яА-Я]{3,}    #-> русское слово длинной три символа и больше
☞
  • Квантификатор с одним параметром называется точным и указывает точное количество повторений
  • Квантификатор с двумя агрументами называется конечным и указывает конечный диапазон в котором варьируется количество повторений
  • Квантификатор без второго параметра (но с запятой) называется бесконечным и ограничивает количество повторений лишь снизу
  • Некоторые популярные квантификаторы имеют короткую запись

Краткие записи популярных квантификаторов

Короткая запись | Полная запись | Описание
* | {0,} | любое количество
+ | {1,} | один и более
? | {0,1} | есть или нет
☞
Снова посмотрите на примеры правил. Теперь вам они понятны? Если нет, то перечитайте две предыдущие главы — в них основа правил

[править] Альтернатива

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

/(жы|шы)/          #-> или "жы", или "шы"
/(\w+|[а-яА-Я]+)/  #-> или слово на латинице, или русское
☞
Вместо альтернативы можно задействовать логические итераторы .any? и .all? внутри .inject. Получается более гибкая конструкция
В данном примере продемонстрирована альтернатива с группировкой. В принципе альтернатива может существовать и без нее, но так возникает меньше ошибок у начинающих

[править] Группировка

Группировка используется, когда необходимо обрабатывать результат частями. Например, при обработке ссылок в HTML-документе удобно отдельно обрабатывать текст ссылки и URL. Группировка также как и альтернатива, заключается в круглые скобки. Более того, альтернатива обрабатывается как группировка. Доступ к результату совпадения каждой группировки осуществляется посредством специальных переменных $1, $2, ..., $9. Подробнее группировки будут рассмотрены в подразделе "Правильная замена". Пример использования группировки:

"2+7*3".gsub(/(\d+)\*(\d+)/){ $1.to_i * $2.to_i }  #-> "2+21"

Почти калькулятор!

☞
Существует много видов группировок. Например, (?:…) — группировка без сохранения результата в «долларовую переменную» или (?!…) — негативная группировка. В любом случае они ограничиваются парой круглых скобок

[править] Фиксирующая директива

Фиксирующие директивы — это символы, которые привязывают правило к некоторому признаку. Например, к концу или началу строки.

/^\d+/   #-> первое число в строке
/\w+$/   #-> последнее слово на латинице или число
/^$/     #-> пустая строка

Насколько видно из примеров,

  • ^ — привязка к началу строки
  • $ — привязка к концу строки
Фиксирующих директив гораздо больше двух. Об остальных читайте в специализированной литературе

[править] Модификатор

Модификатор предназначен для изменения поведения правила. Он размещается сразу же после правила (после последней наклонной черты). Пример использования модификатора:

/(hello|world)/i   #-> или "hello", или "world". Причем независимо от регистра
/\s+/mix           #-> несколько подряд идущих пробельных символов

Бывают следующие модификаторы:

  • multiline — перенос строки считается простым символом
  • ignorcase — поиск без учета регистра
  • extended — игнорировать пробельные символы
Игнорирование регистра работает только для латиницы
☞
  • Можно применять любое количество модификаторов и в любом порядке
  • Обратите внимание, что модификаторы образуют слово mix
☞
Теперь можно не бояться страшных правил

[править] Правильное разбиение

Разбиение называется «правильным» тогда, когда в качестве аргумента метода .split используется правило. Например, можно разбить текст по знакам препинания. Для этого необходимо выполнить следующий код.

"Раз, два, три!".split(/[,\.?! ]+/)  #-> ["Раз","два","три"]

Обратите внимание, что в результирующем массиве знаки припинания отсутствуют.

[править] Правильная замена

С правильной заменой не все так просто. Дело в том, что методы .sub и .gsub совместно с правилами становятся итераторами, которые последовательно обрабатывают каждое совпадение с правилом. Чтобы это увидеть в действии, давайте решим задачу исправления ошибок:

"Жыло-было шыбко шыпящее жывотное".gsub(/(ж|ш)ы/){ $1 + 'и' }
    #-> "Жыло-было шибко шипящее животное"

Опаньки, а первое слово не исправилось! Видимо дело в том, что слово "Жыло" начинается с прописной буквы. Сейчас исправим:

"Жыло-было шыбко шыпящее жывотное".gsub(/(Ж|Ш|ж|ш)ы/){ $1 + 'и' }
    #-> "Жило-было шибко шипящее животное"

Вот, теперь гораздо лучше. Как мы этого добились? Давайте разберемся. Начнем с регулярного выражения:

/(Ж|Ш|ж|ш)ы/

Оно состоит из двух частей:

  • альтернативы с группировкой — (Ж|Ш|ж|ш)
  • символа — ы

В альтернативе мы указали буквы с которых начинается неправильный слог. Символ просто добавляется к букве из альтернативы.

Зачем была использована группировка? Для пояснения причины, рассмотрим код в ушках:

{ $1 + 'и' }

Вот для того, чтобы можно было использовать переменную $1 (результат первой группировки) мы и задействовали группировку. В данном случае, в $1 сохраняется первая буква слога, которая в результате исправления оШЫбки не меняется.

☞
  • Для того, чтобы получить доступ к результату первой группировки, надо обратиться к переменной $1 (один доллар), ко второй $2 (два доллара) и так далее до переменной $9 (девять долларов)
  • Переменные $1-$9 заимствованы из языка Perl

Можно ли было решить эту же задачу иначе? Конечно можно!

"Жыло-было шыбко шыпящее жывотное".gsub(/([ЖШжш])ы/){ $1 + 'и' }
    #-> "Жило-было шибко шипящее животное"

На этот раз мы просто задействовали символьный класс вместо альтернативы, который описывает первую букву слога с оШЫбкой.

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

  • А как получить весь текст, который совпал с правилом?
  • Неужели необходимо делать всеобщую группировку?

Ответ на эти вопросы одназначный — нет! Достаточно придумать название переменной (которая будет содержать совпавший текст) и правильно описать внутри ушек:

"Раз, два, три!".gsub(/[а-я]+/){ |cJIoBo| cJIoBo.reverse }
     #-> "Рза, авд, ирт!"

Нашей переменной мы дали название cJIoBo, что вполне согласуется с нашим geJIom.

[править] Правильный поиск

Вот здесь метод .scan может развернуться в полную силу. Хотите получить массив всех русских слов в тексте? Запросто:

"Раз, два, три!".scan(/[А-Яа-я]+/)
     #-> ["Раз","два","три"]

Хотите получить все знаки препинания? Нет ничего проще:

"Раз, два, три!".scan(/[,\.;:! ]+/)
     #-> [", ",", ","!"]
Если необходимо в метод .scan передавать правило с группировкой, то желательно использовать группировку без сохранения результата, т.е. (?:…). Иначе результатом метода .scan будет совпадение с группировкой, а не с правилом

Например, ниже записана программа, которая занимается поиском адресов электронной почты.

cTpoka = "495-506-13 56 nata@rambler.ru(34) 1.5.1232 12.14.56 31.декабря.9999"
cTpoka.scan(/(?:[-a-z_\.])*@(?:[-a-z])*(?:\.[a-z]{2,4})+/) #-> ["nata@rambler.ru"]

Выполните ее, посмотрите результат, а потом замените любую из группировок (?:…) на (…) и снова взгляните на рузультат.

Ну со .scan должно быть все понятно. А вот то, что метод [] начинает тоже правильно искать — пока нет.

"Раз, два, три!"[/[А-Яа-я]+/] #-> "Раз"
Если методу [] передать в качестве параметра правило, то он вернет либо совпадение с правилом, либо nil

Очень полезно использовать [] в ситуациях, когда надо узнать ответ на вопрос "Есть хотя бы одна подстрока, которая удовлетворяет правилу?" или получить первое (или единственное) совпадение с правилом.

☞
Существует древнее поверье, что если использовать одно и тоже правило для .scan и .split, то получаются две части текста, из которых реально получить исходный.
TekcT.scan( npaBuJIo ) + TekcT.split( npaBuJIo ) = TekcT
Это значит, что если метод .split использует правило, описывающие все знаки припинания, то результатом будет текст без знаков припинания. А вот если это же правило будет использовать метод .scan, то в результате мы получим все знаки препинания без текста
Рекомендуется использовать метод [] вместо метода =~ (заимствованного из Perl), так как [] более функционален

[править] Жадность

Речь пойдет о жадности среди квантификаторов. Возьмем некоторый квантификатор {n,m} и посмотрим как он работает.

☞
Нежадные квантификаторы иногда называют щедрыми

Сперва он начинает искать последовательность длины m (вот так щедрость) и если правило не срабатывает, он начинает уменьшать длинну последовательности вплоть до n. Так работают обычные (щедрые) кванторы.

Но иногда щедрые кванторы не могут справиться с задачей. Например, в файле на языке HTML мы осуществляем поиск ссылок (тег <a>). Правило с щедрым квантором найдет начало первой ссылки и конец последней. Весь остальной текст оно примет за текст ссылки. Понятно, что работать верно оно будет только в двух случаях: когда в тексте нет ссылок или когда ссылка только одна.

Для решения вышеописанной проблемы и был придуман так называемый жадный квантификатор. От щедрого он отличается обратным ходом обработки, то есть длину последовательности он не уменьшает от m к n, а наоборот, увеличивает от n до m. Научить жадности квантификатор можно знаком вопроса ? после любого (щедрого) квантификатора.

"Раз, два, три!".scan(/[А-Яа-я]+?/)
    #-> ["Р","а","з","д","в","а","т","р","и"]
"Жуй жвачку, жывотное!".gsub(/([жЖшШ]??)ы/){ $1 + 'и' }
    #-> "Жуй жвачку, животное!"
С рождения квантификаторы щедры. Жадность — обретаемый признак.

[править] Хитрости

[править] Перенос по словам

Несколько лет назад (еще при жизни http://ruby-forum.ru) решали мы интересную задачу: как реализовать автоматический перенос на новую строку (wrap). Для тех, кто не застал те времена, уточню задание: дан текст, необходимо, вставить переносы таким образом, чтобы каждая из полученных строк была меньше n (для определенности n=80). Недавно я осознал, что не могу решить эту задачу тем способом, который был нами тогда найден. Я его просто не помнил... Решение нашлось быстро, достаточно было вспомнить, что на англ. языке эта задача именуется коротким и емким словом wrap.

   class String
       def wrap(col = 80)
         gsub(/(.{1,#{col}})( +|$\n?)|(.{1,#{col}})/,"\\1\\3\n")
       end
   end

Немного о структуре кода... метод .wrap реализован для экземпляров класса String. Также стоит обратить внимание на то, что внутри правила (регулярного выражения) возможна "вставка" (как в "рабочих строках"). Используется сей метод следующим образом:

p "wrapping text with regular expressions".wrap( 10 )
       # -> "wrapping\ntext with\nregular\nexpression\ns\n"

Теперь давайте разберемся с правилом. Чтобы не смущать неокрепшие умы, заменим вставку на 80. Правило станет короче и менее страшным.

(.{1,80})( +|$\n?)|(.{1,80})

Очевидно, что оно состоит из четырех частей:

  • (.{1,80}) — строка длиной от 1 до 80 символов (любых). Результат группировки записывается в $1 (один доллар) или "\\1"
  • ( +|$\n?) — пробелы или конец строки. Результат группировки записывается в $2 (два доллара) или "\\2". Обратите внимание на запись $\n?, которая означает "конец строки ($), после которого может идти перенос (\n)". Обратите внимание, что $2 мы не используем и поэтому можно использовать (?: ) (группировку без сохранения результата)
  • | — или
  • (.{1,80}) — строка длиной от 1 до 80 символов (любых). Результат группировки записывается в $3 (три доллара) или "\\3"

В результате работы этого правила произойдет сопоставление с группировками 1 и 2 или с группировкой 3. В первом случае, будет обрабатываться строка, слова в которой по длине не превышают 80. Во втором случае, строка будет принудительно усечена до 80 символов. Другими словами, мы пытаемся сделать перенос по словам, но если у нас не получается, то мы будем делать перенос так, как у нас получится.

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

Источник (англоязычный): http://macromates.com/blog/archives/2006/06/28/wrapping-text-with-regular-expressions/

[править] Методы преобразования к строке

Руби сам преобразует типы для некоторых простых операций. Например, при включении строки в другую он воспользуется имеющимся у обьекта методом .to_s:

class Container
   def to_s
      "контейнер"
   end
 end

 cont = Container.new

 p "Это #{cont}" #-> "Это контейнер"

Если нужно, чтобы ваши обьекты упорядочивались и сравнивались с обычными строками, следует применять примесь Comparable и единственный специальный метод to_str. Наличие этого метода у вашего объекта — знак для Ruby, что для сравнения следует применять не встроенный в String метод, а ваш.

class Container
   include Comparable
   def to_str
      "контейнер"
   end

   def to_s
      "контейнер"
   end

   def <=>(other)
     to_s <=> other.to_s
   end
 end

 cont = Container.new

 "контейнер" == cont #-> true

[править] Задачи

  1. Дана строка слов, разделёных пробелами. Вывести длиннейшее слово слово.
  2. Дана строка, содержащая кириллицу, латиницу и цифры. Вывести все слова, длина которых равна средней.
  3. Найти в строке первое целиком кириллическое слово.
  4. Дан текст (строка с переносами). Найти все слова, содержащие лишь три буквы "о".
    • Только для русских слов.
    • Для французских и русских слов.
    • Для любого регистра буквы "о".
  5. Найти в тексте время в формате "часы:минуты:секунды".
  6. Найти все слова без повторяющихся букв (например, "Лисп" или "Ruby", но не "Паскаль" или "Java").
    • Только для русскоязычных слов.
    • Не учитывайте цифры в словах.
  7. Найти в тексте слова, содержащие две прописные буквы и исправить.
    • Решите задачу для слов и в кириллице, и в латинице.
  8. Найти в тексте даты формата "день.месяц.год".
    • Найдите дату, где день ограничен числом 31, а месяц 12. Год ограничивайте четырёхзначными числами.
    • Распознавайте месяц в виде "31.марта.2001".
  9. Дан текст. Найдите все URL адреса и вычлените из них только корневой домен (например, из http://ru.wikibooks.org/wiki/Ruby сделайте http://ru.wikibooks.org ).

[править] Подробнее о методах

Все функции в Ruby являются методами, то есть свойственны обьектам. При программировании на это можно не обращать внимания, поскольку любая программа на Руби уже является определением класса. У методов могут быть обязательные или необязательные параметры. Методы разграничиваются фигурными скобками или ключевыми словами def и end.

[править] Создание метода

Благодаря тому, что указание класса-носителя метода необязательно, на Ruby можно программировать в функциональном стиле, не заботясь о создании класса-"носителя" для каждой группы методов. Метод создается с помощью ключевых слов def ... end

 def sum(a, b)
   return a + b
 end
 sum(10, 2) #-> 12

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

 def sum(a, b)
     a + b
 end
 sum(10, 2) #=> 12

[править] Указание значений по умолчанию

У методов могут быть необязательные аргументы; для этого им нужно присвоить значение, которое следует применять "по умолчанию"

 def sum(a, b = 5)
   a + b
 end
 sum(10, 2) #-> 12
 sum(10) #-> 15

[править] Методы с восклицательным и вопросительным знаком

В Ruby при создании методов можно применять простейшую пунктуацию. Два стандартных приема применения такой пунктуации - восклицательный и вопросительный знак в конце метода. Методы с вопросительным знаком традионно работают как предикаты, то есть возвращают true или false. Пример методов-предикатов, — методы массива. Например, в Java подобные методы начинались бы со слова is: isVolatile(), isEnabled

Обычно программист, чтобы проверить, пуст ли массив, посмотрит его длину:

arr = []
if arr.length == 0
  puts "empty"
else
  puts "not empty"
end

У массива в Ruby есть метод-предикат .empty?, возвращающий true если массив пуст, и метод-предикат .any?, возвращающий true если массив содержит хотя бы один элемент.

arr = []
if arr.empty?
  puts "empty"
else
  puts "not empty"
end

Если вы реализуете программу, которой будут пользоваться другие, считается хорошим тоном реализовывать методы-предикаты.

Еще одна их прелесть — сочетание с модификаторами выражения:

 arr = [1,2 ,3]
 p "Array has something" if arr.any?

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

string = "   Some string with spaces     "
string.strip! #-> "Some string with spaces" - возвращает результат операции...

string        #-> "Some string with spaces" #=> ...и меняет состояние объекта-адресата

[править] Методы присвоения

Другие особые варианты пунктуации - знак равенства и арифметические знаки.

Знак равенства в конце названия метода означает, что этот метод присваивает свойству объекта значение:

 class Bottle
   def capacity
     @capacity
   end

   def capacity=(new_cap)
     @capacity = new_cap
   end
 end

 bytilka = Bottle.new
 bytilka.capacity = 10 #-> 10, автоматически преобразуется в вызов метода capacity=

[править] Операторы

Операторы (умножение, деление, возведение в степень и так далее - вплоть до сравнения!) - тоже методы. Например:

class BeHuK
  def+(another)
   12 + another
  end
end

meTeJIka = BeHuK.new
meTeJIka + 10        #-> 22

Это применяется, например, во встроенном в Ruby объекте Time. При прибавлении к нему целого числа он возвращает новый объект Time с добавленным количеством секунд:

t = Time.now #-> Sun Jun 11 20:29:51
t + 60             #-> Sun Jun 11 20:30:51 - на минуту позже

То же самое характерно для имеющегося в стандартной библиотеке класса Date, но, в отличие от Time, он считает дни вместо секунд

 require 'date'
 d = Date.today #-> Sun Jun 11
 d + 1 #-> Mon Jun 12 - на день позже

[править] "Поглощение" аргументов метода

Можно "свернуть" аргументы с помощью звездочки - тогда метод получит массив в качестве аргумента

def sum(*members)
  members[0] + members[1]
end
sum(10, 2) #-> 12

Поскольку теперь наш метод принимает неограниченное количество элементов, мы можем пользоваться ими как массивом и в теле функции

def sum(*members)
  initial = 0
  members.collect{ | item | initial += item }
  initial
end
sum(10, 2)         #-> 12
sum(10, 2, 12, 34) #-> 58

Можно разделить аргументы на обязательные и необязательные, просто пометив последний аргумент "звездочкой". Если методу будут переданы только обязательные аргументы, в переменной "со звездочкой" в теле метода будет пустой массив.

Звездочкой полезно пользоваться и когда нужно передать методу аргументы, но не хочется указывать их по отдельности. Следуя тому же примеру:

array_to_sum = [10, 2, 12, 34]
sum(*array_to_sum) #-> 58

[править] Подробнее о блоках

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

Ruby позволяет создавать анонимные методы и передавать их функциям - такие анонимные методы называются **блоками**. Очень большое количество функций Ruby основано на использовании блоков - например, итераторы (такие как each и map). Блок - это фактически "функция в функции" - программист определяет операцию, которую необходимо выполнить, но непосредственно ее выполнение осуществляет метод, которому блок передается.

[править] Зачем они нужны

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

  • Поддержание индекса в цикле
  • Забота об итераторах как отдельных обьектах
  • Закрытие ресурса после его использования
  • Забота о контексте, в котором выполняется операция

[править] Как создать блок

Блок передается методу через конструкцию do... end или фигурные скобки. Общепринятым является использовать фигурные скобки, если вызов блока умещается на одну строку программы. Для демонстрации работы блока, мы будем использовать метод .map. Этот метод принимает блок и выполняет его строго заданное число раз.

При передаче блока методу блок следует за скобками аргументов.

 puts (1..3).map(){ "Bay!" } # выводит Bay! три раза

Поскольку при отсутствии аргументов скобки необязательны, простейшая запись такова:

 puts (1..3).map{ "Bay!" } # выводит Bay! три раза

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

 puts (1..3).map{ word = 'Bay!'; word } #-> выводит Bay! три раза
 # поскольку блок знает про переменную word и она определена в нем
 puts word # вызывает сообщение об ошибке - вне блока об этой переменной ничего не известно

Как уже упоминалось, если блок многострочный целесообразней пользоваться формой с do ... end

 puts (1..3).map do
   random_number = rand()
   "Bay - случайный номер!\n"+
   random_number.to_s
 end

[править] Блоки принимают аргументы

Другое замечательное свойство блоков — они, как и функции, могут принимать аргументы. В таком случае метод, которому передан блок, сам "решает", что этот блок получит в качестве аргумента. Например, уже продемонстрированный метод .map еще и передает блоку аргумент, который можно захватить следующим образом:

 puts (1..3).map do | index |
   index
 end

В данном случае при каждом выполнении блока переменная index будет устанавливаться на положение итератора, начиная с 1! Аргументы метода указываются после открывающей фигурной скобки или после слова do через запятую, и разграничиваются двумя вертикальми чертами. Чтобы не перепутать черту со строчной латинской L, принято "отбивать" аргументы блока от вертикальной черты пробелами.

 method { | argument | .. }

[править] Свои методы с блоками

Ключевое слово yield в методе открывает раздвижные двери, впускающие аргумент[ы] в блок.

def pa3_u_gBa
 yield "u pa3"
 yield "u gBa"
end

pa3_u_gBa { | cJloBa | puts "!!! " + cJloBa }  #-> !!! u pa3
                                            #-> !!! u gBa

При этом строка будет передаваться блоку в переменную cJloBa при каждом выполнении.

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

def twice(&block)
 yield "u pa3"
 yield "u gBa"
end

twice #-> Ошибка LocalJumpError - отсутствует блок

Блок можно также передать другому методу, просто указав его как последний аргумент с амперсандом:

def writing_to(file, &block)
 File.open(file, 'w', &block)
end

[править] Некоторые применения блоков

Блоки — одна из главных особенностей Ruby. Уметь ими пользоваться — ключ к очень коротким и очень понятным программам, делающим очень много.

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

connected { download_email }

В данном случае мы пишем только блок с download_email, все заботы по открытию (а главное — закрытию) соединения возьмет на себя метод connected:

def connected
  connect_to_internet
  result = yield
  disconnect
  result
end

В данном случае мы сохраняем то, что вернул блок в метод, закрываем соединение и возвращаем результат блока как свой собственный. Чаще всего о методах, принимающих блоки, можно говорить как о деепричастном обороте — например "соединившись", "внутри_транзакции", "с файлом", "трижды".

Если воспользоваться встроенной проверкой исключений, то метод принимает такой вид:

def connected
  connect_to_internet
  begin
    result = yield
  ensure
    disconnect
  end
  result
end

Тогда даже если метод вызовет ошибку, соединение все равно будет закрыто.

[править] Методы, которых не было

Экспериментально замечено, что во время сессии у студентов в разы повышается способность к изобретениям различного рода. Иногда, удается направить эту энергию в мирное русло: некоторые студенты во время сдачи зачета начинают придумывать свои методы. Естественно, что "придуманные методы" они реализовать не могут, но с этим замечательно справляются их преподаватели. Некоторым методам даже дают имена студентов, которые приложили свое незнание к их созданию. Многие из таких методов включают в последующие версии языка.

[править] Ширяевский .size

Студент МЭТТ Ширяев Денис, на одном из зачетов предложил использовать метод .size в качестве итератора. Он использовал его для подсчета количества элементов массива, удовлетворяющих условию. По сути, он предложил укоротить связку .find_all{ ... }.size. Вот как будет выглядеть программа подсчета количества четных элементов массива:

maccuB = [1,2,3,4,5,6]
maccuB.size{ |i| (i%2).zero? }   #-> 3

Чтобы заставить работать данную программу, необходимо перед использованием итератора .size написать следующий код, который будет реализовывать эту функциональность:

class Array
   def size( &block )
       block ? inject( 0 ){ |count,elem| (yield elem ) ? count + 1 : count } : length
   end
end

Метод реализован только для массивов, но возможно его добавление к хешам или строкам.

[править] Случайное число из диапазона

Студенты часто возмущаются, почему чтобы получить случайное число от 3 до 6 нужно писать нечто невнятное вида:

3 + rand( 4 )

Откуда чего берется? Почему нельзя написать проще? Например вот так:

(3..6).rand

Действительно, почему? Давайте добавим такую функциональность к классу Range:

class Range
    def rand
        first + Kernel.rand( last - first  + ( exclude_end? ? 2 : 1 ) )
    end
end

Для проверки можно выполнить следующий код:

p Array.new(100){ (3..6).rand }.uniq.sort   #-> [3,4,5,6]

Что и требовалось реализовать! Кстати, данная реализация имеет один изъян: для строковых диапазонов, метод Range#rand будет выдавать ошибку. Решается проблема достаточно просто. Надо реализовать Array#rand (получение случайного элемента массива), а внутри Range#rand вызывать связку .to_a.rand. Теперь тоже самое, но на Ruby:

class Array
    def rand
        self[ Kernel.rand( size ) ]
    end
end

class Range
    def rand
        to_a.rand
    end
end

Для проверки выполним следующий код:

p Array.new(100){ ('a'..'c').rand }.uniq.sort  #-> ["a","b","c"]

Странно, но видимо все работает!

[править] Способы расширения библиотеки методов

[править] Как добавить метод к массиву/строке/венику?

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

class BeHuK
  def mecTu
  end
end

BeHuK.instance_methods #-> [..., "mecTu", ...]

class BeHuK
  def noMblTb_B_yHuta3e(yHuTa3)
  end
end

BeHuK.instance_methods #-> [..., "mecTu", ..., "noMblTb_B_yHuta3e", ...]
метод .instance_methods возвращает массив, который содержит имена методов, которые можно вызвать.

Добавленные методы становятся доступны немедленно, в том числе для уже созданнных экземпляров типа. Стоит помнить, что методы в Ruby — на самом деле "сообщения", и у каждого метода есть "приемник", то есть объект, которому сообщение отправлено. Метод по умолчанию ищет другие методы в экземпляре класса, поскольку приемником для него является self.

Простейший пример — добавление метода классу String, выводящий только согласные буквы из строки:

 str = "Crazy fox jumps over a lazy dog"
 class String
   def consonants
     cons = []
     self.scan /[BCDFGHJKLMNPRSTbcdfghjklmnprst]/ do | m |
       cons << m
     end
     cons.uniq.join
   end
 end

 str.consonants #-> "Crfjmpsldg"
операция расширения класса (добавление нового метода к существующему) по сути не отличается от создания нового класса.

У обьектов в Ruby есть методы класса и методы экземпляра — в нашем примере consonants — это именно методы экземпляра. При создании нового класса или изменении существующего создать метод класса можно, начав его имя с имени класса или с self и точки:

 str = "Crazy fox jumps over a lazy dog"
 class String
   def self.consonants_from(str)
     cons = []
     str.scan /[BCDFGHJKLMNPRSTVWZbcdfghjklmnprstvwz]/ do | m |
       cons << m
     end
     cons.uniq.join
   end
 end

 String.consonants_from(str) #-> "Crfjmpsvlzdg"

Одним из специфических свойств Ruby является то, что классы сами по себе — экземпляры класса Class, и с ними можно работать как с обычными объектами. Специальный синтаксис для доступа к методам класса в Ruby не нужен. Классы можно хранить в переменных, передавать методам и так далее.

В контексте класса self - это сам класс.

Проиллюстрируем это простым примером. Как мы знаем, у класса File есть метод open. Создадим метод у класса File, дающий нам доступ к временному файлу, создаваемому в момент выполнения кода. Это такой же метод, но открывающий только файлы из директории /tmp:

class File
  def self.temporary(&block)
    # определим директорию в которой в данный момент запущена программа
    # методы dirname и expand_path в данном случае - File.dirname и File.expand_path
    my_dir = self.dirname(self.expand_path(__FILE__))
    base = basename(__FILE__, '.rb') # -> имя файла с программой без расширения .rb
    stamp = "#{base}_#{Time.now.to_i}.tmp" # -> системное время в секундах и расширение tmp

    # File.join соединит фрагменты пути обратным слешем в Windows и прямым слешем на UNIX
    path = join(my_dir, stamp)
    self.open(path, 'w', &block)
  end
end

File.temporary { |f| f << "Some info" } # #<File:/Tests/(irb)_1151198720.tmp (closed)>
☞
Для управления временными файлами в Руби существует класс Tempfile - помимо других достоинств он гарантирует, что созданные временные файлы по завершении программы будут удалены. Так что даже в этом случае велосипед изобретать не стоит!

Если к классу надо добавить много методов сразу, то при описании класса можно выйти на уровень его обьекта-класса. Это свойство в Ruby называется eigenclass (нем. eigen = "свой, особый"). Подозревая, что многие из читателей незнакомы с математическим понятием собственного значения/вектора/пространства, мы кратко и по-программистски назовём eigenclass эйгенклассом.

Добавим к классу File метод myself, который дает быстрый доступ к текущему файлу с исходным кодом:

class Manager
  class << self
    def create
     ...
    end

    def manage
     ...
    end
  end
end

Если нужно добавить метод только к конкретному экземпляру, нужно выйти на его эйгенкласс:

str = "Crazy fox jumps over a lazy dog"
other_string = "Three black witches"

def str.vowels
    vowels = []
    scan /[AEIOUYaeiuoy]/ do | m |
      vowels << m
    end
    vowels.uniq.join
  end
end

str.vowels #-> "ayoue"
other_string.vowels #-> NoMethodError: у этой строки нету метода `vowels'...

Возможность добавлять и изменять устройство уже существующих классов — одно из основных свойств Ruby, обеспечивающих великую гибкость языка. Часто бывает, что метод возвращает не тот результат, который нам нужен — тогда при его изменении все программы, обращающиеся к данному методу будут получать измененный результат.

[править] Программист-разрушитель

Как ни странно, изредка программисту приходится взять на себя позицию разрушителя — удалить существующий метод или константу. Метод undef позволяет сделать это:

class BeHuK
  def mecTu
    "МетёмЪ!"
  end
end

class BeHuk_6epe3oBblu < BeHuK
  def xJlecTaTb(cnuHa)
  end
  def noMo4uTb_B_Ta3uke(ta3uk)
  end
  undef mecTu
end

venik = BeHuK.new
beriozovy_venik = BeHuk_6epe3oBblu.new

venik.mecTu #-> "МетёмЪ!"
beriozovy_venik.mecTu #-> Ошибка NoMethodError - такого метода нет, хоть он и был унаследован

Уничтожение класса несколько сложнее, но тоже возможно

Object.send(:remove_const, :BeHuK)

После этого BeHuK будет сущствовать только для объекта-экземпляра

class BeHuK
end

meTeJIka = BeHuK.new

Object.send(:remove_const, :BeHuK)

BeHuK # => Ошибка NameError: неизвестная константа BeHuK

meTeJIka.class #=> BeHuK, все еще существует для экземпляра

Это свойство Ruby крайне полезно, если нужно создать класс, наследующий от другого, но при этом имеющий другого родителя. Например:

# в чужой программе...
class Connection < Socket
  # ... много-много методов connection...#
end

conn = Connection.new()

# в нашей программе...
Object.send(:remove_const, :Сonnection)

class Connection < EncryptedSocket
  # ... такие-же методы как у connection, но работающие с шифрованным соединением...#
end

# в итоге чужая программа будет использовать созданный нами Connection!
Полная замена чужих классов довольно опасна, но бывают ситуации, когда эта методика спасает.

История из жизни

При разработке своего Rails-приложения мной применялся класс OrderedHash, который работает как стандартный хеш, но при этом имеет упорядоченные ключи. Это позволяет, к примеру, удобно сгруппировать новости по датам, сохраняя порядок дат. В какой-то момент моя программа перестала работать. Почему? В Rails был, для внутренних нужд, добавлен другой класс OrderedHash, но при этом он не соответствовал моему (и даже не соответствовал обычному Hash — некоторых методов в нем просто не было! Благодаря remove_const мне удалось просто выгрузить их класс и заменить его своим. А тесты в комплекте чужой библиотеки позволили удостовериться, что я ничего не испортил и она с моим "внедренным" классом функционирует нормально.

Julik 01:52, 25 июня 2006

[править] Как написать свой итератор?

[править] Как написать свой класс?

[править] Наследовать или перемешать?

[править] Как сделать свою библиотеку методов?

[править] Матрицы и вектора

Давно хотел написать историю про то, как использовать матрицы и вектора в Ruby, но руки дошли только сейчас. Мое знакомство с реализацией матричного исчисления в Ruby началось еще в мою бытность студентом МГИУ. И надо сказать, что это знакомство было неудачным. Дело в том, что простейшая программа вычисления определителя матрицы

require 'matrix'
p Matrix[[1,-2,3],[3,4,-5],[2,4,1]].det   #-> -50

давала неверный результат (правильный ответ = 62). Как выяснилось позднее, эта проблема связана со спецификой целочисленной арифметики в Ruby («одна вторая в Руби - нуль»). Предположив это, я решил, что проблема легко решится, если я преобразую элементы матрицы к дробному типу (классу Float):

require 'matrix'
p Matrix[[1.0,-2.0,3.0],[3.0,4.0,-5.0],[2.0,4.0,1.0]].det   #-> 62.0

И в некоторых случаях это меня действительно выручало. Но не всегда... стоило появиться делению на 3, как появлялись ненужные остатки и погрешности. И чем больше исходная матрица, тем больше вероятность появления таких остатков. В некоторых случаях это было несущественным, а в остальных приходилось прибегать к услугам специальных математических пакетов (например,Maxima). Было жутко неудобно (я тогда писал курсовой, который решал все варианты контрольной работы по предмету «Математическое программирование». Да простит меня преподаватель, который так и не понял секрета тотальной успеваемости группы) и обидно за такую "кривую реализацию".

На этом бы история и закончилась (как позже я узнал, на этом она заканчивалась для многих), но мне в руки попалась книга Programming Ruby 2ed с описанием возможностей стандартной библиотеки версии 1.8.2. Именно там (на стр. 671) я наткнулся на описание библиотеки mathn. Уникальность ее состоит в том, что она существенно расширяет возможности стандартных чисел, добавляя к ним рациональные числа (класс Rational) и комплексные числа (класс Complex).

☞
Проще говоря, появляется возможность делить числа без погрешностей (класс Rational) и возможность извлекать квадратный корень из отрицательного числа (класс Complex)

Одновременно с этим она добавляет матричное и векторное исчисления (правда, почему-то в книге дополнительно подключали complex и matrix). И после этого матричное счисление начинает работать "из коробки" и еще как работать!!! Хотите обратую матрицу? Пожалуйста! Хотите определитель? Нет ничего проще! Обидно только одно... программу к тому времени я уже написал и эти возможности мне были не нужны.

Чуть позднее, один из моих студентов написал мне письмо с просьбой объяснить как «работать с матрицами в Руби»? При этом он задал всего три вопроса:

  1. Как создать новую матрицу?
  2. Как послать в матрицу 2-мерный массив?
  3. Как поменять элемент матрицы?

Для того, чтобы начать наше знакомство с матрицами, я отвечу сперва на них.

[править] Как создать новую матрицу?

Самый простейший способ создать матрицу — использовать метод «батарейка» (метод [] выглядит как индикатор заряда батареи на сотовом телефоне):

require 'mathn'
Matrix[[1, -2, 3], [3, 4, -5], [2, 4, 1]]

Этот способ создания мы уже с вами видели раньше. Обратите внимание, что для использования матриц необходимо подключать библиотеку mathn.

[править] Как послать в матрицу 2-мерный массив?

Немножко перефразируем вопрос: «как преобразовать двумерный массив в матрицу»? Пусть ответ будет неожиданным, но это делается при помощи того-же метода «батарейка»:

require 'mathn'
maccuB = [[1, -2, 3], [3, 4, -5], [2, 4, 1]]
Matrix[ *maccuB ]

Оператор * преобразует maccuB в список параметров, что позволяет существенно упросить процесс создания матрицы. Более того, это единственно удобный метод ее создания. Все остальные методы создания матрицы (например, метод .new) работают не столь изящно и интуитивно.

[править] Как изменить элемент матрицы?

И вот тут всплывает «недоделанность» матриц. Метода для изменения элемента матрицы в них нет! Для того, чтобы изменить элемент матрицы, надо преобразовать матрицу в массив, изменить элемент массива и преобразовать массив в матрицу. Примерно вот так (меняем элемент с индексом 0,0):

require 'mathn'
maTpuLLa =  Matrix[[1,-2,3],[3,4,-5],[2,4,1]]
maccuB = maTpuLLa.to_a
maccuB[0][0] = 0
p Matrix[ *maccuB ] #-> Matrix[[0, -2, 3], [3, 4, -5], [2, 4, 1]]

[править] Исправляем оплошность разработчиков

Для начала, рассматриваем поближе библиотеку matrix (исходник или описание в fxri) и выясняем, что для получения значения элемента используется метод «батарейка» с двумя аргументами. Вполне закономерно использовать метод «батарейка равно» (то есть []=) для изменения элемента. Сейчас мы его и реализуем:

require 'mathn'
class Matrix
  def []=( i, j, value )
    @rows[i][j] = value
  end
end

maTpuLLa =  Matrix[[1,-2,3],[3,4,-5],[2,4,1]]
maTpuLLa[0,0] = 5
p maTpuLLa  #-> Matrix[[5, -2, 3], [3, 4, -5], [2, 4, 1]]

Ну вот как-то примерно так... Почему не могли этого сделать разработчики, я так и не понял. Скорее всего по идеологическим соображениям («не дело, чтобы матрицы вели себя как простые массивы»).

[править] Методы работы с матрицами

Из методов работы с матрицами наиболее важные это: вычисление определителя (.det), вычисление обратной матрицы (.inv), поиск минора матрицы (.minor), преобразование матрицы в массив (.to_a), умножение (*), сложение (+), вычитание (-) и деление (/) матриц. Без остальных методов можно обойтись. Поэтому изучите их самостоятельно.

[править] Зачем нужны вектора?

Во-первых, для Ruby вектор — это объект класса Vector. Подключается он одновременно с матрицами (класс Matrix). Во-вторых, очень похож на массив, но с одним существенным отличием (cобственно это отличие и определяет полезность вектора): массивы и вектора по разному складываются и вычитаются. Давайте рассмотрим небольшой пример:

require 'mathn'
maccuB = [1,2,3,4,5]
BekTop = Vector[ *maccuB ]
p BekTop + BekTop   #-> Vector[2, 4, 6, 8, 10]
p maccuB + maccuB   #-> [1, 2, 3, 4, 5, 1, 2, 3, 4, 5]
☞
Обратите внимание, что BekTop создается точно также, как и матрица. По сути, вектор — это матрица, которая состоит лишь из одной строки. А матрица, в свою очередь — это массив векторов

Итак, в чем соль? Дело в том, что для сложения векторов необходимо сложить их соответствующие координаты. В случае же с массивами происходит конкатенация (сцепление) массивов. Вот именно в этой роли вектора не имеют себе равных. В остальных случаях, замечательно работает массив.

[править] Хитрости

[править] Задачи

[править] Решение систем линейных уравнений методом Гаусса

Как я уже рассказывал раньше, первая моя программа на Ruby (декабрь 2002 года) -- реализация симплексного алгоритма для решения задач оптимизации. Весь фокус в том, что я эту программу нашел! Это 11 Кбайт и 363 строчки (140 + 223) программного кода. Кстати, если найду подходящее для нее место, то обязательно надо будет опубликовать.

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

 require 'mathn'
 ypaBHeHue = [Vector[1,2,1,1],Vector[1,5,6,2],Vector[1,5,7,10]]

Почему был выбран именно массив векторов, а не матрица или двумерный массив? Дело в том, что в методе Гаусса приходится выполнять такие векторные операции, как: вычитание векторов и деление вектора на скаляр. Поэтому смысла создавать матрицу (векторных операций не предусмотрено) или двумерный массив (придется реализовывать эти операции) -- нет! Кстати, вполне возможно создать массив векторов и из двумерного массива (что мы и сделаем в следующем примере). Итак, приступим к реализации прямого хода метода Гаусса:

 require 'mathn'
 ypaBHeHue = [[1,2,1,1],[1,5,6,2],[1,5,7,10]].map{ |array| Vector[ *array ] }
 # Прямой ход метода Гаусса
 ypaBHeHue[0] /= ypaBHeHue[0][0]

И вот тут нас ждет неприятный сюрприз: у векторов не реализован метод /. Смотрим в файл matrix.rb (который мирно лежит в директории со стандартными библиотеками). Действительно нет! Метод * (умножить) есть, а разделить -- нет. Ну и ладно, мы -- программисты не гордые... сами напишем!

 require 'mathn'

 class Vector
   def /( arg )
     self * (1/arg)
   end
 end

 ypaBHeHue = [[1,2,1,1],[1,5,6,2],[1,5,7,10]].map{ |array| Vector[ *array ] }
 # Прямой ход метода Гаусса
 ypaBHeHue[0] /= ypaBHeHue[0][0]

Во! Теперь работает (только со скалярами, но нам больше и не надо). Заканчиваем реализовывать прямой проход метода Гаусса:

 require 'mathn'

 class Vector
   def /( arg )
     self * (1/arg)
   end
 end

 ypaBHeHue = [[1,2,1,1],[1,5,6,2],[1,5,7,10]].map{ |array| Vector[ *array ] }
 # Прямой ход метода Гаусса
 ypaBHeHue[0] /= ypaBHeHue[0][0]
 ypaBHeHue[1] -= ypaBHeHue[0]*ypaBHeHue[1][0]
 ypaBHeHue[2] -= ypaBHeHue[0]*ypaBHeHue[2][0]

 ypaBHeHue[1] /= ypaBHeHue[1][1]
 ypaBHeHue[2] -= ypaBHeHue[1]*ypaBHeHue[2][1]

 ypaBHeHue[2] /= ypaBHeHue[2][2]

 p ypaBHeHue  #-> [Vector[1,2,1,1], Vector[0,1,5/3,1/3], Vector[0,0,1,8]]

Судя по всему, программа работает. Кстати, обратите внимание, что результирующие вектора содержат рациональные дроби). Теперь добавим обратный ход метода Гаусса:

 require 'mathn'

 class Vector
   def /( arg )
     self * (1/arg)
   end
 end

 ypaBHeHue = [[1,2,1,1],[1,5,6,2],[1,5,7,10]].map{ |array| Vector[ *array ] }
 # Прямой ход метода Гаусса
 ypaBHeHue[0] /= ypaBHeHue[0][0]
 ypaBHeHue[1] -= ypaBHeHue[0]*ypaBHeHue[1][0]
 ypaBHeHue[2] -= ypaBHeHue[0]*ypaBHeHue[2][0]

 ypaBHeHue[1] /= ypaBHeHue[1][1]
 ypaBHeHue[2] -= ypaBHeHue[1]*ypaBHeHue[2][1]

 ypaBHeHue[2] /= ypaBHeHue[2][2]

 # Обратный ход метода Гаусса
 ypaBHeHue[1] -= ypaBHeHue[2]*ypaBHeHue[1][2]
 ypaBHeHue[0] -= ypaBHeHue[2]*ypaBHeHue[0][2]

 ypaBHeHue[0] -= ypaBHeHue[1]*ypaBHeHue[0][1]

 p ypaBHeHue  #-> [Vector[1,0,0,19], Vector[0,1,0,-13], Vector[0,0,1,8]]

Ну вот и все... вроде как решение получили. Но было бы замечательно, если бы выводилось не все уравнение, а только столбец свободных членов. Задачка простенькая, но важная. Давайте ее решим:

 p ypaBHeHue.map{ |vector| vector.to_a }.transpose[-1]  #-> [19,-13,8]

Теперь задачу можно считать решенной! Жаль только, что программа работает только для уравнения 3х4. Надо бы добавить несколько итераторов, чтобы они самостоятельно определяли размерность уравнения. Для этого нужно проследить чередование индексов и записать для них итераторы (хоть я и недолюбливаю .each, но для данного случая он пришелся как нельзя кстати). Расписывать преобразование я не буду, так как не вижу в этом большой надобности. Поэтому, смотрите сразу то, что у меня получилось:

 require 'mathn'

 class Vector
   def /( arg )
     self * (1/arg)
   end
 end

 ypaBHeHue = Matrix[[1,2,1,1],[1,5,6,2],[1,5,7,10]].row_vectors

 # Прямой ход метода Гаусса
 (0...ypaBHeHue.size).each{ |i|
   ypaBHeHue[i] /= ypaBHeHue[i][i]
   (i+1...ypaBHeHue.size).each{ |j| ypaBHeHue[j] -= ypaBHeHue[i]*ypaBHeHue[j][i] }
 }

 # Обратный ход метода Гаусса
 (1...ypaBHeHue.size).to_a.reverse.each{ |i|
   (0...i).each{ |j| ypaBHeHue[j] -= ypaBHeHue[i]*ypaBHeHue[j][i] }
 }

 p ypaBHeHue.map{ |vector| vector[-1] }  #-> [19,-13,8]

Обратите внимание, что ypaBHeHue задается через матрицу (которая, не без помощи метода .row_vectors, преобразуется в массив векторов). Также обратите внимание, что получить последний столбец можно посредством итератора .map и метода "батарейка".

Кстати, реализация симплексного алгоритма подразумевает еще несколько интересных задач:

  1. Если какой либо элемент главной диагонали нулевой, то необходимо переставить строки и/или столбцы, чтобы это исправить.
  2. Преобразование целевой функции и системы уравнений из текстового формата (2*x1-x2-x4 → min и x1-2*x2-2*x3-x4 <= 10) в YAML
  3. Первичная обработка целевой функции (если она стремится к min, то необходимо умножить вектор целевой функции на -1) и уравнений системы (если равенство не строгое, то преобразовать его в строгое, путем добавления новой неизвестной: x1-2*x2-2*x3-x4 <= 10 → x1-2*x2-2*x3-x4+x5 = 10).
  4. Поиск ведущего элемента.
  5. Смена ведущего элемента.
  6. Вывод всего уравнения в читабельном виде. Лучше всего в виде таблицы.

Буду рад, если кто-то опубликует их решение.

[править] Работа с файлами

Файлы в программах играют роль хранилищ, в которые можно записать любые объекты. В отличие отпривычных нам объектов, файлы позволяют хранить данные даже тогда, когда программа завершила свою работу. Именно поэтому они могут использоваться для передачи данных между разными программами или разными запусками одной и той же программы.

☞
Среди профессиональных программистов принято сохранять настройки программы в файлы, чтобы их мог исправить пользователь или конфигурационная программа
☞
В своих операционных системах фирма Microsoft ввела понятие "двоичный файл", но оно порождает больше проблем, чем удобств. Особенно при создании кроссплатформенных приложений

Как организована работа с файлами? В самом общем случае работа с файлами состоит из следующих этапов:

  1. Открытие файла. Сущность этого этапа состоит в создании объекта класса File.
  2. Запись или чтение. Вызываются привычные нам методы вывода на экран и не совсем привычные — ввода.
  3. Закрытие файла. Во время закрытия файла происходят действия с файловой системой. С объектом, который создается при открытии файла ничего не происходит, но после этого он указывает на закрытый файл и производить операции чтения/записи при помощи него уже нельзя.

Существует масса способов реализации работы с файлами:

  1. Чтение при помощи класса IO. Класс IO имеет множество методов, которые позволяют производить чтение из текстовых файлов (с "двоичными файлами" лучше так не работать). Если нужно считать весь текстовый файл, то лучше пользоваться методами класса IO.
  2. Перенаправление потоков. Существует три предопределенных переменных: $stdio, $stdin и $stderr. Если им присвоить объект класса File (создаваемый во время открытия файла), то весь вывод пойдет в файл, который присвоили переменной $stdout. Весь ввод будет браться из файла, который присволи переменной $stdin, а все ошибки будут сохраняться в файле, который присвоили переменной $stderr. Если нужно работать только с одним файлом на чтение и одним файлом на запись, то обычно используют этот способ. Также очень удобно использовать перенаправление потока ошибок (переменная $stderr) для программ, которые работаю в составе пакетных файлов и используют только интерфейс командной строки.
  3. Универсальный способ. Используется в ситуациях, когда нельзя использовать предыдущие два способа.

Подведем небольшой итог:

  1. Если нужно считать весь файл целиком, то надо испольвать методы класса IO.
  2. Если нужно работать только с одним файлом на чтение и только одиним файлом на запись, то надо использовать перенаправление потока.
  3. Если нельзя применить два вышеперечисленных способа, то надо использовать универсальный способ работы с файлами.

[править] Чтение при помощи класса IO

Для чтения файла целиком используется метод .read. Он считывает весь файл в виде строки. Во время его использования не стоит задумывать об открытии/закрытии файла, так как эти операции скрыты внутри метода.

config = IO.read('config.yaml')
config.class   #-> String
Имя файла — это строка. Не стоит усложнять себе жизнь и думать, что имя файла имеет некий "божественный" тип данных. Будьте проще и люди к вам потянутся

В примере можно увидеть, как считывается файл config.yaml в переменную config. Вторая строчка примера показывает, что, при использовании метода .read, файл считывается в виде строки (класс String). Теперь к переменной config можно применять любые методы из богатого строкового арсенала.

В качестве упражнения предлагаю вам считывать и выводить на экран любые файлы, которые вы только найдете у себя на диске.

При считывании "двоичных файлов" в операционных системах фирмы Microsoft использовать данный способ нельзя, так как файл будет считан не до конца. Следует использовать универсальный способ работы с файлами

[править] Перенаправление потока

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

$stdin = File.open('входные данные.txt')

После этого, он сильно бьет себя по лбу и выкрикивает нечто вроде «Я идиот!» И все от осознания того, что процесс откладки давно можно было упростить таким способом.

А вот другая история. Программист пишет и отлаживает программу, которая все необходимые данных выводит на экран. Но в конечном итоге программа должна запускаться без участия человека и ее вывод нужно сохранять в файл (для дальнейшей обработки). Переписывать всю программу лень и поэтому в начало своей программы он вставляет парочку волшебных строчек:

$stdout = File.open('выходные данные.txt','w')
$stderr = File.open('сообщения об ошибках.txt','a')
☞
Вторым параметром метода .open передается модификатор доступа, то есть кодовое слово по которому метод .open может предположить то, что вы будете делать с этим файлом. В нашем примере мы использовали модификатор w (от англ. write - писать), который говорит о том, что мы будем только писать в файл. Причем каждый раз файл будет перезаписываться. При помощи модификатора a (от англ. append - добавлять) мы указали, что мы будем добавлять данные в файл, а не перезаписывать, как в случае с w

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

$stdout = File.open('выходные данные.txt','w')
$stderr = File.open('сообщения об ошибках.txt','a')

puts 'Очень важные данные'
puts 'которые будут сохранены в файл'
raise 'Принудительно вызываем ошибку'
☞
Метод raise — для принудительного вызова ошибки.

Теперь можете смело экспериментировать! Для начала, попробуйте поменять модификаторы w и a местами.

[править] Универсальный способ работы с файлами

Универсальным способом я назвал способ с использованием метода File.open. Дело в том, что при помощи него можно осуществлять не только считывание, запись и перезапись, но и их закрытие файлов (чего нельзя сделать при использовании способа с переменными $stdout, $stdin и $stderr). Это позволяет несколько раз (за время выполнения программы) осуществлять операции открытия-закрытия файла. В виду того, что эта возможность нужна далеко не всегда, то и используется этот способ только тогда, когда использование всех предыдущих невозможно. Чтение из файла 'входные данные.txt' при помощи универсального метода будет выглядеть следующим образом:

cTpoka = File.open('входные данные.txt','r'){ |file| file.read }

Модификатор доступа ('r') указывать необязательно, так как он устанавливается по умолчанию. Поэтому следующий код тоже верен:

cTpoka = File.open('входные данные.txt'){ |file| file.read }

Если необходимо записать данные, то нужно использовать модификатор доступа 'a' (добавление к концу файла) или 'w' (добавление в файл с его предварительной очисткой). Само собой разумеется, что запись данных в файл осуществляется методами puts, write и т.д.

File.open('выходные данные.txt','w'){ |file| file.write cTpoka }
File.open('выходные данные.txt','a'){ |file| file.puts cTpoka }

Блок метода .open (т.е. фигурные скобки) нужен для того, чтобы при выходе их блока автоматически осуществлять закрытие файла.

[править] Хитрости

[править] Задачи

[править] Сети

[править] Как написать троян?

Однажды, один из студентов (Geka) попросил меня рассказать о том, как создать простейшее клиент-серверное приложение на Ruby. Перед тем, как подойти ко мне он уже облазил несколько форумов, но у него все равно осталось много вопросов. Отчаявшись он наконец подошел ко мне...

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

[править] Построение серверной части

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

require 'socket'
server = TCPServer.new('localhost', 3000)
while (srv = server.accept)
  str = srv.gets.chomp.split(' ')
  cmd = str[0]
  arg = str[1]
  case cmd
    when ".."
        Dir.chdir("..")
        srv.print "OK!"
    when "ls"
        srv.print Dir.entries(".").join("\n")
    when "cd"
        begin
            Dir.chdir( arg )
            srv.print "OK!"
        rescue
            srv.print "No such file or directory - #{ arg }"
        end
    when "md"
        Dir.mkdir( arg )
        srv.print "OK!"
    when "rmd"
        begin
            Dir.rmdir( arg )
            srv.print "OK!"
        rescue
            srv.print "No such file or directory - #{ arg }"
        end
    when "shutdown"
        break
    else
        srv.print "Bad Command!"
  end
  srv.close
end

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

require 'socket'
TCPServer.open('localhost', 3000){ |server|
    if (session = server.accept)
        session.print "Do6po no}i{aJIoBaTb Ha cepBep\r\n"
        session.print "BbI mo}i{eTe Ha6paTb komaHgbI: ls <dir> | cd <dir> | shutdown\r\n"
        loop{
            cmd, arg = *session.gets.chomp.split
            case cmd
                when "ls"
                    begin
                        session.print Dir[ arg || "*" ].map{ |str| str + "\r\n" }
                    rescue
                        session.print "HeT Takoro qpau'JIa uJIu gupekTopuu - #{ arg.inspect }\r\n"
                    end
                when "cd"
                    begin
                        Dir.chdir( arg )
                        session.print "OK!\r\n"
                    rescue
                        session.print "HeT Takoro qpau'JIa uJIu gupekTopuu - #{ arg.inspect }\r\n"
                    end
                when "shutdown"
                    session.close
                    break
                else
                    session.print "HeBepHa9 komaHga!\r\n"
            end
        }
    end
}
☞
Для того, чтобы соединиться с этим сервером, необходимо выполнить команду telnet и набрать o localhost 3000. После успешного соединения можете набирать команды ls, cd или shutdown

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

После небольших манипуляций с кодом стал виден базовый каркас сервера. Он состоит из следующих блоков:

  • require 'socket'

Для того, чтобы работать с классом TCPServer (и не только с ним) необходимо подключить библиотеку socket.

  • TCPServer.open('localhost',3000){ |server| ... }

Этот программный код создает сервер, который будет прослушивать порт 3000. В качестве порта может использоваться любой другой (например, 31337). Менять имя хоста (localhost) не нужно, если только у Вас не несколько сетевых интерфейсов. Если у Вас их все таки несколько, то ничего по поводу смены хоста Вам объяснять не надо. Вы и так все, скорее всего, знаете ...

  • if (session = server.accept) ... end

При помощи такой нехитрой комбинации ловится соединение с сервером. Обратите внимание, что в примере студента использовалась конструкция while. Ее пришлось упразнить, т.к. большой необходимости в ней не было. Как только вызов server.accept возвращает значение, то это означает, что к серверу подсоединился клиент. В переменную session записывается указатель на соединение. С ним-то мы и будем работать дальше...

  • cmd, arg = *session.gets.chomp.split

Данный код интересен тем, что в программе студента для его реализации задействовано аж три строчки. И все лишь потому, что он не знал, что такое оператор * (звездочка). Этот оператор преобразует массив в список параметров. Тем самым он освобождает нас от нуждого присваивания. Сам же код получает от клиента строку, которая интерпретируется им как "команда и аргумент, разделенные пробелом". Обратите внимание, что работа идет с переменной session, а не server.

  • case cmd ... end

Ветвление case необходимо для обработки команд. В зависимости от значения переменной cmd будут выполнены те или иные действия.

  • Остальной код описывать не буду, так как каркасом клиент-серверного приложения он не является, а реализует функциональность конкретного приложения.
☞
Для того, чтобы прекратить работу сервера необходимо нажать комбинацию клавишь Ctrl+C или Ctrl+Break. Команду отключения данный сервер не поддерживает. Команда shutdown относится к клиенту

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

   require 'gserver'
   class Tpo9H < GServer
       def serve( session )
           session.print "Do6po no}i{aJIoBaTb Ha cepBep\r\n"
           session.print "BbI mo}i{eTe Ha6paTb komaHgbI: ls <dir> | cd <dir> | shutdown\r\n"
           loop{
               cmd, arg = *session.gets.chomp.split
               case cmd
                   when "ls"
                       begin
                           session.print Dir[ arg || "*" ].map{ |str| str + "\r\n" }
                       rescue
                           session.print "HeT Takoro qpau'JIa uJIu gupekTopuu - #{ arg.inspect }\r\n"
                       end
                   when "cd"
                       begin
                           Dir.chdir( arg )
                           session.print "OK!\r\n"
                       rescue
                           session.print "HeT Takoro qpau'JIa uJIu gupekTopuu - #{ arg.inspect }\r\n"
                       end
                   when "shutdown"
                       session.close
                       break
                   else
                       session.print "HeBepHa9 komaHga!\r\n"
               end
           }
       end
   end
   tpo9H = Tpo9H.new( 31337 )
   tpo9H.audit = true
   tpo9H.start
   tpo9H.join

Программа претерпела множество изменений, но зато теперь поддерживаются несколько одновременно работающих клиентов. Давайте рассмотрим подробнее изменения, которые пришлось внести:

  • require 'gserver'

Вместо библиотеки socket мы подключаем библиотеку gserver. Для нас это означает то, что работать с классом TCPServer напрямую мы не будем. Использовать мы будем класс GServer.

  • class Tpo9H < GServer

Этот код создает класс Tpo9H, который наследует функциональность от класса GServer. Можно было бы конечно просто расширить класс GServer, но так хотелось "хакерское" название класса, что я не смог удержаться и произвел наследование.

  • def serve( session )

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

  • tpo9H = Tpo9H.new( 31337 )

Создается экземпляр класса Tpo9H. В качестве порта для "прослушки" указан 31337. Почему не 3000? Потому, что захотелось! При желании можете его сменить на любой другой.

  • tpo9H.audit = true

Если пропустить эту строчку, то сервер будет работать молча. Настолько молча, что Вы не сможете понять работает он или нет? Надпись об удачном запуске сервера будет выглядеть примерно так:

[Mon Oct 23 23:33:12 2006] Tpo9H 127.0.0.1:3000 start

Если она не появилась, то появится какая-то другая, которая известит Вас об синтаксической ошибке, которую Вы допустили при переписывании кода.

  • tpo9H.start

Этой командой мы стартуем сервер.

  • tpo9H.join

Класс GServer (и его наследники) прослушивает порт в фоновом режиме. Но если программа завершается, то завершатся и все нити (Thread's). Поэтому надо чем-то занять программу на время работы сервера. Вот и было решено ожидать завершения прослушивающей нити сервера. Эта команда останавливает выполнение программы (за исключением нитей в фоне) до окончания прослушивания экземпляром созданного класса.

[править] Построение клиентской части

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

Хотя Geka и имел в своем распоряжении клиентскую программу, ее мы рассматривать не будем, т.к. с тех пор его сервер претерпел некоторые изменения и прежняя клиентская часть не может работать с нынешней серверной. Мы напишем новую клиенскую часть "с нуля".

Для соединения с сервером по протоколу TCP/IP используется класс TCPSocket из библиотеки socket. Для того, чтобы наша задача была конкретней, мы заставим сервер (при помощи нашего клиента) выполнить следующие команды: ls, cd .., ls и shutdown. Вывод результата выполнения этих команд мы будем осуществлять на экран, но пользователь не получит возможности изменять эту последовательность действий (кроме как исправив программу). Он увидит только результат. Логика здесь следующая: зачем заставлять пользователя вводить команды, если это может делать программа? Если же пользователю нужна другая последовательность команд, то пусть использует telnet или правит клиентскую часть под свои нужды.

Итак, давайте наберем клиентский код, который будет соединяться с нашей серверной частью. Скорее всего он будет выглядеть вот так:

   require 'socket'
   TCPSocket.open('localhost',31337){ |client|
       2.times{ puts client.gets.chomp }
       client.puts "ls"
       puts client.read
       client.puts "cd .."
       puts client.gets
       client.puts "ls"
       puts client.read
       client.puts "shutdown"
   }

Все замечательно, но программа не работает. Не вся, конечно... Она выводит приглашение к диалогу и все, дальше виснет. Это связано с тем, что используется метод .read, который считывает весь поток целиком, пока не встретит символ EOF. Его, то наш сервер как раз и не передает. Не будем пока спешить и править сервер, а применим один нечестный прием: будем использовать не метод .read, а метод .sysread( n ). Почему прием нечестный? Да все потому, что метод .sysread( n ) считывает n-первых сиволов из потока. Так как мы не знаем сколько нам надо считать символов, то мы зададим в качестве n очень большое число. Например, 5000. Если символов в потоке меньше, чем 5000, то .sysread( n ) считает столько, сколько есть. Эту особенность мы и используем. Разве можно сказать, что такой прием честный?

   require 'socket'
   TCPSocket.open('localhost',31337){ |client|
       2.times{ puts client.gets.chomp }
       client.puts "ls"
       puts client.sysread(5000)
       client.puts "cd .."
       puts client.gets
       client.puts "ls"
       puts client.sysread(5000)
       client.puts "shutdown"
   }

Уже лучше... по крайней мере программа работает. Но давайте поразмышляем над ситуацией, которая произошла с методом .read. Если немного подправить сервер и выдавать после каждой передачи этот символ, то программа с .read могла бы с успехом работать. Какова здесь мораль? А мораль в том, что для успешной работы необходимо с сервера передавать сигнал, который означал бы "последняя строка, которую я передаю клиенту". Чтобы клиент не пытался читать данные с сервера, а начинал их передачу. Вполне естественно, что добавление такого сигнала означает модификацию сервера. В качестве сигнала последней строки мы будем использовать строку +ОК. Почему именно такую? Просто видел ее где-то, вот и решил использовать. Если хотите, то можете использовать свою строку. Только не забудьте об этом, когда будете писать программу-клиент. Вот модифицированный сервер:

   require 'gserver'
   class Tpo9H < GServer
       def serve( session )
           session.puts "Do6po no}i{aJIoBaTb Ha cepBep\r\n"
           session.puts "BbI mo}i{eTe Ha6paTb komaHgbI: ls <dir> | cd <dir> | shutdown\r\n"
           loop{
               cmd, arg = *session.gets.chomp.split
               case cmd
                   when "ls"
                       begin
                           session.puts Dir[ arg || "*" ].map{ |str| str + "\r\n" }

                       rescue
                           session.puts "HeT Takoro qpau'JIa uJIu gupekTopuu - #{ arg.inspect }\r\n"
                       end
                   when "cd"
                       begin
                           Dir.chdir( arg )
                           session.puts "OK!\r\n"
                       rescue
                           session.puts "HeT Takoro qpau'JIa uJIu gupekTopuu - #{ arg.inspect }\r\n"
                       end
                   when "shutdown"
                       session.close
                       break
                   else
                       session.puts "HeBepHa9 komaHga!\r\n"
               end
               session.puts "+OK"
           }
       end
   end
   tpo9H = Tpo9H.new( 31337 )
   tpo9H.audit = true
   tpo9H.start
   tpo9H.join

Была добавлена лишь одна команда (хотя, самые внимательные могут заметить, что еще и метод .print был заменен на .puts) - session.puts "+OK". Она будет выполнятся после каждой передачи данных от сервера к клиенту. Тем самым мы будем извещать клиента о том, что передача завершается. Теперь займемся переписыванием клиента. Необходимо исправить код там, где происходит чтение, чтобы он учитывал строки +OK.

   require 'socket'
   TCPSocket.open('localhost',31337){ |client|
       2.times{ puts client.gets.chomp }
       client.puts "ls"
       loop{
           str = client.gets.chomp
           if str[/^\+OK/]
               break
           else
               puts str
           end
       }
       client.puts "cd .."
       loop{
           str = client.gets.chomp
           str[/^\+OK/] ? break : puts( str )
       }
       client.puts "ls"
       while !(str = client.gets.chomp)[/^\+OK/]
           puts str
       end
       client.puts "shutdown"
   }

Код стал выглядеть ужасней, зато стал более честным. Стоит отметить, что предложено три варианта обработки строки +OK. Правда, отличаются они лишь в деталях и функционально делают одно и то же. Давайте рассмотрим подробней решения, которые были реализованы в ходе исправления:

  • loop{ ... }

Используя бесконечный цикл, мы получаем возможнось самостоятельно определять точку выхода из цикла. Это бывает полезно лишь на начальных стадиях реализации. Дальше лучше исбавляться от бесконечного цикла и переходить сначала к циклам с предусловием, а потом (при возможности) к итераторам.

  • if str[/^\+OK/] then ... else ... end

При помощи команды str[/^\+OK/] мы проверяем наличие строки +OK в переменной str. Если проверка прошла успешно, то происходит выход из бесконечного цикла. Если нет, то продолжается получение данных и вывод их на экран.

  • str[/^\+OK/] ? break : puts( str )

Это всего лишь иная запись кода: if str[/^\+OK/] then ... else ... end. Зато более короткая...

  • while !(str = client.gets.chomp)[/^\+OK/] do ... end

Условный оператор и бесконечный цикл были объеденены в цикл с предусловием. Условия выхода тоже самое, но оно совмещено с чтением и присваиванием. Выглядит жутковато, но это вполне работоспособный код.

Из всех предложенных вариантов вы вольны выбирать любой... но мне не нравится вообще весь клиент. Как-то он сильно разросся и теперь выглядит монстрозно. Конечно же, есть возможность загнать чтение в отдельный метод, но мы этого делать не будем. Хотя... где наша не пропадала?! Давайте вынесем код отсылки команды и получения ответа в отдельный код. Естественно, что это будет метод для класса TCPSocket (который мы будем расширять). Назовем мы его .cmd.

   class TCPSocket
       def cmd( command, regexp = /^\+OK/ )
           self.puts command
           while !(str = self.gets.chomp)[ regexp ]
               yield str
           end
       end
   end

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

  • self.puts command

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

  • yield str

Данная команда передает значение переменной str в блок. Наличие этой команды подразумевает наличие блока во время вызова метода (имеется в виду .cmd). Блок позволит нам обрабатывать результат так, как мы хотим. Не ограничивая себя выводом результата на экран.

Теперь остается исправить клиент и посмотреть на него в действии:

   TCPSocket.open('localhost',31337){ |client|
       2.times{ puts client.gets.chomp }
       client.cmd("ls"){ |str| puts str }
       client.cmd("cd .."){ |str| puts str }
       client.cmd("ls"){ |str| puts str }
       client.cmd("shutdown"){ |str| puts str }
   }

Обратите внимание на код:

   client.cmd("ls"){ |str| puts str }

Метод .cmd работает как итератор. Последовательно передавая пришедшие строки в блок для дальнейшей обработки.

Чуть не забыл. Данный код выдает ошибку вида:

   client_001.rbw:18:in `cmd': private method `chomp' called for nil:NilClass (NoMethodError)

Это все потому, что передача команды shutdown не подразумевает ответа. А это неправильно. В очередной раз исправим сервер, чтобы избавится от этой ошибки:

   require 'gserver'
   class Tpo9H < GServer
       def serve( session )
           session.puts "Do6po no}i{aJIoBaTb Ha cepBep\r\n"
           session.puts "BbI mo}i{eTe Ha6paTb komaHgbI: ls <dir> | cd <dir> | shutdown\r\n"
           loop{
               cmd, arg = *session.gets.chomp.split
               case cmd
                   when "ls"
                       begin
                           session.puts Dir[ arg || "*" ].map{ |str| str + "\r\n" }
                       rescue
                           session.puts "HeT Takoro qpau'JIa uJIu gupekTopuu - #{ arg.inspect }\r\n"
                       end
                   when "cd"
                       begin
                           Dir.chdir( arg )
                           session.puts "CmeHa gupekTopuu Ha #{arg} npowJIa yga4Ho!\r\n"
                       rescue
                           session.puts "HeT Takoro qpau'JIa uJIu gupekTopuu - #{ arg.inspect }\r\n"
                       end
                   when "shutdown"
                       session.puts "+OK"
                       session.close
                       break
                   else
                       session.puts "HeBepHa9 komaHga!\r\n"
               end
               session.puts "+OK"
           }
       end
   end
   tpo9H = Tpo9H.new( 31337 )
   tpo9H.audit = true
   tpo9H.start
   tpo9H.join

На этом можно было бы и закончить, если бы не одно НО: метод .cmd уже реализован в рамках класса Net::Telnet. И не переписать наш клиент под этот класс было бы неправильно. Переписываем:

   require 'net/telnet'
   client = Net::Telnet.new( 'Host' => 'localhost', 'Port' => 31337, "Prompt" => /^\+OK/n )
   client.cmd("ls"){ |str| puts str }
   client.cmd("cd .."){ |str| puts str }
   client.cmd("ls"){ |str| puts str }
   client.close

Вот теперь, уж точно все!

☞
Вскоре, после публикации данной главы в учебнике, Geka прислал клиентскую часть, которую он реализовал в две строчки:
system( "telnet " + gets )
loop{ system( gets ){ |str| puts str } }

Способ не совсем честный, но нет причин о нем не рассказать. Данный способ использует метод system, который вызывает внешнюю программу (в данном случае telnet). Далее все введенное с клавиатуры уходит в программу telnet, а выдаваемое на экран берется из результата работы этой программы.

После запуска данной программы надо ввести имя хоста и порт. Далее, можно вводить команды, которые поддерживает сервер (в нашем случае ls, cd и shutdown).

[править] Как создать сетевой блокнот?

Идея написать подобную программу появилась после прочтения статьи Создаем свой online-блокнот. Продемонстрированная там программа предельно проста, но на ее примере очень удобно показать работу с ERB. А учитывая тот факт, что ERB используется в составе инструментария Руби на Рельсах, то ценность этой статьи становится очевидной.

[править] Первое приближение

В первом приближении мы попытаемся реализовать ту же самую функциональность, что и описана в статье. Вот только PHP подразумевает наличие веб-сервера, который будет заниматься интерпретацией его команд. В нашем же примере мы самостоятельно поднимем веб-сервер (написанный на Ruby), чтобы не заморачиваться с настройкой стороннего.

Смысл всей программы следующий: надо создать страницу с окном ввода и кнопкой "Сохранить", при нажатии на которую происходит сохранение текста из окна ввода в файл notepad.txt. Ввод осуществляется через браузер по адресу http://localhost:8080.

☞
Сервер будем запускать на порт 8080, так как стандартный для веб-серверов порт 80 у меня занял Skype. При желании, порт можно легко поменять

Теперь, собственно, сама программа:

   require 'webrick'
   server = WEBrick::HTTPServer.new( :Port => 8080 )
   server.mount_proc('/'){ |req,resp|
       File.open('notepad.txt','w'){ |f| f.write( req.query["text"] ) } if req.query["text"]
       resp['Content-Type'] = 'text/html'
       resp.body = %& <html><body><center><form method="post">
        <textarea name="text" rows="4" cols="40">#{IO.read('notepad.txt')}</textarea>
        <br /><input type="submit" name="update" value="Сохранить" />
        </form></center></body></html>&
   }
   server.start

Рассмотрим код более подробно. В частности, хочу обратить ваше внимание на следующие фрагменты:

  • require 'webrick'
Подключение библиотеки WEBrick для построения серверов (в том числе и веб-серверов).
  • :Port => 8080
Как уже было сказано ранее, порт 80 у меня занят. Поэтому приходится искать другой. Исключительной магической силой порт 8080 не обладает. Поэтому, при желании, его можно сменить на другой.
  • server.mount_proc('/')
На виртуальную корневую директорию мы вешаем процедурный сервлет. Он будет заниматься обработкой запроса на адрес http://localhost:8080/, то есть обращение к виртуальной корневой директории. Чтобы изменить запрос, на который будет откликаться сервлет, достаточно заменить строку '/' на другую, например '/notepad'. Тогда, адрес сервлета будет http://localhost:8080/notepad. Только, зачем писать больше?
  • { |req,resp| ... }
Процедурному (как и любому другому) сервлету передается в блок две переменные. Переменная req содержит информацию о запросе (какой браузер запрашивает, какие переменные передаются и так далее), а при помощи переменной resp формируется ответ (какой тип информации передается, информация для отображения и так далее).
  • ... if req.query["text"]
Постфиксная запись условного оператора if. Блок, перед if будет выполняться только в том случае, когда сервлету будет передаваться переменная text. Метод .query возвращает ассоциативный массив в виде {имя_переменной => значение}.
  • File.open('notepad.txt','w'){ |f| f.write( req.query["text"] ) }
Открываем файл notepad.txt для записи и пишем туда значение переменной text (передается сервлету в теле запроса).
  • resp['Content-Type'] = 'text/html'
Устанавливаем тип передаваемых нами данных. Обычно тип определяется по расширению файла, но процедурный сервлет про файл, а тем более про его расширение, ничего не слышал. Вот и приходится указывать тип передаваемых данных через параметр Content-Type.
  • resp.body = %& ... &
При помощи метода .body= мы передаем HTML-код в качестве ответа на запрос. Сам HTML-код передается в виде строки, заключенной в %& ... & . Это альтернативный способ задания строки. После символа % идет символ, который будет замыкающим (в нашем случае & ). Такой способ задания строки используется в случаях, когда строка содержит много кавычек и апострофов (чтобы не заниматься их экранированием).
  • server.start
При помощи метода .start мы запускаем веб-сервер.
☞
Для того, чтобы прекратить работу веб-сервера, надо его просто выгрузить. Это делается при помощи комбинации клавиш Ctrl+Break или Ctrl+C

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

[править] Добавляем ERB

Теперь приступим к ERB. Это шаблонная библиотека, которая позволяет осуществлять вставку в произвольный текст любого Ruby-кода. Для этого имеются два основных тега:

  • <% ... %>
Код внутри тега выполняется. Во время обработки он заменяется на пустую строку.
  • <%= ... %>
Код внутри тега выполняется. Результат последнего вычисления преобразуется в строку и вставляется вместо тега.
Обработка кода внутри тегов ERB идет только во время обработки шаблона (вызова метода .result)

Не знаю почему, но ERB для меня это "PHP, только на Ruby". Эта фраза обладает столь магическим свойством, что после нее собеседнику все становится понятным. Вот и вам советую воспринимать ERB как "PHP на Ruby".

☞
Библиотека WEBrick поддерживает PHP-скрипты благодаря CGI-сервлету

Давайте посмотрим, как может выглядеть наша программа в которой используется ERB-шаблон:

   require 'webrick'
   require 'erb'
   server = WEBrick::HTTPServer.new( :Port => 8080 )
   server.mount_proc('/'){ |req,resp|
       File.open('notepad.txt','w'){ |f| f.write req.query["text"] } if req.query["text"]
       resp['Content-Type'] = 'text/html'
       wa6JIoH = %& <html><body><center><form method="post">
        <textarea name="text" rows="4" cols="40"><%=IO.read('notepad.txt')%></textarea>
        <br /><input type="submit" name="update" value="Сохранить" />
        </form></center></body></html>&
       resp.body = ERB.new( wa6JIoH ).result
   }
   server.start

Что изменилось? Изменений немного, но они все же есть:

  • require 'erb'
Подключаем библиотеку ERB.
  • wa6JIoH = %& ... <%=IO.read('notepad.txt')%> ... &
Создаем переменную wa6JIoH и присваиваем ей строку, которая по совместительству является ERB-шаблоном. Внутри строки можно заметить тег <%= ... %> внутри которого осуществляется считывание файла notepad.txt. Результат считывания будет вставлен вместо тега <%= ... %> во время обработки ERB-шаблона.
  • resp.body = ERB.new( wa6JIoH ).result

Создаем объект ERB и передаем туда подготовленный ERB-шаблон. Обрабатываем его методом .result и результирую строку передаем методу .body=, который подставляет ее в качестве ответа на запрос.

И что мы получаем в результате? Подключили "лишнюю" библиотеку и создали "лишнюю" переменную? Не будем спешить с выводами. Использование библиотеки ERB позволяет вынести шаблон во внешний файл. Тем самым мы очищаем Ruby-код от HTML-кода.

"Кесарю - кесарево!" (с) Иешуа Ганоций

[править] Выносим ERB-шаблон во внешний файл

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

Файл с ERB-шаблоном (index.html) будет выглядеть следующим образом:

   <html><body><center><form method="post">
    <textarea name="text" rows="4" cols="40"><%=IO.read('notepad.txt')%></textarea>
    <br /><input type="submit" name="update" value="Сохранить" />
    </form></center></body></html>

Переменную шаблон мы убираем, а вместо нее вставим считывание файла c ERB-шаблоном (index.html).

   require 'webrick'
   require 'erb'
   server = WEBrick::HTTPServer.new( :Port => 8080 )
   server.mount_proc('/'){ |req,resp|
       File.open('notepad.txt','w'){ |f| f.write req.query["text"] } if req.query["text"]
       resp['Content-Type'] = 'text/html'
       resp.body = ERB.new( IO.read('index.html') ).result
   }
   server.start

Вот, уже другое дело! Можно было бы этим и ограничиться, если бы в библиотеке WEBrick отсутствовал бы ERB-сервлет... Но он есть!

[править] ERB-шаблон превращается в ERB-сервлет

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

   require 'webrick'
   server = WEBrick::HTTPServer.new( :Port => 8080 )
   server.mount('/',WEBrick::HTTPServlet::ERBHandler,'index.html')
   server.start
☞
Самые внимательные читатели заметили, что строка require 'erb' волшебным образом испарилась. Связано это с тем, что реализация ERB-сервлета уже подключает библиотеку ERB

Но, чтобы добиться столь существенного уменьшения кода программы, пришлось немного изменить файл index.html (с ERB-шаблоном):

   <% File.open('notepad.txt','w'){ |f| f.write query["text"] } if query["text"] %>
    <html><body><center><form method=post>
    <textarea name="text" rows="4" cols="40"><%=IO.read('notepad.txt')%></textarea>
    <br /><input type="submit" name="update" value="Сохранить" />
    </form></center></body></html>

Как можно заметить, в самое начало шаблона был добавлен тег, который осуществляет сохранение содержимого переменной text в файл notepad.txt. Код тега был перенесен из программы практически один к одному. За одним только исключением: к переменной text мы теперь обращаемся через переменную query, а не через req.query. Вот и все отличие!

На этом все... из чего состоит наша программа теперь?

  • notepad.rb. Программа-сервер. Назвать файл можно на свое усмотрение. Главное, чтобы работал. Содержит логику, которая осуществляет конфигурирование и запуск сервера.
  • index.html. ERB-шаблон. В нем содержится вся логика программы, кроме реализованной в программе-сервере.
  • notepad.txt. Файл данных. В нем содержатся записи, которые мы вводим и отображаем посредством нашей программы.

В качестве задания для самостоятельной проработки, предлагаю вам реализовать не только ввод и редактирование, но и просмотр без возможности редактирования. Подсказка: подключайте второй сервлет.

[править] Гнезда, которые свили не птицы

Примерное содержание главы
Примерное содержание главы

[править] Как пропинговать компьютер в сети?

Открываем новую серию статей, которые будут рассказывать про использование встроенных библиотек Ruby. Первая статья будет посвящена написанию утилиты ping (в очень упрощенной форме). Смотрим в стантартную библиотеку и обнаруживаем файлик ping.rb. Смотрим в него и обнаруживаем метод pingecho. Метод используется следующим образом:

require 'ping'
host = ARGV[0] || "localhost"
printf("%s alive? - %sn", host, Ping::pingecho(host, 5))

Данный метод имеет один маленький недостаток. Он не отслеживает никаких ошибок кроме Timeout::Error и Errno::ECONNREFUSED. Меня это немного смутило и поэтому я убрал явное указание на Timeout. Получился примерно такой метод:

require 'socket'
require 'timeout'
def ping( host, service = "echo",timeout = 5)
    begin
        timeout( timeout ){
            TCPSocket.open( host, service){}
        }
    rescue Errno::ECONNREFUSED
        true
    rescue false
    else true
    end
end
p ping(ARGV[0] || "localhost")

Итак, давайте разберем, что делает наш метод. Он создает соединение посредством класса TCPSocket и тут же закрывает его. Если соединение проходит слишком долго (хост не существует в сети) или произошла какая-то другая ошибка (не поддерживается протокол или еще что-то), то метод возвращает false. Если удаленный хост явно отверг наш запрос или принял его, то мы возвращаем true. Как видите, ничего сложного в этом нет.

[править] Простейший датчик работы службы

Летним воскресным утром мне захотелось сделать что-то приятное и красивое... Я написал письмо в конференцию о моих светлых идеях, но не смог его отправить. SMTP-сервер безбожно висел. И решило мое больное воображение написать программку, которая документировала бы подобные ситуации. В общем мне нужен был простейший документ (лог работы службы), который бы доказывал, что наши сетевые админы зря едят свой хлеб (а на самом деле я добрый). Для начала я определился с информацией, которая мне была нужна. А именно:

  • текущее время;
  • время отклика;
  • баннер службы.

Ее я решил писать в логи следующим образом... Каждый день будет создаваться файл лога и каждый час в него будет писаться информация о работе службы. В качестве планировщика заданий использовался виндовый Cron, который запускал мою программу в нужное время. Для начала я написал программу, которая соединяется со службой SMTP и получает от него баннер:

require 'socket'

request = ""
begin_time = Time.now
t = TCPSocket.open('mail.scli.ru', 'smtp'){
request = t.gets.chomp
t.close
end_time = Time.now
File.open( Time.now.strftime('d:/logs/smtp/%Y_%m_%d.smtp'), File::APPEND | File::CREAT | File::WRONLY ){ |f|
    f.puts( "#{sprintf( '%02d',Time.now.hour )} | #{end_time-begin_time} | #{request}" )
}

Как и следовало ожидать, программа не работала. Она подвисала и не хотела ничего писать в файл. Висела она на строчке:

request = t.gets.chomp

Чтобы разобраться с проблемой, пришлось читать книжку. Слава богу, что под рукой оказалась книга TCP/IP. Учебный курс. В ней на с.345 черным по серому была начертана схема взаимодействия SMTP протокола. Как оказалось, чтобы получить баннер от службы, надо послать команду NOOP. Переписываем наш фрагмент программы.

require 'socket'

request = ""
begin_time = Time.now
t = TCPSocket.open('mail.scli.ru', 'smtp')
t.puts('NOOP')
request = t.gets.chomp
t.close
end_time = Time.now
File.open( Time.now.strftime('d:/logs/smtp/%Y_%m_%d.smtp'), File::APPEND | File::CREAT | File::WRONLY ){ |f|
    f.puts( "#{sprintf( '%02d',Time.now.hour )} | #{end_time-begin_time} | #{request}" )
}

Великолепно! Программа работает... но иногда зависает. И тогда меня посетила еще одна мысль: на соединение отводить всего одну секунду (не уложился — сам дурак). Если соединение зависало, то в файл записывалось timeout. Чтобы "отводить время на выполнение операции" нужно задействовать библиотеку timeout. Она у меня входила в состав дистрибутива. Итак, переписываем нашу программу:

require 'timeout'
require 'socket'

request = ""
beachmark = ""
begin
    beachmark = Time.now
    timeout(1){
        t = TCPSocket.open('mail.scli.ru', 'smtp')
        t.puts('NOOP')
        request = t.gets.chomp t.close
    }
    beachmark = Time.now - beachmark
rescue
    beachmark = 'timeout'
end
File.open( Time.now.strftime('d:/logs/smtp/%Y_%m_%d.smtp'), File::APPEND | File::CREAT | File::WRONLY ){ |f|
    f.puts( "#{sprintf( '%02d',Time.now.hour )} | #{beachmark} | #{request}" )
}

Все бы хорошо, но вот beachmark хотелось бы считать "по-взрослому", а именно при помощи пакета beachmark. И снова переписываем код:

require 'timeout'
require 'socket'
require 'benchmark'

request = ""
beachmark = ""
begin
    beachmark = Benchmark.measure{
        timeout(1){
            t = TCPSocket.open('mail.scli.ru', 'smtp')
            t.puts('NOOP')
            request = t.gets.chomp
            t.close
        }
    }.to_s
rescue
    beachmark = 'timeout'
end
File.open( PATH_LOG_SMTP + Time.now.strftime('d:/logs/smtp/%Y_%m_%d.smtp'), File::APPEND | File::CREAT | File::WRONLY ){ |f|
    f.puts( "#{sprintf( '%02d',Time.now.hour )} | #{beachmark} | #{request}" )
}

И тут мы замечаем, что Beachmark очень многословен. Он выдает информацию в виде:

user system total ( real )

Нам нужен только real. Все остальное — для более детального анализа. Поэтому немного доработаем результат блока measure:

beachmark = Benchmark.measure{
    timeout(1){
        t = TCPSocket.open('mail.scli.ru', 'smtp')
        t.puts('NOOP')
        request = t.gets.chomp
        t.close
    }
}.real

Или можно просто вместо Benchmark.measure использовать Benchmark.realtime. Теперь надо бы разделить ошибки по таймауту и ошибки соединения. Для этого надо добавить лишь еще один блок ;rescue. Помимо этого мне не понадобится все сообщение от службы. Мне и кода сообщения достаточно. Итак, смотрим, что получилось:

require 'socket'
require 'benchmark'
require 'timeout'

request = ""
beachmark = ""
begin
    timeout(1){
        beachmark = Benchmark.measure{
            request = TCPSocket.open('mail.scli.ru', 'smtp'){ |t|
                t.puts('NOOP')
                t.gets.chomp
            }[0..2]
        }.real
    }
rescue Timeout::Error
    beachmark = 'timeout'
rescue
    beachmark = ' '
    request = 'error'
end
File.open( Time.now.strftime('d:/logs/smtp/%Y_%m_%d.smtp'), File::APPEND | File::CREAT | File::WRONLY ){ |f|
    f.puts( "#{sprintf( '%02d',Time.now.hour )} | #{beachmark} | #{request}" )
}

А теперь вопрос: как переписать программу так, чтобы она могла тестировать не только SMTP, но и HTTP, FTP, POP3 и т.д? Но это уже для самостоятельного изучения.

[править] Датчик обновления сайта

Ни для кого не секрет, что админы — это жутко ленивый народ. Но начальство, как назло, не хочет платить деньги за просто так. Ему нужны отчеты! Представляю на Ваш суд датчик обновления новостной ленты сайта. Для начала следует как следует поставить себе задачу:

  • Есть сайт и админ его частенько обновляет.
  • Будем рассматривать только случай добавления новостей. Все остальные разделы можно контролировать аналогично.
  • В качестве тестового сайта выберем http://www.minjust.ru. Но, с небольшой доработкой, программа может быть адаптирована и для любого другого сайта.
  • Обновление сайта просходит примерно 1-2 раза в день. Не больше, но может быть и меньше (это зависит от ЦОС Минюста РФ). Для более частого обновления придётся существенно дорабатывать программу.
  • Новости располагаются на первой странице сайта.

Итак, для чего нам нужен датчик? Допустим Вы админ сайта и постоянно добавляете новости. По окончании недели (месяца, года, столетия, ...) от Вас требуют отчет о проделанной работе. Вам приходится заходить на сайт и смотреть те новости, которые Вы добавили за последний период. Муторно и неэффективно. Намного приятнее постоянно вести записи о добавленных новостях (при помощи программы, конечно) и по завершении периода просто сделать соотвествующую выборку.

Итак, немного об алгоритме программы... Обычно новостей на главной странице строго определенное количество. На нашем тестовом сайте из ровно пять. У нас есть файл в котором мы храним дату добавления новости и заголовок новости. Разделитель у нас может быть произвольным, но в качестве примера будет использован набор символов ' ^_^ '. Вообще для данной задачи даже разделитель не очень-то и нужен (дата состоит из строго определенного количества символов и записывается в строго определенном формате)... но универсальность превыше всего!

Каждый раз при запуске программы, мы скачиваем заглавную страницу сайта и выдираем оттуда даты и заголовки новостей. Потом читаем файл с подобным же набором даных. Читаем весь файл, хотя можно читать только последние n-строк. Но мы будем создавать каждый месяц новый файл и поэтому особой загрузки памяти происходить не должно. Далее сравниваем эти два набора данных и с помощью пересечения и вычитания множеств мы получаем те данные, которых до сих пор нет в файле. Как раз эти данные мы и добавляем в конец файла. ВСЕ! Просто и лаконично. Теперь код, который выполняет поставленную задачу:

require 'net/http'

h = Net::HTTP.new( 'www.minjust.ru', 80 )
resp, data = h.get( '/index.html' )
dates = data.scan(/<DIV ALIGN="RIGHT" STYLE="font-size : x-small;">(d{4}-d{2}-d{2})</div>/).map{ |ar| ar[0] }
texts = data.scan(/<DIV CLASS="TITLE2">(.*?)</div>/m ).map{ |ar|
    ar[0].gsub("n",' ').gsub("r"," ").gsub(' '*2, ' ').strip
}
File.open( Time.now.strftime('log/%Y_%m.log'), File::APPEND | File::CREAT | File::RDWR ){ |f|
    from_inet = (0...5).inject([]){ |result, i| result + [ dates[i] + ' ^_^ ' + texts[i] ] }
    from_file = f.readlines.map{ |str| str.chomp }.compact
    f.puts( ( from_inet - (from_file & from_inet) ).sort )
}

В результате мы получаем файлы, которые могут быть с легкостью использованы для составления отчетов. Будут предложения по оптимизации — пишите на форум.

[править] Приемо-передача файлов

[править] Как скачать HTML-страницу

Для скачивания HTML-страниц обычно используется протокол HTTP. Адрес страницы задается в виде URL. В Руби существует несколько способов скачать страницу по протоколу HTTP. Начнем с самого древнего – класса Net::HTTP (древнее только Socket'ы, но про них мы умолчим). Для определенности мы будем скачивать этот учебник и сохранять его в виде файла RubyBook.html.

   require 'net/http'

   Net::HTTP.open('ru.wikibooks.org'){ |http|
       File.open('RubyBook.html','w'){ |file|
           file.write http.get('/wiki/Ruby').body
       }
   }

Недостатком использования класса Net::HTTP является то, что URL разбивается на две части: адрес сервера (ru.wikibooks.org) и адрес страницы (/wiki/Ruby). Такое разбиение удобно только в том случае, когда необходимо скачивать несколько HTML-страниц с одного сервера. А вот как раз таких ситуации возникают крайне редко. Чаще приходится запрашивать страницы с различных серверов. Для этих целей и был придуман метод open из библиотеки open-uri. Он самостоятельно разбирает URL и производит нужный запрос (не только HTTP).

require 'open-uri'

File.open('RubyBook.html','w'){ |file|
    file.write open( 'http://ru.wikibooks.org/wiki/Ruby' ).read
}
Обратите внимание, что необходимо указывать полный URL с указанием протокола (в данном случае http://)

[править] Запрос заголовка

Во время скачивания передается не только сама страница (тело сообщения или на англ. boby), но и техническая информация (заголовок или на англ. head). Мы еще не раз будем свидетелями такого поведения в протоколах сети Интернет. В заголовке содержится информация об успешности запроса (код возврата), типе передаваемых данных, кодировке, HTTP-сервере и так далее. Для примера, мы будем производить HTTP-запрос и получать ответ только в виде заголовка. Запись заголовка в файл производить не будем, так как в реальной практике этот прием практически не используется. Сначала «потренируемся на кошках», то есть на классе Net::HTTP:

require 'net/http'

Net::HTTP.open('ru.wikibooks.org'){ |http|
    p http.head('/wiki/Ruby')
}

Как и обещалось ранее, вывод заголовка мы делаем на экран. А запрос заголовка, вместо тела сообщения, осуществляется простой заменой метода .get на метод .head. А как тогда получить заголовок и тело сообщения одновременно? Очень просто:

require 'net/http'

Net::HTTP.open('ru.wikibooks.org'){ |http|
   head, body = http.get('/wiki/Ruby')
}

Для этого достаточно присвоить результат метода .get двум переменным. Произойдет присвоение списков и в первую переменную попадет заголовок, а во вторую тело сообщения.

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

require 'open-uri'

p open( 'http://ru.wikibooks.org/wiki/Ruby' ).meta

Замена метода .read на метод .meta позволяет нам дотянуться до заголовка. Заголовок имеет вид ассоциативного массива (объект класса Hash), где ключем является имя параметра (по стандарту MIME), а значением – значение параметра. Читать одновременно заголовок и тело сообщения можно вот так:

require 'open-uri'

open( 'http://ru.wikibooks.org/wiki/Ruby/' ){ |f|
       p f.meta
       p f.read
}

Мы использовали свойство метода .open прикреплять к себе блок.

[править] Работа через прокси

Прокси-сервер – это сервер, перенаправляющий запросы другим серверам. Обычно таковой используется для скрытия истинного IP-адреса или для контроля за трафиком.

Необходимость HTTP-запроса через прокси возникает, когда соединение с целевым сервером напрямую невозможно (например, администратор сети посчитал, что соединение посредством шлюза дает ему слишком мало возможностей контроля за трафиком). Использование подобного запроса маловероятно, но необходимо знать о возможности посылки http-запроса через прокси. Начнем с класса Net::HTTP::Proxy:

require 'net/http'

Net::HTTP::Proxy('you.proxy.host',8808).start('ru.wikibooks.org'){ |http|
 p http.get('/wiki/Ruby').body
}

Добавив всего лишь небольшой фрагмент кода мы получили работу через прокси. В нашем случае использовался прокси-сервер с адресом you.proxy.host, который предоставляет прокси-доступ через порт 8808.

Теперь посмотрим, как эта же самая функциональность можеть быть реализована с использованием метода open (из библиотеки open-uri).

require 'open-uri'

p open( 'http://ru.wikibooks.org/wiki/Ruby',:proxy => 'http://you.proxy.host:8808/').read

Добавился второй параметр (ассоциативный массив с единственной парой) в вызове метода open. К слову сказать, во втором параметре можно указать множество параметров запроса (например, название браузера).

☞
Реализация соединения через прокси у метода open более удобна, так как в зависимости от внешних факторов можно соединяться как через прокси, так и напрямую. В случае Net::HTTP и Net::HTTP::Proxy используются два разных класса и подобные трюки затруднительны (хотя и возможны).

[править] Вам письмо или You have got mail

[править] Запускаем свой веб-сервер

Для того, чтобы создать простейший веб-сервер не нужно реализовывать протокол HTTP на уровне сокетов. Достаточно знать, какую библиотеку использовать. В нашем случае это будет библиотека WEBrick (http://www.webrick.org), которая уже включена в дистрибутив Руби. Возможности библиотеки настолько широкие, что для их описания потребуется создание отдельного учебника. Мы рассмотрим лишь часть из них, зато на реальном жизненном примере.

☞
Библиотека WEBrick (переводится как «куча паутины») отнюдь не лучшая реализация библиотеки для построения веб-сервера. Сейчас набирает обороты библиотека Mongrel (переводится как «полукровка», что указывает на смешанную структуру библиотеки. В ней критичные участки кода реализованы не на Руби, а на Си. Это в лучшую сторону сказалось на скорости работы данного симбиоза), которая имеет гораздо лучшую реализацию, но для ее использования нужно скачивать и устанавливать библиотечные файлы, так как в дистрибутив она пока не включена. Именно поэтому построение нашего первого веб-сервера будет проходить на базе WEBrick

Секретарша начальника Управления производственного планирования ЗИЛ попросила меня реализовать программу, которая облегчала бы ей процесс создания служебных записок. В частности, требовалось по кодам подразделений выставлять фамилии и должность начальников этих подразделений. Без программы это сущая морока, особенно, если необходимо отправлять «служебку» необходимо в несколько подразделений. У меня не было особенного настроения, но я решил сделать два дела одновременно: заслужить благодарность секретарши и попутно продемонстрировать процесс создания web-серверного приложения. Благодарность я уже получил, поэтому приступаю ко второй части.

Для того, чтобы проверить работает ли библиотека WEBrick я выполнил следующий код:

   require 'webrick'
   WEBrick::HTTPServer.new( :DocumentRoot => 'public_html', :Port => 8080 ).start

После успешного запуска сервера, я запустил Firefox и набрал адрес: http://localhost:8080/. И тут я узнал, что директория public_html пуста. Горевал я по этому поводу не долго и быстро создал файл public_html/index.html примерно следующего содержания:

   <html><body><h1>Сервер работает!</h1></body></html>

После обновления страницы на экране красовалась крупная надпись: "Сервер работает!" Все! Мы с вами написали свой первый веб-сервер.

Для того, чтобы прекратить выполнение сервера нужно нажать комбинацию клавиш Ctrl+Break или закрыть окно терминала. Для данной реализации веб-сервера это единственный способ прекратить его работу

Настало время разобраться в том, как это все работает:

  • require 'webrick'
Подключение библиотеки WEBrick. Ничего интересного и неожиданного здесь не произошло.
  • WEBrick::HTTPServer.new( :DocumentRoot => 'public_html', :Port => 8080 ).start
  • Библиотека WEBrick реализована в виде модуля с одноименным названием, поэтому до любого класса приходится «дотягиваться» через префикс: WEBrick::.
  • HTTPServer — это название класса, который используется для создания веб-серверов.
  • Метод .new создает экземпляр класса WEBrick::HTTPServer. Методу передается два параметра (на самом деле — один ассоциативный массив с двумя парами):
  • :DocumentRoot => 'public_html' указывает на то, что веб-сервер будет просматривать директорию ./public_html для выдачи html-страниц (и не только) по запросу. Если этот параметр не указывать, то сервер просто не будет работать.
  • :Port => 8080 задает порт 8080 для «прослушивания» сервером. Если этот параметр не указывать, то сервер будет прослушивать порт 80 (стандарт для веб-серверов). Но случилось так, что «слушать» 80-порт он у меня отказался, так как там «обосновался» Skype. Вот и пришлось в срочном порядке придумывать новый. Ничего умнее, как продублировать число 80 мне в голову не пришло. Если фантазия у вас развита лучше, то придумайте любой другой порт.
  • Метод .start запускает сервер. В отличии от класса GServer, дополнительные манипуляции с методами .sleep и loop не требуются.
☞
Очень часто необходимо осуществить передачу данных между компьютерами по сети. Для этого вполне подойдет WEBrick. Быстро напишите веб-сервер, который будет просматривать нужную директорию. Теперь, для вас это больше не проблема!

Но вернемся к заказанной мне программе. Она будет состоять из двух частей: обычная html-страница (index.html) и сервлет (/input). Страница public_html/index.html будет содержать форму в которую будут вводится исходные данные. Ее задачей будет формирование запроса для сервлета. Сервлет /input будет получать номера подразделений, а выдавать их названия и ФИО действующих начальников. Вот код public_html/index.html:

   <HTML><BODY><form action='/input' method='post'><div align=center>
    <table border=0 bgcolor=#000000 width=350><tr><td><table border=0 bgcolor=#CCCCFF width=100%>
    <tr><td align=right>Лит-1 - 01</td><td><INPUT TYPE="checkbox" NAME="check_01"></td>
    <td><INPUT TYPE="checkbox" NAME="check_02"></td><td>02 - ГЛЦЧ</td></tr>
    <tr><td align=right>ГКЦ - 04</td><td><INPUT TYPE="checkbox" NAME="check_04"></td>
    <td><INPUT TYPE="checkbox" NAME="check_05"></td><td>05 - Прессовый</td></tr>
    <tr><td align=right>РПЦ - 06</td><td><INPUT TYPE="checkbox" NAME="check_06"></td>
    <td><INPUT TYPE="checkbox" NAME="check_09"></td><td>09 - Моторный</td></tr>
    <tr><td align=right>Кузовной - 10</td><td><INPUT TYPE="checkbox" NAME="check_10"></td>
    <td><INPUT TYPE="checkbox" NAME="check_11"></td><td>11 - ПСК</td></tr>
    <tr><td align=right>МСК-1 - 12</td><td><INPUT TYPE="checkbox" NAME="check_12"></td>
    <td><INPUT TYPE="checkbox" NAME="check_14"></td><td>14 - ПОиСА</td></tr>
    <tr><td align=right>МСЦ-3 - 17</td><td><INPUT TYPE="checkbox" NAME="check_17"></td>
    <td><INPUT TYPE="checkbox" NAME="check_18"></td><td>18 - МСЦ-2</td></tr>
    <tr><td align=right>Нормаль - 19</td><td><INPUT TYPE="checkbox" NAME="check_19"></td>
    <td><INPUT TYPE="checkbox" NAME="check_20"></td><td>20 - Арматурный</td></tr>
    <tr><td align=right>АСК - 21</td><td><INPUT TYPE="checkbox" NAME="check_21"></td>
    <td><INPUT TYPE="checkbox" NAME="check_22"></td><td>22 - Термический</td></tr>
    <tr><td align=right>РААЗ - 25</td><td><INPUT TYPE="checkbox" NAME="check_25"></td>
    <td><INPUT TYPE="checkbox" NAME="check_27"></td><td>27 - МЗАА</td></tr>
    <tr><td align=right>УО - 30</td><td><INPUT TYPE="checkbox" NAME="check_30"></td>
    <td><INPUT TYPE="checkbox" NAME="check_34"></td><td>34 - ЗИЛтехоснастка</td></tr>
    <tr><td align=right>УКЭР - 57</td><td><INPUT TYPE="checkbox" NAME="check_57"></td>
    <td><INPUT TYPE="checkbox" NAME="check_58"></td><td>58 - ПенЗА</td></tr>
    <tr><td align=right>СААЗ - 61</td><td><INPUT TYPE="checkbox" NAME="check_61"></td>
    <td><INPUT TYPE="checkbox" NAME="check_62"></td><td>62 - ТУ</td></tr>
    <tr><td align=right>УКК - 64</td><td><INPUT TYPE="checkbox" NAME="check_64"></td>
    <td><INPUT TYPE="checkbox" NAME="check_67"></td><td>67 - УМТС</td></tr>
    <tr><td align=right>УСК - 74</td><td><INPUT TYPE="checkbox" NAME="check_74"></td>
    <td><INPUT TYPE="checkbox" NAME="check_76"></td><td>76 - ЦИТ</td></tr>
    <tr><td align=right>УСХ - 81</td><td><INPUT TYPE="checkbox" NAME="check_81"></td>
    <td><INPUT TYPE="checkbox" NAME="check_82"></td><td>82 - ПЗА</td></tr>
    <tr><td align=right>РЗАА - 85</td><td><INPUT TYPE="checkbox" NAME="check_85"></td>
    <td><INPUT TYPE="checkbox" NAME="check_"></td><td> </td></tr>
    <tr><td colspan=4 align=center><INPUT TYPE="submit" NAME="Сформировать шапку"></td></tr>
    <tr><td align=right>Рассказов</td><td><INPUT TYPE="checkbox" NAME="check_001"></td>
    <td><INPUT TYPE="checkbox" NAME="check_002"></td><td>Коновалова</td></tr>
    <tr><td align=right>Принцев</td><td><INPUT TYPE="checkbox" NAME="check_003"></td>
    <td><INPUT TYPE="checkbox" NAME="check_004"></td><td>Сорокин</td></tr>
    <tr><td align=right>Журавлев</td><td><INPUT TYPE="checkbox" NAME="check_005"></td>
    <td><INPUT TYPE="checkbox" NAME="check_006"></td><td>Корабельников</td></tr>
    <tr><td align=right>Фет</td><td><INPUT TYPE="checkbox" NAME="check_007"></td>
    <td><INPUT TYPE="checkbox" NAME="check_008"></td><td>Ярков</td></tr>
    <tr><td align=right>Болотин</td><td><INPUT TYPE="checkbox" NAME="check_009"></td>
    <td><INPUT TYPE="checkbox" NAME="check_010"></td><td>Шрамов</td></tr>
    <tr><td align=right>Поленов</td><td><INPUT TYPE="checkbox" NAME="check_011"></td>
    <td><INPUT TYPE="checkbox" NAME="check_012"></td><td>Копылов</td></tr>
    </table></td></tr></table></div></form></BODY></HTML>
☞
Geka, когда увидел этот код, воскликнул: "И не лень было такой код писать?" На что я ему резонно ответил, что этот код является результатом программы-генератора, которую я не привожу здесь, чтобы не отвлекать внимание читателя от более интересной темы

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

  • action='/input'
Адрес, по которому будут передаваться данные. Это может быть как CGI-приложение, так и сервлет. В нашем случае это сервлет /input.
  • method='post'
В протоколе HTTP используется два метода передачи данных: POST и GET. Отличаются они тем, что POST не отображает передаваемые данные в адресной строке браузера. Возьмите за правило использовать метод POST.

Внутри тега FORM присутствует великое множество тегов INPUT, которые как раз и формируют данные. Каждый из тегов INPUT имеет два параметра: NAME и TYPE.

  • TYPE="checkbox"
Параметр TYPE указывает на тип формируемых данных. В данном случае это checkbox (переключатель). Он имеет два значения: "on" (включен) и "off" (выключен). Значение "off" обычно не передается. Получается своеобразный булевский тип.
  • NAME="check_01"
Все данные передаются в виде пары имя=значение. Параметром NAME задается имя. Значение же задается пользователем.

Вот как выглядит страница index.html после обработки браузером (Firefox):

Вид страницы public_html/index.html в Firefox

Теперь рассмотрим серверную часть программы (сервлет /input), которая будет обрабатывать запросы (сформированные файлом public/index.html). Сервлет является частью сервера. Поэтому листинг сервера будет одновременно и листингом сервлета.

   bosses = {
   'check_01' => "\tНачальнику Лит-1\tБокову Ю.В.",
   'check_02' => "\tНачальнику ГЛЦЧ\tНазарову А.В.",
   'check_04' => "\tНачальнику ГКЦ\tЛасунину Б.Д.",
   'check_05' => "\tНачальнику Прессового корпуса\tЯшнову Ю.М.",
   'check_06' => "\tНачальнику РПЦ\tШепелеву Е.И.",
   'check_09' => "\tДиректору МАП\tБагатурии Р.С.",
   'check_10' => "\tНачальнику Кузовного корпуса\tАшмарину А.Г.",
   'check_11' => "\tНачальнику ПСК\tАнаньву А.С.",
   'check_12' => "\tНачальнику МСК-1\tМиролюбову В.П.",
   'check_14' => "\tДиректору ПОиСА\tСаттарову М.Д.",
   'check_17' => "\tНачальнику МСЦ-3\tБородуле П.Н.",
   'check_18' => "\tНачальнику МСЦ-2\tГрищенкову А.И.",
   'check_19' => "\tНачальнику цеха \"Нормаль\"\tАфонину А.Н.",
   'check_20' => "\tНачальнику Арматурного цеха\tДавыдову В.И.",
   'check_21' => "\tНачальнику АСК\tБорисюку В.Д.",
   'check_22' => "\tНачальнику Термического цеха\tВерташову Н.А.",
   'check_25' => "\tДиректору ЗАО РААЗ\tСавчуку В.Ф.",
   'check_27' => "\tДиректору ЗАО МЗАА\tСоловьеву Н.И.",
   'check_30' => "\tНачальнику УО\tЮру А.Е.",
   'check_34' => "\tДиректору ООО \"ЗИЛтехоснастка\"\tТимофееву Г.П.",
   'check_57' => "\tГлавному конструктору АМО ЗИЛ-начальнику УКЭР\tРыбину Е.Л.",
   'check_58' => "\tДиректору ЗАО ПенЗА\tГудкову В.И.",
   'check_61' => "\tДиректору ЗАО СААЗ\tНовикову В.А.",
   'check_64' => "\tНачальнику УКК\tМинину Д.С.",
   'check_62' => "\tНачальнику ТУ\tУстинкину В.В.",
   'check_67' => "\tНачальнику УМТС\tМелешкину В.Д.",
   'check_74' => "\tНачальнику УСК\tТарабрину В.В.",
   'check_76' => "\tДиректору ЦИТ\tИгнатьеву В.П.",
   'check_81' => "\tНачальнику УСХ\tТурчину Н.В.",
   'check_82' => "\tДиректору ЗАО ПЗА\tПлешакову И.В.",
   'check_85' => "\tДиректору ОАО РЗАА\tДобрынину Ю.Г.",
   'check_001' => "\tНачальнику УК\tРассказову А.А.",
   'check_002' => "\tНачальнику ЭУ\tКоноваловой С.Н.",
   'check_003' => "\tИсполнительному директору АМО ЗИЛ\tПринцеву И.В.",
   'check_004' => "\tДиректору по экономике и финансам АМО ЗИЛ\tСорокину А.В.",
   'check_005' => "\tДиректору по производству АМО ЗИЛ\tЖуравлеву В.С.",
   'check_006' => "\tКоммерческому директору АМО ЗИЛ\tКорабельникову Е.В.",
   'check_007' => "\tДиректору по дочерним и зависимым обществам\tФету О.Л.",
   'check_008' => "\tГлавному инженеру АМО ЗИЛ\tЯркову Г.А.",
   'check_009' => "\tДиректору по качеству АМО ЗИЛ\tБолотину Ю.М.",
   'check_010' => "\tДиректору ЗАО \"Торговый дом ЗИЛ\"\tШрамову В.П.",
   'check_011' => "\tДиректору ООО ВТФ \"ЗИЛ-экспорт\"\tПоленову А.Ю.",
   'check_012' => "\tДиректору Прессового производства\tКопылову Ю.П."
   }
   require 'webrick'
   server = WEBrick::HTTPServer.new( :Port => 8080 )
   server.mount_proc('/'){ |req,resp| resp.body = IO.read('public_html/index.html') }
   server.mount_proc('/input'){ |req,resp|
       resp.body = %!<html><body><div align=center><form action='/' method='post'>
    <textarea rows='5' cols='60'>#{req.query.map{ |key,value| bosses[ key ] }.compact.join("\n") }
    </textarea><br /><input type='submit' value='Повторим?'></form></div></body></html>!
   }
   server.start

Начинаем разбираться с кодом веб-сервера:

  • bosses = { ... }
Создается ассоциативный массив, который будет использоваться при обработке данных. Ключем является имя «переключателя», а значением — строка, на которую надо этот ключ заменить. Путем такой замены мы будем формировать результат.
  • require 'webrick'
Подключаем библиотеку WEBrick. По-хорошему ее надо было подключать перед инициализацией ассоциативного массива. Но не хотелось разбивать на части код, относящийся к реализации веб-сервера.
  • server = WEBrick::HTTPServer.new( :Port => 8080 )
Создаем экземпляр класса WEBrick::HTTPServer. На этот раз мы его сохраняем в переменную server. Это необходимо для подключения сервлетов. Параметр :DocumentRoot мы не указываем, так как файл public_html/index.html мы тоже сделаем сервлетом. Поэтому подключать для этого всю папку public_html просто глупо.
  • server.mount_proc('/'){ |req,resp| resp.body = IO.read('public_html/index.html') }
  • При помощи метода .mount_proc('/') мы закрепили сервлет на корневую директорию, то есть при запросе http://localhost:8080/ будет вызываться именно он.
  • Параметры req и resp означают запрос сервлету (req) и ответ сервлета (resp), то есть сервлет получает переменную req и в качестве результата должен сформировать переменную resp (resp.body = ).
  • В качестве результата сервлет возвращает содержимое файла public_html/index.html. По хорошему, надо было бы считать этот файл при запуске сервера, чтобы сэкономить время обработки сервлета.
  • server.mount_proc('/input'){ |req,resp| ... }
Таким образом мы создаем сервлет /input, который будет обрабатывать запросы. Его тело мы рассмотрим в несколько заходов:
  • При помощи resp.body = ... мы формируем выходные данные (строка с HTML-кодом).
  • HTML-код: <html><body><div align=center><form action='/' method='post'><textarea rows='5' cols='60'>#{..}</textarea><br /><input type='submit' value='Повторим?'></form></div></body></html>; просто создает окружения для наших выходных данных. В частности видно, что наши выходные данные будут помещаться в тег TEXTAREA. Это сделано для того, чтобы удобней было копировать данные. Вот примерно так это будет выглядеть в браузере:
  • Код обработки данных: req.query.map{ |key,value| bosses[ key ] }.compact.join("\n"); получает список передаваемых параметров в виде ассоциативного массива (метод .query) и заменяет имена переменных на значения из ассоциативного массива bosses. Далее идет преобразование в строку при помощи метода .join("\n").
  • server.start
Запуск сервера.

Вот почти такую программу я и презентовал секретарше Анюте. «Почти» потому, что фамилии должны идти строго в определенном порядке (чего нельзя добиться в ассоциативном массиве), но для того, чтобы посмотреть реализацию веб-сервера — продемонстрированной версии программы должно быть достаточно. Смело меняйте код и создавайте свои веб-сервера!

[править] Сервлетки в рубиновом соусе

Мы уже неоднократно упоминали понятие сервлет, но особенно на нем не останавливались. Давайте сейчас рассмотрим типовые сервлеты, которые имеются в стандартной поставке WEBrick

  • Файловый сервлет
Реализует взаимосвязь запроса с реальным файлом (или директорией). В самом первом примере мы использовали файловый сервлет, когда передавали :DocumentRoot => 'public_html' в качестве параметра методу .new. Это было равнозначно созданию файлового сервлета на корневой директории веб-сервера. Функциональность файлового сервлета описана в классе WEBrick::HTTPServlet::FileHandler.
  • CGI-сервлет
Реализует взаимосвязь запроса с CGI-приложением, то есть считывается первая строка и вытаскивается оттуда путь к интерпретатору. Далее производится запуск интерпретатора, которому параметром передается вызванный файл. Строка для CGI-приложений на Ruby будет выглядеть приемерно так:
  • #!/usr/bin/ruby
для операционных систем семейства Unix
  • #!c:/ruby/bin/ruby.exe
для операционных систем семейства Windows
Кстати, я столкнулся с тем, что под Windows CGI-сервлеты отказывались работать. Это связано с тем, что WEBrick считает, что для того, чтобы запустить скрипт достаточно просто его вызывать (./sctipt_name) и он сам запустится. Понятное дело, что в Windows такое работать не будет. Поэтому мне пришлось немножко переписать часть библиотеки WEBrick, которая отвечает за запуск CGI-программ. Для того, чтобы мои изменение стали доступны и вам, я написал небольшую программку, которая устанавливает себя куда надо:
 File.open(Config::CONFIG['rubylibdir']+'/webrick/httpservlet/cgi_runner.rb','w'){|file| file.puts IO.read($0).split('#'*10).last }
 exit
 ##########
 #
 # cgi_runner.rb -- CGI launcher.
 #
 # Author: IPR -- Internet Programming with Ruby -- writers
 # Copyright (c) 2000 TAKAHASHI Masayoshi, GOTOU YUUZOU
 # Copyright (c) 2002 Internet Programming with Ruby writers. All rights
 # reserved.
 # Copyright (c) 2007 Rubynovich
 #
 # $IPR: cgi_runner.rb,v 2.0 2007/02/23 18:53:15 Rubynovich Exp $

 STDIN.binmode
 STDOUT.reopen(open(STDIN.sysread(STDIN.sysread(8).to_i), "w"))
 STDERR.reopen(open(STDIN.sysread(STDIN.sysread(8).to_i), "w"))

 ENV.clear
 ENV.update( Marshal.restore(STDIN.sysread(STDIN.sysread(8).to_i)) )

 if IO.read(ENV["SCRIPT_FILENAME"],50) =~ /^#!(.*)$/
   exec($1,ENV["SCRIPT_FILENAME"])
 else
   exec(ENV["SCRIPT_FILENAME"])
 end
Немного поясню. Первая строчка -- это программа, которая записывает нижеследующий код в папку с библиотеками.
Функциональность CGI-сервлета описана в классе WEBrick::HTTPServlet::CGIHandler
  • ERB-сервлет
Реализует взаимосвязь запроса с ERB-шаблонами, в которые вставляются предварительно подготовленные данные.
  • Процедурный сервлет
Этим сервлетом мы уже пользовались, когда писали .mount_proc. Он обеспечивает взаимосвязь запроса и процедуры обработки (описанной внутри блока метода .mount_proc).

Для того, чтобы было проще пользоваться всеми видами сервлетов WEBrick я написал небольшую программку, которая создает метод mount_file (для файлового сервлета), mount_erb (для ERB-сервлета) и mount_cgi (для CGI-сервлета). Как вы могли заметить mount_proc уже существует (собственно, его название и послужило прототипом для остальных). Вот эта программа:

require 'webrick'
module WEBrick
  class HTTPServer
    ['ERB','CGI','File'].each{ |handler|
      class_eval( "def mount_#{handler.downcase}(point,file)\nmount(point,HTTPServlet::#{handler}Handler,file)\nend" )
    }
  end
end

Теперь можно использовать эти сервлеты следующим образом:

require 'webrick'
module WEBrick
  class HTTPServer
    ['ERB','CGI','File'].each{ |handler|
      class_eval( "def mount_#{handler.downcase}(point,file)\nmount(point,HTTPServlet::#{handler}Handler,file)\nend" )
    }
  end
end

server = WEBrick::HTTPServer.new( :Port => 8090 )
server.mount_file('/','html')
server.mount_cgi('/hello','cgi-bin/hello.exe' )
server.mount_cgi('/hello.cgi','cgi-bin/hello.rb' )
server.start

Ну вот вроде и все, что можно сказать по сервлетам.

[править] Приложения

/Задачник
Сборник задач
/Справочник
Справочник по базовым классам
/Лицензия
Перевод Лицензии Руби
/Жаргон
Придумываем слова с рубинами и рельсами
/Фольклор
Народное творчество любителей Руби
/Избранное с RubyNews
К разграблению. Полезные статьи перемещаются в основную часть учебника, затем это приложение удалим.
/Идеология
программирования вообще и на Руби в частности

[править] Дальнейшее чтение

[править] Русскоязычные ресурсы

  • Форум Ruby on rails. Русскоязычный форум написанный на Ruby on Rails (пока в тестовом варианте) но планируется быстрое развитие.

[править] Русскоязычная литература

  • Ruby — руководство пользователя. Перевод Александра Мячкова.
  • Конспект лекций по высокоуровневому программированию. Шипиев Р.Н.
  • Rolling with Ruby on Rails. Перевод Андрея Горбатова (pdf).
  • Самоучитель Ruby. Параллельный проект по написанию учебника.
  • ЧАсто задаваемые ВОпросы. FAQ по Ruby.
  • Программирование на Ruby. Учебное пособие по языку Ruby, Роганова Н.А., Тузов А.С.
  • Лекции по программированию. Роганова Н.А.

[править] Иноязычная литература

  • Why’s (Poignant) Guide to Ruby — эта книга достойна чтения, даже если Вам не нужно знание Руби. Просто шедевр. Распространяется бесплатно. Там ещё богатая подсветка текста; эх, нам бы такую.. Частичный перевод здесь.
  • Programming Ruby Дэйва Томаса. Многие пытались, но не перевели на русский. Первая редакция книги содержится в пакете «Установка за один щелчок» для Windows. Владельцы других ОС смогут найти её в Сети (например, в виде набора вебстраниц). Вторую редакцию книги можно купить на Amazon.com в бумажном или электронном виде.

[править] Полезные ссылки

  • Ruby.on-page.net (англ.) — самый простой справочник по Ruby

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

Получено с http://ru.wikibooks.org/wiki/Ruby

Категории: Языки программирования | Ruby | Объектно-ориентированное программирование

Просмотры
  • Страница
  • Обсуждение
  • Править
  • История
Личные инструменты
  • Представиться системе
Навигация
  • Учебники
  • Изменения
  • Справка
  • Дела
  • Форум
Инструменты
  • Ссылки сюда
  • Связанные правки
  • Закачать файл
  • Спецстраницы
  • Для печати
  • Адрес версии
На других языках
  • Deutsch
  • English
  • Esperanto
Powered by MediaWiki
  • Последнее изменение этой страницы: 07:42, 11 апреля 2007.
  • Текст распространяется по GNU Free Documentation License.
  • О приватности
  • Введение
  • Предупреждения