İçeriğe geç
academia.sh

Ders 05 / 23

Denetim Akışı

`if` ve `when` ifadelerini, `is` denetimini ve `for` döngüsünü tanıtır; `when` dalının `==` ile eşleştiğini ve sıradan sınıf örneğinde tutmadığını ölçer.

Yayın Güncelleme

Bu derste

  • `if` ve `when` yapılarının değer üreten ifade olarak kullanılışını göstermek
  • `is` ile tip denetimini, `..`, `until`, `downTo` ve `step` ile aralıkları yazmak
  • `when` dalının değeri `==` ile eşleştirdiğini ve bunun sıradan sınıfta ne sonuç verdiğini açıklamak
İçindekiler

Dizgi şablonlarında aynı metnin iki biçimde kurulabildiğini gördük. Bu derste akışa geçiyoruz: bir koşula göre değer seçmek, bir değeri dallara dağıtmak, bir aralığı dolaşmak. Sonunda kimlik ölçütünü yeniden buluyoruz: when bir değeri bir dalla eşleştirirken == kullanır ve eşitliği olmayan bir örnek dalı tutturamaz.

if, when ve for

Kotlin’de if bir ifadedir (expression): bir değer üretir ve o değer bir değişkene atanabilir. Ayrı bir üçlü işleç gerekmez; val label = if (score >= 50) "gecti" else "kaldi" yazılır. when de bir ifadedir. Koşulsuz biçimde her dal bir mantıksal ifade taşır ve ilk doğru dal çalışır. Bir değere bağlı biçimde ise when (value) yazılır; her dal ya bir değerle ya da is ile bir tip denetimiyle başlar. when ifade olarak kullanıldığında derleyici bütün durumların kapsandığından emin olmak ister: öznesiz biçimde else dalı gerekir; özneli biçimde ise Boolean, numaralandırma ya da mühürlü sınıf tüm durumlarıyla sayılmışsa else zorunlu değildir.

for döngüsü bir aralığı ya da bir koleksiyonu dolaşır. Aralık 1..3 kapalı bir aralıktır: üst sınır dahildir. 1 until 7, üst sınırı dışarıda bırakır. step adımı belirler ve 3 downTo 1 geriye sayar.

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 bunları bir arada çalıştırır. grade koşulsuz bir when ile bir puanı sözcüğe çevirir. describe ise Any bir değeri is ile tipine göre ayırır: Int ise iki katını, String ise uzunluğunu söyler.

// Main.kt — if, when, is ve for
fun grade(score: Int): String = when {
    score >= 90 -> "pekiyi"
    score >= 70 -> "iyi"
    else -> "gecer"
}

fun describe(value: Any): String = when (value) {
    is Int -> "tam sayi, iki kati ${value * 2}"
    is String -> "dizgi, uzunluk ${value.length}"
    else -> "bilinmeyen"
}

fun main() {
    val score = 82
    val label = if (score >= 50) "gecti" else "kaldi"
    println("$label, ${grade(score)}")
    println(describe(21))
    println(describe("kotlin"))
    println((1..3).joinToString(" "))
    println((1 until 7 step 2).joinToString(" "))
    println((3 downTo 1).joinToString(" "))
}
gecti, iyi
tam sayi, iki kati 42
dizgi, uzunluk 6
1 2 3
1 3 5
3 2 1

İlk satırda label değişkeni if ifadesiyle gecti değerini aldı ve grade(82) çağrısı iyi döndürdü, çünkü 82 doksanın altında, yetmişin üstündedir. İkinci ve üçüncü satırlar describe fonksiyonunun iki dalını gösterir: 21 bir Int olduğu için iki katı olan 42 basıldı, "kotlin" bir String olduğu için uzunluğu olan 6 basıldı. Bu dalların içinde value değişkenine tipin yeni hâliyle erişildi; bu özellik akıllı tip dönüşümü (smart cast) adını taşır. Son üç satır aralıklardır: 1..3 üçünü de verir, 1 until 7 step 2 bir, üç, beş değerlerini verir, 3 downTo 1 geriye sayar.

