İçeriğe geç
academia.sh

Ders 08 / 23

Kapsam Fonksiyonları

`let`, `run`, `with`, `apply` ve `also` kapsam fonksiyonlarını tanıtır; alıcının `this` mi `it` mi olduğunu ve dönüş değerini ölçer, sonucun eşitlik ölçütünü değiştirmediğini gösterir.

Yayın Güncelleme

Bu derste

  • `let`, `run`, `with`, `apply` ve `also` fonksiyonlarını alıcı ve dönüş değeri açısından ayırt etmek
  • Bir ifadenin hangi nesne üzerinde çalıştığını (`this` ya da `it`) okuyabilmek
  • Kapsam fonksiyonunun döndürdüğü nesnenin eşitlik ölçütünü değiştirmediğini ölçmek
İçindekiler

Önceki derste lambdaları değer olarak taşıdık. Kapsam fonksiyonları (scope functions) bu lambdaları bir nesnenin bağlamında çalıştıran beş hazır yüksek mertebeden fonksiyondur: let, run, with, apply ve also. Aralarındaki fark iki sorudan çıkar: nesneye lambdanın içinde nasıl ulaşılır ve fonksiyon ne döndürür? Dersin sonunda bir üçüncü soru geliyor: kapsam fonksiyonundan geçen bir nesnenin eşitlik ölçütü değişir mi?

Alıcı ve Dönüş Değeri

Bir kapsam fonksiyonu, üzerinde çağrıldığı nesneyi lambdaya iki yoldan verir. let ve also nesneyi lambda parametresi it olarak verir. run, with ve apply ise nesneyi alıcı (receiver) yapar: lambdanın içinde nesnenin üyelerine this ile, çoğunlukla da hiçbir önek yazmadan ulaşılır. Bu ayrım, bir ifadenin hangi nesne üzerinde çalıştığını okurken ilk bakılacak yerdir.

Dönüş değeri iki biçimdedir. let, run ve with lambdanın son ifadesinin sonucunu döndürür. apply ve also ise nesnenin kendisini döndürür. Yani apply bir nesneyi kurarken, also nesneye dokunmadan yan bir iş yaparken kullanılır.

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 }}")
    }
}

Bu seçim bir okunabilirlik kararıdır. Lambdanın içinde nesnenin üyelerini önek yazmadan kullanmak istiyorsanız alıcı yazımlı üç fonksiyondan biri uygundur; nesneyi bir ad altında başka bir şeye argüman olarak geçirecekseniz it yazımlı ikisi uygundur. Dönüş değeri ise bloktan sonra elinizde ne kalacağına karar verir: nesnenin kendisi mi, hesaplanmış bir sonuç mu.

Aşağıdaki program beş fonksiyonu sırayla çalıştırır. Point iki değiştirilebilir alanı olan bir veri sınıfıdır.

// Main.kt — let, run, with, apply ve also
data class Point(var x: Int, var y: Int)

fun main() {
    val letResult = Point(1, 2).let { it.x + it.y }
    val runResult = Point(3, 4).run { x * y }
    val withResult = with(Point(5, 6)) { "$x,$y" }
    val applied = Point(0, 0).apply {
        x = 7
        y = 8
    }
    val alsoResult = Point(9, 9).also { println("gozlem: ${it.x}") }
    println(letResult)
    println(runResult)
    println(withResult)
    println(applied)
    println(alsoResult)
}
gozlem: 9
3
12
5,6
Point(x=7, y=8)
Point(x=9, y=9)

İlk satır also içindeki println çağrısından geldi ve gozlem: 9 yazdı; also bloğu nesneyi gözlemledi ve nesne olduğu gibi döndü. let bloğunda iki alan it üzerinden toplandı ve 3 çıktı. run bloğunda x * y alıcıyla, önek olmadan yazıldı ve 12 çıktı. with de aynı alıcı yazımını kullandı ve lambdanın sonucu olan "5,6" metnini verdi. apply bloğunda alanlar atandı ve fonksiyon nesnenin kendisini döndürdü: Point(x=7, y=8). Son satır alsoın döndürdüğü nesneyi gösterir: Point(x=9, y=9).

DT52. Beş fonksiyon yalnız Point üzerinde ve yalnız tam sayı alanlarla ölçüldü; boş değer alabilen alıcı üzerindeki ?.let, ?.apply ve takeIf kullanımı dersin ilerleyen bölümünde yalnız String? ile ölçülür.

DT53. with bir uzantı değil, nesneyi argüman olarak alan sıradan bir fonksiyondur; bu ayrım burada yalnız söylendi, ölçülmedi.

