Что стоит за конкретным идентификатором в данном окружении
Давайте разберемся с программным контекстом в Ruby: какие переменные и другие объекты доступны в конкретном месте
программы, и как интерпретатор их ищет? Что обозначает конкретный идентификатор, откуда он берется? Почему отсюда,
а не оттуда? И чему, наконец, в этом трижды перекинутом блоке будет равен self
?
Ruby очень гибок и позволяет переопределить так много, что, образно выражаясь, вы можете выстрелить себе в ногу из самой этой ноги. Картечью.
В программировании, неважно на каком языке, есть такое понятие — контекст выполнения — если мы не работаем исключительно с глобальными переменными, важно понимать, какие локальные объекты доступны и задействованы в каждой конкретной точке программы. Это достаточно просто для понимания, хотя и важно, в случае объектно-ориентированных языков, дизайн которых направлен на то, чтобы максимально изолироваться от глобального окружения и работать внутри одного объекта; и несколько сложнее, но еще более важно, в случаях, когда язык поддерживает замыкания — по сути вынесение кода вместе с его контекстом куда-то в другое место.
На самом деле, никакой особой магии (по крайней мере, в случае Ruby) тут нет, и правила, определяющие работу с контекстом,
довольно просты, а главное — логичны. Однако их надо знать и понимать очень четко, поскольку вариантов использования очень
много, а кроме того, в языке есть способы переопределить поведение по умолчанию. Кроме того, блоки, образующие замыкания,
в Ruby очень удобны и используются постоянно. При этом переменные не требуют отдельного объявления (подобного var
в других
языках), а определяются в момент инициализации — первого присваивания значения. Все это может привести к недопониманию
и кажущейся неоднозначности.
Из чего состоит контекст?
В Ruby в любой точке программы мы имеем доступ к трем слоям контекста: локальный контекст, контекст объекта и глобальный. Рассмотрим их, так сказать, сверху вниз — от глобального к локальному.
В глобальном контексте, строго говоря, находятся только глобальные переменные — это те, имена которых начинаются с символа
«$
». Однако, мы же можем обращаться к другим элементам — константам, методам — находясь как бы в чисто глобальном окружении —
непосредственно в тексте исходного файла вне всяких class
и def
? Можем, но только потому, что на самом деле находимся
в неявном безымянном методе неявного объекта main
. А «глобальные» константы и методы на самом деле принадлежат классу Object
,
к которому относится и main
(поскольку от этого класса наследуются все остальные, его элементы и доступны в любом контексте).
Строго говоря, начиная с Ruby 1.9, это не совсем так — существует класс BasicObject
, являющийся не наследником,
а предком Object
. Если мы для каких-то целей унаследуемся непосредственно от него, то внезапно обнаружим, что нам очень мало,
чего доступно. Но так делать имеет смысл только в очень специфических задачах, на грани «хака».
Контекст объекта позволяет нам обращаться к его методам и константам класса без указания самого объекта, а также к его
переменным экземпляра с префиксом «@
» и переменным класса с «@@
». Сам же текущий объект мы всегда можем получить
посредством ключевого слова «self
».
Наконец, локальный контекст — это все локальные переменные заданные выше по тексту в рамках текущего метода.
Одна из особенностей Ruby — то, что принадлежность идентификатора тому или иному контексту, как правило, можно определить
и не просматривая снизу вверх области видимости — глобальные переменные, переменные экземпляра и класса отличаются префиксами,
имена констант всегда начинаются с большой буквы, а локальных переменных — с маленькой. Некоторую сумятицу вносят только
методы — обладая именами, как у локальных переменных, они принадлежат контексту объекта. Тут действует простое правило:
присваивание создает переменную и перекрывает имя метода. Тем не менее, к нему по прежнему можно обратится посредством
«self.‹имя›
». Стоит заметить, что присваивание всегда создает переменную, даже если у нас ранее определен атрибут,
доступный для присваивания. Т.е. в ситуации1:
Атрибут после вызова beta
будет равен единице, поскольку строчка без self
к нему отношения не имеет.
Блоки
Блоком в Ruby называется конструкция вида:
или же:
Это две равнозначные формы записи одного и того же. Вторая обычно используется, когда блок умещается в одну строку. Вызываемый метод частью блока не является, но необходим — каким-либо другим способом блоки не используются.
Блок в Ruby — очень часто используемая конструкция языка, одна из определяющих, если так можно выразиться, ruby-way.
Выглядит в реальности это примерно так (выводим элементы массива):
или так (преобразуем массив в массив строк):
С другой стороны — со стороны метода — блок может быть вызван посредством ключевого слова «yield
»:
Здесь проверка block_given?
нужна, чтобы определить, а был ли собственно передан блок, или метод вызывали без него.
Другой вариант — это объявить специальный параметр, который в теле метода волшебным образом превратится в объект класса
Proc
(и его уже можно будет не только вызвать непосредственно, но и сохранить в переменную или передать в другой метод):
Если же при вызове блок не будет передан, параметр будет равен nil
.
В рамках разговора о контекстах важно, что блок образует замыкание, т.е. несмотря на то, что выполняться он будет где-то там в глубинах вызванного метода, а то и вовсе — будет сохранен, а затем вызван уже совсем в другое время, в блоке можно обращаться к локальным переменным, доступным в месте его объявления. Но при этом блок еще и образует собственный контекст: имена его формальных параметров, а так же переменные, впервые инициализированные внутри блока, снаружи не доступны. Рассмотрим такой пример:
Использованный здесь оператор «||=
» выполняет присваивание в том случае, если переменная слева от него логически ложна
(равна false
или nil
), или не определена. Приведенный код должен дать следующий вывод:
Как видим, в методе, определенном через «def
», внешняя переменная не видна, а вот метод, созданный из блока, ее видит,
поскольку она попала в замыкание. Если же мы закомментируем первую строчку примера, то на момент определения beta
переменная
существовать не будет, соответственно, в замыкание не попадет, и результат будет следующий:
Несмотря на то, что присваивание строки «TEST» никуда не делось, оно уже не имеет отношения к той переменной a
, которая
расположена в локальном контексте блока.
Формальные аргументы блока всегда относятся исключительно к его контексту, даже если их имена совпадают с внешними переменными. В старых версиях Ruby, по 1.8.7 включительно, параметры блока не были изолированы, что вызывало множество нареканий.
Например, код:
Выдаст следующее:
Здесь можно видеть, что переменная a
изолирована в блоке, тогда как b
— нет.
Что же касается контекста объекта, то он, как и локальный, попадает в замыкание, т.е. соответствует месту объявления блока,
если метод, которому передан блок, не подразумевает иное (как, в частности, define_singleton_method
). К методам, изменяющим
контекст, мы еще вернемся, а сейчас рассмотрим подробнее контекст объекта как таковой.
Контекст объекта
Как уже говорилось выше, в Ruby мы всегда действуем в контексте некоего объекта, причем доступные методы полностью определяются
его классом. Но, в общем случае, это не тот класс, который был использован при создании объекта и возвращается методом class
,
а «персональный» класс, присущий только данному объекту и никому более — наследник его «номинального» класса. Чтобы получить этот
«персональный» класс, используется метод singleton_class
.
Пример, демонстрирующий вышесказанное:
В результате должен получиться примерно такой вывод:
В общем случае при вызове метода происходит его поиск сначала в «персональном» классе объекта, а затем в классах и модулях,
список которых выдается методом ancestors
— именно в том порядке, в каком они перечислены. Если оставить одни классы,
получится цепочка наследования, а модули там появляются путем «подмешивания» (в английской терминологии — «mixin») методом
include
(другой вариант добавления «примесей» — extend
— полностью соответствует include
, выполненному для синглтон-класса).
В примере выше можно видеть модуль Kernel
, подмешанный в класс Object
.
Что касается переменных, то в данном контексте имеются, во-первых, переменные объекта, чьи имена начинаются с символа «@
».
С ними все просто, поскольку они принадлежат конкретному экземпляру и больше ниоткуда не доступны. Есть, правда, еще методы
instance_variable_get
, _set
и т.д., но, будучи, как и всякие методы, применяемы к конкретному объекту, они не вносят
дополнительной путаницы.
Несколько интересней с переменными класса — это те, чьи имена начинаются с «@@
». Во-первых, их следовало бы назвать
переменными модуля, поскольку в модулях они ведут себя так же, как и в классах. Во-вторых, они наследуются, т.е. если где-то
в цепочке ancestors
уже была объявлена переменная с таким именем, будет использоваться именно она, а не создана новая
для текущего класса. И это довольно важный момент, поскольку при сложном многоуровневом наследовании одноименные переменные
могут появиться и случайно — тут надо быть внимательным. Наконец, в третьих, эти переменные трактуются по разному, когда
используются в контексте обычного объекта — они считаются относящимися к его классу, и в контексте модуля или класса
(а это ведь с точки зрения Ruby тоже объект) — тогда они относятся непосредственно к нему.
Небольшой пример, где мы инициализируем переменную в контексте класса, изменяем ее в контексте экземпляра этого класса, а затем еще раз изменяем в контексте класса-наследника:
Во всех случаях мы имеем дело с одной и той же переменной и, соответственно, получаем ожидаемый вывод:
Константы и пространства имен
Константы в Ruby отличаются от всего остального заглавной первой буквой. Имена классов и модулей — это тоже константы,
значением которых является соответствующий объект класса Class
или Module
.
Константы в чем-то подобны переменным класса, только доступны снаружи (посредством «::
»), и повторное присваивание
им значения выдает предупреждение. Есть и еще два существенных отличия.
Первое — если в классе-предке и классе-потомке имеются одноименные константы, то это разные константы, т.е. переопределение задает новую константу для потомка, а не затирает значение в предке. Второй же момент — это то, что к поиску «по предкам» добавляется такая вещь как пространства имен.
В принципе, пространства имен в Ruby понять достаточно просто: имена классов и модулей представляют собой константы, при этом классы и модули сами могут содержать константы, в том числе, правильно, другие классы и модули. Выглядит это примерно так:
Так вот, если где-то в классе Beta
обратиться к константе, после собственного класса, интерпретатор будет ее искать во внешнем
классе — Alpha
. И даже более того — такой вложенный поиск более приоритетен, чем поиск по цепочке наследования. Немного
парадоксальный пример:
Выдаст, невзирая на здравый смысл, два разных значения:
Т.е. в первом случае найдена ближайшая внешняя константа, а во втором — ближайшая унаследованная… Что с этим делать?
Могу порекомендовать только одно: в сложных случаях не рассчитывать на определенное поведение интерпретатора — оно-то
определено и стабильно (вышеприведенный код я проверил на версиях 1.8, 1.9, 2.0 и 2.1), но не всегда очевидно разработчику
и зависит от способов обращения, которые в течении жизненного цикла кода могут изменяться. В общем, при малейшем подозрении
на неоднозначность, лучше прописывать явно полный идентификатор со всеми «::
». Кстати, к именам верхнего уровня, никуда
не вложенным, можно обратиться так: «::Object
» или «::Kernel
» — это всегда будет работать правильно, что бы ни было
одноименное определено в том контексте, где находится вызов. Ну и, конечно, не стоит злоупотреблять пространствами имен
и переопределением уже использованных идентификаторов. Как и любыми другими возможностями языка: Ruby очень гибок и позволяет
переопределить так много, что, образно выражаясь, вы можете выстрелить себе в ногу из самой этой ноги. Картечью.
Замена контекста
Локальный контекст, равно как и контекст объекта, может быть указан явно. Для этого существует несколько разнородных техник, о которых мы сейчас и поговорим.
Начнем с простого и прозрачного — явного указания объекта. Для этой цели служат методы instance_eval
и instance_exec
,
немного различающиеся между собой синтаксисом. Они позволяют выполнить блок в контексте заданного объекта. При этом блок
остается замыканием, т.е. локальный контекст он захватывает свой. Пример:
Выдаст примерно следующее:
А если мы перенесем присвоение значения переменной alpha
в строчку сразу за блоком, то получим:
Таким образом видно, что идентификатор сначала ищется в замыкании, а если его там нет — в методах объекта.
Для классов и модулей есть методы module_eval
и module_exec
(существуют также методы class_eval
и class_exec
,
являющиеся полными синонимами module_xxx
.), которые отличаются от instance
-методов семантикой определения методов.
Внутри instance_eval
конструкция «def
» определяет синглтон-метод, независимо от того, является ли объект модулем/классом,
или нет; в случае module_eval
она определяет метод экземпляра. То есть:
Нам покажет:
Схожим образом формируется контекст при определении методов из блоков посредством define_method
, или define_singleton_method
.
И это зачастую очень удобный способ создавать методы, опирающиеся на замыкания. Как-то так:
С результатом:
Что же касается локального контекста, с ним сложнее. Нельзя, скажем, взять и выполнить блок в чужом локальном контексте,
однако можно сохранить некий контекст и выполнить в нем код, представленный в виде строки. Для этого используется метод
binding
, возвращающий объект класса Binding
. Выглядит это примерно так:
Если быть точным, то объект класса Binding
хранит не только локальный контекст, но и объектный — это полный контекст
в той точке, где был вызван метод binding
. С учетом того, что выполнение кода из строки — процесс довольно медленный
(по сравнению с нормальным, предварительно разобранным кодом), использовать эту технику как-либо, кроме как в отладке,
наверное, не стоит. С другой стороны, локальный контекст на то и локальный, чтобы не заботиться о нем снаружи.
Замыкания и многозадачность
Если мы пишем многопоточную программу, надо помнить, что замыкания содержат переменные, а не их значения. Т.е. значения могут поменяться со времени старта потока.
Выдаст :continue
, а не :start
. И не забываем оборачивать обращения к внешним переменным в блок метода exclusive
во избежание конфликтов. В данном примере, конечно, можно без него обойтись, но только потому, что ничего полезного
в нем и не делается.
Другая картина в случае, если мы захотим использовать дочерний процесс — в этом случае мы получим полную копию всего, включая глобальный контекст, и никаких общих переменных — для обмена данными между процессами используются уже совсем другие средства, например, dRuby2. Т.е.:
Выдаст нам все-таки :start
, именно это значение будет скопировано в момент форка вместе со всем остальным.
Вообще говоря, сказанное в этом разделе довольно очевидно для программистов, представляющих себе управление потоками и процессами как таковое, однако скриптовые языки, и Ruby в частности, нередко используют люди, в недавнем прошлом далекие от программирования… А задач, которые можно распараллелить — множество, тем более, что в Ruby это очень просто.
Итого
Надеюсь, понимание вышеизложенного поможет избегать ошибок при написании программ. Впрочем, еще важнее это понимание
при чтении чужих исходников — чтобы не возникало вопросов: а что у нас тут обозначает этот идентификатор, откуда он берется?
А почему именно отсюда, а не оттуда? И чему, наконец, в этом трижды перекинутом между разными методами блоке будет равен self
?..
Отдельно хотелось бы сказать: несмотря на то, что поведение интерпретатора всегда однозначно и для большинства случаев стабильно от версии к версии, лучше избегать неочевидностей — человеческий мозг не компьютер и может долго «не замечать», что какой-то нужный идентификатор оказался перекрыт другим, или что вместо переменной объекта используется переменная класса, и т.д.
-
Полные тексты примеров — https://gist.github.com/shikhalev/8301163. ↩
-
Шихалев И. Распределенный Ruby. Прозрачный RPC для взаимодействия Ruby-программ // Системный администратор, №12(133), 2013г., — С. 58—61 ↩