İçeriğe geç
academia.sh

Ders 06 / 23

Fonksiyonlar

Varsayılan değerli parametreleri, adlandırılmış argümanları ve `vararg` parametresini tanıtır; fonksiyon referansının her alınışta ayrı ama eşit bir nesne verdiğini ölçer.

Yayın Güncelleme

Bu derste

  • `fun` ile fonksiyon bildirmek; varsayılan değerli parametre, adlandırılmış argüman ve `vararg` kullanmak
  • Varsayılan değerin her çağrıda yeniden hesaplandığını ve bunun eşitlik ölçütüne etkisini açıklamak
  • Fonksiyon referansı almanın ayrı ama eşit nesneler verdiğini ölçmek
İçindekiler

Denetim akışında bir değeri dallara dağıtmayı ve bir aralığı dolaşmayı gördük. Bu derste o akışı adlandırılmış, yeniden kullanılabilir parçalara paketliyoruz: fonksiyonlar. Önce bir fonksiyonun parametrelerini nasıl esnetebileceğimize bakıyoruz, sonra aynı soruyu fonksiyonların kendisine soruyoruz: aynı fonksiyonu iki kez bir değer olarak aldığımızda iki eşit nesne mi elde ederiz, yoksa tek nesne mi?

Parametreleri Esnetmek

Kotlin’de fonksiyon fun anahtar sözcüğüyle bildirilir: ad, parantez içinde ad: Tip biçiminde parametreler, ardından iki nokta ve dönüş tipi gelir. Gövde tek bir ifadeyse = ile yazılabilir. Üç özellik parametre listesini esnetir.

Birincisi varsayılan değerdir: bir parametreye = değer eklenirse çağıran onu atlayabilir. İkincisi adlandırılmış argümandır (named argument): çağrıda punctuation = "?" yazılırsa değer sıraya göre değil adına göre bağlanır ve aradaki parametreler varsayılanlarında kalır. Üçüncüsü vararg parametresidir: bir parametre vararg ile işaretlenirse çağıran istediği kadar değer verebilir, fonksiyon içinde bunlar bir dizi olarak görünür. Elindeki bir diziyi vararg parametresine vermek için dizinin önüne * konur.

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

Varsayılan değer bir sabit olmak zorunda değildir; herhangi bir ifade olabilir. Çağıran parametreyi atladığında o ifade çağrı anında hesaplanır, bu yüzden aynı fonksiyonun iki çağrısı varsayılandan iki ayrı sonuç alabilir. Adlandırılmış argüman da yalnız sıra kolaylığı değildir: çağrıyı okuyan biri punctuation = "?" yazımından değerin neye ait olduğunu fonksiyonun bildirimine bakmadan anlar. İkinci bölümde varsayılan değerin bu davranışını ölçeceğiz.

Aşağıdaki program üç özelliği birlikte kullanır. greet iki varsayılan değerli parametre taşır, sum ise vararg alır.

// Main.kt — varsayilan deger, adlandirilmis arguman ve vararg
fun greet(name: String, greeting: String = "Merhaba", punctuation: String = "."): String =
    "$greeting, $name$punctuation"

fun sum(vararg numbers: Int): Int = numbers.sum()

fun main() {
    println(greet("Ada"))
    println(greet("Ada", punctuation = "?"))
    println(greet(greeting = "Selam", name = "Can"))
    println(sum())
    println(sum(1, 2, 3))
    val values = intArrayOf(4, 5)
    println(sum(*values, 6))
}
Merhaba, Ada.
Merhaba, Ada?
Selam, Can.
0
6
15

İlk iki satır adı tek başına verilen ve noktalamayı adlandırılmış argümanla değiştirilen çağrıları gösterir: greeting ve punctuation atlandığında Merhaba ile nokta kullanıldı, punctuation = "?" yazıldığında yalnız noktalama değişti. Üçüncü satırda argümanlar sıra dışı, adlarıyla verildi ve Selam, Can. çıktı. Son üç satır vararg içindir: hiç değer verilmediğinde toplam 0, üç değerde 6 oldu; sum(*values, 6) ise dizinin iki öğesini ve ek altıyı birlikte topladı ve 15 verdi.

