Статьи

Понимание области применения в Ruby

Сфера действия перекрывающихся букв Векторный Icon

Сфера — это важная вещь для понимания не только в Ruby, но и на любом языке программирования. В те времена, когда я начинал писать на Ruby, более половины ошибок, которые были брошены на меня, были результатом недостаточного понимания этой концепции. Такие вещи, как переменные не определены, неправильные назначения переменных и так далее. Все в результате непонимания сферы достаточно хорошо. Вам не нужно проходить через все эти головные боли! Надеюсь, эта статья поможет вам избежать многих ошибок, которые я сделал.

Что такое сфера, точно?

Когда кто-то упоминает область, подумайте о двух словах: переменные и видимость. , Это довольно просто: сфера — это то, где что-то видно. Все дело в том, что (переменные, константы, методы) доступно вам в данный момент. Если вы достаточно хорошо понимаете область действия, вы сможете указать на любую строку вашей программы на Ruby и сказать, какие переменные доступны в этом контексте и, что более важно, какие нет.

Так какой смысл ограничивать видимость переменных в первую очередь? Почему бы не иметь все в любом месте? Жизнь была бы намного проще, не так ли? Ну не совсем…

Программисты не согласны со многими вещами (использование функционального или объектно-ориентированного подхода, использование различных шаблонов имен переменных и т. Д.). Сфера не является одним из них. Тем более, что люди получают больше опыта в программировании, они становятся его сильными сторонниками. Это почему?

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

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

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

Ruby Variable Scope: краткий справочник

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

  • Переменная класса (@@ a_variable): Доступно из определения класса и любых подклассов. Не доступно нигде снаружи.
  • Переменная экземпляра (@a_variable): доступна только в пределах конкретного объекта для всех методов в экземпляре класса. Не доступно непосредственно из определений классов.
  • Глобальная переменная ($ a_variable): доступна везде в вашем скрипте Ruby.
  • Локальная переменная (a_variable): зависит от области видимости. Вы будете работать с ними чаще всего и, таким образом, столкнетесь с большинством проблем, потому что их объем зависит от разных вещей.

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

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

Когда в области находится локальная переменная?

Вам не нужно объявлять переменные экземпляра в Ruby. Вы можете поместить что-то вроде @anything_goes_here в определение метода, и в результате вы получите nil . Теперь попробуйте удалить этот @ в начале (таким образом, превратив переменную в локальную), и вы получите NameError ( undefined local variable or method ).

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

 if false # the code below will not run a = 'hello' # the interpreter saw this, so the local var. is in scope from now on end pa # nil, since that code didn't execute, thus the variable wasn't initialized 

Попробуйте запустить этот код как есть, а затем удалите a = 'hello' и запустите его снова, чтобы посмотреть, что произойдет.

Конфликты именования локальных переменных

Предположим, у вас есть этот код:

 def something 'hello' end p something ==> hello something= 'Ruby' p something ==> Ruby #'hello' is not printed 

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

Если у вас есть инициализируемая локальная переменная и вызов метода с тем же именем в той же области видимости, локальная переменная будет «затенять» метод и иметь приоритет. Это не означает, что метод ушел и не может быть доступен вообще. Вы можете легко получить к нему доступ, добавив круглые скобки в конце (с something() ) или добавив явный self.something перед ним (с self.something ). Взглянем:

 def some_var; 'I am a method'; end public :some_var # Because all methods defined at the top level are private by default some_var = 'I am a variable' p some_var # I am a variable p some_var() # I am a method p self.some_var # I am a method 

Полезное упражнение для определения, находится ли переменная вне области видимости

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

  1. Достигните начала вашей области (блок def / class / module / do-end)
  2. Достигните кода, который выполняет присваивание этой локальной переменной.

Если вы достигнете 1) до 2), вы, вероятно, столкнетесь с NameError в вашем коде. Если вы дойдете до 2) до 1), то поздравляю.

Локальные и переменные экземпляра

Переменные экземпляра связаны с конкретным объектом. Пока вы находитесь в этом объекте, у вас есть доступ к ним. Локальные переменные, в отличие от переменных экземпляра, связаны с определенной областью действия. Пока вы находитесь в этой области, вы будете иметь к ним доступ. Переменные экземпляра меняются и заменяются каждым новым объектом. Локальные переменные меняются и заменяются при каждой новой области видимости. Как узнать, изменился ли объем? Два слова: сфера ворот.

