Ruby'de Enumerator, bir koleksiyon üzerinde gezinmeyi nesne hâline getirir. each, map ya da each_slice gibi metotları blok vermeden çağırdığınızda çoğu bir Enumerator döndürür; bunu with_index gibi metotlarla zincirleyebilir, next ile eleman eleman ilerleyebilir (dış iterasyon) ya da lazy ile sonsuz dizileri yalnızca gerektiği kadar hesaplayabilirsiniz. Enumerator.produce ise bir başlangıç değerinden adım adım yeni değerler üreten bir enumerator kurar.
Bu yazıda enumerator oluşturmayı, iç ve dış iterasyon farkını, sık kullanılan zincir metotlarını, Enumerator::Lazy ile tembel değerlendirmeyi, Enumerator.produce ve Ruby 4.0'da gelen size: seçeneğini, son olarak kendi sınıfınızda to_enum kullanmayı anlatıyoruz. Örnekler Ruby 4.0 ile çalıştırıldı. Bloklara yabancıysanız önce blok ve yield yazısını okuyun.
Bloksuz çağrı bir Enumerator döndürür

Belgelere göre enumerator Object#to_enum, Object#enum_for ya da Enumerator.new ile oluşturulur. Ama günlük kodda çoğu zaman farkında olmadan oluşturursunuz: bloklu metotların çoğunun iki biçimi vardır ve bloksuz biçim, aynı gezinmeyi saran yeni bir enumerator döndürür. Enumerator Enumerable modülünü içerdiği için map, select, to_a gibi metotların hepsi üzerinde çalışır.
e = [10, 20, 30].each
p e.class # çıktı: Enumerator
p e.size # çıktı: 3
p e.map { |x| x + 1 } # çıktı: [11, 21, 31]with_index, each_with_object, each_slice ve each_cons
Enumerator'ın asıl gücü zincirlemedir. map.with_index(1) yazdığınızda map bloksuz çağrıldığı için enumerator döner, with_index da buna 1'den başlayan bir sıra numarası ekler. each_with_object her elemanla birlikte aynı nesneyi bloğa verir ve sonunda o nesneyi döndürür; sayım ya da gruplama için uygundur. Enumerable#each_slice(n) elemanları n'li ayrık gruplara böler, each_cons(n) ise n'li kayan pencereler üretir.
p %w[a b c].map.with_index(1) { |harf, i| "#{i}.#{harf}" } # çıktı: ["1.a", "2.b", "3.c"]
p %w[a b c].each_with_index.to_a # çıktı: [["a", 0], ["b", 1], ["c", 2]]
p (1..7).each_slice(3).to_a # çıktı: [[1, 2, 3], [4, 5, 6], [7]]
p (1..5).each_cons(2).to_a # çıktı: [[1, 2], [2, 3], [3, 4], [4, 5]]
sayim = %w[elma armut elma].each_with_object(Hash.new(0)) { |m, h| h[m] += 1 }
p sayim # çıktı: {"elma" => 2, "armut" => 1}each_slice toplu işlemlerde, örneğin kayıtları yüzerli paketler hâlinde bir servise göndermek gibi durumlarda kullanışlıdır. Enumerable modülünün diğer metotları için Enumerable: map, select, reduce yazısına bakabilirsiniz.
Dış iterasyon: next, peek ve rewind
Normalde gezinmeyi metot yönetir ve her elemanı bloğa verir; buna iç iterasyon denir. Enumerator ise dış iterasyonu da destekler: next sıradaki elemanı verip konumu ilerletir, peek sıradaki elemanı konumu değiştirmeden gösterir, rewind başa sarar. Sonuna gelindiğinde next bir StopIteration hatası fırlatır. Kernel#loop bu hatayı yakalayıp döngüyü sessizce bitirdiği için, iki enumerator'ı aynı anda ilerletmek kolaylaşır.
e = [1, 2, 3].each
p e.next # çıktı: 1
p e.peek # çıktı: 2
p e.next # çıktı: 2
p e.next # çıktı: 3
begin
e.next
rescue StopIteration => hata
puts "#{hata.class}: #{hata.message}" # çıktı: StopIteration: iteration reached an end
end
e.rewind
p e.next # çıktı: 1
harfler = %w[x y].each
sayilar = [1, 2, 3].each
loop do
puts "#{harfler.next}#{sayilar.next}"
end
# çıktı: x1
# çıktı: y2Enumerator.new ve yielder ile kendi dizinizi üretmek

