Ders 11 / 23
Sınıflar ve Yapıcılar
Birincil yapıcının, init bloğunun ve ikincil yapıcının çalışma sırasının ve equals yazılmamış bir sınıfın örneklerinin neden kimlik ölçütüne düştüğünün ölçümü.
Yayın Güncelleme
Bu derste
- Özellik başlatıcılarının, init bloğunun ve ikincil yapıcı gövdesinin çalışma sırasını ölçer
- Varsayılan değerli parametrelerin ve özel erişimci tanımının davranışını gösterir
- equals yazılmamış bir sınıfın iki yoldan kurulan aynı değerli örneklerinin kimlik ölçütüne düştüğünü ölçer
- Eşitliği sınıfın değil yazılan equals yöntemlerinin belirlediğini sınırlayıcı ölçümle doğrular
İçindekiler
Önceki ders Elvis işlecinin sağ tarafında equals’i yazılmamış bir sınıfın ölçütü kimliğe çevirdiğini gösterdi. Bu ders o sınıfların nasıl yazıldığına bakar: birincil yapıcı, init bloğu, ikincil yapıcı ve özellikler. Önce hangisinin ne zaman çalıştığı ölçülür, sonra aynı değerli iki örneğin neden eşit sayılmadığı ölçüt üzerinden açıklanır.
Ö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 kimlik kesitini ölçer ve sınırlayıcı ölçümde içerik ile kısmi içeriğe geç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 }}")
}
}
Yapıcıların Çalışma Sırası
Kotlin’de bir sınıfın birincil yapıcısı sınıf başlığına yazılır: class Member(val name: String, val level: Int). Parametrenin önündeki val hem parametreyi hem aynı adlı özelliği tanımlar. Gövdede başlatıcılı özellikler ve init blokları yer alabilir; bunlar birincil yapıcının gövdesi gibi davranır ve dosyadaki yazılış sırasıyla çalışır. İkincil yapıcı constructor anahtar sözcüğüyle yazılır ve : this(...) ile birincil yapıcıya yönlenmek zorundadır. Yönlenilen yapıcı önce koşar, ikincil yapıcının kendi gövdesi en sonda.
Aşağıdaki blok bu sırayı çıktıdan okunur hâle getirir. trace fonksiyonu her çağrıldığında bir satır basar. title özelliğinin başlatıcısı trace çağırır, init bloğu bir satır basar, ikincil yapıcının gövdesi de bir satır basar. Member("ada") ikincil yapıcıyı, Member("can", 2) doğrudan birincil yapıcıyı çağırır.
// Main.kt — yapici sirasi
fun trace(label: String): String {
println("adim: $label")
return label
}
class Member(val name: String, val level: Int) {
val title = trace("ozellik")
init {
println("init: $name")
}
constructor(name: String) : this(name, 1) {
println("ikincil: $name")
}
}
fun main() {
val first = Member("ada")
println(first.level)
val second = Member("can", 2)
println(second.level)
}
adim: ozellik init: ada ikincil: ada 1 adim: ozellik init: can 2
İki kurulumda da adim: ozellik satırı init: ... satırından önce gelir, çünkü title bloktan önce yazılmıştır. İkincil yapıcı yalnızca ilk kurulumda koşar ve ikincil: ada satırı init: ada satırından sonra basılır: birincil yapıcı ve onun başlatıcıları ikincil yapıcının gövdesinden önce tamamlanır. İkinci kurulumda ikincil satırı yoktur, çünkü birincil yapıcı doğrudan çağrılmıştır.
TN11. Bu ölçüm yalnızca bu sınıf için sıra gösterir. Başlatıcı ile init bloğunun yer değiştirmesi çıktıyı değiştirir; sıra yazılış sırasıdır. Hangi başlatıcının hangi init bloğundan önce geleceği, ölçülen sınıfın yazılışına bağlıdır.
Özellikler, Varsayılan Değerler ve Erişimciler
Birincil yapıcıdaki val ve var parametreleri özellik olur. Özelliklerin hesaplanan türü de vardır: val area: Int get() = width * height bir alanda saklanmaz, her okunduğunda hesaplanır. Parametrelere varsayılan değer verilebilir ve çağıran yer parametreleri adıyla, sırayı bozarak da verebilir. var bir özelliğe özel set yazılırsa atanan değer alana geçmeden önce işlenebilir; alanın kendisine field adıyla erişilir.
Aşağıdaki blok üç şeyi ölçer. Rectangle(3) yüksekliği varsayılan değer olan 1’e bırakır. Rectangle(height = 2, width = 5) parametreleri sırasını bozarak adıyla verir. Son olarak label özelliğine baş ve sonunda boşluk olan bir dizgi atanır; set bu boşlukları temizler.
// Main.kt — ozellikler
class Rectangle(val width: Int, val height: Int = 1) {
val area: Int get() = width * height
var label: String = "adsiz"
set(value) {
field = value.trim()
}
}
fun main() {
val narrow = Rectangle(3)
println(narrow.area)
val wide = Rectangle(height = 2, width = 5)
println(wide.area)
wide.label = " kutu "
println("[" + wide.label + "]")
}
3 10 [kutu]
Alan 3 çarpı 1 ve 5 çarpı 2 olarak hesaplanmıştır. Etiket atandığı gibi değil, set içinden geçtikten sonra saklanmıştır: köşeli parantezler boşluk kalmadığını gösterir.
TN12. area bir alanda saklanmaz. Ölçülen blok yalnızca okuma sonucunu gösterir; width ve height değişebilir olsaydı area’nın değişeceği bu bloktan çıkmaz. Bu yalnızca yapıcıdan gelen val değerleri için ölçüldü.
Equals Yazılmamış Bir Sınıf
Şimdi ölçüte dönülür. Coordinate sınıfının iki yapıcısı vardır: birincil yapıcı iki sayı alır, ikincil yapıcı "3,4" biçimindeki bir dizgiyi ayırarak aynı birincil yapıcıya yönlenir. Sınıfta equals yoktur. Kotlin’de her sınıf Any’den türer ve Any’nin equals yöntemi iki referansın aynı nesneyi gösterip göstermediğine bakar. Sınıf bunu yeniden tanımlamadıkça == işleci de kimliğe bakar.
// Coordinate.kt — equals yazilmamis sinif
class Coordinate(val x: Int, val y: Int) {
constructor(text: String) : this(text.substringBefore(",").toInt(), text.substringAfter(",").toInt())
}
Ölçüm üç satırdan oluşur. İlkinde üç değerden ilk ikisi aynı nesnedir. İkincisinde iki ayrı Coordinate(3, 4) birincil yapıcıdan kurulur. Üçüncüsünde ikiz ikincil yapıcıdan, yani dizgiden kurulur; değerler yine 3 ve 4’tür.
// Main.kt — iki yoldan kurulan ornekler
fun main() {
val same = Coordinate(1, 2)
val checks = listOf(
Criterion("ayni-nesne", same, same, Coordinate(1, 3)),
Criterion("ayni-yol", Coordinate(3, 4), Coordinate(3, 4), Coordinate(3, 5)),
Criterion("iki-yol", Coordinate(3, 4), Coordinate("3,4"), Coordinate(3, 5)),
)
for (check in checks) check.report()
summarize(checks)
}
ayni-nesne: ayni-nesne=true, ikiz=true, farkli=false -> tekil ayni-yol: ayni-nesne=false, ikiz=false, farkli=false -> kimlik iki-yol: ayni-nesne=false, ikiz=false, farkli=false -> kimlik tekil: 1 kimlik: 2 icerik: 0 kismi-icerik: 0
Kural ilk satırda ilk soruda durur: aynı nesne olduğu için ölçüt tekildir. İkinci ve üçüncü satırlarda alanlar birebir aynı olduğu hâlde ikiz=false çıkar ve ölçüt kimliktir. Kurulum yolu, yani birincil mi ikincil mi olduğu, sonucu değiştirmez. Aynı değerli iki örneği eşit yapan şey sınıfın alanları değildir; eşit yapan, equals’in ne yaptığıdır.
Bu dersin ölçüsünü bir ayrımla tamamlamak gerekir. Burada ölçülen, iki örneğin birbirine eşit sayılıp sayılmadığıdır. Bir örneğin başka bir yerden paylaşılıp paylaşılmadığı ya da bir değişikliğin başka bir referansta görünüp görünmediği ayrı bir sorudur ve bu derste ölçülmedi. Kimlik ölçütü yalnızca şunu söyler: == iki ayrı nesne için false verir.
TN13. Ölçüm yalnızca Coordinate ve iki yapıcı yolu içindir. Alanları başka tipte olan, ya da Any’den başka bir üst sınıftan türeyen bir sınıf için aynı sonuç ölçülmedi. Üst sınıf equals’i yeniden tanımlamışsa tablo başka olabilir.
Kalıtımda Yapıcı Sırası
Bir sınıf başka bir sınıftan türediğinde yapıcı sırasına bir halka daha eklenir. Kotlin sınıfları varsayılan olarak kapalıdır; türetilecek sınıfa open yazılır ve alt sınıf, üst sınıfın yapıcısını başlık satırında çağırır. Aşağıdaki blokta hem üst sınıfın hem alt sınıfın bir init bloğu vardır. Hangisinin önce çalıştığı çıktıdan okunur.
// Main.kt — ust sinif yapicisi
open class Base(val name: String) {
init {
println("Base init: $name")
}
}
class Derived(name: String, val extra: Int) : Base(name) {
init {
println("Derived init: $extra")
}
}
fun main() {
val item = Derived("ada", 3)
println(item.name + item.extra)
}
Base init: ada Derived init: 3 ada3
Çıktı sırayı açıkça gösterir: Base init: ada satırı Derived init: 3 satırından önce gelir. Alt sınıfın başlık satırındaki Base(name) çağrısı, alt sınıfın kendi başlatıcıları çalışmadan önce üst sınıfı kurar. Nedeni basittir: alt sınıfın durumu üst sınıfın durumuna dayanabilir, bu yüzden dayanılan taraf önce hazır olmalıdır. Son satır, name değerinin üst sınıftan, extra değerinin alt sınıftan geldiğini ve ikisine tek nesne üzerinden erişildiğini gösterir.
Bu sıra, önceki bölümdeki çalışma sırasını genişletir. Tek sınıfta sıra özellik başlatıcıları, init blokları ve ikincil yapıcı gövdesidir; türetmede bunun önüne üst sınıfın tamamı eklenir. Üst sınıfın yapıcısı içinde alt sınıfın bir özelliğine dokunulursa o özellik henüz kurulmamıştır; bu nedenle üst sınıf yapıcıları alt sınıfa ait durumu okumaz. Sorumluluğun sınırı bu çıktıda görünür: her sınıf yalnızca kendi init bloğunda kendi alanlarını kurar.
Okuma Başına Hesap ve Tek Seferlik Başlatıcı
Daha önce area özelliğinin bir alanda saklanmadığı, her okunduğunda hesaplandığı söylendi. Bunun ölçülebilir bir sonucu var: hesaplayan fonksiyon kaç kez çağrılır? Blokta iki özellik aynı fonksiyonu çağırır. Biri başlatıcıyla tanımlanmıştır, öteki get() ile. İkisi de üçer kez okunur; sayaçlar fonksiyonun kaç kez koştuğunu tutar.
// Main.kt — okuma basina hesap
var storedCalls = 0
var computedCalls = 0
fun compute(kind: String): Int {
if (kind == "stored") storedCalls++ else computedCalls++
return 10
}
class Holder {
val stored: Int = compute("stored")
val computed: Int get() = compute("computed")
}
fun main() {
val holder = Holder()
println(holder.stored + holder.stored + holder.stored)
println(holder.computed + holder.computed + holder.computed)
println("stored=$storedCalls computed=$computedCalls")
}
30 30 stored=1 computed=3
Çıktının son satırı sayaçları verir: stored=1 computed=3. İki özellik de üçer kez okundu ve ikisi de 30 toplamını verdi, ama hesaplayan fonksiyon farklı sayıda çalıştı. Başlatıcıyla tanımlanan özellik değeri nesne kurulurken bir kez hesaplar ve bir alanda saklar. get() ile tanımlanan özellik ise alan taşımaz; her okumada fonksiyon yeniden çalışır.
İki sonuç da aynı değeri döndürdüğü için fark çıktıdan değil, yalnızca sayaçtan görünür. Bu yüzden get() ile tanımlanan özellik, ucuz ve her seferinde güncel olması gereken değerler için uygundur: başka özelliklerden türeyen bir alan ya da bir toplam. Pahalı bir hesap ya da yan etkisi olan bir fonksiyon get() içine konursa her okuma o bedeli öder ve yan etki okuma sayısı kadar tekrarlanır. Hesabın bir kez yapılması gerekiyorsa başlatıcı, her seferinde güncel olması gerekiyorsa erişimci seçilir.
Yazılan Equals ve Başka Tipler
Elle yazılan bir equals yalnızca aynı tipteki değerlerle karşılaşmaz. Parametresi Any? olduğu için null ve alakasız tipler de gelebilir. Yazdığımız other is ValueCoordinate denetimi bu durumları ayıklar. Blok equals’i dört kez çağırır: null ile, bir dizgiyle, aynı değerli başka bir örnekle ve nesnenin kendisiyle.
// Main.kt — equals ve baska tipler
class ValueCoordinate(val x: Int, val y: Int) {
override fun equals(other: Any?): Boolean =
other is ValueCoordinate && x == other.x && y == other.y
override fun hashCode(): Int = 31 * x + y
}
fun main() {
val point = ValueCoordinate(1, 2)
val anything: Any = "1,2"
println(point.equals(null))
println(point.equals(anything))
println(point == ValueCoordinate(1, 2))
println(point.equals(point))
}
false false true true
Dört çağrının hepsi Boolean döner ve çıktı false, false, true, true olur. equals(null) false verir, çünkü is denetimi null için başarısız olur. Dizgi Any olarak verildiğinde de false çıkar; tip uyuşmadığı için alanlara bakılmaz bile. Aynı değerli ayrı örnekte true, nesnenin kendisinde yine true döner.
Parametrenin Any? olması bir sözleşmedir: equals her türlü değerle karşılaşabilir ve istisna fırlatmadan false demelidir. Sık yapılan hata, parametreyi ValueCoordinate varsayıp doğrudan dönüştürmek, ya da tip denetimini atlayıp alanlara erişmektir. Doğru kalıp önce is denetimidir; && zinciri sayesinde alan karşılaştırması yalnızca denetim geçtikten sonra yapılır ve derleyici sağ taraftaki other değerini ValueCoordinate olarak görür. hashCode aynı alanlardan hesaplanır; eşit nesneler eşit karma kodu üretmelidir, aksi halde karma tabanlı koleksiyonlar tutarsız çalışır.
Sınırlayıcı Ölçüm
Eşitliği sınıfın alanları değil, yazılan equals belirliyorsa aynı alanlara sahip sınıflar equals’e göre farklı ölçütlere düşmelidir. Sınırlayıcı ölçüm bunu üç satırla dener. ValueCoordinate iki alana da bakan bir equals ve onunla uyumlu bir hashCode yazar. LeftCoordinate yalnızca x alanına bakar. Üç satırdan ilki tam eşitliği, ikincisi x aynı y farklı durumu, üçüncüsü x farklı durumu dener. Bloğun sonundaki iki satır hashSetOf ile aynı değerli iki örneği bir kümeye koyar ve kümenin boyutunu basar; equals ile birlikte hashCode’un da yazıldığını bu yoldan gözlemler. hashCode değerlerinin kendisi basılmaz.
// Main.kt — equals yazilinca
class ValueCoordinate(val x: Int, val y: Int) {
override fun equals(other: Any?): Boolean =
other is ValueCoordinate && x == other.x && y == other.y
override fun hashCode(): Int = 31 * x + y
}
class LeftCoordinate(val x: Int, val y: Int) {
override fun equals(other: Any?): Boolean = other is LeftCoordinate && x == other.x
override fun hashCode(): Int = x
}
fun main() {
val checks = listOf(
Criterion("tam", ValueCoordinate(3, 4), ValueCoordinate(3, 4), ValueCoordinate(3, 5)),
Criterion("yalniz-x-y-farkli", LeftCoordinate(3, 4), LeftCoordinate(3, 4), LeftCoordinate(3, 5)),
Criterion("yalniz-x-x-farkli", LeftCoordinate(3, 4), LeftCoordinate(3, 4), LeftCoordinate(2, 4)),
)
for (check in checks) check.report()
summarize(checks)
println(hashSetOf(Coordinate(3, 4), Coordinate(3, 4)).size)
println(hashSetOf(ValueCoordinate(3, 4), ValueCoordinate(3, 4)).size)
}
tam: ayni-nesne=false, ikiz=true, farkli=false -> icerik yalniz-x-y-farkli: ayni-nesne=false, ikiz=true, farkli=true -> kismi-icerik yalniz-x-x-farkli: ayni-nesne=false, ikiz=true, farkli=false -> icerik tekil: 0 kimlik: 0 icerik: 2 kismi-icerik: 1 2 1
Aynı sınıf kalıbı üç satırda iki ölçüt verir. ValueCoordinate iki alanı da karşılaştırdığı için ikiz ve farklı değer ayrışır: içerik. LeftCoordinate y farklı olduğunda bile eşit çıkar, dolayısıyla üçüncü değer ikizle aynı sonucu verir ve ölçüt kısmi içeriğe düşer; x farklı olduğunda ise içerik çıkar. Aynı sınıfın iki ayrı satırı iki ayrı ölçüt verir; demek ki ölçütü bir sınıf değil, equals’in baktığı alanlar ve karşılaştırılan değer çifti birlikte belirler. Kümelerde Coordinate iki ayrı öğe olarak kalır, ValueCoordinate tek öğeye iner.
TN14. Bu ölçüm iki alanlı küçük bir sınıf içindir. Daha çok alanı olan bir sınıfta hangi alanların equals’e girdiği her sınıfa özgüdür; aynı davranış ölçülmeden varsayılamaz.
Özet
- Birincil yapıcı sınıf başlığına yazılır; başlatıcılar ve
initblokları yazılış sırasıyla koşar, ikincil yapıcının gövdesi en sonda. - İkincil yapıcı
: this(...)ile birincil yapıcıya yönlenmek zorundadır ve yönlenilen yapıcı önce tamamlanır. - Özelliklere varsayılan değer, adlı argüman, hesaplanan okuma ve özel
setile davranış verilebilir. equalsyazılmamış bir sınıfın aynı değerli örnekleri, hangi yapıcıyla kurulurlarsa kurulsunlar, kimlik ölçütüne düşer.- Bu dersin ölçütü: kimlik. Sınırlayıcı ölçümde aynı sınıf kalıbı
equals’e göre içeriğe ve kısmi içeriğe de düştü.
Sonraki Adım
Bir sınıfı değer gibi davranacak hâle getirmek için equals, hashCode ve toString’i elle yazmak gerekti. Bir sonraki ders bu üçünü birincil yapıcıdaki özelliklerden üreten veri sınıflarını ölçer: copy ile kurulan ayrı nesnenin eşit sayılıp sayılmadığına ve yapısöküm bildiriminin ne yaptığına bakar.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.