Kotlin文法 - 分解宣言、範囲、型チェックとキャスト

Kotlin 文法 - 関数とラムダの続き。

Kotlin Referenceの Other 章 Destructuring Declarations, Ranges, Type Checks and Casts の大雑把日本語訳。適宜説明を変えたり端折ったり補足したりしている。

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

分解宣言

オブジェクトを幾つかの変数に分解できたら便利なことがあるよね。こんな風に。

// personの中身をname, ageにバラして代入
val (name, age) = person

// 個別に使える
println(name)
println(age)

この記法は 分解宣言(destructuring declaration) とよばれている。分解宣言では一度に複数の変数が宣言できる。

分解宣言は以下のコードにコンパイルされる。

val name = person.component1()
val age = person.component2()

もちろん component3(), component4()と続けることができる。これらの componentN()関数は operator キーワードを付けて宣言しておく必要がある。

分解宣言は for ループでも使える。

for ((a, b) in collection) { ... }

例: 関数から2つの値を返す

関数から2つの値を返す必要がある場合を考えよう。例えば結果と何らかのステータスを返す場合。Kotlin でこれをやるには、データクラスを宣言してそのインスタンスを結果として返す方法が使える。

// 2つの値をもつデータクラス
data class Result(val result: Int, val status: Status)

fun function(...): Result {
    // 計算

    // データクラスを使って2つの値を返す
    return Result(result, status)
}

// 関数を使って、結果から2つの値を取り出す
val (result, status) = function(...)

データクラスは自動的に componentN()関数を作ってくれるので、分解宣言が機能する。

注:標準で用意されている Pair も使えるけど、ちゃんと名前の付いたプロパティを持つデータを使ったほうがいいよ。

例: マップと分解宣言

たぶんマップを横断するのにこんな風にできたらいいよね。

for ((key, value) in map) {
   // do something with the key and the value
}

これをやるには、以下であるべき。

  • iterator() を提供することで値の列としてマップを表現する
  • component1(), component2() を提供することでペアとして各要素を表現する

で実際、標準ライブラリはこんな拡張を提供してる。

operator fun <K, V> Map<K, V>.iterator(): Iterator<Map.Entry<K, V>> = entrySet().iterator()
operator fun <K, V> Map.Entry<K, V>.component1() = getKey()
operator fun <K, V> Map.Entry<K, V>.component2() = getValue()

なのでマップを for ループするのに自由に分解宣言を利用できる(データクラスインスタンスのコレクションと同様に)。

範囲

範囲式は .. 演算子を使って表し、in, !in と一緒に使える。範囲 は比較可能な型に定義されているが、整数のプリミティブ型に対しては最適化された実装を持つ。

※演算子オーバーロードの項で説明するが、a .. b 演算子は a.rangeTo(b) メソッド呼び出しに変換される。また a in ba.contains(b), a !in b!a.contains(b) に変換される。

if (i in 1..10) { // 1 <= i && i <= 10 と等価
  println(i)
}

整数型の 範囲(IntRange, LongRange, CharRange) は特別な機能を持っており、これらはイテレート可能。コンパイラは特別なオーバーヘッドなしに Java の for 文のインデックスのように扱う。

for (i in 1..4) print(i) // "1234"と表示される

for (i in 4..1) print(i) // 何も表示されない

逆順に回したい時はどうするかって?標準ライブラリにある downTo() 関数が使える。

for (i in 4 downTo 1) print(i) // "4321"と表示される

ステップを自由に決めるにはどうするのかって?step() 関数を使う。

for (i in 1..4 step 2) print(i) // "13"と表示される

for (i in 4 downTo 1 step 2) print(i) // "42"と表示される

どうやって動いているのか

範囲 はライブラリ内の共通インターフェース ClosedRange<T> を実装している。

ClosedRange<T> は比較可能な型に定義されている数学で言う所の閉区間を表している。そして範囲内に含まれる startencInclusive の2つの端点を持つ。主要演算の contains は通常 in, !in 演算子の形で利用される。

整数型の IntProgression, LongProgression, CharProgression は数学で言う数列を表す。これは first, last, そしてゼロでない increment を要素に持つ。最初の要素が first で、続く要素は前の要素に increment 足したものになる。たとえ数列が空であっても last は必ずイテレーションでヒットする。

数列は Iterable<N> のサブ型であり、ここで NInt, Long, Char である。なので for ループや mapfilter といった関数で使うことができる。数列に対するイテレーションは Java や Javascript のインデックス付き for ループと等価である。

for (int i = first; i != last; i += increment) {
  // ...
}

整数型において .. 演算子は ClosedRange<T>○○Progression を実装したオブジェクトを生成する。例えば IntRangeClosedRange<Int> を実装し、IntProgression を継承する。なので IntProgression に定義されている全ての演算子は IntRange でも同様に有効。downTo(), step() 関数の結果は常に ○○Progression である。

数列はコンパニオンオブジェクトに定義されている fromClosedRange() 関数で構築される。

  IntProgression.fromClosedRange(start, end, increment)

数列の last 要素は、increment が正なら end 以下で最大の、increment が負なら end 以上で最小の (last - first) % increment == 0 である値が計算される1

