Ders 16 / 23
Uzantı Fonksiyonları
Uzantı fonksiyonu, uzantı özelliği ve boş değer alabilen alıcıya uzantı yazar; uzantının üyeyi gölgeleyemediğini ve statik tipe göre seçildiğini ölçer.
Yayın Güncelleme
Bu derste
- Var olan bir tipe uzantı fonksiyonu ve uzantı özelliği yazmak
- Boş değer alabilen bir alıcıya uzantı yazıp null değerle çağırmak
- Çağrıya uyan bir üye varken uzantının hiç seçilmediğini ölçmek
- Uzantının statik tipe göre seçildiğini, üyenin ise nesneye göre seçildiğini ayırmak
İçindekiler
Önceki derste bir sınıf, davranışını by ile başka bir nesneye devretti ve devretmenin equals’ü iletmediğini gördük. Bu derste davranış sınıfa dışarıdan ekleniyor: kaynağına dokunamadığınız bir tipe yeni bir fonksiyon yazdırmak mümkün. Soru, bu eklemenin tipin kendi üyesinin yerini alıp alamayacağı ve özellikle equals gibi bir üyeyi değiştirip değiştiremeyeceği.
Uzantı fonksiyonu (extension function), var olan bir tipin adı önüne yazılan ve o tipin bir örneği üzerinde çağrılan fonksiyondur: fun String.wrapped(...). Tipin tanımına dokunulmaz; uzantı yalnız çağrı sözdizimini ona benzetir. Gövde içinde this, fonksiyonun çağrıldığı nesneyi, yani alıcıyı (receiver) gösterir.
Bu this, Kapsam Fonksiyonları dersinde gördüğümüz this ve it ayrımıyla karıştırılmamalı: orada soru hangi nesnenin göründüğüydü, burada this yalnız uzantının alıcısıdır ve ölçü o sorudan ayrıdır. Bu dersin ölçüsü bu kursta hep aynı: bu iki değeri eşit yapan ne?
Ö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 }}")
}
}
TN30. Uzantıların hepsi Main.kt içinde, main’in yanında yazıldı; ayrı bir dosyada tanımlanıp içe alınan uzantı bu derste ölçülmedi.
Uzantı Fonksiyonu ve Özelliği
Aşağıdaki blok üç uzantı yazar. wrapped, bir dizginin iki yanına bir işaret ekler. lastWord, bir uzantı özelliğidir: değeri saklanmaz, her okunuşta get() ile hesaplanır. Üçüncüsü, alıcısı String? olan bir uzantıdır; null değer üzerinde de çağrılabilir ve gövdede this boş değer alabilen bir tiptir.
// Main.kt — uzanti fonksiyonu, uzanti ozelligi ve bos deger alabilen alici
fun String.wrapped(mark: String): String = mark + this + mark
val String.lastWord: String
get() = this.substringAfterLast(' ')
fun String?.orPlaceholder(): String = this ?: "yok"
fun main() {
println("ada".wrapped("*"))
println("bir iki uc".lastWord)
val present: String? = "var"
val missing: String? = null
println(present.orPlaceholder())
println(missing.orPlaceholder())
}
*ada* uc var yok
wrapped ve lastWord bir String üzerinde, sanki tipin üyesiymiş gibi yazıldı. orPlaceholder ise null değerli missing üzerinde patlamadı: normal bir üye çağrısı null üzerinde çalışmazdı, ama uzantı alıcıyı bir parametre gibi aldığı için null da geçerli bir alıcı. Bu yüzden String? alıcılı bir uzantı, güvenli çağrı (?.) yazmadan null’ı karşılayabilir.
Uzantı özelliği bir alan taşıyamaz: ona saklanan değer verilemez. Önce bunu doğruladık: aşağıdaki parça Kotlin 2.4.21’de derlenmedi, bu yüzden çıktısız kalıyor.
val String.cache: Int = 0
TN31. Reddin nedenini, yani uzantı özelliğinin alan taşımadığını, derleyicinin hata metninden değil dilin tanımından aktarıyoruz; hata metnini derse yazmıyoruz.
Uzantı Üyeyi Gölgeleyemez
Bir tipin üyesiyle aynı adı taşıyan, ama daha dar bir parametre tipi alan bir uzantı yazarsanız ne olur? Aşağıdaki blok Plain sınıfına, Any’den gelen equals(other: Any?) üyesinin yanına bir equals(other: Plain) uzantısı ekler. Uzantı, iki Plain’in id’sini karşılaştırır. Üç yoldan çağırıyoruz: doğrudan çağrı, == ve Any tipli bir değişken üzerinden çağrı.
// Main.kt — equals uzantisi uyeyi golgelemez
class Plain(val id: Int)
fun Plain.equals(other: Plain): Boolean = this.id == other.id
fun main() {
val a = Plain(1)
val b = Plain(1)
val asAny: Any = b
println(a.equals(b))
println(a == b)
println(a.equals(asAny))
Criterion("equals-uzantisi", Plain(1), Plain(1), Plain(2)).report()
}
false false false equals-uzantisi: ayni-nesne=false, ikiz=false, farkli=false -> kimlik
Üç çağrı da aynı sonucu verdi: id’ler eşit olduğu hâlde karşılaştırma yanlış çıktı. Uzantı yazıldı, ama hiçbir çağrı ona gitmedi: üye Any? aldığı için Plain argümanı da ona uyar ve çağrıya uyan bir üye varsa uzantıya bakılmaz, uzantının parametresi daha özgül olsa bile. Uzantının imzası farklıdır; üyeyi gölgelemez, yalnız bir aşırı yükleme olarak durur. a == b ise her zaman equals(Any?) üyesine çözülür ve uzantıyı hiç çağırmaz. Bu yüzden ölçüt kimlik: iki Plain aynı id’yi taşıyor, ama eşitlik kuralı bunu bilmiyor.
TN32. Uzantının üye ile çakıştığında derleyicinin bir uyarı verdiği iddiasını yazmıyoruz; derleyici uyarısı bu derste denetlenmiyor. Bir uzantı üyeden farklı bir parametre tipiyle yazılsa da argüman üyeye uyuyorsa üye seçilir; uzantıya ancak üye uygulanamadığında sıra gelir. Bu durum bir sonraki bölümde Box.scale ile ölçülüyor.
Aynı Ad, Farklı Parametre
Önceki bölümde argümanın hem üyeye hem uzantıya uyduğu bir çağrıya baktık ve üyenin kazandığını gördük. Peki argüman üyeye uymuyorsa? Üye Int alır, uzantı Double alır ve çağrının hangisine gideceğine argümanın tipi karar verir. Aşağıdaki blok bunu Box sınıfı üzerinde ölçer. Ayrıca infix işaretli bir uzantıyı ve boş değerli bir alıcı üzerinde toString çağrısını ekler.
// Main.kt — farkli parametreli uzanti secilir, infix uzanti, bos alicida toString
class Box(val size: Int) {
fun scale(factor: Int): Int = size * factor
}
fun Box.scale(factor: Double): Double = size * factor
infix fun Box.bigger(other: Box): Boolean = size > other.size
fun main() {
val box = Box(3)
println(box.scale(2))
println(box.scale(2.5))
println(box bigger Box(2))
val nothing: Any? = null
println(nothing.toString())
println(nothing?.toString())
Criterion("kutu", Box(1), Box(1), Box(2)).report()
}
6 7.5 true null null kutu: ayni-nesne=false, ikiz=false, farkli=false -> kimlik
box.scale(2) üyeye gitti ve 6 verdi; box.scale(2.5) üyenin Int parametresine uymadığı için uzantıya gitti ve 7.5 verdi. Uzantı, üye çağrıya uyduğu sürece kaybetti ve üyenin karşılamadığı bir argüman tipini karşıladığında çalıştı. Bu, “uzantı üyeyi gölgeleyemez” cümlesinin sınırını çizer: üye uygulanabiliyorsa uzantıya bakılmaz.
box bigger Box(2) ifadesi true verdi. infix işareti bir uzantıyı nokta ve parantez olmadan, iki işlenenli bir işleç gibi yazmayı sağlar. Bu yalnız yazım kolaylığıdır; çağrı yine alıcı olarak box’ı alan sıradan bir uzantıdır.
Boş değerli alıcı satırları ilginçtir. nothing değişkeni Any? tipli ve değeri null; nothing.toString() patlamadan null yazdı. Çünkü standart kitaplıkta Any? alıcılı bir toString uzantısı vardır; üyeymiş gibi çağrılır ama null’ı karşılar. nothing?.toString() de null yazdı, ama bu kez güvenli çağrı yüzünden: çağrı hiç yapılmadı ve ifadenin değeri null oldu. İki satır aynı çıktıyı verdi, ama farklı yoldan; birinde fonksiyon koştu, diğerinde koşmadı.
Criterion satırı yine kimlik: Box sıradan bir sınıf, uzantılar eşitliğe dokunmadı.
TN33. Box.scale üyesi Int, uzantısı Double alır; imzaları farklıdır. box.scale(2) çağrısının üyeye gittiği ölçüldü; uzantı Double beklediği için Int argümanla zaten aday değildir. Int argüman üyeye uyduğu için üye seçildi.
Genel Alıcı ve Sınıf İçindeki Uzantı
Uzantı iki yerde daha tuhaf bir davranış gösterir. Alıcı tipi genel (generic) olabilir: List<T> üzerine yazılan bir uzantı her eleman tipinde çalışır. Ve bir uzantı bir sınıfın içinde bildirilebilir; o zaman yalnız o sınıfın içinden çağrılır ve sınıfın üyelerine de ulaşabilir. Aşağıdaki blok ikisini birlikte ölçer.
// Main.kt — genel alicili uzanti ve sinif icinde bildirilen uzanti
fun <T> List<T>.secondOrNull(): T? = if (size >= 2) this[1] else null
class Formatter(private val mark: String) {
fun String.marked(): String = mark + this + mark
fun run(text: String): String = text.marked()
}
fun main() {
println(listOf(1, 2, 3).secondOrNull())
println(listOf("a").secondOrNull())
println(emptyList<Int>().secondOrNull())
println(Formatter("|").run("ada"))
Criterion("bicimleyici", Formatter("|"), Formatter("|"), Formatter("#")).report()
}
2 null null |ada| bicimleyici: ayni-nesne=false, ikiz=false, farkli=false -> kimlik
secondOrNull üç listede çalıştı: üç elemanlı Int listesinde 2, tek elemanlı String listesinde null, boş listede null. Tek uzantı farklı eleman tipleri için yazılmış olduğu hâlde her çağrı kendi tipini çıkardı; dönüş tipi T? olduğu için boş durumda null güvenle döndü.
Formatter içindeki marked uzantısı sınıfın mark özelliğini okudu: Formatter("|").run("ada") |ada| verdi. Üstte yazdığımız uzantılarda alıcı tek bir this idi; burada iki alıcı vardır, Formatter örneği ve String alıcısı. marked yalnız Formatter gövdesinde görünür; sınıfın dışında "ada".marked() yazılamaz.
Ölçüt yine kimlik: iki Formatter aynı işareti taşıyor olsa da eşit değil. Uzantıların, ister dışarıda ister bir sınıfın içinde yazılsın, tipin eşitliğine dokunmadığı bu dersin üçüncü ölçümünde de yinelendi.
TN34. Sınıf dışından marked çağrısının derlenmediği ayrıca ölçülmedi; kapsam bilgisi dilin tanımından aktarılıyor.
Ölçümlerden Çıkan Seçim Kuralı
Uzantı fonksiyonu bir tipe dışarıdan eklenir, ama çağrı yazılışı üye çağrısına benzer. Bu benzerlik, hangisinin seçildiği sorusunu gerekli kılar. Bu dersin ölçümleri üç kural verdi. Birincisi, argüman üyeye uyuyorsa üye kazanır; equals üyesi Any? aldığı için Plain argümanı da ona uydu ve Plain.equals uzantısı, parametresi daha özgül olduğu hâlde, üç çağrının hiçbirinde seçilmedi. İkincisi, üye uygulanamıyorsa sıra uzantıya gelir; Box.scale üyesi Int beklediği için scale(2.5) ona uymadı ve uzantıya gitti. Üçüncüsü, seçim değişkenin statik tipine göre yapılır; uzantı çalışma zamanındaki sınıfa bakmaz.
Sık yapılan hata, uzantıyı üyeyi değiştirmenin bir yolu sanmaktır. Bir tipin üyesini “düzeltmek” için aynı adla uzantı yazan kişi, kodun çalışmaya devam ettiğini ve hiçbir şeyin değişmediğini görür; çünkü üye çağrıya uyduğu sürece uzantı hiç çağrılmaz. Üyenin davranışını değiştirmek gerekiyorsa tipin kendisi değişir ya da yeni bir ad seçilir. Bu ölçümde equals için de aynı sonuç görüldü ve ölçüt kimlik kaldı.
Boş alıcı ayrıntısı ikinci bir tuzağı gösterir. nothing.toString() ve nothing?.toString() aynı çıktıyı yazdı, ama ilkinde fonksiyon koştu, ikincisinde koşmadı. Çıktıya bakarak hangisinin kullanıldığı anlaşılmaz; fark, fonksiyonun yan etkisi olduğunda görünür. Alıcısı boş değer alabilen bir uzantı yazan kişi, bu fonksiyonun null alıcıda da koşacağını bilmeli ve gövdesini buna göre yazmalıdır.
Genel alıcılı uzantı ile sınıf içindeki uzantı ise kapsam sorusunu getirir. secondOrNull bir kez yazıldı ve üç ayrı listede çalıştı; marked ise yalnız Formatter gövdesinde göründü. İlki tipin dışında, her yerden çağrılabilen bir fonksiyondur; ikincisi sınıfın üyelerine ulaşan ama yalnız o sınıfın içinden çağrılabilen bir fonksiyondur. Hangisini seçeceğiniz, fonksiyonun sınıfın durumuna ihtiyaç duyup duymamasına bağlıdır: durum gerekiyorsa sınıfın içine, gerekmiyorsa dışına yazılır.
Ölçmeden Önce Tahmin Etmek
Bu dersteki her ölçüm aynı kalıba girer: bir çağrı yazın, hangisinin koşacağını söyleyin, sonra çıktıya bakın. box.scale(2) için üyeyi, box.scale(2.5) için uzantıyı söyleyebiliyorsanız seçim kuralını anlamışsınız demektir: argüman üyenin parametresine uyuyorsa üye, uymuyorsa uzantı. Aynı mantık bigger için de geçerli; Box üzerinde bigger adında bir üye yoksa uzantı çalışır ve infix işareti yalnız yazılışı değiştirir.
Boş alıcıda tahmin iki aşamalıdır. nothing.toString() için “patlar” diyen kişi, toString’in bir üye olduğunu, null üzerinde çağrılamayacağını varsaymıştır. Ölçüm null yazdı, çünkü çağrı üye değil, boş alıcıyı karşılayan bir uzantıya gitti. Bu, bir ifadenin yazılışının (nothing.toString()) hangi fonksiyonu çağırdığını söylemediği anlamına gelir; alıcının tipine ve görünür uzantılara bakmak gerekir.
Genel alıcılı uzantıda tahmin dönüş değeri üzerindedir. Üç elemanlı listede 2, tek elemanlıda ve boşta null beklenir; T? dönüş tipi bu iki durumu aynı değerle karşılar. Kimlik satırlarında ise Box ve Formatter sıradan sınıflar olduğu için == her zaman false verecektir; uzantıların kaç tane yazıldığı sonucu etkilemez.
Tahmin ile ölçümün ayrıldığı yerde durmak değerlidir. Bu derste bir kez, nothing için iki farklı yazılışın aynı çıktıyı verdiği görüldü ve bu, iki yazılışın aynı şey olduğu anlamına gelmedi. Çıktının eşit olması ile yolun eşit olması ayrı iki olgudur; bir ölçümde yalnız çıktıya bakan kişi ikincisini görmez. Bu yüzden her satırın yanına, o çıktıya hangi yoldan gelindiğini de yazmak iyi bir alışkanlıktır.
Sınırlayıcı Ölçüm
Uzantı üyeyi gölgeleyemiyorsa, uzantı hangi kuralla seçilir? Üyeler nesnenin çalışma zamanındaki sınıfına göre seçilir. Uzantılar ise değişkenin bildirilmiş tipine, yani statik tipe göre. Statik bağlama (static dispatch), çağrılacak fonksiyonun derleme sırasında değişkenin bildirilmiş tipine bakılarak seçilmesidir; dinamik bağlama (dynamic dispatch) ise nesnenin çalışma zamanındaki sınıfına bakılarak seçilmesi. Uzantılar ilkini kullanır.
Aşağıdaki blok aynı nesneyi iki farklı statik tiple tutar. Speaker ve Loud için birer label uzantısı yazılıdır, speak ise sınıfın bir üyesidir ve Loud onu yeniden tanımlar. M08/K02’de kararı hangi tipin verdiğine bakmıştık; bu ölçü o soruya dönmüyor, aşağıdaki satırlar yalnız bu iki seçim kuralını yan yana koyuyor.
// Main.kt — uzanti statik tipe gore, uye nesneye gore secilir
open class Speaker {
open fun speak(): String = "temel"
}
class Loud : Speaker() {
override fun speak(): String = "yuksek"
}
fun Speaker.label(): String = "etiket-temel"
fun Loud.label(): String = "etiket-yuksek"
fun main() {
val exact = Loud()
val asSpeaker: Speaker = exact
println(exact.label())
println(asSpeaker.label())
println(exact.speak())
println(asSpeaker.speak())
Criterion("iki-statik-tip", asSpeaker, exact, Speaker()).report()
}
etiket-yuksek etiket-temel yuksek yuksek iki-statik-tip: ayni-nesne=true, ikiz=true, farkli=false -> tekil
asSpeaker ve exact aynı nesneyi tutuyor: son satır tekil yazdı. Buna karşın label iki ayrı sonuç verdi, çünkü uzantı değişkenin tipine bakar: exact için Loud uzantısı, asSpeaker için Speaker uzantısı seçildi. speak ise iki değişkende de aynı sonucu verdi, çünkü üye nesnenin gerçek sınıfına bakar. Eşitlik de bir üyedir ve bu yüzden bir uzantı onu değiştiremez: == çalışma zamanındaki nesnenin equals’üne gider, bir uzantı seçilemez.
Özet
- Uzantı fonksiyonu, bir tipe kaynağına dokunmadan yeni bir fonksiyon ekler; gövdede
thisalıcıdır. - Uzantı özelliği alan taşıyamaz, her okunuşta hesaplanır; saklanan değerli bir uzantı özelliği derlenmez.
- Alıcısı boş değer alabilen bir tip olan uzantı null üzerinde çağrılabilir ve null’ı kendisi karşılar.
- Çağrıya uyan bir üye varsa üye kazanır, uzantının parametresi daha özgül olsa bile;
==hepequals(Any?)üyesine çözüldüğü için uzantıequals’ü değiştiremez. - Uzantı değişkenin statik tipine göre, üye nesnenin çalışma zamanındaki sınıfına göre seçilir.
- Bu dersin ölçütü: kimlik. Uzantı
equalsyazsa bile üye kazandığı için eşitlik, sınıf kendiequals’ünü yeniden tanımlamadıkça kimliğe düşer.
Sonraki Adım
Bu derste bir tipin dışına fonksiyon ekledik ve eklenen şeyin tipi değil, değişkenin tipi tarafından seçildiğini ölçtük. Sıradaki ders tipin kendisini parametreleştiriyor: genellikler, bir sınıfı ya da fonksiyonu birden fazla tiple çalışacak biçimde yazdırır, varyans da bu tiplerin birbirine atanıp atanamayacağını söyler.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.