İçeriğe geç
academia.sh

Ders 12 / 23

Veri Sınıfları

Veri sınıfının birincil yapıcıdaki özelliklerden ürettiği yöntemlerin, copy ile kurulan ayrı nesnenin ve alan tipinin ölçüte etkisinin ölçümü.

Yayın Güncelleme

Bu derste

  • Veri sınıfının equals, hashCode, toString ve copy yöntemlerini birincil yapıcıdan ürettiğini ölçer
  • copy ile kurulan ayrı nesnenin içerik ölçütüne düştüğünü gösterir
  • Yapısöküm bildiriminin componentN yöntemlerine karşılık geldiğini ölçer
  • Alanın tipinin, yani dizi ya da liste olmasının ölçütü belirlediğini sınırlayıcı ölçümle doğrular
İçindekiler

Önceki ders equals’i yazılmamış bir sınıfın aynı değerli örneklerinin kimlik ölçütüne düştüğünü, equals ve hashCode elle yazılınca içeriğe geçtiğini gösterdi. O iki yöntemi her sınıfta elle yazmak sıkıcı ve hataya açıktır. Kotlin bu işi data class anahtar sözcüğüyle derleyiciye bırakır: birincil yapıcıdaki özelliklerden equals, hashCode, toString, copy ve componentN yöntemlerini üretir. Bu ders üretilenlerin ne yaptığını ve ölçütü nasıl kurduğunu ölçer.

Ölçü ekseni aynı kalır: bir değerin kendisine eşit sayılan başka bir değerle ilişkisi, yani ölçüt. Criterion sınıfı açılış dersindeki hâliyle aşağıda yeniden verilir. Kural sırayla işler: ilk değer ikiziyle aynı nesneyse tekil; değilse ve ikiz eşit değilse kimlik; ikiz eşit ama üçüncü değer eşit değilse içerik; üçüncü değer de eşitse kısmi içerik. Bu ders içerik kesitini ölçer.

// 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 }}")
    }
}

Üretilen Yöntemler

data class Ticket(val id: Int, val owner: String) tek satırdır ve gövdesi yoktur. Aşağıdaki blok üretilenleri sırayla dener. println(ticket) üretilmiş toString’i gösterir. İki karşılaştırma equals’e bakar: aynı değerli ayrı bir Ticket ile ve owner’ı farklı bir Ticket ile. Dördüncü satır iki eşit nesnenin hashCode değerlerinin eşit olup olmadığını sorar; değerin kendisi basılmaz. Son iki satır copy’yi dener: owner değiştirilerek kurulan kopya ile ilk nesnenin kendisi.

// Main.kt — uretilen yontemler
data class Ticket(val id: Int, val owner: String)

fun main() {
    val ticket = Ticket(7, "ada")
    println(ticket)
    println(ticket == Ticket(7, "ada"))
    println(ticket == Ticket(7, "can"))
    println(ticket.hashCode() == Ticket(7, "ada").hashCode())
    val moved = ticket.copy(owner = "can")
    println(moved)
    println(ticket)
}
Ticket(id=7, owner=ada)
true
false
true
Ticket(id=7, owner=can)
Ticket(id=7, owner=ada)

toString alanları adlarıyla basar. equals aynı değerli ayrı nesneyi eşit, owner’ı farklı olanı eşit değil bulur ve hashCode eşit nesnelerde eşit çıkar. copy(owner = "can") ayrı bir nesne kurar; ilk nesne değişmeden kalır.

TN15. Ölçüm iki alanlı küçük bir veri sınıfı içindir. Çok alanlı bir sınıfta ya da alanlardan biri başka bir veri sınıfı olduğunda üretilen yöntemlerin aynı davranıp davranmadığı bu bloktan çıkmaz.

Copy ve Ölçüt

Şimdi aynı yöntemler ölçüte bağlanır. Üç satır vardır. İlkinde üç değerden ilk ikisi aynı nesnedir. İkincisinde ikiz ticket.copy() ile, yani hiçbir alan değiştirilmeden kurulur. Üçüncüsünde ikiz birincil yapıcıyı doğrudan çağırarak kurulur. Her satırda üçüncü değer id’si ya da owner’ı farklı bir Ticket’tır.