DT31. when ifadesi tam sayı ve dizgi dallarıyla ölçüldü; numaralandırma ve Boolean üzerindeki kapsayıcı when aşağıda ölçülür, mühürlü sınıf (sealed class) üzerindeki denetim bu dersin dışındadır.

DT32. Aralıklar yalnız joinToString ile yazdırıldı; boş bir aralığın döngüde ne yaptığı aşağıda ayrıca ölçülür.

when Dalı == ile Eşleşir

Dersin ölçümü when dalının eşleşme kuralına bakar. when (value) içinde bir dal ifadesi yazıldığında Kotlin, value == dal karşılaştırmasını yapar. Bu yüzden dalın tutup tutmaması eşitlik ölçütüne bağlıdır. branchMatches fonksiyonu bunu doğrudan sorar: aldığı value değerini branch ile when üzerinden eşleştirir ve true ya da false döndürür.

Üç durum deneniyor. Birincisi Point(1, 2) ile Point(1, 2): veri sınıfı, yani içerik ölçütü. İkincisi Box(1) ile Box(1): sıradan sınıf, yani kimlik. Üçüncüsü aynı box örneğinin kendisiyle eşleşmesi. Ardından aynı Box(1) değerine is Box dalı verilir. Son olarak Criterion üç gözlemi sınıflandırır.

// Main.kt — when dali == ile eslesir
class Box(val size: Int)

data class Point(val x: Int, val y: Int)

fun branchMatches(value: Any, branch: Any): Boolean = when (value) {
    branch -> true
    else -> false
}

fun main() {
    val box = Box(1)
    println(branchMatches(Point(1, 2), Point(1, 2)))
    println(branchMatches(Box(1), Box(1)))
    println(branchMatches(box, box))
    println(when (Box(1)) {
        is Box -> "is Box dali tutar"
        else -> "tutmaz"
    })
    val checks = listOf(
        Criterion("when-dali-box", Box(1), Box(1), Box(2)),
        Criterion("when-dali-point", Point(1, 2), Point(1, 2), Point(1, 3)),
        Criterion("aralik", 1..3, 1..3, 1..4),
    )
    for (check in checks) {
        check.report()
    }
    summarize(checks)
}
true
false
true
is Box dali tutar
when-dali-box: ayni-nesne=false, ikiz=false, farkli=false -> kimlik
when-dali-point: ayni-nesne=false, ikiz=true, farkli=false -> icerik
aralik: ayni-nesne=false, ikiz=true, farkli=false -> icerik
tekil: 0
kimlik: 1
icerik: 2
kismi-icerik: 0

branchMatches(Point(1, 2), Point(1, 2)) true verdi: veri sınıfında == içeriğe bakar, dal tuttu. branchMatches(Box(1), Box(1)) ise false verdi: iki Box(1) aynı kurucu argümanını taşısa da ayrı nesnedir ve sıradan sınıfta == kimliğe bakar. Aynı box örneği kendisiyle eşleşti, çünkü bir nesne kendisine eşittir. is Box dalı ise tipe bakar, değere değil: yeni kurulmuş bir Box(1) bile is Box dalını tutturur. Bu yüzden is Box dali tutar satırı basıldı.

Üç sınıflandırma satırı bunu özetler. when-dali-box kimliktir: ikiz ile == false verir. when-dali-point içeriktir: ikizle == true, farklı değerle false verir. aralik de içeriktir: 1..3 aralığı, ayrı kurulmuş başka bir 1..3 aralığına eşittir ve 1..4 aralığına eşit değildir. Özet satırlarında sıfır tekil, bir kimlik, iki içerik ve sıfır kısmi içerik görünür.

DT33. branchMatches fonksiyonu dalı bir değişken olarak alır; when içinde sabit bir dal yazılması durumunda aynı == kuralının geçerli olduğu bu derste ayrıca ölçülmedi.