Enumerator.new'a verdiğiniz blok bir "yielder" nesnesi alır; y << deger (ya da y.yield deger) ile dışarıya değer gönderirsiniz. Blok sonsuz bir döngü içerebilir, çünkü değerler yalnızca istendiği kadar üretilir. Aşağıdaki Fibonacci dizisinde take(10) ilk on değeri alır ve durur.
fib = Enumerator.new do |y|
a, b = 0, 1
loop do
y << a
a, b = b, a + b
end
end
p fib.take(10) # çıktı: [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
p fib.lazy.select(&:even?).first(4) # çıktı: [0, 2, 8, 34]
p fib.size # çıktı: nilsize burada nil döner, çünkü boyut önceden hesaplanamaz. Belgelere göre Enumerator.new'a isteğe bağlı bir boyut argümanı (değer ya da çağrılabilir nesne) verebilirsiniz; ancak Ruby bu değerin doğru olup olmadığını kontrol etmez, bu yüzden size bir ipucu olarak görülmelidir.
Enumerator::Lazy: yalnızca gerektiği kadar hesaplamak
Enumerable#lazy, çoğu Enumerable metodunu yeniden tanımlayan bir Enumerator::Lazy döndürür. Bu metotlar işi hemen yapmaz, yalnızca bir işlem zinciri kurar. Gerçek hesaplama, first ya da to_a (takma adı force) gibi yeniden tanımlanmamış bir metot çağrıldığında başlar ve yalnızca istenen kadar eleman işlenir. Bu sayede sonsuz aralıklarla çalışabilir ve ara diziler oluşturmaktan kaçınabilirsiniz. NEWS dosyalarına göre Enumerator::Lazy Ruby 2.0 ile, zinciri yeniden normal bir enumerator'a çeviren eager metodu ise Ruby 2.7 ile geldi.
sonuc = (1..Float::INFINITY).lazy
.map { |x| x * x }
.select { |x| x % 3 == 0 }
.first(3)
p sonuc # çıktı: [9, 36, 81]
z = (1..20).lazy.select(&:even?)
p z.class # çıktı: Enumerator::Lazy
p z.first(2) # çıktı: [2, 4]
p z.force.size # çıktı: 10
p z.eager.class # çıktı: Enumerator
kontrol = (1..3).lazy.map { |x| puts "işleniyor #{x}"; x * 10 }
p kontrol.first(2)
# çıktı: işleniyor 1
# çıktı: işleniyor 2
# çıktı: [10, 20]Son örnekte üçüncü eleman hiç işlenmez, çünkü yalnızca iki sonuç istendi. Belgelerdeki örnek bunun pratik değerini gösterir: bir URL listesinden veri çekerken lazy kullanırsanız, yeterli sonuç toplandığında kalan adreslere hiç istek atılmaz. Dikkat edilecek bir nokta da şu: normal enumerator'larda bloksuz çağrılabilen map gibi metotlar, lazy enumerator'da blok ister. [1, 2].lazy.map çağrısı ArgumentError: tried to call lazy map without a block hatası verir.
Enumerator.produce ve Ruby 4.0 size seçeneği
Ruby 2.7 NEWS dosyasına göre eklenen Enumerator.produce, verdiğiniz bloğu tekrar tekrar çağırarak sonsuz bir enumerator üretir; her çağrı bir önceki sonucu alır. Başlangıç değeri verirseniz bu değer ilk eleman olur. Blok içinde StopIteration fırlatmak üretimi durdurur. Belgeler bu yöntemi, detect ya da take_while gibi metotlarla birlikte while ve until döngülerine enumerator tabanlı bir alternatif olarak sunar.
p Enumerator.produce(1, &:succ).take(5) # çıktı: [1, 2, 3, 4, 5]
p Enumerator.produce(1) { it * 2 }.take(6) # çıktı: [1, 2, 4, 8, 16, 32]
p Enumerator.produce(1) { it * 2 }.size # çıktı: Infinity
geri = Enumerator.produce(5) { it.zero? ? raise(StopIteration) : it - 1 }
p geri.to_a # çıktı: [5, 4, 3, 2, 1, 0]
p geri.size # çıktı: Infinity
sinirli = Enumerator.produce(5, size: 6) { it.zero? ? raise(StopIteration) : it - 1 }
p sinirli.size # çıktı: 6
bilinmez = Enumerator.produce(5, size: nil) { it - 1 }
p bilinmez.size # çıktı: nilKendi sınıfınızda to_enum ile enumerator döndürmek
Kendi each metodunuzu yazarken, blok verilmediğinde çekirdek sınıflar gibi bir enumerator döndürmek iyi bir alışkanlıktır. Belgelerin önerdiği kalıp, block_given? ile kontrol edip to_enum döndürmektir. to_enum'a verilen blok, gezinmeden boyutu hesaplamak için kullanılır. Sınıfa Enumerable eklerseniz map, each_slice ve diğerleri de kendiliğinden gelir.
class Raf
include Enumerable
def initialize(*kitaplar) = @kitaplar = kitaplar
def each
return to_enum(:each) { @kitaplar.size } unless block_given?
@kitaplar.each { |k| yield k }
self
end
end
raf = Raf.new("Dune", "Solaris", "Neuromancer")
e = raf.each
p e.class # çıktı: Enumerator
p e.size # çıktı: 3
p e.next # çıktı: "Dune"
p raf.map(&:upcase) # çıktı: ["DUNE", "SOLARIS", "NEUROMANCER"]
p raf.each_slice(2).to_a # çıktı: [["Dune", "Solaris"], ["Neuromancer"]]Kendi sınıflarınıza Enumerable ve Comparable eklemenin ayrıntıları için Comparable ve Enumerable yazısına bakabilirsiniz.
Metotlar bir bakışta