// Main.kt — copy ve olcut
data class Ticket(val id: Int, val owner: String)

fun main() {
    val ticket = Ticket(7, "ada")
    val checks = listOf(
        Criterion("ayni-nesne", ticket, ticket, ticket.copy(id = 8)),
        Criterion("copy", ticket, ticket.copy(), ticket.copy(id = 8)),
        Criterion("kurucu", ticket, Ticket(7, "ada"), Ticket(7, "can")),
    )
    for (check in checks) check.report()
    summarize(checks)
}
ayni-nesne: ayni-nesne=true, ikiz=true, farkli=false -> tekil
copy: ayni-nesne=false, ikiz=true, farkli=false -> icerik
kurucu: ayni-nesne=false, ikiz=true, farkli=false -> icerik
tekil: 1
kimlik: 0
icerik: 2
kismi-icerik: 0

Kural ilk satırda ilk soruda durur: aynı nesne olduğu için ölçüt tekildir. İkinci ve üçüncü satırlarda ikiz ayrı bir nesnedir, ikiz=true çıkar, üçüncü değer eşit olmadığı için ölçüt içeriktir. copy() ile kurulan nesne ile yapıcıyla kurulan nesne arasında ölçüt farkı yoktur. Önceki dersteki Coordinate ise aynı iki satırda kimlik veriyordu. Fark kurulum yolunda değil, equals’in kim tarafından yazıldığındadır.

Bu dersin ayrımı da buradadır: ölçülen, copy’nin bir yöntem olarak bulunması değil, eşitliği hangi ölçütün verdiğidir. copy ayrı bir nesne kurar; o nesnenin eşit sayılması üretilen equals’ten gelir.

TN16. Ölçüm yalnızca Ticket içindir. copy() yüzeysel bir kopya kurar; alanlar değişebilir bir nesneyse iki kopyanın o alanı paylaşıp paylaşmadığı bu blokta ölçülmedi.

Yapısöküm Bildirimi ve componentN

Veri sınıfının ürettiği component1, component2 gibi yöntemler yapısöküm bildirimini mümkün kılar: val (id, owner) = ticket satırı, her ada sırayla bir componentN çağrısı atar. Sıra birincil yapıcıdaki parametre sırasıdır. Aşağıdaki blok bildirimi ve iki componentN çağrısını dener. Ardından üretilen yöntemlerin hangi alanlara baktığını sorar: Stamped sınıfında note alanı birincil yapıcıda değil gövdede tanımlıdır. İki Stamped nesnesi aynı yapıcı değerleriyle ama farklı note ile kurulur ve karşılaştırılır.

// Main.kt — yapisokum ve componentN
data class Ticket(val id: Int, val owner: String)

data class Stamped(val id: Int, val owner: String) {
    var note: String = ""
}

fun main() {
    val ticket = Ticket(7, "ada")
    val (id, owner) = ticket
    println("$id $owner")
    println(ticket.component1())
    println(ticket.component2())
    val first = Stamped(1, "ada")
    first.note = "a"
    val second = Stamped(1, "ada")
    second.note = "b"
    println(first == second)
    println(first)
}
7 ada
7
ada
true
Stamped(id=1, owner=ada)

Bildirim 7 ada verir. component1() ve component2() aynı değerleri doğrudan döndürür. first == second ise true çıkar, oysa note alanları a ve b’dir. toString da note’u basmaz. Üretilen yöntemler yalnızca birincil yapıcıdaki özelliklere bakar; gövdedeki özellik eşitliğe ve çıktıya girmez.

TN17. Bu yalnızca gövdede var ile tanımlanan bir özellik için ölçüldü. Gövdede tanımlanıp hesaplanan bir özelliğin ya da üst sınıftan gelen bir özelliğin ayrı bir davranışı olup olmadığı ölçülmedi.

Copy Yüzeyseldir

