Kendi sınıfınızın nesnelerini <, >, between? ve clamp ile karşılaştırılabilir yapmak için sınıfa include Comparable ekleyip tek bir <=> metodu yazmanız yeterlidir. Bu metot karşılaştırılan nesne küçükse negatif, eşitse 0, büyükse pozitif bir sayı, karşılaştırılamıyorsa nil döndürmelidir. Benzer şekilde bir koleksiyon sınıfına include Enumerable ekleyip elemanları sırayla yield eden bir each metodu yazarsanız map, select, sort, min, max, sum gibi onlarca metot kendiliğinden gelir.
Bu yazıda iki mixin'i sıfırdan bir sınıfa ekliyor, karşılaştırılamayan değerlerin nasıl ele alınacağını, == ile eql?/hash farkını ve each'in blok verilmeden çağrıldığında ne döndürmesi gerektiğini Ruby 4.0.9'da çalıştırılmış örneklerle gösteriyoruz. Mixin kavramının kendisi için Ruby module yazısına, hazır koleksiyonlardaki Enumerable metotları için Enumerable map, select, reduce yazısına bakabilirsiniz.
Comparable: tek metottan altı karşılaştırma

Belgelere göre Comparable, <=> metodunu kullanarak <, <=, ==, >=, >, between? ve clamp metotlarını sağlar. Örnek olarak "3.10.0" gibi sürüm numaralarını doğru sıralayan bir sınıf yazalım. String olarak karşılaştırsaydık "3.10", "3.4"'ten küçük çıkardı; parçaları sayıya çevirip dizileri karşılaştırarak bu sorunu çözüyoruz.
class Surum
include Comparable
attr_reader :parcalar
def initialize(metin)
@parcalar = metin.split(".").map(&:to_i)
end
def <=>(diger)
return nil unless diger.is_a?(Surum)
parcalar <=> diger.parcalar
end
def to_s = parcalar.join(".")
def inspect = "v#{self}"
end
a = Surum.new("3.4.1")
b = Surum.new("4.0.0")
c = Surum.new("3.10.0")
p a < b # çıktı: true
p c > a # çıktı: true
p a == Surum.new("3.4.1") # çıktı: true
p a.between?(Surum.new("3.0"), b) # çıktı: true
p Surum.new("5.1").clamp(a, b) # çıktı: v4.0.0
p Surum.new("2.0").clamp(a..b) # çıktı: v3.4.1clamp değeri verilen alt ve üst sınırın içine çeker: sınırın altındaysa alt sınırı, üstündeyse üst sınırı, aradaysa nesnenin kendisini döndürür. Range biçimini de kabul eder; belgelere göre sonu hariç tutulan (...) ve sonu nil olmayan bir aralık verirseniz ArgumentError oluşur.
Karşılaştırılamayan değerler: nil döndürün
Belgeler, diğer nesne karşılaştırılabilir değilse <=> metodunun nil döndürmesini istiyor. Bu durumda == sessizce false döndürür, < gibi sıralama operatörleri ise ArgumentError verir. Böylece yanlış türle yapılan karşılaştırma gizli bir hataya dönüşmez:
p a == "3.4.1" # çıktı: false
a < 3
# ArgumentError: comparison of Surum with 3 failedsort, min ve max sizin sınıfınızla
Dizilerin ve diğer Enumerable koleksiyonların sort, min, max ve minmax metotları blok verilmediğinde elemanların kendi <=> metodunu kullanır. Yani <=> tanımladığınız anda bu metotlar sizin nesnelerinizle de çalışır:
p [b, c, a].sort # çıktı: [v3.4.1, v3.10.0, v4.0.0]
p [b, c, a].min # çıktı: v3.4.1
p [b, c, a].max # çıktı: v4.0.0
p [b, c, a].minmax # çıktı: [v3.4.1, v4.0.0]Data ile Comparable
Data.define bloğu içine de Comparable ekleyebilirsiniz. Belgelerdeki ölçü örneğine benzer aşağıdaki kodda, birimi farklı iki ölçü karşılaştırılamaz sayılıyor. Dikkat: sort da aynı hatayı verir, çünkü sıralama sırasında <=> nil döndürür.
Olcu = Data.define(:miktar, :birim) do
include Comparable
def <=>(diger)
return unless diger.is_a?(self.class) && diger.birim == birim
miktar <=> diger.miktar
end
end
p Olcu[3, "m"] < Olcu[5, "m"] # çıktı: true
p [Olcu[7, "kg"], Olcu[2, "kg"]].max # çıktı: #<data Olcu miktar=7, birim="kg">
Olcu[3, "m"] < Olcu[5, "kg"]
# ArgumentError: comparison of Olcu with Olcu failed
[Olcu[3, "m"], Olcu[5, "kg"]].sort
# ArgumentError: comparison of Olcu with Olcu failedData sınıflarının ayrıntıları için Ruby Struct ve Data yazısına bakabilirsiniz.
Comparable eşitliği hash anahtarı için yetmez
Comparable size == verir ama eql? ve hash vermez. Hash anahtarları ve uniq ise bu ikisine bakar. Bu yüzden iki eşit sürüm nesnesi uniq sonrası iki ayrı eleman olarak kalır. Object#hash belgesi, kendi sınıfınızda hash tanımlarken sınıf ve önemli alanlardan oluşan bir dizinin hash değerini kullanmayı öneriyor:
x = Surum.new("1.0")
y = Surum.new("1.0")
p x == y # çıktı: true
p x.eql?(y) # çıktı: false
p [x, y].uniq.size # çıktı: 2
class Surum
def eql?(diger) = self == diger
def hash = [self.class, parcalar].hash
end
p [x, y].uniq.size # çıktı: 1
p({x => "ilk"}[y]) # çıktı: "ilk"Enumerable: each yazın, gerisi gelsin
Belgelere göre Enumerable kullanmak için iki şey gerekir: modülü include etmek ve elemanları sırayla yield eden bir each metodu yazmak. Enumerable'daki hemen her metot bu each'i çağırır. Aşağıdaki Sepet sınıfı kalemlerini bir dizide tutuyor ama dışarıya dizi değil, kendi arayüzünü sunuyor:
class Sepet
include Enumerable
Kalem = Data.define(:ad, :fiyat)
def initialize
@kalemler = []
end
def ekle(ad, fiyat)
@kalemler << Kalem.new(ad, fiyat)
self
end
def each
return to_enum(:each) unless block_given?
@kalemler.each { |k| yield k }
self
end
end
sepet = Sepet.new.ekle("Kitap", 180).ekle("Kalem", 25).ekle("Çanta", 640)
p sepet.map(&:ad) # çıktı: ["Kitap", "Kalem", "Çanta"]
p sepet.select { |k| k.fiyat > 100 }.map(&:ad) # çıktı: ["Kitap", "Çanta"]
p sepet.sum(&:fiyat) # çıktı: 845
p sepet.min_by(&:fiyat) # çıktı: #<data Sepet::Kalem ad="Kalem", fiyat=25>
p sepet.max_by(&:fiyat).ad # çıktı: "Çanta"
p sepet.sort_by(&:fiyat).map(&:ad) # çıktı: ["Kalem", "Kitap", "Çanta"]
p sepet.count # çıktı: 3
p sepet.find { |k| k.ad.start_with?("Ç") } # çıktı: #<data Sepet::Kalem ad="Çanta", fiyat=640>
p sepet.include?(Sepet::Kalem.new("Kalem", 25)) # çıktı: true
p sepet.each_slice(2).map { |grup| grup.map(&:ad) } # çıktı: [["Kitap", "Kalem"], ["Çanta"]]
p sepet.each_with_index.map { |k, i| "#{i + 1}. #{k.ad}" } # çıktı: ["1. Kitap", "2. Kalem", "3. Çanta"]
p sepet.each.next # çıktı: #<data Sepet::Kalem ad="Kitap", fiyat=180>
p sepet.lazy.map(&:fiyat).select(&:even?).first(2) # çıktı: [180, 640]each metodunun ilk satırı önemli: blok verilmeden çağrıldığında to_enum(:each) ile bir Enumerator döndürüyoruz. Böylece sepet.each.next gibi harici gezinme ve zincirleme kullanım mümkün oluyor. Enumerator ve lazy ayrıntıları için Enumerator ve lazy yazısına bakın. include? Data nesnelerinin içerik eşitliği sayesinde true döndü.
Elemanlar Comparable ise sort ve min de çalışır
Enumerable'ın sort, min, max gibi blok almayan biçimleri elemanların <=> metodunu kullanır. Elemanlar sayı, string ya da yukarıdaki Surum gibi Comparable bir sınıfın nesneleriyse ek bir şey yapmanız gerekmez. Basit bir örnekte each'i doğrudan iç diziye devrediyoruz:
class Fiyatlar
include Enumerable
def initialize(*fiyatlar) = @fiyatlar = fiyatlar
def each(&blok) = @fiyatlar.each(&blok)
end
s = Fiyatlar.new(30, 10, 20)
p s.sort # çıktı: [10, 20, 30]
p s.min # çıktı: 10
p s.max(2) # çıktı: [30, 20]
p s.minmax # çıktı: [10, 30]
p s.sort { |a, b| b <=> a } # çıktı: [30, 20, 10]
p s.reduce(:+) # çıktı: 60
p s.tally # çıktı: {30 => 1, 10 => 1, 20 => 1}Çıktıdaki {30 => 1} biçimi Ruby 4.0'ın hash gösterimidir; sembol olmayan anahtarlar => ile yazılır.
Sık yapılan hatalar

