Ders 02 / 23
Değişkenler
`val` ve `var` ile bildirimi, tip çıkarımını ve `const val` sabitini tanıtır; aynı dizgi sabitinin iki değişkende tek nesne olduğunu ölçer.
Yayın Güncelleme
Bu derste
- `val` ile `var` arasındaki farkı ve yeniden atamanın ne zaman reddedildiğini söylemek
- Tip çıkarımının neyi bulduğunu ve açık tip yazmanın ne zaman gerektiğini açıklamak
- Aynı sabitten gelen iki değişkenin aynı nesneye işaret ettiğini, çalışma anında kurulan değerin ise içerik ölçütüne düştüğünü göstermek
İçindekiler
Önceki derste eşitliğin dört ölçütünü sekiz değer üzerinde gördük. Bu derste değerin bir ada bağlanışına bakıyoruz: val ve var ile bildirilen değişkenler. Soru şudur: aynı sabiti iki ayrı değişkene bağlarsam iki ayrı nesne mi oluşur, yoksa tek nesneye iki ad mı verilmiş olur?
val, var ve Tip Çıkarımı
Kotlin’de bir değişkeni val ya da var ile bildirirsiniz. val yeniden atanamayan değişkendir (read-only): ilk değer verildikten sonra aynı ada başka bir değer atanamaz. var ise yeniden atanabilir. İki biçim de değer verirken tipi yazmak zorunda bırakmaz; derleyici sağdaki ifadeden tipi bulur. Bu çıkarım (type inference) derleme zamanında olur ve değişkenin tipi sonradan sabit kalır. Tipi açıkça yazmak isterseniz ad ile değer arasına iki nokta ve tip koyarsınız.
val ile bildirilen bir değişkenin bağlı olduğu nesnenin hiç değişmediğini düşünmek yanlış olur: val yalnız adın bir daha başka değere bağlanamayacağını söyler. Bağlandığı nesne içinden değişebiliyorsa o değişebilir. Bu ayrım ilerleyen derslerde işe yarayacak; burada yalnız adın yeniden atanıp atanamadığına bakıyoruz.
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 bildirimin dört yüzünü bir arada gösterir. city yeniden atanamaz, visits yeniden atanır, ratio tipini açıkça yazar, inferred ise tipini çıkarımdan alır. Dosyanın tepesindeki const val ise derleme zamanı sabitidir (compile-time constant): değeri derleme sırasında bilinen ve yalnız temel tip ya da dizgi olabilen ve üst düzeyde ya da bir nesne bildirimi içinde duran bir sabit.
// Main.kt — val, var, tip cikarimi ve const val
const val MAX_VISITS = 3
fun main() {
val city = "Ankara"
var visits = 1
visits = visits + 1
val ratio: Double = 2.0
val inferred = 7
println("$city, ziyaret: $visits, oran: $ratio")
println("cikarilan tip: ${inferred::class.simpleName}")
println("ust sinir: $MAX_VISITS")
}
Ankara, ziyaret: 2, oran: 2.0 cikarilan tip: Int ust sinir: 3
Çıktıda Ankara, ziyaret: 2, oran: 2.0 satırı visits değişkeninin bir kez yeniden atandığını gösterir. cikarilan tip: Int satırı, 7 sabitinden çıkarılan tipin Int olduğunu doğrular; bunu inferred::class.simpleName ile çalışma anında sorduk. Son satır const val sabitinin değerini basar. Aynı çıktı sandbox’ta Kotlin 2.4.21 ile alındı.
DT8. Çıkarılan tip yalnız 7 sabitiyle ölçüldü; Long ya da Double gerektiren bağlamlarda çıkarımın ne yaptığı bir sonraki dersin konusudur.
DT9. const val yalnız dosyanın tepesinde ölçüldü; bir object ya da companion object içindeki hâlinin kuralları bu dersin dışındadır.
Bu çıktıdan çıkan pratik sonuç şudur. visits gibi bir sayaç var ister, çünkü değeri programın akışında değişir. city gibi bir ad ise val ister, çünkü ilk atamadan sonra başka değere bağlanması bir hata olurdu. Karar değişkenin rolüne göre verilir; tipin çıkarımla mı yazıldığı bu kararı etkilemez.
Yeniden Atama Reddedilir
val ile bildirilen bir değişkene ikinci kez değer atamak derleyicide reddedilir. Bunu sandbox’ta derleyerek gördük; aşağıdaki parça bu yüzden çıktısızdır ve bir main taşımaz.
val limit = 10 limit = 20
Derleyici ikinci satırı val yeniden atanamaz diyerek reddeder, program hiç çalışmaz. Bu, yeniden atamayı çalışma anının değil derleme zamanının sorunu yapar. Kotlin’de varsayılan tercih val olur; var yalnız değerin gerçekten yeniden atanması gerektiğinde yazılır. Böylece bir ada bakan okuyucu, o adın bir yerde başka değere bağlanıp bağlanmadığını aramak zorunda kalmaz.
DT10. Reddedilme tek bir yerel val ile ölçüldü; üst düzey val ve sınıf özelliği olan val için hata mesajının biçimi bu derste doğrulanmadı.
Aynı Sabit, İki Değişken
Şimdi dersin ölçümü. Dört gözlem var. val-literal: iki ayrı val, aynı "kotlin" sabitine bağlanır. var-literal: bir var ile aynı sabit karşılaştırılır. val-kurulan ve var-kurulan ise aynı metni çalışma anında kurar; biri joinToString ile, öbürü buildString ile. Bildirim biçimi val ya da var olsa da fark etmeyip etmediğini böylece ölçeriz.
Her gözlemde Criterion üç şey alır: ölçülen değer, aynı biçimde kurulmuş bir ikiz ve farklı bir değer. Sabit gözlemlerinde ikiz ikinci bir değişkendir ya da doğrudan sabitin kendisidir. Kurulan gözlemlerde ikiz, aynı yöntem bir kez daha çağrılarak üretilir; böylece iki taraf da çalışma anında ayrı ayrı oluşur. Farklı değer her seferinde bir harfi değiştirilmiş kotlim dizgisidir.
// Main.kt — ayni literal iki degiskende
fun main() {
val first = "kotlin"
val twin = "kotlin"
var changeable = "kotlin"
val built = listOf("kot", "lin").joinToString("")
var builtVar = buildString {
append("kot")
append("lin")
}
val checks = listOf(
Criterion("val-literal", first, twin, "kotlim"),
Criterion("var-literal", changeable, "kotlin", "kotlim"),
Criterion("val-kurulan", built, listOf("kot", "lin").joinToString(""), "kotlim"),
Criterion("var-kurulan", builtVar, buildString {
append("kot")
append("lin")
}, "kotlim"),
)
for (check in checks) {
check.report()
}
summarize(checks)
}
val-literal: ayni-nesne=true, ikiz=true, farkli=false -> tekil var-literal: ayni-nesne=true, ikiz=true, farkli=false -> tekil val-kurulan: ayni-nesne=false, ikiz=true, farkli=false -> icerik var-kurulan: ayni-nesne=false, ikiz=true, farkli=false -> icerik tekil: 2 kimlik: 0 icerik: 2 kismi-icerik: 0
val-literal tekildir: first ile twin aynı nesnedir, ayni-nesne=true. Aynı dizgi sabiti, derleme sırasında tek bir nesne olarak tutulur ve onu kullanan her ifade aynı nesneye ulaşır. var-literal de tekildir; var kullanmak sabitin aynı nesne olmasını bozmaz, çünkü var yalnız adın yeniden atanabilirliğini etkiler. val-kurulan ile var-kurulan ise içeriktir: joinToString ve buildString çalışma anında yeni bir dizgi üretir, bu yüzden ayni-nesne=false; ikizle == yine true verir, farklı değerle false.
Özet satırlarında iki tekil, iki içerik görünür. Yani bir değeri val ile ya da var ile bağlamak ölçütü belirlemez; ölçütü belirleyen, değerin nereden geldiğidir: sabit mi, çalışma anında kurulmuş bir değer mi.
DT11. Dizgi sabitinin tek nesne olması Kotlin 2.4.21’in JVM hedefinde ölçüldü; bu davranış JVM’in dizgi havuzuna dayanır ve başka hedeflerde aynı olduğu iddia edilmiyor.
DT12. Kurulan dizgiler joinToString ve buildString ile ölçüldü; ikisi dışındaki kurma yollarının aynı sonucu verdiği bu derste ölçülmedi.
val Bağlanan Nesneyi Dondurmaz
Yukarıda val için “ad bir daha başka değere bağlanamaz” demiştik. Bu cümlenin sınırı, bağlanan nesnenin kendisinin değişebilmesidir. Aşağıdaki blok val ile bildirilmiş bir değişebilir liste kurar ve ona eleman ekler. Ardından ilk değeri bildirim satırında verilmeyen, sonradan atanan bir val gösterir.
// Main.kt — val bagli nesneyi degistirebilir, ilk deger sonra verilebilir
fun main() {
val names = mutableListOf("ada")
names.add("can")
println(names)
val assigned: Int
val flag = names.size > 1
if (flag) {
assigned = 10
} else {
assigned = 20
}
println(assigned)
}
[ada, can] 10
names bir valdir; yine de add çağrısı listeyi [ada, can] hâline getirir. Hata veren bir şey yoktur, çünkü names adı hâlâ aynı listeye bağlıdır; değişen listenin içidir. İkinci değişken assigned bildirim satırında değer almaz, tipi yazılıdır ve if ile else dallarının her birinde tam bir kez atanır. Derleyici bu deseni kabul eder ve ekrana 10 basar. Kural şudur: val değişken, kapsamının her yolunda en çok bir kez değer alabilir.
Sık yapılan hata, val sözcüğünü “nesne değiştirilemez” diye okumaktır. Bir listenin içeriğini dondurmak istiyorsanız salt okunur bir koleksiyon tipi seçilir (listOf); val tek başına bunu sağlamaz. Java karşılaştırması: Java’daki final değişken aynı sınırı taşır, final bir listeye eleman eklenebilir ve ilk değerin sonradan verildiği final değişken de Java’da bilinen bir desendir. Bu iki Java davranışı burada ölçülmedi, bilinen davranış olarak anıldı.
DT13. Bu bölümde MutableList ve Int ile ölçüldü; val bir data class örneğine bağlıysa özelliklerinin değişip değişmediği bu derste ölçülmedi.
Lambda İçinden var Değiştirmek
Kotlin, bir lambdanın kendi dışındaki var değişkenini okumasına ve değiştirmesine izin verir. Aşağıdaki blok bir toplamı ve bir çağrı sayacını forEach içinden günceller; ardından toplamın o andaki değerini bir val içine kopyalayıp asıl var değişkenini değiştirir.
// Main.kt — lambda icinden var degistirmek
fun main() {
var total = 0
var calls = 0
listOf(1, 2, 3).forEach {
total += it
calls++
}
println("toplam: $total, cagri: $calls")
val snapshot = total
total = 100
println("kopya: $snapshot, var: $total")
}
toplam: 6, cagri: 3 kopya: 6, var: 100
Birinci satır toplam: 6, cagri: 3 verir: lambda üç kez çalıştı ve dışarıdaki iki değişken de güncellendi. İkinci satır kopyanın ayrı kaldığını gösterir. snapshot değeri 6 olarak kalır, total ise 100 olur. Int değeri atandığı anda kopyalandığı için sonradan değişen var kopyaya yansımaz.
Mekanizmanın nedeni lambdanın dışarıdaki değişkeni değer olarak değil, o değişkenin kendisini taşıyarak yakalamasıdır; lambda ve dış kod aynı var üzerinde çalışır. Java’da aynı iş için lambdanın yakaladığı yerel değişkenin etkin olarak final olması gerekir, bu yüzden sayaç tutmak için dizi ya da AtomicInteger gibi bir kap kullanılır. Kotlin’de böyle bir geçici çözüme gerek yoktur. Bu Java kuralı ve Kotlin’in arkada yakalanan değişkeni nasıl taşıdığı bu derste ölçülmedi; ölçülen yalnız dışarıdaki var değerlerinin lambda sonrasında güncel olduğudur.
DT14. Yakalanan değişken Int tipindeydi ve forEach ile ölçüldü; çok iş parçacıklı bir bağlamda aynı güncellemenin davranışı bu dersin dışındadır.
const val ve Birleşim
Dersin başında const val için “değeri derleme sırasında bilinen sabit” dedik. Bu tanımın bir sonucu var: iki const val dizgisinin birleşimi de derleme sırasında hesaplanır. Aşağıdaki blok iki const val birleşimini, aynı metni çalışma anında joinToString ile kuran bir üst düzey val ile karşılaştırır. İkizi her iki ölçümde aynı ifadenin bir kez daha yazılmasıyla gelir.
// Main.kt — const val birlesimi ve calisma ani degeri
const val PREFIX = "kot"
const val FULL = PREFIX + "lin"
val runtimeFull = listOf("kot", "lin").joinToString("")
fun main() {
val checks = listOf(
Criterion("const-birlesim", FULL, "kotlin", "kotlim"),
Criterion("calisma-ani-top-level", runtimeFull, "kotlin", "kotlim"),
)
for (check in checks) {
check.report()
}
summarize(checks)
}
const-birlesim: ayni-nesne=true, ikiz=true, farkli=false -> tekil calisma-ani-top-level: ayni-nesne=false, ikiz=true, farkli=false -> icerik tekil: 1 kimlik: 0 icerik: 1 kismi-icerik: 0
const-birlesim tekildir: PREFIX + "lin" ifadesi derleme zamanında "kotlin" sabitine dönüşür ve dizgi havuzundaki tek nesneye bağlanır. calisma-ani-top-level içeriktir: aynı metin çalışma anında kurulduğunda ayrı bir dizgi olur, ayni-nesne=false. Özet satırında bir tekil ve bir içerik görünür.
Burada sık yapılan hata, const val ile üst düzey val değişkenini birbirinin yerine kullanılabilir saymaktır. İkisi de dosyanın tepesinde durur ve ikisi de yeniden atanamaz; ama biri değerini derleme sırasında, öbürü program çalışırken alır. Sınır durumu da buradan çıkar: const val yalnız derleyicinin hesaplayabildiği değerlerle kurulur. Java karşılaştırması: Java’daki static final alan benzer bir ayrımı taşır, derleme zamanı sabiti olan ve olmayan static final alanlar vardır. Bu Java ayrımı ölçülmedi.
DT15. Bu karşılaştırma iki const val dizgisi ve joinToString ile ölçüldü; sayısal const val birleşimlerinin davranışı bu bölümde ölçülmedi.
Ölçümlerden Çıkan Alışkanlıklar
val ile var arasındaki fark, bu dersin ölçümlerinde bir değişkenin bağlanmasıyla nesnenin içeriği arasındaki fark olarak ortaya çıktı. val bir kez bağlanan bir ad demektir; bağlanan nesne değiştirilebilir bir liste ise listeye eleman eklenebilir ve [ada, can] çıktısı bunu gösterdi. Dolayısıyla bir val için “değiştirilemez” demek dikkat ister: yeniden atanamayan bağlanmadır, nesne değildir. İçeriği korumak için salt okunur bir koleksiyon tipi ya da data class içinde val alanlar kullanılır.
Lambda içinden bir var değiştirildiğinde, sayaç dışarıda da göründü: toplam: 6, cagri: 3. Ölçüm, lambdanın ve dış kodun aynı değişkene baktığını gösterdi; bunun altında değişkenin bir kutuya sarılması olduğu bilinir, ama bu derste ölçülmedi. Kopya alınan değer ise ayrı kaldı: kopya: 6, var: 100 satırında var yeniden atandığında kopya etkilenmedi. Sık yapılan hata, bir değeri yakalayıp sonra değişkeni değiştirdiğinde yakalanan değerin de değişeceğini sanmaktır; kopya ile referans arasındaki farkı atamanın nerede yapıldığına bakarak ayırmak gerekir.
const val ise derleme anı sabitidir ve yalnız temel tip ya da dizgi değeri alabilir; üst düzeyde, bir nesne bildiriminde ya da yardımcı nesnede tanımlanır. Ölçümde iki const val birleşimi tek bir nesne olarak ortaya çıktı, çalışma anında hesaplanan birleşim ise içerik olarak eşit ama ayrı bir değer verdi. Pratik kural şudur: bir değer derleme anında bilinebiliyorsa const val, bilinemiyorsa val yazılır. Java’da aynı ayrım static final ile ifade edilir, ama derleme anı sabiti olup olmadığı alanın başlatma ifadesine bağlıdır ve yazımdan okunmaz.
Bu üç gözlemi tek bir cümleye indirgemek gerekirse: bir adın değişip değişmediğini, adın bağlandığı nesnenin değişip değişmediğinden ayrı sorun. Kodu okurken her val gördüğünüzde bu iki soruyu birbirinden ayırmak, hataların büyük kısmını önceden yakalar.
Sınırlayıcı Ölçüm
Tekil ölçüt sabitle sınırlıdır. Aynı metin çalışma anında kurulduğunda ölçüt içeriğe döner: val-kurulan ve var-kurulan bunun iki örneğidir. Aynı "kotlin" metni, bir yerde sabit olarak yazıldığında tekil, başka bir yerde joinToString ya da buildString ile kurulduğunda içerik ölçütüne düşer. Bu yüzden kodda metin karşılaştırırken === yerine == kullanılır; ikincisi her iki durumda da doğru yanıtı verir.
Özet
valyeniden atanamayan,varyeniden atanabilir değişkendir; ikisinin de tipi çıkarımla bulunabilir.const valderleme zamanı sabitidir; değeri derleme sırasında bilinmelidir.valdeğişkene ikinci kez atama derleyicide reddedilir.valya davarseçimi eşitlik ölçütünü belirlemez; değerin sabitten mi çalışma anından mı geldiği belirler.- Bu dersin ölçütü: tekil; sınırlayıcı ölçüm: çalışma anında kurulan aynı metin içerik ölçütüne düşer.
Sonraki Adım
Bir değişkenin tipi çıkarımla bulunuyordu. Sıradaki derste temel tiplere bakıyoruz: sayılar, karakter ve mantıksal değer. Aynı 1 sayısı bir bağlamda Int, başka bir bağlamda Long olur; bu iki tipi karıştıran bir == neden reddedilir, onu ölçeriz.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.