Ders 14 / 23
Nesne Bildirimleri
Nesne bildirimi, yardımcı nesne, `data object` ve nesne ifadesini iki değeri eşit yapan ölçütle ölçer; bildirimin tekil, ifadenin her değerlendirmede yeni nesne olduğunu gösterir.
Yayın Güncelleme
Bu derste
- Bir nesne bildirimini yazmak ve iki kez okunan değerin tek nesne olduğunu ölçmek
- Yardımcı nesneyi (`companion object`) ve onun fabrika fonksiyonunu birbirinden ayırmak
- `data object` bildiriminin `toString` çıktısını ve eşitliğini okumak
- Nesne bildirimi ile nesne ifadesinin aynı sözcükle yazıldığı hâlde farklı davrandığını açıklamak
İçindekiler
Önceki derste, “Mühürlü Tipler ve Numaralandırmalar” dersinde, numaralandırma girdisi ve data object değerlerinin tekil olduğunu gördük: aynı girdiyi iki kez okuyunca aynı nesne geliyordu. Bu ders o tekilliğin kaynağına iniyor. object sözcüğü Kotlin’de üç ayrı yerde karşınıza çıkar: bir bildirimde, bir sınıfın içinde yardımcı nesne olarak ve bir ifadenin başında. Üçü de aynı sözcükle yazılır; bu ders üçünün aynı şeyi yapıp yapmadığını ölçüyor.
Ölçü, bu kursun her dersinde aynı soru: bu iki değeri eşit yapan ne? Dört olası cevap var: aynı nesne olmaları (tekil), eşitliğin kimliğe dayanması (kimlik), içeriklerinin eşit olması (içerik) ve yalnız bazı içeriklerin eşit olması (kısmi içerik).
Nesne bildirimi (object declaration), bir tipi tanımlar ve o tipin tek örneğini aynı anda kurar. Bildirimin adı hem tipin hem de o tek değerin adıdır. Yapıcı çağrılmaz; adı yazmak değerin kendisini okumaktır.
Ölçüm Düzeneği
Criterion her derste aynı biçimde ilk blokta durur. Her gözlem üç değer alı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: first === twin ise tekil; değilse ve first == twin yanlışsa kimlik; first == other yanlışsa icerik; hiçbiri değilse kismi-icerik.
// 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 }}")
}
}
TN21. Bir nesne bildiriminde twin ayrıca kurulamaz, çünkü ikinci bir örnek yoktur; bu derste twin olarak aynı bildirimin ikinci kez okunan değerini verdik. other olarak aynı gövdeli ama ayrı adlı bir bildirim kullandık.
Bildirim Tek Nesnedir
Aşağıdaki blok iki nesne bildirimi yazar. IdSource bir sayaç tutar; OtherSource aynı gövdeye sahip ayrı bir bildirimdir. IdSource iki ayrı değişkene okunur, sayaç da bu iki değişken ve doğrudan adı üzerinden ilerletilir.
// Main.kt — nesne bildirimi iki kez okunur
object IdSource {
private var next = 1
fun take(): Int = next++
}
object OtherSource {
private var next = 1
fun take(): Int = next++
}
fun main() {
val a = IdSource
val b = IdSource
println(a.take())
println(b.take())
println(IdSource.take())
Criterion("nesne-bildirimi", a, b, OtherSource).report()
}
1 2 3 nesne-bildirimi: ayni-nesne=true, ikiz=true, farkli=false -> tekil
Üç take() çağrısı, hangi değişken üzerinden yapılırsa yapılsın aynı sayacı ilerletti, çünkü a ve b aynı nesneyi gösteriyor. Kural da bunu tekil olarak yazdı: === doğru çıktığı için ikizin eşitliğine bakılmadı bile. OtherSource’un sayacı ayrı olduğu için o, ilk nesneye eşit değil.
Bildirim bir yapıcı çağrısı olmadığından, adın sonuna parantez koymak da çalışmaz. Önce bunu doğruladık: aşağıdaki parça Kotlin 2.4.21’de derlenmedi, bu yüzden çıktısız kalıyor.
object IdSource
fun main() {
val source = IdSource()
}
TN22. Reddin hata metnini derse yazmıyoruz; yalnız parçanın derlenmediği doğrulandı. Bildirimin başka bir sınıftan kalıtım alıp alamayacağı ya da bir arayüz gerçekleştirip gerçekleştirmeyeceği bu derste ölçülmedi.
Yardımcı Nesne ve Fabrika
Yardımcı nesne (companion object), bir sınıfın içinde companion object diye yazılan nesne bildirimidir. Sınıf adıyla çağrılabilen üyeleri tutar. Bildirim olduğu için tekildir. Ama içine yazılan bir fabrika fonksiyonu, sınıfın örneklerini kurar ve o örnekler tekil değildir. İkisini yan yana ölçüyoruz.
// Main.kt — yardimci nesne tekildir, fabrika her seferinde yeni nesne uretir
class Account private constructor(val owner: String) {
companion object {
fun open(owner: String): Account = Account(owner)
}
}
fun main() {
val checks = listOf(
Criterion("yardimci-nesne", Account.Companion, Account.Companion, Any()),
Criterion("fabrika-sonucu", Account.open("ada"), Account.open("ada"), Account.open("can")),
)
for (check in checks) check.report()
summarize(checks)
}
yardimci-nesne: ayni-nesne=true, ikiz=true, farkli=false -> tekil fabrika-sonucu: ayni-nesne=false, ikiz=false, farkli=false -> kimlik tekil: 1 kimlik: 1 icerik: 0 kismi-icerik: 0
yardimci-nesne satırı tekil, fabrika-sonucu satırı kimlik çıktı. Aynı sınıfın içinde yazılan iki object karşılığı, iki ayrı ölçüt verdi: yardımcı nesnenin kendisi tek, onun ürettiği hesaplar her çağrıda ayrı. İki Account’un sahibi aynı olsa bile == doğru dönmedi, çünkü Account sıradan bir sınıftır ve equals’ü yeniden tanımlamaz; eşitlik kimliğe düşer. Bu, kural dilinde kimlik demektir. Yapıcıyı özel yapmak (private constructor) örnek kurma yolunu yardımcı nesnenin fonksiyonuna daralttı; fabrika böylece tek giriş kapısı oldu.
TN23. Fabrikanın her çağrıda yeni bir Account kurduğu burada yalnız bu sınıf için ölçüldü. Bir fabrikanın önceden kurulmuş bir örneği döndürmesi de mümkündür; o durumda ölçüt tekil olurdu, ama bu derste öyle bir fabrika yazmadık.
data object
data object, bir nesne bildirimine veri sınıfının okunabilir toString’ini ve equals/hashCode’unu ekler. Düz bir object’in varsayılan toString çıktısını bu kursta basmıyoruz; data object ise yalnız adını yazar. Mühürlü hiyerarşilerde durum nesnelerine data object yazılmasının nedeni budur: çıktıda okunur bir ad görünür.
// Main.kt — data object okunabilir bir toString tasir
data object Accepted
fun main() {
println(Accepted)
println(Accepted == Accepted)
val checks = listOf(Criterion("data-object", Accepted, Accepted, "Accepted"))
for (check in checks) check.report()
}
Accepted true data-object: ayni-nesne=true, ikiz=true, farkli=false -> tekil
Çıktının ilk satırı bildirimin adından başka bir şey içermiyor. Ölçüt yine tekil: data object yeni bir örnek üretmez, yalnız mevcut tekil nesneye okunabilirlik ekler. other olarak verdiğimiz "Accepted" dizgisi ise Accepted’e eşit değil; ikisinin yazılışı aynı olsa da tipleri ayrı.
İlk Erişimde Kurulum ve Arayüz
Bir nesne bildiriminin tek örneği ne zaman kurulur? Program başlarken mi, yoksa ilk kullanıldığı anda mı? Bu sorunun cevabı, bildirimin gövdesindeki init bloğunda görünür: blok her ne zaman koşarsa örnek o zaman kurulmuştur. Bildirim ayrıca bir arayüz gerçekleştirebilir, yani sıradan bir sınıf gibi davranışı sözleşmeye bağlayabilir. Son olarak bildirim, when ifadesinde bir sabit gibi eşleştirilebilir.
Aşağıdaki blokta Polite, Greeter arayüzünü gerçekleştiren ve kurulduğunda bir satır basan bir nesne bildirimidir. Idle ise bir data object’tir. describe fonksiyonu bir değeri üç yoldan eşleştirir: Idle ile eşitlik, is Polite ile tip denetimi ve geri kalan her şey. main ilk satırda basladi yazdırır; böylece init satırının nerede göründüğü, bildirimin hangi anda kurulduğunu gösterir.
// Main.kt — nesne ilk erisimde kurulur, arayuz gerceklestirir, when ile eslesir
interface Greeter {
fun greet(name: String): String
}
object Polite : Greeter {
init {
println("polite kuruldu")
}
override fun greet(name: String): String = "merhaba " + name
}
data object Idle
fun describe(state: Any): String = when (state) {
Idle -> "bos"
is Polite -> "nazik"
else -> "baska"
}
fun main() {
println("basladi")
val greeter: Greeter = Polite
println(greeter.greet("ada"))
println(Polite.greet("can"))
println(describe(Idle))
println(describe(Polite))
println(describe(Any()))
Criterion("arayuz-nesnesi", greeter, Polite, Idle).report()
}
basladi polite kuruldu merhaba ada merhaba can bos nazik baska arayuz-nesnesi: ayni-nesne=true, ikiz=true, farkli=false -> tekil
polite kuruldu satırı basladi’dan sonra ve ilk greet çağrısından önce göründü. Polite programın başında kurulmadı: bildirim, adına ilk erişildiği anda kuruldu ve init bloğu o sırada koştu. İkinci erişim yeni bir satır basmadı, çünkü örnek zaten vardı. greeter ve Polite aynı nesneyi gösterdi, ölçüt tekil çıktı; greeter değişkeninin tipi Greeter olsa da altındaki nesne değişmedi.
describe üç ayrı sonuç verdi. Idle için bos, Polite için nazik, sıradan bir Any() için baska yazıldı. Bir nesne bildirimi tekil olduğu için when içinde sabit gibi eşleşir: Idle -> satırı Idle == state karşılaştırmasına iner. is Polite da çalıştı, çünkü bildirim hem bir değer hem bir tip tanımlar; tek örneği olan bir tip için ikisi aynı sonucu verir.
Bu ölçümden çıkan pratik sonuç iki yanlıdır. Birincisi, init bloğunda yan etki bırakmak, yani bir satır basmak ya da bir kaynağı açmak, bildirimin kurulma anını programın akışına bağlar; hangi satırın önce koşacağı ilk erişimin yerine bağlıdır. İkincisi, bir nesne bildirimi arayüz gerçekleştirdiğinde, o arayüz tipli her değişken yine aynı tek örneği taşır; bu yüzden ölçüt tipe göre farklılaşmaz, nesnenin kendisine bağlı kalır.
TN24. Nesne bildirimine ilk erişimin birden çok iş parçacığından aynı anda yapılması bu derste ölçülmedi; yalnız tek akışta kurulum sırası gözlendi.
Ölçümlerden Çıkan Okuma Kuralı
Bu noktaya kadar ölçtüğümüz satırlar ve her satır aynı soruya cevap verdi: iki okuma aynı nesneyi mi veriyor? Satırları yan yana koyunca bir okuma kuralı çıkıyor. Bildirim, adı yazıldığında bir değer üretir; Stamp, Polite, Accepted ve Idle bu yüzden tekil çıktı. İfade ise her değerlendirildiğinde yeni bir nesne kurar; makeMarker() bu yüzden kimlik çıktı. Sınıfın fabrikası bir fonksiyondur, bildirim değildir; Account.open her çağrıda yeni bir Account kurdu ve sonuç yine kimlik oldu. Kural, sözcüğün yazıldığı yere bakar: ad tek bir değere gidiyorsa tekil, çağrı ya da ifade yeni değer kuruyorsa kimlik.
Sık yapılan hata bu kuralı tersine çevirmektir. Bir object sözcüğü gören okuyucu “bu tekildir” der; oysa object : Tip { ... } biçimi bir ifadedir. Tersi de yanlıştır: bir sınıf içinde companion object yazıldığında yardımcı nesnenin ürettikleri tekil sanılır. Ölçüm, yardımcı nesnenin kendisinin tek, onun fabrikasının ürettiklerinin ayrı olduğunu gösterdi. Bu yüzden bir tekil nesneye bağlanan durum, örneğin bir sayaç ya da bir önbellek, programın bütün parçalarınca paylaşılır; bir fabrikanın ürettiği nesneye bağlanan durum ise yalnız o nesnenin sahibine aittir.
Kurulum anı ikinci bir ayrıntıdır. polite kuruldu satırı ilk erişimde göründü ve ikinci erişimde görünmedi. Bir tekil nesnenin kuruluş maliyeti ya da yan etkisi, programın başında değil, ilk kullanıldığı yerde ödenir. Bu, init bloğuna yazdığınız şeyin ne zaman koşacağını sıralamak gerektiğinde işe yarar: sıralama, hangi koddan ilk erişildiğine bağlıdır ve o kod yer değiştirirse satır da yer değiştirir. Bu yüzden init bloğunu kısa tutmak, yalnız alan kurmak ve çıktı üretmemek akıllıca olur; bu derste yalnız ölçümü görünür kılmak için çıktı bastırdık.
Üçüncü ayrıntı when ifadesidir. Idle ile eşleştirme ve is Polite ile tip denetimi aynı işi yapar, çünkü bildirimin tek örneği vardır. Tek örneği olan bir tipte “bu değer o değere eşit mi” ile “bu değer o tipte mi” soruları aynı cevabı verir. İki örnek mümkün olan bir sınıfta bu iki soru ayrılırdı; bu ayrılmanın olmaması tekilliğin gözlenebilir bir sonucudur.
Ölçmeden Önce Tahmin Etmek
Bir satırı koşturmadan önce ölçütü tahmin etmek, kuralı gerçekten anlayıp anlamadığınızı sınar. Criterion düzeneği üç soru sorar: aynı nesne verildiğinde === doğru mu, ikiz verildiğinde == doğru mu, farklı nesne verildiğinde == doğru mu? Nesne bildiriminde ilk soru her zaman doğru cevaplanır, çünkü ad tek bir değere gider; bu yüzden ikizin de aynı nesne olması gerekir ve üçüncü soru yalnızca farklı bir tip verildiğinde yanlış çıkar. Ölçüt tekil olarak yazılır.
Nesne ifadesinde ilk soru bile yanlış cevaplanır. Aynı fonksiyonu iki kez çağırmak iki ayrı nesne verir ve ikizler birbirine eşit sayılmaz; ikisi de false olunca ölçüt kimlik olur. Bu dersteki her satırı bu iki yoldan birine koyabilirsiniz: ad bir bildirimi gösteriyorsa tekil, bir çağrıyı ya da ifadeyi gösteriyorsa kimlik. data object satırı bile bu kuralın dışına çıkmadı; data önekinin eklediği şey eşitlik değil, okunabilir bir yazı oldu.
Tahminin tutmadığı yer, bir kuralı eksik hatırladığınız yerdir. Örneğin describe fonksiyonunda Idle için bos yazacağını bildiyseniz ama Polite için nazik yerine baska bekliyorsanız, is denetiminin bildirim adıyla çalışmadığını düşünmüşsünüz demektir. Ölçüm bu varsayımı düzeltti: bildirim hem bir değer hem bir tiptir.
Sınırlayıcı Ölçüm
Nesne ifadesi (object expression), object sözcüğünün bir ifadenin başında kullanılmasıdır: object { ... } ya da bir tip adından sonra object : Tip { ... }. Ad taşımaz ve ifade her değerlendirildiğinde yeni bir nesne kurar. Bildirimle ifade arasındaki fark tam burada: aynı sözcük, bildirimde tekil bir nesne, ifadede her seferinde yeni bir nesne üretir. Bu yüzden bu kursun ölçüsü onları ayırır.
// Main.kt — nesne ifadesi her degerlendirmede yeni nesne kurar
object Stamp
fun makeMarker(): Any = object {
val side = 4
}
fun main() {
val checks = listOf(
Criterion("bildirim", Stamp, Stamp, makeMarker()),
Criterion("ifade", makeMarker(), makeMarker(), makeMarker()),
)
for (check in checks) check.report()
summarize(checks)
}
bildirim: ayni-nesne=true, ikiz=true, farkli=false -> tekil ifade: ayni-nesne=false, ikiz=false, farkli=false -> kimlik tekil: 1 kimlik: 1 icerik: 0 kismi-icerik: 0
bildirim satırı tekil, ifade satırı kimlik çıktı. İfadenin bir adı, dolayısıyla bir yapıcısı olmadığı hâlde, her makeMarker() çağrısı yeni bir nesne kurdu ve bu nesneler equals’ü yeniden tanımlamadığı için birbirine eşit çıkmadı. Bu ölçüm bir sınırdır: object sözcüğünü gördüğünüzde tekil olduğunu varsaymak yetmez; sözcüğün bildirimde mi yoksa ifadede mi durduğuna bakmak gerekir.
TN25. Nesne ifadesinin yakaladığı bir değişkenin nasıl göründüğü ve ifadenin bir tipi genişletmesi bu derste ölçülmedi; yalnız adsız, tipsiz bir ifade kullanıldı.
Özet
- Nesne bildirimi bir tipi ve o tipin tek örneğini birlikte kurar; adı yazmak değeri okumaktır ve iki okuma aynı nesneyi verir.
- Yardımcı nesne bir sınıfın içindeki nesne bildirimidir; tekildir, ama onun fabrikası sınıfın örneklerini her çağrıda ayrı kurabilir.
data object, tekil nesneye yalnız okunabilir birtoStringve eşitlik ekler; ölçütü değiştirmez.- Nesne ifadesi her değerlendirmede yeni bir nesne kurar; aynı
objectsözcüğü bildirimde tekil, ifadede kimliktir. - Bu dersin ölçütü: tekil. İki okuma aynı nesneyi verdiğinde
===doğru çıkar ve eşitliğin gerekçesi aranmaz.
Sonraki Adım
Bu derste bir nesnenin tek mi, yoksa her seferinde yeni mi kurulduğuna baktık ve davranışı yalnız sınıfın kendi gövdesinde tuttuk. Sıradaki ders davranışı bir tipten ayırıyor: arayüzler, bir sözleşmeyi yazar ve bir sınıf onu başka bir nesneye devredebilir. Devretmenin eşitliği de devredip devretmediği ölçülecek.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.