1. 概要

この記事では、Kotlin言語の一般的な型について説明します。

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

2. パラメータ化されたクラスの作成

パラメータ化されたクラスを作成するとします。 ジェネリック型を使用すると、Kotlin言語でこれを簡単に行うことができます。

class ParameterizedClass<A>(private val value: A) {

    fun getValue(): A {
        return value
    }
}

コンストラクターを使用するときにパラメーター化された型を明示的に設定することにより、このようなクラスのインスタンスを作成できます。

val parameterizedClass = ParameterizedClass<String>("string-value")

val res = parameterizedClass.getValue()

assertTrue(res is String)

幸い、Kotlinはパラメーター型から汎用型を推測できるため、コンストラクターを使用するときにそれを省略できます。

val parameterizedClass = ParameterizedClass("string-value")

val res = parameterizedClass.getValue()

assertTrue(res is String)

3. Kotlin outおよびinキーワード

3.1. アウトキーワード

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

Kotlinを使用してそれを実現するには、 を使用する必要がありますジェネリック型のoutキーワード。 これは、この参照をそのスーパータイプのいずれかに割り当てることができることを意味します。 アウト値は、指定されたクラスによってのみ生成でき、消費されません

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

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

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

val parameterizedProducer = ParameterizedProducer("string")

val ref: ParameterizedProducer<Any> = parameterizedProducer

assertTrue(ref is ParameterizedProducer<Any>)

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

3.2. inキーワード

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

サブタイプの参照に割り当てる場合は、汎用タイプでinキーワードを使用できます。 inキーワードは、生成されるのではなく、消費されるパラメータータイプでのみ使用できます。

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

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

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

val parameterizedConsumer = ParameterizedConsumer<Number>()

val ref: ParameterizedConsumer<Double> = parameterizedConsumer

assertTrue(ref is ParameterizedConsumer<Double>)

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

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

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

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

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

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

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

val ints: Array<Int> = arrayOf(1, 2, 3)
val any: Array<Any?> = arrayOfNulls(3)

copy(ints, any)

assertEquals(any[0], 1)
assertEquals(any[1], 2)
assertEquals(any[2], 3)

4.2. サブタイプの要素をそのスーパータイプの配列に追加する

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

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

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

val objects: Array<Any?> = 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. 一般的な制約

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

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

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

Compareable、を実装する要素のリストを引数として取る 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のリストを簡単に渡すことができます。

5.1. 複数の上界

アングルブラケット表記では、最大で1つの一般的な上限を宣言できます。 型パラメーターに複数の一般的な上限が必要な場合は、その特定の型パラメーターに個別のwhere句を使用する必要があります。 例えば:

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

上記のように、パラメータ T は、CharSequenceおよびComparableインターフェイスを同時に実装する必要があります。 同様に、複数の一般的な上限を持つクラスを宣言できます。

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

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

6.1. 型消去

Javaと同様に、Kotlinのジェネリックは実行時に消去されます。 つまり、汎用クラスのインスタンスは、実行時にその型パラメーターを保持しません

たとえば、 設定それにいくつかの文字列を入れます。実行時には、それを次のようにしか見ることができません。 設定

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

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

実行時のタイプ情報設定設定消去され、両方ともプレーンとして表示されますセット。 したがって、実行時にその値が設定 、それが設定文字列、整数、またはその他のものの: その情報は消去されました。

では、Kotlinのコンパイラはどのようにして私たちが非文字列設定 ? または、から要素を取得するとき設定 、要素が

答えは簡単です。 コンパイラは型情報の消去を担当しますが、その前に、books変数にString要素が含まれていることを実際に認識しています。

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

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

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

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

itisT」の部分では、コレクション要素ごとに、要素がタイプ T のインスタンスであるかどうかを確認しますが、タイプ情報は実行時に消去されているため、次のことができます。この方法で型パラメーターを反映します。

それともできますか?

型消去規則は一般的に当てはまりますが、この制限を回避できる場合が1つあります。それは、インライン関数です。 インライン関数の型パラメーターを変更できるため、実行時にそれらの型パラメーターを参照できます。

インライン関数の本体はインライン化されています。 つまり、コンパイラは、通常の関数呼び出しではなく、関数が呼び出される場所に直接本体を置き換えます。

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

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

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

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

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

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

洗練されたインライン関数を使用して、よりエレガントで構文の恐怖が少ないLogger定義を記述できます。

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

次に、次のように書くことができます。

class User {
    private val log = logger<User>()

    // ...
}

これにより、ロギング、Kotlinウェイを実装するためのよりクリーンなオプションが提供されます。

6.3. インライン具体化の詳細

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

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

たとえば、次のように記述します。

class User {
    private val log = logger<User>()

    // ...
}

コンパイラがロガーをインライン化するとき ()関数呼び出し、それは実際のジェネリック型パラメーターを知っています– ユーザー。 したがって、コンパイラーは、タイプ情報を消去する代わりに、具体化の機会をつかみ、実際のタイプパラメーターを具体化します。

7. 結論

この記事では、KotlinGenericタイプについて説明しました。 outおよびinキーワードの適切な使用方法を確認しました。 タイププロジェクションを使用し、一般的な制約を使用する一般的なメソッドを定義しました。

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