Возможно, больше, чем что-либо другое, мне пришлось уточнить мои предыдущие предположения и ментальные модели в отношении масштаба. После прохождения курса RB120 в Launch School я получил более целостное представление о масштабах и их важности.

Обратите внимание, что эта статья не является введением в область применения. Я думаю, что стоит сначала развить интуицию масштаба посредством практики и экспериментов, прежде чем перерабатывать эту интуицию в более широкую ментальную модель.

В этой статье мы сосредоточимся на концепции области действия и коснемся ее родственного аналога: разрешения имен. Эти концепции можно анализировать на многих уровнях детализации; Я попытаюсь представить довольно подробную мысленную модель области действия и рассказать, как она применяется к различным типам переменных. Эта статья подходит для тех, кто хочет прояснить или углубить свое понимание основ области видимости. Читатели этой статьи также могут быть ознакомлены с некоторой полезной терминологией, с которой им еще предстоит столкнуться.

Несмотря на то, что я трачу значительное количество времени на размышления над этими темами, я ни в коем случае не являюсь экспертом по масштабам. Поэтому я не стремлюсь к идеальной точности в этой статье и не погружаюсь в более низкоуровневые детали области действия и доступа к переменным. Скорее, эта статья представляет собой накопленный набор ментальных моделей, основанный на ключевых моментах Launch School в отношении охвата, форумов, дискуссий с другими, различных статей и моих собственных взглядов и идей.

Последнее замечание для тех, кто работает в RB130: в этой статье не рассматриваются какие-либо подробности о правилах области действия замыканий. Возможно, я опубликую следующую статью на эту тему.

Что такое переменная область видимости? Как насчет разрешения имен?

Область относится к видимости: какие объекты видны, где в вашей программе. На практике термин «сфера действия» обычно используется в двух разных контекстах:

  1. Что такое «область действия» переменной? Где в программе доступна данная переменная?
  2. Что входит в область применения на определенном этапе программы? Какие видимые переменные, методы и классы доступны в настоящее время?

В некотором смысле эти две интерпретации противоположны. Если переменная находится «в области видимости» в каком-то месте, область действия переменной должна каким-то образом включать это место. В этой статье будут использоваться оба значения объема. На протяжении большей части статьи мы будем уделять больше внимания точке зрения, предлагаемой первой интерпретацией масштаба, и закончим второй интерпретацией.

Разрешение имени (тесно связанное с «поиском») — это процесс, посредством которого некоторая ссылка на «имя» в вашем коде (например, имя переменной) связывается с фактической переменной и ее значением. Точно так же, как разные типы переменных имеют разные правила области видимости, ссылки на эти переменные могут разрешаться по-разному. Обратите внимание, что разрешение имен сильно зависит от области действия, но эти два термина ни в коем случае не являются синонимами.

Процесс разрешения для заданного имени переменной в первую очередь зависит от двух вещей:

  1. Что входит в область действия (можно ли найти доступную переменную?)
  2. Когда происходит разрешение

Я не буду вдаваться в подробности об аспекте когда разрешения имен, потому что это выходит за рамки того, что нужно понимать большинству рубистов. Однако это будет уместно, когда мы подойдем к обсуждению постоянной разрешающей способности, которая заметно отличается от других переменных. Держать крепко.

Область действия и разрешение имен: визуализация

Прежде чем углубляться, давайте пройдемся по визуализации, чтобы получить более четкое представление о двух интерпретациях области действия, а также о разрешении имен.

Представьте себе набор пузырьков, каждый из которых представляет собой «прицел». Некоторые перекрываются, некоторые заключены в другие, некоторые полностью изолированы. Каждый пузырь может содержать некоторые переменные (локальные, экземплярные, классовые и т. д.). Не думайте о пузырьках с точки зрения позиционной структуры вашего кода — хотя структура вашего кода может соответствовать пузырькам, правильнее думать о них с точки зрения окружения: методов, определения классов/модулей, объектов, блоков и т. д.

