1. 概要

このチュートリアルでは、 reified インライン関数が、エレガントで簡潔な一般的な抽象化を作成するのにどのように役立つかを見ていきます。

まず、型消去と、それがコードの可読性にどのように影響するかについて説明します。 次に、Kotlinがreifiedインライン関数の問題をどのように解決するかを確認します。

2. 型消去

KotlinおよびJavaeraseの一般的な型情報をコンパイル時に実行します。 つまり、次のような汎用型パラメータ 、は単にソースコードに存在します。

したがって、ジェネリック型のすべての可能な形式は、実行時に1つの単純なraw型として現れます。 例えば、 リストリスト両方ともリスト実行時。 この動作は消去として知られています。

時々、消去は私たちにエレガントでない方法で単純な考えを表現することを強いるでしょう。 たとえば、多くのJava開発者は、JSONを解析するためにこれを作成することに慣れています。

String json = objectMapper.readValue(data, String.class);

怪我に侮辱を加えると、スーパータイプトークンパターンはコードをさらに読みにくくします。

Map<String, String> json = objectMapper.readValue(data, 
  new TypeReference<Map<String, String>>() {});

これはすべて、汎用パラメーターのクラスメタデータにアクセスできないためです。

public <T> T readValue(byte[] data) {
    Class<T> type = T.class; // won't compile
}

T は実行時に変更されないため、JavaではT.classのようなものを使用できません。 技術的に言えば、タイプは実行時に存在する場合に修正されます。

Kotlinがこの制限をどのように修正するかを見てみましょう。

3. インライン関数

インライン関数は、コンパイル時のもう1つの錯覚です。 それらはソースコードに存在しますが、コンパイラは各呼び出しサイトでそれらの本体を置き換え、それによって関数呼び出しを完全に回避します。

例えば:

fun main() {
    printHello()
}

inline fun printHello() {
    print("Hello ")
    println("World")
}

Kotlinのコンパイラが printHello としてマークされているため、本体を使用した関数呼び出し列をなして。 したがって、の実行時表現主要機能は次のとおりです。

fun main() {
    print("Hello ")
    println("World")
}

4. 洗練された機能

インライン化はエキサイティングな機会をもたらします:実行時にジェネリック型情報を再定義します。 つまり、引数として T ::classの形式で型情報を渡す必要がなくなりました。

必要なのは、typeパラメーターをreifiedキーワードでマークすることだけです。

inline fun <reified T> ObjectMapper.readValue(data: ByteArray): T =
  readValue(data, object : TypeReference<T>() {})

これが可能なのは、Kotlinが関数をインライン化し、呼び出しサイトのコンテキストをすでに知っているためです。

したがって、この拡張関数を使用すると、型情報を明示的に渡すことなく、readValueメソッドを簡単に呼び出すことができます。

val json = objectMapper.readValue<String>(data)

同様に、一般的な構文を完全に回避できます。

val json: String = objectMapper.readValue(data)

さらに、拡張機能にすでにカプセル化されているため、スーパータイプのトークンも必要ありません。

val json: Map<String, String> = objectMapper.readValue(data)

5. バイトコード表現

Kotlinでreified関数を使用する方法がわかったので、それらが内部でどのように見えるかを見てみましょう。

val objectMapper = ObjectMapper()
val data = """{"answer": 42}""".toByteArray()
val json: Map<String, String> = objectMapper.readValue(data)

そのために、前のスニペットに対して生成されたバイトコードを見てみましょう。

// omitted 
35: aload_3
36: aload_1
37: new           #65                 // class MainKt$main$$inlined$readValue$1
40: dup
41: invokespecial #66                 // Method MainKt$main$$inlined$readValue$1."<init>":()V
44: checkcast     #30                 // class TypeReference
47: invokevirtual #35                 // Method ObjectMapper.readValue:([BLTypeReference;)LObject;
50: checkcast     #68                 // class Map
53: astore_2

このバイトコードは、次の手順を実行します。

  • JSONバイト配列をロードし、インライン拡張関数を準備します
  • ロードされたパラメータをreadValueメソッドに渡し、そのメソッドを呼び出します(invokevirtual)
  • Object のインスタンスを返します(消去が何をするか見てください!)
  • 返されたオブジェクトマップにキャストします。これは、コンテキストからマップである必要があることがわかっているためです。

基本的に、このバイトコードはJavaコードと同等です。

Map<String, String> json = objectMapper.readValue(data, 
  new TypeReference<Map<String, String>>() {});

ただし、次の構文を使用します。

val json: Map<String, String> = objectMapper.readValue(data)

これは、まったく同じ内部表現を備えたはるかに快適な構文ですが、さらに別のコンパイラの錯覚です。

6. 結論

このチュートリアルでは、reifiedタイプのパラメーターが解決しようとしている問題について理解しました。 次に、それらの使用方法と、内部でどのように見えるかを確認しました。