Ders 17 / 23
Genellikler ve Varyans
Genel sınıf, genel fonksiyon, üst sınır kısıtı, `out` ve `in` varyansı ile tip silmeyi ölçer; çalışma zamanında tip parametresinin silindiğini, eşitliğin değere baktığını gösterir.
Yayın Güncelleme
Bu derste
- Genel bir sınıf ve üst sınırlı genel bir fonksiyon yazmak
- `out` ve `in` ile bir tip parametresinin kovaryans ve kontravaryansını belirtmek
- Değiştirilebilir bir listenin invaryant, salt okunur bir listenin kovaryant olduğunu ölçmek
- Tip silmeyi ve genel bir veri sınıfında eşitliğin değere bakmasını açıklamak
İçindekiler
Önceki derste bir uzantının statik tipe göre seçildiğini gördük: karar değişkenin bildirilmiş tipinde veriliyordu. Bu derste o bildirilmiş tipin içine bir parametre koyuyoruz. List<Int> ile List<String> iki ayrı tip gibi görünür; soru, çalışma zamanında bu ikisinin gerçekten ayrı olup olmadığı ve bir List<Int>’in bir List<Any> yerine geçip geçemeyeceği.
Genellik (generic), bir sınıfı ya da fonksiyonu, tipi sonradan belirlenen bir tip parametresi (type parameter) ile yazma yoludur: class Holder<T>. Parametre, çağrı sırasında somut bir tiple doldurulur. Kısıt (constraint), tip parametresinin hangi tiplerle doldurulabileceğini sınırlayan koşuldur: T : Comparable<T> yalnız karşılaştırılabilir tipleri kabul eder.
Ölçü bu kursun her dersindeki gibi: bu iki değeri eşit yapan ne? M08/K02’de bir kararı hangi tipin verdiğine bakmıştık; burada soru farklı, tip parametresinin eşitliğe etki edip etmediği ölçülüyor.
Ö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 }}")
}
}
TN35. Bu derste Criterion’ın parametreleri Any? olduğu için, tip parametreli değerler de ona geçerken sıradan nesneler olarak görünür; ölçüm statik tipi değil çalışma zamanındaki nesneyi görür.
Genel Sınıf, Fonksiyon ve Kısıt
Aşağıdaki blok üç genel tanım yazar. Holder<T> herhangi bir tipten bir değer tutar. largest, T : Comparable<T> kısıtıyla yalnız karşılaştırılabilir tiplerde çalışır. sumInts iki kısıtı birlikte ister: where bölümü, tek bir parametreye birden fazla kısıt koymanın yoludur.
// Main.kt — genel sinif, ust sinir kisiti ve where
class Holder<T>(val item: T) {
fun describe(): String = "kutu:" + item
}
fun <T : Comparable<T>> largest(first: T, second: T): T =
if (first >= second) first else second
fun <T> sumInts(first: T, second: T): Int where T : Number, T : Comparable<T> =
first.toInt() + second.toInt()
fun main() {
println(Holder(7).describe())
println(Holder("ada").describe())
println(largest(3, 9))
println(largest("elma", "armut"))
println(sumInts(2, 3))
}
kutu:7 kutu:ada 9 elma 5
Aynı Holder bir Int ve bir String ile kullanıldı; tip parametresi her çağrıda çıkarıldı; yazıya hiç tip eklenmedi. largest hem sayılarla hem dizgilerle çalıştı, çünkü ikisi de Comparable. sumInts yalnız hem Number hem Comparable olan tiplerde derlenir; Int ikisini de sağlıyor.
TN36. Kısıtı sağlamayan bir tipin largest’e verilince derlenmediği ayrıca ölçülmedi; yalnız kısıtı sağlayan çağrılar yazıldı.
Çalışma Zamanında Tip Parametresi Yok
Bir genel sınıfın tip parametresi derleme zamanında denetlenir ve çalışma zamanına taşınmaz. Buna tip silme (type erasure) denir. Sonuç, aynı genel tipin farklı parametrelerle kurulmuş iki değerinin çalışma zamanında aynı sınıftan olmasıdır. Daha çarpıcısı, bazı standart fonksiyonların tek bir ortak nesne döndürmesidir: boş bir liste her eleman tipi için aynı nesne olabilir. Bunu ölçüyoruz.
Üç küçük aracı da yan yana koyuyoruz. Yıldız yansıtması (star projection), List<*> biçiminde, tip parametresini bilmeden okumaya izin verir. inline bir fonksiyonda reified işaretli parametre ise çalışma zamanında okunabilir hâle gelir; silmenin tek istisnası budur, çünkü derleyici o fonksiyonun gövdesini çağrı noktasına açar.
// Main.kt — tip silme ve reified
class Holder<T>(val item: T)
fun countAll(values: List<*>): Int = values.size
inline fun <reified T> nameOf(): String = T::class.simpleName ?: "adsiz"
fun main() {
val ints = Holder(1)
val texts = Holder("ada")
println(ints::class == texts::class)
println(countAll(listOf(1, 2, 3)))
println(countAll(listOf("a", "b")))
println(nameOf<Int>())
println(nameOf<String>())
val checks = listOf(
Criterion("bos-listeler", emptyList<Int>(), emptyList<String>(), listOf(1)),
Criterion("kutular", ints, texts, Holder(2)),
)
for (check in checks) check.report()
summarize(checks)
}
true 3 2 Int String bos-listeler: ayni-nesne=true, ikiz=true, farkli=false -> tekil kutular: ayni-nesne=false, ikiz=false, farkli=false -> kimlik tekil: 1 kimlik: 1 icerik: 0 kismi-icerik: 0
ints ve texts farklı tip parametreleriyle kurulmuştu ama sınıfları aynı çıktı. countAll iki listenin elemanlarını bilmeden boyutlarını okudu. nameOf ise silmenin sınırını gösterdi: reified ile tip parametresi çalışma zamanında okunabildi ve Int, String adları basıldı.
Ölçüt tablosunun ilk satırı tekil: emptyList<Int>() ile emptyList<String>() aynı nesneye çözüldü. İki ayrı tip parametresiyle çağrılmış olmalarına rağmen elde tek bir boş liste vardı; çünkü eleman tipi nesnede saklanmıyor. İkinci satır kimlik: Holder equals’ü yeniden tanımlamayan sıradan bir sınıf. Tip parametresi eşitliği bir yönde değiştirmedi.
Varyans: out ve in
Tip parametresi nesnenin içinde silinse de, derleyici onu atamalarda denetler. Varyans (variance), Kutu<Alt> değerinin Kutu<Üst> yerine geçip geçemeyeceğini söyler. Üç hâli var. Kovaryans (covariance), alt tipli kutunun üst tipli kutu yerine geçmesidir ve out ile işaretlenir: parametre yalnız çıktı konumunda, yani dönüş tipinde kullanılabilir. Kontravaryans (contravariance), tersidir ve in ile işaretlenir: parametre yalnız girdi konumunda, yani fonksiyon parametresinde kullanılabilir. İnvaryans (invariance), hiçbirinin geçmediği hâldir ve işaretsiz tip parametresinin varsayılanıdır.
// Main.kt — out ve in ile atama
class Source<out T>(private val value: T) {
fun get(): T = value
}
class Sink<in T> {
fun accept(item: T): String = "kabul:" + item
}
fun main() {
val ints: List<Int> = listOf(1, 2)
val anys: List<Any> = ints
println(anys.size)
val source: Source<Any> = Source<String>("ada")
println(source.get())
val sink: Sink<String> = Sink<Any>()
println(sink.accept("ali"))
Criterion("kovaryant-atama", ints, anys, listOf<Any>("x")).report()
}
2 ada kabul:ali kovaryant-atama: ayni-nesne=true, ikiz=true, farkli=false -> tekil
List<Int> bir List<Any> değişkenine atandı, çünkü List salt okunurdur ve out T ile bildirilmiştir; o atamada yeni bir nesne kurulmadı, aynı nesne iki ayrı tiple okundu ve ölçüt tekil çıktı. Source<String> de Source<Any> yerine geçti. Sink ise ters yönde çalıştı: bir Sink<Any>, String kabul eden bir Sink<String> yerine geçti, çünkü her Any kabul eden bir alıcı String’i de kabul eder.
Değiştirilebilir liste ise kovaryant olamaz. Ö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 numbers: MutableList<Int> = mutableListOf(1) val anys: MutableList<Any> = numbers
TN37. Reddin nedeni, MutableList<Any>’a bir String eklenebilseydi MutableList<Int>’in bozulacağıdır; bu akıl yürütme dilin tanımından geliyor ve burada ayrı bir blokla ölçülmedi. Yalnız atamanın derlenmediği doğrulandı.
Silmenin Sınırları
Silme, tip parametresinin çalışma zamanında okunamaması demektir. Bunun üç sonucu var. Birincisi, bir değer List<*> mi diye sorulabilir ama List<String> mi diye sorulamaz; ikincisi, List<String>’e denetimsiz bir dönüşüm yapılabilir ve hata ancak bir eleman okunduğunda çıkar; üçüncüsü, varyans yalnız sınıf bildiriminde değil, kullanım yerinde de belirtilebilir. Aşağıdaki blok üçünü ölçer.
numbers aslında Int içeren bir listedir ama Any tipiyle tutulur. texts o listenin List<String> olarak denetimsiz görünümüdür. sink ise MutableList<in Int> olarak bildirilir: tip parametresi kullanım yerinde kontravaryant yapılmıştır.
// Main.kt — is List<*>, denetimsiz donusum ve kullanim yeri varyansi
fun main() {
val numbers: Any = listOf(1, 2, 3)
println(numbers is List<*>)
@Suppress("UNCHECKED_CAST")
val texts = numbers as List<String>
println(texts.size)
val outcome = try {
val first: String = texts[0]
"okundu " + first
} catch (e: ClassCastException) {
"sinif-hatasi"
}
println(outcome)
val sink: MutableList<in Int> = mutableListOf<Any>("x")
sink.add(5)
println(sink.size)
println(sink[1])
Criterion("belirsiz-liste", numbers, texts, listOf("a")).report()
}
true 3 sinif-hatasi 2 5 belirsiz-liste: ayni-nesne=true, ikiz=true, farkli=false -> tekil
numbers is List<*> doğru çıktı: yıldız yansıtması, tip parametresini bilmeden yapılan sorudur. texts.size 3 yazdı. Dönüşüm denetimsiz olduğundan, boyutu okumak elemanlara dokunmadığı için patlamadı. Hata, ilk eleman String olarak okunduğunda çıktı: sinif-hatasi satırı, ClassCastException’ın yakalandığını gösteriyor. Derleyici texts’in List<String> olduğuna inandı, çalışma zamanı ise elemanın gerçekte Int olduğunu ancak okuma anında gördü.
Bu, silmenin bedelidir. Tip parametresi çalışma zamanında nesnede olmadığı için dönüşümün doğruluğu orada denetlenemez; denetim eleman okunana kadar ertelenir. Ölçüt satırı tekil: numbers ile texts aynı nesnedir. Dönüşüm yeni bir liste kurmadı, aynı nesneye yeni bir tip etiketi verdi.
sink kullanım yeri varyansını gösterdi. MutableList<in Int>, içine Int eklenebilen ama okunduğunda yalnız Any? veren bir görünümdür. sink.add(5) sonrasında boyut 2 oldu ve sink[1] 5 yazdı; ilk eleman "x" olarak listede kalmıştı. Sınıf bildirimindeki out/in ile aynı kural, tek bir değişken için geçerli kılındı.
TN38. Denetimsiz dönüşümün derleyici uyarısı verdiği iddiasını yazmıyoruz; uyarı bu derste denetlenmiyor. @Suppress işaretini yalnız dönüşümü açıkça kabul ettiğimizi göstermek için yazdık.
Ölçümlerden Çıkan Silme Kuralı
Bu dersin ölçümleri tek bir olguya dayanır: tip parametresi çalışma zamanında nesnede yoktur. Bundan çıkan üç sonuç ölçüldü. Boş iki liste, tip parametreleri farklı olsa da aynı nesne çıktı. is List<*> sorusu cevaplandı, ama List<String> sorusu sorulamadı. Denetimsiz dönüşüm de hemen patlamadı: texts.size 3 yazdı ve hata ancak eleman okunduğunda çıktı.
Sık yapılan hata, dönüşümün güvenli olduğunu varsaymaktır. as List<String> yazan kişi, derleyicinin listeyi artık List<String> saydığını görür ve sonraki kodda her şey düzgün görünür. Oysa nesnenin içeriği değişmedi; yalnız onu okuyan koda verilen söz değişti. Söz tutulmadığında hata, dönüşümün yapıldığı satırda değil, elemanın okunduğu satırda çıkar. Bu iki satır birbirinden uzaksa hatanın kaynağını bulmak zorlaşır. Bu yüzden dışarıdan gelen bir listeyi dönüştürmek yerine, elemanları tek tek denetleyerek yeni bir liste kurmak daha sağlamdır; bu ders onu ölçmedi, yalnız dönüşümün sınırını gösterdi.
Varyans iki ayrı yerde belirtilebilir. Sınıf bildiriminde out ya da in yazılırsa kural sınıfın bütün kullanımlarında geçerlidir; kullanım yerinde MutableList<in Int> yazılırsa kural yalnız o değişken için geçerlidir. Sonuncusunun bedeli okuma yönündedir: sink[1] 5 yazdı, ama değişkenin tipi Any? olduğu için okunan değerle bir işlem yapabilmek için önce tipini denetlemek gerekir.
Kısıtın ve reified’ın rolü de silme ile birlikte okunur. Kısıt, derleme zamanında tip parametresinin neye benzemesi gerektiğini söyler; çalışma zamanında hiçbir şey denetlemez. reified ise yalnız satır içi (inline) fonksiyonlarda silmeyi aşar, çünkü derleyici tip bilgisini çağrı yerine yazar. Bu iki araç silmenin sınırlarını daraltır, ama kaldırmaz.
Ölçmeden Önce Tahmin Etmek
Silme konusunda tahmin, “çalışma zamanında hangi bilgi var” sorusuna iner. Bir nesnenin sınıfı vardır; tip parametresi yoktur. Bu yüzden numbers is List<*> doğru, texts.size 3 ve ilk eleman String olarak okunduğunda sınıf hatası beklenir. Üç sonucun üçü de ölçümle tutuyor. Tutmayan tahmin, “dönüşüm satırı patlar” tahminidir: dönüşüm sırasında nesneye bakan bir denetim yoktur.
Kullanım yeri varyansında tahmin yönlere ayrılır. MutableList<in Int> değişkenine yazma yönünde Int kabul edilir; okuma yönünde elemanın tipi Any? olur. Boyutun 2 olacağını söylemek için listede başlangıçta bir eleman olduğunu ve bir eleman eklendiğini bilmek yeterlidir. Okunan 5 ise, eklenen değerin konumunu bilmeye bağlıdır.
Ölçüt satırı için tahmin basittir. numbers ile texts aynı listeye bakar, dönüşüm yeni nesne kurmaz; bu yüzden ayni-nesne doğru olur ve ölçüt tekil kalır. Bu satırları tekil yapan şey, tip bilgisinin nesnede olmamasıdır: bilgi nesneye değil, kodu okuyan derleyiciye aittir.
Bu tahminlerin hiçbiri bir alışkanlık değildir; hepsi tek bir cümleden türer. Çalışma zamanı nesnenin sınıfını bilir, tip parametresini bilmez. Bir ifadenin sonucu bu cümleden çıkarılamıyorsa, ifade derleme zamanında çözülen bir şeyi, örneğin atamanın geçerliliğini sorguluyordur. Derleme zamanı sorusu ile çalışma zamanı sorusunu ayırmak, genellikler konusunda yapılan hataların büyük kısmını önler.
Sınırlayıcı Ölçüm
Tip parametresi çalışma zamanında silindiğine göre, genel bir tipin iki değerini eşit yapan şey tip parametresi olamaz: eleman değerleri olmalı. Aşağıdaki blok bunu iki yolla ölçer. Genel bir veri sınıfı olan Wrapper’a 1 ve 1L verilir, yani Int ve Long. İkinci satır Any tipli iki listeyi karşılaştırır.
// Main.kt — genel veri sinifinda esitlik degere bakar
data class Wrapper<T>(val value: T)
fun main() {
val checks = listOf(
Criterion("sarmalayici", Wrapper(1), Wrapper(1), Wrapper(1L)),
Criterion("liste-1-1L", listOf<Any>(1), listOf<Any>(1), listOf<Any>(1L)),
Criterion("bos-ve-dolu", emptyList<Int>(), emptyList<Int>(), listOf(1)),
)
for (check in checks) check.report()
summarize(checks)
}
sarmalayici: ayni-nesne=false, ikiz=true, farkli=false -> icerik liste-1-1L: ayni-nesne=false, ikiz=true, farkli=false -> icerik bos-ve-dolu: ayni-nesne=true, ikiz=true, farkli=false -> tekil tekil: 1 kimlik: 0 icerik: 2 kismi-icerik: 0
İlk iki satır icerik çıktı: aynı tarifle ayrıca kurulan iki değer eşitti, 1 yerine 1L konunca eşitlik bozuldu. Wrapper(1) ile Wrapper(1L) eşit değil, çünkü value alanları Int ve Long değerleridir ve equals onlara bakar; bu karşılaştırma tip parametresine değil, çalışma zamanındaki değerlere dayanır. Üçüncü satır tekil: aynı boş liste nesnesi.
Özet
- Genellik, bir sınıfı ya da fonksiyonu tip parametresiyle yazdırır;
T : Comparable<T>bir kısıt,wherebirden fazla kısıt koyma yoludur. - Tip parametresi çalışma zamanında silinir;
emptyList<Int>()veemptyList<String>()aynı nesnedir,reifiedise silmeyi bir fonksiyonda aşar. outkovaryans,inkontravaryans verir; işaretsiz parametre invaryanttır veMutableList<Int>,MutableList<Any>değişkenine atanamaz.- Salt okunur
List<Int>,List<Any>’a atanır; atamada yeni nesne oluşmaz. - Genel bir veri sınıfında eşitlik tip parametresine değil, alanların çalışma zamanı değerlerine bakar.
- Bu dersin ölçütü: tekil. Tip parametresi silindiği için farklı parametrelerle çağrılan iki boş liste aynı nesnedir.
Sonraki Adım
Bu dersle tipler konusu kapanıyor: tekil nesneden devredilen davranışa, dışarıdan eklenen fonksiyona ve silinen tip parametresine kadar, eşitliğin her zaman değerin kendisinden geldiğini gördük. Sıradaki bölüm “Koleksiyon Türleri”: liste, küme ve eşlem de genel tiplerdir ve içlerindeki değerlerin eşit sayılması koleksiyonun türüne değil, içeriğine bağlıdır.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.