Generik di Kotlin

1. Ikhtisar

Pada artikel ini, kita akan melihat tipe generik dalam bahasa Kotlin .

Mereka sangat mirip dengan bahasa Java, tetapi pembuat bahasa Kotlin mencoba membuatnya sedikit lebih intuitif dan mudah dimengerti dengan memperkenalkan kata kunci khusus seperti keluar dan masuk.

2. Membuat Kelas Parameter

Katakanlah kita ingin membuat kelas berparameter. Kita dapat dengan mudah melakukan ini dalam bahasa Kotlin dengan menggunakan tipe generik:

class ParameterizedClass(private val value: A) { fun getValue(): A { return value } }

Kita dapat membuat instance dari kelas seperti itu dengan menyetel tipe berparameter secara eksplisit saat menggunakan konstruktor:

val parameterizedClass = ParameterizedClass("string-value") val res = parameterizedClass.getValue() assertTrue(res is String)

Untungnya, Kotlin bisa menyimpulkan tipe generik dari tipe parameter sehingga kita bisa menghilangkannya saat menggunakan konstruktor:

val parameterizedClass = ParameterizedClass("string-value") val res = parameterizedClass.getValue() assertTrue(res is String)

3. Kotlin out dan di Kata Kunci

3.1. The Out Kata Kunci

Katakanlah kita ingin membuat kelas produser yang akan menghasilkan hasil dari beberapa tipe T. Kadang-kadang; kami ingin menetapkan nilai yang dihasilkan ke referensi yang merupakan supertipe tipe T.

Untuk mencapai itu dengan menggunakan Kotlin, kita perlu menggunakan kata kunci out pada tipe generik. Itu berarti kita dapat menetapkan referensi ini ke salah satu supertipe-nya. Nilai keluar hanya dapat diproduksi oleh kelas tertentu tetapi tidak digunakan :

class ParameterizedProducer(private val value: T) { fun get(): T { return value } }

Kami mendefinisikan kelas ParameterizedProducer yang dapat menghasilkan nilai tipe T.

Lanjut; kita dapat menetapkan instance kelas ParameterizedProducer ke referensi yang merupakan supertipe dari itu:

val parameterizedProducer = ParameterizedProducer("string") val ref: ParameterizedProducer = parameterizedProducer assertTrue(ref is ParameterizedProducer)

Jika tipe T di kelas ParamaterizedProducer bukan tipe out , pernyataan yang diberikan akan menghasilkan kesalahan compiler.

3.2. The di Kata Kunci

Kadang-kadang, kita memiliki situasi arti berlawanan bahwa kita memiliki referensi tipe T dan kami ingin dapat menetapkan ke subtipe dari T .

Kita dapat menggunakan kata kunci in pada tipe generik jika kita ingin memasukkannya ke referensi subtipe-nya. Kata kunci in hanya dapat digunakan pada jenis parameter yang dipakai, tidak diproduksi :

class ParameterizedConsumer { fun toString(value: T): String { return value.toString() } }

Kami menyatakan bahwa toString () metode hanya akan memakan nilai tipe T .

Selanjutnya, kita dapat menetapkan referensi tipe Number ke referensi subtipe-nya - Double:

val parameterizedConsumer = ParameterizedConsumer() val ref: ParameterizedConsumer = parameterizedConsumer assertTrue(ref is ParameterizedConsumer)

Jika tipe T di ParameterizedCounsumer tidak akan menjadi tipe in , pernyataan yang diberikan akan menghasilkan kesalahan compiler.

4. Jenis Proyeksi

4.1. Salin Array of Subtype ke Array of Supertypes

Misalkan kita memiliki array dari beberapa tipe, dan kita ingin menyalin seluruh array ke dalam array tipe Any . Ini adalah operasi yang valid, tetapi untuk memungkinkan compiler mengkompilasi kode kita, kita perlu menambahkan catatan parameter input dengan kata kunci out .

Ini memungkinkan kompilator mengetahui bahwa argumen input dapat berupa tipe apa pun yang merupakan subtipe dari Any :

fun copy(from: Array, to: Array) { assert(from.size == to.size) for (i in from.indices) to[i] = from[i] }

Jika dari parameter tidak dari luar Setiap jenis, kita tidak akan mampu melewati array dari Int tipe sebagai argumen:

val ints: Array = arrayOf(1, 2, 3) val any: Array = arrayOfNulls(3) copy(ints, any) assertEquals(any[0], 1) assertEquals(any[1], 2) assertEquals(any[2], 3)

4.2. Menambahkan Elemen Subtipe ke Array Supertipe-nya

Katakanlah kita memiliki situasi berikut - kita memiliki array tipe Any yang merupakan supertipe Int dan kita ingin menambahkan elemen Int ke array ini. Kita perlu menggunakan kata kunci in sebagai tipe dari larik tujuan agar kompiler tahu bahwa kita dapat menyalin nilai Int ke larik ini :

fun fill(dest: Array, value: Int) { dest[0] = value }

Kemudian, kita dapat menyalin nilai tipe Int ke array Any:

val objects: Array = arrayOfNulls(1) fill(objects, 1) assertEquals(objects[0], 1)

4.3. Proyeksi Bintang

