Edit Page

ジェネリクス

Javaと同じように、Kotlinのクラスは型パラメータを持っている場合があります。

class Box<T>(t: T) {
  var value = t
}

一般的に、このようなクラスのインスタンスを作成するために、我々は、型引数を提供する必要があります。

val box: Box<Int> = Box<Int>(1)

しかし、パラメータを推測することができる場合には、(例えば、コンストラクタの引数から、または何らかの他の手段によって)、「1」は、型引数を省略することができます:

val box = Box(1) // 1 は Int型をもつため、ここでは Box<Int> について話しているとコンパイラはわかる

分散

Javaの型システムの最もトリッキーな部分の一つは、ワイルドカードの種類(JavaのジェネリックのFAQを参照してください)です。そして、Kotlinは、いずれも持っていません。その代わり、2つの別のものがあります:宣言箇所の分散と型プロジェクションです。

まずは、Javaがこれらの神秘的なワイルドカードを必要とする理由について考えてみましょう。この問題はEffective Javaの項目28「APIの柔軟性を高めるためのバインドされたワイルドカードの使用」で説明されています。まず、Javaでジェネリック型は不変です。これは、 List<String>List<Object> のサブタイプ ではない ことを意味します。なぜそうなのか?もしリストが 不変 でなかった場合は、次のコードはコンパイルされ、実行時に例外を発生させていたので、それは、Javaの配列より良いものではなかったでしょう。

// Java
List<String> strs = new ArrayList<String>();
List<Object> objs = strs; // !!! 今後の問題の原因はここにあります。 Javaはこれを禁止しています!
objs.add(1); // Integer を Strings のリストへ入れる
String s = strs.get(0); // !!! ClassCastException: Integer を String へキャストできない

つまり、Javaの実行時の安全性を保証するためにそのようなことを禁止しているのです。しかし、これはいくつかの意味があります。例えば、 Collection インタフェースからの addAll() メソッドを考えます。このメソッドのシグネチャは何でしょうか?直感的に、我々はそれをこのように置くと思います。

// Java
interface Collection<E> ... {
  void addAll(Collection<E> items);
}

しかし、その後、次のような簡単なこと(完全に安全である)を行うことができなくなります。

// Java
void copyAll(Collection<Object> to, Collection<String> from) {
  to.addAll(from); // !!! addAllのネイティブの宣言ではコンパイルできません:
                   //       Collection<String> は Collection <Object> のサブタイプではありません
}

(Javaでは、この教訓に堅い方法を学びました。 Effective Javaの項目25「 配列よりリストを好む 」を参照してください。)

これが、 addAll() の実際のシグネチャが以下の通りになる理由です:

// Java
interface Collection<E> ... {
  void addAll(Collection<? extends E> items);
}

ワイルドカード型引数 ? extends T は、このメソッドが受け入れるコレクションは T のサブタイプ のオブジェクトであって、 T 自身ではないことを示します。 私たちが安全に T の項目(このコレクションの要素は T のサブクラスのインスタンスです)を 読み取る ことができても、未知の T のサブタイプに対して、どのオブジェクトが応じるかわからないため、 書き込みができない 理由はこれです。 この制限と引き換えに、私たちは望んだ動作を得ます: Collection<String>Collection<? extends Object> のサブタイプ である ということ。 「賢い言葉」で言い換えると、 拡張する バインド( 上昇 のバインド)のワイルドカードは 型共変 になります。

このトリックがなぜ働くのかを理解するための鍵は、かなりシンプルです:コレクションからアイテムを 取り出す ことだけできるのならば、String のコレクションを使用して、 Object で読み出せば良いのです。 反対に、コレクションにアイテムを 入れる ことだけできるのならば、 Object のコレクションを使用し、 String を入れても良いのです。 Javaでは List<Object>スーパータイプ である、 List<? super String> を使用します。

後者は 反変性 と呼ばれ、 String を List<? super String> の引数としたメソッドを呼ぶことのみができます(例えば、 add(String)set(int, String) を呼ぶことができます)。ただし、List<T>T を返す何かを呼んだとき、得るのは String ではなく Object ですが。

ジョシュア・ブロック (Joshua Bloch) はこれらのオブジェクトを 「 プロデューサ(生産者) からのみ 読み込みコンシューマ(消費者) にのみ 書き込む 」と呼びました。彼の勧めによると、 「最大の柔軟性を得るために、プロデューサやコンシューマを表す入力パラメータにワイルドカードタイプを使用する」 。 次の記憶術 (mnemonic) も提案しています。

