Ders 04 / 23
Dizgi Şablonları
Dizgi şablonlarını, ham dizgileri ve `trimIndent` fonksiyonunu tanıtır; şablonla kurulan dizginin içerik ölçütüne düştüğünü, tek değişkenli şablonun ise tekil kaldığını ölçer.
Yayın Güncelleme
Bu derste
- `$ad` ve `${ifade}` şablon biçimlerini ve kaçış gerektiren durumu yazmak
- Üç tırnaklı ham dizgiyi ve `trimIndent` fonksiyonunu kullanmak
- Şablonla kurulan bir dizginin eşitlik ölçütünü ve bunun tek değişkenli şablonda nasıl değiştiğini açıklamak
İçindekiler
Sayı tiplerinde otomatik kutulama, aynı değerin iki nesneye bölünebileceğini gösterdi. Dizgiler bu soruyu başka bir yerden sorar: bir metni şablonla kurduğumda, yazdığım metin ile aynı nesneyi mi elde ederim? Önce şablonun kendisine bakıyoruz: nasıl yazılır, ham dizgiyle nasıl birleşir ve çıktıda neyi değiştirir. Ardından aynı şablonları eşitlik ölçüsünden geçiriyoruz ve tek bir yazım biçiminin iki ölçüte düştüğü yeri arıyoruz.
Şablon, Ham Dizgi ve trimIndent
Kotlin’de çift tırnaklı bir dizgi içine değer yerleştirmek için $ işareti kullanılır. $ad bir değişkenin değerini, ${ifade} ise bir ifadenin sonucunu dizgiye koyar. İfade biçimi bir fonksiyon çağrısı ya da bir özellik erişimi de olabilir: ${items.size}, ${items.first().uppercase()}. Dizgi içinde $ karakterini kendi anlamıyla yazmak isterseniz önüne ters bölü koyarsınız: \$5.
Üç çift tırnakla başlayan dizgi ham dizgidir (raw string). Satır sonları ve girintiler olduğu gibi korunur, kaçış dizileri çalışmaz, ama şablonlar çalışır. Ham dizgi çok satırlı olduğunda her satırın başındaki girinti metnin parçası olur; trimIndent fonksiyonu ortak girintiyi siler ve baştaki ile sondaki boş satırı atar.
Bu derste de ölçümler ilk derste tanıtılan yardımcı dosyayla yapılır. Criterion sınıfı, bir değer ile ikizini ve farklı bir değeri alıp üç soruyu sorar; dosya aşağıda aynen verilmiştir.
// 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 }}")
}
}
Aşağıdaki program bu üç şeyi birlikte çalıştırır.
// Main.kt — sablonlar, ham dizgi ve trimIndent
fun main() {
val name = "Zeynep"
val items = listOf("elma", "armut")
println("Merhaba, $name")
println("Oge sayisi: ${items.size}, ilki: ${items.first().uppercase()}")
println("Fiyat: \$5")
val text = """
ad: $name
ogeler: ${items.joinToString()}
""".trimIndent()
println(text)
}
Merhaba, Zeynep Oge sayisi: 2, ilki: ELMA Fiyat: $5 ad: Zeynep ogeler: elma, armut
İlk satır tek bir değişkeni yerleştirir: Merhaba, Zeynep. İkincisi iki ifade yerleştirir: items.size listenin uzunluğunu verir, items.first().uppercase() ilk elemanı büyük harfe çevirir; çıktıda Oge sayisi: 2, ilki: ELMA görünür. Üçüncü satırda ters bölü, $5 metnini şablon olarak yorumlanmaktan kurtarır ve Fiyat: $5 basılır. Ham dizgi dört satırlık kaynak koddan iki satırlık bir metin üretir: girinti trimIndent ile silinir ve çıktıda ad: Zeynep ile ogeler: elma, armut satırları hizalı görünür. items.joinToString() eleman listesini virgülle ayırarak tek metne çevirir.
Programdaki üç yazım biçimi üç ayrı iş görür. Düz $ad en kısa yazımdır ve yalnız bir adın değerini koyar. Süslü parantezli biçim, ad tek başına yetmediğinde ya da şablonun hemen ardından harf geldiğinde gerekir; çünkü parantez, adın nerede bittiğini açıkça gösterir. Ham dizgi ise kaçış dizilerinden kaçınılması gereken çok satırlı metinlerde, örneğin bir yapılandırma örneğinde ya da biçimli bir ileti taslağında işe yarar. trimIndent kaynak kodu girintili tutmanıza izin verir; çıktıdaki metin ise sol kenara yaslı kalır.
DT24. Şablon içinde yalnız basit ifadeler ölçüldü; iç içe şablon ya da dizgi içinde dizgi içeren bir ifadenin davranışı bu dersin dışındadır.
DT25. trimIndent ortak girintiyi siler; girintisi düzensiz satırlarda ne yaptığı bu derste ölçülmedi.
Şablonla Kurulan Dizginin Ölçütü
Dersin sorusuna dönüyoruz. Önceki derste aynı dizgi sabiti iki değişkende tek nesne oldu ve tekil çıktı. Şablonla kurulan dizgi için üç gözlem yazıyoruz. sablon-ile-kurulan: "dil: $name" ile "dil: $name", yani iki ayrı şablon ifadesi. tek-degiskenli-sablon: yalnız "$name" biçiminde, başka metin içermeyen şablon, name değişkenine karşı. sablon-ve-ek: "$name.", yani değişken artı sabit bir ek.
İkinci derste joinToString ve buildString ile kurulan dizgilerin içerik çıktığını görmüştük. Şablon da çalışma anında bir dizgi kuran bir yazım biçimidir; bu yüzden aynı sonucu bekleriz. Beklentiyi ölçüyoruz, çünkü şablonun derleyici tarafından nasıl işlendiği bir dil kuralı değil, bir uygulama ayrıntısıdır ve ayrıntı yazıdan çıkarılamaz. Her gözlemde ikiz, aynı şablonun ikinci kez yazılmasıyla ya da sabitle elde edilir; farklı değer ise kotlim harf değişikliğini taşır.
// Main.kt — sablonla kurulan dizgi
fun main() {
val name = "kotlin"
val checks = listOf(
Criterion("sablon-ile-kurulan", "dil: $name", "dil: $name", "dil: kotlim"),
Criterion("tek-degiskenli-sablon", name, "$name", "kotlim"),
Criterion("sablon-ve-ek", "$name.", "$name.", "kotlim."),
)
for (check in checks) {
check.report()
}
summarize(checks)
}
sablon-ile-kurulan: ayni-nesne=false, ikiz=true, farkli=false -> icerik tek-degiskenli-sablon: ayni-nesne=true, ikiz=true, farkli=false -> tekil sablon-ve-ek: ayni-nesne=false, ikiz=true, farkli=false -> icerik tekil: 1 kimlik: 0 icerik: 2 kismi-icerik: 0
sablon-ile-kurulan içeriktir. İki şablon ifadesi iki ayrı dizgi üretir; ayni-nesne=false, ama == ile eşittirler ve farklı metinle eşit değildirler. sablon-ve-ek de içeriktir; bir değişkeni sabit bir ekle birleştiren şablon her çalıştığında yeni bir nesne kurar.
tek-degiskenli-sablon ise tekildir: "$name" yazıldığında elde edilen nesne, name değişkeninin işaret ettiği nesnenin kendisidir, ayni-nesne=true. Başka bir metin eklenmeyen şablon, yeni bir dizgi kurmaz.
Bu üç gözlemin ortak noktası, hepsinin aynı name değişkenini kullanmasıdır. Değişken tek, şablon biçimi değişiyor. Bu seçim bilinçlidir: ölçütü etkileyen şeyin değişkenin değeri değil, şablonun biçimi olduğunu görmek istiyoruz. sablon-ve-ek içindeki nokta, adın hemen ardından gelen sabit bir metindir; şablon bu noktayı adın parçası saymaz, yalnız name değişkenini yerleştirir ve noktayı olduğu gibi bırakır.
DT26. Bu üç gözlem Kotlin 2.4.21 ile ölçüldü; "$s" ifadesinin yeni bir nesne kurmadığı derleyicinin bu sürümdeki davranışıdır ve başka sürümlerde aynı olacağı iddia edilmiyor.
DT27. name değişkeni burada bir dizgi sabitine bağlı bir val; değişkenin çalışma anında kurulmuş bir dizgiye bağlı olduğu hâlde aynı sonucun çıkması ölçülmedi.
Bu sonuç dizgilerle çalışırken bir alışkanlık önerir. Bir metni başka bir metne eşitlik için karşılaştırırken, metnin sabitten mi şablondan mı geldiğini düşünmek gerekmez; == her ikisinde de metne bakar. Metnin hangi nesne olduğu ancak === sorulduğunda ortaya çıkar ve o zaman da yanıt şablonun içinde başka bir şey olup olmadığına göre değişir.
Dolar İşareti Ne Zaman Şablon Başlatır
Yukarıda \$5 ile dolar işaretinin kaçırılabildiğini gördük. Peki her $ kaçış ister mi? Aşağıdaki blok yedi yazımı arka arkaya basar: rakamla biten bir $, boşlukla çevrili bir $, dizginin sonundaki bir $, iki ayrı kaçış yolu ve değişken adının hemen ardına yazılan nokta.
// Main.kt — dolar isareti ne zaman sablon baslatir
fun main() {
val name = "kotlin"
println("fiyat: $5")
println("a $ b")
println("son: $")
println("${'$'}name")
println("\$name")
println("$name.length")
println("${name.length}")
}
fiyat: $5 a $ b son: $ $name $name kotlin.length 6
İlk üç satırda $ olduğu gibi basıldı: $5, a $ b ve sonda duran $. Kural şudur: $ işaretinden sonra bir tanımlayıcının ilk harfi gelirse şablon başlar; rakam, boşluk ya da dizgi sonu gelirse $ sıradan bir karakterdir. Bu yüzden "fiyat: $5" kaçış istemeden çalıştı. Dördüncü ve beşinci satır, adın başında $ isteyen metni yazmanın iki yolunu verir: "${'$'}name" ve "\$name" aynı $name metnini basar.
Altıncı satır sık yapılan hatayı gösterir. "$name.length" yazan kişi 6 bekler, ekrana kotlin.length çıkar: düz $ad biçimi yalnız adı alır, noktadan sonrası metin olarak kalır. Özelliğe erişmek için "${name.length}" yazılır ve son satır 6 verir. Mekanizmanın nedeni düz biçimin ifade değil yalnız ad okumasıdır; süslü parantez ifadenin sınırını açıkça belirler.
Java karşılaştırması: Java’da dizgi içine değer koymak için + ile birleştirme, String.format ya da formatted kullanılır; $ bir özel karakter değildir. Java’dan Kotlin’e geçen biri $ işaretini her zaman kaçıracağını sanabilir. Bu gözlem yalnız Kotlin tarafını ölçer.
DT28. Bu yedi yazım tek bir name değişkeniyle ölçüldü; $ ardından Unicode harf gelen adların nasıl ayrıştığı bu derste ölçülmedi.
Dizgi Değiştirilemez
Kotlin’de String değiştirilemez: hiçbir yöntem var olan nesnenin içeriğini değiştirmez. Değişikliği yapan yöntemler yeni bir dizgi döndürür. Aşağıdaki blok önce uppercase ile bunu gösterir, sonra dört yöntemi eşitlik ölçüsünden geçirir. Soru şudur: bir yöntem bir şey değiştirmediğinde yeni bir nesne mi kurar, yoksa eldekini mi döndürür?
// Main.kt — dizgi degistirilemez, yontemler yeni nesne dondurebilir
fun main() {
val text = "kotlin"
val upper = text.uppercase()
println(text)
println(upper)
val checks = listOf(
Criterion("replace-eslesme-yok", text, text.replace("x", "y"), "kotlim"),
Criterion("replace-eslesme-var", text, text.replace("k", "k"), "kotlim"),
Criterion("uppercase-lowercase", text, text.uppercase().lowercase(), "kotlim"),
Criterion("trim-bosluksuz", text, text.trim(), "kotlim"),
)
for (check in checks) {
check.report()
}
summarize(checks)
}
kotlin KOTLIN replace-eslesme-yok: ayni-nesne=true, ikiz=true, farkli=false -> tekil replace-eslesme-var: ayni-nesne=false, ikiz=true, farkli=false -> icerik uppercase-lowercase: ayni-nesne=false, ikiz=true, farkli=false -> icerik trim-bosluksuz: ayni-nesne=true, ikiz=true, farkli=false -> tekil tekil: 2 kimlik: 0 icerik: 2 kismi-icerik: 0
İlk iki satır değiştirilemezliği doğrular. text hâlâ kotlin iken upper ayrı bir nesne olarak KOTLIN değerini taşır. Ölçüm satırları ise iki ölçüte ayrıldı. replace-eslesme-yok tekildir: "x" metnini "y" ile değiştirmeye çalışan replace, eşleşme bulmayınca aynı nesneyi döndürür. trim-bosluksuz de tekildir: kırpılacak boşluğu olmayan dizgi için trim kendi nesnesini döner. replace-eslesme-var içeriktir: eşleşme bulunduğunda, yerine konan metin aynı olsa bile yeni bir dizgi kurulur. uppercase-lowercase de içeriktir, çünkü iki dönüşüm ardışık yeni nesneler üretir.
Bu, ikinci derste gördüğümüz kuralı genişletir: ölçütü belirleyen değerin nereden geldiğidir. Burada kaynak bir yöntemdir ve yöntem iş yapmadığında girdisini döndürür. Bu davranış dilin güvencesi değil, ölçülen bir sonuçtur; === ile test eden kod bu yüzden kırılgandır. Java’da String de değiştirilemez ve benzer biçimde yöntemler yeni dizgi döndürür, ama bu bölümde Java tarafı ölçülmedi.
DT29. Dört yöntem tek bir dizgiyle ölçüldü; trim dışındaki trimStart, trimEnd ve substring için aynı “iş yoksa aynı nesne” davranışının geçerli olduğu bu derste ölçülmedi.
Uzunluk, Kaçış ve UTF-16
length bir dizgideki karakter sayısını verir, ama “karakter” sözcüğü burada tuzaklıdır. Aşağıdaki blok kaçış dizili bir dizgiyi ham eşiyle karşılaştırır, ardından bir emoji karakterinin uzunluğunu ve kod noktası sayısını sorar. Sonda ham dizginin trimMargin ile satır başlarından kırpılması gösterilir.
// Main.kt — uzunluk, kacis dizisi ve UTF-16
fun main() {
val escaped = "a\nb"
val raw = """a\nb"""
println(escaped.length)
println(raw.length)
val face = "\uD83D\uDE00"
println(face.length)
println(face.codePointCount(0, face.length))
val margin = """
|satir bir
|satir iki
""".trimMargin()
println(margin.lines().size)
println(margin.lines().last())
}
3 4 2 1 2 satir iki
"a\nb" dizgisinin uzunluğu 3 çıkar: \n tek bir satır sonu karakteridir. Aynı yazım ham dizgide 4 verir, çünkü ham dizgide kaçış çalışmaz ve ters bölü ile n ayrı iki karakter olarak sayılır. Kaçışın çalışıp çalışmadığı bu iki sayıdan okunur.
Emoji için length 2 verir, codePointCount ise 1. Kotlin dizgileri UTF-16 kodlamasıyla tutulur ve bu karakter iki kod birimine (surrogate pair) ihtiyaç duyar. length kod birimi sayar, kod noktası saymaz. Sık yapılan hata, kullanıcıya gösterilen “karakter sayısı” için length kullanmaktır; emoji içeren metinde sayı yanlış çıkar. Java’da String.length() aynı kod birimi sayısını verir; bu Kotlin dizgisinin altında JVM dizgisi olmasından gelir.
Son iki satır trimMargin sonucudur. | işaretiyle başlayan iki satırdan iki satır kalır ve ikincisi satir iki olarak okunur. trimIndent ortak girintiyi siler, trimMargin ise her satırda | işaretine kadar olan kısmı atar; girintisi düzensiz metinde ikincisi daha öngörülebilirdir. Bu karşılaştırma yalnız bu iki satırlık örnekle ölçüldü.
DT30. Uzunluk ölçümü bir emoji ve bir kaçış dizisiyle yapıldı; birleşik karakterler (örneğin harf ile ayrı aksan işareti) için length sonucunun ne olduğu bu derste ölçülmedi.
Ölçümlerden Çıkan Alışkanlıklar
Şablon yazımında en sık hata, düz $ad biçimini ifade yazımıyla karıştırmaktır. Kural basittir: özelliğe, yönteme ya da herhangi bir işleme ihtiyaç varsa süslü parantez kullanılır. Ölçümde "$name.length" çıktısı kotlin.length oldu; bu hata derleme sırasında yakalanmaz çünkü yazım geçerlidir ve sonuç yalnızca yanlış görünür. Bu tür hatalar testte değil, çıktıya bakınca fark edilir. Bu yüzden şablonlu mesajlar yazarken, noktadan sonra gelen her şeyin süslü parantezin içinde olması bir alışkanlık hâline getirilmelidir.
Dizgilerin değiştirilemez olması iki sonuç doğurur. Birincisi, uppercase gibi bir yöntemin sonucunu bir değişkene atamayı unutmak, değişikliğin kaybolması demektir: text.uppercase() satırını tek başına yazan kod hiçbir şey yapmaz. İkincisi, bir döngüde dizgiyi + ile büyütmek her adımda yeni bir nesne üretir; çok adımlı birleştirmede StringBuilder ya da buildString kullanılır. Java’da da aynı kural geçerlidir ve Kotlin burada ayrı bir güvence getirmez.
Uzunluk konusunda ise “karakter” sözcüğüne güvenmemek gerekir. length UTF-16 kod birimi sayar; bu yüzden emoji ve bazı yazı sistemlerindeki karakterler iki birim sayılır. Kullanıcıya “en çok 20 karakter” sınırı gösteren bir form, bu sayıyı length ile denetlerse emoji kullanan biri için sınırı yanlış uygular. Kod noktası sayısı gerekiyorsa codePointCount kullanılır; kullanıcının algıladığı karakter (grafem) sayısı ise ayrı bir kütüphane gerektirir ve bu derste ölçülmedi.
Ham dizgilerde kaçış çalışmaz; bu yüzden dosya yolları, düzenli ifadeler ve çok satırlı metinler için uygundur, ama \n yazan kişi satır sonu elde etmez. Ölçümde aynı yazım ham dizgide dört, kaçışlı dizgide üç karakter verdi. Hangisinin kullanıldığını sayıdan okuyabilmek, hata ayıklarken işe yarar.
Sınırlayıcı Ölçüm
Şablonla kurulan dizginin ölçütü içeriktir, ama bu genelleme tek değişkenli şablonu dışarıda bırakır. "$name" yalnız bir değişkeni yerleştirdiğinde tekil çıktı; metin ya da ek geldiği anda içeriğe döndü. Aynı yazım biçimi, yani şablon, iki ölçüte düşebiliyor ve ayrım şablonun içinde bir şeyin daha olup olmadığına bakıyor.
Buradan bir kural çıkarılabilir: dizgileri === ile karşılaştırmak yerine == kullanın. Sınırlayıcı ölçüm bu kuralın gerekçesini gösterir: aynı şablon biçimi tekil de verebilir, içerik de verebilir; == her iki durumda da doğru yanıtı verir.
Özet
$adve${ifade}bir değeri dizgiye yerleştirir; dizgide$karakterini yazmak için\$kullanılır.- Üç tırnaklı ham dizgi satır sonlarını korur;
trimIndentortak girintiyi siler. - İki ayrı şablon ifadesi aynı metni üretse de ayrı nesnelerdir.
- Yalnız bir değişken yerleştiren
"$name"şablonu, Kotlin 2.4.21’de, değişkenin kendi nesnesini verir. - Bu dersin ölçütü: içerik; sınırlayıcı ölçüm: tek değişkenli şablon tekildir.
Sonraki Adım
Dizgileri kurmayı gördük. Sıradaki derste programın akışına geçiyoruz: if ve when ifadeleri, for döngüleri ve aralıklar. when bir değeri dalla eşleştirirken == kullanır; hangi örneğin dalı tutturamadığını ölçeriz. O derste kimlik ölçütü yeniden karşımıza çıkacak ve is denetiminin == ile aynı soruyu sormadığını göreceğiz.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.