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. Отражение
В языках Smalltalk, LISP и Java реализована (с разной степенью полноты) идея рефлексивного программирования — активная среда может опрашивать структуру объектов и расширять либо модифицировать их во время выполнения.
В языке Ruby имеется развитая поддержка отражения, но все же он не заходит так далеко, как Smalltalk, где даже управляющие конструкции являются объектами. В Ruby управляющие конструкции и блоки не представляют собой объекты. (Объект Proc можно использовать для того, чтобы представить блок в виде объекта, но управляющие конструкции объектами не бывают никогда.)
Для определения того, используется ли идентификатор с данным именем, служит ключевое слово defined? (обратите внимание на вопросительный знак в конце слова):
if defined? some_var
puts "some_var = #{some_var}"
else
puts "Переменная some_var неизвестна."
end
Аналогично метод respond_to? выясняет, может ли объект отвечать на вызов указанного метода (то есть определен ли данный метод для данного объекта). Метод respond_to? определен в классе Object.
В Ruby запрос информации о типе во время выполнения поддерживается очень полно. Тип или класс объекта можно определить, воспользовавшись методом type (из класса Object). Метод is_a? сообщает, принадлежит ли объект некоторому классу (включая и его суперклассы); синонимом служит имя kind_of?. Например:
puts "abc".class "" # Печатается String
puts 345.class # Печатается Fixnum
rover = Dog.new
print rover.class # Печатается Dog
if rover.is_a? Dog
puts "Конечно, является."
end
if rover.kind_of? Dog
puts "Да, все еще собака."
end
if rover.is_a? Animal
puts "Да, он к тому же и животное."
end
Можно получить полный список всех методов, которые можно вызвать для данного объекта. Для этого предназначен метод methods из класса Object. Имеются также его варианты private_instance_methods, public_instance_methods и т.д.
Аналогично можно узнать, какие переменные класса или экземпляра ассоциированы с данным объектом. По самой природе ООП в перечни методов и переменных включаются те, что определены как в классе самого объекта, так и во всех его суперклассах. В классе Module имеется метод constants, позволяющий получить список всех констант, определенных в модуле.
В классе Module есть метод ancestors, возвращающий список модулей, включенных в данный модуль. В этот список входит и сам данный модуль, то есть список, возвращаемый вызовом Mod.ancestors, содержит по крайней мере элемент Mod. В этот список входят не только родительские классы (отобранные в силу наследования), но и «родительские» модули (отобранные в силу включения).
В классе Object есть метод superclass, который возвращает суперкласс объекта или nil. Не имеет суперкласса лишь класс Object, и, значит, только для него может быть возвращен nil.
Модуль ObjectSpace применяется для получения доступа к любому «живому» объекту. Метод _idtoref преобразует идентификатор объекта в ссылку на него; можно считать, что это операция, обратная той, что выполняет двоеточие в начале имени. В модуле ObjectSpace есть также итератор each_object, который перебирает все существующие в данный момент объекты, включая и те, о которых иным образом узнать невозможно. (Напомним, что некоторые неизменяемые объекты небольшого размера, например принадлежащие классам Fixnum, NilClass, TrueClass и FalseClass, не хранятся в куче из соображений оптимизации.)
1.4.3. Отсутствующие методы
При вызове метода (myobject.mymethod) Ruby ищет поименованный метод в следующем порядке:
1. Синглетные методы, определенные для объекта myobject.
2. Методы, определенные в классе объекта myobject.
3. Методы, определенные в предках класса объекта myobject.
Если найти метод mymethod не удается, Ruby ищет метод с именем method_missing. Если он определен, то ему передается имя отсутствующего метода (в виде символа) и все переданные ему параметры. Этот механизм можно применять для динамической обработки неизвестных сообщений, посланных во время выполнения.
1.4.4 Сборка мусора