PECSはProducer-Extends, Consumer-Super を意味します。

:プロデューサオブジェクトを使用する場合(たとえば、 List<? extends Foo> )、このオブジェクト上の add()set() を呼び出すことができません。しかし、このオブジェクトは イミュータブル(不変) であるというわけでもありません。例えば、 clear() は全くパラメータを取らないため、リストからすべての項目を削除するために clear() を呼び出しても構いません。ワイルドカード(または分散の他の型)によって唯一保証されるのは 型の安全性 です。不変性は全く別の話です。

宣言箇所分散

ジェネリックインターフェイスの Source<T> があると仮定します。また、パラメータとして T をとるメソッドを持たず、 T を返すメソッドのみを持つとします。

// Java
interface Source<T> {
  T nextT();
}

それは Source<Object> 型の変数(呼び出せるコンシューマメソッドがない)内で Source<String> のインスタンスへの参照を保持するのに完全に安全です。 – しかし、Javaはこれを知っているし、まだそれを禁止していません:

// Java
void demo(Source<String> strs) {
  Source<Object> objects = strs; // !!! Java では許可されていない
  // ...
}

これを修正するために、Source<? extends Object> 型のオブジェクトを宣言する必要があります。全ての同メソッドを前のような変数で呼ぶことができるので、順序に意味はなく、より複雑な型で追加することに価値はありません。しかし、コンパイラはそれを知りません。

Kotlinでは、コンパイラにこの種の問題を説明する方法があります。これは、 宣言箇所分散 と呼ばれています:ソースの 型パラメータ TSource<T> のメンバからのみ 返し (プロデュースする)、消費されることがないということを確認するために、アノテーションを付けることができます。これを行うために、我々は out 修飾子を提供します。

abstract class Source<out T> {
  abstract fun nextT(): T
}

fun demo(strs: Source<String>) {
  val objects: Source<Any> = strs // これは OK 、なぜなら T はoutパラメータのため
  // ...
}

一般的なルールは次のとおりです。クラス C の型パラメータ T が、 out として宣言されているとき、 C のメンバの中で out の位置でのみそれが起きることがあります。 しかし、 C<Base> を返すときは C<Derived> のスーパータイプに安全になり得ます。

「賢い言葉」でいうと、クラス C は、パラメータ T共変 である、または T共変 の型パラメータであるとなります。 CTプロデューサ であり、 Tコンシューマ ではない、と考えることができます。

out 修飾子は、 分散アノテーション と呼ばれ、それは型パラメータの宣言箇所で提供されているので、我々は 宣言箇所分散 について話しています。 これは、ワイルドカードが使用時に型を共変にする、Javaの 使用箇所分散 とは対照的です。

out に加えて、Kotlinは in という補完的な分散アノテーションを提供します。これは、型パラメータの 反変 を行います。消費されるのみであり、決してプロデュース(生産)されません。 反変クラスの良い例は Comparable です:

abstract class Comparable<in T> {
  abstract fun compareTo(other: T): Int
}

fun demo(x: Comparable<Number>) {
  x.compareTo(1.0) // 1.0 は Number のサブタイプである Double 型をもつ
  // それゆえ、 x を Comparable<Double> 型の変数へ代入できる
  val y: Comparable<Double> = x // OK!
}

(C#で何度も成功しているように)inout は自己説明的であるゆえに、以前述べた記憶術(ニーモニック)は本当は不要で、より高次の目的のために言い換えることができます:

実存的言い換え:コンシューマ(消費者)は in、プロデューサ(生産者)は out ! :-)

タイププロジェクション(型投影)

利用箇所の分散:タイププロジェクション

out 型パラメータTを宣言し、使用箇所のサブタイプとの問題がないことは非常に便利です。 そう、それは問題のクラスが実際に T のインスタンスのみを返すよう制限 できる ときですが、できないのはどんなときでしょう。この良い例は、Arrayです。

class Array<T>(val size: Int) {
  fun get(index: Int): T { /* ... */ }
  fun set(index: Int, value: T) { /* ... */ }
}

このクラスは T の共変または反変のいずれかにもなることはできません。そして、これは特定の不撓(ふとう)性(曲げられない特性)を課しています。次の関数を考えてみます:

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

この関数は、ある配列から別の配列へ、アイテムをコピーすることになっています。それでは、実際にそれを適用してみましょう:

val ints: Array<Int> = arrayOf(1, 2, 3)
val any = Array<Any>(3)
copy(ints, any) // エラー: (Array<Any>, Array<Any>) が期待されている

