Ders 10 / 23
Güvenli Çağrı ve Elvis İşleci
Güvenli çağrı zincirinin null gördüğünde nerede durduğunun, Elvis işlecinin sağ tarafını ne zaman değerlendirdiğinin ve null olmama iddiasının hangi istisnayı fırlattığının ölçümü.
Yayın Güncelleme
Bu derste
- Güvenli çağrı zincirinin ilk null değerde durduğunu ve argümanı değerlendirmediğini ölçer
- Elvis işlecinin sağ tarafının yalnızca sol taraf null olduğunda değerlendirildiğini gösterir
- Null olmama iddiasının null değerde NullPointerException fırlattığını yakalayarak gösterir
- Elvis işlecinin sağ tarafının ölçütü belirlediğini sınırlayıcı ölçümle doğrular
İçindekiler
Önceki ders null denetiminin bir if ile yapıldığını ve denetimden sonra tipin daraldığını gösterdi. Aynı işi birkaç değerin üst üste bindiği yerde yapmak, her adım için ayrı bir if yazmayı gerektirir. Kotlin bu işi iki işleçle kısaltır: ?. ile güvenli çağrı ve ?: ile Elvis işleci. Üçüncü bir yol da vardır: denetimi derleyiciden alıp programcının üstüne yüklemek. Bu ders üçünü de ölçer ve her birinin null gördüğünde nerede durduğuna bakar.
Ölçü ekseni önceki dersteki gibidir: 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. Önceki ders 2/2/3/1 dağılımının tekil kesitini ölçtü; bu ders içerik kesitine bakar.
// 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 }}")
}
}
Güvenli Çağrı
Güvenli çağrı, bir üyeye erişimden önce alıcının null olup olmadığına bakar. Alıcı doluysa çağrı yapılır ve sonuç döner; alıcı null ise çağrı hiç yapılmaz ve ifadenin değeri null olur. Sonuç tipi bu yüzden her zaman boş değer alabilen bir tiptir: String? üzerinde ?.length bir Int? verir.
Zincirlenen güvenli çağrılarda ilk null değer zinciri durdurur. user.address?.city?.uppercase() biçimindeki bir ifadede address null ise city’ye hiç bakılmaz; city null ise uppercase çağrılmaz. Aşağıdaki blok üç kullanıcıyla bunu ölçer: biri tam doldurulmuş, birinin adresi hiç yok, birinin adresi var ama şehri yok. Ardından ikinci bir soru sorar: çağrılmayan bir yöntemin argümanı değerlendirilir mi? trace fonksiyonu her çalıştığında bir satır basar, dolayısıyla çalışıp çalışmadığı çıktıdan okunur. Son iki satır let ile aynı soruyu sorar: let içindeki blok null alıcıda hiç koşmaz.
// Main.kt — guvenli cagri zinciri
class Address(val city: String?)
class User(val name: String?, val address: Address?)
fun trace(label: String): String {
println("degerlendirildi: $label")
return label
}
fun main() {
val users = listOf(User("ada", Address("izmir")), User("can", null), User("eda", Address(null)))
for (user in users) {
println(user.address?.city?.uppercase())
}
val missing: String? = null
println(missing?.plus(trace("arguman")))
val present: String? = "ab"
println(present?.plus(trace("arguman")))
missing?.let { println("let calisti") }
present?.let { println("let calisti: $it") }
}
IZMIR null null null degerlendirildi: arguman abarguman let calisti: ab
İlk üç satır zincirin davranışını verir. Tam doldurulmuş kullanıcı için sonuç IZMIR’dir. Adresi olmayan kullanıcı için null basılır; ikinci halkaya, yani şehre, hiç ulaşılmaz. Adresi olan ama şehri olmayan kullanıcı için de null basılır; üçüncü halka, yani uppercase, hiç çağrılmaz. Üç sonuç da aynı ifadeden çıkar, yalnızca zincirin nerede koptuğu değişir.
Dördüncü satır, alıcı null iken plus çağrısının argümanının değerlendirilmediğini gösterir: degerlendirildi: arguman satırı bu çağrıdan sonra gelmez, yalnızca alıcı dolu olan çağrıdan önce görünür. Argüman, çağrı yapılmayacaksa hesaplanmaz. Aynı şey let için geçerlidir: null alıcıda blok koşmaz, dolu alıcıda koşar ve it alıcının kendisidir.
TN6. Bu ölçüm yalnız uppercase, plus ve let için; başka yöntemlerin ve atama biçimindeki güvenli çağrının (bir özelliğe değer yazmanın) davranışı denenmedi. Argümanın değerlendirilmemesi yalnızca plus ile ölçüldü.
Elvis İşleci
?: bir değer üretir: sol taraf null değilse sol tarafın kendisi, null ise sağ taraf. Sağ taraf yalnızca sol taraf null olduğunda değerlendirilir, tıpkı güvenli çağrıdaki argüman gibi. Sol tarafın tipi String? ise, işlecin sonucu çoğu zaman boş değer alamayan bir tiptir; çünkü sağ taraf bir String verirse ifadenin hiçbir yolu null döndürmez.
Sağ taraf bir değer olmak zorunda değildir. return ve throw da birer ifadedir ve tipi bütün tiplerin alt tipi sayılır; bu yüzden name ?: return "isim-yok" ve name ?: throw ... geçerlidir. Birincisi fonksiyondan erken çıkar, ikincisi bir istisna fırlatır. Sol taraf doluysa ikisi de çalışmaz ve sonraki satırlar String tipli bir değerle devam eder.
Aşağıdaki blok dört şeyi ölçer. İlk iki satırda sol taraf dolu ve boşken sağ tarafın trace çağrısı koşar mı? Sonraki iki satırda describe fonksiyonu ?: return ile erken çıkar. Son satırda needName bir IllegalArgumentException fırlatır ve çağıran yer onu yakalar; basılan şey yalnızca istisnanın sınıf adıdır.
// Main.kt — Elvis
fun trace(label: String): String {
println("degerlendirildi: $label")
return label
}
fun describe(name: String?): String {
val clean = name ?: return "isim-yok"
return "isim=" + clean
}
fun needName(name: String?): String = name ?: throw IllegalArgumentException("bos")
fun main() {
val present: String? = "ada"
val missing: String? = null
println(present ?: trace("sag-taraf"))
println(missing ?: trace("sag-taraf"))
println(describe(present))
println(describe(missing))
try {
needName(missing)
} catch (e: IllegalArgumentException) {
println(e::class.simpleName)
}
}
ada degerlendirildi: sag-taraf sag-taraf isim=ada isim-yok IllegalArgumentException
Dolu sol tarafta trace hiç çalışmaz: ilk satır yalnızca ada basar. Boş sol tarafta trace çalışır, degerlendirildi: sag-taraf satırı basılır ve ifadenin değeri olan sag-taraf yazılır. describe(present) isim=ada, describe(missing) isim-yok verir; ikincisinde fonksiyon return ile çıktığı için "isim=" + clean satırına hiç gelinmez. Son satır, fırlatılan istisnanın sınıf adını verir.
TN7. return ve throw yalnız fonksiyon gövdesinde ve tek ifadeli gövdede ölçüldü; bir lambda içinde ?: return kullanımı ve etiketli return denenmedi. İstisnanın ileti metni basılmadı; yalnızca sınıf adı ölçüldü.
Null Olmama İddiası
Üçüncü yol, denetimi derleyiciden almak ve sorumluluğu programcıya vermektir. Null olmama iddiası, bir String? değerini, null olmadığını ileri sürerek String’e çevirir. Değer gerçekten doluysa sorun yoktur. Değer null ise derleyici o anı beklemeden hata vermez; hata çalışma anında bir NullPointerException olarak fırlar. İşleç, iki ünlem işaretinden oluşur ve ders metninde işaretin kendisi yazılmaz; iş adıyla anılır, kod bloğunda ise görünür.
Aşağıdaki blok iki çağrı yapar. Dolu değerde iddia geçer ve uzunluk basılır. Boş değerde iddia tutmaz; try bloğu istisnayı yakalar ve yalnızca sınıf adını basar. İstisnanın ileti metni ve yığıt izi çalışma ortamına bağlıdır, bu yüzden basılmaz.
// Main.kt — null olmama iddiasi
fun main() {
val present: String? = "ada"
val missing: String? = null
println(present!!.length)
try {
println(missing!!.length)
} catch (e: NullPointerException) {
println(e::class.simpleName)
}
}
3 NullPointerException
Dolu değerde 3, boş değerde NullPointerException basılır. Güvenli çağrı ile aradaki fark çıktıdan okunur: güvenli çağrı null için null döndürüp programı sürdürür, iddia ise aynı durumda programı bir istisnayla durdurur. İddia, hatayı programcının bildirdiği bir noktaya taşır; derleyicinin sunduğu koruma o satırda kalkar.
TN8. İstisna yalnız yerel bir değişken üzerinde ve doğrudan length çağrısında yakalandı; bir özellik ya da fonksiyon dönüş değeri üzerinde iddianın davranışı denenmedi. NullPointerException adının bu çıktıda görünmesi Kotlin 2.4.21’in JVM hedefine bağlı bir ölçümdür.
Aynı İfade İki Kez
Kursun ölçü ekseni için asıl soru şudur: aynı güvenli çağrı ifadesi iki kez koşturulursa sonuçlar nasıl bir ilişkide olur? Alıcı doluysa uppercase her çağrıda ayrı bir dizgi üretir; iki sonuç eşittir ama aynı nesne değildir. Alıcı null ise iki ifade de null verir ve null ile null aynı nesnedir. Aşağıdaki blok bu iki durumu ölçer. Dolu durumda alıcı buildString ile kurulur; böylece iki çağrı da derleme zamanında bilinen sabitlerle çalışmaz. Üçüncü değer ikizden tek bir harfle ayrılır: ADI, ADA değildir.
// Main.kt — ayni ifade iki kez
fun main() {
val name: String? = buildString { append("a"); append("da") }
val missing: String? = null
val checks = listOf(
Criterion("dolu", name?.uppercase(), name?.uppercase(), "ADI"),
Criterion("bos", missing?.uppercase(), missing?.uppercase(), "ADI"),
)
for (check in checks) check.report()
summarize(checks)
}
dolu: ayni-nesne=false, ikiz=true, farkli=false -> icerik bos: ayni-nesne=true, ikiz=true, farkli=false -> tekil tekil: 1 kimlik: 0 icerik: 1 kismi-icerik: 0
Dolu satırda ayni-nesne=false ve ikiz=true vardır; üçüncü değer ayrıldığı için farkli=false çıkar. Ölçüt içeriktir: iki ADA ayrı nesnelerdir ama == onları eşit bulur. Boş satırda ise aynı ifade null verir ve ölçüt tekildir. Güvenli çağrının kendisi bir ölçüt belirlemez; alıcının dolu ya da boş olması belirler.
TN9. Bu ölçüm uppercase için; aynı alıcıyla iki çağrının daima ayrı nesne üretmesi başka dizgi yöntemleri için denenmedi. Dolu satırdaki ayrı nesne sonucu === karşılaştırmasından okundu; çalışma ortamının benzer dizgileri paylaşıp paylaşmadığı ayrıca ölçülmedi.
Null İçeren Listelerde İşleçler
İşleçler tek değerle sınırlı değildir. null öğe taşıyan bir listede ?. ve ?: her öğeye ayrı ayrı uygulanır. Blok List<String?> üzerinde dönüştürme, süzme ve toplama fonksiyonlarını dener: map her öğe için bir sonuç üretir, mapNotNull ve filterNotNull ise null sonuçları atar. Son iki satır null değerin kendisini arar.
// Main.kt — null iceren listede islecler
fun main() {
val names: List<String?> = listOf("ab", null, "cde")
println(names.map { it?.length })
println(names.mapNotNull { it?.length })
println(names.filterNotNull().size)
println(names.sumOf { it?.length ?: 0 })
println(names.firstOrNull { it == null })
println(names.indexOf(null))
}
[2, null, 3] [2, 3] 2 5 null 1
map her öğe için bir sonuç üretir, bu yüzden null öğe ?.length ile null sonuca dönüşür ve liste [2, null, 3] olarak kalır. mapNotNull aynı dönüşümü yapıp null sonuçları atar, çıktı [2, 3] olur. filterNotNull yalnızca süzer, tipi List<String> yapar ve iki öğe kalır. sumOf bloğundaki ?: 0 ise yok olan uzunluğu sıfırla değiştirir, toplam 5 çıkar.
Son iki satır null değerin kendisini arar. firstOrNull koşulu sağlayan ilk öğeyi verir; null öğe bulunsa bile sonuç null olduğundan, bu çıktı aramanın başarısız olmasından ayırt edilemez. indexOf(null) ise konumu söyler ve 1 döner. Sık yapılan hata, firstOrNull sonucunun null olmasını “öğe yok” diye okumaktır. Liste null öğe taşıyabiliyorsa varlığı sormak için konum ya da any gibi bir fonksiyon seçilir. Genel kural basittir: null taşıyabilen bir koleksiyonda işleç öğe başına çalışır, koleksiyonun kendisi hakkında bir şey söylemez.
İşleç Öncelikleri
Elvis işleci toplama işlecinden daha düşük öncelikli olduğu için parantezsiz yazılmış bir ifade göründüğünden farklı çalışabilir. Blok aynı ifadeyi iki biçimde yazar: sağ tarafta toplama olan ve sol tarafı parantez içine alınmış. Bunu dolu ve boş bir değerle dener. Ardından ?. ile ?: birlikte kullanılır ve bir Int? üzerinde hem ?: hem ?.plus çalıştırılır.
// Main.kt — islec oncelikleri
fun main() {
val missing: String? = null
val present: String? = "x"
println(present ?: "a" + "b")
println((present ?: "a") + "b")
println(missing ?: "a" + "b")
println((missing ?: "a") + "b")
println(missing?.length ?: -1)
println(missing?.length.toString())
val count: Int? = null
println((count ?: 0) + 1)
println(count?.plus(1))
}
x xb ab ab -1 null 1 null
İlk satır işleç önceliğini gösterir. Elvis işleci toplama işlecinden düşük öncelikli olduğundan present ?: "a" + "b" ifadesi present ?: ("a" + "b") olarak okunur. Sol taraf dolu olduğu için sağ taraf hiç hesaplanmaz ve sonuç x çıkar. Parantezle (present ?: "a") + "b" yazıldığında ise xb elde edilir. Sol taraf null olduğunda iki biçim aynı sonucu, ab’yi verir, bu yüzden hata yalnızca dolu değerde ortaya çıkar ve testlerde gözden kaçabilir.
Sonraki satırlarda missing?.length ?: -1 -1 verir. Aşağıdaki satır daha sinsidir: missing?.length.toString() sonucu null basar, ama bu null bir dizgidir. Zincirin sonundaki toString çağrısı Int? alıcıda çalışır ve "null" dizgisini üretir; kısa devre yapmaz. Son iki satır Int? üzerinde aynı ayrımı gösterir: (count ?: 0) + 1 sayıyı 1 yapar, count?.plus(1) ise null bırakır. Yok olan değerden sayı üretmek isteniyorsa Elvis, yokluğu korumak isteniyorsa ?. seçilir.
Sınırlayıcı Ölçüm
Bu dersin kuralı, güvenli çağrının tek bir ölçüt vermediğidir. Elvis işleci için de aynısı geçerlidir, ama bu kez ölçütü işlecin kendisi değil sağ tarafı belirler. Sol taraf her üç gözlemde de null’dur ve ifade her seferinde sağ tarafı değerlendirir. Sağ taraf üç farklı biçimde yazılır: bir nesne bildirimi (ayrıntısı bu kursun ilerleyen bir dersindedir), equals’i yazılmamış bir sınıfın her seferinde kurulan örneği ve her seferinde buildString ile kurulan bir dizgi.
// Main.kt — sag tarafa gore
object Fallback
class Wrapper(val value: Int)
fun main() {
val nothing: String? = null
val checks = listOf(
Criterion("nesne", nothing ?: Fallback, nothing ?: Fallback, Wrapper(1)),
Criterion("sinif", nothing ?: Wrapper(0), nothing ?: Wrapper(0), Wrapper(1)),
Criterion("dizgi", nothing ?: buildString { append("bo"); append("s") },
nothing ?: buildString { append("bo"); append("s") }, "dolu"),
)
for (check in checks) check.report()
summarize(checks)
}
nesne: ayni-nesne=true, ikiz=true, farkli=false -> tekil sinif: ayni-nesne=false, ikiz=false, farkli=false -> kimlik dizgi: ayni-nesne=false, ikiz=true, farkli=false -> icerik tekil: 1 kimlik: 1 icerik: 1 kismi-icerik: 0
Aynı işleç, aynı null sol taraf, üç ölçüt çıktı. Nesne bildirimi her değerlendirmede aynı örneği verir, dolayısıyla ölçüt tekildir. Sıradan sınıf her değerlendirmede ayrı bir örnek kurar ve equals yazılmadığı için iki örnek eşit sayılmaz: ölçüt kimliktir. Dizgi her seferinde ayrı bir nesnedir ama String içeriğe göre eşitlendiği için ölçüt içeriktir. Üç satırın toplamı 1/1/1 dağılımını verir; işleç değişmedi, değişen sağ taraftır.
Bu, Elvis işlecini kullanırken beklenmedik bir sonuca yol açabilir. nothing ?: Wrapper(0) yazıldığında ifade her koşuda ayrı bir örnek üretir; iki koşunun sonucunu == ile karşılaştıran bir kod, Wrapper’ın equals’i yazılmadığı sürece hiçbir zaman eşit bulmaz. Eşitlik sağ tarafın tipinden gelir.
TN10. Sınırlayıcı ölçüm yalnız sol tarafı null olan üç gözlemle yapıldı; sol tarafın dolu olduğu durumda sağ tarafın değerlendirilmediği Elvis bölümündeki ölçümden alındı, burada tekrarlanmadı. Sınıfın equals’ini yazmanın sonucu bir sonraki dersin konusudur ve burada ölçülmedi.
Özet
- Güvenli çağrı alıcı
nullise çağrıyı yapmaz, argümanı değerlendirmez ve sonuç olaraknullverir; zincir ilknulldeğerde kopar. - Elvis işlecinin sağ tarafı yalnızca sol taraf
nullolduğunda değerlendirilir; sağ tarafreturnya dathrowolabilir. - Null olmama iddiası denetimi derleyiciden alır; boş değerde çalışma anında
NullPointerExceptionfırlar. - Aynı güvenli çağrı ifadesi dolu alıcıda ayrı ama eşit iki dizgi, boş alıcıda iki
nullverir: ilki içerik, ikincisi tekil. - Elvis işlecinin ölçütünü sağ taraf belirler: nesne bildirimi tekil,
equals’siz sınıf kimlik, dizgi içerik. - Bu dersin ölçütü: içerik. Aynı güvenli çağrı iki kez koşunca dolu alıcıda iki ayrı ama eşit dizgi çıkar.
Sonraki Adım
Elvis işlecinin sağ tarafında equals’i yazılmamış bir sınıf ölçütü kimliğe çevirdi. Bir sonraki ders bu sınıfların nasıl yazıldığını ve equals’in nereden geldiğini ölçer: birincil yapıcı, init bloğu, ikincil yapıcı ve özellikler. İki yoldan kurulan aynı değerli iki örneğin neden eşit sayılmadığı, orada ölçüte bakılarak açıklanır.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.