Kapsam Fonksiyonu Ölçütü Değiştirmez

Ölçüm şunu sorar: bir nesneyi kapsam fonksiyonundan geçirmek, ikizinin eşit sayılıp sayılmayacağını değiştirir mi? Burada Point değişmeyen alanlı bir veri sınıfıdır. Üç gözlem yazıyoruz. let-copy: Point(1, 2).let { it.copy() }. run-copy: Point(1, 2).run { copy() }; aynı kopyalama, bu sefer alıcı yazımıyla. also-ikiz: aynı source değişkeni also { } ile geçirilir ve kendisiyle karşılaştırılır.

let ve run lambdanın sonucunu döndürdüğü için döndürülen şey copy() ile kurulan ayrı bir nesnedir. also ise kendi nesnesini döndürür.

// Main.kt — kapsam fonksiyonunun dondurdugu
data class Point(val x: Int, val y: Int)

fun main() {
    val source = Point(1, 2)
    val checks = listOf(
        Criterion("let-copy", Point(1, 2), Point(1, 2).let { it.copy() }, Point(1, 3)),
        Criterion("run-copy", Point(1, 2), Point(1, 2).run { copy() }, Point(1, 3)),
        Criterion("also-ikiz", source, source.also { }, Point(1, 3)),
    )
    for (check in checks) {
        check.report()
    }
    summarize(checks)
}
let-copy: ayni-nesne=false, ikiz=true, farkli=false -> icerik
run-copy: ayni-nesne=false, ikiz=true, farkli=false -> icerik
also-ikiz: ayni-nesne=true, ikiz=true, farkli=false -> tekil
tekil: 1
kimlik: 0
icerik: 2
kismi-icerik: 0

let-copy ve run-copy içeriktir: ayni-nesne=false, ikiz=true. Kapsam fonksiyonundan çıkan kopya, kaynakla aynı nesne değildir ama aynı değerleri taşıdığı için veri sınıfının equals’i onları eşit sayar. Hangi kapsam fonksiyonunun kullanıldığı ölçütü belirlemedi; ölçütü belirleyen Pointın veri sınıfı olmasıdır.

also-ikiz tekildir: ayni-nesne=true. also nesnenin kendisini döndürdüğü için ikiz ile birinci değer aynı nesne çıktı. Özet satırları bir tekil, sıfır kimlik, iki içerik ve sıfır kısmi içerik verir.

DT54. also-ikiz gözleminde ikiz kurulmadı; kapsam fonksiyonu nesnenin kendisini döndürdüğü için karşılaştırma bir nesnenin kendisiyle yapıldı. Bu gözlem ölçüm koşulunu bozar, aşağıda açıklanıyor.

DT55. copy() çağrısı yüzeysel kopya üretir; iç içe nesne taşıyan bir veri sınıfında kopyanın nasıl davrandığı bu derste ölçülmedi.

Beş Fonksiyon Neyi Döndürür

Kapsam fonksiyonları arasındaki asıl fark ikiye iner: nesneye it ile mi this ile mi erişilir ve ifade neyi döndürür. Aşağıdaki blok aynı Point nesnesi üzerinde beşini çalıştırır ve dönen nesneyi === ile kaynağa karşı sınar.

// Main.kt — kapsam fonksiyonlari neyi dondurur
data class Point(var x: Int, var y: Int)

fun main() {
    val p = Point(1, 2)
    println(p.let { it } === p)
    println(p.run { this } === p)
    println(with(p) { this } === p)
    println(p.apply { x = 10 } === p)
    println(p.also { it.y = 20 } === p)
    println(p.let { 5 })
    println(p.apply { x = 11 }.run { x + y })
    println(p)
}
true
true
true
true
true
5
31
Point(x=11, y=20)

Beş satırın hepsi true verdi, ama beşi aynı şey için değil. p.let { it } === p ve p.run { this } === p doğrudur çünkü lambdanın son ifadesi nesnenin kendisidir. with(p) { this } === p aynı sebeple doğrudur. apply ve also ise lambdanın değerine bakmaz, her zaman alıcı nesneyi döndürür.

Altıncı satır 5 verdi: let { 5 } ifadesinin değeri lambdanın son ifadesidir, nesne değil. Yedinci satır 31: apply { x = 11 } nesneyi döndürdü, run { x + y } ise toplamı hesapladı, 11 ile 20’nin toplamı 31. Son satır Point(x=11, y=20) verdi, yani önceki beş çağrıdaki atamalar nesnenin kendisini değiştirdi. Bu atamalar yerel bir kopyada değil, p üzerinde yapılmıştı.

