İçeriğe geç
academia.sh

Ders 01 / 09

Sınıf Tanımı ve Örnek Değişkenler

Bir yöntemi kimin yazdığını dört yazanla sınıflandırma; örnek değişkenlerinin nesnede, yöntemlerin sınıfta durması; initialize'ın özel oluşu; attr_reader ile yazılmış yöntemin def ile yazılmışa göre farkı.

Yayın Güncelleme

Bu derste

  • Bir yöntemin yazanını programcı, ata, kod ve kimse olarak sınıflandırır
  • Örnek değişkenlerinin nesnede, yöntemlerin sınıfta durduğunu instance_variables ve instance_methods ile ölçer
  • Aynı sınıfta def ile yazılmış yöntem ile attr_reader ile üretilmiş yöntemi iseq üzerinden ayırır
İçindekiler

Önceki kurs bir iletinin karşılığını nerede bulduğunu ölçtü: alıcının sınıfında, karıştırılmış bir modülde, verilen blokta ya da sözdiziminde (M13/K01). Kapanışında bir soru açık kaldı. hesap.bakiye_metni, nokta.x, ayar.renk ve vekil.sort aynı biçimde çağrılır, ama dördünün gövdesi dört ayrı yoldan gelir. Yöntemin belirli bir sahipte durduğu biliniyor; o tanımı kimin yazdığı bilinmiyor.

Bu kurs o soruyu ölçer. İlk ders bir sınıfın en küçük hâlini alır: örnek değişkenleri nerede durur, yöntemler nerede durur, ve bir yöntemin yazanı nasıl sınıflandırılır?

Dört Yazan ve Ölçüm Çekirdeği

Kursun her dersi aynı soruyu sorar: bu yöntemi kim yazdı? Yanıt dört kategoriden biridir. Programcı, gövdeyi alıcının kendi sınıfında def ile yazan kişidir. Ata, tanımı zincirde bir üst sınıfta ya da bir modülde bırakandır. Kod, tanımı çalışma zamanında bir çağrıyla üreten düzenektir: attr_reader, define_method, Struct. Kimse, zincirde hiç tanım bulunmayan ve iletinin method_missing ile yakalandığı durumdur. Çıktıda bu dört ad ASCII yazılır: programci, ata, kod, kimse.

SM1. Bu kursta “yazan”, bir yöntemin tanımının nereden geldiğini söyleyen kategoridir. Sınıflandırmayı etiket değil kural yapar: alıcının tekil sınıfında tanım yoksa kimse; tanımın sahibi alıcının sınıfı ya da tekil sınıfı değilse ata; tanımın iseq değeri nil ya da etiketi block ile başlıyorsa kod; hiçbiri değilse programcı. Kural tanımın biçimini okur, yazanın niyetini okumaz. Bu cümle kursun birkaç sınırlayıcı ölçümünün ortak yüzüdür.

Kural yazan.rb dosyasındadır. Kurs boyunca yeniden yazılmaz, her derste aynı biçimde yüklenir. Dosyadaki sıra önemlidir. Önce tanımın var olup olmadığı sorulur, çünkü tanımı olmayan bir yöntemin sahibi sorulamaz. Sonra sahip sorulur, çünkü sahibi alıcının sınıfı olmayan bir tanım zaten başka bir yerden gelmiştir. En son tanımın nasıl üretildiğine bakılır: yalnız alıcının kendi sınıfında duran tanımlar için gövdenin bir def mi yoksa bir çağrının ürünü mü olduğunu ayırmak gerekir.

# yazan.rb — bir yontemi kimin yazdigini siniflandiran yardimci

# Siniflandirma sirayla sorar: once alicinin zincirinde bir tanim var mi,
# sonra tanimi alicinin kendi sinifi mi tasiyor, sonra tanim nasil uretildi.
def yazan(alici, ad)
  zincir = alici.singleton_class
  unless zincir.method_defined?(ad) || zincir.private_method_defined?(ad)
    return ["kimse", "tanim yok"]
  end

  tanim = zincir.instance_method(ad)
  sahip = tanim.owner
  yer = sahip == zincir ? "tekil sinif" : sahip.to_s
  return ["ata", yer] unless sahip == alici.class || sahip == zincir

  iz = RubyVM::InstructionSequence.of(tanim)
  return ["kod", "#{yer}, ruby govdesi yok"] if iz.nil?
  return ["kod", "#{yer}, blok govdesi"] if iz.label.start_with?("block")

  ["programci", "#{yer}, def govdesi"]