| Metot | Ne döndürür | Kullanım |
|---|---|---|
| each, map (bloksuz) | Enumerator | Zincirleme için başlangıç |
| with_index(n) | Blok sonucu veya Enumerator | n den başlayan sıra numarası |
| each_with_object(o) | o nesnesi | Sayım, gruplama |
| each_slice(n) | self veya Enumerator | n li ayrık gruplar |
| each_cons(n) | self veya Enumerator | n li kayan pencere |
| next, peek | Sıradaki eleman | Dış iterasyon, sonda StopIteration |
| lazy | Enumerator::Lazy | Tembel zincir, sonsuz diziler |
| force, first(n) | Array | Lazy zinciri çalıştırır |
| Enumerator.produce | Enumerator | Önceki değerden yenisini üretir |
Sık yapılan hatalar
- Sonsuz bir lazy zincirde to_a çağırmak.
(1..Float::INFINITY).lazy.map { ... }.to_ahiç bitmez;first(n)ya datake(n)ile sınır koyun. - Lazy enumerator'da bloksuz map çağırmak.
ArgumentErrorverir. - next ile gezinirken StopIteration'ı unutmak. Ya
loopiçinde kullanın ya da hatayı yakalayın. - size değerine kesin bilgi gibi güvenmek. Belgelere göre boyut bir ipucudur;
produceboyutu belirtilmezse sonsuz kabul edilir. - Kendi each metodunda bloksuz çağrıyı ele almamak.
to_enumdöndürmezsenizraf.each.with_indexgibi zincirler çalışmaz.
Sık Sorulan Sorular
Enumerator ile Array arasındaki fark nedir?
Array elemanları bellekte tutan bir koleksiyondur. Enumerator ise bir gezinme tarifidir; elemanları istendiğinde üretebilir. Bu yüzden Fibonacci dizisi gibi sonsuz dizileri enumerator ile temsil edebilirsiniz. Array metotları için Ruby Array metotları yazısına bakın.
lazy her zaman daha hızlı mı?
Hayır. Lazy, gereksiz işi ve ara dizileri önler; tüm elemanları zaten işleyecekseniz ek bir kazanç sağlamayabilir. Belgeler lazy'yi uzun ya da sonsuz diziler ve yavaş hesaplanan değerler için önerir.
loop neden StopIteration hatasında durur?
Kernel#loop belgelerine göre blok içinde fırlatılan StopIteration döngüyü kırar ve loop, hatanın içinde saklanan "result" değerini döndürür.
Ruby 4.0 öncesinde Enumerator.produce size: kabul ediyor mu?
Hayır, size: argümanı Ruby 4.0 NEWS dosyasında yeni özellik olarak listeleniyor. Daha eski sürümlerde bu argümanı kullanmayın. Ruby 4.0'ın diğer değişiklikleri için Ruby 4.0 yenilikleri yazısına bakabilirsiniz.
Marka adları sahiplerine aittir. Bu yazı bağımsız bir teknik rehberdir; adı geçen kuruluşlarla bayilik veya ortaklık ilişkisi bulunduğu anlamına gelmez.
İlgili içerikler: Ruby öğrenme rehberi · Enumerable: map, select, reduce · Blok ve yield · Range aralıkları · Thread ve Fiber · Ruby 4.0 yenilikleri