Ruby забывает локальные переменные во время цикла while?

StackOverflow https://stackoverflow.com/questions/1654637

  •  11-09-2019
  •  | 
  •  

Вопрос

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

Итак, я написал для этого простую программу, но вижу кое-что, что меня удивляет:Похоже, 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:

альтернативный текст 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

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

  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

Вот небольшой тестовый пример, демонстрирующий странность описанного выше поведения, когда оно происходит в цикле 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
Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top