Kotlin文法 - データクラス, ジェネリクス

More than 3 years have passed since last update.


はじめに

Kotlin文法 - インターフェース、アクセス修飾子、拡張 の続き。

Kotlin ReferenceのClasses and Objects章Data Classes, Genericsの大雑把日本語訳。適宜説明を変えたり端折ったり補足したりしている。


データクラス

何もしないけどデータだけ保持したいクラスってよく作るよね?そういうのには data って付けよう。

data class User(val name: String, val age: Int)

これだけでコンパイラが勝手に以下のものを作ってくれる。ただしクラス内または継承元に明示的に定義されていれば勝手に生成したりしない。


  • equals()/hashCode()ペア

  • "User(name=John, age=42)"って表示するtoString()

  • 宣言順で内容を取り出すcomponentN()関数

  • copy()

生成されるコードに一貫性を保つため、データクラスは以下を満たす必要がある。


  • プライマリコンストラクタは少なくとも1つ引数を持つ

  • 全てのプライマリコンストラクタ引数はvalかvarでマークされている

  • abstract, open, sealed, innerであってはならない

  • データクラスは他のクラスを継承してはいけない(がインターフェースを実装するかもしれない)

JVM上では生成されるクラスにパラメータなしコンストラクタが必要になるなら、全てのプロパティにデフォルト値が与えられている必要がある。

data class User(val name: String = "", val age: Int = 0)


コピー

プロパティの一部だけ変えたコピーが欲しいことってちょくちょくあるよね?そんなときは copy() を使おう。

// こんなメソッドがコンパイラによって自動生成される

fun copy(name: String = this.name, age: Int = this.age) = User(name, age)

こんな感じで使う。

val jack = User(name = "Jack", age = 1)

val olderJack = jack.copy(age = 2) // nameはそのままでageだけ変更したコピーを生成


分解宣言

val jane = User("Jane", 35)   // データクラスのインスタンス作成

val (name, age) = jane // 中身をバラして取り出す
println("$name, $age years of age") // "Jane, 35 years of age"って表示される


標準データクラス

PairTriple が標準ライブラリに用意されてるよ。でも大抵の場合はちゃんと名前をつけたデータクラスを用意した方がいい。コードが読みやすくなるからね。


ジェネリクス

JavaのようにKotlinのクラスも型パラメータを持てる。

class Box<T>(t: T) {

var value = t
}

// 一般書式では型引数を与えて使う
val box: Box<Int> = Box<Int>(1)

// でも型推論できる場合は省略できる
val box2 = Box(1) // 1の型はIntなのでコンパイラ にはBox<Int>だと分かる


変性(Variant)

Javaの型システムで最もトリッキーなのはワイルドカード型。Kotlinはそんなん持ってない。代わりに宣言側変性(declaration-site variant)1と型投影(type projections)の2つを導入してる。

※Kotlin本家のリファレンスマニュアルには、ここにJavaのジェネリクスの変性についての話が書いてあるけど、Javaの話なので省略。"? extends T" とか "? super T" をどうやって使うかって話。PECS(Producer-Extends Consumer-Super)原則とか。

※変性(variant)についてのJavaに限らない一般的な説明は共変性と反変性 (計算機科学)を参照。

※変性についてあまり難しく考えないで! 変性について考えられたジェネリックなクラスや関数を設計するのは難しいかもしれないけど、ほとんどの人は出来なくても大丈夫。ジェネリックなクラスや関数を利用するのは難しくないから。ほとんどのJavaプログラマが、ちゃんと理解してなくてもワイルドカード使ったライブラリを普通に使ってるはず。


宣言側変性(Declaration-site variant)

Tを引数として受け取るメソッドは持たず、Tを返すメソッドしか持たないジェネリックインターフェース Source<T> を考えてみよう。

// Java

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

Source<String> インスタンスへの参照を Source<Object> 型の変数へ代入するのは完全に安全なはずだ。消費する(consumer)メソッド2はないから。でもJavaはこれを許可してくれない。

// Java

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

これを解決するにはobjectsを Source<? extends Object> 型として宣言しなければならない。

Kotlinでは宣言側変性(Declaration-site variant)を使ってコンパイラに型パラメータ T戻り値にしか使われないことを伝えられる。これには out を使う。

// <out T>と書いてるから、Tは戻り値の型としてしか使わないよ

abstract class Source<out T> {
abstract fun nextT(): T // 戻り値にTを使ってる
// 引数としてTを使ってるメソッドはない
}

fun demo(strs: Source<String>) {
val objects: Source<Any> = strs // これはOKだよ。だってTはoutパラメータだから。
// ...
}

一般的なルールはこうだ。クラス C の out として宣言された型パラメータ T は、C のメンバの出力位置にしか現れない。その見返りとして C<Base> は 安全にC<Derived> のスーパー型になれる。3

out 修飾子は変性アノテーションと呼ばれている。そして型パラメータの宣言側で提供されるので、宣言側変性であると言える。これはJavaのワイルドカードが利用側変性であるのと対照的だ。