Теперь считайте себя работающей программой. По мере продвижения вы вводите множество вызовов методов, определений классов и других сред, переходя от одного пузырька к другому и изменяя переменные, которые в настоящее время находятся в вашей области. Объекты объекты, методы, классы, модули и/или блоки, с которыми вы работаете в любой момент, определяют ваш контекст выполнения: широкий термин, описывающий обстоятельства, которые существуют в данный момент в вашем так называемом «рантайме».

В Ruby связанный с областью объект Binding точно описывает этот контекст. В любом заданном контексте у вас (программы) есть объект binding, обозначающий доступные в данный момент переменные и метод. Позже мы обсудим, как меняется привязка во время выполнения программы, а пока поймите, что binding отслеживает текущий:

  • Доступные локальные переменные
  • Receiver или «вызывающий объект» (значение self)

Поскольку binding отслеживает этот так называемый получатель, он неявно предоставляет нам доступ к переменным экземпляра и класса в этом получателе, тем самым определяя переменные экземпляра и переменные класса в области видимости.

Попробуйте угадать, что выводится на репрезентативном примере ниже. Не беспокойтесь, если вы еще не можете — к концу этой статьи вы, вероятно, сможете. Отказ от ответственности: пузырьковая диаграмма не является иллюстрацией приведенного ниже фрагмента кода.

Скажем, вы хотите получить переменную и запросить локальную переменную a внутри вашего пузыря (пузырей). Вот где вступает в игру разрешение имени. Как правило, обращение к имени будет пытаться найти соответствующую переменную в этих пузырьках.

Константы не совсем подходят под эту аналогию, потому что вся работа с их областью действия и разрешением выполняется еще до того, как будет введено первое облачко — до того, как начнется выполнение программы.

Почему масштаб имеет значение?

Чтобы понять, почему область действия имеет значение, рассмотрим опыт работы программы без правил области действия или разрешения: все будет доступно везде. Программы и их переменные будут работать в одном гигантском пузыре. Это может не быть проблемой для крохотных скриптов, но для чего-либо значительного размера это имеет огромные последствия для:

  1. Именование: без области действия нам пришлось бы называть все уникальным образом, иначе Ruby не знал бы, к какой переменной/методу/классу обращаться. Благодаря области действия переменные с одинаковыми именами могут существовать в программе без конфликтов имен.
  2. Защита и безопасность данных. Область ограничивает случайный или преднамеренный доступ к переменным в разных частях программы.
  3. Объектно-ориентированное программирование: Scope — это основа инкапсулирующих чудес ООП. Без границ области инкапсуляция была бы невозможна. Фактически, ООП вводит типы переменных (переменные экземпляра и класса) с определенными правилами области видимости, предназначенными для инкапсуляции состояния. Для тех, кто в RB120: если основным механизмом инкапсуляции методов является управление доступом к методу, мы можем думать о области видимости как управление доступом к переменным.

Помимо важности области действия в программах, понимание основ области действия даст вам больше контроля и точности над кодом, который вы пишете, и большую ясность над кодом, который вы испортите.

Как Ruby устанавливает область действия переменной?

Мы знаем, что у переменных есть области видимости, но как переменные вообще получают свои области видимости? Я сталкивался с различной семантикой поведения области видимости в Ruby. Независимо от конкретной терминологии, я пришел к выводу, что область видимости переменной определяется тремя способами:

  1. Тип переменной: local_variable, @instance_variable, @@class_variable, CONSTANT или $global_variable?
  2. выполнение контекст (binding) программы при инициализации переменной: вы (программа) можете добавлять новые переменные в кружки, в которых вы в данный момент находитесь. Локальные, классовые и переменные экземпляра используют этот критерий для определения своей области видимости.
  3. Простая структура кода, окружающая переменную, в которой она инициализируется. Это звучит похоже на предыдущий пункт, но немного отличается. Этот тип масштаба зависит исключительно от местоположения. Программе даже не необходимо запускаться для оценки области видимости переменных, как здесь; это можно выяснить заранее. Говорят, что переменная с такой областью видимости имеет чистую лексическую область видимости. Константы используют этот критерий для определения своей области видимости.

Итак, какого пузыря (области действия) является данная переменная часть? Все зависит от ее типа переменной и ее контекста при инициализации.

