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

Ответ 1

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

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

Ответ 2

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

alt text http://bks0.books.google.com/books?id=jcUbTcr5XWwC&printsec=frontcover&img=1&zoom=5&sig=ACfU3U1rnYKha_p7vEkpPm1Ow3o9RAM0nQ

Блоки и область видимости

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

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

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

hash.each {|key,value; i,j,k| ... }

Ответ 3

В отличие от некоторых других ответов, циклы 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

Не оглавление, но видимость Parsing-visibility

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

При определении того, определена ли локальная переменная (т.е. defined? возвращает true) в любой точке кода, анализатор проверяет текущую область действия, чтобы увидеть, был ли какой-либо код ей назначен раньше, даже если этот код никогда не запускался ( синтаксический анализатор не может ничего знать о том, что было или не выполнялось на этапе синтаксического анализа). "До" означает: на линии выше или на той же линии и на левой стороне.

Упражнение, чтобы определить, определено ли локальное (т.е. видимое)

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

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

  • достигнет начала текущей области (вы должны понимать правила рубинового обзора, чтобы знать, когда это произойдет)
  • код доступа, который присваивает 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 есть undefined!
    • Но когда код достигает случая else, dest_arr снова отображается, потому что интерпретатор видит, что он был определен заранее (2 строки вверх).
    • Обратите внимание также, что dest_arr скрыт только в начале цикла; его ценность никогда не теряется.

Это также объясняет, почему назначение локатора перед циклом while устраняет проблему. Назначение не должно выполняться; он должен появиться только в исходном коде.

Пример Lambda

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, за исключением того, что вместо того, чтобы стать undefined выше кода назначения, метод dest_arr становится "unmasked" / "unshadowed", чтобы метод был вызван без круглых скобок. Но любой код после назначения увидит локальную переменную.

Некоторые лучшие практики, которые мы можем извлечь из всего этого

  • Не называйте локальные переменные такими же, как имена методов в той же области
  • Не помещайте начальное назначение локальной переменной в тело цикла while или for или что-либо, что заставляет выполнение прыгать в пределах области (вызов lambdas или Continuation#call может сделать это тоже). Поместите задание перед циклом.

Ответ 4

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

Ответ 5

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

telemachus ~ $ ruby test.rb < huh 
test.rb:4:in `<main>': undefined method `<<' for nil:NilClass (NoMethodError)

Ответ 6

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

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