DT38. Varsayılan değerler ve vararg yalnız String ve Int parametreleriyle ölçüldü; türü bir sınıf olan parametreler için aynı davranış bu derste ayrıca sınanmadı.

DT39. Adlandırılmış argümanla sıra değiştirme yalnız bir çağrıda gösterildi; adlandırılmış ve konumsal argümanların aynı çağrıda karıştırılmasının kuralları bu dersin dışındadır.

Fonksiyon Referansı ve Varsayılan Değer

Dersin ölçümü iki şeye bakar. Birincisi fonksiyon referansıdır (function reference): ::plain yazımı, plain fonksiyonunu çağırmadan bir değer olarak verir. Aynı yazımı iki kez kullanmak aynı nesneyi mi verir, ayrı nesneleri mi? İkincisi varsayılan değerdir: into: MutableList<Int> = mutableListOf() varsayılanı her çağrıda yeni bir liste mi kurar, yoksa tek bir listeyi mi paylaşır?

collectMutable bir öğeyi aldığı listeye ekler ve listeyi döndürür; liste verilmezse varsayılan olan boş değiştirilebilir liste kullanılır. defaultList ise listOf() varsayılanını doğrudan döndürür. Üç gözlem yazıyoruz: fonksiyon-referansi, iki ::plain ve farklı olarak ::other; varsayilan-mutableListOf, collectMutable(1) iki kez ve collectMutable(2) farklı değer; varsayilan-listOf, defaultList() iki kez ve listOf(1) farklı değer.

// Main.kt — fonksiyon referansi ve varsayilan degerler
fun plain(n: Int): Int = n

fun other(n: Int): Int = n + 1

fun collectMutable(item: Int, into: MutableList<Int> = mutableListOf()): MutableList<Int> {
    into.add(item)
    return into
}

fun defaultList(into: List<Int> = listOf()): List<Int> = into

fun main() {
    val checks = listOf(
        Criterion("fonksiyon-referansi", ::plain, ::plain, ::other),
        Criterion("varsayilan-mutableListOf", collectMutable(1), collectMutable(1), collectMutable(2)),
        Criterion("varsayilan-listOf", defaultList(), defaultList(), listOf(1)),
    )
    for (check in checks) {
        check.report()
    }
    summarize(checks)
}
fonksiyon-referansi: ayni-nesne=false, ikiz=true, farkli=false -> icerik
varsayilan-mutableListOf: ayni-nesne=false, ikiz=true, farkli=false -> icerik
varsayilan-listOf: ayni-nesne=true, ikiz=true, farkli=false -> tekil
tekil: 1
kimlik: 0
icerik: 2
kismi-icerik: 0

fonksiyon-referansi satırında ayni-nesne=false ve ikiz=true görünür: iki ::plain yazımı ayrı nesne verdi, ama bu iki nesne == ile eşit çıktı. Ayrı nesne, eşit içerik: içerik ölçütü. varsayilan-mutableListOf da içeriktir. İki çağrı iki ayrı liste kurdu, ikisi de [1] taşıdığı için eşit. Varsayılan değer fonksiyon her çağrıldığında yeniden hesaplanır; bir çağrıda eklenen öğe sonraki çağrıya sızmaz.

varsayilan-listOf ise tekildir: ayni-nesne=true. Boş listOf() çağrısı iki seferde aynı nesneyi verdi. Özet satırları bir tekil, sıfır kimlik, iki içerik ve sıfır kısmi içerik gösterir.

DT40. Fonksiyon referansı eşitliği yalnız üst düzey fonksiyonlarla ölçüldü; bir nesneye bağlı referansın (nesne::ad) nasıl eşitlendiği bu derste ölçülmedi.

DT41. Boş listOf() çağrısının tek nesne vermesi Kotlin 2.4.21’in JVM hedefinde ölçüldü; bu davranış listenin gerçekleştirimine bağlı olduğundan başka sürümler için aynısı iddia edilmiyor.

Varsayılan Değer Her Çağrıda Hesaplanır