- each tanımlamadan Enumerable eklemek: Modül
each'e dayandığı için ilk metot çağrısındaNoMethodError: undefined method 'each'alırsınız. - <=> içinde türü kontrol etmemek: Farklı türde bir nesneyle karşılaştırmada metodunuz başka bir hata fırlatabilir. Önce türü kontrol edip
nildöndürün. - Comparable ile hash anahtarı eşitliği beklemek:
uniqveHashiçineql?vehashtanımlayın. - each'te blok yoksa Enumerator döndürmemek:
to_enumsatırı olmadansepet.each.nextgibi kullanımlar çalışmaz. - Toplanamayan elemanlarla sum çağırmak:
sumvarsayılan olarak 0'dan başlar; string elemanlardaTypeError: String can't be coerced into Integeralırsınız. Başlangıç değeri verin ya da blok kullanın.
class Takim
include Enumerable
def each
yield "Ali"
yield "Ece"
end
end
Takim.new.sum
# TypeError: String can't be coerced into Integer
class Bos
include Enumerable
end
Bos.new.map { |x| x }
# NoMethodError: undefined method 'each' for an instance of BosSık Sorulan Sorular
Comparable'ın == metodunu kullanmak zorunda mıyım?
Hayır. Sınıfınızda kendi == metodunuzu tanımlarsanız o kullanılır. Comparable'ınki, belgelere göre <=> 0 döndürdüğünde ya da iki nesne aynı nesne olduğunda true verir.
Enumerable eklediğim sınıfta size metodu olur mu?
Enumerable count sağlar ama size sağlamaz. Eleman sayısını hesaplamadan bilebiliyorsanız size metodunu kendiniz tanımlayın.
Dizi döndüren bir metot yazmak yerine neden Enumerable?
Kendi koleksiyon sınıfınız iç yapısını gizleyebilir, yine de bütün Enumerable metotlarını sunar. Kullanıcı select, group_by ya da lazy gibi metotları sizin ek kod yazmanıza gerek kalmadan kullanır.
Ruby 4.0'da bu modüllerde ne değişti?
Ruby 4.0 NEWS dosyası bu iki modül için öne çıkan bir değişiklik listelemiyor; yalnızca Array#find metodunun, Enumerable#find'ı dizilerde daha verimli biçimde ezmek için eklendiğini belirtiyor. Diğer yenilikler için Ruby 4.0 yenilikleri yazısına bakın.
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 · Enumerator ve lazy · Ruby modülleri · Struct ve Data · Ruby Array metotları