Generik di Kotlin

1. Gambaran keseluruhan

Dalam artikel ini, kita akan melihat jenis generik dalam bahasa Kotlin .

Mereka sangat serupa dengan bahasa Jawa, tetapi pencipta bahasa Kotlin berusaha menjadikannya sedikit lebih intuitif dan difahami dengan memperkenalkan kata kunci khas seperti keluar dan masuk.

2. Membuat Kelas Parameter

Katakanlah bahawa kita mahu membuat kelas parameter. Kita boleh melakukannya dengan mudah dalam bahasa Kotlin dengan menggunakan jenis generik:

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

Kita boleh membuat contoh kelas seperti itu dengan menetapkan jenis parameter secara eksplisit ketika menggunakan konstruktor:

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

Dengan senang hati, Kotlin dapat menyimpulkan jenis generik dari jenis parameter sehingga kita dapat menghilangkannya ketika menggunakan konstruktor:

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

3. Kotlin keluar dan dalam Kata Kunci

3.1. The Daripada Kata-kata

Katakan bahawa kita ingin mewujudkan kelas pengeluar yang akan menghasilkan hasil dari beberapa jenis T. Kadang-kadang; kami ingin memberikan nilai yang dihasilkan kepada rujukan yang merupakan supertype jenis T.

Untuk mencapainya menggunakan Kotlin, kita perlu menggunakan kata kunci keluar pada jenis generik. Ini bermaksud bahawa kita dapat memberikan rujukan ini kepada mana-mana supertipenya. Nilai keluar hanya dapat dihasilkan oleh kelas yang diberikan tetapi tidak habis digunakan :

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

Kami menentukan kelas ParameterizedProducer yang dapat menghasilkan nilai jenis T.

Seterusnya; kita boleh memberikan contoh kelas ParameterizedProducer ke rujukan yang merupakan supertype:

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

Sekiranya jenis T dalam kelas ParamaterizedProducer bukan jenis keluar , pernyataan yang diberikan akan menghasilkan ralat penyusun.

3.2. Yang dalam Kata-kata

Kadang-kadang, kita mempunyai keadaan makna bertentangan yang kita ada sebutan jenis T dan kami mahu dapat untuk memberikan kepada subjenis daripada T .

Kita boleh menggunakan dalam kata kunci kepada jenis generik jika kita ingin berikan kepada rujukan subjenis itu. Kata kunci dalam boleh digunakan hanya pada jenis parameter yang digunakan, tidak dihasilkan :

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

Kami mengaku bahawa toString () kaedah hanya akan memakan nilai jenis T .

Seterusnya, kita dapat menetapkan rujukan jenis Nombor ke rujukan subjenisnya - Double:

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

Jika jenis T dalam ParameterizedCounsumer tidak akan menjadi yang dalam jenis, pernyataan yang diberikan akan menghasilkan ralat compiler.

4. Taipkan Unjuran

4.1. Salin Array of Subtype ke Array of Supertypes

Katakan bahawa kita mempunyai susunan beberapa jenis, dan kita ingin menyalin keseluruhan susunan ke dalam array Jenis apa pun . Ini adalah operasi yang sah, tetapi untuk membolehkan pengkompil menyusun kod kita, kita perlu memberi penjelasan pada parameter input dengan kata kunci keluar .

Ini membolehkan pengkompil mengetahui bahawa argumen input boleh dari jenis apa pun yang merupakan subjenis dari Mana - mana :

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

Sekiranya parameter dari tidak ada Jenis apa pun , kami tidak akan dapat meneruskan array jenis Int 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. Menambah Elemen Subtipe ke Array Supertype nya

Katakan bahawa kita mempunyai situasi berikut - kita mempunyai pelbagai jenis Apa saja yang merupakan supertype Int dan kami ingin menambahkan elemen Int pada array ini. Kita perlu menggunakan dalam kata kunci sebagai sejenis pelbagai destinasi untuk membiarkan tahu pengkompil bahawa kita boleh menyalin Int nilai kepada pelbagai ini :

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

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

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

4.3. Unjuran Bintang

Terdapat situasi ketika kita tidak mementingkan jenis nilai tertentu. Katakan bahawa kita hanya mahu mencetak semua elemen larik dan tidak kira jenis elemen dalam larik ini.

Untuk mencapainya, kita boleh menggunakan unjuran bintang:

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

Kemudian, kita boleh meneruskan pelbagai jenis jenis ke kaedah printArray () :

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

Semasa menggunakan jenis rujukan unjuran bintang, kita dapat membaca nilai darinya, tetapi kita tidak dapat menulisnya kerana akan menyebabkan ralat penyusunan.

5. Kekangan Generik

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.

Pelaksanaan semua contoh dan coretan kod ini terdapat dalam projek GitHub - ini adalah projek Maven, jadi mudah untuk diimport dan dijalankan sebagaimana adanya.