copy alanları kopyalar, ama bir alan değişebilir bir koleksiyonsa kopyalanan şey koleksiyonun kendisi değil, ona giden referanstır. Blokta Basket bir MutableList taşır. İlk kopyaya öğe eklenince ilk nesne etkilenir mi? İkinci denemede liste açıkça kopyalanır ve aynı soru yeniden sorulur.

// Main.kt — copy yuzeyseldir
data class Basket(val owner: String, val items: MutableList<String>)

fun main() {
    val first = Basket("ada", mutableListOf("elma"))
    val second = first.copy(owner = "can")
    second.items.add("armut")
    println(first.items)
    println(second.items)
    println(first.items === second.items)
    val deep = first.copy(items = first.items.toMutableList())
    deep.items.add("kiraz")
    println(first.items)
    println(deep.items)
}
[elma, armut]
[elma, armut]
true
[elma, armut]
[elma, armut, kiraz]

Çıktının ilk iki satırı [elma, armut] olarak aynıdır. second üzerinden eklenen armut, first içinde de görünür. Üçüncü satır nedenini söyler: first.items === second.items true verir, yani iki nesne aynı listeye gidiyor. copy alanları tek tek kopyalar, ama liste alanında kopyalanan şey listenin kendisi değil, ona giden referanstır. Kopyalama yüzeyseldir.

Son iki satırda liste toMutableList() ile açıkça kopyalanır. Bu kez deep içine eklenen kiraz, first içinde görünmez. Sık yapılan hata, copy’yi tam bağımsız bir kopya sanmaktır. Değişebilir bir koleksiyon taşıyan veri sınıfında copy çağrısı, bir yandan eşitlik semantiğini korur, öte yandan iki nesneyi aynı durumun sahibi yapar. Çözüm ya alanı salt okunur List yapmak ya da koleksiyonu copy çağrısında açıkça kopyalamaktır. İçerik ölçütü burada da geçerlidir: iki nesne equals bakımından eşittir, ama biri değişince öteki de değişir.

Anahtar Olarak Kullanım ve Sık Yapılan Hata

Üretilen hashCode, veri sınıfını harita anahtarı ve küme öğesi olarak kullanılabilir kılar. Aynı değerli ayrı bir nesneyle arama bulur. Ama alanı var olan bir veri sınıfı kümeye girdikten sonra değiştirilirse karma kodu değişir ve nesne kümede bulunduğu yerden ayrılır. Blok önce haritadan aynı değerli bir anahtarla okur, sonra kümeye konan bir nesnenin alanını değiştirip aramayı yineler.

// Main.kt — anahtar olarak veri sinifi
data class Ticket(val id: Int, var owner: String)

fun main() {
    val prices = hashMapOf(Ticket(1, "ada") to 10)
    println(prices[Ticket(1, "ada")])
    val key = Ticket(2, "can")
    val set = hashSetOf(key)
    println(set.contains(key))
    key.owner = "eda"
    println(set.contains(key))
    println(set.size)
    println(set.contains(Ticket(2, "can")))
    println(set.any { it == key })
}
10
true
false
1
false
true

Haritadan okuma 10 verir: aynı değerli ayrı bir Ticket anahtar olarak bulunur, çünkü eşitlik ve karma kodu alan değerlerinden gelir. Kümeye konan nesne için contains önce true döner. key.owner değiştirildikten sonra aynı nesneyle yapılan arama false olur, ama size hâlâ 1’dir: nesne kümededir, yalnızca bulunamaz. Eski değerle kurulan Ticket(2, "can") de bulunamaz, çünkü kümedeki öğe şimdi eda sahibini taşır ve eşit değildir. Doğrusal any taraması ise nesneyi bulur.

Mekanizmanın nedeni şudur: karma tabanlı koleksiyon öğeyi, eklendiği andaki karma koduna göre yerleştirir. Alan değişince kod değişir ve arama başka bir yere bakar. Sık yapılan hata, anahtar ya da küme öğesi olarak kullanılan bir veri sınıfında var alan bırakmaktır. Kural: koleksiyona konan bir nesnenin eşitliğe giren alanları o nesne koleksiyondayken değiştirilmez. Anahtar olacak veri sınıfları val alanlarla yazılır, değişiklik gerekiyorsa copy ile ayrı bir nesne üretilip koleksiyondan eskisi çıkarılır.