DT34. Aralık eşitliği IntRange ile ölçüldü; LongRange ve CharRange için aynı davranış iddia edilmiyor.

when ile Aralık, Çok Değerli Dal ve Numaralandırma

when dalları yalnız tek bir değerle sınırlı değildir. Aşağıdaki blok üç şeyi sorar: virgülle ayrılmış çok değerli dal, in ile aralık dalı ve aralığın dışını yakalayan dal birlikte kullanılınca hangi sıra ile değerlendirilir; numaralandırma (enum) üzerindeki when else olmadan derlenir mi; ve Boolean üzerindeki when iki değerle kapanır mı.

// Main.kt — when: cok degerli dal, aralik ve numaralandirma
enum class Level { LOW, MEDIUM, HIGH }

fun classify(n: Int): String = when (n) {
    0 -> "sifir"
    1, 2, 3 -> "az"
    in 4..9 -> "orta"
    !in 0..99 -> "aralik disi"
    else -> "cok"
}

fun weight(level: Level): Int = when (level) {
    Level.LOW -> 1
    Level.MEDIUM -> 5
    Level.HIGH -> 10
}

fun main() {
    println(listOf(0, 2, 7, 50, 120).map { classify(it) })
    println(Level.entries.map { weight(it) })
    val flag = true
    println(when (flag) {
        true -> "evet"
        false -> "hayir"
    })
}
[sifir, az, orta, cok, aralik disi]
[1, 5, 10]
evet

Girdiler 0, 2, 7, 50 ve 120 için sonuç sifir, az, orta, cok, aralik disi oldu. 2 değeri 1, 2, 3 dalına, 7 ise in 4..9 dalına düştü. 50 değeri aralık dışı dalını tutturmadı ve else dalına gitti, 120 ise aralık dışı dalını tuttu. Kural şudur: dallar yukarıdan aşağıya sınanır ve ilk tutan çalışır. Aralık dışı dalı else dalından önce yazıldığı için 120 değeri ona ulaştı; else dalı daha önce yazılsaydı aralik disi dalına hiç ulaşılamazdı; bu ters sıra derlenmediği için ölçülemedi.

İkinci satır [1, 5, 10] verdi. weight fonksiyonu else dalı taşımıyor ve yine de derleniyor: derleyici Level numaralandırmasının üç değerini de dalların kapsadığını görür. Sık yapılan hata, numaralandırmaya yeni bir değer ekleyip when ifadelerini güncellemeyi unutmaktır; else dalı olmayan ifade bu durumda derleme hatası verir ve eksik dalı bulur. else yazılırsa bu güvence kaybolur. Bu davranış when ifade olarak kullanıldığı için geçerlidir. Java’da switch ifadesi de numaralandırmada kapsayıcılık denetler, ama eski switch deyimi denetlemez.

Üçüncü satır evet verdi: Boolean iki değerli bir tip olduğundan true ve false dalları else olmadan kapsayıcıdır.

DT35. Kapsayıcılık enum class ve Boolean ile ölçüldü; mühürlü sınıf üzerindeki when denetimi bu derste ölçülmedi.

Boş Aralık, Adım ve Etiketli break

Aralıklar kolay görünür ve sınır durumları nedeniyle hata üretir. Aşağıdaki blok boş bir aralığı sayar, step kullanan bir downTo ve .. kurar, sıfır adımı dener ve iç içe döngüden etiketli break ile çıkar.

// Main.kt — bos aralik, adim ve etiketli break
fun main() {
    println((5..1).count())
    println((1..0).toList())
    println((10 downTo 1 step 3).toList())
    println((1..10 step 4).last)
    try {
        println((1..3 step 0).toList())
    } catch (e: IllegalArgumentException) {
        println(e.message)
    }
    var visited = 0
    outer@ for (row in 1..3) {
        for (col in 1..3) {
            if (row * col == 4) break@outer
            visited++
        }
    }
    println(visited)
}
0
[]
[10, 7, 4, 1]
9
Step must be positive, was: 0.
4

