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