便利関数

rangeTo()

整数型の rangeTo() 演算子は単純に ○○Range のコンストラクタを呼ぶ。

class Int {
  //...
  operator fun rangeTo(other: Long): LongRange = LongRange(this, other)
  //...
  operator fun rangeTo(other: Int): IntRange = IntRange(this, other)
  //...
}

浮動小数点数( Double, Float )には rangeTo は定義されていない。標準ライブラリがジェネリックな Comparable 型のために用意しているものを代わりに使う。

  public operator fun <T: Comparable<T>> T.rangeTo(that: T): ClosedRange<T>

この関数が返す ClosedRange はイテレーションには利用できない。

downTo()

downTo() 拡張関数は整数型のそれぞれのペアに用意されている。以下はそのうち2つの例。

fun Long.downTo(other: Int): LongProgression {
  return LongProgression.fromClosedRange(this, other, -1.0)
}

fun Byte.downTo(other: Int): IntProgression {
  return IntProgression.fromClosedRange(this, other, -1)
}

reversed()

reversed() 拡張関数はそれぞれの ○○Progression クラスに定義されている。これらは逆順の数列を返す。

fun IntProgression.reversed(): IntProgression {
  return IntProgression.fromClosedRange(last, first, -increment)
}

step()

step() 拡張関数はそれぞれの ○○Progression クラスに定義されている。これはステップ量を変更した数列を返す。ステップ量は常に正でなければならず、この関数は数列の方向を変更しない。

fun IntProgression.step(step: Int): IntProgression {
  if (step <= 0) throw IllegalArgumentException("Step must be positive, was: $step")
  return IntProgression.fromClosedRange(first, last, if (increment > 0) step else -step)
}

fun CharProgression.step(step: Int): CharProgression {
  if (step <= 0) throw IllegalArgumentException("Step must be positive, was: $step")
  return CharProgression.fromClosedRange(first, last, step)
}

last が元の数列と異なること場合があることに注意。(last - first) % increment == 0 を満たさなければならないため。例えば以下のように。

  (1..12 step 2).last == 11  // 次の値からなる数列 [1, 3, 5, 7, 9, 11]
  (1..12 step 3).last == 10  // 次の値からなる数列 [1, 4, 7, 10]
  (1..12 step 4).last == 9   // 次の値からなる数列 [1, 5, 9]

型チェックとキャスト

is と !is 演算子

実行時にオブジェクトがある型の一種であるかどうか2を判定するのに is が、その否定に !is が利用できる。

if (obj is String) {
  print(obj.length)
}

if (obj !is String) { // !(obj is String) と同じ
  print("Not a String")
}
else {
  print(obj.length)
}

スマートキャスト

多くの場合 Kotlin では明示的なキャストは必要ない。コンパイラが不変な値への is チェックを行ったことを追跡して、必要であれば自動的にキャストを(安全に)挿入してくれる。

fun demo(x: Any) {
  if (x is String) {  // ここでxがStringがどうかチェックしたので
    print(x.length)   // ここではxは自動的にStringにキャストされている
  }
}

また || や && の右側で、

  // `||`の右側ではxは自動的にStringにキャストされている
  if (x !is String || x.length == 0) return

  //`&&`の右側では x は自動的にStringにキャストされている
  if (x is String && x.length > 0)
      print(x.length) // xは自動的にStringにキャストされている

チェックと利用との間で変数が変更されないことをコンパイラが保証できない時は、スマートキャストが効かないことに注意。もっと仕様っぽい書き方をすると、次のルールに沿って有効になる。

  • val なローカル変数 - 常に。
  • val なプロパティ - プロパティが privateinternal かプロパティが宣言されたのと同じモジュール内でチェックが実行される場合。open なプロパティやカスタム getter を持つプロパティでは有効にならない。
  • var なローカル変数 - チェックと利用の間で値が変更されず、それを変更するようなラムダにキャプチャされない場合。
  • var なプロパティ - 決して有効にならない(コード内でいつでも変更可能なので)。

「安全でない」キャスト演算子

通常キャスト演算子はキャストに失敗すると例外を投げる。なので「安全でない」と言っている。Kotlin での安全でないキャストは as 接中辞演算子で行われる。

val x: String = y as String

nullString にキャストできないことに注意。Nullable でないので。つまりもし ynull なら上のコードは例外を投げる。Java でのキャストと同じようにするには、右側に Nullable 型を用いる。こんな風に。

val x: String? = y as String?

「安全な」(nullable)キャスト演算子

例外が投げられるのを避けたければ、安全なキャスト演算子 as? が使える。これは失敗すると null を返す。

val x: String? = y as? String

as? の右側が Nullbale でない String であっても、キャストの結果は Nullable になることに注意。


  1. これ本当?for (i in 4..1)ではループは実行されないという。first=4, end=1, increment=1 なので、last は 1 になる。・・・上のインデックス付きループと等価だったらループしまくって i がオーバーフローするじゃん。空の場合は last は first と同じ値が入るルールがある?

  2. Java の instanceof と同じ

Tags