(5..1).count() 0 verdi ve (1..0).toList() boş liste verdi: .. işleci küçükten büyüğe çalışır, başlangıç bitişten büyükse aralık boştur ve döngü hiç dönmez. Geriye sayılacaksa downTo yazılmalıdır. Sık yapılan hata budur: for (i in 5..1) yazan kişi geri sayım bekler, program sessizce hiçbir şey yapmaz.

10 downTo 1 step 3 listesi [10, 7, 4, 1] oldu. 1..10 step 4 aralığının last değeri 9 çıktı, yani 10 değil: adım üst sınıra tam oturmazsa son değer sınırın altında kalır. Sıfır adım ise Step must be positive, was: 0. iletisiyle IllegalArgumentException fırlattı; adım pozitif olmak zorundadır, geriye sayma downTo ile yapılır, negatif adımla değil.

Son satır 4 verdi. İç döngüde row * col == 4 olduğunda break@outer hem iç hem dış döngüyü bitirdi. Etiketsiz break yalnız en içteki döngüyü bitirirdi. Ziyaret sayısı, satır 1’de üç, satır 2’de bir ve satır 2’nin ikinci sütununda kırılma olduğundan toplam dörttür. Java’da etiketli break aynı biçimde çalışır; sözdizimi outer: ile break outer; şeklindedir.

DT36. Aralık ölçümleri Int ile yapıldı; Long aralığında adım ve taşma davranışı bu derste ölçülmedi.

try Bir İfadedir

Kotlin’de try de değer üretir. Aşağıdaki blok try ifadesinin değerini bir fonksiyonun dönüşü yapar, finally bloğunun ne zaman çalıştığına bakar ve bir if ifadesinin içinde fonksiyon çağrısını kullanır. Son olarak firstOrNull ile elvis işleci birlikte denenir.

// Main.kt — try bir ifadedir
fun parse(text: String): Int = try {
    text.toInt()
} catch (e: NumberFormatException) {
    -1
} finally {
    println("denendi: $text")
}

fun main() {
    println(parse("42"))
    println(parse("kotlin"))
    val label = if (parse("7") > 5) "buyuk" else "kucuk"
    println(label)
    val firstNegative = listOf(3, -2, 5).firstOrNull { it < 0 } ?: 0
    println(firstNegative)
}
denendi: 42
42
denendi: kotlin
-1
denendi: 7
buyuk
-2

İlk çağrı parse("42") önce finally bloğunun çıktısı olan denendi: 42 satırını, sonra dönüş değeri 42 satırını bastı: finally, değer çağırana ulaşmadan önce çalışır. İkinci çağrıda "kotlin" sayıya çevrilemediği için catch dalı çalıştı ve -1 döndü; yine denendi: kotlin önce basıldı. Üçüncü satırda parse("7") çağrısı if içinde kullanıldı: denendi: 7 basıldı ve 7 değeri 5’ten büyük olduğu için buyuk yazıldı.

Sık yapılan hata, finally bloğuna değer döndüren bir ifade koyup bunun try ifadesinin değerini değiştireceğini sanmaktır. Burada println son ifade olsa da finally bloğunun değeri ifadenin değerine katılmaz; döndürülen değer try ya da catch bloğunun son ifadesidir. Son satır -2 verdi: listede it < 0 koşulunu sağlayan ilk öğe -2 idi; hiçbiri olmasaydı elvis işleci 0 döndürürdü, ama bu dal ölçülmedi. Java’da try bir deyimdir ve değer üretmez.

DT37. try ölçümleri NumberFormatException ile yapıldı; finally bloğunda return yazılması halinde değerin nasıl ezildiği bu derste ölçülmedi.

Ölçümlerden Çıkan Alışkanlıklar

Akış denetiminde ölçümlerin ortak dersi, sınır durumlarının sessiz olmasıdır. Boş aralık hata vermez, yalnız hiçbir şey yapmaz. Adım değeri 1..10 step 4 gibi sınıra tam oturmazsa son değer sınırın altında kalır. Sıfır adım ise tek bir iletiyle istisna verir. Bu üç durumdan yalnız sonuncusu hata olarak görünür; ötekiler yanlış çıktı üretir.

