Variance

Variance in dan out pada lokasi deklarasi serta star projection.

Daftar isi

Variance mengatur hubungan antara tipe generic yang argumennya berbeda, misalnya apakah Sumber<Int> boleh ditulis ke variabel Sumber<Any>. Secara bawaan tipe generic bersifat invariant, yaitu hubungan subtype antara argumennya tidak diwariskan ke tipe pembungkusnya, sehingga Kotlin menyediakan kata kunci out dan in untuk menyatakannya di lokasi deklarasi kelas (declaration-site variance). Topik ini membahas out untuk kelas produsen (producer), in untuk kelas konsumen (consumer), posisi yang dilarang compiler, serta star projection (star projection) untuk argumen tipe yang tidak diketahui. Seluruh contoh dieksekusi dengan compiler Kotlin.

out pada kelas produsen#

Kelas dengan out T hanya memproduksi nilai bertipe T, yaitu T muncul sebagai tipe kembalian dan tidak pernah menjadi tipe parameter fungsi. Karena nilainya hanya keluar dan tidak pernah masuk, compiler mengizinkan penugasan Sumber<Int> ke Sumber<Any>. Apa pun yang dibaca dari Sumber<Any> memang dijanjikan sebagai Any, dan setiap Int adalah Any, sehingga pembacaan tersebut selalu benar.

Program berikut memuat kelas produsen Sumber<out T>, kelas konsumen Penampung<in T> untuk bagian berikutnya, serta fungsi ukuran untuk bagian star projection.

kotlin main.kt
class Sumber<out T>(private val nilai: T) {
    fun ambil(): T = nilai
}

class Penampung<in T> {
    private val riwayat = mutableListOf<String>()

    fun simpan(item: T) {
        riwayat.add(item.toString())
    }

    fun jumlah(): Int = riwayat.size
}

fun ukuran(daftar: List<*>): Int = daftar.size

fun main() {
    val sumberAngka: Sumber<Int> = Sumber(24)
    val sumberUmum: Sumber<Any> = sumberAngka
    println(sumberUmum.ambil())

    val penampungUmum: Penampung<Any> = Penampung()
    val penampungAngka: Penampung<Int> = penampungUmum
    penampungAngka.simpan(100)
    penampungAngka.simpan(200)
    println(penampungUmum.jumlah())

    println(ukuran(listOf(1, "dua", 3.0)))
    println(ukuran(listOf("pagi", "siang", "malam", "dini hari")))
}
bash
# compiler Kotlin dan pustaka standar yang dipakai jalur ini
export PATH=/home/kal/my-project/tools/kotlinc/bin:/home/kal/my-project/tools/jdk-merged/bin:$PATH
export STDLIB=/home/kal/my-project/tools/kotlinc/lib/kotlin-stdlib.jar

# kompilasi berkas main.kt menjadi out.jar, lalu jalankan programnya
kotlinc main.kt -d out.jar && java -cp "out.jar:$STDLIB" MainKt
text Keluaran
24
2
3
4

Penugasan val sumberUmum: Sumber<Any> = sumberAngka sah karena Sumber dideklarasikan dengan out. Pemanggilan ambil() pada variabel umum tersebut mengembalikan nilai Int aslinya, yaitu 24, dan nilai itu tetap diperlakukan sebagai Any. Tanpa modifier out, penugasan yang sama akan ditolak compiler meskipun Int adalah subtype dari Any, karena secara bawaan kedua tipe itu dianggap tidak berhubungan.

in pada kelas consumer#

Kelas dengan in T hanya mengonsumsi nilai bertipe T, yaitu T hanya muncul sebagai tipe parameter fungsi dan tidak pernah menjadi tipe kembalian. Arah penugasannya berlawanan dengan out: Penampung<Any> boleh ditulis ke variabel Penampung<Int>, karena penampung yang sanggup menerima Any pasti sanggup menerima Int. Hubungan terbalik inilah yang disebut kontravarians.

Pada keluaran di atas, simpan(100) dan simpan(200) dipanggil melalui penampungAngka, sedangkan perhitungan jumlah() dilakukan melalui penampungUmum. Keduanya menunjuk objek yang sama, sehingga hasilnya 2. Fungsi jumlah() mengembalikan Int dan bukan T, sehingga tetap sah pada kelas yang dideklarasikan dengan in. Catatan penerimaannya disimpan sebagai String melalui toString(), agar tidak ada anggota bertipe T yang harus keluar dari kelas konsumen.

