Ruby'de Set, aynı öğeyi yalnızca bir kez tutan, sırası önemli olmayan bir koleksiyondur. Set[1, 2, 3] ya da Set.new(dizi) ile oluşturulur, add veya << ile öğe eklenir, include? ile üyelik sorgulanır. Birleşim için | (union), kesişim için & (intersection), fark için - (difference), alt küme kontrolü için subset? kullanılır. Ruby 4.0 ile Set çekirdek sınıf oldu; require "set" yazmanız gerekmez.

Set belgesi sınıfı, dizilerin kolay kullanımıyla hash'lerin hızlı aramasını birleştiren bir yapı olarak tanımlar; içeride yalnızca anahtarları olan bir hash'e benzer bir veri yapısı kullanılır. Aşağıdaki örnekleri Ruby 4.0.9 ile çalıştırdık. Dizi ve hash tarafı için Ruby Array metotları ve Ruby Hash metotları yazılarına, dilin geneli için Ruby öğrenme rehberi sayfasına bakabilirsiniz.

Ruby 4.0'da Set'te neler değişti?

Ruby 4.0'da Set'te neler değişti? Set, otomatik yüklenen bir standart kütüphane sınıfı olmaktan çıkıp çekirdek..., Set#inspect, dizi literaline benzeyen sade bir görünüme geçti...

NEWS dosyası Set için şu değişiklikleri listeliyor:

  • Set, otomatik yüklenen bir standart kütüphane sınıfı olmaktan çıkıp çekirdek sınıf oldu.
  • Set#inspect, dizi literaline benzeyen sade bir görünüme geçti: #<Set: {1, 2, 3}> yerine Set[1, 2, 3].
  • Set#to_set ve Enumerable#to_set metotlarına argüman vermek kullanımdan kaldırılma sürecine girdi.
  • set/sorted_set.rb silindi ve SortedSet artık otomatik yüklenmiyor; gerekirse sorted_set gem'ini kurup require "sorted_set" yazmanız gerekiyor.
  • Range#to_set sonsuz aralıklar için boyut kontrolü yapıyor.
  • C eklentileri için bir Set C API'si eklendi (rb_set_new, rb_set_add gibi).

Set belgesi ayrıca sınıfın 4.0 için C ile yeniden yazıldığını ve bazı çekirdek metotların davranışının ayarlandığını belirtiyor. Bu değişikliğin alt sınıflar üzerindeki etkisini aşağıda ayrı başlıkta gösteriyoruz.

Set oluşturmak

En kısa yol Set[] yazımıdır. Set.new herhangi bir Enumerable alır ve isteğe bağlı bir blokla öğeleri eklemeden önce dönüştürebilir. Diziler ve diğer Enumerable nesneler to_set ile de kümeye çevrilir. Yinelenen öğeler kendiliğinden atılır:

ruby
Set[3, 1, 2, 3]                 # => Set[3, 1, 2]
Set.new([1, 2, 1])              # => Set[1, 2]
Set.new(1..4) { |x| x * x }     # => Set[1, 4, 9, 16]
[:a, :b, :a].to_set             # => Set[:a, :b]
Set.new                         # => Set[]
Set.instance_method(:add).owner # => Set

Öğe eşitliği eql? ve hash metotlarıyla belirlenir; bu yüzden 1 ile 1.0 farklı öğe sayılır. Belge, bir metin öğe olarak saklanırken, metin zaten dondurulmuş değilse dondurulmuş bir kopyasının saklandığını da belirtiyor.

Öğe eklemek, silmek ve sorgulamak

add ve takma adı << öğeyi ekleyip kümenin kendisini döndürür; öğe zaten varsa hiçbir şey değişmez. Eklemenin gerçekleşip gerçekleşmediğini bilmek istiyorsanız add? kullanın: öğe zaten varsa nil döner. Silmek için delete ve delete? aynı mantıkla çalışır:

ruby
etiketler = Set["ruby"]
etiketler << "json"
etiketler.add("ruby")
etiketler                      # => Set["ruby", "json"]
etiketler.add?("ruby")         # => nil
etiketler.add?("set")          # => Set["ruby", "json", "set"]
etiketler.size                 # => 3
etiketler.include?("json")     # => true
etiketler.member?("python")    # => false
etiketler.delete("set")        # => Set["ruby", "json"]

include? metodunun takma adlarından biri === olduğu için bir kümeyi doğrudan case ifadesinin when dalında kullanabilirsiniz:

ruby
case "ruby"
when etiketler then puts "etiket var"
end
# çıktı: etiket var

