Ders 09 / 23
Null Güvenliği
Boş değer alabilen ve alamayan tiplerin ayrı tipler olduğunun, null denetiminden sonra tipin daraldığının ve null ile null eşitliğinin hangi ölçütü verdiğinin ölçümü.
Yayın Güncelleme
Bu derste
- String ile String? arasındaki ayrımı derleyici reddi üzerinden gösterir
- Null denetiminden sonra akıllı tip dönüşümünün tipi daralttığını ölçer
- İki null değerin aynı nesne sayıldığını ve ölçütün tekil çıktığını gösterir
- Ölçütün ilk soruda durduğunu, sonraki sorulara bakılmadığını sınırlayıcı ölçümle doğrular
İçindekiler
Önceki ders kapsam fonksiyonlarının hangi değeri döndürdüğünü ölçtü. Döndürülen şey bazen hiçbir şeydir: bir arama sonuç bulamaz, bir alan doldurulmamıştır, bir dönüşüm tamamlanamaz. Bu durum bir değerle, örneğin null ile ifade edilebilir; o zaman o değerin her kullanımdan önce denetlenmesi gerekir ve denetim unutulduğunda hata çalışma anında ortaya çıkar.
Kotlin yokluğu tipin kendisine yazar. String her zaman bir dizgidir; String? ise ya bir dizgidir ya da null. Bu iki tip aynı tip değildir ve derleyici birini ötekinin yerine kullandırmaz. Buna null güvenliği denir: boş değer alabilen tip ile boş değer alamayan tip ayrı tiplerdir, dolayısıyla null olasılığı olan her yer kaynak kodda görünür. Bu ders o ayrımı derleyicinin vereceği red üzerinden ölçer, sonra aynı ayrımın eşitlik sorusuna ne söylediğine bakar.
Kursun ölçü ekseni, bir değerin kendisine eşit sayılan başka bir değerle ilişkisidir. Bir ölçüt dört biçimden birini alır: tekil, kimlik, içerik ve kısmi içerik. Kural sırayla işler. İlk değer ile ikizi aynı nesneyse ölçüt tekildir. Değilse ve ikiz eşit değilse kimliktir: yalnızca aynı nesne eşit sayılır. İkiz eşit ama ilkinden bir bakımdan ayrılan üçüncü değer eşit değilse içeriktir: nesneler ayrı, eşitlik içerikten gelir. Üçüncü değer de eşitse kısmi içeriktir: ayrılan bakım eşitliği etkilemez. Açılış dersindeki bağlayıcı ölçümde sekiz değerin dağılımı 2/2/3/1 idi; bu ders yalnız kendi kesitini ölçer.
Sınıflandırmayı yapan Criterion sınıfı aşağıda olduğu gibi verilir. Kod blokları yalnızca sandbox içinde derlenip koşturulur; çıktı bloklarındaki her satır sandbox’ın bastığıdır.
// 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 }}")
}
}
Parametreler Any? tipindedir; yani null da bir argüman olabilir. Bu dersin ilk sorusu tam olarak budur: null bir değer olarak ele alındığında hangi ölçütü verir?
İki Ayrı Tip
Aşağıdaki blokta aynı içeriğe sahip bir String ve bir String? yan yana durur. Karşılaştırma derlenir, çünkü == iki tarafın tipini birbirine denk getirmeyi gerektirmez; yalnız içeriğe bakar. Ama uzunluk gibi tipe bağlı bir işlem yalnız boş değer alamayan tipte sorunsuzdur.
// Main.kt — iki tip
fun main() {
val a: String = "kutu"
val b: String? = "kutu"
println(a == b)
println(a.length)
val c: String? = null
println(c == null)
println(c)
}
true 4 true null
a == b doğrudur: iki dizgi aynı içeriği taşır ve String? tipinin dolu hâli bir dizgi olarak karşılaştırılır. c ise null taşır ve ekrana null yazılır; boş değer alabilen bir değişkenin yazdırılması bir hata değildir. Dikkat edilecek nokta, a.length satırında a’nın boş değer alamayan bir tip olmasıdır. Aynı satırı b ile yazmak derleyicinin reddine yol açar.
İki red vardır ve ikisi de sandbox’ta ölçüldü. Birincisi String tipli bir değişkene null atamaktır. Derleyici bunu Kotlin 2.4.21’de reddeder: null boş değer alamayan bir tipin değeri olamaz. Hata derleme sırasında çıkar, program hiç koşmaz.
fun main() {
val text: String = null
println(text)
}
İkincisi, String? tipli bir değişkende doğrudan length istemektir. Derleyici bunu da reddeder: alıcı boş değer alabilen bir tip olduğunda yalnızca güvenli çağrıya ya da null olmama iddiasına izin verilir. Güvenli çağrı bir sonraki dersin konusudur; null olmama iddiası da orada, hatasıyla birlikte ölçülür.
fun main() {
val text: String? = "kutu"
println(text.length)
}
TN1. Bu iki red yalnız yerel değişken için ölçüldü; sınıf özelliği, fonksiyon parametresi ya da dönüş tipi için ayrıca denenmedi. Hata metni derleyici sürümüne bağlıdır ve burada aktarılmaz; ölçülen şey, programın hiç koşmamasıdır.
Null Denetiminden Sonra Tip Daralır
Bir String? değerinin uzunluğunu almak için önce null olmadığından emin olmak gerekir. Kotlin bunun için ayrı bir dönüştürme yazdırmaz. Bir if içinde null denetimi yapıldığında, o dalın içinde değişkenin tipi derleyici tarafından String’e daraltılır. Bu davranışa akıllı tip dönüşümü (smart cast) denir: programcı bir dönüştürme yazmaz, denetimin kendisi tipi değiştirir.
Aşağıdaki blokta lengthOf fonksiyonu String? alır; if (text != null) dalının içinde text.length doğrudan çağrılır. Blok ayrıca içinde iki null bulunan bir listeyi filterNotNull ile süzer; sonuç List<String?> değil List<String> olur ve elemanlar dönüştürme yapılmadan length verir.
// Main.kt — akilli tip donusumu
fun lengthOf(text: String?): Int {
if (text != null) {
return text.length
}
return -1
}
fun main() {
println(lengthOf("kutu"))
println(lengthOf(null))
val values = listOf("a", null, "bb", null)
println(values.size)
val present = values.filterNotNull()
println(present.size)
println(present.sumOf { it.length })
}
4 -1 4 2 3
lengthOf("kutu") dört, lengthOf(null) ise -1 verir. Liste dört elemanlıdır ama filterNotNull iki null’u atar ve geriye iki dizgi kalır; uzunluklarının toplamı üçtür. Burada ölçülen şey, null denetiminin yalnızca bir koşul olmadığıdır: denetimden sonra aynı değişken başka bir tiptir.
TN2. Akıllı tip dönüşümü yalnız if ve filterNotNull üzerinden ölçüldü; when içinde denetim, && ile zincirlenen denetimler ve değiştirilebilir değişkenler için daralmanın ne zaman bozulduğu denenmedi. Daralma, denetimle kullanım arasında değişkenin değişmeyeceği bilindiği sürece geçerlidir.
Soru İşareti Nereye Yazılır
? işareti bir tipin yalnızca bir yerine yazılabilir ve yeri anlamı değiştirir. List<String?> elemanları boş olabilen bir listedir: liste vardır, içindeki bazı yerler null’dur. List<String>? ise listenin kendisi boş olabilen bir tiptir: liste ya vardır ya da yoktur, ama varsa hiçbir elemanı null değildir. İkisi birbirinden bağımsızdır ve ikisi aynı anda da yazılabilir.
Aşağıdaki blok iki durumu yan yana koyar. Elemanları boş olabilen listenin boyutu doğrudan okunur, çünkü liste kendisi boş olamaz. Listenin kendisi boş olabilen tipte ise önce null denetimi gerekir; denetimden sonra aynı değişken List<String> olarak davranır ve size doğrudan çağrılır.
// Main.kt — soru isareti nereye yazilir
fun main() {
val items: List<String?> = listOf("a", null)
val maybeList: List<String>? = null
println(items.size)
println(maybeList == null)
if (maybeList != null) {
println(maybeList.size)
} else {
println("liste-yok")
}
}
2 true liste-yok
TN3. Bu ölçüm boş bir listeyi değil, hiç bulunmayan bir listeyi (null) ele alır; boş ama var olan bir listenin davranışı denenmedi. İki ? işaretinin birlikte yazıldığı List<String?>? biçimi de ölçülmedi.
null ile null
Kursun ölçü ekseninde asıl soru şudur: iki null aynı nesne midir? Kotlin’de null bir sınıfın örneği değil, tek bir değerdir. Her null ötekiyle aynıdır; birini diğerinden ayıracak bir kimlik yoktur. Dolayısıyla kimlik eşitliği (===) ve yapısal eşitlik (==) iki null için aynı yanıtı verir. Kural ilk soruda durur: ilk değer ile ikizi aynı nesneyse ölçüt tekildir ve geri kalan sorulara bakılmaz.
Aşağıdaki blokta dört gözlem vardır. İlk ikisinde ilk değer ile ikizi null’dur ve üçüncü değer birinde dolu, ötekinde yine null’dur. Üçüncü ve dördüncü gözlemde dolu dizgiler kullanılır: iki dizgi buildString ile ayrı ayrı kurulur, böylece aynı içeriği taşıyan iki ayrı nesne elde edilir.
// Main.kt — null ile null
fun main() {
val nullFirst: String? = null
val nullTwin: String? = null
val full1 = buildString { append("ku"); append("tu") }
val full2 = buildString { append("ku"); append("tu") }
val checks = listOf(
Criterion("null-null", nullFirst, nullTwin, "kutu"),
Criterion("null-null-null", nullFirst, nullTwin, null),
Criterion("dolu-dolu", full1, full2, "baska"),
Criterion("dolu-null", full1, nullTwin, "baska"),
)
for (check in checks) check.report()
summarize(checks)
}
null-null: ayni-nesne=true, ikiz=true, farkli=false -> tekil null-null-null: ayni-nesne=true, ikiz=true, farkli=true -> tekil dolu-dolu: ayni-nesne=false, ikiz=true, farkli=false -> icerik dolu-null: ayni-nesne=false, ikiz=false, farkli=false -> kimlik tekil: 2 kimlik: 1 icerik: 1 kismi-icerik: 0
Sonuç dört satırda okunur. null ile null tekildir. İki dolu dizgi, aynı içerikle ama ayrı nesneler olarak içerik ölçütünü verir: ayni-nesne=false, ikiz=true, farkli=false. Dolu bir dizgi ile null ise kimliktir: ikiz eşit değildir. Dolu dizgilerin ölçütü, bu derste yeniden tanımlanan hiçbir tipin değil, String’in kendi equals’inin sonucudur.
Burada bir tuzak vardır: iki null’un ölçütü tekil çıktı diye null ile karşılaştırılan her şeyin tekil olduğu sonucuna varılamaz. Tekil olan yalnızca null ile null’dur. Dolu bir değer null’a karşı kimliktir, çünkü ikiz eşit değildir.
TN4. Bu ölçüm yalnız String? tipli null değerleri için; başka tipli iki null’un aynı nesne sayılması denenmedi. Dolu dizgilerin ayrı nesne olduğu, buildString ile kurulduklarında === sonucundan okunmuştur; sabit dizgilerin aynı nesne olarak paylaşılıp paylaşılmadığı denenmedi.
Null Olabilen Değer Üzerinde Hazır Fonksiyonlar
null denetimi her zaman bir if ile yazılmaz. Standart kitaplık, alıcısı String? olan birkaç fonksiyonu hazır sunar: toString, isNullOrEmpty ve orEmpty. Bu fonksiyonlar null alıcıda istisna fırlatmaz, çünkü tanımları alıcının null olabileceğini baştan hesaba katar. Aşağıdaki blok üç değerle (null, boş dizgi ve dolu dizgi) bu fonksiyonları dener. Bir de dizgiyle null toplandığında ne çıktığına bakar.
// Main.kt — null olabilen alici uzerinde hazir islevler
fun main() {
val missing: String? = null
val empty: String? = ""
val present: String? = "ab"
println(missing.toString())
println("[" + missing + "]")
println(missing.isNullOrEmpty())
println(empty.isNullOrEmpty())
println(present.isNullOrEmpty())
println(missing == null)
println(missing.orEmpty().length)
}
null [null] true true false true 0
Çıktıdan üç şey okunur. Birincisi, missing.toString() istisna fırlatmaz; null alıcıda "null" dizgisini verir. İlk satırdaki null bu yüzden bir dizgidir, null değer değildir. İkincisi, dizgiyle null toplamak da çalışır ve sonuç [null] olur: String.plus argüman olarak Any? aldığı için derleyici itiraz etmez; boş olabilen taraf sağdaki argümandır. Üçüncüsü, isNullOrEmpty hem null hem boş dizgi için true döner, dolu dizgi için false; orEmpty ise null’ı boş dizgiye çevirdiği için uzunluk 0 çıkar.
Bu fonksiyonların ortak yanı alıcı tipinin String? olmasıdır. Sıradan bir String yöntemi bu tipte doğrudan çağrılamaz, çünkü alıcı yok olabilir. Sık yapılan hata, toString sonucunu null değerle karıştırmaktır. Sonucun tipi her zaman String’dir; bir değerin yokluğunu "null" dizgisini arayarak ölçmek yanlıştır. Yokluk == null ile sorulur. Dizgiyi günlüğe yazan kodda iki durumun aynı görünmesi, hatanın geç fark edilmesinin başlıca nedenidir.
Tip Dönüşümü ve Null
Tip dönüşümü de null ile karşılaşır. as işleci dönüşümü zorlar, as? ise başarısız olunca null verir, is ise yalnızca soru sorar. Blok, değeri null olan bir Any? üzerinde üçünü dener; dönüşüm başarısız olursa fırlayan istisnanın yalnızca sınıf adı basılır. Son satırda dolu ama yanlış tipteki bir değer as? ile dönüştürülür.
// Main.kt — as ve as? null uzerinde
fun main() {
val value: Any? = null
println(value as? String)
println(value is String)
println(value is String?)
try {
val text = value as String
println(text.length)
} catch (e: Exception) {
println(e::class.simpleName)
}
val other: Any? = 5
println(other as? String)
}
null false true NullPointerException null
Üç ifade üç ayrı tavır sergiler. as? başarısız olduğunda istisna fırlatmaz, null verir; hem null değer hem de yanlış tipteki değer için sonuç null’dır. is String null için false döner, ama is String? true döner, çünkü null değeri String? tipinin bir üyesidir. Zorlayıcı as String ise null değerde NullPointerException fırlatır; blok bunu yakalayıp yalnızca sınıf adını basar.
Buradaki ders, iki null çıktısının aynı anlama gelmediğidir. İlk satırdaki null, dönüştürülecek değerin zaten yok olmasından gelir. Son satırdaki null ise değerin var olup tipinin uymamasından gelir ve yalnızca çıktıya bakarak bu ikisini ayırt etmek mümkün değildir. Ayrım gerekiyorsa dönüşümden önce value == null ayrıca sorulur. Dönüşümün zorlayıcı biçimi yalnızca tipin kesin bilindiği yerde yazılır; yanlış varsayım çalışma anında istisna olarak geri döner, derleme anında değil.
Sınırlayıcı Ölçüm
Bu dersin kuralı şudur: ölçüt, ilk sorunun yanıtıdır. Kural dört soruyu sırayla sorar ve ilk “evet”te durur. Önceki bölümdeki ilk iki satır bunu zaten gösterdi, ama farkı açıkça görmek gerekir: null-null satırında üçüncü değer dolu ("kutu"), null-null-null satırında üçüncü değer null. Üçüncü değerin eşitliği (farkli) iki satırda zıt çıktı, ölçüt ise ikisinde de tekildir. farkli=false ile farkli=true arasında ölçütü değiştiren hiçbir şey yoktur; çünkü Criterion ilk soruda durur ve üçüncü değer ölçüte hiçbir bilgi katmaz.
Bu, sınırın nerede olduğunu gösterir. farkli sütunu basılır, çünkü Criterion üçüncü karşılaştırmayı her durumda hesaplar; ama tekil bir ölçütte o sütun okunmaz. Bir değerin tekil olduğunu söylemek, ona ne kadar çok değerin eşit olduğunu söylemez. Yalnızca ikizin aynı nesne olduğunu söyler.
Aynı sınır tersinden de okunur. İlk değer null, ikizi dolu bir dizgi olsaydı ayni-nesne=false ve ikiz=false çıkardı: ölçüt kimlik olurdu. Yani null ile başlamak ölçütü tekil yapmaz; tekil olmak için ikizin de null olması gerekir. Ölçütü belirleyen, ilk değerin tipi değil, ilk değer ile ikizinin aynı nesne olup olmadığıdır.
TN5. Sınırlayıcı ölçüm iki null ve bir dolu ya da null üçüncü değerle yapıldı; ilk değerin dolu, ikizin null olduğu sıralamanın ayrı bir çıktısı alınmadı, yalnızca dolu-null satırındaki kimlik sonucundan çıkarıldı. Criterion’ın kuralı başka sıralarla denenmedi.
Özet
StringileString?ayrı tiplerdir:String’enullatamak veString?üzerinde doğrudanlengthistemek derlenmez.- Bir
ifiçindeki null denetimi, o dalda değişkenin tipiniString’e daraltır; bu akıllı tip dönüşümüdür ve dönüştürme yazılmaz. filterNotNullbirList<String?>’iList<String>’e çevirir venullelemanları atar.- İki
nullaynı nesnedir ve ölçüt tekildir; dolu ayrı iki dizgi içerik, dolu dizgi ilenullkimlik verir. - Kural ilk soruda durur: ilk iki satırda üçüncü değerin eşitliği zıt çıktı, ölçüt aynı kaldı.
- Bu dersin ölçütü: tekil.
nullilenullaynı nesnedir.
Sonraki Adım
Bu derste null’u tipte gördük ve if ile denetledik. Her null denetimi ayrı bir if ister ve denetimler art arda dizildikçe kod uzar. Bir sonraki ders aynı işi kısa yoldan yapan işleçleri ölçer: ?. ile güvenli çağrı zinciri null gördüğünde nerede durur, ?: ile Elvis işleci sağ tarafını ne zaman değerlendirir ve null olmama iddiası yanlış çıktığında hangi istisna fırlar.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.