end

yazan(alici, ad) iki değerli bir dizi döndürür: yazanın adı ve kanıt. Kanıt, tanımın sahibini ve gövdenin nasıl yazıldığını söyler. Dokuz gözlemi bir arada koşan ikinci dosya yazanlar.rb’dir. Her gözlem aynı biçimde çağrılan bir yöntemdir; ayrım yalnız çağrının arkasındadır.

# yazanlar.rb — dokuz gozlem: bu yontemi kim yazdi
require_relative "yazan"

module Raporlanir
  def rapor = "rapor: #{bakiye_metni}"
end

class Hesap
  include Raporlanir

  def initialize(sahip, bakiye)
    @sahip = sahip
    @bakiye = bakiye
  end

  def bakiye_metni = "#{@sahip} #{@bakiye}"
  def net = @bakiye - komisyon

  private

  def komisyon = 2
end

class Vadeli < Hesap
end

module Kilit
  def ac = "kilit acildi, #{super}"
end

class Kasa
  prepend Kilit
  def ac = "kasa acildi"
end

Nokta = Struct.new(:x, :y)

class Ayar
  %i[renk boyut].each do |ad|
    define_method(ad) { "#{ad} varsayilan" }
  end
end

class Vekil
  def initialize(hedef)
    @hedef = hedef
  end

  def method_missing(ad, *argumanlar, &blok)
    return super unless @hedef.respond_to?(ad)

    @hedef.public_send(ad, *argumanlar, &blok)
  end

  def respond_to_missing?(ad, ozel = false) = @hedef.respond_to?(ad, ozel) || super
end

class Model
  def self.alan(ad, varsayilan)
    define_method(ad) { instance_variable_get("@#{ad}") || varsayilan }
  end
end

class Urun < Model
  alan :ad, "adsiz"
  alan :fiyat, 0
end

hesap = Hesap.new("ece", 100)
gozlemler = {
  "durum okuma"   => [hesap, :bakiye_metni, -> { hesap.bakiye_metni }],
  "ust sinif"     => [Vadeli.new("can", 50), :bakiye_metni, -> { Vadeli.new("can", 50).bakiye_metni }],
  "karisim"       => [hesap, :rapor, -> { hesap.rapor }],
  "ozel yardimci" => [hesap, :komisyon, -> { hesap.net }],
  "one eklenen"   => [Kasa.new, :ac, -> { Kasa.new.ac }],
  "uye okuyucu"   => [Nokta.new(3, 4), :x, -> { Nokta.new(3, 4).x }],
  "uretilen"      => [Ayar.new, :renk, -> { Ayar.new.renk }],
  "vekil"         => [Vekil.new([3, 1, 2]), :sort, -> { Vekil.new([3, 1, 2]).sort }],
  "bildirim"      => [Urun.new, :fiyat, -> { Urun.new.fiyat }],
}

adet = { "programci" => 0, "ata" => 0, "kod" => 0, "kimse" => 0 }

printf("%-15s%-11s%-27s%s\n", "gozlem", "yazan", "kanit", "sonuc")
gozlemler.each do |ad, (alici, ileti, cagri)|
  tur, kanit = yazan(alici, ileti)
  adet[tur] += 1
  printf("%-15s%-11s%-27s%s\n", ad, tur, kanit, cagri.call.inspect)
end

puts
adet.each { |tur, n| printf("%-11s%d\n", tur, n) }
puts "toplam gozlem: #{adet.values.sum}"
gozlem         yazan      kanit                      sonuc
durum okuma    programci  Hesap, def govdesi         "ece 100"
ust sinif      ata        Hesap                      "can 50"
karisim        ata        Raporlanir                 "rapor: ece 100"
ozel yardimci  programci  Hesap, def govdesi         98
one eklenen    ata        Kilit                      "kilit acildi, kasa acildi"
uye okuyucu    kod        Nokta, ruby govdesi yok    3
uretilen       kod        Ayar, blok govdesi         "renk varsayilan"
vekil          kimse      tanim yok                  [1, 2, 3]
bildirim       kod        Urun, blok govdesi         0

programci  2
ata        3
kod        3
kimse      1
toplam gozlem: 9

