1概要

この記事では、Kotlin言語の

ジェネリック型

に注目します。

それらはJava言語のものと非常に似ていますが、Kotlin言語の作成者は

out



in.

のような特別なキーワードを導入することによってそれらをもう少し直感的で理解しやすいものにすることを試みました。


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.

out

キーワード

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

Kotlinを使ってそれを実現するには、

総称型で



out

キーワードを使う必要があります。つまり、この参照をそのすべてのスーパータイプに割り当てることができます。 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

クラスの

T

型が

out

型にならない場合、指定された文はコンパイラエラーを生成します。


3.2.

in

キーワード

時々、私たちは

T

型の参照を持ち、それを

T

のサブタイプに代入できるようにしたいという反対の状況を持っています。

  • サブタイプの参照に割り当てたい場合は、総称型に

    in

    キーワードを使用できます。

    in keyword

    は、生成されるのではなく、消費されるパラメータタイプでのみ使用できます** 。

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


toString()

メソッドは

T

型の値のみを消費すると宣言します。

次に、

Number

型の参照をそのサブタイプの参照 –

Double:

に割り当てることができます。

val parameterizedConsumer = ParameterizedConsumer<Number>()

val ref: ParameterizedConsumer<Double> = parameterizedConsumer

assertTrue(ref is ParameterizedConsumer<Double>)


ParameterizedCounsumer

の型

T



in

型にならない場合、指定された文はコンパイラエラーを生成します。


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

パラメータが

out Any

タイプではない場合、

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

のスーパータイプである

Any

型の配列があり、この配列に

Int

要素を追加します。

Int

値をこの配列にコピーできることをコンパイラーに知らせるには、

in

キーワードを宛先配列のタイプとして使用する必要があります。

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一般的な制約

要素の配列をソートしたいとしましょう、そして各要素型は

Comparable

インターフェースを実装するべきです。一般的な制約を使用してその要件を指定できます。

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

与えられた例では、すべての要素

T



Comparable

インターフェースを実装する必要があると定義しました。そうでなければ、このインタフェースを実装していない要素のリストを渡そうとすると、コンパイラエラーが発生します。


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の総称は実行時に消去されます。つまり、

ジェネリッククラスのインスタンスは実行時にその型パラメータを保持しません

たとえば、

Set <String>

を作成してその中にいくつかの文字列を入れると、実行時には

Set

としてしか見ることができません。

2つの異なる型パラメータを持つ2つの

Sets

を作成しましょう。

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

実行時には、

Set <String>



Set <Int>

の型情報は消去され、どちらもプレーンな

Setとして表示されます。つまり、実行時に値が

Set

であることを確認することは完全に可能ですが、それが文字列、整数、または他の何かの

Set__であるかどうかはわかりません。

その情報は消去されています。

では、Kotlinのコンパイラが

Non-String



Set <String>

に追加するのをどのように妨げているのでしょうか。あるいは、

Set <String>

から要素を取得したときに、その要素が

String

であることをどのように認識するのでしょうか。

答えは簡単です。

型情報の消去を担当するのはコンパイラです。

それ以前は、

books

変数に

String

要素が含まれていることを実際に知っています。

そのため、そこから要素を取得するたびに、コンパイラはそれを

String

にキャストするか、要素を追加するときには、入力をチェックします。


6.2. 具体化された型パラメータ

ジェネリックスをもっと楽しんで、その型に基づいて

Collection

要素をフィルタリングするための拡張関数を作成しましょう。

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

各コレクション要素の“

it is T”

部分は、その要素が

T

型のインスタンスであるかどうかをチェックしますが、型情報は実行時に消去されているため、この方法では型パラメータに反映できません。


できますか?

型消去規則は一般的に当てはまりますが、この制限を回避できる場合が1つあります。それは

Inline functions

です。

インライン関数の型パラメータは

reified

にすることができるので、実行時にそれらの型パラメータを参照することができます。

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

前の関数を

inline

として宣言し、typeパラメータを

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定義に精通しています。

[source,java,gutter:,true]

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

   //...
}

具体化されたインライン関数を使用して、より洗練された、構文に煩わしくない__Logger__定義を書くことができます。

[source,actionscript3,gutter:,true]

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

それから我々は書くことができます:

[source,java,gutter:,true]

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

   //...
}

これにより、https://www.baeldung.com/kotlin-logging[logging、Kotlinの方法]を実装するためのよりクリーンなオプションが得られます。

====  **  6.3. インラインの具体化への深い飛び込み**

それでは、インライン関数に関して特別なことは何ですか?型の具体化がそれらと一緒にしか機能しないということですか?私たちが知っているように、Kotlinのコンパイラはインライン関数のバイトコードをその関数が呼ばれる場所にコピーします。

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

たとえば、次のように書きます。

[source,java,gutter:,true]

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

   //...
}

** コンパイラが__logger <User>()__関数呼び出しをインライン化すると、実際の総称型パラメータ -  **  __User.__を認識します。したがって、型情報を消去する代わりに、コンパイラは具体化の機会を捉えて実際の型パラメータを具体化します。

===  **  7. 結論**

この記事では、Kotlinの総称型を調べました。 __out__および__in__キーワードを正しく使用する方法を見ました。型射影を使用し、一般的な制約を使用する一般的な方法を定義しました。

これらすべての例とコードスニペットの実装はhttps://github.com/eugenp/tutorials/tree/master/core-kotlin[GitHubプロジェクト]にあります。そのままインポートして実行します。