Ruby забывает локальные переменные во время цикла while? - PullRequest
15 голосов
/ 31 октября 2009

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

Итак, я создал простую программу для этого, но я вижу нечто, что меня удивляет: похоже, Руби забывает, что существуют локальные переменные - или я обнаружил ошибку в программировании? [хотя я не думаю, что имею: если я определяю переменную 'message' перед циклом, я не вижу ошибки].

Вот упрощенный пример с примерами входных данных и сообщения об ошибке в комментариях:

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)
#

Ответы [ 6 ]

29 голосов
/ 31 октября 2009

Из языка программирования Ruby:

альтернативный текст http://bks0.books.google.com/books?id=jcUbTcr5XWwC&printsec=frontcover&img=1&zoom=5&sig=ACfU3U1rnYKha_p7vEkpPm1Ow3o9RAM0nQ

Блоки и переменная область действия

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

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

Ruby 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 является параметром блока: он получает значение, когда блок вызывается с yield. у - локальная переменная блока Он не получает никакого значения от вызова yield, но имеет значение nil до тех пор, пока блок фактически не назначит ему другое значение. Смысл объявления этих блочно-локальных переменных состоит в том, чтобы гарантировать, что вы случайно не забьете значение некоторой существующей переменной. (Это может произойти, если блок вырезан и вставлен, например, из одного метода в другой.) Если вы вызываете Ruby 1.9 с параметром -w, он предупредит вас, если локальная переменная блока затеняет существующую переменную.

Блоки могут иметь более одного параметра и, конечно, более одной локальной переменной. Вот блок с двумя параметрами и тремя локальными переменными:

hash.each {|key,value; i,j,k| ... }
12 голосов
/ 23 января 2014

В отличие от некоторых других ответов, петли 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, затем продолжайте нажимать стрелку влево, пока вы не выполните одно из следующих действий:

  1. достичь начала текущей области (вы должны понимать правила области видимости ruby, чтобы знать, когда это произойдет)
  2. код доступа, который присваивает 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 может сделать это тоже ). Поместите назначение перед циклом.
8 голосов
/ 31 октября 2009

Я думаю, это потому, что сообщение определено внутри цикла. В конце цикла итерация «сообщение» выходит за рамки. Определение «сообщения» вне цикла останавливает выход переменной из области видимости в конце каждой итерации цикла. Поэтому я думаю, что у вас есть правильный ответ.

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

2 голосов
/ 31 октября 2009

Я не уверен, почему вы удивлены: в строке 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). В противном случае, если ваша первая строка не не соответствует hello, вы в конечном итоге попытаетесь добавить line к nil - и вы получите эту ошибку:

telemachus ~ $ ruby test.rb < huh 
test.rb:4:in `<main>': undefined method `<<' for nil:NilClass (NoMethodError)
2 голосов
/ 31 октября 2009

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

0 голосов
/ 20 января 2011

Вы можете просто сделать это:

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
...