Dokuz gözlemin 2’sini programcı, 3’ünü bir ata yazdı; 3’ünü bir kod üretti; 1’inde tanım yoktu, iletiyi method_missing yakaladı. Bu sayılar kurs boyunca değişmez. Programcı durum okuma ve ozel yardimci satırlarını yazdı: ikisinin gövdesi Hesap içinde bir def ile durur. Ata ust sinif, karisim ve one eklenen satırlarını yazdı; üçünün tanımı alıcıdan uzakta, Hesap’ta, Raporlanir’de ve Kilit’te durur. uye okuyucu, uretilen ve bildirim satırlarında tanımı bir çağrı üretti. vekil satırında tanım yok ve sonuç yine de döndü.

Bu dersin payı ilk programcı satırıdır: durum okuma. Sonraki dersler ata, kod ve kimse yazanlarını sırayla açar; bu ders ise programcının yazdığı en sade yapıyı, bir sınıfı ve onun örnek değişkenlerini ölçer.

Durum Nesnede, Davranış Sınıfta

Hesap sınıfının initialize yöntemi iki değer alır ve ikisini örnek değişkenine atar. Örnek değişkeni @ ile başlayan, nesneye bağlı bir değişkendir (M13/K01). Aşağıdaki dosya sınıfın küçük bir kesitini yeniden tanımlar, çünkü yazanlar.rb yüklendiğinde dokuz satırlık tabloyu basar; bu kesit onu tekrarlamadan çalışır.

# durum.rb — ornek degiskenleri nesnede durur
class Hesap
  def initialize(sahip, bakiye)
    @sahip = sahip
    @bakiye = bakiye
  end
end

hesap = Hesap.new("ece", 100)
p hesap.instance_variables
p hesap.instance_variable_defined?(:@bakiye)
p Hesap.instance_methods(false)
begin
  hesap.bakiye
rescue NoMethodError => e
  puts e.class
end
[:@sahip, :@bakiye]
true
[]
NoMethodError

instance_variables iki değişkeni atama sırasıyla listeledi: @sahip ve @bakiye. Değişkenler Hesap sınıfında değil, hesap nesnesinde durur. Hesap.instance_methods(false) ise boş bir dizi verdi: sınıfın genel bir yöntemi yok. initialize özeldir ve false argümanı yalnız sınıfın kendi tanımlarını sorar. Bu yüzden hesap.bakiye çağrısı NoMethodError verdi: değişken nesnenin içindedir, ama onu okuyan bir yöntem yazılmadığı için dışarıdan görünmez.

SM2. Bu derste “örnek değişkeni”, bir nesneye bağlı olan ve @ ile başlayan değişkendir. Değişkenin bir nesnede bulunması, o değişkeni okuyan bir yöntemin bulunduğu anlamına gelmez. Durum nesnede, davranış sınıfta durur; iki taraf @bakiye gibi bir adla birbirine bağlanır.

Bir örnek değişkeni, atanmadıkça nesnede hiç oluşmaz. Aşağıdaki dosya bunu ölçer: okunan değişken henüz atanmamıştır.

# atanmamis.rb — atanmamis ornek degiskeni nil okunur ve listede yoktur
class Hesap2
  def oku = @yok
  def yaz(deger) = @yok = deger
end

nesne = Hesap2.new
p nesne.oku
p nesne.instance_variables
nesne.yaz(5)
p nesne.oku
p nesne.instance_variables
nil
[]
5
[:@yok]

Atanmamış değişken nil olarak okundu, ama instance_variables listesine girmedi. Atama yapıldıktan sonra değişken hem okundu hem listelendi. Okumak nesnede bir şey oluşturmaz; yalnız atama oluşturur. Bu davranış Hesap2 gibi bir sınıfın nesneleri arasında bir fark da doğurur: yalnız yaz çağrılmış bir nesne @yok değişkenini taşır, çağrılmamış bir nesne taşımaz. İki nesne aynı sınıftan gelse de örnek değişkenlerinin listesi aynı olmak zorunda değildir; liste, sınıfın değil, nesnenin geçmişini yansıtır. Hata vermeyen bir okuma bu yüzden yazım hatasını gizleyebilir: @bakyie yazılırsa Ruby sessizce nil verir.

İki Nesne, Tek Sınıf

Durumun nesnede, davranışın sınıfta durması iki nesneyle ölçülür. Aşağıdaki dosya aynı sınıftan iki hesap üretir ve iki şeyi karşılaştırır: yöntemin sahibini ve örnek değişkenlerinin adlarını.

# iki.rb — iki nesne ayni yontemi paylasir, degerleri paylasmaz
class Hesap
  def initialize(sahip, bakiye)
    @sahip = sahip
    @bakiye = bakiye
  end

  def bakiye_metni = "#{@sahip} #{@bakiye}"
