コトリンのジェネリック医薬品

コトリンのジェネリック

1. 概要

この記事では、generic types in the Kotlin languageを見ていきます。

これらはJava言語のものと非常に似ていますが、Kotlin言語の作成者は、outin.などの特別なキーワードを導入することで、それらをもう少し直感的で理解しやすいものにしようとしました。

3. Kotlinoutおよびinキーワード

3.1. Outキーワード

あるタイプTの結果を生成するプロデューサークラスを作成するとします。 時々;その生成された値を、型Tのスーパータイプの参照に割り当てます。

Kotlinを使用してこれを実現するには、we need to use theout keyword on the generic type. It means that we can assign this reference to any of its supertypes. The out value can be only be produced by the given class but not consumed

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

タイプTの値を生成できるParameterizedProducerクラスを定義しました。

次; ParameterizedProducerクラスのインスタンスを、そのスーパータイプである参照に割り当てることができます。

val parameterizedProducer = ParameterizedProducer("string")

val ref: ParameterizedProducer = parameterizedProducer

assertTrue(ref is ParameterizedProducer)

ParamaterizedProducerクラスのタイプToutタイプでない場合、指定されたステートメントはコンパイラエラーを生成します。

3.2. inキーワード

場合によっては、逆の状況が発生します。つまり、タイプTの参照があり、それをTのサブタイプに割り当てたいということです。

We can use the in keyword on the generic type if we want to assign it to the reference of its subtype. The in keyword can be used only on the parameter type that is consumed, not produced

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

toString()メソッドはタイプTの値のみを消費することを宣言します。

次に、タイプNumberの参照をそのサブタイプの参照に割り当てることができます–Double:

val parameterizedConsumer = ParameterizedConsumer()

val ref: ParameterizedConsumer = parameterizedConsumer

assertTrue(ref is ParameterizedConsumer)

ParameterizedCounsumerのタイプTinタイプでない場合、指定されたステートメントはコンパイラエラーを生成します。

4. タイププロジェクション

4.1. サブタイプの配列をスーパータイプの配列にコピーします

あるタイプの配列があり、配列全体をAnyタイプの配列にコピーするとします。 これは有効な操作ですが、コンパイラーがコードをコンパイルできるようにするには、入力パラメーターにoutキーワードで注釈を付ける必要があります。

これにより、コンパイラは、入力引数がAnyのサブタイプである任意のタイプである可能性があることを認識します。

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

fromパラメータがout Anyタイプでない場合、引数としてIntタイプの配列を渡すことはできません。

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. サブタイプの要素をそのスーパータイプの配列に追加する

次のような状況があるとします。IntのスーパータイプであるAny型の配列があり、この配列にInt要素を追加したいとします。 Int値をこの配列:にコピーできることをコンパイラーに通知するために、宛先配列のタイプとしてinキーワードを使用する必要があります。

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

次に、Intタイプの値をAny:の配列にコピーできます。

val objects: Array = arrayOfNulls(1)

fill(objects, 1)

assertEquals(objects[0], 1)

4.3. スタープロジェクション

特定のタイプの値を気にしない場合があります。 配列のすべての要素を出力したいだけで、この配列の要素のタイプは関係ないとします。

それを実現するために、スター投影を使用できます。

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

次に、任意のタイプの配列をprintArray()メソッドに渡すことができます。

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

スター投影参照型を使用すると、そこから値を読み取ることができますが、コンパイルエラーが発生するため、書き込むことはできません。

5. 一般的な制約

要素の配列を並べ替えたいとし、各要素タイプはComparableインターフェースを実装する必要があるとしましょう。 一般的な制約を使用して、その要件を指定できます。

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

与えられた例では、Comparableインターフェースを実装するために必要なすべての要素Tを定義しました。 そうでない場合、このインターフェイスを実装しない要素のリストを渡そうとすると、コンパイラエラーが発生します。

Comparable,を実装する要素のリストを引数として取るsort関数を定義したので、sorted()メソッドを呼び出すことができます。 そのメソッドのテストケースを見てみましょう。

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

