Ruby'de birkaç alanı bir arada tutan basit bir sınıfı tek satırda oluşturmanın iki yolu vardır: Struct.new(:ad, :fiyat) değiştirilebilir, sıralanabilir (Enumerable) bir kayıt sınıfı üretir; Data.define(:ad, :fiyat) ise değiştirilemez bir değer nesnesi sınıfı üretir. Struct'ta alanlar sonradan urun.fiyat = 30 ile değişebilir ve eksik alanlar nil olur; Data'da bütün alanlar zorunludur, yazıcı metot yoktur ve değişiklik için with ile yeni bir kopya alınır.

Bu yazıda iki sınıfı, keyword_init seçeneğini, Data'nın değiştirilemezlik sınırlarını, varsayılan değer vermek için initialize ezmeyi ve ikisinin pattern matching ile kullanımını Ruby 4.0.9'da denenmiş örneklerle anlatıyoruz. Elle sınıf yazmanın ayrıntıları için Ruby class ve nesneler yazısına bakabilirsiniz.

Struct: hızlı ve değiştirilebilir kayıt sınıfı

Struct: hızlı ve değiştirilebilir kayıt sınıfı: Eksik ve fazla argümanlar, keyword_init ile tek bir biçime zorlamak

Struct.new yeni bir sınıf döndürür; bunu bir sabite atadığınızda sınıf o adı alır. Her alan için hem okuyucu hem yazıcı metot oluşur. Ruby 3.2 NEWS dosyasına göre Struct nesneleri o sürümden beri keyword_init: true vermeden de anahtar kelime argümanlarıyla oluşturulabiliyor.

ruby
Urun = Struct.new(:ad, :fiyat)

u = Urun.new("Kalem", 25)
p u                                   # çıktı: #<struct Urun ad="Kalem", fiyat=25>
p Urun.new(ad: "Defter", fiyat: 60)   # çıktı: #<struct Urun ad="Defter", fiyat=60>
u.fiyat = 30
p u[:fiyat]     # çıktı: 30
p u["ad"]       # çıktı: "Kalem"
p u[1]          # çıktı: 30
p u.to_a        # çıktı: ["Kalem", 30]
p u.to_h        # çıktı: {ad: "Kalem", fiyat: 30}
p u.members     # çıktı: [:ad, :fiyat]
p u == Urun.new("Kalem", 30)   # çıktı: true
p Urun.superclass              # çıktı: Struct
u.each_pair { |ad, deger| puts "#{ad} = #{deger}" }
# çıktı: ad = Kalem
# çıktı: fiyat = 30

Struct Enumerable modülünü içerdiği için each, map, select gibi metotlar alan değerleri üzerinde çalışır. Alanlara sembol, string ya da sıra numarasıyla [] üzerinden de erişebilirsiniz. Ruby 4.0'da to_h çıktısının {ad: "Kalem"} biçiminde gösterildiğine dikkat edin.

Eksik ve fazla argümanlar

Struct'ta argümanların hepsini vermek zorunlu değildir; verilmeyen alanlar nil olur. Alan sayısından fazla argüman ya da tanımsız bir anahtar verirseniz hata alırsınız:

ruby
Urun = Struct.new(:ad, :fiyat)
p Urun.new("Silgi")              # çıktı: #<struct Urun ad="Silgi", fiyat=nil>
Urun.new("Silgi", 10, 3)
# ArgumentError: struct size differs
Urun.new(ad: "Silgi", renk: "beyaz")
# ArgumentError: unknown keywords: renk

keyword_init ile tek bir biçime zorlamak