end

a = Hesap.new("ece", 100)
b = Hesap.new("can", 50)
puts a.bakiye_metni
puts b.bakiye_metni
p a.method(:bakiye_metni).owner == b.method(:bakiye_metni).owner
p a.instance_variables == b.instance_variables
p Hesap.instance_of?(Class)
p a.class
ece 100
can 50
true
true
true
Hesap

Aynı yöntem iki nesnede iki ayrı sonuç verdi: ece 100 ve can 50. İki nesnenin yönteminin sahibi aynıdır, ikisinin örnek değişkenlerinin adları da aynıdır; ayrılan yalnız değerlerdir. Yöntem gövdesi bir kez, sınıfta yazılır ve her çağrıda çağrıyı alan nesnenin değişkenlerini okur. Hesap.instance_of?(Class) true verdi: class Hesap satırı Hesap adında bir sabit kurar ve sabitin gösterdiği şey Class türünde bir nesnedir. a.class bu nesneyi geri döndürür. Sınıf, yöntemlerin durduğu yer olduğu kadar, nesneleri üreten bir nesnedir de.

Bu ayrımın pratik bir sonucu vardır. Yöntem sayısı nesne sayısıyla çoğalmaz: bin hesap yaratıldığında bakiye_metni yine tek bir yerde, tek bir kez durur. Nesne başına çoğalan şey yalnız örnek değişkenlerinin değerleridir. Bir yöntemin yazanını sormak bu yüzden hep sınıfa ya da zincire yönelir; nesnenin kendisi yalnız durumu taşır ve yazanı soran ölçüm onun değişkenlerine bakmaz.

initialize Özeldir ve Argüman Sayısını Denetler

Hesap.new("ece", 100) çağrısı yeni bir nesne üretir ve initialize yöntemini bu nesnede çağırır. initialize’ı çağıran new’dir, dışarıdaki kod değil. Sınıf bunu özel yöntem olarak saklar; kod hesap.initialize(...) yazarak onu çağıramaz. Aşağıdaki dosya çekirdekteki Hesap’ın tam tanımını kullanır, çünkü özel yöntem komisyon da burada ölçülür.

# ozel.rb — initialize ozel bir yontemdir
require_relative "yazan"

class Hesap
  def initialize(sahip, bakiye)
    @sahip = sahip
    @bakiye = bakiye
  end

  def bakiye_metni = "#{@sahip} #{@bakiye}"
  def net = @bakiye - komisyon

  private

  def komisyon = 2
end

hesap = Hesap.new("ece", 100)
p hesap.respond_to?(:initialize)
p Hesap.private_instance_methods(false).sort
p yazan(hesap, :initialize)
begin
  Hesap.new("a")
rescue ArgumentError => e
  puts e.class
end
false
[:initialize, :komisyon]
["programci", "Hesap, def govdesi"]
ArgumentError

respond_to?(:initialize) false verdi: özel yöntem dışarıdan sorulduğunda görünmez. private_instance_methods(false) sınıfın iki özel yöntemini listeledi: initialize ve komisyon. initialize için yazan programcıyı buldu, çünkü gövdeyi sınıfta bir def yazdı. Hesap.new("a") tek bir argümanla çağrıldığında ArgumentError verdi: initialize iki argüman bekler ve Ruby sayıyı çağrı anında denetler.

SM3. Bu derste “argüman sayısı”, bir yöntemin kabul ettiği argümanların sayısıdır; Hesap.new bu sayıyı initialize tanımından alır. initialize(sahip, bakiye) iki argüman ister; Hesap.new("a") bu yüzden ArgumentError verdi ve hiçbir nesne döndürmedi. Ruby sayıyı gövde koşmadan önce denetler, @sahip ataması da yapılmadı. Argüman sayısı böylece nesnenin ilk durumunu da sınırlar: iki örnek değişkenini kuran yapıcı, iki değer verilmeden çalışmaz. initialize içinde atanmayan bir değişken, sonradan başka bir yöntem tarafından atanmadıkça o nesnede hiç var olmaz; sınıfın yapıcısı bu yüzden nesnenin ilk durumunu kurduğu yerdir ve değişken adlarını bir arada görmek için en güvenilir noktadır.

Sınıfı yeniden açmak, aynı sınıf adıyla bir class bloğu daha yazmaktır. Mevcut tanım silinmez, yenisi eklenir (M12/K03). Bir sınıfın tanımı bu yüzden tek bir yerde olmak zorunda değildir; aşağıdaki ölçüm sınıfı yeniden açar.