Arah penugasan yang terbalik ini menuntut kehati-hatian saat membacanya. Hubungan Penampung<Any> ke Penampung<Int> tampak menyimpang dari intuisi subtype, tetapi benar dari sisi janjinya, yaitu penampung yang menerima segala tipe pasti sanggup menerima tipe sempitnya. Kebalikannya tidak sah, karena Penampung<Int> hanya dijanjikan sanggup menerima Int, sedangkan Penampung<Any> mungkin diminta menyimpan nilai apa pun. Apabila fungsi kembalian bertipe T dicoba ditambahkan pada kelas ini, compiler akan menolaknya dengan pesan error posisi yang sama seperti pada bagian berikutnya.

Posisi yang dilarang compiler#

Modifier variance menuntut disiplin posisi. Pada kelas dengan out T, T hanya boleh berada di posisi keluar, misalnya tipe kembalian fungsi atau tipe properti val. Pada kelas dengan in T, T hanya boleh berada di posisi masuk, yaitu tipe parameter fungsi. Berkas berikut melanggar aturan itu dengan menambahkan fungsi penerima masukan pada kelas Sumber<out T>.

kotlin salahvariance.kt
class Sumber<out T>(private val nilai: T) {
    fun ambil(): T = nilai

    fun isi(nilaiBaru: T) {
        println(nilaiBaru)
    }
}

fun main() {
    val s = Sumber("pagi")
    println(s.ambil())
}
bash
# kompilasi berkas salahvariance.kt untuk melihat pesan error dari compiler
kotlinc salahvariance.kt -d salahvariance.jar
text Keluaran
salahvariance.kt:4:24: error: type parameter 'T' is declared as 'out' but occurs in 'in' position in type 'T (of class Sumber<out T>)'.
    fun isi(nilaiBaru: T) {
                       ^

Parameter fungsi adalah posisi masuk, sehingga fun isi(nilaiBaru: T) bertentangan dengan deklarasi out. Larangan ini mencegah jebakan nyata. Apabila fungsi isi diizinkan, Sumber<Any> yang menampung Sumber<Int> dapat dipakai memasukkan nilai String, dan setelah itu pembacaan yang dijanjikan menghasilkan Int menjadi tidak benar. Compiler menolak kombinasi itu sejak awal agar kontradiksi semacam itu tidak mungkin terjadi. Perbaikannya adalah menghapus out agar kelasnya invariant, atau memisahkan peran produsen dan konsumen ke dalam dua kelas yang berbeda.

Star projection#

Apabila argumen tipe tidak diketahui atau tidak penting, tipe generic dapat ditulis dengan star projection berbentuk Tipe<*>. Pada List<*>, isi daftar diketahui hanya sebagai Any?, sehingga membaca elemennya aman, sedangkan menambahkan elemen tidak diizinkan karena compiler tidak tahu tipe yang diminta daftar itu. Star projection berbeda dari out Any: projection ini tidak menyatakan hubungan subtyping, melainkan menyatakan ketidaktahuan argumen tipenya.

Fungsi ukuran pada main.kt memakai List<*> sebagai parameternya, sehingga menerima daftar dengan argumen tipe apa pun. Pemanggilan pertama mengirim daftar campuran Int, String, dan Double, lalu menghasilkan 3, sedangkan pemanggilan kedua mengirim empat string dan menghasilkan 4. Keduanya sah karena fungsi itu tidak menyentuh tipe elemennya, melainkan hanya membaca jumlah elemennya.

Operasi yang tersedia pada List<*> terbatas pada yang tidak bergantung pada tipe elemen, misalnya size dan isEmpty(), serta pembacaan elemen sebagai Any?. Pada pustaka standar, List<E> sendiri dideklarasikan dengan out, sehingga penugasan List<Int> ke List<Any> langsung sah tanpa projection. Star projection tetap berguna apabila argumen tipenya benar-benar tidak diketahui, misalnya saat menerima objek dari API luar yang tipenya hilang saat berjalan.

Ringkasan#

  • out mendeklarasikan kelas produsen yang hanya mengeluarkan T, sehingga Sumber<Int> dapat ditulis ke variabel Sumber<Any>.
  • in mendeklarasikan kelas konsumen yang hanya menerima T, sehingga Penampung<Any> dapat ditulis ke variabel Penampung<Int>.
  • Compiler menolak T pada posisi yang bertentangan dengan modifier variannya, misalnya parameter fungsi pada kelas out.
  • Star projection List<*> menyatakan argumen tipe yang tidak diketahui, aman untuk pembacaan sebagai Any?, dan tidak mengizinkan penambahan elemen.

Referensi#