Varsayılan değerin bir ifade olabildiğini gördük. Asıl soru ifadenin ne zaman çalıştığıdır: fonksiyon tanımlanırken bir kez mi, yoksa her çağrıda mı? Aşağıdaki blok varsayılan değeri bir sayaç artıran fonksiyondan alır ve bir parametrenin varsayılanının öncekine bağlı olabildiğini de dener.

// Main.kt — varsayilan deger her cagrida hesaplanir
var counter = 0

fun nextId(): Int {
    counter++
    return counter
}

fun label(text: String, id: Int = nextId(), width: Int = text.length): String =
    "$text#$id/$width"

fun main() {
    println(label("ab"))
    println(label("abc"))
    println(label("abc", id = 100))
    println(label("abc", width = 1))
    println(counter)
}
ab#1/2
abc#2/3
abc#100/3
abc#3/1
3

İlk çağrı ab#1/2 verdi: id için nextId() çalıştı ve 1 üretti, width varsayılanı text.length olduğu için 2 oldu. İkinci çağrı abc#2/3 verdi; sayaç ikinci kez arttı. Demek ki varsayılan değer her çağrıda yeniden hesaplanır. Üçüncü çağrıda id açıkça 100 verildi ve abc#100/3 basıldı; bu durumda nextId() hiç çalışmadı. Dördüncü çağrı abc#3/1 verdi: width adlandırılmış argümanla 1 oldu, id için sayaç yeniden çalıştı ve 3 üretti. Son satır counter değerini 3 olarak verdi: dört çağrıdan yalnız üçü varsayılan id kullandı.

Bu iki şeyi kanıtlar. Birincisi, varsayılan ifade çağrı anında çalışır; yan etkisi varsa her çağrıda tekrar eder. İkincisi, varsayılanlar kendinden önceki parametrelere erişebilir (width, text‘e bakar). Sık yapılan hata, varsayılan değerin tanım anında dondurulduğunu sanmaktır; Python’da değiştirilebilir varsayılan değerin tanımda bir kez hesaplanması bilinen bir tuzaktır, Kotlin’de böyle bir tuzak yoktur. Java’da varsayılan parametre yoktur; aynı etki aşırı yükleme ile elde edilir.

DT42. Varsayılan değer ölçümü üst düzey bir sayaçla yapıldı; çok iş parçacıklı bir ortamda sayaç davranışı bu derste ölçülmedi.

Unit, Nothing ve Yerel Fonksiyon

Dönüş değeri yazılmayan bir fonksiyon aslında bir şey döndürür. Aşağıdaki blok Unit dönen bir fonksiyonu yazdırır, Nothing dönen bir fonksiyonu try içinde yakalar ve başka fonksiyonun içine yerleştirilmiş bir fonksiyon kullanır.

// Main.kt — Unit, Nothing ve yerel fonksiyon
fun nothingToReturn() {
}

fun fail(message: String): Nothing = throw IllegalStateException(message)

fun total(values: List<Int>): Int {
    fun square(n: Int) = n * n
    var sum = 0
    for (v in values) {
        sum += square(v)
    }
    return sum
}

fun main() {
    println(nothingToReturn())
    println(nothingToReturn() === Unit)
    println(total(listOf(1, 2, 3)))
    val result = try {
        fail("durdu")
    } catch (e: IllegalStateException) {
        e.message
    }
    println(result)
}
kotlin.Unit
true
14
durdu

İlk satır kotlin.Unit verdi: bir şey döndürmediğini sandığımız fonksiyon aslında Unit adlı tekil nesneyi döndürür ve println bunu yazdırabilir. İkinci satırdaki true, nothingToReturn() === Unit karşılaştırmasının doğru olduğunu gösterir: Unit tek bir nesnedir. Bu, Java’daki void ile temel bir farktır; void bir değer değildir, Unit ise gerçek bir değerdir ve bu yüzden genel tiplerde Unit kullanılabilir.

Üçüncü satır 14, yani 1, 4 ve 9’un toplamıdır. square fonksiyonu total içinde tanımlıdır ve yalnız oradan çağrılabilir. Yerel fonksiyon dış fonksiyonun değişkenlerine erişebilir; burada kullanılmadı ve erişim ölçülmedi.

