Ders 15 / 23
Arayüzler ve Devretme
Arayüzü, varsayılan gerçekleştirimi ve `by` ile devretmeyi iki değeri eşit yapan ölçütle ölçer; devretmenin arayüzün üyelerini ilettiğini, `equals`'ü iletmediğini gösterir.
Yayın Güncelleme
Bu derste
- Bir arayüzde özellik ve varsayılan gerçekleştirimli fonksiyon yazmak
- İki arayüzün aynı varsayılan fonksiyonunu tek sınıfta nasıl birleştireceğini göstermek
- `by` ile bir arayüzün gerçekleştirimini başka bir nesneye devretmek
- Devretmenin eşitliği iletmediğini ve veri sınıfıyla birlikte ne olduğunu ölçmek
İçindekiler
Önceki derste bir nesnenin tekil mi yoksa her seferinde yeni mi kurulduğuna baktık. Bu derste nesnenin davranışını sınıfın dışına çıkarıyoruz: arayüz bir sözleşmeyi yazar, bir sınıf o sözleşmeyi ya kendisi gerçekleştirir ya da başka bir nesneye devreder. Soru devretmeye geliyor: bir nesne davranışını başkasına bıraktığında, iki devreden nesneyi eşit yapan ne?
Arayüz (interface), bir tipin uyması gereken üyeleri tanımlar. Kotlin’de bir arayüz yalnız imza değil, gövdeli fonksiyon ve gövdeli okunur özellik de taşıyabilir; bunlara varsayılan gerçekleştirim denir. Bir sınıf arayüzü gerçekleştirdiğinde gövdesiz üyeleri yazmak zorundadır, gövdelileri isterse yeniden tanımlar (override).
Bu derste ölçüt eksenini bir kez daha hatırlatalım. Java müfredatındaki, kararı hangi tipin verdiğini soran ölçü (M08/K02) burada yok: biz tipe değil, iki değerin eşit sayılmasının nedenine bakıyoruz. Aynı nesne olmaları tekil, eşitliğin kimliğe düşmesi kimlik, içeriklerin eşit olması içerik, yalnız bir kısmının eşit olması kısmi içerik.
Ölçüm Düzeneği
Criterion her derste ilk blokta aynı biçimde durur. Her gözlem üç değer taşır: first, twin (aynı tarifle ayrıca kurulan değer) ve other (yalnız bir bakımdan farklı değer). Kural sonucu görür, gerekçeyi görmez.
// Criterion.kt — iki degeri esit yapan olcutu siniflandirir
class Criterion(val label: String, first: Any?, twin: Any?, other: Any?) {
val sameObject = first === twin
val equalTwin = first == twin
val equalOther = first == other
val name: String = when {
sameObject -> "tekil"
!equalTwin -> "kimlik"
!equalOther -> "icerik"
else -> "kismi-icerik"
}
fun report() {
println("$label: ayni-nesne=$sameObject, ikiz=$equalTwin, farkli=$equalOther -> $name")
}
}
fun summarize(checks: List<Criterion>) {
for (name in listOf("tekil", "kimlik", "icerik", "kismi-icerik")) {
println("$name: ${checks.count { it.name == name }}")
}
}
Dersin kendi modeli ikinci bir yardımcı dosyada durur. Printer arayüzü bir okunur özellik ve bir varsayılan fonksiyon taşır; iki sınıf onu farklı biçimde gerçekleştirir.
// Printers.kt — arayuzde ozellik ve varsayilan gerceklestirim
interface Printer {
val prefix: String
val width: Int
get() = prefix.length
fun render(text: String): String = prefix + text
}
class PlainPrinter : Printer {
override val prefix: String = "> "
}
class LoudPrinter : Printer {
override val prefix: String = "# "
override fun render(text: String): String = prefix + text.uppercase()
}
TN26. Printer’ın yalnız bir gövdesiz özelliği ve bir gövdeli okunur özelliği var; arayüzde saklanan (alan taşıyan) bir özellik yazılamaz ve bu burada ölçülmedi. Aşağıdaki bloklar iki Printer gerçekleştirimini ve by ile yazılan sarmalayıcıları bu yardımcıyla birlikte derler.
Arayüz Üyesi Gövde Taşıyabilir
Aşağıdaki blok iki sınıfı yan yana kullanır. PlainPrinter yalnız gövdesiz özelliği yazdı; render ile width arayüzdeki gövdeleri aldı. LoudPrinter ise render’ı yeniden tanımladı. Son satır iki PlainPrinter’ı ölçer.
// Main.kt — arayuz varsayilani ve yeniden tanimlama
fun main() {
val plain = PlainPrinter()
val loud = LoudPrinter()
println(plain.render("ada"))
println(loud.render("ada"))
println(plain.width)
val printers: List<Printer> = listOf(plain, loud)
println(printers.map { it.render("x") })
Criterion("iki-varsayilan", PlainPrinter(), PlainPrinter(), loud).report()
}
> ada # ADA 2 [> x, # X] iki-varsayilan: ayni-nesne=false, ikiz=false, farkli=false -> kimlik
plain arayüzün render gövdesini kullandı, loud kendi gövdesini; printers listesinden ikisi de aynı Printer adıyla çağrıldı ve her biri kendi sonucunu verdi. width yalnız arayüzde tanımlıydı ve prefix’in uzunluğunu verdi. Son satırda iki ayrı PlainPrinter kimlik çıktı: sınıf equals’ü yeniden tanımlamadığı için eşitlik kimliğe düştü ve iki örnek aynı arayüz gövdesini kullansa da eşit olmadı. Varsayılan gerçekleştirim davranışı paylaştırır, eşitliği paylaştırmaz.
Çakışan Varsayılanlar
Bir sınıf iki arayüzü gerçekleştirir ve ikisi de aynı imzalı gövdeli fonksiyon taşırsa, derleyici hangisinin seçileceğini bilemez. Sınıfın fonksiyonu kendisinin yeniden tanımlaması gerekir ve gövde içinde super<Arayüz> ile her birine ayrı ayrı ulaşılabilir.
// Main.kt — iki arayuzun ayni varsayilani tek sinifta birlesir
interface Reader {
fun describe(): String = "okur"
}
interface Writer {
fun describe(): String = "yazar"
}
class Both : Reader, Writer {
override fun describe(): String = super<Reader>.describe() + "+" + super<Writer>.describe()
}
fun main() {
println(Both().describe())
println(Both().let { it as Reader }.describe())
}
okur+yazar okur+yazar
Both iki kaynağı birleştirdi. İkinci satır önemli: Both nesnesi Reader olarak görüldüğünde de aynı birleşik gövde koştu, çünkü seçimi nesnenin kendi yeniden tanımladığı fonksiyon yaptı, değişkenin tipi değil.
TN27. Bu blokta çakışmayı çözmeyen bir sınıfın derlenmediğini ayrıca doğrulamadık; yalnız çözülmüş hâli ölçüldü.
by ile Devretme
Devretme (delegation), bir arayüzün gerçekleştirimini başka bir nesneye bırakmaktır. Kotlin’de bu : Printer by inner biçiminde yazılır: derleyici arayüzün her üyesi için, çağrıyı inner üzerine ileten bir fonksiyon üretir. İleten nesneye devreden, çağrının gittiği nesneye temsilci (delegate) denir.
// Main.kt — by ile devretme arayuzun uyelerini iletir
class Labeled(private val inner: Printer) : Printer by inner
class Quiet(private val inner: Printer) : Printer by inner {
override fun render(text: String): String = inner.render(text).lowercase()
}
fun main() {
val loud = LoudPrinter()
val labeled = Labeled(loud)
println(labeled.render("ada"))
println(labeled.prefix)
println(labeled.width)
println(Quiet(loud).render("ADA"))
val checks = listOf(
Criterion("ayni-temsilci", Labeled(loud), Labeled(loud), Labeled(PlainPrinter())),
Criterion("yeniden-tanimli", Quiet(loud), Quiet(loud), Quiet(PlainPrinter())),
)
for (check in checks) check.report()
summarize(checks)
}
# ADA # 2 # ada ayni-temsilci: ayni-nesne=false, ikiz=false, farkli=false -> kimlik yeniden-tanimli: ayni-nesne=false, ikiz=false, farkli=false -> kimlik tekil: 0 kimlik: 2 icerik: 0 kismi-icerik: 0
Labeled hiçbir üye yazmadı; render, prefix, width yine de çalıştı: üçü de loud’a gitti. render sonucunun büyük harfli olması, çağrının Printer’ın varsayılan gövdesine değil LoudPrinter’ın yeniden tanımladığı gövdeye gittiğini gösteriyor. Quiet ise bir üyeyi kendisi yeniden tanımladı ve geri kalanını devretti.
Aynı temsilciyi saran iki Labeled ve aynı temsilciyi saran iki Quiet ikisi de kimlik çıktı. İki devreden nesne aynı loud’a bağlı olsa da eşit sayılmadı.
TN28. by’dan sonra gelen ifadenin bir kurucu parametresi olması şart değildir; burada yalnız parametreyi gösteren biçimi ölçtük. Devretmenin, arayüzde olmayan bir üyeyi de iletip iletmediği ölçülmedi.
Fonksiyonel Arayüz, Nesne İfadesi ve Devir İfadesi
Arayüz yalnız sınıflarla gerçekleştirilmez. Tek bir soyut fonksiyonu olan bir arayüz, fun interface ile işaretlenirse bir lambda ile kurulabilir. Bir arayüz ayrıca adsız bir nesne ifadesiyle de gerçekleştirilebilir. Üçüncü olarak, by’dan sonra gelen ifade bir kurucu parametresi olmak zorunda değildir; herhangi bir ifade olabilir. Bu üçünü tek bir blokta ölçüyoruz.
Shout iki lambda ile kurulur: ikisinin gövdesi aynıdır. anon bir nesne ifadesiyle Printer gerçekleştirir. Cached sınıfı ise temsilciyi makePrinter() çağrısıyla kurar ve made sayacı, bu çağrının kaç kez koştuğunu sayar.
// Main.kt — fun interface, nesne ifadesi ve devir ifadesinin bir kez degerlendirilmesi
fun interface Shout {
fun apply(text: String): String
}
var made = 0
fun makePrinter(): Printer {
made += 1
return PlainPrinter()
}
class Cached : Printer by makePrinter()
fun main() {
val first = Shout { it.uppercase() }
val second = Shout { it.uppercase() }
println(first.apply("ada"))
val anon = object : Printer {
override val prefix: String = "~ "
}
println(anon.render("x"))
val one = Cached()
println(one.render("a"))
println(one.render("b"))
println(made)
Cached()
println(made)
val checks = listOf(
Criterion("fonksiyonel-arayuz", first, second, Shout { it }),
Criterion("nesne-ifadesi", anon, anon, PlainPrinter()),
)
for (check in checks) check.report()
}
ADA ~ x > a > b 1 2 fonksiyonel-arayuz: ayni-nesne=false, ikiz=false, farkli=false -> kimlik nesne-ifadesi: ayni-nesne=true, ikiz=true, farkli=false -> tekil
Bloğun ilk sonucu, gövdesi aynı olan iki lambdanın eşit sayılmadığıdır: fonksiyonel-arayuz satırı kimlik çıktı. Gövdelerin metin olarak aynı olması eşitlik üretmedi; her lambda ifadesi kendi nesnesini kurdu ve fonksiyonel arayüz equals’ü yeniden tanımlamıyor. Aynı arayüzü sınıfla gerçekleştirmek ile lambda ile gerçekleştirmek eşitlik bakımından ayrılmaz.
İkinci sonuç nesne ifadesi içindir. nesne-ifadesi satırı tekil çıktı, çünkü anon tek bir değişkendir ve aynı değer iki kez ölçüye verildi. Önceki derste gördüğümüz ayrım burada da geçerli: ifade her değerlendirildiğinde yeni bir nesne kurar, ama bir kez kurulup bir değişkene konan nesne tek kalır. anon.render("x") çıktısındaki ~ öneki, arayüzdeki varsayılan render gövdesinin nesne ifadesinde de kullanıldığını gösterdi.
Üçüncü sonuç by ifadesi içindir. made ilk Cached() çağrısından sonra 1, ikinciden sonra 2 yazdı. Temsilci ifadesi her Cached örneği kurulurken bir kez değerlendirilir: iki örnek, iki ayrı PlainPrinter taşır. Aynı örneğin iki render çağrısı ise temsilciyi yeniden kurmadı; made yalnız kurucuda arttı. Bu, devretmenin bir yöntem çağrısı değil, kurucuda saklanan bir alan olduğunu söyler: temsilci örnek başına bir kez kurulur ve sonra iletilir.
TN29. Bir lambdanın dış değişkeni yakalayıp yakalamamasının eşitliği değiştirip değiştirmediği ölçülmedi; iki lambda da hiçbir şey yakalamadı.
Ölçümlerden Çıkan Eşitlik Kuralı
Bu derste ölçtüğümüz her satır, bir davranışın nereden geldiğini sorduktan sonra eşitliğin nereden geleceğini sordu. Arayüz davranış paylaşır ama durum taşımaz ve equals yazmaz; by davranışı bir temsilciye iletir ama equals’ü iletmez; fun interface bir lambdayı arayüz değerine çevirir ama iki özdeş lambdayı eşit saymaz. Üç düzeneğin ortak sonucu, eşitliğin davranışın paylaşılmasıyla değil, sınıfın kendi equals tanımıyla belirlendiğidir.
Sık yapılan hata, devretmeyi kalıtım gibi okumaktır. Kalıtımda alt sınıf üst sınıfın equals’ünü miras alır; by ile devredilen bir sınıf ise arayüzün üyelerini iletir, ama equals arayüzde tanımlı bir üye olmadığı için iletilmez. Bu yüzden aynı temsilciyi saran iki Labeled ya da iki Quiet örneği, davranışları birebir aynı olsa da eşit çıkmadı. Eşitlik gerekiyorsa sınıf bunu kendisi yazar ya da data class olarak bildirir; devretme bu işi yapmaz.
Devir ifadesinin örnek başına bir kez değerlendirilmesi ikinci bir pratik sonuç verir. made sayacı iki örnek için 2 yazdı; yani temsilci bir alandır, örneğin ömrü boyunca aynı kalır. Bir temsilcinin içinde durum tutuluyorsa, örneğin bir sayaç ya da bir önbellek, o durum devreden örneğe özgüdür ve iki örnek arasında paylaşılmaz.
Lambda ile kurulan arayüz değerlerinde de aynı ders geçerlidir: iki gövde aynı metni taşısa bile iki ayrı değerdir ve sonuç kimlik çıkar. Eşitliği davranıştan beklemek, bu derste ölçtüğümüz her düzenekte yanlış çıktı. Davranış ile eşitliği ayrı düşünmek, bir koleksiyonda ya da bir == karşılaştırmasında beklenmedik bir sonuçla karşılaşmamayı sağlar.
Ölçmeden Önce Tahmin Etmek
Bu dersin ölçütü kimlik olduğu için tahmin kolaydır, ama hangi satırın neden kimlik verdiğini söylemek daha öğreticidir. Sınıf ile gerçekleştirilen bir arayüzde eşitlik sınıfın equals’üne bağlıdır; sınıf onu yazmadıysa Object’in kimlik karşılaştırması geçerlidir. by bu tabloya bir şey eklemez, çünkü iletilen üyeler arayüzde tanımlı olanlardır. Veri sınıfı olarak bildirilen bir sınıfta equals derleyici tarafından üretilir ve içeriğe bakar; bu ders onu bir karşılaştırma olarak gösterdi.
Fonksiyonel arayüz satırında tahmin tuzağı vardır. İki lambda aynı metni taşıyor ve aynı arayüze çevriliyor; “aynı iş yapıyorlar, demek ki eşitler” diye düşünmek doğal görünür. Ölçüm kimlik verdi. Nesne ifadesi satırında ise aynı değişken iki kez verildi ve sonuç tekil oldu; ifade değerlendirilirken yeni nesne kurulur, ama bir değişkene konan nesne bundan sonra tek kalır. Bu iki satırın farkı, “iki ifade” ile “bir ifade, iki okuma” arasındaki farktır.
Devir ifadesinde tahmin edilecek şey bir sayıdır. İki Cached kuran bir program için made değeri 2 olacaktır, çünkü temsilci kurucuda bir kez değerlendirilir. Bu sayıyı 1 ya da çağrı sayısı kadar bekleyen kişi, devretmenin bir alan mı, yoksa her çağrıda yeniden hesaplanan bir ifade mi olduğunu yanlış anlamıştır.
Sınırlayıcı Ölçüm
Neden iki devreden nesne eşit değil? Çünkü by yalnız arayüzün üyelerini iletir; equals, hashCode ve toString arayüzün üyesi değil, Any‘nin üyesidir. Önce bunu doğruladık: aşağıdaki parça Kotlin 2.4.21’de derlenmedi, bu yüzden çıktısız kalıyor. Bir arayüzün Any’nin equals’ü için varsayılan gerçekleştirim vermesi de yasak.
interface Same {
override fun equals(other: Any?): Boolean = true
}
Devretme equals’ü iletmediğine göre, eşitliği değiştiren tek yol sınıfın kendisinin onu yeniden tanımlamasıdır. Veri sınıfı bunu otomatik yapar. Aşağıdaki blok, aynı temsilciyi saran bir sıradan sınıfı ve bir veri sınıfını yan yana ölçer; üçüncü satır devreden nesne ile temsilcisini karşılaştırır.
// Main.kt — devretme equals'u iletmez, veri sinifi kendi uretir
class Wrapped(val inner: Printer) : Printer by inner
data class Tagged(val inner: Printer) : Printer by inner
fun main() {
val loud = LoudPrinter()
val checks = listOf(
Criterion("sinif-devretme", Wrapped(loud), Wrapped(loud), Wrapped(PlainPrinter())),
Criterion("veri-sinifi-devretme", Tagged(loud), Tagged(loud), Tagged(PlainPrinter())),
Criterion("devreden-ve-temsilci", Wrapped(loud), loud, PlainPrinter()),
)
for (check in checks) check.report()
summarize(checks)
}
sinif-devretme: ayni-nesne=false, ikiz=false, farkli=false -> kimlik veri-sinifi-devretme: ayni-nesne=false, ikiz=true, farkli=false -> icerik devreden-ve-temsilci: ayni-nesne=false, ikiz=false, farkli=false -> kimlik tekil: 0 kimlik: 2 icerik: 1 kismi-icerik: 0
Sıradan sınıfta ölçüt kimlik, veri sınıfında icerik çıktı. Veri sınıfı kendi equals’ünü üretip birincil yapıcıdaki inner’ı karşılaştırdığı için, aynı temsilciyi saran iki örnek eşit oldu; farklı temsilciyi saran ise eşit olmadı. Üçüncü satır devretmenin sınırını açık gösteriyor: Wrapped(loud) ile loud birbirine eşit değil. Devreden nesne temsilcisi gibi davranır ama temsilcisi değildir.
Özet
- Arayüz, bir tipin uyması gereken üyeleri yazar; gövdeli fonksiyon ve gövdeli okunur özellik taşıyabilir.
- İki arayüz aynı varsayılan fonksiyonu getirirse sınıf onu yeniden tanımlar ve
super<Arayüz>ile ikisine ayrı ulaşır. by, arayüzün üyelerini temsilciye iletir; temsilcinin kendi yeniden tanımladığı gövdeler korunur.- Devretme
equals’ü iletmez ve bir arayüzequalsiçin varsayılan gerçekleştirim veremez; aynı temsilciyi saran iki sıradan nesne eşit değildir, iki veri sınıfı ise eşittir. - Bu dersin ölçütü: kimlik. Davranış paylaşılsa bile eşitlik, sınıfın
equals’ünü yeniden tanımlamadıkça kimliğe düşer.
Sonraki Adım
Devretme, davranışı sınıfın içinde başka bir nesneye taşıdı. Sıradaki ders davranışı sınıfın dışından ekliyor: uzantı fonksiyonları, kaynağına dokunmadığınız bir tipe yeni fonksiyon yazdırır. Bir uzantının, tipin kendi üyesinin yerine geçip geçemeyeceğini, özellikle equals üzerinde ölçeceğiz.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.