when ifadesinde else dalının bulunmaması bir tasarım aracıdır. Numaralandırmada else yazmazsanız, yeni bir değer eklendiğinde derleyici sizi bütün when ifadelerine yönlendirir. else yazarsanız bu uyarıyı kendiniz kapatmış olursunuz. Bu nedenle else dalı, gerçekten “geri kalan her şey” anlamına gelen yerlerde, örneğin tam sayı aralıklarında kullanılmalıdır; kapalı bir değer kümesinde kullanılmamalıdır. Sıra konusunda da dikkat gerekir: dallar yukarıdan aşağıya sınandığı için özel dal genel daldan önce yazılır, ölçümde aralık dışı dalı else’ten önce olduğu için 120 değeri ona ulaştı.

try ifadesinde finally bloğunun değeri sonuca katılmaz, ama yan etkisi her zaman çalışır. Kaynak kapatma ve günlük yazma gibi işler bu yüzden finally içinde yapılır, değer hesabı ise try ve catch içinde. Java’da try bir deyimdir; değeri bir değişkene atamak için dışarıda bir değişken tanımlamak gerekir. Kotlin’de val result = try { ... } catch ... yazımı bu değişkeni yeniden atanamaz bir val yapar ve daha kısa bir kod verir.

Etiketli break iç içe döngülerden çıkmak için en okunur yöntemdir, ama kullanımı sınırlı tutulmalıdır. Çoğu durumda aramayı ayrı bir fonksiyona taşıyıp return ile çıkmak, etiket gerektirmez ve test edilmesi de kolaydır.

Sınırlayıcı Ölçüm

Kimlik ölçütü yalnız == ile eşleştirilen dallar için geçerlidir. is Box dalı tutar, çünkü tip denetimi bir eşitlik sorusu değildir. Yani aynı when içinde iki dal yan yana durabilir: biri kimlik yüzünden tutmaz, ötekisi tip yüzünden tutar. Bir Box(1) örneğini ikinci kez kurup when içinde eşleştirmeye çalışan program, Box bir veri sınıfı olmadığı sürece dalı hiç tutturamaz; ama is Box yazıldığında dal tutar.

İkinci sınır aralıklardadır: 1..3 kimlik ölçütüne düşmez. İki aralık nesnesi ayrı kurulmuş olsa da eşittir. Bir aralığın eşitliği ile in dalının tutması farklı sorulardır: ilki bu bölümde, ikincisi when bölümünde ölçüldü.

Özet

  • if ve when değer üreten ifadelerdir; when ifade olarak kullanıldığında bütün durumlar kapsanmalıdır; bunu else dalı ya da numaralandırma, Boolean veya mühürlü sınıf öznesinde tüm durumların sayılması sağlar.
  • is bir tip denetimidir ve dal içinde değeri o tipin değişkeni olarak kullanmaya izin verir.
  • .., until, downTo ve step aralıkları kurar; for bu aralıkları dolaşır.
  • when (value) içindeki bir dal value == dal ile eşleşir; sıradan sınıf örneği kimliğe dayandığı için dalı tutturamaz.
  • Bu dersin ölçütü: kimlik; sınırlayıcı ölçüm: is Box dalı tutar ve iki 1..3 aralığı içerik ölçütüne düşer.

Sonraki Adım

Akışı gördük; şimdi akışı paketlemenin aracına geçiyoruz: fonksiyonlar. Varsayılan değerli parametreler, adlandırılmış argümanlar ve vararg ile başlıyoruz. Aynı fonksiyonu iki kez referans olarak almak, aynı nesneyi mi verir, yoksa içerik mi, onu ölçeriz.

İlerlemeni kaydetmek ve not almak için Giriş yap

Notlarım

Not almak için giriş yapmalısın.

Aramak için yazmaya başlayın.

↑↓ ↵ Esc gezin · aç · kapat