val sorted = sort(listOfInts)

assertEquals(sorted, listOf(1,2,3,4,5))

IntタイプはComparableインターフェースを実装しているため、Intsのリストを簡単に渡すことができます。

6. 実行時のジェネリック

6.1. タイプ消去

Javaと同様に、Kotlinのジェネリックは実行時にerasedです。 つまり、an instance of a generic class doesn’t preserve its type parameters at runtimeです。

たとえば、Set<String>を作成し、それにいくつかの文字列を挿入すると、実行時にはSetとしてのみ表示されます。

2つの異なるタイプパラメータを使用して2つのSetsを作成しましょう。

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

実行時に、Set<String>Set<Int>の型情報が消去され、両方ともプレーンなSets. として表示されます。したがって、実行時にその値が%であることを確認することは完全に可能ですが、 (t3)s、それが文字列、整数、または他の何かのSetであるかどうかはわかりません:that information has been erased.

では、KotlinのコンパイラはどのようにしてNon-StringSet<String>に追加できないようにするのでしょうか? または、Set<String>から要素を取得すると、その要素がStringであることがどのようにわかりますか?

答えは簡単です。 The compiler is the one responsible for erasing the type informationですが、その前に、books変数にString要素が含まれていることを実際に認識しています。

したがって、要素を取得するたびに、コンパイラはそれをStringにキャストするか、要素を追加するときに、コンパイラは入力をタイプチェックします。

6.2. 洗練されたタイプパラメータ

ジェネリックスをもっと楽しんで、タイプに基づいてCollection要素をフィルタリングする拡張関数を作成しましょう。

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

it is T”部分は、各コレクション要素について、要素がタイプTのインスタンスであるかどうかをチェックしますが、タイプ情報は実行時に消去されているため、この方法でタイプパラメーターを反映することはできません。 。

それともできますか?

型消去ルールは一般的に当てはまりますが、この制限を回避できるケースが1つあります。それはInline functionsです。 Type parameters of inline functions can be reified, so we can refer to those type parameters at runtime.

インライン関数の本体はインライン化されています。 つまり、コンパイラは、通常の関数呼び出しの代わりに、関数が呼び出される場所に本体を直接代入します。

前の関数をinlineとして宣言し、型パラメーターをreifiedとしてマークすると、実行時にジェネリック型情報にアクセスできます。

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

インライン具体化は魅力のように機能します。

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

別の例を書いてみましょう。 私たちは皆、それらの典型的なSLF4jLoggerの定義に精通しています。

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

    // ...
}

具象化されたインライン関数を使用すると、よりエレガントで構文の恐れが少ないLogger定義を記述できます。

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

次に、次のように記述できます。

class User {
    private val log = logger()

    // ...
}

これにより、logging, the Kotlin wayを実装するためのよりクリーンなオプションが得られます。

6.3. Deep Dive into Inline Reification

では、インライン関数の何が特別なので、型の具体化はそれらでのみ機能するのでしょうか。 ご存知のとおり、Kotlinのコンパイラは、インライン関数のバイトコードを関数が呼び出される場所にコピーします。

各呼び出しサイトで、コンパイラーは正確なパラメーター型を知っているため、ジェネリック型パラメーターを実際の型参照に置き換えることができます。

たとえば、次のように書くと:

class User {
    private val log = logger()

    // ...
}

When the compiler inlines the logger<User>() function call, it knows the actual generic type parameter –User.したがって、コンパイラは、型情報を消去する代わりに、具体化の機会を捉えて、実際の型パラメータを具体化します。

7. 結論

この記事では、Kotlinジェネリック型を見ていました。 outおよびinキーワードを適切に使用する方法を見ました。 型射影を使用し、汎用制約を使用する汎用メソッドを定義しました。

これらすべての例とコードスニペットの実装は、GitHub projectにあります。これはMavenプロジェクトであるため、そのままインポートして実行するのは簡単です。