Ders 05 / 09
Yöntem Arama Zinciri
Bir ileti alıcıya ulaşınca yöntemin ata zincirinde nasıl arandığı; önüne eklenen modül, super_method, zincirin sonu, sınıf yöntemlerinin zinciri ve tek nesneye eklenen tekil yöntem.
Yayın Güncelleme
Bu derste
- Bir yöntem çağrısının ata zincirinde soldan sağa arandığını ancestors ve method(...).owner ile gösterir
- prepend ile eklenen modülün sınıfın önüne geçtiğini ve super ile sınıfa indiğini ölçer
- Sınıf yöntemlerinin tekil sınıf zincirinden geldiğini ölçer
- Tek bir nesneye eklenen tekil yöntemin zincirin başına girdiğini ve diğer nesneleri etkilemediğini gösterir
İçindekiler
Önceki ders sonunda Kasa sınıfında ve karıştırılmış Kilit modülünde aynı adlı ac yönteminin olduğunu, ve Kasa.new.ac çağrısında hangisinin çalıştığını sormayı bıraktı. Önceki üç ders bu sorunun parçalarını ayrı ayrı yanıtladı: sınıf kendi tanımını yazar, üst sınıf devralır, modül karıştırılır. Hepsi aynı arama sırasının içinde.
Bu dersin yazanı ata yazanıdır, ama bu kez atanın bir üst sınıf mı yoksa bir modül mü olduğu önemli değildir. M13/K01’de yazanı tanımın yazarı üzerinden ölçmüştük; burada yazar sorusu aynı kalır, arama sırasının kendisi ölçülür. Tanımın üst sınıfta mı modülde mi durduğu ayrılmaz: ikisi tek bir kategoridir, ata.
Önüne Eklenen Modül
include modülü sınıfın üstüne yerleştirir, yani sınıf kazanır. prepend ise modülü sınıfın önüne yerleştirir; alıcıya ulaşan ileti önce modüle gider. Aşağıdaki dosya bir Kilit modülünü Kasa’ya prepend ile ekler.
# 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
# one_eklenen.rb — prepend ile modul sinifin onune gecer
require_relative "yazan"
module Kilit
def ac = "kilit acildi, #{super}"
end
class Kasa
prepend Kilit
def ac = "kasa acildi"
end
kasa = Kasa.new
puts kasa.ac
p yazan(kasa, :ac)
p Kasa.ancestors.take(3)
p kasa.method(:ac).owner
p kasa.method(:ac).super_method.owner
kilit acildi, kasa acildi ["ata", "Kilit"] [Kilit, Kasa, Object] Kilit Kasa
Kasa.new.ac iki parçayı birleştirdi: önce Kilit’in ön eki, ardından super ile Kasa’nın kendi tanımı. yazan ata yazanını ve kanıt olarak Kilit’i verdi: zincirde ilk bulunan tanım Kilit’te, çünkü Kasa.ancestors listesinde Kilit Kasa’nın önünde durur. method(:ac).owner Kilit, super_method.owner Kasa çıktı. Arama soldan sağa yürür, ilk eşleşme çalışır ve super o noktadan zincirde bir adım ilerler.
SM11. Bu derste “yöntem arama”, bir ileti alıcıya ulaştığında Ruby’nin alıcının ata zincirini ancestors sırasıyla soldan sağa gezip adı eşleşen ilk tanımı bulmasıdır. “Önüne eklemek”, prepend ile bir modülü sınıfın önüne yerleştirmektir; include ise modülü sınıfın arkasına yerleştirir.
include ile prepend Yan Yana
İki anahtar sözcüğün farkı, aynı modülle aynı sınıfı iki kez kurup zincirleri karşılaştırınca görünür. Aşağıdaki dosya Kilit’i bir sınıfa include, öbürüne prepend ile ekler; ardından iki modülü art arda önüne ekleyerek sırayı da sınar.
# include_prepend.rb — iki anahtar sozcugun zincirdeki yeri
module Kilit
def ac = "kilit acildi"
end
module Alarm
def ac = "alarm, #{super}"
end
class KasaI
include Kilit
def ac = "kasa acildi"
end
class KasaP
prepend Kilit
def ac = "kasa acildi"
end
class KasaIki
prepend Kilit
prepend Alarm
def ac = "kasa acildi"
end
p KasaI.ancestors.take(3)
puts KasaI.new.ac
p KasaP.ancestors.take(3)
puts KasaP.new.ac
p KasaIki.ancestors.take(4)
puts KasaIki.new.ac
[KasaI, Kilit, Object] kasa acildi [Kilit, KasaP, Object] kilit acildi [Alarm, Kilit, KasaIki, Object] alarm, kilit acildi
include ile KasaI zincirinde sınıf önce geldi ve Kilit arkasına düştü; ac çağrısı sınıfın kendi tanımında durdu, modülün tanımına hiç ulaşmadı. prepend ile KasaP zincirinde Kilit sınıfın önüne geçti, ama Kilit#ac super çağırmadığı için bu kez sınıfın tanımı çalışmadı. Yani önüne eklenen modül, super yazmazsa sınıfı tamamen gölgeler. KasaIki zincirinde ikinci prepend ilkinin önüne girdi: sonradan eklenen alıcıya en yakın durur, tıpkı include sırasında olduğu gibi. Üç sınıfta da kural aynıdır; yalnız halkanın sınıfın önüne mi arkasına mı gireceği değişir.
Zinciri sonuna kadar yürümek için super_method bir döngüde de kullanılabilir. Aşağıdaki dosya tekil yöntemi, önüne eklenen modülü ve sınıfı üst üste koyar ve her halkanın sahibini sırayla toplar. Tekil sınıfın adı bellek adresi taşıdığı için sahip, çıktıya adıyla değil :tekil etiketiyle yazılır.
# zinciri_yuru.rb — super_method ile halkalari saymak
module Kilit
def ac = "kilit acildi, #{super}"
end
class Kasa
prepend Kilit
def ac = "kasa acildi"
end
k = Kasa.new
def k.ac = "tekil, #{super}"
sahipler = []
yontem = k.method(:ac)
while yontem
sahip = yontem.owner
sahipler << (sahip == k.singleton_class ? :tekil : sahip)
yontem = yontem.super_method
end
p sahipler
puts k.ac
p Kasa.new.method(:ac).super_method.super_method
[:tekil, Kilit, Kasa] tekil, kilit acildi, kasa acildi nil
Döngü üç halka buldu: tekil sınıf, Kilit ve Kasa; bu sıra ancestors okuması ile birebir örtüşür. puts k.ac her halkanın kendi parçasını aynı sırayla birleştirdi. Tekil yöntemi olmayan bir Kasa nesnesinde zincir iki halkadır ve super_method.super_method nil verir: Kasa’dan sonra zincirde ac tanımlayan başka halka yoktur.
Zincirin Sonu
Zincir sonsuz değildir. Object’ten türeyen her sınıfın zinciri Object, Kernel ve BasicObject ile biter. Aşağıdaki dosya Hesap sınıfının zincirini sonuna kadar okur ve son duraktaki özel bir yöntemi sorar.
# zincirin_sonu.rb — zincir BasicObject ile biter class Hesap end p Hesap.ancestors p Hesap.ancestors.last p Hesap.ancestors.size p BasicObject.private_method_defined?(:method_missing) p Kernel.private_method_defined?(:puts)
[Hesap, Object, Kernel, BasicObject] BasicObject 4 true true
Boş bir Hesap sınıfının zinciri [Hesap, Object, Kernel, BasicObject] dizisidir ve ancestors.size dördü verdi; karıştırılmış bir modül olsaydı beş olurdu. BasicObject zincirin son durağıdır. method_missing orada özel olarak tanımlıdır: zincirde ad bulunamazsa son çare olarak bu yöntem çağrılır ve varsayılan davranışı NoMethodError yükseltmektir. puts ise Kernel’de özel olarak durur; bu yüzden her nesnenin içinden alıcısız çağrılabilir, ama "x".puts yazılamaz.
Zincirin sonuna ulaşıp ad bulunamazsa ne olacağı BasicObject’in özel method_missing tanımına bağlıdır. Bir sınıf bu yöntemi kendi gövdesinde yeniden tanımlarsa, eşleşmeyen ileti ona düşer. Aşağıdaki dosya iki sınıfın aynı eşleşmeyen çağrıya verdiği yanıtı karşılaştırır.
# son_care.rb — zincirde bulunamayan ad method_missing'e duser
class Hesap
end
class Hesap2
def method_missing(ad, *) = "yok: #{ad}"
def respond_to_missing?(ad, ozel = false) = false
end
begin
Hesap.new.baska
rescue NoMethodError => e
puts e.class
end
puts Hesap2.new.baska
p Hesap2.new.respond_to?(:baska)
p Hesap2.instance_method(:method_missing).owner
p Hesap.instance_method(:method_missing).owner
NoMethodError yok: baska false Hesap2 BasicObject
Hesap için zincir boyunca ad bulunamadı, iletiyi BasicObject#method_missing aldı ve NoMethodError yükseltti. Hesap2 kendi method_missing’ini yazdığı için ileti zincirin sonuna inmeden, sınıfın kendi tanımına düştü. respond_to? yanlış verdi; bir nesnenin böyle yanıt vermesi, onun yöntem tanımlamış olduğu anlamına gelmez. owner karşılaştırması Hesap2 için sınıfın kendisini, Hesap için BasicObject’i gösterdi. Yani zincirin sonu bir varsayılan davranıştır ve sınıf onu zincirin herhangi bir halkasında değiştirebilir.
SM16. Bu derste “zincirin sonu”, alıcının zincirinde adın bulunamadığı durumda iletiyi alan method_missing çağrısıdır. Varsayılan tanımı BasicObject’tedir ve NoMethodError yükseltir; zincirde daha yakın bir halka onu yeniden tanımlayabilir.
Sınıf Yöntemlerinin Zinciri
Hesap.new çağrısındaki new bir yöntemdir ve alıcısı Hesap sınıfının kendisidir. Sınıfın da bir zinciri vardır. Bu zincir, sınıfa özel tekil sınıf üzerinden kurulur.
# sinif_zinciri.rb — sinifin kendi zinciri class Hesap end p Hesap.method(:new).owner p Hesap.singleton_class p Hesap.singleton_class.ancestors.take(4)
Class #<Class:Hesap> [#<Class:Hesap>, #<Class:Object>, #<Class:BasicObject>, Class]
Hesap.new için new yönteminin sahibi Class çıktı: sınıf yöntemleri de aynı arama kuralıyla bulunur, yalnız alıcı bir sınıftır ve zincir onun tekil sınıfından başlar. Sınıfın tekil sınıfı #<Class:Hesap> adıyla yazdırılır. Zincir bu tekil sınıftan Object’in tekil sınıfına, BasicObject’in tekil sınıfına ve en sonunda Class’a doğru yürür. Yani üst sınıfta tanımlı bir sınıf yöntemi, alt sınıftan çağrılınca da bulunur.
Zincirin tekil sınıflardan sonra neden Class’a bağlandığı ve alt sınıfın üst sınıfın sınıf yöntemini nasıl bulduğu iki ayrı soru. Aşağıdaki dosya ikisini ölçer; karşılaştırmalar çıktıdaki adla değil, eşitlik sorusuyla yapılır, böylece sonuç adın yazılışına değil nesnelerin kendisine dayanır.
# sinif_zinciri_ek.rb — sinif yontemi alt siniftan bulunur
class Hesap
def self.varsayilan = new("-", 0)
def initialize(sahip, bakiye)
@sahip = sahip
@bakiye = bakiye
end
end
class Vadeli < Hesap
end
p Vadeli.varsayilan.class
p Vadeli.method(:varsayilan).owner == Hesap.singleton_class
p Vadeli.singleton_class.superclass == Hesap.singleton_class
p Vadeli.singleton_methods
p Vadeli.singleton_methods(false)
p BasicObject.singleton_class.superclass
p Hesap.singleton_class.ancestors.drop(3)
Vadeli true true [:varsayilan] [] Class [Class, Module, Object, Kernel, BasicObject]
Vadeli.varsayilan bir Vadeli nesnesi üretti: yöntem Hesap’ın tekil sınıfında yazılı olduğu hâlde alıcı Vadeli olduğu için new ona uygulandı. owner karşılaştırması tanımın Hesap’ın tekil sınıfında durduğunu, superclass karşılaştırması Vadeli’nin tekil sınıfının Hesap’ın tekil sınıfına bağlandığını gösterdi. singleton_methods üst sınıftan gelen sınıf yöntemini de saydı, singleton_methods(false) ise boş kaldı. Zincirin tekil sınıflardan sonra Class’a geçmesinin nedeni de buradan okunur: BasicObject’in tekil sınıfının üst sınıfı Class’tır, o noktadan sonra zincir Module, Object, Kernel ve BasicObject ile sıradan sınıf zincirine karışır.
Sınırlayıcı Ölçüm
Şimdiye dek zincirler sınıflardan ve modüllerden kuruluydu. Bir nesnenin zinciri o nesneye özgü bir halka da taşıyabilir: tekil sınıf. Aşağıdaki dosya tek bir Kasa nesnesine bir tekil yöntem ekler ve bir modül extend eder.
# tekil.rb — tek nesneye ozgu halka
require_relative "yazan"
module Ek
def ac = "ek, #{super}"
end
module Kilit
def ac = "kilit acildi, #{super}"
end
class Kasa
prepend Kilit
def ac = "kasa acildi"
end
k = Kasa.new
def k.ac = "tekil, #{super}"
puts k.ac
puts Kasa.new.ac
p yazan(k, :ac)
p k.singleton_methods
j = Kasa.new
j.extend(Ek)
p j.singleton_class.ancestors.drop(1).take(4)
puts j.ac
tekil, kilit acildi, kasa acildi kilit acildi, kasa acildi ["programci", "tekil sinif, def govdesi"] [:ac] [Ek, Kilit, Kasa, Object] ek, kilit acildi, kasa acildi
def k.ac tek bir nesneye bir tekil yöntem ekledi. k.ac önce kendi ön ekini, sonra zincirin geri kalanını verdi; yeni üretilen bir Kasa ise etkilenmedi. Yazan programcı çıktı, kanıt olarak tekil sinif, def govdesi: kuralın dilinde tanımın sahibi nesnenin tekil sınıfıdır. k.singleton_methods yalnız [:ac] verdi. j.extend(Ek) ise Ek modülünü j nesnesinin tekil sınıfına karıştırdı ve j.singleton_class.ancestors.drop(1) listesi Ek’le başlayıp ardından Kilit, Kasa, Object ile devam etti. Zincirin başındaki tekil sınıf adı çıktıya yazılmadı, çünkü o ad bellek adresi taşır.
SM12. Bu derste “tekil sınıf”, tek bir nesneye ya da bir sınıfa özgü, ata zincirinin en başına eklenen gizli sınıftır. “Tekil yöntem”, o sınıfta tanımlanan yöntemdir. Başka nesneler bu zinciri paylaşmaz.
Yöntemi Kimin Verdiğini Sormak
Zincir okunurken sık yapılan bir ayrım, method ile instance_method arasındadır. İlki nesneye, ikincisi sınıfa sorar; bu yüzden tekil yöntemi yalnız ilki görür. Aşağıdaki dosya ikisini ve Kernel’den gelen bir yöntemi yan yana koyar.
# kim_verdi.rb — method ile instance_method farki module Kilit def ac = "kilit acildi" end class Kasa prepend Kilit def ac = "kasa acildi" end k = Kasa.new def k.ac = "tekil acildi" p k.method(:ac).owner == k.singleton_class p Kasa.instance_method(:ac).owner p Kasa.new.method(:ac).owner p k.method(:puts).owner p k.method(:class).owner p Kasa.instance_method(:ac).super_method.owner
true Kilit Kilit Kernel Kernel Kasa
method nesneye sorduğu için ilk satırda tekil sınıfın sahip çıktığı görüldü. instance_method yalnız sınıfın zincirine baktı; tekil yöntem o zincirde olmadığından cevap Kilit oldu, yani önüne eklenen modül sınıfın önünde durduğu için sahip olarak o göründü. Kasa.new ile tekil yöntemi olmayan bir nesnede de method aynı cevabı verdi. puts ve class için sahip Kernel çıktı: bu iki yöntemi yazan ne programcıdır ne sınıf, Object zincirine karışan Kernel modülüdür. Son satır, Kilit#ac’ın bir altındaki halkayı sorar ve Kasa#ac’ı bulur; tek halkalık bir önüne ekleme bile super_method ile iki halkalık bir zincir olarak okunabilir.
Bu soruların hepsi aynı alışkanlığa çıkar: bir yöntemin hangi tanımla çalışacağı, tanımın metne göre nerede yazıldığıyla değil, alıcının zincirinde hangi halkanın önce geldiğiyle belirlenir. Sahip sorusu, o halkanın adını verir.
Aramayı Okumanın Üç Kuralı
Yukarıdaki ölçümlerden arama için üç kural çıkar. Birincisi, sıra alıcının zincirinden gelir: yöntemin hangi sınıfta yazıldığına değil, alıcının hangi zincire sahip olduğuna bakılır. İkincisi, zincirdeki ilk eşleşmeyi alır, öbür tanımlar yalnız super ile yeniden çağrılırsa çalışır. Üçüncüsü, super zincirin başına dönmez, bulunduğu halkadan bir sağa ilerler. Kilit#ac içindeki super bu yüzden Kilit’e değil Kasa’ya iner.
Bu kurallar include, prepend ve tekil yöntemin tek bir mekanizmanın üç görünümü olduğunu da söyler. Üçü de zincire bir halka ekler; fark halkanın nereye gireceğidir. include halkayı sınıfın arkasına koyar, prepend önüne, tekil sınıf ise nesnenin kendi zincirinin en başına. Hangi halkanın kazandığını anlamak için tahmin yürütmek gerekmez: ancestors dizisini soldan sağa okumak yeter. Tekil sınıf listede görünmez, ama singleton_class.ancestors ile sorulursa zincirin başında durur.
Zincirin okunabilir olması, hata ayıklamada da işe yarar. Bir yöntem beklenmeyen bir sonuç verdiğinde ilk soru hangi tanımın çalıştığıdır. Bunun yanıtı method(:ad).owner satırıdır. İkinci soru o tanımın arkasında başka bir tanım olup olmadığıdır; yanıt super_method ile bir halka daha sorularak alınır ve nil dönene kadar tekrarlanır. super_method zincirdeki sıradaki tanımı verir; tanımın super çağırıp çağırmadığını yöntemin gövdesi gösterir. Bu iki yöntemle zincir tahmin edilmez, okunur.
Özet
- Yöntem arama, alıcının
ancestorszincirinde soldan sağa yürür ve adı eşleşen ilk tanımı çalıştırır. prependmodülü sınıfın önüne koyar;Kasa.new.acönceKilit’i,superile sonraKasa’yı çalıştırır.method(:ac).ownerilk tanımın sahibini,super_method.ownerzincirde bir sonrakini verir.Object’ten türeyen bir sınıfın zinciriBasicObjectile biter;method_missingorada,putsiseKernel’de özel olarak durur.- Sınıf yöntemleri sınıfın tekil sınıf zincirinden gelir:
Hesap.newiçinnewyönteminin sahibiClass’tır. - Bu dersin yazanı atadır; tek nesneye eklenen tekil yöntem ise zincirin başına girer ve yazanı programcı yapar.
Sonraki Adım
Nokta adlı bir Struct sınıfında x yöntemi hiç def ile yazılmadı, yine de nokta.x çalışıyor ve yazan onun yazanını kod olarak veriyor. Yöntemleri bir nesneden çalışma zamanında sormak ve x’i kimin yazdığını nesneye sordurmak mümkün mü? Sonraki ders, Yansıma, nesneye bu soruları sormayı ölçer.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.