Ders 03 / 23
Temel Tipler
Sayı tiplerini, karakteri ve mantıksal değeri tanıtır; boş değer alabilen `Int` kutulandığında eşitliğin ölçütünün değiştiğini ölçer.
Yayın Güncelleme
Bu derste
- `Int`, `Long`, `Double`, `Char` ve `Boolean` tiplerinin rollerini ayırt etmek
- Sayı tipleri arasındaki dönüşümün açık yazıldığını ve karşılaştırmanın tipleri eşleştirmediğini göstermek
- Kutulanan sayıların `===` ile karşılaştırıldığında neden tutarsız çıktığını açıklamak
- `Double.NaN` değerinin statik tipe göre farklı eşitlik verdiğini söylemek
İçindekiler
Önceki derste 7 sabitinden çıkarılan tipin Int olduğunu gördük. Şimdi temel tiplere bakıyoruz: tam sayı için Int ve Long, kayan noktalı sayı için Double, tek karakter için Char, mantıksal değer için Boolean. Bu tiplerde eşitlik sorusunun iki yüzü var. Birincisi, farklı tipteki sayılar == ile karşılaştırılabilir mi? İkincisi, bir sayı kutulandığında (boxing) aynı değer iki nesneye bölünebilir mi?
Tipler ve Açık Dönüşüm
Kotlin’de tam sayı sabiti varsayılan olarak Int olur. Int sınırını aşan sabit kendiliğinden Long olur; L soneki küçük bir değeri de Long yapmak için yazılır. Ondalıklı sabit Double olur. Char tek tırnakla yazılır ve sayısal kodunu .code ile verir. Dönüşümler açıktır: Int bir Long değişkene kendiliğinden atanmaz; count.toLong() yazılır. Aritmetik ise tipleri karıştırabilir: Int ile Double toplanınca sonuç Double olur.
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 kuralların hepsini bir arada çalıştırır. total değişkeni Int değerini Long’a çevirip milyarla çarpar. 7 / 2 ile 7 / 2.0 iki bölmeyi karşılaştırır: ilkinde iki taraf tam sayıdır, ikincisinde bir taraf ondalıklıdır. letter + 1 bir Char ile bir sayıyı toplar.
// Main.kt — sayilar, karakter, mantiksal deger ve acik donusum
fun main() {
val count: Int = 3
val total: Long = count.toLong() * 1_000_000_000L
val size = 2.5
val letter = 'K'
val ready = count > 2
println(count + size)
println(total)
println(7 / 2)
println(7 / 2.0)
println(letter.code)
println(letter + 1)
println(ready)
}
5.5 3000000000 3 3.5 75 L true
count + size 5.5 verir: Int ile Double toplandı ve sonuç Double oldu. total 3000000000 basar; bu değer Int sınırını aştığı için Long gerekti. 7 / 2 üç verir, çünkü iki taraf tam sayıdır ve bölme sayı bölmesi olarak yapılır, kesirli kısım atılır. 7 / 2.0 ise 3.5 verir. letter.code büyük K harfinin kodu olan 75’i basar. letter + 1 bir sonraki karakter olan L harfini verir. Son satır ready değişkeninin true olduğunu gösterir.
DT16. Bu bölümde bölme 7 ve 2 sabitleriyle ölçüldü; negatif sayıda bölmenin davranışı ayrı bir bölümde ölçülür.
DT17. Char aritmetiği yalnız letter + 1 ile ölçüldü; iki Char arasındaki farkın tipi bu derste ölçülmedi.
Dönüşüm Açıktır, Karşılaştırma da
Aritmetiğin tipleri karıştırabilmesi atama ve karşılaştırma için geçerli değildir. Int değerini Long tipli bir değişkene atamak derleyicide reddedilir; bunu sandbox’ta derleyerek gördük, parça bu yüzden çıktısızdır.
val count: Int = 3 val wide: Long = count
Derleyici Long beklenen yerde Int bulduğunu bildirir. Karşılaştırma da aynı kuralı izler: bir Int ile bir Long sabitini == ile karşılaştırmak yine derlenmez.
val count: Int = 1 println(count == 1L)
Derleyici == işlecinin Int ile Long arasında uygulanamayacağını bildirir. Çözüm dönüşümü açık yazmaktır: count.toLong() == 1L. Ama tipler Any olarak tutulursa durum değişir: derleyici statik tipi bilemez, eşitliği çalışma anındaki nesnelere sorar.
// Main.kt — Any olarak tutulan sayilar
fun main() {
val a: Any = 1
val b: Any = 1L
println(a == b)
}
false
1 ile 1L, Any olarak tutulduğunda eşit çıkmadı: çıktı false. Bir Int nesnesi ile bir Long nesnesi, sayısal değerleri aynı olsa da farklı sınıfa aittir ve equals bu farkı gözetir. Yani derleyicinin reddettiği karşılaştırma, tipi gizlendiğinde reddedilmez ama yine de yanıtı false olur.
DT18. Derleyici hataları Kotlin 2.4.21 ile alındı; ileri sürümlerde mesaj sözcükleri değişebilir, ama iki karşılaştırmanın reddedileceği kuralı bu derste ölçülen budur.
Kutulanan Sayı ve NaN
Dersin ölçümü iki şeye bakar. Birincisi, boş değer alabilen bir tip olan Int? kutulanmış bir sayı tutar; JVM’de bu bir Integer nesnesidir. İki kutulanmış sayı aynı değerde olsa bile aynı nesne mi? İkincisi, Double.NaN (sayı olmayan değer) statik tipe göre farklı eşitlik verir. Önce iki tarafı da Double olan karşılaştırma, sonra aynı değerin Any olarak tutulduğu karşılaştırma yazdırılır.
// Main.kt — kutulanan sayilar ve NaN
fun main() {
val boxedBig: Int? = 1000
val boxedBigTwin: Int? = 1000
val boxedSmall: Int? = 127
val boxedSmallTwin: Int? = 127
val nan = Double.NaN
val anyNan: Any = nan
val anyNanTwin: Any = Double.NaN
println(nan == nan)
println(anyNan == anyNan)
val checks = listOf(
Criterion("kutulanan-1000", boxedBig, boxedBigTwin, 1001),
Criterion("kutulanan-127", boxedSmall, boxedSmallTwin, 128),
Criterion("nan-any-tipinde", anyNan, anyNanTwin, 1.0),
)
for (check in checks) {
check.report()
}
summarize(checks)
}
false true kutulanan-1000: ayni-nesne=false, ikiz=true, farkli=false -> icerik kutulanan-127: ayni-nesne=true, ikiz=true, farkli=false -> tekil nan-any-tipinde: ayni-nesne=false, ikiz=true, farkli=false -> icerik tekil: 1 kimlik: 0 icerik: 2 kismi-icerik: 0
İlk iki satır NaN eşitliğidir. nan == nan, statik tip Double olduğu için IEEE kuralını izler ve false verir; NaN kendisine eşit değildir. anyNan == anyNan ise true verir, çünkü statik tip Any olduğunda == nesnenin equals yöntemini çağırır ve equals NaN’ı kendisine eşit sayar. Aynı sayı, hangi tipte tutulduğuna göre iki ayrı yanıt veriyor.
Üç ölçüm satırında kutulanan-1000 içeriktir: iki Int? değişkeni 1000 değerinde ama ayrı nesnelerdir, ayni-nesne=false. kutulanan-127 ise tekildir: aynı değer JVM’de aynı nesneye işaret eder, ayni-nesne=true. Tipi aynı, değer biçimi aynı iki sayı, değere göre farklı ölçüte düştü. nan-any-tipinde içeriktir: ikizle == true verir, ayrı nesnelerdir.
DT19. 127 ve 1000 JVM’de, Kotlin 2.4.21’de ölçüldü; hangi değerlerin paylaşıldığına dair genel bir sınır bu derste çıkarılmıyor ve 127 ile 1000 dışındaki değerler için bir iddia kurulmuyor.
DT20. NaN gözlemi tek bir Double.NaN değeriyle ve Any tipli tutarak ölçüldü; Float.NaN için aynı sonucun çıkması bu derste ölçülmedi.
Taşma, Sıfıra Bölme ve Ondalık Hata
Tipin bir sınırı olması, sınırın aşıldığı durumda ne olacağını da bilmeyi gerektirir. Aşağıdaki blok üç sınır durumunu bir arada gösterir: Int sınırını aşan toplam, sıfıra tam sayı bölmesi ve sıfıra ondalıklı bölme. Ardından ondalıklı sayıların ikilik gösterimden gelen hatasını 0.1 + 0.2 ile sınar. Taşmayı hata olarak yakalayan Math.addExact çağrısı JVM’in kendi yöntemidir; Kotlin onu doğrudan çağırabilir.
// Main.kt — tasma, sifira bolme ve ondalik hata
fun main() {
val zero = 0
val big = 3_000_000_000
println(Int.MAX_VALUE + 1)
println(Long.MAX_VALUE)
println(big::class.simpleName)
try {
Math.addExact(Int.MAX_VALUE, 1)
} catch (e: ArithmeticException) {
println(e.message)
}
try {
println(1 / zero)
} catch (e: ArithmeticException) {
println(e.message)
}
println(1.0 / 0.0)
println(0.1 + 0.2)
println(0.1 + 0.2 == 0.3)
}
-2147483648 9223372036854775807 Long integer overflow / by zero Infinity 0.30000000000000004 false
Int.MAX_VALUE + 1 hata vermez, sessizce -2147483648 olur: sayı en büyük değerden en küçüğe sarar. Long.MAX_VALUE için 9223372036854775807 basılır. 3_000_000_000 sabiti Int sınırını aştığı için tip çıkarımı onu kendiliğinden Long yapar; çıktıdaki Long bunu doğrular. Math.addExact ise aynı toplamda integer overflow iletisiyle hata fırlatır; yani taşmanın fark edilmesini istiyorsanız bu yolu seçersiniz.
Tam sayı sıfıra bölününce / by zero iletisi gelir. Ondalıklı bölmede aynı işlem hata vermez ve Infinity döner. Son iki satır ondalıklı sayının sınırını gösterir: 0.1 + 0.2 sonucu 0.30000000000000004 olur ve 0.3 ile == karşılaştırması false verir.
Sık yapılan hata iki yerde çıkar. Birincisi, taşmanın hata vermeyeceğini unutup toplamı Int içinde biriktirmektir. İkincisi, ondalıklı iki sayıyı == ile karşılaştırmaktır; para gibi tam çıkması gereken değerlerde ondalıklı tip yanlış seçimdir. Java karşılaştırması: bu çıktılar Java ile aynıdır, çünkü Kotlin burada JVM’in kendi aritmetiğini kullanır; Kotlin bu davranış için ayrı bir tip yazmayı gerektirmez. Bu aynılık Java kodu çalıştırılarak değil, çalışma ortamının aynı olmasına dayanarak söyleniyor.
DT21. Taşma Int toplamıyla, sıfıra bölme Int ve Double ile ölçüldü; Long taşması ve Float için sonucun aynı biçimde çıktığı bu derste ölçülmedi.
Daraltan Dönüşümler
Genişleten dönüşüm sorun çıkarmaz; sorun değerin dar bir tipe sığdırıldığı yerde çıkar. Aşağıdaki blok beş dönüşümü ve metinden sayıya çevirmeyi ölçer: 300 değerini Bytea, ondalıklı sayıları Inte, 65 değerini Chara çevirmek ve geçerli ya da geçersiz bir dizgiyi Inte çevirmek.
// Main.kt — daraltan donusumler ve metinden sayi
fun main() {
println(300.toByte())
println(3.99.toInt())
println((-3.99).toInt())
println(65.toChar())
println("42".toInt() + 1)
println("x".toIntOrNull())
try {
"x".toInt()
} catch (e: NumberFormatException) {
println(e.message)
}
}
44 3 -3 A 43 null For input string: "x"
300.toByte() sekiz bitten fazlasını taşıyamaz ve 44 olur: yalnız alt sekiz bit kalır, üst bitler atılır ve hata verilmez. 3.99.toInt() yuvarlamaz, kesirli kısmı keser; 3 çıkar. Negatif değerde (-3.99).toInt() de -3 olur, yani sıfıra doğru kesme vardır, aşağı yuvarlama değil. 65.toChar() büyük A harfini verir; sayı ile karakter kodu arasındaki köprü açıktır.
Metinden sayı çevirmenin iki yolu vardır. "42".toInt() + 1 geçerli metinde 43 verir. Geçersiz metinde ise iki seçenek ayrılır: toIntOrNull() hata fırlatmak yerine null döndürür, toInt() ise For input string: "x" iletisiyle bir NumberFormatException fırlatır. Kullanıcıdan gelen metin için ilk yol bir null denetimi ister ama programı düşürmez, ikinci yol ise düşürür.
Mekanizmanın nedeni dönüşümlerin açık yazılmasıdır. Derleyici dar bir tipe sessizce atama yapmaz; toByte() yazmak, bilgi kaybını bilerek kabul ettiğini söyler. Java’da aynı daralma (byte) 300 biçimindeki dönüştürme (cast) ile yazılır ve çoğu zaman gözden kaçar.
DT22. Dönüşümler Byte, Int ve Char ile ölçüldü; Short, Float ve Long için aynı kesme kurallarının geçerli olduğu bu derste ölçülmedi.
Negatif Sayıda Bölme ve Mod
İlk bölümde 7 / 2 ifadesinin kesirli kısmı attığını gördük. Peki bölünen negatifse? Aşağıdaki blok negatif bir bölme ve kalan işleminin dört yazılışını, ardından üç başka sınır durumunu bir arada gösterir: en küçük Intin mutlak değeri, onaltılık ve ikilik sabit yazımı, iki Char arasındaki sıralama.
// Main.kt — negatif sayilarda bolme ve mod
import kotlin.math.abs
fun main() {
println(-7 / 2)
println(-7 % 2)
println((-7).mod(2))
println(Math.floorMod(-7, 2))
println(abs(Int.MIN_VALUE))
println(0xFF)
println(0b1010)
println('a' < 'b')
}
-3 -1 1 1 -2147483648 255 10 true
-7 / 2 sonucu -3 verir; kesirli kısım sıfıra doğru atılır, aşağıya doğru değil. -7 % 2 ise -1 verir: kalanın işareti bölünenin işaretini izler. Kalanın her zaman sıfır ya da pozitif çıkmasını istiyorsanız (-7).mod(2) ya da JVM’in Math.floorMod(-7, 2) çağrısı gerekir; ikisi de 1 verdi. Sık yapılan hata, bir dizinin indeksini % ile dönüştürürken negatif kalanın çıkabileceğini unutmaktır.
abs(Int.MIN_VALUE) sonucu -2147483648 olur; mutlak değer bile pozitif çıkmaz, çünkü Int içinde +2147483648 karşılığı yoktur. 0xFF sabiti 255, 0b1010 sabiti 10 basılır; onaltılık ve ikilik sabit yazımı tipin kendisini değiştirmez, yalnız yazılışı değiştirir. 'a' < 'b' ifadesi true verir, çünkü Char değerleri karakter koduna göre sıralanır.
Java karşılaştırması: % işleci Java’da da bölünenin işaretini izler. Kotlin’in eklediği mod, Java’da Math.floorMod ile elde edilen sonucu işlecin yanında bir yöntem olarak sunar. Bu iki sonuç da yukarıdaki çıktıda gözlendi.
DT23. Negatif bölme ve kalan tek bir çiftle (-7, 2) ölçüldü; negatif bölen ya da Long tipi için aynı işaret kuralının geçerli olduğu bu derste ölçülmedi.
Ölçümlerden Çıkan Alışkanlıklar
Sayısal tiplerde Kotlin’in güvenceleri sınırlıdır ve bu dersin ölçümleri sınırın nerede başladığını gösterdi. Taşma sessizdir: Int.MAX_VALUE + 1 çıktısı -2147483648 oldu ve hiçbir istisna fırlatılmadı. Bu davranış Java ile aynıdır. Taşmanın zararlı olabileceği yerlerde, örneğin para hesabında ya da süre toplamında, Long kullanmak ya da Math.addExact gibi denetimli işlemler yazmak gerekir. Long için aynı taşma çok daha uzak bir sınırda olduğundan çoğu durumda yeterlidir, ama sınırı ortadan kaldırmaz.
Sıfıra bölme tam sayıda istisna fırlatır (/ by zero), ondalık sayıda ise Infinity verir. Bu fark, bir hesabın tipi değiştirildiğinde hata davranışının da değiştiği anlamına gelir. Double kullanan bir kod parçası sıfıra bölündüğünde sessizce çalışmaya devam eder ve sonucu yayar; hatayı fark etmek için isInfinite ya da isNaN denetimi gerekir. Ondalık sayılar ikili tabanda tutulduğu için 0.1 + 0.2 çıktısı 0.30000000000000004 oldu ve == 0.3 karşılaştırması false verdi. Para gibi tam değer isteyen yerlerde BigDecimal ya da kuruşa çevrilmiş tam sayı kullanılır.
Daraltan dönüşümler de dikkat ister. Büyük bir tipten küçüğe çevirmek sayıyı kırpar ya da çevirir ve ölçümde 44, 3, -3, A gibi sonuçlar bunun her zaman “yuvarlama” olmadığını gösterdi: toInt ondalık kısmı atar, yani -3.99 değeri -3 olur. toIntOrNull ise çevrilemeyen dizgi için null döndürür, toInt aynı durumda istisna fırlatır ve For input string: "x" iletisi bu davranışı doğruladı. Kullanıcıdan gelen metni sayıya çevirirken OrNull biçimini seçmek, hatayı akışın bir parçası yapar.
Son olarak negatif bölme ve mod ölçümü, sonucun işaretinin Java ile aynı kuralı izlediğini gösterdi: bölme sıfıra doğru yuvarlanır, kalan ise bölünenin işaretini taşır. Matematikteki mod ile program mod işlemi aynı şey değildir; negatif sayılarla çalışan bir döngüsel indeks hesabında mod yerine % kullanan kod, indeksi eksiye çıkarabilir.
Sınırlayıcı Ölçüm
Kutulanan sayı için ölçüt değere bağlıdır: 1000 içerik, 127 tekil. Bu yüzden sayıların kimliğini === ile karşılaştırmak güvenilmez; sayı karşılaştırması == ile yapılır. Aynı değer 127 iken tekil, 1000 iken içerik çıktı. Tek bir ölçüt yerine iki ölçüt gördük.
NaN ise ayrı bir sınır koyar: statik tipi Double olan karşılaştırmada NaN eşit değildir, Any tipinde eşittir. == işlecinin anlamı, işlecin soldaki ifadenin statik tipine bağlı olduğu bir yerde değişir.
Özet
- Tam sayı sabiti
Intolur; sınırı aşan değerLongister. Ondalıklı sabitDoubleolur. IntdeğeriLongdeğişkene kendiliğinden atanmaz ve birIntile birLongsabiti==ile karşılaştırılamaz.- Aritmetik tipleri karıştırabilir:
IntileDoubletoplamıDoubleolur. - Boş değer alabilen
Intkutulanır; 127 tekil, 1000 ise içerik çıktı. - Bu dersin ölçütü: içerik (1000); sınırlayıcı ölçüm: 127 değeri JVM’de tekildir ve NaN statik tipe göre farklı eşitlik verir.
Sonraki Adım
Sayıları ve karakteri gördük. Sıradaki derste dizgilere dönüyoruz: şablonlar, ham dizgiler ve trimIndent. Bir dizgiyi şablonla kurmak, aynı metni elle yazmakla aynı nesneyi mi verir, onu ölçeriz.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.