Что стоит за конкретным идентификатором в данном окружении
Давайте разберемся с программным контекстом в Ruby: какие переменные и другие объекты доступны в конкретном месте программы, и как интерпретатор их ищет? Что обозначает конкретный идентификатор, откуда он берется? Почему отсюда, а не оттуда? И чему, наконец, в этом трижды перекинутом блоке будет равен self?
... ... ...
Ruby очень гибок и позволяет переопределить так много, что, образно выражаясь, вы можете выстрелить себе в ногу из самой этой ноги. Картечью.
Блоки и контекст в 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
?..
Отдельно хотелось бы сказать: несмотря на то, что поведение интерпретатора всегда однозначно и для большинства случаев стабильно от версии к версии, лучше избегать неочевидностей — человеческий мозг не компьютер и может долго «не замечать», что какой-то нужный идентификатор оказался перекрыт другим, или что вместо переменной объекта используется переменная класса, и т.д.
1)Полные тексты примеров — https://gist.github.com/shikhalev/8301163.
2)Шихалев И. Распределенный Ruby. Прозрачный RPC для взаимодействия Ruby-программ // Системный администратор, №12(133), 2013г., — С. 58—61
спасибо автору за прекрасный пост.
ОтветитьУдалить