Ada situasi ketika kita tidak peduli dengan jenis nilai tertentu. Katakanlah kita hanya ingin mencetak semua elemen dari sebuah array dan tidak peduli apa jenis elemen dalam array ini.

Untuk mencapai itu, kita dapat menggunakan proyeksi bintang:

fun printArray(array: Array) { array.forEach { println(it) } }

Kemudian, kita dapat mengirimkan array jenis apa pun ke metode printArray () :

val array = arrayOf(1,2,3) printArray(array)

Saat menggunakan tipe referensi proyeksi bintang, kita dapat membaca nilai darinya, tetapi kita tidak dapat menuliskannya karena akan menyebabkan kesalahan kompilasi.

5. Batasan Umum

Let's say that we want to sort an array of elements, and each element type should implement a Comparable interface. We can use the generic constraints to specify that requirement:

fun 
    
      sort(list: List): List { return list.sorted() }
    

In the given example, we defined that all elements T needed to implement the Comparable interface. Otherwise, if we will try to pass a list of elements that do not implement this interface, it will cause a compiler error.

We defined a sort function that takes as an argument a list of elements that implement Comparable, so we can call the sorted() method on it. Let's look at the test case for that method:

val listOfInts = listOf(5,2,3,4,1) val sorted = sort(listOfInts) assertEquals(sorted, listOf(1,2,3,4,5))

We can easily pass a list of Ints because the Int type implements the Comparable interface.

5.1. Multiple Upper Bounds

With the angle bracket notation, we can declare at most one generic upper bound. If a type parameter needs multiple generic upper bounds, then we should use separate where clauses for that particular type parameter. For instance:

fun  sort(xs: List) where T : CharSequence, T : Comparable { // sort the collection in place }

As shown above, the parameter T must implement the CharSequence and Comparable interfaces at the same time. Similarly, we can declare classes with multiple generic upper bounds:

class StringCollection(xs: List) where T : CharSequence, T : Comparable { // omitted }

6. Generics at Runtime

6.1. Type Erasure

As with Java, Kotlin's generics are erased at runtime. That is, an instance of a generic class doesn't preserve its type parameters at runtime.

For example, if we create a Set and put a few strings into it, at runtime we're only able to see it as a Set.

Let's create two Sets with two different type parameters:

val books: Set = setOf("1984", "Brave new world") val primes: Set = setOf(2, 3, 11)

At runtime, the type information for Set and Set will be erased and we see both of them as plain Sets. So, even though it’s perfectly possible to find out at runtime that value is a Set, we can’t tell whether it’s a Set of strings, integers, or something else: that information has been erased.

So, how does Kotlin's compiler prevent us from adding a Non-String into a Set? Or, when we get an element from a Set, how does it know the element is a String?

The answer is simple. The compiler is the one responsible for erasing the type information but before that, it actually knows the books variable contains String elements.

So, every time we get an element from it, the compiler would cast it to a String or when we're gonna add an element into it, the compiler would type check the input.

6.2. Reified Type Parameters

Let's have more fun with generics and create an extension function to filter Collection elements based on their type:

fun  Iterable.filterIsInstance() = filter { it is T } Error: Cannot check for instance of erased type: T

The “it is T” part, for each collection element, checks if the element is an instance of type T, but since the type information has been erased at runtime, we can't reflect on type parameters this way.

Or can we?

The type erasure rule is true in general, but there is one case where we can avoid this limitation: Inline functions. Type parameters of inline functions can be reified, so we can refer to those type parameters at runtime.

The body of inline functions is inlined. That is, the compiler substitutes the body directly into places where the function is called instead of the normal function invocation.

If we declare the previous function as inline and mark the type parameter as reified, then we can access generic type information at runtime:

inline fun  Iterable.filterIsInstance() = filter { it is T }

The inline reification works like a charm:

>> val set = setOf("1984", 2, 3, "Brave new world", 11) >> println(set.filterIsInstance()) [2, 3, 11]

Let's write another example. We all are familiar with those typical SLF4j Logger definitions:

class User { private val log = LoggerFactory.getLogger(User::class.java) // ... }

Using reified inline functions, we can write more elegant and less syntax-horrifying Logger definitions:

inline fun  logger(): Logger = LoggerFactory.getLogger(T::class.java)

Then we can write:

class User { private val log = logger() // ... }

This gives us a cleaner option to implement logging, the Kotlin way.

6.3. Deep Dive into Inline Reification

So, what's so special about inline functions so that type reification only works with them? As we know, Kotlin's compiler copies the bytecode of inline functions into places where the function is called.

Since in each call site, the compiler knows the exact parameter type, it can replace the generic type parameter with the actual type references.

For example, when we write:

class User { private val log = logger() // ... }

When the compiler inlines the logger() function call, it knows the actual generic type parameter –User. So instead of erasing the type information, the compiler seizes the reification opportunity and reifies the actual type parameter.

7. Conclusion

In this article, we were looking at the Kotlin Generic types. We saw how to use the out and in keywords properly. We used type projections and defined a generic method that uses generic constraints.

Penerapan semua contoh dan cuplikan kode ini dapat ditemukan di proyek GitHub - ini adalah proyek Maven, jadi semestinya mudah untuk mengimpor dan menjalankannya apa adanya.