ここで同じようなよくある問題に遭遇します: Array<T>T において 不変 であり、ゆえに Array<Int>Array<Any> も他のサブタイプではありません。 どうして?コピーが何か悪いこと(すなわち from への文字列の 書き込み や出力の試行)をやっている 可能性がある ためです。また Int の配列を実際に渡されると、ClassCastException が時々後で投げられるでしょう。

ここで、唯一の保証したいことは、copy() がいかなる悪さもしないということです。 copy()from に書き込むことを禁止したく、それを行うことはできます:

fun copy(from: Array<out Any>, to: Array<Any>) {
 // ...
}

ここで起こったことは、 タイププロジェクション(型投影) と呼ばれています。 from は単純に配列なのではなく、制限された( 投影された )ものであるということです。型パラメータ T を返すこれらのメソッドはこのケースでのみ呼び出すことができます。 つまり、 get() を呼ぶことのみができるということです。これが、 使用箇所分散 のための我々のアプローチであり、Javaの Array<? extends Object> に対応しますが、少しだけ簡単な方法です。

このように in でタイププロジェクション(型投影)を使用できます:

fun fill(dest: Array<in String>, value: String) {
  // ...
}

Array<in String> は Javaの Array<? super String> に対応します。すなわち、 CharSequence の配列や Object の配列を fill() 関数へ渡すことができます。

スタープロジェクション (star-projections)

型引数について何も知らないが、それでも安全な方法で使用したいと、時には言いたくなることもあるでしょう。ここでの安全な方法とは、ジェネリック型のそのようなプロジェクションを定義することです。ジェネリック型を具体的にインスタンス化すると、全てそのプロジェクションのサブタイプになります。

Kotlinはこのために、いわゆる スタープロジェクション (star-projection) 構文を提供します:

  • Foo <out T> の場合、T は上限 TUpper を持つ共変の型のパラメータであり、 Foo <*>Foo<out TUpper> と等価です。これは、 T が不明な場合に、安全には Foo <*> から TUpper の値を読み取ることができることを意味します。
  • T が反変の型パラメータである Foo<in T> については、 Foo<*>Foo <in Nothing> と等価です。それは T は不明である場合は安全な方法で Foo <*> に書き込むことができる方法がないことを意味します。
  • Foo <T> の場合、 T は上限 TUpper を持つ不変の型パラメータであり、値を読み込むための Foo<out TUpper>および値を書き込むための Foo<in Nothing>Foo <*> は同等です。

ジェネリック型がいくつかの型パラメータをもつ場合、それらは独立してプロジェクション(投影)することができます。 例えば、型が interface Function<in T, out U> として宣言されている場合ならが、次のようなスタープロジェクションを想像することができます:

  • Function<*, String>Function<in Nothing, String> を意味します
  • Function<Int, *>Function<Int, out Any?> を意味します
  • Function<*, *>Function<in Nothing, out Any?> を意味します

注意 :スタープロジェクションは非常にJavaの raw タイプににていますが、安全です。

ジェネリック関数

型パラメータを持つことができるのはクラスだけではありません。関数も同じです。 型パラメータは、関数名の前に置かれます。

fun <T> singletonList(item: T): List<T> {
  // ...
}

fun <T> T.basicToString() : String {  // 拡張関数
  // ...
}

ジェネリック関数を呼び出すには、関数名の 後に 呼び出し箇所で型引数を指定します。

val l = singletonList<Int>(1)

ジェネリックの制約

与えられる型パラメータに置換することができるすべての許容される型の集合は、 ジェネリックの制約 によって制限されてもかまいません。

上限 (Upper bounds)

制約の最も一般的なタイプは、Javaの extends キーワードに対応する 上限 です。

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

コロンの後に指定されたタイプが 上限 です。 Comparable<T> のサブタイプは T の代わりに使用することができます。例えば:

sort(listOf(1, 2, 3)) // OK. Int は Comparable<Int> のサブタイプです
sort(listOf(HashMap<Int, String>())) // エラー: HashMap<Int, String> は Comparable<HashMap<Int, String>> のサブタイプではない

デフォルトの上限(いずれも指定されていない場合)は Any? です。唯一の上限を、角括弧内で指定することができます。 同じ型パラメータに複数の上限を必要とする場合、それぞれ独立した where 句が必要になります:

fun <T> cloneWhenGreater(list: List<T>, threshold: T): List<T>
    where T : Comparable,
          T : Cloneable {
  return list.filter { it > threshold }.map { it.clone() }
}