İçeriğe geç
academia.sh

Ders 13 / 23

Mühürlü Tipler ve Numaralandırmalar

Numaralandırma girdisinin, data object nesnesinin, veri sınıfının ve sıradan sınıfın aynı mühürlü hiyerarşide hangi ölçüte düştüğünün ve tümlü when'in eksik dalı reddedişinin ölçümü.

Yayın Güncelleme

Bu derste

  • Numaralandırmanın entries, ordinal ve valueOf davranışını ve girdilerin tekil olduğunu ölçer
  • Mühürlü arayüz üzerindeki tümlü when ifadesinin eksik dalı derleme sırasında reddettiğini gösterir
  • data object, veri sınıfı ve sıradan sınıf alt tiplerinin üç ayrı ölçüt verdiğini ölçer
  • Ölçütü hiyerarşinin değil alt tipin türünün belirlediğini sınırlayıcı ölçümle doğrular
İçindekiler

Önceki ders veri sınıfının equals’i birincil yapıcıdaki özelliklerden ürettiğini ve ölçütün alan tiplerine bağlı olduğunu gösterdi. Şimdiye kadar ölçülen her sınıf, kaç örneği olursa olsun açık bir tipti: ayrı bir örnek her yerde kurulabilir. Bir değerin yalnızca önceden sayılmış birkaç biçimden biri olabildiği durumlar için Kotlin iki araç sunar: enum class ile numaralandırmalar ve sealed anahtar sözcüğüyle mühürlü sınıflar ve arayüzler. Bu ders ikisini ölçer ve her birinin ölçüte nasıl düştüğüne bakar.

Ölçü ekseni aynı kalır: bir değerin kendisine eşit sayılan başka bir değerle ilişkisi, yani ölçüt. Criterion sınıfı açılış dersindeki hâliyle aşağıda yeniden verilir. Kural sırayla işler: ilk değer ikiziyle aynı nesneyse tekil; değilse ve ikiz eşit değilse kimlik; ikiz eşit ama üçüncü değer eşit değilse içerik; üçüncü değer de eşitse kısmi içerik. Bu ders, önceki üç dersin kesitlerini tek hiyerarşide bir araya getirir.

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

Numaralandırma

Numaralandırma, sınıfın bütün örneklerinin tanımda tek tek sayıldığı bir sınıftır. enum class Level { LOW, MEDIUM, HIGH } tam üç Level değeri tanımlar ve dışarıdan dördüncüsü kurulamaz. Her girdi sınıfın tek örneğidir. entries bütün girdileri tanım sırasıyla verir, ordinal sıra numarasını sıfırdan başlayarak döndürür, valueOf bir dizgiden girdiyi bulur ve bulamazsa istisna fırlatır.

Aşağıdaki blok bunları dener. describe fonksiyonu when ile her girdiye bir dizgi eşler ve else dalı yoktur: derleyici üç girdinin hepsinin karşılandığını bilir. Blok ayrıca valueOf("MEDIUM") ile bulunan girdinin kodda doğrudan yazılan Level.MEDIUM ile aynı nesne olup olmadığını === ile sorar. Tanımsız bir ad verilince fırlayan istisna yakalanır ve yalnızca sınıf adı basılır.

// Main.kt — numaralandirma
enum class Level { LOW, MEDIUM, HIGH }

fun describe(level: Level): String = when (level) {
    Level.LOW -> "dusuk"
    Level.MEDIUM -> "orta"
    Level.HIGH -> "yuksek"
}

fun main() {
    println(Level.entries)
    println(Level.HIGH.ordinal)
    println(Level.valueOf("MEDIUM"))
    println(Level.valueOf("MEDIUM") === Level.MEDIUM)
    println(describe(Level.LOW))
    try {
        Level.valueOf("EXTREME")
    } catch (e: IllegalArgumentException) {
        println(e::class.simpleName)
    }
}
[LOW, MEDIUM, HIGH]
2
MEDIUM
true
dusuk
IllegalArgumentException