Yapısöküm Döngülerde

Yapısöküm bildirimi yalnızca val satırlarında değil, for döngülerinde ve lambda parametrelerinde de çalışır. Kullanılmayan bir konum _ ile atlanır. Harita girdileri de component1 ve component2 sağladığı için aynı biçimde açılır. Blok bu biçimlerin hepsini dener.

// Main.kt — yapisokum dongude
data class Ticket(val id: Int, val owner: String)

fun main() {
    val tickets = listOf(Ticket(1, "ada"), Ticket(2, "can"))
    for ((id, owner) in tickets) println("$id=$owner")
    for ((_, owner) in tickets) println(owner)
    val (first) = tickets[1]
    println(first)
    val prices = mapOf("elma" to 3, "armut" to 5)
    for ((name, price) in prices) println("$name:$price")
    println(tickets.map { (id, _) -> id })
}
1=ada
2=can
ada
can
2
elma:3
armut:5
[1, 2]

İlk iki döngü 1=ada, 2=can, sonra yalnızca sahip adlarını basar; _ ilk konumu atlar. val (first) = tickets[1] satırı dikkat ister: first ilk konumdaki değerdir (component1), yani id’dir ve çıktı 2 olur, ilk bilet değil. Harita döngüsünde (name, price) girdileri component1 ve component2 ile açar. Son satırdaki lambda parametresi de aynı biçimde açılır ve [1, 2] verir.

Yapısökümün belirleyici yanı, alan adlarına değil sıraya dayanmasıdır. componentN fonksiyonları, birincil yapıcıdaki parametre sırasıyla numaralanır. Sık yapılan hata, sınıfa ek bir alan eklerken sıralamayı bozmaktır; yapıcıda id ile owner arasına bir alan girerse eski yapısöküm satırları hâlâ derlenir ama başka alanı okur. Bu nedenle alanlar yalnızca sonuna eklenir ve yapısöküm yalnızca küçük, kararlı veri sınıflarında kullanılır. Gerisi için ticket.owner biçimi, sıraya değil adın anlamına dayandığı için daha dayanıklıdır.

İç İçe Veri Sınıfları

Bir veri sınıfının alanı da veri sınıfıysa üretilen yöntemler iç içe çalışır. toString iç nesneyi de açar, equals iç nesnenin equals’ine iner, copy ise iç nesneyi olduğu gibi taşır. Blok, null olabilen bir iç alanla bunu dener.

// Main.kt — ic ice veri siniflari
data class Address(val city: String)
data class Person(val name: String, val address: Address?)

fun main() {
    val person = Person("ada", Address("izmir"))
    println(person)
    println(person == Person("ada", Address("izmir")))
    println(person.copy(address = null))
    println(person.copy(address = Address("ankara")) == person)
    println(Person("ada", null) == Person("ada", null))
}
Person(name=ada, address=Address(city=izmir))
true
Person(name=ada, address=null)
false
true

Üretilen toString iç nesneyi de açar: Person(name=ada, address=Address(city=izmir)). equals iç nesnenin equals’ine iner, bu yüzden aynı değerli iki ayrı Person eşit çıkar (true). copy(address = null) yalnızca belirtilen alanı değiştirir ve null bir alan olarak sorunsuz taşınır; toString bunu address=null olarak gösterir. Farklı şehirle kurulan kopya özgün nesneye eşit değildir (false), iki null adresli kişi ise eşittir (true).