Belgeye göre kümede saklanan bir öğenin kimliği değişmemelidir; bir öğeyi küme içindeyken değiştirirseniz küme güvenilmez duruma düşer. Böyle bir durumda reset metodu iç durumu yeniler. case yazımı için Ruby if, unless ve case yazısına bakın.

Küme işlemleri: birleşim, kesişim, fark

Küme işlemleri: birleşim, kesişim, fark: | veya +, intersection, difference, yok

Küme işlemleri yeni bir küme döndürür ve sağ taraf olarak dizi gibi herhangi bir Enumerable kabul eder:

OperatörMetot adıSonuç
| veya +unionİki taraftaki tüm öğeler
&intersectionYalnızca ortak öğeler
-differenceSoldaki öğelerden sağdakiler çıkarılmış hali
^yokOrtak olanlar dışındaki tüm öğeler
ruby
a = Set[1, 2, 3, 4]
b = Set[3, 4, 5]
a | b               # => Set[1, 2, 3, 4, 5]
a.union(b)          # => Set[1, 2, 3, 4, 5]
a & b               # => Set[3, 4]
a.intersection([4, 9]) # => Set[4]
a - b               # => Set[1, 2]
a.difference(b)     # => Set[1, 2]
a ^ b               # => Set[1, 2, 5]
a + [9]             # => Set[1, 2, 3, 4, 9]

Kümeyi yerinde değiştirmek istiyorsanız yeni küme döndüren bu operatörler yerine merge (ekleme) ve subtract (çıkarma) metotlarını kullanın.

Pratik örnek: iki günün ziyaretçileri

Küme işlemleri, iki listeyi karşılaştırma işlerini tek satıra indirir. Aşağıda iki günün ziyaretçi listesinden her iki gün gelenleri, yalnızca ikinci gün gelenleri ve toplam tekil ziyaretçi sayısını buluyoruz. Son satırda add? metodunun nil dönüşünden yararlanarak bir listede tekrar eden öğeleri ayıklıyoruz:

ruby
pazartesi = Set["ali", "ayşe", "can"]
sali = ["can", "deniz", "ali", "can"].to_set

pazartesi & sali           # => Set["can", "ali"]
sali - pazartesi           # => Set["deniz"]
(pazartesi | sali).size    # => 4

gorulen = Set.new
%w[a b a c b a].select { |x| !gorulen.add?(x) } # => ["a", "b", "a"]

Aynı işi dizilerle de yapabilirsiniz, ancak üyelik sorgusu çok sayıda tekrarlandığında kümenin hash tabanlı yapısı belge tarafından hızlı arama için tasarlanmış bir yöntem olarak öne çıkarılıyor.

Alt küme, üst küme ve karşılaştırma

subset? (takma adı <=) soldaki kümenin tüm öğelerinin sağda olup olmadığını sorar. proper_subset? (<) ayrıca iki kümenin eşit olmamasını ister. superset? ve proper_superset? ters yöndeki karşılıklarıdır. disjoint? ortak öğe yoksa, intersect? en az bir ortak öğe varsa true döner:

ruby
kucuk = Set[1, 2]
buyuk = Set[1, 2, 3]
kucuk.subset?(buyuk)          # => true
kucuk <= buyuk                # => true
kucuk < buyuk                 # => true
buyuk.superset?(kucuk)        # => true
kucuk.proper_subset?(Set[1, 2]) # => false
kucuk.disjoint?(Set[7, 8])    # => true
kucuk.intersect?([2, 9])      # => true

kucuk <=> buyuk               # => -1
Set[1, 2] <=> Set[1, 3]       # => nil
Set[1, 2] == Set[2, 1]        # => true

<=> operatörü alt küme ilişkisini yansıtır; iki kümenin birbirinde olmayan öğeleri varsa karşılaştırılamaz ve nil döner. Eşitlikte sıra önemli değildir.

Enumerable metotları ve dönüşümler

Set, Enumerable modülünü içerir ve getirme ile dolaşma için büyük ölçüde bu modülün metotlarına dayanır. Dikkat edilecek nokta şu: select ve map gibi Enumerable metotları dizi döndürür. Sonucu yine küme olarak istiyorsanız to_set ekleyin ya da yerinde değiştiren select! ve map! metotlarını kullanın:

ruby
s = Set[1, 2, 3, 4, 5, 6]
s.select(&:even?)       # => [2, 4, 6]
s.map { it * 10 }       # => [10, 20, 30, 40, 50, 60]
s.to_a                  # => [1, 2, 3, 4, 5, 6]
s.sum                   # => 21
s.min                   # => 1
s.classify(&:odd?)      # => {true => Set[1, 3, 5], false => Set[2, 4, 6]}