entries üç girdiyi sırayla verir, HIGH ikinci sıfırdan sayılınca 2 numaralıdır ve valueOf ile bulunan girdi doğrudan yazılanla aynı nesnedir. Bu, ölçütün ilk sorusudur: girdi kendisinin ikizidir ve ölçüt tekildir. Tanımsız ad IllegalArgumentException fırlatır.

TN19. Ölçüm yalnızca üç girdili, alanı ve yöntemi olmayan bir numaralandırma içindir. Girdilerin kendi alanları ya da yöntemleri olduğunda, ya da numaralandırma bir arayüz uyguladığında davranışın aynı kalıp kalmadığı ölçülmedi.

Mühürlü Arayüz ve Tümlü When

sealed interface Shape, Shape’i uygulayabilecek tüm tiplerin aynı pakette ve aynı derlemede önceden bilindiği bir arayüzdür. Aşağıdaki hiyerarşi üç alt tipten oluşur: data object Empty tek bir örneği olan bir nesne bildirimidir, data class Circle bir veri sınıfıdır, class Blob ise equals’i yazılmamış sıradan bir sınıftır. Bu üç tip, önceki dersleri tek hiyerarşide toplar. Derleyici alt tiplerin tam listesini bildiği için when (shape) ifadesinin hepsini karşılayıp karşılamadığını denetleyebilir.

// Main.kt — kapali hiyerarsi
sealed interface Shape
data object Empty : Shape
data class Circle(val radius: Int) : Shape
class Blob(val size: Int) : Shape

fun describe(shape: Shape): String = when (shape) {
    Empty -> "bos"
    is Circle -> "daire " + shape.radius
    is Blob -> "leke " + shape.size
}

fun main() {
    val shapes = listOf(Empty, Circle(2), Blob(3))
    for (shape in shapes) println(describe(shape))
    println(Empty)
    println(Circle(2))
}
bos
daire 2
leke 3
Empty
Circle(radius=2)

Üç dal üç alt tipi karşılar ve else yazılmaz. println(Empty) yalnızca adı basar: data object, toString yöntemini nesnenin adıyla üretir. println(Circle(2)) ise veri sınıfının alanlı biçimini basar. Sıradan bir object için varsayılan toString bir ad ve karma kodu verirdi; data değiştiricisi bunu okunur kılar.

Tümlü when’in asıl değeri başka yerdedir: hiyerarşiye bir alt tip eklenip dallardan biri unutulursa derleyici reddeder. Aşağıdaki parça Blob dalını bırakır. Parça, açılış dersinde olduğu gibi ölçüldü ve derlenmedi; derleyici eksik dalı adıyla gösteren bir hata verdi. Hata metni burada basılmaz.

sealed interface Shape
data object Empty : Shape
data class Circle(val radius: Int) : Shape
class Blob(val size: Int) : Shape

fun describe(shape: Shape): String = when (shape) {
    Empty -> "bos"
    is Circle -> "daire " + shape.radius
}

fun main() {
    println(describe(Empty))
}

TN20. Derleme reddi Kotlin 2.4.21’de ölçüldü. when bir ifade olarak kullanılmadığında, yalnızca deyim olduğunda, aynı denetimin uygulanıp uygulanmadığı bu derste ölçülmedi.

Alanlı Numaralandırma

Numaralandırma girdileri alan ve yöntem taşıyabilir. Girdiler parantez içinde yapıcı değerleri alır, girdi listesi ; ile biter ve ardından üyeler gelir. Aşağıdaki blok boyut ve kod taşıyan bir numaralandırmayı dener: girdi üzerinde yöntem çağırır, entries üzerinde dönüştürme yapar, girdileri sıra numarasıyla kıyaslar ve olmayan bir kodu arar.

// Main.kt — alanli numaralandirma
enum class Planet(val code: String, val size: Int) {
    SMALL("s", 1),
    MEDIUM("m", 5),
    LARGE("l", 9);

    fun isBigger(other: Planet): Boolean = size > other.size
}