Кроме того, как только область видимости переменной установлена ​​в Ruby, она не меняется. Переменные не могут перемещать пузырьки. Это называется «статической областью действия», в отличие от «динамической области действия», которая редко используется в современных языках программирования.

Область действия и разрешение: с точки зрения переменной

Теперь мы углубимся в конкретные правила, управляющие областью действия каждого типа переменных, и как осуществляется доступ к этим переменным, когда они необходимы.

Локальные переменные

  • Область: определение класса/модуля, метод или блок, в котором он инициализируется; локальные переменные имеют самую узкую область видимости из всех типов переменных.
  • Решение. Учитывая имя, состоящее только из символов нижнего регистра, в текущей привязке выполняется поиск как локальных переменных, так и методов (например, binding.local_variables).

Обратите внимание, что область действия локальной переменной, инициализированной в методе, распространяется на блоки, которые вызывает метод, но область действия локальной переменной, инициализированной в классе, модуле или на верхнем уровне, не распространяется на вложенные классы/модули/методы.

Примеры:

Существует четыре разных local_variable, каждый с отдельной областью действия. Обратите внимание на герметичность прицелов; например, local_variable, инициализированный в модуле A, недоступен из класса B.

Область действия local_variable1 находится в любом месте вызова метода, включая блок, переданный each. local_variable2 ограничивается только блоком.

Переменные экземпляра

  • Область действия. В Ruby весь код выполняется в контексте некоторого вызывающего объекта, также известного как «получатель». Каждый вызов метода имеет некоторый получатель: получатель метода экземпляра либо определен явно: receiver.method; в противном случае подразумевается — получатель method без каких-либо предваряющих его слов — self. Дело в том, что при вызове любого метода выполняется определенный вызывающий объект. Если переменная экземпляра инициализируется при выполнении объекта, она ограничивается этим объектом.
  • Решение. Имя, начинающееся с @, сигнализирует Ruby о необходимости поиска переменной экземпляра в области выполняемого в данный момент объекта: это можно представить как self или binding.receiver.

Примеры:

self хранит текущий исполняемый объект. В данном случае это объект main (объект верхнего уровня в Ruby). Поэтому область @instance_variable является основным объектом. instance_method определяется в main, поэтому вызывающий объект по-прежнему находится main в instance_method, что делает @instance_variable доступным.

При вызове print_name в строке 12 @name еще не находится в области действия person, поскольку его еще предстоит инициализировать. В Ruby ссылка на неинициализированную переменную экземпляра дает nil.

При построении каждого объекта Person две отдельные переменные экземпляра @name ограничиваются соответствующими объектами.

Несмотря на то, что он занимает модуль, Nameable#print_name вызывается для объекта person, поэтому вызов Nameable#print_name имеет доступ к person переменным экземпляра, включая @name.

Person::print_name вызывается для класса Person, поэтому областью внутри этого вызова метода является класс Person. @name ограничен person, а не Person.

Переменные класса

  • Область действия. Переменные класса можно рассматривать как глобальные переменные в контексте классов. Переменная класса, инициализированная в любом месте определения класса, имеет область, включающую класс, в котором она определена, любые экземпляры этого класса, любые подклассы этого класса и любые экземпляры этих подклассов — действительно довольно широкую область.
  • Решение. Имя с префиксом @@ будет искать переменную класса в области видимости класса. Из-за их широких правил области действия только одна общая копия переменной класса с некоторым именем может существовать среди класса и всех его подклассов. Следовательно, если @@class_variable инициализируется в суперклассе, ссылки на него в любых подклассах разрешатся в одну и ту же переменную.

Несмотря на то, что Ruby сканирует (анализирует) класс перед выполнением, фактическое выполнение определений класса и создание переменных класса происходит во время выполнения — это означает, что переменные класса являются динамическими объектами во время выполнения. исполнение.

Примеры:

Несмотря на то, что @@type инициализируется внутри объекта, область его действия ограничена классом Person и доступна в любом месте Person или подклассах Person.

Обратите внимание, что переменные класса могут быть определены и доступны как на уровне класса, так и на уровне объекта.

