
Что стоит за конкретным идентификатором в данном окружении
Давайте разберемся с программным контекстом в Ruby: какие переменные и другие объекты доступны в конкретном месте
программы, и как интерпретатор их ищет? Что обозначает конкретный идентификатор, откуда он берется? Почему отсюда,
а не оттуда? И чему, наконец, в этом трижды перекинутом блоке будет равен self
?
Ruby очень гибок и позволяет переопределить так много, что, образно выражаясь, вы можете выстрелить себе в ногу из самой этой ноги. Картечью.
В программировании, неважно на каком языке, есть такое понятие — контекст выполнения — если мы не работаем исключительно с глобальными переменными, важно понимать, какие локальные объекты доступны и задействованы в каждой конкретной точке программы. Это достаточно просто для понимания, хотя и важно, в случае объектно-ориентированных языков, дизайн которых направлен на то, чтобы максимально изолироваться от глобального окружения и работать внутри одного объекта; и несколько сложнее, но еще более важно, в случаях, когда язык поддерживает замыкания — по сути вынесение кода вместе с его контекстом куда-то в другое место.
На самом деле, никакой особой магии (по крайней мере, в случае Ruby) тут нет, и правила, определяющие работу с контекстом,
довольно просты, а главное — логичны. Однако их надо знать и понимать очень четко, поскольку вариантов использования очень
много, а кроме того, в языке есть способы переопределить поведение по умолчанию. Кроме того, блоки, образующие замыкания,
в Ruby очень удобны и используются постоянно. При этом переменные не требуют отдельного объявления (подобного var
в других
языках), а определяются в момент инициализации — первого присваивания значения. Все это может привести к недопониманию
и кажущейся неоднозначности.
Из чего состоит контекст?
В Ruby в любой точке программы мы имеем доступ к трем слоям контекста: локальный контекст, контекст объекта и глобальный. Рассмотрим их, так сказать, сверху вниз — от глобального к локальному.
В глобальном контексте, строго говоря, находятся только глобальные переменные — это те, имена которых начинаются с символа
«$
». Однако, мы же можем обращаться к другим элементам — константам, методам — находясь как бы в чисто глобальном окружении —
непосредственно в тексте исходного файла вне всяких class
и def
? Можем, но только потому, что на самом деле находимся
в неявном безымянном методе неявного объекта main
. А «глобальные» константы и методы на самом деле принадлежат классу Object
,
к которому относится и main
(поскольку от этого класса наследуются все остальные, его элементы и доступны в любом контексте).
Строго говоря, начиная с Ruby 1.9, это не совсем так — существует класс BasicObject
, являющийся не наследником,
а предком Object
. Если мы для каких-то целей унаследуемся непосредственно от него, то внезапно обнаружим, что нам очень мало,
чего доступно. Но так делать имеет смысл только в очень специфических задачах, на грани «хака».
Контекст объекта позволяет нам обращаться к его методам и константам класса без указания самого объекта, а также к его
переменным экземпляра с префиксом «@
» и переменным класса с «@@
». Сам же текущий объект мы всегда можем получить
посредством ключевого слова «self
».
Наконец, локальный контекст — это все локальные переменные заданные выше по тексту в рамках текущего метода.
Одна из особенностей Ruby — то, что принадлежность идентификатора тому или иному контексту, как правило, можно определить
и не просматривая снизу вверх области видимости — глобальные переменные, переменные экземпляра и класса отличаются префиксами,
имена констант всегда начинаются с большой буквы, а локальных переменных — с маленькой. Некоторую сумятицу вносят только
методы — обладая именами, как у локальных переменных, они принадлежат контексту объекта. Тут действует простое правило:
присваивание создает переменную и перекрывает имя метода. Тем не менее, к нему по прежнему можно обратится посредством
«self.‹имя›
». Стоит заметить, что присваивание всегда создает переменную, даже если у нас ранее определен атрибут,
доступный для присваивания. Т.е. в ситуации1:
class Alpha
attr_accessor :alpha
def beta
self.alpha = 1
alpha = 2
end
end
Атрибут после вызова beta
будет равен единице, поскольку строчка без self
к нему отношения не имеет.
Блоки
Блоком в Ruby называется конструкция вида:
‹вызов метода› do |‹аргументы›|
‹какие-то действия›
end
или же:
‹вызов метода› { |‹аргументы›| ‹какие-то действия› }
Это две равнозначные формы записи одного и того же. Вторая обычно используется, когда блок умещается в одну строку. Вызываемый метод частью блока не является, но необходим — каким-либо другим способом блоки не используются.
Блок в Ruby — очень часто используемая конструкция языка, одна из определяющих, если так можно выразиться, ruby-way.
Выглядит в реальности это примерно так (выводим элементы массива):
[1, 2, 3].each do |item|
puts item
end
или так (преобразуем массив в массив строк):
strs = [1, 2, 3].map { |i| i.to_s }
С другой стороны — со стороны метода — блок может быть вызван посредством ключевого слова «yield
»:
def do_smth
if block_given?
yield self
end
end
Здесь проверка block_given?
нужна, чтобы определить, а был ли собственно передан блок, или метод вызывали без него.
Другой вариант — это объявить специальный параметр, который в теле метода волшебным образом превратится в объект класса
Proc
(и его уже можно будет не только вызвать непосредственно, но и сохранить в переменную или передать в другой метод):
def do_smth_else &block
@smth = block
do_smth &block
end
Если же при вызове блок не будет передан, параметр будет равен nil
.
В рамках разговора о контекстах важно, что блок образует замыкание, т.е. несмотря на то, что выполняться он будет где-то там в глубинах вызванного метода, а то и вовсе — будет сохранен, а затем вызван уже совсем в другое время, в блоке можно обращаться к локальным переменным, доступным в месте его объявления. Но при этом блок еще и образует собственный контекст: имена его формальных параметров, а так же переменные, впервые инициализированные внутри блока, снаружи не доступны. Рассмотрим такой пример:
a = 'INIT'
def alpha
a ||= :a
p [:alpha, a]
end
define_singleton_method :beta do
a ||= :b
p [:beta, a]
end
a = 'TEST'
alpha
beta
Использованный здесь оператор «||=
» выполняет присваивание в том случае, если переменная слева от него логически ложна
(равна false
или nil
), или не определена. Приведенный код должен дать следующий вывод:
$ ruby demo02.rb
[:alpha, :a]
[:beta, "TEST"]
Как видим, в методе, определенном через «def
», внешняя переменная не видна, а вот метод, созданный из блока, ее видит,
поскольку она попала в замыкание. Если же мы закомментируем первую строчку примера, то на момент определения beta
переменная
существовать не будет, соответственно, в замыкание не попадет, и результат будет следующий:
[:alpha, :a]
[:beta, :b]
Несмотря на то, что присваивание строки «TEST» никуда не делось, оно уже не имеет отношения к той переменной a
, которая
расположена в локальном контексте блока.
Формальные аргументы блока всегда относятся исключительно к его контексту, даже если их имена совпадают с внешними переменными. В старых версиях Ruby, по 1.8.7 включительно, параметры блока не были изолированы, что вызывало множество нареканий.
Например, код:
a = 'A'
b = 'B'
2.times do |a|
b = a
p [a, b]
end
p [a, b]
Выдаст следующее:
$ ruby demo03.rb
[0, 0]
[1, 1]
["A", 1]
Здесь можно видеть, что переменная a
изолирована в блоке, тогда как b
— нет.
Что же касается контекста объекта, то он, как и локальный, попадает в замыкание, т.е. соответствует месту объявления блока,
если метод, которому передан блок, не подразумевает иное (как, в частности, define_singleton_method
). К методам, изменяющим
контекст, мы еще вернемся, а сейчас рассмотрим подробнее контекст объекта как таковой.
Контекст объекта
Как уже говорилось выше, в Ruby мы всегда действуем в контексте некоего объекта, причем доступные методы полностью определяются
его классом. Но, в общем случае, это не тот класс, который был использован при создании объекта и возвращается методом class
,
а «персональный» класс, присущий только данному объекту и никому более — наследник его «номинального» класса. Чтобы получить этот
«персональный» класс, используется метод singleton_class
.
Пример, демонстрирующий вышесказанное:
class Alpha
def alpha
end
end
a = Alpha.new
a.define_singleton_method :beta do
end
p a
p [a.class, a.class.instance_methods(false)]
p [a.singleton_class,
a.singleton_class.instance_methods(false)]
p a.class.ancestors
p a.singleton_class.ancestors
В результате должен получиться примерно такой вывод:
$ ruby demo04.rb
♯‹Alpha:0x0000000175c140›
[Alpha, [:alpha]]
[♯‹Class:♯‹Alpha:0x0000000175c140››, [:beta]]
[Alpha, Object, Kernel, BasicObject]
[Alpha, Object, Kernel, BasicObject]
В общем случае при вызове метода происходит его поиск сначала в «персональном» классе объекта, а затем в классах и модулях,
список которых выдается методом ancestors
— именно в том порядке, в каком они перечислены. Если оставить одни классы,
получится цепочка наследования, а модули там появляются путем «подмешивания» (в английской терминологии — «mixin») методом
include
(другой вариант добавления «примесей» — extend
— полностью соответствует include
, выполненному для синглтон-класса).
В примере выше можно видеть модуль Kernel
, подмешанный в класс Object
.
Что касается переменных, то в данном контексте имеются, во-первых, переменные объекта, чьи имена начинаются с символа «@
».
С ними все просто, поскольку они принадлежат конкретному экземпляру и больше ниоткуда не доступны. Есть, правда, еще методы
instance_variable_get
, _set
и т.д., но, будучи, как и всякие методы, применяемы к конкретному объекту, они не вносят
дополнительной путаницы.
Несколько интересней с переменными класса — это те, чьи имена начинаются с «@@
». Во-первых, их следовало бы назвать
переменными модуля, поскольку в модулях они ведут себя так же, как и в классах. Во-вторых, они наследуются, т.е. если где-то
в цепочке ancestors
уже была объявлена переменная с таким именем, будет использоваться именно она, а не создана новая
для текущего класса. И это довольно важный момент, поскольку при сложном многоуровневом наследовании одноименные переменные
могут появиться и случайно — тут надо быть внимательным. Наконец, в третьих, эти переменные трактуются по разному, когда
используются в контексте обычного объекта — они считаются относящимися к его классу, и в контексте модуля или класса
(а это ведь с точки зрения Ruby тоже объект) — тогда они относятся непосредственно к нему.
Небольшой пример, где мы инициализируем переменную в контексте класса, изменяем ее в контексте экземпляра этого класса, а затем еще раз изменяем в контексте класса-наследника:
class Alpha
@@alpha = 'A'
def Alpha.alpha
@@alpha
end
def set_alpha x
@@alpha = x
end
end
a = Alpha.new
a.set_alpha 'X'
p Alpha.alpha
class Beta < Alpha
@@alpha = 'B'
end
p Alpha.alpha
Во всех случаях мы имеем дело с одной и той же переменной и, соответственно, получаем ожидаемый вывод:
$ ruby demo06.rb
"X"
"B"
Константы и пространства имен
Константы в Ruby отличаются от всего остального заглавной первой буквой. Имена классов и модулей — это тоже константы,
значением которых является соответствующий объект класса Class
или Module
.
Константы в чем-то подобны переменным класса, только доступны снаружи (посредством «::
»), и повторное присваивание
им значения выдает предупреждение. Есть и еще два существенных отличия.
Первое — если в классе-предке и классе-потомке имеются одноименные константы, то это разные константы, т.е. переопределение задает новую константу для потомка, а не затирает значение в предке. Второй же момент — это то, что к поиску «по предкам» добавляется такая вещь как пространства имен.
В принципе, пространства имен в Ruby понять достаточно просто: имена классов и модулей представляют собой константы, при этом классы и модули сами могут содержать константы, в том числе, правильно, другие классы и модули. Выглядит это примерно так:
class Alpha
class Beta
end
end
b = Alpha::Beta.new
Так вот, если где-то в классе Beta
обратиться к константе, после собственного класса, интерпретатор будет ее искать во внешнем
классе — Alpha
. И даже более того — такой вложенный поиск более приоритетен, чем поиск по цепочке наследования. Немного
парадоксальный пример:
class Alpha
ALPHA = 'A'
end
module Beta
ALPHA = 'B'
class Gamma < Alpha
def Gamma.alpha
ALPHA
end
end
end
p Beta::Gamma.alpha
p Beta::Gamma::ALPHA
Выдаст, невзирая на здравый смысл, два разных значения:
$ ruby demo07.rb
"B"
"A"
Т.е. в первом случае найдена ближайшая внешняя константа, а во втором — ближайшая унаследованная… Что с этим делать?
Могу порекомендовать только одно: в сложных случаях не рассчитывать на определенное поведение интерпретатора — оно-то
определено и стабильно (вышеприведенный код я проверил на версиях 1.8, 1.9, 2.0 и 2.1), но не всегда очевидно разработчику
и зависит от способов обращения, которые в течении жизненного цикла кода могут изменяться. В общем, при малейшем подозрении
на неоднозначность, лучше прописывать явно полный идентификатор со всеми «::
». Кстати, к именам верхнего уровня, никуда
не вложенным, можно обратиться так: «::Object
» или «::Kernel
» — это всегда будет работать правильно, что бы ни было
одноименное определено в том контексте, где находится вызов. Ну и, конечно, не стоит злоупотреблять пространствами имен
и переопределением уже использованных идентификаторов. Как и любыми другими возможностями языка: Ruby очень гибок и позволяет
переопределить так много, что, образно выражаясь, вы можете выстрелить себе в ногу из самой этой ноги. Картечью.
Замена контекста
Локальный контекст, равно как и контекст объекта, может быть указан явно. Для этого существует несколько разнородных техник, о которых мы сейчас и поговорим.
Начнем с простого и прозрачного — явного указания объекта. Для этой цели служат методы instance_eval
и instance_exec
,
немного различающиеся между собой синтаксисом. Они позволяют выполнить блок в контексте заданного объекта. При этом блок
остается замыканием, т.е. локальный контекст он захватывает свой. Пример:
class Alpha
attr_accessor :alpha
end
alpha = 'a'
x = 'x'
a = Alpha.new
a.alpha = 'A'
a.instance_eval do
p [alpha, x, self]
self.alpha = x
end
p [alpha, x, a]
Выдаст примерно следующее:
$ ruby demo08.rb
["a", "x", ♯‹Alpha:0x0000000107bb50 @alpha="A"›]
["a", "x", ♯‹Alpha:0x0000000107bb50 @alpha="x"›]
А если мы перенесем присвоение значения переменной alpha
в строчку сразу за блоком, то получим:
["A", "x", ♯‹Alpha:0x0000000213bad0 @alpha="A"›]
["a", "x", ♯‹Alpha:0x0000000213bad0 @alpha="x"›]
Таким образом видно, что идентификатор сначала ищется в замыкании, а если его там нет — в методах объекта.
Для классов и модулей есть методы module_eval
и module_exec
(существуют также методы class_eval
и class_exec
,
являющиеся полными синонимами module_xxx
.), которые отличаются от instance
-методов семантикой определения методов.
Внутри instance_eval
конструкция «def
» определяет синглтон-метод, независимо от того, является ли объект модулем/классом,
или нет; в случае module_eval
она определяет метод экземпляра. То есть:
class Alpha
end
Alpha.instance_eval do
def alpha
end
end
Alpha.module_eval do
def beta
end
end
p [Alpha.methods(false),
Alpha.instance_methods(false)]
Нам покажет:
$ ruby demo09.rb
[[:alpha], [:beta]]
Схожим образом формируется контекст при определении методов из блоков посредством define_method
, или define_singleton_method
.
И это зачастую очень удобный способ создавать методы, опирающиеся на замыкания. Как-то так:
class Alpha
def name_method name
define_singleton_method name do
"name: #{name}"
end
end
end
a = Alpha.new
a.name_method :alpha
a.name_method :beta
p [a.alpha, a.beta]
С результатом:
$ ruby demo10.rb
["name: alpha", "name: beta"]
Что же касается локального контекста, с ним сложнее. Нельзя, скажем, взять и выполнить блок в чужом локальном контексте,
однако можно сохранить некий контекст и выполнить в нем код, представленный в виде строки. Для этого используется метод
binding
, возвращающий объект класса Binding
. Выглядит это примерно так:
def get_binding
local = 100
return binding
end
b = get_binding
b.eval 'p local'
Если быть точным, то объект класса Binding
хранит не только локальный контекст, но и объектный — это полный контекст
в той точке, где был вызван метод binding
. С учетом того, что выполнение кода из строки — процесс довольно медленный
(по сравнению с нормальным, предварительно разобранным кодом), использовать эту технику как-либо, кроме как в отладке,
наверное, не стоит. С другой стороны, локальный контекст на то и локальный, чтобы не заботиться о нем снаружи.
Замыкания и многозадачность
Если мы пишем многопоточную программу, надо помнить, что замыкания содержат переменные, а не их значения. Т.е. значения могут поменяться со времени старта потока.
a = :start
t = Thread.new do
sleep 1
Thread.exclusive { p a }
end
a = :continue
t.join
Выдаст :continue
, а не :start
. И не забываем оборачивать обращения к внешним переменным в блок метода exclusive
во избежание конфликтов. В данном примере, конечно, можно без него обойтись, но только потому, что ничего полезного
в нем и не делается.
Другая картина в случае, если мы захотим использовать дочерний процесс — в этом случае мы получим полную копию всего, включая глобальный контекст, и никаких общих переменных — для обмена данными между процессами используются уже совсем другие средства, например, dRuby2. Т.е.:
$a = :start
fork do
sleep 1
p $a
end
$a = :continue
Process.wait
Выдаст нам все-таки :start
, именно это значение будет скопировано в момент форка вместе со всем остальным.
Вообще говоря, сказанное в этом разделе довольно очевидно для программистов, представляющих себе управление потоками и процессами как таковое, однако скриптовые языки, и Ruby в частности, нередко используют люди, в недавнем прошлом далекие от программирования… А задач, которые можно распараллелить — множество, тем более, что в Ruby это очень просто.
Итого
Надеюсь, понимание вышеизложенного поможет избегать ошибок при написании программ. Впрочем, еще важнее это понимание
при чтении чужих исходников — чтобы не возникало вопросов: а что у нас тут обозначает этот идентификатор, откуда он берется?
А почему именно отсюда, а не оттуда? И чему, наконец, в этом трижды перекинутом между разными методами блоке будет равен self
?..
Отдельно хотелось бы сказать: несмотря на то, что поведение интерпретатора всегда однозначно и для большинства случаев стабильно от версии к версии, лучше избегать неочевидностей — человеческий мозг не компьютер и может долго «не замечать», что какой-то нужный идентификатор оказался перекрыт другим, или что вместо переменной объекта используется переменная класса, и т.д.
-
Полные тексты примеров — https://gist.github.com/shikhalev/8301163. ↩
-
Шихалев И. Распределенный Ruby. Прозрачный RPC для взаимодействия Ruby-программ // Системный администратор, №12(133), 2013г., — С. 58—61 ↩