fun main() {
    println(Planet.LARGE.isBigger(Planet.SMALL))
    println(Planet.entries.map { it.code })
    println(Planet.entries.maxByOrNull { it.size })
    println(Planet.MEDIUM.name + Planet.MEDIUM.ordinal)
    println(Planet.SMALL < Planet.LARGE)
    println(Planet.entries.firstOrNull { it.code == "x" })
    println(Planet.entries.firstOrNull { it.code == "x" }?.name ?: "yok")
}
true
[s, m, l]
LARGE
MEDIUM1
true
null
yok

LARGE.isBigger(SMALL) true verir; yöntem, girdinin kendi size alanını kullanır. entries girdileri tanım sırasıyla verir, dolayısıyla kodların listesi [s, m, l] çıkar. maxByOrNull en büyük boyutlu girdiyi bulur ve LARGE basar. name ile ordinal birleşince MEDIUM1 elde edilir: ikinci girdinin sırası 1’dir. SMALL < LARGE true verir, çünkü girdiler sıra numarasına göre karşılaştırılabilir. Olmayan bir kod aranınca sonuç null olur; ?.name ?: "yok" bunu yok’a çevirir.

Numaralandırma burada kapalı ve sabit bir değerler kümesidir ve her girdi tek bir örnektir. Alan eklemek, girdilerin tanımlandığı satırda değer vermek anlamına gelir; girdi listesi ; ile bitirilmeden üye yazılamaz. Aramanın null dönmesi aynı zamanda valueOf’tan farkı gösterir: valueOf olmayan ada istisna fırlatır, firstOrNull ise yokluğu değer olarak taşır. Hangisinin seçileceği, yokluğun hata mı yoksa beklenen bir durum mu olduğuna bağlıdır.

Else Dalı Tümlülüğü Susturur

Tümlü when’in eksik dalı reddetmesi bir güvencedir; ama bu güvence else yazıldığı anda kaybolur. Aşağıdaki blok yukarıda reddedilen hiyerarşinin aynısını kullanır ve Blob dalı yerine bir else koyar. Derleme bu kez geçer. Bloğun gösterdiği, sonradan bir alt tip eklendiğinde ne olacağıdır: o alt tip sessizce else dalına düşer.

sealed interface Shape
data object Empty : Shape
data class Circle(val radius: Int) : Shape
class Blob(val size: Int) : Shape

fun describe(shape: Shape): String = when (shape) {
    Empty -> "bos"
    is Circle -> "daire"
    else -> "bilinmiyor"
}

fun main() {
    println(describe(Empty))
    println(describe(Circle(1)))
    println(describe(Blob(1)))
}
bos
daire
bilinmiyor

Çıktı bos, daire, bilinmiyor olur. Derleme geçti, çünkü else her kalan durumu karşılar. Blob örneği bilinmiyor dalına düştü; yukarıdaki tümlü sürümde bu durum derleme hatasıydı. Fark, else dalının derleyicinin eksik alt tip denetimini kapatmasıdır.

Sorun bugünün kodunda değil, yarının değişikliğinde ortaya çıkar. Hiyerarşiye sonradan eklenen bir alt tip, else dalı olan her when ifadesinde sessizce o dala düşer ve derleyici hiçbir uyarı vermez. Tümlü ifadede aynı ekleme, ilgili her when ifadesini derleme hatasıyla işaretlerdi. Sık yapılan hata, “her ihtimale karşı” diye else eklemektir. Mühürlü tipin sağladığı güvence tam da bu dalı yazmamaktan gelir. Gerçekten varsayılan bir davranış gerekiyorsa o davranış açık bir alt tip olarak modellenir, else bu işi sessizce üstlenmez.

Mühürlü Sınıf ve Akıllı Tip Dönüşümü

Mühürlü arayüzün yanı sıra sealed class da vardır; fark, sınıfın yapıcı durumu taşıyabilmesidir. Alt tipler genellikle sınıfın içine yazılır. Aşağıdaki Outcome üç durumu anlatır: değer taşıyan başarı, neden taşıyan hata ve durum taşımayan bekleme. Tümlü when, her dalda alt tipe akıllı tip dönüşümü yapar ve outcome.value gibi alanlara doğrudan erişir.