Определение класса Runner в строках 9–11 переопределяет значение одной переменной класса @@type с областью видимости Person и Runner. Это дурацкое поведение отговаривает рубистов от использования переменных класса с наследованием, вместо этого используя переменные экземпляра или константы.

Константы (постоянные переменные)

Константы — это то, где все становится весело. Они могут показаться неважными по сравнению с другими типами переменных, пока вы не узнаете, что имена модулей и классов сами по себе являются константами, а на имена модулей и классов ссылаются все время. Поскольку константы должны быть статическими объектами, которые не изменяются во время выполнения, Ruby присваивает им некоторые специальные правила области видимости и разрешения.

  • Область. Технически константы доступны откуда. Как это ни парадоксально, это не означает, что их охват есть везде. Область видимости константы состоит только из лексической (позиционной) области видимости, в которой она объявлена, — окружающего модуля или определения класса. Константы доступны из-за пределов их области действия с помощью метода разрешения пространства имен, обсуждаемого ниже.
  • Разрешение. В отличие от других переменных, константы разрешаются перед выполнением, до любого понятия «контекст выполнения». Это может показаться странным, но Ruby достигает этого с помощью пошаговой процедуры после сканирования постоянных ссылок (любых имен, начинающихся с заглавной буквы). Для каждой ссылки на константу, которая необходима во время выполнения, выполняется следующая последовательность шагов:
  1. Поиск осуществляется в непосредственной лексической области, окружающей имя, начиная с самой внутренней лексической области, продвигаясь наружу (исключая верхний уровень, который не считается частью лексической области).
  2. Просматривается иерархия наследования самого внутреннего в данный момент открытого класса/модуля.
  3. Выполняется поиск на верхнем уровне (любые константы, определенные вне определения класса/метода, на «верхнем уровне»).

Обратите внимание, что эти шаги служат концептуальной моделью поиска констант; фактический процесс добавляет еще много технических деталей и причуд. Основная идея состоит в том, что постоянное разрешение должно использовать набор критериев, совершенно отличный от других типов переменных, из-за когда оно происходит.

Наконец, для имен констант, предваряемых пространством имен (namespace::CONSTANT), процесс разрешения констант сначала перенаправляется в пространство имен (определение класса/модуля) — как будто само имя константы находится внутри него.

Примеры:

По ссылке на CONSTANT в строке 9 Ruby ищет непосредственную лексическую область видимости, класс Person, а затем следующую включающую лексическую область видимости, модуль Earth, где находится Earth::CONSTANT.

Чтобы действительно понять, чем отличаются константы, раскомментируйте строку 8: выводится сообщение об ошибке о «динамическом присвоении констант». Методы выполняются во время выполнения, но Ruby хочет оценить все константы до выполнения и не может этого сделать, если константе присваивается значение в методе во время выполнения. Назначение констант в определениях классов/модулей — это нормально, потому что эти определения анализируются перед выполнением.

Руби делает вывод, что Constantable является предком Person. Поэтому Constantable::CONSTANT замечен в иерархии наследования окружающего класса Person.

Несмотря на то, что областью действия вызова print_constant является объект Person во время выполнения, процесс разрешения констант не имеет значения, поскольку значения констант определяются перед выполнением. В строке 3 не найдено ни CONSTANT ни в лексической области видимости, ни в иерархии наследования объемлющего модуля, Constantable, ни на верхнем уровне.

Оператор пространства имен :: в строке 3 перенаправляет Ruby в другое место (в данном случае класс self) для поиска CONSTANT. Кроме того, в отличие от переменных класса, область действия константы, инициализированной в классе, не распространяется на его подклассы. Таким образом, отдельные константы с одинаковыми именами могут существовать отдельно в одной иерархии наследования, что, как правило, хорошо.

Полное понимание постоянного поиска выходит далеко за рамки потребностей большинства разработчиков, но иметь общее представление о нем полезно.

Глобальные переменные

Я не буду подробно обсуждать здесь глобальные переменные, потому что их использование широко считается злоупотреблением служебным положением, а правила их определения довольно просты: областью действия глобальной переменной является вся программа.

Область действия и разрешение имен: с точки зрения программы

