Ruby забывает локальные переменные во время цикла while?
Вопрос
Я обрабатываю текстовый файл на основе записей:поэтому я ищу начальную строку, которая представляет собой начало записи:маркера конца записи нет, поэтому я использую начало следующей записи, чтобы ограничить последнюю запись.
Итак, я написал для этого простую программу, но вижу кое-что, что меня удивляет:Похоже, Ruby забывает о существовании локальных переменных - или я обнаружил ошибку программирования?[хотя я не думаю, что у меня есть:если я определяю переменную «сообщение» перед циклом, я не вижу ошибки].
Вот упрощенный пример с примером входных данных и сообщением об ошибке в комментариях:
flag=false
# message=nil # this is will prevent the issue.
while line=gets do
if line =~/hello/ then
if flag==true then
puts "#{message}"
end
message=StringIO.new(line);
puts message
flag=true
else
message << line
end
end
# Input File example:
# hello this is a record
# this is also part of the same record
# hello this is a new record
# this is still record 2
# hello this is record 3 etc etc
#
# Error when running: [nb, first iteration is fine]
# <StringIO:0x2e845ac>
# hello
# test.rb:5: undefined local variable or method `message' for main:Object (NameError)
#
Решение
Я думаю, это потому, что сообщение определяется внутри цикла.В конце итерации цикла «сообщение» выходит за рамки.Определение «сообщения» вне цикла предотвращает выход переменной за пределы области видимости в конце каждой итерации цикла.Поэтому я думаю, что у вас есть правильный ответ.
Вы можете выводить значение сообщения в начале каждой итерации цикла, чтобы проверить правильность моего предложения.
Другие советы
Из языка программирования Ruby:
Блоки и область видимости переменных
Блоки определяют новую область видимости переменной:переменные, созданные внутри блока, существуют только внутри этого блока и не определены вне блока.Однако будьте осторожны;локальные переменные в методе доступны для любых блоков внутри этого метода.Таким образом, если блок присваивает значение переменной, которая уже определена вне блока, это не создает новую локальную переменную блока, а вместо этого присваивает новое значение уже существующей переменной.Иногда это именно то поведение, которое нам нужно:
total = 0
data.each {|x| total += x } # Sum the elements of the data array
puts total # Print out that sum
Однако иногда мы не хотим изменять переменные во внешней области, но делаем это непреднамеренно.Эта проблема особенно важна для параметров блоков в Ruby 1.8.В Ruby 1.8, если параметр блока имеет то же имя, что и существующая переменная, то вызовы блока просто присваивают значение этой существующей переменной, а не создают новую локальную переменную блока.Например, следующий код проблематичен, поскольку он использует тот же идентификатор i, что и параметр блока для двух вложенных блоков:
1.upto(10) do |i| # 10 rows
1.upto(10) do |i| # Each has 10 columns
print "#{i} " # Print column number
end
print " ==> Row #{i}\n" # Try to print row number, but get column number
end
Руби 1.9 отличается:Параметры блока всегда локальны для своего блока, и вызовы блока никогда не присваивают значения существующим переменным.Если Ruby 1.9 вызывается с флагом -w, он предупредит вас, если параметр блока имеет то же имя, что и существующая переменная.Это поможет вам избежать написания кода, который работает по-разному в версиях 1.8 и 1.9.
Ruby 1.9 отличается и еще в одном важном отношении.Синтаксис блока был расширен, чтобы вы могли объявлять локальные переменные, которые гарантированно являются локальными, даже если переменная с таким же именем уже существует во внешней области.Для этого после списка параметров блока следует указать точку с запятой и список локальных переменных блока, разделенных запятыми.Вот пример:
x = y = 0 # local variables
1.upto(4) do |x;y| # x and y are local to block
# x and y "shadow" the outer variables
y = x + 1 # Use y as a scratch variable
puts y*y # Prints 4, 9, 16, 25
end
[x,y] # => [0,0]: block does not alter these
В этом коде x является параметром блока:он получает значение, когда блок вызывается с выходом.y — блочная локальная переменная.Он не получает никакого значения от вызова доходности, но имеет значение nil до тех пор, пока блок фактически не присвоит ему какое-либо другое значение.Смысл объявления этих локальных блочных переменных состоит в том, чтобы гарантировать, что вы случайно не забьете значение какой-либо существующей переменной.(Это может произойти, например, если блок вырезается и вставляется из одного метода в другой.) Если вы вызываете Ruby 1.9 с опцией -w, он предупредит вас, если локальная переменная блока затеняет существующую переменную.
Разумеется, блоки могут иметь более одного параметра и более одной локальной переменной.Вот блок с двумя параметрами и тремя локальными переменными:
hash.each {|key,value; i,j,k| ... }
В отличие от некоторых других ответов, while
циклы на самом деле не создают новую область видимости.Проблема, которую вы видите, более тонкая.
Чтобы показать контраст, блоки передаются вызову метода. ДЕЛАТЬ создайте новую область видимости, чтобы вновь назначенная локальная переменная внутри блока исчезала после выхода из блока:
### 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
Вот небольшой тестовый пример, демонстрирующий странность описанного выше поведения, когда оно происходит в цикле 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
тоже могу это сделать).Поместите задание перед циклом.
Почему вы думаете, что это ошибка?Интерпретатор сообщает вам, что сообщение может быть неопределенным при выполнении этого конкретного фрагмента кода.
Не знаю, почему вы удивлены:в строке 5 (при условии, что message = nil
строки нет), вы потенциально можете использовать переменную, о которой интерпретатор никогда раньше не слышал.Переводчик говорит: «Что message
?Это не тот метод, который я знаю, это не известная мне переменная, это не ключевое слово...", а затем вы получаете сообщение об ошибке.
Вот более простой пример, чтобы показать вам, что я имею в виду:
while line = gets do
if line =~ /./ then
puts message # How could this work?
message = line
end
end
Который дает:
telemachus ~ $ ruby test.rb < huh
test.rb:3:in `<main>': undefined local variable or method `message' for main:Object (NameError)
Кроме того, если вы хотите подготовить путь для message
, я бы инициализировал его как message = ''
, так что это строка (а не nil
).В противном случае, если ваша первая строка не делает совпадение, здравствуйте, в конечном итоге вы попытаетесь добавить line
к nil
- что приведет к этой ошибке:
telemachus ~ $ ruby test.rb < huh
test.rb:4:in `<main>': undefined method `<<' for nil:NilClass (NoMethodError)
вы можете просто сделать это:
message=''
while line=gets do
if line =~/hello/ then
# begin a new record
p message unless message == ''
message = String.new(line)
else
message << line
end
end
# hello this is a record
# this is also part of the same record
# hello this is a new record
# this is still record 2
# hello this is record 3 etc etc