2015-03-05 1 views
2

Я новичок в Ruby, и ясно увидеть и найти в Интернете, что следующий сбой:Почему ruby ​​array.delete_at внутри array.each не удается?

arr = [10, 20, 30, 40]  
arr.each.with_index do |elmt, i| 
    print "#{elmt}, #{i}, " 
    arr.delete_at(i) if elmt == 20 
    puts arr.length 
end 

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

10, 0, 4 
20, 1, 3 
40, 2, 3 
=> [10, 30, 40] 
+1

Изменение 'Array' (или вообще любой' Enumerable') при итерации по нему вызывает неопределенное поведение (по крайней мере, в MRI). [Cite] (https://groups.google.com/d/msg/ruby-core-google/wYAJdScsv9w/C-Yy9CgP9yIJ). Matz (создатель Ruby): «Неопределенное поведение итераторов, если блок изменяет итерационный объект, который может разбиться или упасть до бесконечного цикла ». –

+0

@Holger хорошая информация. Хотелось бы, чтобы это было в документации для соответствующих методов. Разве я дошел до того, что назвал это ошибкой в ​​документе? – user3546411

ответ

1

лампочка !!

Кажется, что delete_at сразу корректирует базовую структуру данных, перемещая все элементы справа от удаленного элемента. Массив больше похож на связанный список, чем на массив. Таким образом, «следующий» элемент (здесь «30») теперь arr [1] (который уже обработан итератором), и когда итератор увеличивается до arr [2], он видит «40». arr [3] возвращает nil, и puts, кажется, ничего не делает с nil.

+1

'arr [3]' будет возвращать нуль, но если вы добавите другой оператор puts, вы увидите, что итерация никогда не достигает 'arr [3]', потому что длина массива меньше 4 к моменту времени выполнения Ruby доходит до этого. Я сделал версию вашего кода, который печатает индексы по мере запуска: https://gist.github.com/DavidEGrayson/31250dfa0a0dd8b443ed –

+0

@David Right - печать индексов делает его более понятным. Я уточню вопрос. Интересный момент относительно времени выполнения, видя длину в верхней части блока и не входя в блок для последнего цикла. – user3546411

+0

Эй, ИМО, это будет понятно для всех, кто знаком с тем, как работают уступчивые ценности от себя к блокам. Я имею в виду, что, уступая блоку, он не должен знать, изменился ли он сам или нет, просто переходит к блоку, что на самом деле [i] в ​​данный момент при каждом уроне. Но я считаю, что многие новички будут игнорировать это, а другие просто будут использовать 'Array # reject',' Array # select' и 'Array # delete_if'. – limekin

1

посмотреть на: https://github.com/ruby/ruby/blob/ca6b174078fa15f33655be704d9409fdbc4f9929/include/ruby/intern.h

https://github.com/ruby/ruby/blob/ca6b174078fa15f33655be704d9409fdbc4f9929/enumerator.c

https://github.com/ruby/ruby/blob/ca6b174078fa15f33655be704d9409fdbc4f9929/array.c

каждый дает нумератор и with_index работает с интервьюером.

Когда вы достигаете элемента 20, вы его распечатываете, а после этого вы удаляете его, и в этот момент Ruby эффективно сдвигает все элементы массива. Теперь переписчик, который опирается на массив подхватывает следующий элемент, который 40, потому что все получили сдвинуты вниз (30 был скопирован 20, 40 был скопирован 30 и массив был изменен)

взглянуть на: https://github.com/ruby/ruby/blob/ca6b174078fa15f33655be704d9409fdbc4f9929/array.c#L3023 Здесь происходит волшебство перемещения элементов через memmove.

+0

Это все очень интересно, потому что вам нужно понять, что происходит под капотом, чтобы узнать, каковы последствия для производительности. Алгоритм, который делает много удалений в большом массиве во вложенном цикле, будет иметь большой коэффициент производительности (вероятно, O (log (n)), используя этот метод. Я надеюсь что-то вроде array.keep_if, которое может обрабатывать все дело сразу, будет реализовано более эффективно, но хммм ... возможно, нет? Помимо взгляда на исходный код интерпретатора есть ли книга или веб-сайт, на котором обсуждаются эта диада реализации и производительности? – user3546411

+0

Ruby - не самая быстрая вещь Сила/красота Ruby происходит от его выразительности, а не от скорости. Я редко видел код ruby, который удаляет элементы в массиве. Обычно существует вариация преобразований в структурах данных, причем эти преобразования связаны цепью в функциональном стиле. Если вам нужно/хотите, чтобы вы сохраняли окончательный результат этих преобразований - чаще всего это быстрее/дешевле, чем изменять исходную структуру. – Mircea

+0

в целом, как правило, большого пальца (и th применяется к каждому языку), вы не должны изменять коллекцию во время ее повторения - это одна из причин, по которой вам не разрешено это делать. другие языки более строги (поднимая ошибки/исключения, когда это происходит), другие языки более расслаблены с предостережением о неожиданных результатах, подобных этому. – Mircea

1

Давайте шаг через него:

arr = [10, 20, 30, 40] 
enum0 = arr.each 
    #=> #<Enumerator: [10, 20, 30, 40]:each> 
enum1 = enum0.with_index 
    #=> #<Enumerator: #<Enumerator: [10, 20, 30, 40]:each>:with_index> 

Мы можем увидеть содержимое enum1 путем преобразования его в массив:

enum1.to_a 
    #=> [[10, 0], [20, 1], [30, 2], [40, 3]] 

Это говорит нам о том, что Enumerator#each (который будет вызывать Array#each) будет проходить четыре элемента перечислителя enum1 в блок, присваивая их по очереди блочным переменным. Первый:

elmt, i = enum1.next 
    #=> [10, 0] 
puts elmt, i 
    # 10 
    # 0 
elmt == 20 
    #=> false 

так arr.delete_at(i) не выполняется.

Ни arr, ни enum1 были изменены:

arr 
    #=> [10, 20, 30, 40] 
enum1.to_a 
    #=> [[10, 0], [20, 1], [30, 2], [40, 3]] 

each Сейчас проходит следующий элемент enum1 в блок:

elmt, i = enum1.next 
    #=> [20, 1] 
elmt == 20 
    #=> true 

так мы выполняем:

arr.delete_at(i) 
    #=> [10, 20, 30, 40].delete_at(1) 
    #=> 20 
arr 
    #=> [10, 30, 40] 
enum1.to_a 
    #=> [[10, 0], [30, 1], [40, 2]] 

Ач ! Таким образом, счетчик был изменен, а также arr. Это имеет смысл, потому что, когда создается счетчик, устанавливается ссылка на исходный приемник вместе с правилами для того, что с ним нужно делать. Поэтому изменения в ресивере будут влиять на счетчик.

Мы можем использовать Enumerator#peek, чтобы посмотреть, что будет следующий элемент enum1, что each будет проходить в блок:

enum1.peek 
    #=> [40, 2] 

Таким образом, вы видите, что each переходит к следующему индексированной позиции, не обращая внимания на то, что предыдущий элемент удален, что приводит к тому, что более поздние элементы сдвигаются на одну позицию, в результате чего each пропускает [30,1].

elmt, i = enum1.next 
    #=> [40, 2] 
elmt == 20 
    #=> false 
arr 
    #=> [10, 30, 40] 
enum1.to_a 
    #=> [[10, 0], [30, 1], [40, 2]] 

На данный момент each достигает конца счетчику, так что работа закончена. Поэтому возвращает оригинальный приемник, arr, но который был изменен, так что мы получаем:

[10, 30, 40] 

Лучший пример может быть:

arr = [10, 20, 20, 40] 

где:

[10, 20, 40] 

будет вернулся.