Sınırlayıcı Ölçüm

Aynı sınıfta, aynı sahipte iki yazan bir arada bulunabilir. Hesap yeniden açılır ve attr_reader :sahip eklenir. attr_reader bir okuyucu yöntem üretir: sahip yöntemi @sahip’i döndürür. Önceki kurslarda (M08/K01) derleyicinin bir sınıfa ne eklediği ölçülmüştü. Burada soru farklıdır: sınıfta duran iki yöntemden birini bir def yazdı, ötekini bir çağrı üretti.

# yeniden.rb — ayni sinifta iki yazan: def ve attr_reader
require_relative "yazan"

class Hesap
  def initialize(sahip, bakiye)
    @sahip = sahip
    @bakiye = bakiye
  end

  def bakiye_metni = "#{@sahip} #{@bakiye}"
  def net = @bakiye - 2
end

class Hesap
  attr_reader :sahip
end

hesap = Hesap.new("ece", 100)
p yazan(hesap, :sahip)
p yazan(hesap, :bakiye_metni)
p Hesap.instance_methods(false).sort
[:sahip, :bakiye_metni].each do |ad|
  tanim = Hesap.instance_method(ad)
  puts "#{ad} #{tanim.owner} #{tanim.source_location.nil?} #{RubyVM::InstructionSequence.of(tanim).nil?}"
end
["kod", "Hesap, ruby govdesi yok"]
["programci", "Hesap, def govdesi"]
[:bakiye_metni, :net, :sahip]
sahip Hesap false true
bakiye_metni Hesap false false

yazan(hesap, :sahip) kodu, yazan(hesap, :bakiye_metni) programcıyı buldu. İkisinin sahibi de Hesap’tır ve ikisi de instance_methods(false) listesinde görünür: [:bakiye_metni, :net, :sahip]. Sahibi bilmek yazanı söylemez. Son döngü ayrımın nereden geldiğini gösterir. source_location iki yöntem için de nil değildir: attr_reader çağrısının satırı kaynak yeri olarak kayıtlıdır. Ayrımı iseq yapar. def ile yazılmış bakiye_metni için iseq vardır, attr_reader ile üretilmiş sahip için nil’dir. Kural bu yüzden kaynak yerine değil iseq’e bakar.

Bu ölçüm, sahibin yazanı belirlemediğini gösterir: iki yöntem aynı sahipte durdu ve iki ayrı yazanla çıktı. Aynı ölçüm sınıfın bir kimlik kartı olmadığını da gösterir. Hesap tek bir yazan taşımaz; her yöntemin yazanı ayrı ölçülür. Sınıf düzeyinde “bu sınıfı programcı yazdı” diyen bir kategori yoktur.

Özet

  • Kursun ölçüsü, bir yöntemi kimin yazdığıdır: yazan programcı, ata, kod ya da kimse olabilir ve kararı yazan.rb içindeki kural verir.
  • Dokuz gözlemin 2’sini programcı, 3’ünü ata yazdı; 3’ünü kod üretti; 1’inde tanım yoktu ve method_missing yakaladı.
  • Örnek değişkenleri nesnede, yöntemler sınıfta durur; okuyucu yoksa hesap.bakiye NoMethodError verir, atanmamış değişken nil okunur ve instance_variables’a girmez.
  • initialize özel bir yöntemdir ve argüman sayısını denetler; Hesap.new("a") ArgumentError verir.
  • Bu dersin yazanı programcıdır: durum okuma satırında bakiye_metni’ni Hesap’ın içinde bir def yazdı.
  • Aynı sınıfta def ile yazılmış yöntem programcı, attr_reader ile üretilmiş yöntem kod çıkar; ikisinin sahibi aynıdır ve ayrımı iseq yapar.

Sonraki Adım

Hesap içinde bakiye_metni yazılı, ama Vadeli adlı boş bir alt sınıf da aynı yöntemi çağırıp yanıt alabiliyor. Alt sınıfta tek satır tanım yok. Yöntemi alt sınıfa kim getiriyor ve aynı yöntemi alt sınıfta yeniden yazmak neyi değiştiriyor? Sonraki ders, Kalıtım ve super, bunu ölçer.

İlerlemeni kaydetmek ve not almak için Giriş yap

Notlarım

Not almak için giriş yapmalısın.

Aramak için yazmaya başlayın.

↑↓ ↵ Esc gezin · aç · kapat