Son satır durdu verdi. fail fonksiyonunun dönüş tipi Nothing: hiçbir zaman normal biçimde dönmez, hep istisna fırlatır. Derleyici bunu bilir; fail(...) çağrısı try içinde değer üretmek zorunda olduğu için yakalanan istisnanın iletisi sonuç oldu. Sık yapılan hata Nothing tipini Unit ile karıştırmaktır. Unit döner ve devam eder, Nothing ise hiç dönmez.

DT43. Nothing ölçümü bir istisna fırlatan fonksiyonla yapıldı; sonsuz döngü ya da exitProcess çağıran fonksiyonların Nothing ile işaretlenmesi bu derste ölçülmedi.

Aşırı Yükleme Seçimi ve Uzantı Fonksiyonu

Aynı adı taşıyan fonksiyonlar parametre tipine göre seçilir. Aşağıdaki blok dört show fonksiyonu tanımlar ve beş farklı çağrıyla hangisinin seçildiğine bakar. Sonra bir Int ve bir String üzerine uzantı fonksiyonu yazar ve bir MutableList parametresinin çağıran tarafı nasıl etkilediğini gösterir.

// Main.kt — asiri yukleme secimi, uzanti fonksiyonu ve parametre
fun show(x: Int) = "Int"
fun show(x: Long) = "Long"
fun show(x: Any) = "Any"
fun show(x: String) = "String"

fun Int.isEven(): Boolean = this % 2 == 0

fun String.initials(): String = split(" ").joinToString("") { it.take(1).uppercase() }

fun modify(list: MutableList<Int>) {
    list.add(99)
}

fun main() {
    println(show(1))
    println(show(1L))
    println(show("a"))
    val any: Any = "a"
    println(show(any))
    println(show(1.5))
    println(4.isEven())
    println("ada lovelace".initials())
    val nums = mutableListOf(1)
    modify(nums)
    println(nums)
}
Int
Long
String
Any
Any
true
AL
[1, 99]

show(1) Int, show(1L) Long, show("a") String verdi: derleyici en özel eşleşmeyi seçer. Dördüncü satır ilginçtir. any değişkeni çalışma anında bir String taşır, ama tipi Any olarak bildirildiği için show(any) Any sürümünü seçti. Aşırı yükleme çözümü derleme anında, değişkenin bildirilen tipine göre yapılır; çalışma anı tipi seçimi etkilemez. Java’da da aşırı yükleme derleme anında çözülür; sanal yöntem çağrısı ise çalışma anı tipine göre çözülür ve ikisi karıştırılmamalıdır. show(1.5) yalnız Any sürümüne uyduğu için Any verdi.

4.isEven() true verdi ve "ada lovelace".initials() AL verdi: uzantı fonksiyonu mevcut bir tipe, tipin kendisini değiştirmeden yeni bir çağrı biçimi ekler. Uzantı fonksiyonu statik olarak çözülür; bir sınıfın gerçek bir üyesi değildir.

Son satır [1, 99] verdi. modify fonksiyonu parametre olarak aldığı listeye ekleme yaptı ve çağıran taraf bunu gördü. Kotlin’de referansın kendisi değer olarak kopyalanır: nesne paylaşılır, ama fonksiyon çağıranın değişkenini başka bir nesneye bağlayamaz; parametre değişkeni de zaten yeniden atanamaz. Sık yapılan hata, fonksiyona geçen koleksiyonun içinde değişiklik yapıp bunun yerel kalacağını sanmaktır.

DT44. Aşırı yükleme seçimi dört tanımla ve beş çağrıyla ölçüldü; Int ve Long yanında Short ya da Byte argümanlarının hangi sürüme gittiği bu derste ölçülmedi.

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