Kural: let, run ve with lambdanın sonucunu verir; apply ve also nesneyi verir. Sık yapılan hata, apply içinde son satıra bir değer yazıp onun döneceğini sanmaktır. Java’da buna denk bir yapı yoktur; zincirleme için yöntemlerin this döndürmesi (builder kalıbı) gerekir.

DT56. Dönüş değerleri Point ve tam sayı sonuçlarla ölçüldü; with bir uzantı değil sıradan bir fonksiyondur, ama bu ayrım ölçülmedi.

Null ile Kapsam Fonksiyonları ve takeIf

En sık kullanım ?.let ile boş olabilecek bir değeri güvenle işlemektir. Aşağıdaki blok bunu dolu ve boş bir String? ile dener, ?.let ile .let { it?.... } arasındaki farka bakar ve takeIf ile takeUnless fonksiyonlarını sınar.

// Main.kt — null ile kapsam fonksiyonlari ve takeIf
fun main() {
    val present: String? = "kotlin"
    val missing: String? = null
    println(present?.let { it.length })
    println(missing?.let { it.length })
    println(missing.let { it?.length })
    println(missing?.apply { length })
    println(missing?.let { it.length } ?: -1)
    println(present.takeIf { it?.startsWith("k") == true })
    println(present.takeUnless { it?.startsWith("k") == true })
    println("abc".takeIf { it.length > 5 })
}
6
null
null
null
-1
kotlin
null
null

present?.let { it.length } 6 verdi; missing?.let { it.length } ise null. Güvenli çağrı ?. alıcı boşsa lambdayı hiç çalıştırmadı. Üçüncü satır da null verdi ama sebebi farklıdır: missing.let { it?.length } ifadesinde let boş değer üzerinde de çağrılır, lambda çalışır ve içeride it?.length boş sonuç üretir. Bu iki biçimin sonucu aynı olsa bile lambda birincisinde çalışmadı, ikincisinde çalıştı. Dördüncü satır null: missing?.apply { length } de alıcı boş olduğu için hiçbir şey çalıştırmadı, sonuç boş değerdir. Beşinci satır -1: elvis işleci boş sonucu varsayılana çevirdi.

takeIf koşul doğruysa nesneyi, yanlışsa null döndürür. present.takeIf { ... } kotlin verdi, çünkü dizgi k ile başlıyor. takeUnless ters çalışır ve aynı koşulla null verdi. "abc".takeIf { it.length > 5 } null verdi, çünkü koşul sağlanmadı. Sık yapılan hata, takeIf sonucunu boş olabilir tipte olduğunu unutarak doğrudan kullanmaktır; derleyici bu durumda derleme hatası verir, ama bu hata ölçülmedi.

DT57. Boş değer ölçümü String? ile yapıldı; takeIf koşulunun yan etki taşıması ya da takeIf zincirleri bu derste ölçülmedi.

İç İçe Kapsam Fonksiyonları ve this

İki kapsam fonksiyonu iç içe yazıldığında this ve it hangi nesneye bakar? Aşağıdaki blok bir run’ın içinde ikinci bir run çalıştırır, dıştaki alıcıya etiketle erişir, ardından run ile nesnesiz bir blok kullanır ve iç içe apply ile size okunur.

// Main.kt — ic ice kapsam fonksiyonlari ve this
data class Box(var size: Int)

fun main() {
    val box = Box(1)
    val result = box.run outer@{
        size = 5
        Box(size * 2).run { "ic: $size, dis: ${[email protected]}" }
    }
    println(result)
    val note = run {
        val temp = 21
        temp * 2
    }
    println(note)
    Box(3).apply {
        Box(4).apply {
            println(size)
        }
        println(size)
    }
}
ic: 10, dis: 5
42
4
3

İlk satır ic: 10, dis: 5 verdi. Dış run’da size = 5 atandı, iç run’da size * 2 ile yeni bir Box(10) kuruldu. İç blokta size en yakın alıcıya, yani Box(10) nesnesine bakar. Dış alıcıya erişmek için [email protected] yazılır ve 5 okunur. İç içe alıcı kullanımı bu yüzden okunması zor kod üretir; also ve let ile it adını vermek daha açıktır. Sık yapılan hata, iç blokta size yazıp dış nesnenin alanını okuduğunu sanmaktır.

İkinci satır 42 verdi: nesnesiz run { } yalnız bir blok açıp son ifadeyi döndürür. Üçüncü ve dördüncü satırlar 4 ve 3 verdi: iç apply içinde size iç nesnenin 4 değeridir, dış apply içinde ise dış nesnenin 3 değeri. İç blok bitince kapsam dışa döner. Java’da aynı etkiyi iç sınıflarda Dis.this.alan yazımı verir; Kotlin’de etiket bunun karşılığıdır.