classify, bloğun döndürdüğü değere göre öğeleri kümelere ayırıp bir hash döndürür. Enumerable metotlarının geneli için Ruby Enumerable: map, select, reduce yazısına bakın.

to_set argümanları ve sonsuz aralıklar

Ruby 4.0'da to_set metoduna argüman vermek kullanımdan kaldırılma sürecinde. Ruby kullanımdan kaldırma uyarılarını varsayılan olarak gizlediği için denememizde Warning[:deprecated] = true ile açtık. Sonsuz bir aralığı kümeye çevirmeye çalışmak da artık hemen hata veriyor:

ruby
Warning[:deprecated] = true
[1, 2].to_set(Set) # => Set[1, 2]
# stderr: warning: passing arguments to Enumerable#to_set is deprecated

begin
  (1..).to_set
rescue => e
  puts "#{e.class}: #{e.message}"
end
# çıktı: RangeError: cannot convert endless range to a set

Aralıklar hakkında daha fazlası için Ruby Range aralıklar yazısına bakabilirsiniz.

Set sınıfından türetmek

Set belgesine göre geriye dönük uyumluluk için, Set sınıfından türeyen sınıflara otomatik olarak Set::SubclassCompatible modülü eklenir. Bu modül alt sınıfın davranışını ve iç metot adlarını 3.4'teki haline yaklaştırır. Farkı inspect çıktısında görebilirsiniz. Yeni kodda uyumluluk katmanına ihtiyacınız yoksa belge Set::CoreSet sınıfından türetmeyi öneriyor:

ruby
class EskiSet < Set; end
EskiSet[1, 2] # => #<EskiSet: {1, 2}>

class YeniSet < Set::CoreSet; end
YeniSet[1, 2] # => YeniSet[1, 2]

SortedSet artık yok

Ruby 4.0'da SortedSet otomatik yüklenmiyor. Eski kodunuzda geçiyorsa şu hatayı alırsınız:

ruby
begin
  SortedSet
rescue NameError => e
  puts "#{e.class}: #{e.message}"
end
# çıktı: NameError: uninitialized constant SortedSet

Çözüm, NEWS dosyasının önerdiği gibi sorted_set gem'ini kurmak ya da sıralı görünüme yalnızca çıktı alırken ihtiyacınız varsa set.sort ile sıralı bir dizi almaktır.

Sık yapılan hatalar

Sık yapılan hatalar: Ruby 4.0'da inspect çıktısının değiştiğini hesaba katmadan, eski #<Set: {...}>..., select veya map sonucunun hâlâ küme olduğunu varsaymak; dizi döner., Kümeye...
  • Ruby 4.0'da inspect çıktısının değiştiğini hesaba katmadan, eski #<Set: {...}> biçimine bakan testler yazmak.
  • select veya map sonucunun hâlâ küme olduğunu varsaymak; dizi döner.
  • Kümeye eklediğiniz değiştirilebilir bir nesneyi (örneğin bir diziyi) sonradan değiştirmek.
  • Sıra gerektiren işlerde kümeye güvenmek; sıralı sonuç için sort kullanın.
  • to_set metoduna sınıf argümanı vermeye devam etmek; bu kullanım kaldırılma sürecinde.

Sık Sorulan Sorular

Set mi Array#uniq mi kullanmalıyım?

Bir kez tekrarları temizleyip diziyle devam edecekseniz uniq yeterli. Sürekli üyelik sorgusu yapacak ve öğe ekleyip çıkaracaksanız, hash'e benzer yapısıyla hızlı arama yapan Set daha uygundur.

Ruby 4.0'da require "set" yazarsam sorun olur mu?

Set artık çekirdek sınıf olduğu için gerek yok. Eski sürümleri de desteklemesi gereken kodlarda bu satırı tutabilirsiniz; bu yazıdaki örneklerde hiç kullanmadık.

Set öğeleri sıralı mı tutulur?

Belge Set'i sırasız değerler koleksiyonu olarak tanımlar. Denememizde öğeler ekleme sırasıyla görüntülendi, ancak sıra gereken yerde sort ile açıkça sıralayın.

İki kümenin ortak öğesi var mı diye nasıl bakarım?

a.intersect?(b) en az bir ortak öğe varsa true döner; tersini sormak için a.disjoint?(b) kullanı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 · Ruby Array metotları · Ruby Hash metotları · Ruby Enumerable · Ruby 4.0 yenilikleri · Ruby Range