Fonksiyon tanımında ölçümlerin gösterdiği ilk şey, varsayılan değerin bir ifade olduğu ve her çağrıda yeniden çalıştığıdır. Bu, Python’daki değiştirilebilir varsayılan tuzağını Kotlin’de ortadan kaldırır, ama başka bir riski doğurur: varsayılan ifadenin yan etkisi varsa, çağrı sayısı program davranışını etkiler. Ölçümde counter değeri dört çağrıdan sonra üç oldu, çünkü yalnız üç çağrıda varsayılan değer kullanıldı. Varsayılan ifadeleri yan etkisiz tutmak, bu yüzden iyi bir alışkanlıktır; sayaç artırmak gibi işler açık bir çağrı olarak gövdede yapılmalıdır.

Unit ve Nothing ayrımı tip sisteminin kullanışlı bir parçasıdır. Unit döndüren fonksiyonun tekil bir değer döndürdüğünü ölçtük; bu yüzden Unit genel tiplerde sorunsuz kullanılır. Nothing ise hiç dönmeyen ifadenin tipidir ve bu sayede val x = y ?: fail("yok") gibi bir yazım x’in boş olmayan tipte kalmasını sağlar. Derleyici fail fonksiyonundan sonra kodun çalışmayacağını bilir, bu yüzden erişilemeyen kod uyarısı verebilir.

Aşırı yüklemede seçim derleme anında yapılır ve bildirilen tipe bakar. any değişkeni bir String taşıdığı hâlde Any sürümü seçildi. Bu davranış, sanal yöntem çağrısının çalışma anı tipine göre çözülmesinden farklıdır ve ikisini karıştıran kod beklenmedik bir sürümü çağırır. Bir aşırı yükleme kümesi büyüdükçe hangi çağrının hangi sürüme gittiğini izlemek güçleşir; bu durumda adlandırılmış fonksiyonlar (showInt, showLong) daha okunur olabilir.

Uzantı fonksiyonları tipi değiştirmeden çağrı biçimi ekler. Bir kütüphane sınıfına yöntem eklemek istediğinizde, alt sınıf oluşturmak yerine uzantı yazmak çoğu zaman yeterlidir. Ama uzantı gerçek bir üye değildir; sınıfın özel alanlarına erişemez ve statik olarak çözülür, yani çalışma anındaki tipe göre seçilmez.

Parametre olarak geçen değiştirilebilir koleksiyon, çağıran tarafta da değişir. Bunu istemiyorsanız fonksiyon parametresini salt okunur List tipinde alın ya da kopyasını çıkarın. Ölçümdeki [1, 99] çıktısı bu tuzağın kısa bir kanıtıdır.

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

İçerik ölçütü fonksiyon referansı için geçerlidir ama referansın her alınışta ayrı bir nesne olduğunu bir sözleşme gibi okumamak gerekir: ölçülen şey, iki alınışın == ile eşit çıkmasıdır. Aynı derste listOf() varsayılanı tekil çıktı. Yani varsayılan değer yazımı tek başına ölçütü belirlemez: mutableListOf() her çağrıda yeni bir nesne kurduğu için içerik, listOf() aynı boş listeyi döndürdüğü için tekildir. Bir parametrenin varsayılanına bakarken ölçüt, o ifadenin ne kurduğuna bağlıdır.

Özet

  • fun ile bildirilen fonksiyonda varsayılan değerli parametre, adlandırılmış argüman ve vararg çağrıyı esnetir; * bir diziyi vararg parametresine açar.
  • Varsayılan değer her çağrıda yeniden hesaplanır; mutableListOf() varsayılanı çağrılar arasında paylaşılmaz.
  • ::plain gibi iki fonksiyon referansı ayrı nesnelerdir ama == ile eşittir: içerik.
  • Boş listOf() varsayılanı iki çağrıda aynı nesneyi verdi: tekil.
  • Bu dersin ölçütü: içerik; sınırlayıcı ölçüm: listOf() varsayılanı tekildir, mutableListOf() varsayılanı içeriktir.

Sonraki Adım

Fonksiyonları bir değer olarak alıp vermeyi gördük. Bir sonraki derste fonksiyonu yerinde yazmaya geçiyoruz: lambdalar ve yüksek mertebeden fonksiyonlar. Orada aynı soruyu iki lambda için soruyoruz; iki lambda aynı nesne mi, eşit mi, yoksa ikisi de değil mi.

İ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