// Main.kt — muhurlu sinif ve akilli tip donusumu
sealed class Outcome {
    data class Success(val value: Int) : Outcome()
    data class Failure(val reason: String) : Outcome()
    data object Pending : Outcome()
}

fun render(outcome: Outcome): String = when (outcome) {
    is Outcome.Success -> "deger " + (outcome.value + 1)
    is Outcome.Failure -> "hata " + outcome.reason.length
    Outcome.Pending -> "bekliyor"
}

fun main() {
    val outcomes = listOf(Outcome.Success(1), Outcome.Failure("kotu"), Outcome.Pending)
    for (outcome in outcomes) println(render(outcome))
    println(Outcome.Pending === Outcome.Pending)
    println(outcomes.filterIsInstance<Outcome.Success>().size)
    println(outcomes.count { it is Outcome.Failure })
}
deger 2
hata 4
bekliyor
true
1
1

sealed class ile sealed interface aynı güvenceyi sağlar: alt tipler derleme anında bilinir. Fark, sınıfın yapıcı durumu ve üst sınıf davranışı taşıyabilmesidir. Çıktıda üç dal çalışır: deger 2, hata 4 ve bekliyor. Her dalda derleyici, outcome değerini o dalın alt tipine daraltır; bu yüzden outcome.value ve outcome.reason doğrudan yazılabilir, ayrıca dönüşüm gerekmez.

Dördüncü satır Pending === Pending ifadesini sorar ve true döner: data object tek bir örnektir, bu yüzden kimlik ve içerik eşitliği örtüşür. Son iki satır koleksiyon fonksiyonlarıyla hiyerarşiyi süzer: filterIsInstance ve is ile sayım ikisi de 1 verir. İç içe yazım alt tiplerin adını Outcome.Success biçimine sokar. Bu, alt tiplerin hiyerarşiye ait olduğunu görünür kılar ve aynı ada sahip başka bir tipin karışmasını önler. Alt tipler aynı pakette olduğu sürece dışarıya da yazılabilir; ama iç içe yazım, tümlülüğün nereye bakıldığını okuyan kişiye gösterir.

Numaralandırma Koleksiyonlarda

Numaralandırma girdileri tekil olduğu için koleksiyonlarda anahtar olarak kullanılması sorunsuzdur. Girdiler Comparable olduğundan sıra numarasına göre sıralanır. Blok bir listede tekrarlanan girdileri sayar, sıralar ve ayırır, sonra iki girdinin sırasını compareTo ile sorar.

// Main.kt — numaralandirma koleksiyonlarda
enum class Level { LOW, MEDIUM, HIGH }

fun main() {
    val levels = listOf(Level.HIGH, Level.LOW, Level.HIGH, Level.MEDIUM)
    println(levels.groupingBy { it }.eachCount())
    println(levels.sorted())
    println(levels.distinct().size)
    println(Level.HIGH.compareTo(Level.LOW))
    println(Level.valueOf("LOW") == Level.LOW)
}
{HIGH=2, LOW=1, MEDIUM=1}
[LOW, MEDIUM, HIGH, HIGH]
3
2
true

groupingBy { it }.eachCount() girdileri sayar ve ilk görülme sırasını korur: {HIGH=2, LOW=1, MEDIUM=1}. Girdiler tekil olduğu için anahtar olarak güvenle kullanılır; iki HIGH aynı örneğe işaret eder ve tek anahtara toplanır. sorted girdileri sıra numarasına göre dizer, [LOW, MEDIUM, HIGH, HIGH] çıkar. distinct üç girdi bırakır. HIGH.compareTo(LOW) bu örnekte 2 verir, sıra numaraları farkıyla uyumlu bir değer. valueOf("LOW") == LOW ise true’dur, çünkü ad aynı örneği döndürür.

Buradaki mekanizma, numaralandırmanın hem tekil hem sıralı olmasıdır. Sıralama tanım sırasına bağlıdır, yani girdilerin dosyadaki konumu davranışın parçasıdır. Sık yapılan hata, girdileri sonradan araya yerleştirmek ve sıralamaya dayanan kodu bozmaktır: sıra numarası değişince sorted sonucu da, compareTo değeri de değişir. Sıralamanın önemli olduğu bir numaralandırmada eklenecek girdi sona eklenir, ya da sıra açık bir alanda tutulur ve karşılaştırma o alan üzerinden yapılır.

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

