Ders 02 / 09
Kalıtım ve `super`
Boş bir alt sınıfın yöntemi üst sınıftan alması; superclass ve ancestors ile ata zinciri; yeniden tanımlamada super; initialize içinde super; çıplak super ile super(...) farkı.
Yayın Güncelleme
Bu derste
- Boş bir alt sınıfın yöntemini atadan aldığını superclass, ancestors ve instance_methods ile ölçer
- Yeniden tanımlamada super çağrısının üst sınıftaki tanıma indiğini super_method ile gösterir
- Çıplak super ile super(...) arasındaki argüman farkını ArgumentError üzerinden ayırır
İçindekiler
Önceki ders Hesap sınıfında bakiye_metni yazılı olduğunu, ama Vadeli adlı boş bir alt sınıfın da aynı yöntemi çağırıp yanıt alabildiğini söyledi. Alt sınıfta tek satır tanım yok. Yöntemi oraya kim getirdi ve aynı yöntemi alt sınıfta yeniden yazmak neyi değiştirir?
Bu dersin yazanı ata yazanıdır: tanım alıcıdan uzakta, zincirde bir üst sınıfta durur. Ders önce boş bir alt sınıfı ölçer, sonra super ile yeniden tanımlamanın tanımı nasıl el değiştirdiğine bakar.
Boş Bir Alt Sınıf Yöntemi Nereden Alır
class Vadeli < Hesap satırı Hesap’ı Vadeli’nin üst sınıfı yapar. Üst sınıfın yöntemleri alt sınıfın nesnelerine açıktır. Bu ilişkiye kalıtım denir (M08/K02). Aşağıdaki dosya Vadeli için çekirdekteki ust sinif gözlemini yeniden kurar. yazan.rb dosyası ilk derste verilen kuraldır ve aynen yüklenir.
# 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.rb bu derste yeni bir şey eklemez; kaynak olarak durur. Ölçüm onu çağıran dosyadadır.
# ust_sinif.rb — bos alt sinif yontemi atadan alir
require_relative "yazan"
class Hesap
def initialize(sahip, bakiye)
@sahip = sahip
@bakiye = bakiye
end
def bakiye_metni = "#{@sahip} #{@bakiye}"
end
class Vadeli < Hesap
end
vadeli = Vadeli.new("can", 50)
p vadeli.bakiye_metni
p yazan(vadeli, :bakiye_metni)
p Vadeli.instance_methods(false)
"can 50" ["ata", "Hesap"] []
vadeli.bakiye_metni can 50 verdi. yazan ata yazanını, kanıt olarak Hesap’ı buldu: tanımın sahibi alıcının sınıfı olan Vadeli değil, Hesap’tır. Vadeli.instance_methods(false) boş bir dizi verdi, yani Vadeli kendi başına hiçbir yöntem taşımıyor. Vadeli.new("can", 50) çağrısı da çalıştı, çünkü initialize de aynı yoldan Hesap’tan geldi. Alt sınıf boş olsa da nesne eksiksiz bir Hesap davranışı taşır.
SM4. Bu derste “üst sınıf”, class Vadeli < Hesap satırında < işaretinin sağında yazılan sınıftır; “alt sınıf” solda yazılandır. “Ata”, bir sınıfın zincirinde kendisinden sonra gelen her sınıf ya da modüldür. Kural, yöntemin sahibi alıcının sınıfı değilse yazanı ata sayar.
Ata Zinciri
Üst sınıf tek bir sınıfı gösterir. Ama Hesap’ın da bir üst sınıfı vardır ve zincir oradan devam eder. Aşağıdaki dosya zinciri iki yoldan sorar: superclass bir adım, ancestors bütün sırayı verir.
# zincir.rb — superclass ve ancestors
module Raporlanir
def rapor = "rapor: #{bakiye_metni}"
end
class Hesap
include Raporlanir
end
class Vadeli < Hesap
end
p Vadeli.superclass
p Hesap.superclass
p Vadeli.ancestors.take(5)
p Integer.ancestors.take(4)
Hesap Object [Vadeli, Hesap, Raporlanir, Object, Kernel] [Integer, Numeric, Comparable, Object]
Vadeli.superclass Hesap, Hesap.superclass Object verdi. ancestors listesi sınıfları ve modülleri bir arada taşır: alıcının sınıfı en başta, sonra Hesap, sonra Hesap’ın karıştırdığı Raporlanir modülü, sonra Object ve Kernel. Modüllerin zincirdeki yeri bir sonraki dersin konusudur; burada yalnız Raporlanir’in Hesap ile Object arasında durduğuna dikkat etmek yeter. Çekirdek sınıflar aynı biçimde zincire sahiptir: Integer önce Numeric’e, sonra Comparable modülüne, sonra Object’e bağlanır. Yani 1.between?(0, 2) çağrısındaki between? de Integer’ın değil, zincirdeki bir atanın yazdığı bir yöntemdir.
Yeniden Tanımlamada super
Alt sınıf üst sınıftaki bir yöntemi aynı adla yeniden yazabilir. Bu işleme yeniden tanımlama denir. Yeniden tanımlama üst sınıfın tanımını silmez; alıcının zincirinde alt sınıf önce geldiği için ileti önce ona ulaşır. super ise bu tanımdan, zincirde bir sonraki aynı adlı tanıma iner. Aşağıdaki dosya Vadeli2 sınıfında bakiye_metni’ni yeniden yazar ve gövdenin yarısını super’e bırakır.
# yeniden_tanim.rb — super zincirde bir sonraki tanima iner
class Hesap
def initialize(sahip, bakiye)
@sahip = sahip
@bakiye = bakiye
end
def bakiye_metni = "#{@sahip} #{@bakiye}"
end
class Vadeli2 < Hesap
def bakiye_metni = "vadeli #{super}"
end
v2 = Vadeli2.new("can", 50)
puts v2.bakiye_metni
p v2.method(:bakiye_metni).owner
p v2.method(:bakiye_metni).super_method.owner
p v2.method(:bakiye_metni).super_method.super_method
vadeli can 50 Vadeli2 Hesap nil
vadeli can 50 çıktısı iki parçanın birleşimidir: vadeli ön ekini Vadeli2 yazdı, can 50 kısmını super çağrısı üzerinden Hesap yazdı. method(:bakiye_metni).owner Vadeli2 verdi, super_method.owner Hesap. super_method zincirde bir sonraki tanımı yöntem nesnesi olarak verir. Hesap’tan sonra bakiye_metni tanımlayan bir ata kalmadığı için super_method.super_method nil döndü. Yani super çağrısı zincirde aşağı yürümeye devam eden bir atlamadır, ama yalnız aynı adlı bir tanım olduğu sürece.
Sahibi ve Kopyayı Ayırmak
Vadeli.instance_methods(false) boş çıktı, ama Vadeli yine de bakiye_metni’ne yanıt veriyor. İki soru ayrı tutulmalı: yöntem bu sınıfa mı yazılı, yoksa bu sınıfın nesneleri tarafından mı çağrılabiliyor? instance_methods(false) yalnız birincisini sorar. method_defined? ikincisini, instance_method(...).owner ise tanımın gerçekte nerede durduğunu söyler. Aşağıdaki dosya üç soruyu aynı sınıfa, ardından yeniden tanımlayan Vadeli2’ye yöneltir.
# sahip_sorusu.rb — yazili olmak ile cagrilabilir olmak ayri sorulardir
class Hesap
def initialize(sahip, bakiye)
@sahip = sahip
@bakiye = bakiye
end
def bakiye_metni = "#{@sahip} #{@bakiye}"
end
class Vadeli < Hesap
end
class Vadeli2 < Hesap
def bakiye_metni = "vadeli #{super}"
end
p Vadeli.method_defined?(:bakiye_metni)
p Vadeli.instance_methods(false)
p Vadeli.instance_method(:bakiye_metni).owner
p Vadeli2.instance_methods(false)
p Vadeli2.instance_method(:bakiye_metni).owner
p Hesap.instance_method(:bakiye_metni).owner
true [] Hesap [:bakiye_metni] Vadeli2 Hesap
Vadeli.method_defined?(:bakiye_metni) doğru verdi, Vadeli.instance_methods(false) ise boş kaldı; ikisi çelişmez. İlki zincirin tamamına bakar, ikincisi yalnız sınıfın kendi gövdesine. instance_method(...).owner Vadeli için Hesap verdi: Vadeli’nin nesneleri çağırdığında çalışan tanım Hesap’a yazılmıştır, Vadeli’de bir kopyası yoktur. Vadeli2 için tablo değişir: instance_methods(false) artık bakiye_metni’ni taşır ve owner Vadeli2’dir. Hesap’ın tanımı yerinde durur ve hâlâ Hesap sahipli çıkar. Yeniden tanımlama bir üzerine yazma değil, zincirin önüne ikinci bir tanım koymaktır.
initialize İçinde super
initialize da bir yöntemdir ve alt sınıfta yeniden tanımlanabilir. Alt sınıf kendi ek değişkenini kurarken üst sınıfın değişkenlerini kurmasını super ile ister. Aşağıdaki dosya iki alt sınıf tanımlar ve ikisinin initialize yöntemi aynı imzayı taşır, ama super farklı yazılır.
# initialize_super.rb — super(...) ile cıplak super
class Hesap
def initialize(sahip, bakiye)
@sahip = sahip
@bakiye = bakiye
end
def bakiye_metni = "#{@sahip} #{@bakiye}"
end
class Faizli < Hesap
def initialize(sahip, bakiye, oran)
super(sahip, bakiye)
@oran = oran
end
end
class Hatali < Hesap
def initialize(sahip, bakiye, oran)
super
@oran = oran
end
end
p Faizli.new("ece", 100, 5).instance_variables
begin
Hatali.new("ece", 100, 5)
rescue ArgumentError => e
puts e.class
end
[:@sahip, :@bakiye, :@oran] ArgumentError
Faizli iki argümanı super(sahip, bakiye) ile üst sınıfa iletti ve instance_variables üç değişkeni sırasıyla listeledi: üst sınıfın iki değişkeni, ardından alt sınıfın @oran’ı. Hatali sınıfında çıplak super yazıldı ve ArgumentError doğdu. Çıplak super, çağıran yöntemin aldığı argümanların hepsini aynen iletir; Hatali#initialize üç argüman aldığı için Hesap#initialize’a üç argüman gitti, oysa o ikisini bekler.
SM5. Bu derste super(...) parantezli çağrıyı, “çıplak super” ise parantezsiz ve argümansız yazılan çağrıyı anlatır. Parantezli çağrı üst sınıfa yalnız yazılan argümanları iletir; çıplak çağrı yöntemin kendi argümanlarını olduğu gibi iletir. Alt sınıfın imzası üst sınıfınkinden farklıysa çıplak super yanlış argümanlar iletebilir; bu durumda parantezli çağrı yazılır.
Parantezli ve parantezsiz biçimin farkı bir üçüncü yazımla tamamlanır: boş parantezli super(). Çıplak super yöntemin kendi argümanlarını iletir, super() ise hiç argüman iletmez. Fark ancak üst sınıftaki yöntem varsayılan değerli bir parametre taşıdığında görünür olur. Aşağıdaki dosya üç alt sınıfta üç biçimi yan yana koyar.
# super_bicimleri.rb — cıplak super, super() ve super(arguman)
class Hesap
def initialize(sahip, bakiye)
@sahip = sahip
@bakiye = bakiye
end
def etiket(onek = "hesap") = "#{onek}: #{@sahip}"
end
class Ciplak < Hesap
def etiket(onek = "ciplak") = super
end
class Bos < Hesap
def etiket(onek = "bos") = super()
end
class Acik < Hesap
def etiket(onek = "acik") = super("sabit")
end
puts Ciplak.new("can", 1).etiket
puts Bos.new("can", 1).etiket
puts Acik.new("can", 1).etiket
puts Ciplak.new("can", 1).etiket("elle")
ciplak: can hesap: can sabit: can elle: can
Çıplak super alt sınıfın onek parametresini üst sınıfa taşıdı; üst sınıftaki varsayılan hesap hiç kullanılmadı. super() hiçbir şey iletmedi, bu yüzden üst sınıf kendi varsayılanına döndü. super("sabit") yazılan değeri iletti ve alt sınıfın parametresini görmezden geldi. Dördüncü satır çıplak super’in ilettiğinin parametrenin o anki değeri olduğunu gösterir: çağrıya elle verilince üst sınıfa elle gitti. Çıplak super’i yararlı kılan bu, tehlikeli kılan da aynı özelliğidir; alt sınıfın imzası üst sınıfınkinden ayrıldığı anda iletilen şey de ayrılır.
Bir super çağrısı zincirde aynı adlı tanım bulamazsa ne olur? Aşağıdaki ölçüm bunu sorar.
# yetim_super.rb — atada tanimi olmayan super
class Hesap
def initialize(sahip, bakiye)
@sahip = sahip
@bakiye = bakiye
end
end
class Yetim < Hesap
def olmayan = super
end
begin
Yetim.new("a", 1).olmayan
rescue NoMethodError => e
puts e.class
end
NoMethodError
super zincirde olmayan adlı bir tanım bulamadı ve NoMethodError verdi. Yani super bir sihir değil, bir arama sorgusudur: zincirde bir sonraki aynı adlı tanımı arar, bulamazsa her iletinin başına gelen hata ona da gelir.
Zincir iki basamakla sınırlı değildir. Her alt sınıf kendi super çağrısıyla bir sonrakine iner ve çağrılar sırayla geri döner. Aşağıdaki dosya üç sınıflı bir zincir kurar ve super_method ile zinciri baştan sona yürüyüp sahiplerini toplar.
# uc_basamak.rb — super_method zinciri sonuna kadar yurunur
class Hesap
def initialize(sahip, bakiye)
@sahip = sahip
@bakiye = bakiye
end
def bakiye_metni = "#{@sahip} #{@bakiye}"
end
class Vadeli2 < Hesap
def bakiye_metni = "vadeli #{super}"
end
class Altin < Vadeli2
def bakiye_metni = "altin #{super}"
end
a = Altin.new("can", 50)
puts a.bakiye_metni
sahipler = []
yontem = a.method(:bakiye_metni)
while yontem
sahipler << yontem.owner
yontem = yontem.super_method
end
p sahipler
altin vadeli can 50 [Altin, Vadeli2, Hesap]
Metin, en içteki tanımdan dışa doğru birleşti: Hesap can 50 kısmını, Vadeli2 vadeli ekini, Altin altin ekini yazdı. Alıcıya en yakın tanım ilk çalışan, ama gövdesinin ortasında bir sonrakini bekleyen tanımdır. super_method döngüsü sahipleri aynı sırayla verdi ve nil döndüğünde durdu. Böylece super çağrısının görünmez bir mekanizma olmadığı, ancestors sırasının üzerinden okunan bir yol olduğu doğrulanır.
Zincir Çağrı Anında Aranır
Vadeli sınıfı tanımlandığında Hesap’ın tanımlarını kendine kopyalamadı; çağrı geldiğinde zincire bakar. Bunun gözlenebilir bir sonucu var: Vadeli kurulduktan sonra Hesap’a eklenen bir yöntem, Vadeli nesnesinde de hemen çağrılabilir. Aşağıdaki dosya aynı nesneye aynı soruyu, Hesap yeniden açılmadan önce ve sonra yöneltir.
# gec_eklenen.rb — atanin sonradan kazandigi yontem alt sinifta gorunur
class Hesap
def initialize(sahip, bakiye)
@sahip = sahip
@bakiye = bakiye
end
end
class Vadeli < Hesap
end
v = Vadeli.new("can", 50)
p v.respond_to?(:faiz)
class Hesap
def faiz = @bakiye / 10
end
p v.respond_to?(:faiz)
p v.faiz
p Vadeli.instance_methods(false)
false true 5 []
İlk respond_to? yanlış verdi, Hesap yeniden açıldıktan sonra doğru verdi ve v.faiz 5 döndü. v bu arada hiç yeniden kurulmadı. Vadeli.instance_methods(false) hâlâ boş: faiz alt sınıfa yazılmadı, yalnız zincirdeki bir atanın tanımı oldu. Aranan tanım çağrı anında zincirin o anki hâline göre bulunur.
SM13. Bu derste “zincir”, alıcının sınıfından başlayıp BasicObject’e kadar giden arama sırasıdır. Zincirin üyeleri sınıfa kopyalanmaz; arama her çağrıda sıfırdan yapılır. Bir atanın sonradan kazandığı yöntem bu yüzden alt sınıfın mevcut nesnelerinde de görünür.
Sınırlayıcı Ölçüm
Vadeli hiçbir şey yazmadı ve yazanı ata çıktı. Vadeli2 gövdenin yarısını atadan aldı, öbür yarısını kendisi yazdı. Kural bu ikinciyi nasıl sınıflandırır?
# yarim_ata.rb — super cagiran yeniden tanim programci sayilir
require_relative "yazan"
class Hesap
def initialize(sahip, bakiye)
@sahip = sahip
@bakiye = bakiye
end
def bakiye_metni = "#{@sahip} #{@bakiye}"
end
class Vadeli < Hesap
end
class Vadeli2 < Hesap
def bakiye_metni = "vadeli #{super}"
end
p yazan(Vadeli.new("can", 50), :bakiye_metni)
p yazan(Vadeli2.new("can", 50), :bakiye_metni)
["ata", "Hesap"] ["programci", "Vadeli2, def govdesi"]
Vadeli için ata, Vadeli2 için programcı çıktı. Vadeli2’nin gövdesi yarısını atadan alsa da kural bunu ölçmez. Kural zincirde ilk bulunan tanımı sayar ve o tanımın sahibini alıcının sınıfıyla karşılaştırır; zincirin geri kalanına bakmaz.
SM6. Bu derste “ilk bulunan tanım”, alıcıdan başlayan arama sırasında karşılaşılan ilk tanımdır. instance_method(ad) onu verir. super çağrısının zincirin devamında başka bir tanıma indiğini kural görmez, çünkü ilk tanımın gövdesine bakmaz.
Sonuç iki yönlüdür. Alt sınıfa tek bir def yazmak, yazanı atadan programcıya çevirir; gövdenin ne kadarının üst sınıfa devredildiği değişmez. Öte yandan yazanı programcı olan bir yöntemin gövdesi tamamen o programcıya ait olmak zorunda değildir. Yazan kategorisi, gövdenin kaynağını değil, zincirdeki ilk tanımın yerini söyler.
Özet
- Boş bir alt sınıf yöntemlerini üst sınıftan alır:
Vadeli.instance_methods(false)boştur veVadeli.new("can", 50).bakiye_metniatadan gelir. superclassbir adımı,ancestorsbütün zinciri verir;Vadeli.ancestors.take(5)alt sınıf, üst sınıf, karıştırılmış modül,ObjectveKernelsırasını gösterir.- Yeniden tanımlama üst sınıfın tanımını silmez;
superzincirde bir sonraki aynı adlı tanıma iner vesuper_method.ownero tanımın sahibini verir. initializeiçindesuper(sahip, bakiye)yalnız yazılan argümanları iletir; aynı imzada çıplaksuperargümanları aynen ilettiği içinArgumentErrorverir.- Bu dersin yazanı atadır:
ust sinifsatırındabakiye_metni’niHesapyazdı,Vadelihiçbir şey yazmadı. - Atada tanımı olmayan
superNoMethodErrorverir;superçağıran yeniden tanımlamada yazan programcı çıkar, çünkü kural ilk bulunan tanımı sayar.
Sonraki Adım
Hesap sınıfında rapor diye bir yöntem hiç yazılmadı, ama hesap.rapor çalıştı ve rapor: ece 100 verdi. Yöntem ne bir üst sınıftan geliyor, ne de sınıfın kendi gövdesinden. Zincirde Hesap ile Object arasında duran Raporlanir o yöntemi nasıl taşıyor? Sonraki ders, Modüller ve Karışımlar, bunu ölçer.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.