Bu zincirleme davranış, iç nesne de bir veri sınıfı olduğu sürece geçerlidir. İç alan equals yazılmamış sıradan bir sınıfsa, dış nesnenin eşitliği o alanın kimliğine düşer: aynı değerli iki dış nesne yine eşit çıkmaz. Sık yapılan hata, yalnızca dış sınıfı veri sınıfı yapıp bunun tüm ağaca yeteceğini varsaymaktır. Eşitlik, ağacın her düğümünde ayrı ayrı sağlanır. Bu dersin ölçütü olan içerik, ancak içerideki alanlar da içerik ölçütüne sahipse en dışa kadar taşınır.

Sınırlayıcı Ölçüm

Üretilen equals alanları == ile karşılaştırır. O hâlde bir alanın tipi, alanın kendi equals’ine göre ölçütü etkilemelidir. Sınırlayıcı ölçüm aynı şekle sahip iki veri sınıfını karşılaştırır: Packet bir IntArray taşır, Message aynı sayıları bir List<Int> içinde taşır. İki satırda da ikiz aynı değerlerle ayrıca kurulur, üçüncü değerin yalnızca id’si farklıdır. Bloğun son satırı farklı bir şey sorar: iki Packet aynı IntArray örneğini paylaşırsa eşit midir?

// Main.kt — alan tipine gore
data class Packet(val id: Int, val bytes: IntArray)
data class Message(val id: Int, val bytes: List<Int>)

fun main() {
    val checks = listOf(
        Criterion("dizi", Packet(1, intArrayOf(1, 2)), Packet(1, intArrayOf(1, 2)), Packet(2, intArrayOf(1, 2))),
        Criterion("liste", Message(1, listOf(1, 2)), Message(1, listOf(1, 2)), Message(2, listOf(1, 2))),
    )
    for (check in checks) check.report()
    summarize(checks)
    val shared = intArrayOf(1, 2)
    println(Packet(1, shared) == Packet(1, shared))
}
dizi: ayni-nesne=false, ikiz=false, farkli=false -> kimlik
liste: ayni-nesne=false, ikiz=true, farkli=false -> icerik
tekil: 0
kimlik: 1
icerik: 1
kismi-icerik: 0
true

Aynı şekle sahip iki sınıf iki ayrı ölçüt verir. Packet, içeriği aynı ama ayrı iki dizi ile kurulduğunda eşit çıkmaz: dizinin equals’i kimliğe bakar ve veri sınıfının ürettiği equals alanı bu equals ile karşılaştırır. Message ise listenin equals’i içeriğe baktığı için içerik verir. Son satır bunu doğrular: aynı dizi örneği paylaşılınca Packet de eşit çıkar. Üretilen yöntem alan tipinin eşitliğini devralır; ölçütü veri sınıfı değil, alanların tipi belirler.

TN18. Ölçüm yalnızca IntArray ve List<Int> içindir. Başka dizi ya da koleksiyon tiplerinin aynı kuralı izlediği ölçülmedi.

Özet

  • data class birincil yapıcıdaki özelliklerden equals, hashCode, toString, copy ve componentN üretir.
  • copy ayrı bir nesne kurar; o nesne üretilen equals ile ilk nesneye eşit sayılır.
  • Yapısöküm bildirimi component1, component2 çağrılarına karşılık gelir ve sırası yapıcı sırasıdır.
  • Gövdedeki özellikler üretilen yöntemlere girmez; alan tipi eşitliği belirler, IntArray kimlik, List<Int> içerik verir.
  • Bu dersin ölçütü: içerik. Sınırlayıcı ölçümde aynı şekle sahip iki veri sınıfı alan tipine göre içeriğe ve kimliğe düştü.

Sonraki Adım

Bir değerin birkaç kapalı biçimden yalnızca birine ait olduğunu anlatmak için sınıflar tek başına yetmez. Bir sonraki ders enum class ile numaralandırmaları ve sealed interface ile mühürlü hiyerarşileri ölçer: tümlü when’in eksik dalı nasıl reddettiğine ve numaralandırma girdisinin hangi ölçüte düştüğüne bakar.

İlerlemeni kaydetmek ve not almak için Giriş yap

Notlarım

Not almak için giriş yapmalısın.

Aramak için yazmaya başlayın.

↑↓ ↵ Esc gezin · aç · kapat