Üç alt tip aynı mühürlü arayüzün altındadır. Mühür, kimin alt tip olabileceğini kapatır; ama equals’i belirlemez. O hâlde aynı hiyerarşi aynı anda birden fazla ölçüt vermelidir. Sınırlayıcı ölçüm bunu dört satırla dener. İlk satır bir numaralandırma girdisini valueOf ile bulunan ikiziyle karşılaştırır. Sonraki üç satır Shape alt tiplerini sırayla dener: Empty, Circle(1) ve Blob(1). Her alt tipte ikiz aynı değerle ayrıca kurulur, üçüncü değerin değeri farklıdır; Empty için üçüncü değer olarak Circle(1) verilir çünkü Empty’nin başka örneği yoktur.

// Main.kt — alt tipin turu
sealed interface Shape
data object Empty : Shape
data class Circle(val radius: Int) : Shape
class Blob(val size: Int) : Shape

enum class Level { LOW, MEDIUM, HIGH }

fun main() {
    val checks = listOf(
        Criterion("girdi", Level.HIGH, Level.valueOf("HIGH"), Level.LOW),
        Criterion("data-object", Empty, Empty, Circle(1)),
        Criterion("data-class", Circle(1), Circle(1), Circle(2)),
        Criterion("sinif", Blob(1), Blob(1), Blob(2)),
    )
    for (check in checks) check.report()
    summarize(checks)
}
girdi: ayni-nesne=true, ikiz=true, farkli=false -> tekil
data-object: ayni-nesne=true, ikiz=true, farkli=false -> tekil
data-class: ayni-nesne=false, ikiz=true, farkli=false -> icerik
sinif: ayni-nesne=false, ikiz=false, farkli=false -> kimlik
tekil: 2
kimlik: 1
icerik: 1
kismi-icerik: 0

Aynı hiyerarşi üç ayrı ölçüt verir. Numaralandırma girdisi de data object de tek örnekli olduğu için tekildir: ikiz zaten aynı nesnedir. Circle veri sınıfı olduğu için içerik verir. Blob’un equals’i yazılmamıştır; aynı değerli iki örnek farklı nesnedir ve ölçüt kimliktir. Ölçütü mühür belirlemez; alt tipin hangi türde yazıldığı belirler. Tekil, kimlik ve içerik tek bir when ile işlenen tek bir arayüzün altında yan yana durabilir. Dağılım 2 tekil, 1 kimlik, 1 içerik, 0 kısmi içeriktir.

Özet

  • Numaralandırma girdileri sınıfın tek örnekleridir: valueOf ile bulunan girdi doğrudan yazılanla aynı nesnedir ve ölçüt tekildir.
  • entries, ordinal ve valueOf girdileri tanım sırasıyla listeler, sıralar ve ada göre bulur; tanımsız ad IllegalArgumentException fırlatır.
  • Mühürlü arayüzde alt tiplerin listesi derleyici tarafından bilinir; tümlü when eksik bir dalı derleme sırasında reddeder.
  • data object tekil, veri sınıfı alt tipi içerik, equals’i yazılmamış sıradan sınıf alt tipi kimlik verir.
  • Bu dersin ölçütü: tekil. Sınırlayıcı ölçümde aynı mühürlü hiyerarşi üç ölçütü birden verdi; ölçütü hiyerarşi değil alt tipin türü belirler.

Sonraki Adım

data object Empty, tek örnekli bir nesneyi bildirimle kurdu. Kotlin’de bu kalıbın genel biçimi object bildirimidir: sınıfı ve tek örneğini birlikte tanımlar. Bir sonraki ders nesne bildirimlerini, bir sınıfın içindeki yardımcı nesneyi ve ad vermeden kurulan nesne ifadesini ölçer ve üçünün ölçüte nasıl düştüğüne bakar.

İ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