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

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

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

この記事は Qiita の記事 と同じものです。

データクラス

何もしないけどデータだけ保持したいクラスってよく作るよね?そういうのには 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() }
}

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

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

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

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

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

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

Tags