Возвращаясь к нашим двум интерпретациям термина «область охвата», мы уделим время рассмотрению второй интерпретации. Поскольку эти две интерпретации являются противоположными (в некотором роде), этот раздел предоставит альтернативную точку зрения и послужит ментальным подкреплением правил определения области видимости, определенных в предыдущем разделе. Мы сосредоточимся на ответе на следующий вопрос: как переменные становятся в области видимостиво время выполнения программы?

Обратите внимание, что в этом разделе будут игнорироваться константы. Нет смысла спрашивать, находится ли константа «в области видимости в определенный момент выполнения программы» — потому что они разрешаются до выполнения программы!

Простой ответ на поставленный вопрос заключается в том, что значение текущего объекта binding сообщает нам, что находится в области видимости в любой момент времени. Конечно, то, что находится в этой изменчивой привязке, зависит от очень многих вещей — всех правил, которые мы обсуждали в предыдущем разделе!

Итак, как вы (программа) измените, какие переменные вам видны? Короткий ответ: вы перемещаетесь по разным пузырям, просто делая что-то. Из всего, что может помочь вам в переходе, я разделил их на два отдела:

  1. Шлюзы области: ключевые слова, определяющие новую границу области действия для только локальных переменных.
  2. Текущий исполняемый объект («вызывающий объект»): значение self в данной точке.

Сфера Ворота

В Ruby есть набор ключевых слов, которые служат границами области [локальной переменной]: module, class, def и end. Первые три сигнализируют Ruby о выходе из текущей области и входе в новую «внутреннюю» область, а end сообщает Ruby о выходе из «внутренней» области и непосредственном входе во «внешнюю» область. Эти ключевые слова известны как шаблоны области, и понять их довольно просто. Представьте себе некоторые пузырьки со строго определенными поверхностями: эти поверхности являются воротами области видимости.

Приведенный выше пример показывает, что мы можем выяснить, какие локальные переменные доступны в любой точке вашей программы, анализируя ворота области. На практике это может показаться чрезмерным, но это помогает понять истинную природу доступа к локальным переменным.

Вызывающий объект

Во-вторых, текущий исполняемый объект self тесно связан с доступными переменными экземпляра и класса. Чтобы обеспечить грубое приближение, доступны все переменные экземпляра, определенные в текущем self, и все переменные класса, определенные в self или классе/надклассах self.

Ruby предоставляет нам два удобных метода, чтобы увидеть это поведение в действии: Object#instance_variables и Module#class_variables.

Применяются те же правила, что и раньше, только представленные в обратном порядке.

В заключение: область применения сложна

Это погружение могло показаться слишком глубоким для наших целей здесь, в Launch School. А если бы мы захотели, то могли бы нырнуть на 300 метров глубже. Но как программисту эта мысленная модель в сочетании с вашими собственными идеями поможет вам обрести больше уверенности и ясности в отношении того, как правила определения области возникают из того, что могло показаться ничтожным. Теперь вы видите, как область действия связана со своим аналогом — разрешением имен. Вы можете не полностью понять эти ментальные модели и правила после одного, двух или [вставьте конечное число здесь] прочтений, поэтому используйте эту статью в качестве справочного материала, когда она вам понадобится.

Если вы нашли что-то неточное или неясное в этой статье, не стесняйтесь, дайте мне знать, комментируя или отправляя мне сообщение в Slack (@Ethan Weiner).

Я хотел бы поблагодарить нескольких человек (посредством имен Slack), которые помогли вдохновить и критиковать эту статью. Мои бесчисленные учебные сессии с @Ryan DeJonghe помогли мне сформировать ментальные модели, которые легли в основу этой статьи, и его подробный отзыв о статье имел первостепенное значение для ее окончательной версии. У меня была полезная дискуссия с @Fred Durham о двух интерпретациях масштаба, и он также предоставил мне некоторые полезные идеи о моей статье — и о многих других вещах. И последнее, но не менее важное: обсуждение в RB101 с @Joel (Joel Barton) об истинной природе масштаба и его почти профессиональный отзыв о моей статье во многом помогли сделать эту статью целостной. И, конечно же, спасибо Launch School.