Сфера Гейтс: основная концепция для понимания сферы

Как вы думаете, что происходит, когда вы:

  1. Определить класс (с class SomeClass )
  2. Определить модуль (с module SomeModule )
  3. Определить метод (с помощью def some_method )?

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

Каждое определение метода / модуля / класса известно как область видимости , потому что создается новая область. Старая область больше не доступна для вас, и все переменные, доступные в ней, заменяются новыми.

Если это сбивает с толку, не волнуйтесь. Вот пример, который поможет вам лучше понять концепцию:

 v0 = 0 class SomeClass # Scope gate v1 = 1 p local_variables # As the name says, it gives you all local variables in scope def some_method # Scope gate v2 = 2 p local_variables end # end of def scope gate end # end of class scope gate some_class = SomeClass.new some_class.some_method 

Вы увидите, как [:v1] и [:v2] выводятся на консоль. Что случилось с переменной v1 когда программа вошла в def some_method ? Область видимости класса была заменена областью метода экземпляра, вводя в набор новый набор переменных (в данном случае это всего лишь одна).

Как насчет v0 ? Это нигде не появилось! Итак, в тот момент, когда мы вошли в class SomeClass , область видимости верхнего уровня ( v0 определена на верхнем уровне) также была заменена. Когда я говорю «заменили», я имею в виду временно, а не постоянно. Попробуйте добавить p local_variables в конце этой программы после some_class.some_method и вы увидите v0 прямо в списке.

Сломай врата!

Определения модуля / метода / класса, как мы видели ранее, ограничивают видимость переменных. Если у вас есть локальные переменные в классе, когда новый класс определен в классе, локальные переменные в классе больше не доступны, как мы видели. Что, если вы хотите иметь доступ к этим переменным, несмотря на определение метода? Как вы можете «сломать» эти ворота?

Ключ к этому прост: замените область видимости вызовами методов. В частности, заменить:

  • определения классов с помощью Class.new
  • определения модуля с помощью Module.new
  • определения методов с define_method

Давайте возьмем приведенный выше код (имена переменных, все одинаковые) и просто заменим ворота областей вызовами методов:

 v0 = 0 SomeClass = Class.new do v1 = 1 p local_variables define_method(:some_method) do v2 = 2 p local_variables end end some_class = SomeClass.new some_class.some_method 

После этого вы увидите две напечатанные строки: [:v1, :v0, :some_class] и [:v2, :v1, :v0, :some_class] . Мы успешно взломали все ворота и сделали доступными внешние переменные. Этот результат стал возможен благодаря силе блоков, которые мы рассмотрим ниже.

Являются ли блоки воротами?

Вы можете подумать, что блоки также являются воротами прицела. В конце концов, они вводят новую область видимости с объявленными внутри переменными, к которым вы не можете получить доступ снаружи, как в этом примере:

 sample_list = [1,2,3] hi = '123' sample_list.each do |item| # the block scope begins here puts hi # will this print 123 or produce an error? hello = 'hello' # declaring and initializing a variable end p hello # undefined local variable or method "hello" 

Как вы можете видеть с помощью «hello», переменные внутри определенного блока являются локальными для этого блока и больше нигде не доступны.

Если бы блоки были воротами области, то при установке puts hi возникнет ошибка, потому что переменная hi находится в отдельной области видимости. Тем не менее, это не так, и вы можете увидеть это, запустив приведенный выше код.

Вы можете не только получить доступ к внешним переменным, но и изменить их содержимое! Попробуйте поместить hi = '456' в do/end и его содержимое будет изменено.

Что если вы не хотите, чтобы блоки модифицировали внешние переменные? Блочные локальные переменные могут помочь. Чтобы определить локальные переменные блока, поставьте точку с запятой в конце параметров блока (в приведенном ниже блоке есть только 1 параметр, i ), а затем просто перечислите их:

 hi = 'hi' hello ='hello' 3.times do |i; hi, hello| pi hi = 'hi again' hello = 'hello again' end p hi # "hi" p hello # "hello" 

Если вы удалите ; hi, hello ; hi, hello часть ; hi, hello , вы получите «снова снова» и «снова привет» как новое содержимое двух переменных.

Помните, что в тот момент, когда вы начинаете блок с помощью do и заканчиваете его end , вы вводите новую область действия:

 [1,2,3].select do |item| # do is here, new scope is being introduced # some code end 

