В отличие от некоторых других ответов, петли while
фактически не создают новую область видимости. Проблема, которую вы видите, более тонкая.
Чтобы показать контраст, блоки, переданные в вызов метода DO , создают новую область видимости, так что вновь назначенная локальная переменная внутри блока исчезает после выхода из блока:
### block example - provided for contrast only ###
[0].each {|e| blockvar = e }
p blockvar # NameError: undefined local variable or method
Но while
петли (как и у вас) разные:
arr = [0]
while arr.any?
whilevar = arr.shift
end
p whilevar # prints 0
Причина, по которой вы получаете ошибку в вашем случае, заключается в том, что строка, которая использует message
:
puts "#{message}"
появляется перед любым кодом, который присваивает message
.
Это та же самая причина, по которой этот код вызывает ошибку, если a
не был определен заранее:
# Note the single (not double) equal sign.
# At first glance it looks like this should print '1',
# because the 'a' is assigned before (time-wise) the puts.
puts a if a = 1
Не видимость, а видимость при разборе
Так называемая "проблема" - то есть локальная переменная видимость в пределах одной области видимости - связана с синтаксическим анализатором ruby . Поскольку мы рассматриваем только одну область действия, правила области видимости не имеют отношения к проблеме . На этапе синтаксического анализа анализатор решает, в каких исходных местоположениях видна локальная переменная, и эта видимость не изменяется во время выполнения.
При определении, определена ли локальная переменная (т. Е. defined?
возвращает true) в любой точке кода, синтаксический анализатор проверяет текущую область видимости, чтобы увидеть, назначал ли ее какой-либо код раньше, даже если этот код никогда не выполнялся синтаксический анализатор не может ничего знать о том, что выполнялось или не выполнялось на этапе анализа). Значение «До»: на линии выше или на той же строке слева.
Упражнение, чтобы определить, определен ли локальный (то есть видимый)
Обратите внимание, что следующее применимо только к локальным переменным, а не к методам. (Определение того, определен ли метод в области действия, является более сложным, поскольку включает в себя поиск включенных модулей и классов-предков.)
Конкретный способ увидеть поведение локальной переменной - открыть файл в текстовом редакторе. Предположим также, что несколько раз нажав клавишу со стрелкой влево, вы можете переместить курсор назад по всему файлу. Теперь предположим, что вам интересно, повлечет ли определенное использование message
NameError
. Для этого наведите курсор на то место, где вы используете message
, затем продолжайте нажимать стрелку влево, пока вы не выполните одно из следующих действий:
- достичь начала текущей области (вы должны понимать правила области видимости ruby, чтобы знать, когда это произойдет)
- код доступа, который присваивает
message
Если вы достигли назначения до достижения границы области, это означает, что использование message
не повысит NameError
. Если вы не достигнете какого-либо назначения, использование увеличит NameError
.
Другие соображения
В случае, если в коде указано присвоение переменной, но она не запущена, переменная инициализируется как nil
:
# a is not defined before this
if false
# never executed, but makes the binding defined/visible to the else case
a = 1
else
p a # prints nil
end
В то время как цикл проверки цикла
Вот небольшой тестовый пример, демонстрирующий странность описанного выше поведения, когда оно происходит в цикле while. Уязвимая переменная здесь dest_arr
.
arr = [0,1]
while n = arr.shift
p( n: n, dest_arr_defined: (defined? dest_arr) )
if n == 0
dest_arr = [n]
else
dest_arr << n
p( dest_arr: dest_arr )
end
end
который выводит:
{:n=>0, :dest_arr_defined=>nil}
{:n=>1, :dest_arr_defined=>nil}
{:dest_arr=>[0, 1]}
Значимые пункты:
- Первая итерация интуитивно понятна,
dest_arr
инициализируется как [0]
.
- Но нам нужно обратить пристальное внимание на второй итерации (когда
n
равно 1
):
- В начале
dest_arr
не определено!
- Но когда код достигает случая
else
, dest_arr
снова виден, потому что интерпретатор видит, что он был определен заранее (2 строки вверх).
- Также обратите внимание, что
dest_arr
только скрыто в начале цикла; его значение никогда не теряется.
Это также объясняет, почему назначение вашей локальной сети до цикла while
решает проблему. Назначение не должно быть выполнено; это только должно появиться в исходном коде.
Пример лямбды
f1 = ->{ f2 }
f2 = ->{ f1 }
p f2.call()
# Fails because the body of f1 tries to access f2 before an assignment for f2 was seen by the parser.
p f1.call() # undefined local variable or method `f2'.
Исправьте это, поместив назначение f2
перед телом f1
. Помните, что задание на самом деле не нужно выполнять!
f2 = nil # Could be replaced by: if false; f2 = nil; end
f1 = ->{ f2 }
f2 = ->{ f1 }
p f2.call()
p f1.call() # ok
Метод маскировки Гоча
Вещи становятся действительно опасными, если у вас есть локальная переменная с тем же именем, что и у метода:
def dest_arr
:whoops
end
arr = [0,1]
while n = arr.shift
p( n: n, dest_arr: dest_arr )
if n == 0
dest_arr = [n]
else
dest_arr << n
p( dest_arr: dest_arr )
end
end
Выходы:
{:n=>0, :dest_arr=>:whoops}
{:n=>1, :dest_arr=>:whoops}
{:dest_arr=>[0, 1]}
Присвоение локальной переменной в области видимости будет «маскировать» / «тень» вызов метода с тем же именем. (Вы по-прежнему можете вызывать метод, используя явные скобки или явный получатель.) Таким образом, это похоже на предыдущий тест цикла while
, за исключением того, что вместо того, чтобы становиться неопределенным над кодом назначения, метод dest_arr
становится "немаскированным" / "не затененным", так что метод можно вызывать без скобок. Но любой код после присваивания увидит локальную переменную.
Некоторые передовые практики, которые мы можем извлечь из всего этого
- Не называйте локальные переменные так же, как имена методов в той же области действия
- Не помещайте начальное присвоение локальной переменной в тело цикла
while
или for
, или что-либо, что заставляет выполнение перескочить в пределах области (вызов лямбды или Continuation#call
может сделать это тоже ). Поместите назначение перед циклом.