keyword_init: true verirseniz sınıf yalnızca anahtar kelime argümanı kabul eder ve konumsal çağrı hata verir. Belgelere göre keyword_init: false ise yalnızca konumsal argüman kabul eder; bu durumda anahtar kelimeler hata vermeden tek bir hash değeri olarak ilk alana yazılır. Seçeneği hiç vermemek ya da nil vermek iki biçime de izin verir. Hangi seçeneğin kullanıldığını keyword_init? söyler (Ruby 3.1'de eklendi).

ruby
Ayar = Struct.new(:host, :port, keyword_init: true)
p Ayar.new(port: 5432, host: "db")   # çıktı: #<struct Ayar host="db", port=5432>
Ayar.new("db", 5432)
# ArgumentError: wrong number of arguments (given 2, expected 0)
p Ayar.keyword_init?                 # çıktı: true
p Struct.new(:a).keyword_init?       # çıktı: nil

Struct.new bir blok da alır; blok yeni sınıfın içinde çalıştığı için ek metotlar tanımlayabilirsiniz:

ruby
Kalem = Struct.new(:ad, :adet, :birim_fiyat) do
  def tutar = adet * birim_fiyat

  def to_s = "#{adet} x #{ad} = #{tutar} TL"
end

k = Kalem.new("Kahve", 2, 85)
puts k        # çıktı: 2 x Kahve = 170 TL
p k.tutar     # çıktı: 170

Data: değiştirilemez değer nesneleri

Data Ruby 3.2'de çekirdeğe eklenen bir sınıftır ve Struct'a benzer ama daha dar ve katı bir arayüz sunar. Belgeler onu değiştirilemez, atomik değerleri saklamak için tasarlanmış olarak tanımlıyor: yazıcı metot ve each gibi gezinme metotları yoktur. Kurucu hem konumsal hem anahtar kelime argümanı kabul eder ve Konum[...] kısa yazımı da çalışır.

ruby
Konum = Data.define(:enlem, :boylam)

ankara = Konum.new(39.93, 32.85)
p ankara                                  # çıktı: #<data Konum enlem=39.93, boylam=32.85>
p Konum.new(enlem: 41.01, boylam: 28.97)  # çıktı: #<data Konum enlem=41.01, boylam=28.97>
p Konum[38.42, 27.14]                     # çıktı: #<data Konum enlem=38.42, boylam=27.14>
p ankara.enlem                            # çıktı: 39.93
p ankara.to_h                             # çıktı: {enlem: 39.93, boylam: 32.85}
p ankara.frozen?                          # çıktı: true
p ankara == Konum.new(39.93, 32.85)       # çıktı: true

ankara.enlem = 40
# NoMethodError: undefined method 'enlem=' for an instance of Konum
Konum.new(39.93)
# ArgumentError: missing keyword: :boylam
ankara.instance_variable_set(:@enlem, 1)
# FrozenError: can't modify frozen Konum: #<data Konum enlem=39.93, boylam=32.85>

Struct'tan farklı olarak eksik alan kabul edilmez; bütün argümanlar zorunludur. Konumsal argümanlar da içeride anahtar kelimeye çevrildiği için hata mesajı missing keyword diyor. Denememizde Data nesnesi frozen? için true döndürdü; dondurulmuş nesnelerin genel davranışını frozen string ve freeze yazısında bulabilirsiniz.

with ile değiştirilmiş kopya almak

Bir alanı değiştirmek istediğinizde nesnenin kendisini değil, with ile alınmış yeni bir kopyasını kullanırsınız. Orijinal nesne olduğu gibi kalır. Tanımsız bir alan adı vermek hatadır:

ruby
kuzey = ankara.with(enlem: 40.5)
p kuzey    # çıktı: #<data Konum enlem=40.5, boylam=32.85>
p ankara   # çıktı: #<data Konum enlem=39.93, boylam=32.85>
ankara.with(rakim: 938)
# ArgumentError: unknown keyword: :rakim
ruby
Etkinlik = Data.define(:saat, :gunler)
e = Etkinlik.new("18:00", %w[Sal Çar])
e.gunler << "Cum"
p e   # çıktı: #<data Etkinlik saat="18:00", gunler=["Sal", "Çar", "Cum"]>

Varsayılan değer ve dönüştürme için initialize

Data'da isteğe bağlı alan yoktur, ama initialize metodunu ezerek varsayılan değer verebilir ya da gelen değeri dönüştürebilirsiniz. Belgelerin vurguladığı ayrıntı: initialize her zaman anahtar kelime argümanı alır ve zorunlu alan kontrolü new'da değil initialize'da yapılır. Bu yüzden konumsal çağrı da aşağıdaki tanımla çalışır:

ruby
Para = Data.define(:miktar, :birim) do
  def initialize(miktar:, birim: "TRY")
    super(miktar: Float(miktar), birim:)
  end

  def to_s = format("%.2f %s", miktar, birim)
end

p Para.new(100)            # çıktı: #<data Para miktar=100.0, birim="TRY">
p Para.new("49.9", "EUR")  # çıktı: #<data Para miktar=49.9, birim="EUR">
puts Para.new(miktar: 12)  # çıktı: 12.00 TRY
Para.new("abc")
# ArgumentError: invalid value for Float(): "abc"

to_s metodunu ezdiğimiz için puts kendi biçimimizi kullandı; p ise inspect çağırdığı için varsayılan gösterim değişmedi. Ayrıca birim: yazımı, aynı adlı yerel değişkeni değer olarak geçirmenin kısa biçimidir; bu konu anahtar kelime argümanları yazısında anlatılıyor.

Pattern matching ile Struct ve Data

Her iki sınıf da deconstruct ve deconstruct_keys metotlarını sağlar. Bu sayede case/in içinde hem sınıfı hem alanları tek seferde kontrol edebilirsiniz. Alansız bir Data.define sınıfı, belgelerde de önerildiği gibi farklı sonuç türlerini temsil etmek için kullanışlıdır:

ruby
Basarili = Data.define(:govde)
Bulunamadi = Data.define
Hata = Struct.new(:kod, :mesaj)

def ozetle(yanit)
  case yanit
  in Basarili(govde:) if govde.empty?
    "boş yanıt"
  in Basarili(govde)
    "içerik: #{govde}"
  in Bulunamadi
    "404"
  in Hata[kod, mesaj]
    "hata #{kod}: #{mesaj}"
  end
end

p ozetle(Basarili.new("<html>"))    # çıktı: "içerik: <html>"
p ozetle(Basarili.new(""))          # çıktı: "boş yanıt"
p ozetle(Bulunamadi.new)            # çıktı: "404"
p ozetle(Hata.new(500, "sunucu"))   # çıktı: "hata 500: sunucu"
p Basarili.new("x").deconstruct_keys(nil)   # çıktı: {govde: "x"}

Basarili(govde) konumsal desen deconstruct'ı, Basarili(govde:) ise deconstruct_keys'i kullanır. Desen sözdiziminin tamamı için Ruby pattern matching yazısına bakın.

Eşitlik ve hash anahtarı olarak kullanım

Data nesneleri aynı sınıftan ve aynı içerikteyse == ile eşittir ve aynı hash değerini üretir; bu yüzden hash anahtarı olarak güvenle kullanılabilir. eql? ise alanları da eql? ile karşılaştırdığı için 1 ile 1.0 farklı sayılır. Aynı alanlara sahip olsa bile farklı Data sınıflarının nesneleri hiçbir zaman eşit değildir.

ruby
Konum = Data.define(:enlem, :boylam)
ziyaret = Hash.new(0)
ziyaret[Konum[39.93, 32.85]] += 1
ziyaret[Konum[39.93, 32.85]] += 1
p ziyaret                              # çıktı: {#<data Konum enlem=39.93, boylam=32.85> => 2}
p Konum[1, 2] == Konum[1.0, 2]         # çıktı: true
p Konum[1, 2].eql?(Konum[1.0, 2])      # çıktı: false
Diger = Data.define(:enlem, :boylam)
p Konum[1, 2] == Diger[1, 2]           # çıktı: false

Struct mı, Data mı, sınıf mı?

Struct mı, Data mı, sınıf mı? Alan değiştirme, Eksik argüman, Enumerable, == ve hash
ÖzellikStructDataElle yazılan sınıf
Alan değiştirmeEvet (yazıcılar var)Hayır, with ile kopyaSiz belirlersiniz
Eksik argümannil olurArgumentErrorSiz belirlersiniz
EnumerableEvetHayırİsterseniz include edersiniz
== ve hashİçeriğe göreİçeriğe göreVarsayılan olarak nesne kimliği
Pattern matchingdeconstruct ve deconstruct_keys vardeconstruct ve deconstruct_keys varKendiniz tanımlarsınız
Uygun olduğu yerDeğişen kayıtlar, geçici gruplamaDeğer nesneleri, sonuç türleriDavranışı ağır nesneler

Kısaca: bir nesnenin kimliği değil yalnızca değeri önemliyse ve oluşturulduktan sonra değişmemesi gerekiyorsa Data; alanların sonradan değişeceği hafif bir kayıt gerekiyorsa Struct; karmaşık davranış ve iç durum yönetimi gerekiyorsa normal sınıf seçin. Basit anahtar-değer verisi için Hash de yeterli olabilir.

Sık Sorulan Sorular

Data hangi sürümde geldi?

Ruby 3.2 NEWS dosyası Data'yı yeni çekirdek sınıf olarak listeliyor. Ruby 4.0 NEWS dosyasında Data veya Struct için öne çıkan bir değişiklik yer almıyor.

Struct.new'e string ad verirsem ne olur?

Belgelere göre ilk argüman bir string ise sınıf Struct::Ad adıyla Struct'ın altında oluşturulur. Genellikle sembollerle çağırıp sonucu kendi sabitinize atamak daha okunur bir ad verir.

Data nesnesine yeni metot ekleyebilir miyim?

Evet. Data.define bir blok alır ve blok yeni sınıfın içinde çalışır; Comparable gibi modülleri de burada ekleyebilirsiniz. Örnek için Comparable ve Enumerable yazısına bakın.

Struct'ı dondurabilir miyim?

freeze her nesnede olduğu gibi Struct'ta da çalışır, ama Data değiştirilemezliği sınıfın tasarımına yerleştirdiği için niyetinizi daha açık ifade eder.

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 class ve nesneler · Ruby pattern matching · frozen string ve freeze · anahtar kelime argümanları · Ruby Hash metotları