Замените select на each , map , detect или любой другой метод. Тот факт, что используется блок (когда вы видите do/end ), означает, что введена новая область.

Некоторые причуды с блоками и областями действия

Попробуйте угадать, что будет печатать этот код Ruby:

 2.times do i ||= 1 print "#{i} " i += 1 print "#{i} " end 

Вы ожидали 1 2 2 2 ? Ответ: 1 2 1 2 . Каждая итерация, использующая times является новым определением блока, которое сбрасывает локальные переменные внутри него. В этом случае у нас есть 2 итерации, поэтому, когда начинается вторая, i снова сбрасываюсь на 1.

Как вы думаете, этот код Ruby напечатает (последняя строка):

 def foo x = 1 lambda { x } end x = 2 p foo.call 

Ответ 1. Причина этого в том, что блоки и блочные объекты (процедуры, лямбды) видят область видимости в своем определении, а не в вызове. Это связано с тем, что в Ruby они рассматриваются как замыкания. Закрытие — это просто код, содержащий поведение, которое может:

  • передаваться как объект (который можно вызвать позже)
  • помните переменные, которые находились в области видимости при определении замыкания (лямбда в этом примере).

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

 def increase_by(i) start = 0 lambda { start += i } end increase = increase_by(3) start = 453534534 # won't affect anything p increase.call # 3 p increase.call # 6 

Вы также можете отложить модификацию переменной с помощью лямбды:

 i = 0 a_lambda = lambda do i = 3 end pi # 0 a_lambda.call pi # 3 

Как вы думаете, будет напечатана последняя строка:

 a = 1 ld = lambda { a } a = 2 p ld.call 

Если бы ваш ответ был 1, вы бы ошиблись. Будет напечатано 2. Но подождите, разве лямбда / процесс не видит область действия в своем определении? Это правда, и если вы подумаете об этом, a = 2 также находится в области определения. Только в первый раз, когда вызывается лямбда, она вычисляет значения переменных в своем определении, как вы можете видеть в этом примере. Незнание этого факта может привести к потенциально трудным для отслеживания ошибкам в вашем коде.

Как два метода могут использовать одну и ту же переменную?

Как только мы узнаем, как взломать ворота, мы сможем использовать эти знания, чтобы сделать некоторые удивительные вещи. Я узнал об этой концепции из книги Metaprogramming Ruby, которая очень помогла мне понять, как работает область видимости. Во всяком случае, вот код:

 def let_us_define_methods shared_variable = 0 Kernel.send(:define_method, :increase_var) do shared_variable += 1 end Kernel.send(:define_method, :decrease_var) do shared_variable -= 1 end end let_us_define_methods # methods defined now! p increase_var # 1 p increase_var # 2 p decrease_var # 1 

Довольно аккуратно, а?

Сфера верхнего уровня

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

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

 p self # main p self.class # Object 

Где я?

Часто при отладке многие головные боли решаются, если вы знаете текущую ценность self . Текущее значение self влияет на переменные экземпляра, а также на методы без явного получателя. Если вы получили ошибку с undefined method/instance variable которая, как вы уверены, была определена (потому что вы можете видеть это в своем коде), то у вас, вероятно, есть отслеживание ошибок self .

Небольшое упражнение: что мне доступно?

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

 class SomeClass b = 'hello' @@m = 'hi' def initialize @some_var = 1 c = 'hi' end def some_method sleep 1000 a = 'hello' end end some_object = SomeClass.new some_object.some_method 

Попробуй остановиться на sleep 1000 . Что ты видишь? Какие переменные доступны вам в данный момент? Попробуйте найти ответы, прежде чем продолжить. Ваш ответ должен содержать не только переменные, но и причины, по которым они доступны.

Как мы уже упоминали ранее, локальные переменные ограничены областью действия. Определение some_method является воротами области, заменяющими всю предыдущую область и начинающими новую. В новой области видимость переменной является единственной доступной локальной переменной.

Переменные экземпляра, как мы упоминали ранее, являются self , и в этом случае some_object является текущим экземпляром, а @some_var доступен для всех связанных с ним методов, включая some_method . Переменные класса похожи, и @mm также будет доступен в области видимости. Локальные переменные b и c будут вне досягаемости из-за области видимости. Если вы хотите, чтобы они были доступны повсеместно, обратитесь к разделу о взломе ворот.

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