• Авторизация


Полезная информация о язык программирования Ruby 24-06-2007 22:42 к комментариям - к полной версии - понравилось!


Часть 1

Введение

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 может быть переведено на русский язык как "рубин", прим. перев.), я буду делиться своим опытом , приобретенным при его изучении. До свидания!

Часть 2

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

Регулярные выражения в 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++) делает объектно-ориентированный стиль программирования естественной привычкой так, что даже когда вам приходится работать над маленькими проектами, у вас не возникает потребности писать уродливый код, чтобы сэкономить усилия. Следующей нашей темой будут "органы управления" (методы объектов), а затем мы перейдем к "фабрикам" (классам). Оставайтесь с нами!

 

Третья часть

Методы

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

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. Но в будущем, как только позволит время, я это продолжу.

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

[566x699]
вверх^ к полной версии понравилось! в evernote
Комментарии (13):
_JeRRiE_ 24-06-2007-23:11 удалить
А мне кажется что это лучше)))) [547x699]
_JeRRiE_ 24-06-2007-23:53 удалить
Ещё_не_побеждённая, там написано: 1. Рассмотрим игру "Делители". Играют двое. Игра начинается с числа 60. За ход разрешается уменьшить имеющееся число на любой из его делителей (включая единицу и его само). Проигрывает тот, после хода которого получится число 0. Напишите программу, позволяющую двум людям играть в эту игру. Пусть позиция задается числом А. Тогда программа, следя за очередностью, принимает ходы игроков (числа, на которое надо уменьшить А), затем проверяет их на корректность. При получении корректного хода она изменяет число А, и ход переходит к другому игроку. В конце игры программа выдает победителя. Пример работы программы: Сейчас А равно 60. Ход первого игрока: 7 Ход неверный. Ход первого игрока: -1 Ход неверный. Ход первого игрока: 30 Ход верный. Сейчас А равно 30. Ход второго игрока: 30 Ход верный. Сейчас А равно 0. Победил первый игрок.
Acryll 25-06-2007-11:21 удалить
Короче на экzамене ты мне поможешь)


Комментарии (13): вверх^

Вы сейчас не можете прокомментировать это сообщение.

Дневник Полезная информация о язык программирования Ruby | _JeRRiE_ - Маленькое безобидное создание | Лента друзей _JeRRiE_ / Полная версия Добавить в друзья Страницы: раньше»