Вот пример определения синглетного метода для строкового объекта:
str = "Hello, world!"
str2 = "Goodbye!"
def str.spell
self.split(/./).join("-")
end
str.spell # "H-e-l-l-o-,- -w-o-r-l-d-!"
str2.spell # Ошибка!
Имейте в виду, что метод определяется для объекта, а не для переменной. Теоретически с помощью синглетных методов можно было бы создать систему объектов на базе прототипов. Это менее распространенная форма ООП без классов[5]. Основной структурный механизм в ней состоит в конструировании нового объекта путем использования существующего в качестве образца; новый объект ведет себя как старый за исключением тех особенностей, которые были переопределены. Тем самым можно строить системы на основе прототипов, а не наследования. Хотя у нас нет опыта в этой области, мы полагаем, что создание такой системы позволило бы полнее раскрыть возможности Ruby.
1.4. Динамические аспекты Ruby
Ruby — динамический язык в том смысле, что объекты и классы можно изменять во время выполнения. Ruby позволяет конструировать и интерпретировать фрагменты кода в ходе выполнения статически написанной программы. В нем есть хитроумный API отражения, с помощью которого программа может получать информацию о себе самой. Это позволяет сравнительно легко создавать отладчики, профилировщики и другие подобные инструменты, а также применять нетривиальные способы кодирования.
Наверное, это самая трудная тема для программиста, приступающего к изучению Ruby. В данном разделе мы вкратце рассмотрим некоторые следствия, вытекающие из динамической природы языка.
1.4.1. Кодирование во время выполнения
Мы уже упоминали директивы load и require. Важно понимать, что это не встроенные предложения и не управляющие конструкции; на самом деле это методы. Поэтому их можно вызывать, передавая переменные или выражения как параметры, в том числе условно. Сравните с директивой #include в языках С и C++, которая обрабатывается во время компиляции.
Код можно строить и интерпретировать по частям. В качестве несколько искусственного примера рассмотрим приведенный ниже метод calculate и вызывающий его код:
def calculate(op1, operator, op2)
string = op1.to_s + operator + op2.to_s
# Предполагается, что operator - строка; построим длинную
# строку, состоящую из оператора и операндов.
eval(string) # Вычисляем и возвращаем значение.
end
@alpha = 25
@beta = 12
puts calculate(2, "+",2) # Печатается 4
puts calculate(5, "*", "@alpha") # Печатается 125
puts calculate("@beta", "**", 3) # Печатается 1728
Вот та же идея, доведенная чуть ли не до абсурда: программа запрашивает у пользователя имя метода и одну строку кода. Затем этот метод определяется и вызывается:
puts "Имя метода: "
meth_name = gets
puts "Строка кода: "
code = gets
string = %[def #{meth_name}\n #{code}\n end] # Строим строку.
eval(string) # Определяем метод.
eval(meth_name) # Вызываем метод.
Зачастую необходимо написать программу, которая могла бы работать на разных платформах или при разных условиях, но при этом сохранить общий набор исходных текстов. Для этого в языке С применяются директивы #ifdef, но в Ruby все определения исполняются. Не существует такого понятия, как «этап компиляции»; все конструкции динамические, а не статические. Поэтому для принятия решения такого рода мы можем просто вычислить условие во время выполнения:
if platform == Windows
action1
elsif platform == Linux
action2
else
default_action
end
Конечно, за такое кодирование приходится расплачиваться некоторым снижением производительности, поскольку иногда условие вычисляется много раз. Но рассмотрим следующий пример, который делает практически то же самое, однако весь платформенно-зависимый код помещен в один метод, имя которого от платформы не зависит:
if platform == Windows
def my_action
action1
end
elsif platform == Linux
def my_action
action2
end
else
def my_action
default_action
end
end
Таким способом мы достигаем желаемого результата, но условие вычисляется только один раз. Когда программа вызовет метод my_action, он уже будет правильно определен.
1.4.2. Отражение