Iterowanie po Array

Temat wydawałoby się banalny, ale niech mi ktoś szybko powie jaki będzie wynik następującego kodu:

a = [1,2,3] a.each{|e| p e; a.delete(e) if e == 1}
Domyślam się, że tak jest w specu Rubiego, ale dla mnie sensu to nie ma.

Ciekawe, że np. modyfikacja tablicy nie powoduje błędu, ale hasza już tak:

[code=ruby]a = [1,2,3]
a.each{|e| a << e } #pętla nieskończona
e = {1 => 2, 3 => 4}
e.each{|k,v| e[5] = 6}

RuntimeError: can’t add a new key into hash during iteration
from (irb):18:in []=' from (irb):18:inblock in irb_binding’
from (irb):18:in `each’
from (irb):18[/code]
Piszę o tym dlatego, że sam implementuję kolekcję, która ma się zachowywać jak tablica i wolałbym w trakcie iteracji rzucać wyjątek przy jej modyfikacji, ale będzie to niezgodne z tym jak zachowuje się klasa Array.

ruby-1.9.2-p290 :007 > a = [1,2,3] => [1, 2, 3] ruby-1.9.2-p290 :008 > a.each{|e| p e; a.delete(e) if e > 1} 1 2 => [1, 3]
Modyfikowanie Array w trakcie .each jest zupełnie bez sensu. Zrób .delete_if, .reject i .reject! i rzucaj wyjątki przy kasowaniu w each, moim zdaniem.

Śmiało rzucaj wyjątek. Nie sądzę, żeby kod, którego działanie zależy od takiego właśnie zachowania klasy Array, nie mógł sobie z wyjątkiem poradzić (albo modyfikuje tę kolekcję w trakcie iterowania nieświadomie, i wtedy dobrze rzucić wyjątkiem, albo robi to świadomie, ale wtedy robi to z dość specyficznego powodu).

Takie rozwiązanie przyjąłem - oczywiści chodziło mi o sytuację, kiedy robię to nieświadomie. Zastanawia mnie jednak niekonsekwencja w implementacji Array i Hash w samym Rubim. Przypuszczalnie pochodzi z czasów 1.8, kiedy Hash nie był uporządkowany, co nie zmienia faktu, że rzucanie wyjątku w obu przypadkach byłoby sensowniejsz.

Jeśli chcesz podrążyć temat (niestety przed październikiem nie bardzo mam czas) – Sprawdź RubySpec i zapytaj na ruby-core czy to planowe zachowanie, czy detal implementacyjny MRI. :slight_smile:

Niestety w Ruby spec nie znalazłem nic na ten temat.

Niestety w RubySpec nie znalazłem nic na ten temat.[/quote]
No właśnie ja też nie:

https://github.com/rubyspec/rubyspec/blob/master/core/array/each_spec.rb
https://github.com/rubyspec/rubyspec/blob/master/core/array/shared/enumeratorize.rb
https://github.com/rubyspec/rubyspec/blob/master/shared/enumerator/each.rb

Nie ma też na oko niczego w testach MRI:

Zatem to albo jeszcze nieotestowane zachowanie, albo zachowanie nieokreślone. Zapytaj na ruby-core. :slight_smile:

Wydaje mi się że nie powinieneś usuwać elementów tablicy z poziomu each(). Może nie ma tego w ruby spec, ale nie jest do zgodne z duchem w jakim zostały zaimplementowane kolekcje w języku Ruby. W językach funkcyjnych, z których Ruby czerpie w tym momencie pełnymi garściami, zawsze zwracana jest zmodyfikowana tablica. Ruby będąc mniej ortodoksyjnym a bardziej pragmatycznym, pozwala to robić, ale ja, widząc tego typu kod, natychmiast nakazałbym przepisanie tego fragmentu w inny sposób.

Wszyscy się z tym zgadzamy. aphollo nie pyta, czy powinno się to robić (nie powinno się), tylko jak powinna zachowywać się implementacja #each dla obiektów, które mają być możliwie podobne do Array, kiedy ktoś to mimo wszystko zrobi – czy powinny się zachowywać tak jak Array, czy nie muszą (moja teza jest, że nie muszą, bo zachowanie Array#each przy równoczesnej modyfikacji tablicy jest nieokreślone, nawet jeśli MRI zachowuje się w jakiś konkretny sposób).

Oczywiście przytoczony fragment kodu nie jest zanadto sensowny (ten w pierwszym poście). Ale scenariusz użycia tej kolekcji jest dużo mniej oczywisty:

Segment.find_all_by_form(form).each do |segment| segment.form = downcase_form segment.store end
Na pierwszy rzut oka nie ma tu żadnej modyfikacji kolekcji, prawda? Dzieje się to dlatego, że segment.store powoduje zaktualizowanie indeksów dla “form”, a przez to również kolekcji, która jest zwrócona przez find_all_by_form.
Rozwiązanie bez rzucania wyjątku mogłoby polegać na każdorazowym tworzeniu kopii tej kolekcji (przy find_all_by…), ale to z kolei kłóci się z założeniami biblioteki, która powinna działać maksymalnie szybko (niepotrzebny narzut na kopiowanie) oraz oszczędzać pamięć.
W tej sytacji użytkownik sam musi sobie sklonować tę kolekcję o ile chce dokonywać takich modyfikacji. W domyślnym scenariuszu jednak kolekcja nie jest kopiowana.