out に加えて in もある4

// <in T>と書いているから、Tは引数の型としてしか使わないよ

abstract class Comparable<in T> {
abstract fun compareTo(other: T): Int // 引数としてTを使ってる
// 戻り値としてTを使ってるメソッドはない
}

fun demo(x: Comparable<Number>) {
x.compareTo(1.0) // 1.0の型はNumberのサブクラスであるDouble
// だからComparable<Double>型の変数にxを代入できる
val y: Comparable<Double> = x // OK!
// Comparable<Double>のメソッドは引数としてDoubleを取るが、
// Comparable<Number>のメソッドに引数としてDoubleを渡せるんだから、
// Comparable<Number>をComparable<Double>として扱っても問題ない。
}


型投影(type projection)


利用側変性: 型投影(type projection)

型Tの利用を戻り値だけとか引数だけに絞れない場合を考えてみよう。

class Array<T>(val size: Int) {

fun get(index: Int): T { /* ... */ }
fun set(index: Int, value: T) { /* ... */ }
}

このクラスでは outin も書いてないから T は共変(covariant)でも反変(contravariant)でもなく不変(invariant)。次の関数を考えてみよう。

// fromからtoへコピーする

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) // Error: (Array<Any>, Array<Any>)を想定してるのにintsが違う

ここでcopyのfromの型が違うためにコンパイルエラーとなる。しかしここでのcopyはfromのうち戻り値にTを使ったメソッド(つまりget)しか使わない。これをコンパイラに知らせるために、次のように書ける。

// 引数fromの型パラメータにoutを指定してるから、

// fromは戻り値に型パラメータを使うメソッドしか使えないよ。
fun copy(from: Array<out Any>, to: Array<Any>) {
// ...
}

ここで起こっていることは型投影(type projection)5と呼ばれている。ここでのfromは単なる配列ではなく、制限された(投影された(projected))それ。これがJavaの Array<? extends Object> に対応する、我々の利用側変性へのアプローチ。

in を使った投影も同様に使える。

// 引数destの型パラメータにinを指定してるから、

// destは引数に型パラメータを使うメソッドしか使えないよ。
fun fill(dest: Array<in String>, value: String) {
// ...
}

Array<in String> はJavaの Array<? super String> に対応する。すなわちfill()関数に CharSequence の配列や Object の配列を渡せる.


Star-projection

※ここのオリジナルの説明はわかりにくいので、完全に別の説明に書き換え。プログラミング言語Kotlin 解説の説明を拝借。

Javaで Foo<?> って書きたい時がある。Kotlinでそれに相当するのが Foo<*> で、Star-projectionと呼んでいる。これは Foo<out Any?> の略記法。Javaの Foo<?> や型を省略したraw type(つまり Foo)よりずっと安全。

val ints: Array<Int> = arrayOf(1, 2, 3)

ints.add(100)

// Star-projectionを使って、型パラメータが何かわかんなくても代入できる変数を用意。
// <out Any?>と同等であり、型パラメータを戻り値として利用しているメソッドしか利用できない。
val nums: List<*> = ints

// numsはリストとして何の型を内部に格納しているか不明なので、Any?としてしか取り出せない
val a: Any? = nums.get(0) // OK
// 実際に中に入ってるのはIntなんだけど、numsはそんなの知らないから!!
val n: Number = nums.get(0) // エラー
val i: Int = nums.get(0) // エラー


ジェネリック関数

クラスだけでなく関数も型パラメータを持てる。型パラメータは関数名の前に書く。

fun <T> singletonList(item: T): List<T> {

// ...
}

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

利用時に明示的に型を指定する場合、関数名の後に指定する。

val l = singletonList<Int>(1)


ジェネリック制約6

最も一般的な制約はJavaならextendsを使う上限境界(upper bounds)だね。

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

// ...
}

ここで T は Comparable<T> のサブ型でないといけない。例えば、

// これはOK。IntはComparable<Int>のサブ型なので。

sort(listOf(1, 2, 3))
// これはダメ。HashMap<Int, String>はComparable<HashMap<Int, String>>のサブ型じゃない。
sort(listOf(HashMap<Int, String>()))

(もし指定しなければ)デフォルトの上限境界は Any? になる。< >括弧の中で1つだけ上限境界を指定できる。もし同じ型パラメータが複数の上限境界を持つ必要があるなら、where を使う。

// TはComparableかつCloneableであることをwhereを使って制限

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


次の章へ

次はKotlin文法 - ネストされたクラス、Enumクラス、オブジェクト、委譲、委譲されたプロパティへGO!





  1. C# 4から導入されたout, inと同じ仕組みを採用している。 



  2. つまり引数にTを使うメソッド 



  3. 「賢い言葉」で言うなら、クラス C は T において共変である。または T は共変な型パラメータである。C を T の消費者(consumer)ではなく生産者(producer)として考えることができる。 



  4. これは反変(contravariant)を提供する。 



  5. この訳が適切かどうかがわからないが・・・ 



  6. どうでもいいですが「ジェネリック製薬」と変換されますた。