DT58. İç içe alıcı ölçümü iki düzeyle yapıldı; üç ya da daha derin iç içe kullanımda etiketsiz this çözümü bu derste ölçülmedi.

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

Kapsam fonksiyonlarını seçerken iki soruya bakılır: nesneye it mi this mi ile erişilecek ve sonuç nesne mi lambdanın değeri mi olacak. Ölçüm tabloyu doğruladı: apply ve also nesneyi döndürür, let, run ve with lambdanın değerini. Pratikte apply nesne kurarken, also yan etki (günlük yazma gibi) eklerken, let boş olabilecek değeri dönüştürürken, run ve with ise nesne üzerinde bir hesap yapıp sonuç alırken kullanılır. Bu eşleşme bir kural değil, yaygın bir alışkanlıktır; aynı iş çoğu zaman başka bir fonksiyonla da yapılabilir.

Aşırı kullanım gerçek bir risktir. İç içe üç kapsam fonksiyonu, this ve it adlarının hangi nesneye gittiğini izlemeyi zorlaştırır. Ölçümde iç run içindeki size değeri iç nesneye, dış değere erişim ise [email protected] ile yapıldı. İç içe iki düzeyi aşan kullanımda adlandırılmış yerel değişkenler daha açıktır. Kural olarak, bir kapsam fonksiyonunun içine bir başkası yazılıyorsa, ya dıştaki it ile adlandırılmalı ya da kod ayrı bir fonksiyona taşınmalıdır.

Boş değerlerle ?.let yaygın kalıptır ve ölçüm iki biçim arasındaki farkı gösterdi: missing?.let { ... } lambdayı hiç çalıştırmaz, missing.let { it?.... } ise lambdayı boş değerle çalıştırır. Sonuçlar aynı görünse de yan etki taşıyan bir lambdada fark ortaya çıkar. takeIf ve takeUnless ise bir koşulu zincire sokar: koşul sağlanmazsa null döner ve sonraki ?. adımları atlanır.

Bir diğer sık hata, apply bloğunun son satırına bir değer yazıp onun döneceğini sanmaktır. apply her zaman nesneyi döndürür, bu yüzden sondaki ifade göz ardı edilir. Sonucu başka bir değere çevirmek istiyorsanız let ya da run seçilmelidir. Java’da bu işlerin karşılığı builder kalıbıdır ve her yöntemin this döndürmesine dayanır; Kotlin bunu dil düzeyinde, mevcut sınıfı değiştirmeden sağlar.

Son olarak, with bir uzantı değil, nesneyi argüman alan sıradan bir fonksiyondur. Bu yüzden boş olabilecek bir nesneye ?. ile bağlanamaz; boş değerli alıcıda ?.run ya da ?.let seçilmelidir.

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

also-ikiz gözlemi ölçüm koşulunu bozar: Criterion bir değerin ayrı kurulmuş ikizini bekler, oysa also aynı nesneyi geri verir. Çıkan tekil sonuç ölçütün bir bulgusu değil, fonksiyonun dönüş değeri kuralının yansımasıdır. Bu yüzden kapsam fonksiyonunun ölçütü değiştirmediğini göstermek için let ve run gözlemleri anlamlıdır, also gözlemi ise yalnız sınırı gösterir. Bir ifadede kapsam fonksiyonu görürseniz şu iki soruyu sorun: bu ifade hangi nesne üzerinde çalışıyor ve fonksiyon bu nesneyi mi yoksa lambdanın sonucunu mu döndürüyor? Eşitlik sorusunun yanıtı hep nesnenin kendi tipinde kalır.

Özet

  • let ve also nesneyi it olarak, run, with ve apply alıcı olarak (this) verir.
  • let, run ve with lambdanın sonucunu, apply ve also nesnenin kendisini döndürür.
  • Point(1, 2).let { it.copy() } ve run { copy() } ayrı ama eşit nesne verdi: içerik.
  • also aynı nesneyi geri verdiği için also-ikiz tekil çıktı ve ölçüm koşulunu bozdu.
  • Bu dersin ölçütü: içerik; sınırlayıcı ölçüm: also ikizi tekildir ve ikiz kurmadığı için ölçüm koşulunu bozar.

Sonraki Adım

Dil temelleri burada kapanıyor. Sonraki konuda eşitlik sorusuna null giriyor: bir değer hiç olmayabildiğinde null ile null aynı mıdır ve derleyici bu durumu tipin içinde nasıl izler? Konu, Null Güvenliği dersiyle başlıyor.

İ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