Kotlin文法 - this、等価性、演算子オーバーロード、Null安全、例外

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

Kotlin Referenceの Other 章 This expressions, Equality, Operator overloading, Null Safety, Exceptions の大雑把日本語訳。適宜説明を変えたり端折ったり補足したりしている。

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

this

現在のレシーバを表すのに this を使う。

  • クラスのメンバの中で、this はそのクラスの現在のオブジェクトを指す。
  • 拡張関数やレシーバ付き関数リテラルの中で、this はドットの左側として渡されるレシーバパラメータを表す。

this が他のスコープにあるものを指すならラベルを付ける必要がある。ラベルが付いていない素の this はスコープの最も内側にあるものを指す。

修飾された this

class A { // 暗黙的に@Aラベル
  inner class B { // 暗黙的に@Bラベル
    fun Int.foo() { // 暗黙的に@fooラベル
      val a = this@A // Aのthis
      val b = this@B // Bのthis

      val c = this // foo()のレシーバ(Intオブジェクト)
      val c1 = this@foo // foo()のレシーバ(Intオブジェクト)

      val funLit = @lambda {String.() ->
        val d = this // funLitのレシーバ
        val d1 = this@lambda // funLitのレシーバ
      }

      val funLit2 = { (s: String) ->
        val d1 = this  // foo()のレシーバ。ラムダ式の中にはレシーバがないから。
      }
    }
  }
}

等価性

Kotlin には2種類の等価性がある。

  • 参照の等価性(2つの参照が同じオブジェクトを指しているかどうか)
  • 構造の等価性(equals() によるチェック)

参照の等価性

参照の等価性は === 演算子(否定は !==)によってチェックできる。a === bab が同じオブジェクトを指している時だけ true になる。

構造の等価性

構造の等価性は == 演算子(否定は !=)によってチェックできる。a == b は以下のように翻訳される。

a?.equals(b) ?: (b === null)

つまり a が null でなければ equals(Any?) 関数が呼ばれ、そうでなければ(つまり anull)、bnull かどうかがチェックされる。

null と比較する時に明示的に最適化しても意味がないことに注意。a == null は自動的に a === null に変換される。

演算子オーバーロード

Kotlin は型に対して事前定義された演算子のセットを提供することができる。これらの演算子は固定の記号(+ とか * のような)による表現と、固定の優先順位を持つ。演算子を実装するには対応する型に対して決まった名前のメンバ関数か拡張関数を用意する。対応する型というのはつまり二項演算子の左側の型と単項演算子の引数の型。演算子をオーバーロードする関数には operator 修飾子を付ける必要がある。

※なんか分かりにくい説明なので補足。つまり演算子は決まった名前のメソッド呼び出しに変換される仕組み。使える記号とその優先順位、呼び出されるメソッドが、あらかじめ決まってる。

変換ルール

単項演算子

変換先
+a a.unaryPlus()
-a a.unaryMinus()
!a a.not()

この表が示しているのは、コンパイラが処理する時に、例えば +a という表現に対して次のステップを実行する。

  • a の型を決める。これを T としよう。
  • レシーバ T に対して operator 修飾子のついた引数なしの unaryPlus() 関数を探す。つまりメンバ関数か拡張関数から。
  • もし関数が見つからないか曖昧ならコンパイルエラー
  • もし関数が見つかってその戻り値の型が R なら、式 +a の型は R

これらの演算子は基本型に対しては最適化され、関数呼び出しのオーバーヘッドはかからないことに注意。

変換先
a++ a.inc() + 以下参照
a-- a.dec() + 以下参照

これらの演算子はレシーバ自身を変更して(オプションとして)値を返すことを想定している。

注!! inc()/dec() はレシーバオブジェクト(の状態)を変更すべきではない。「レシーバ自身を変更」とは レシーバ変数 を意味していて、レシーバオブジェクトのことではない。

コンパイラは後置型(つまり a++)の演算子の解析を以下のステップで行う。

  • a の型を決める。これを T としよう。
  • レシーバ T に対して operator 修飾子のついた引数なしの inc() 関数を探す。
  • もし戻り値の型が R なら、それは T のサブ型でなければならない。

式の計算は以下のように行われる。

  • a の初期値を一時的に a0 に格納
  • a.inc() の結果を a に代入
  • a0 を式の結果として返す

a-- でもステップは同じ(incdec になるだけ)。

前置の --a++a の場合も同じように解析され、式の計算は以下のようになる。

  • a.inc() の結果を a に代入
  • 式の結果として新しい a の値を返す

二項演算子

変換先
a + b a.plus(b)
a - b a.minus(b)
a * b a.times(b)
a / b a.div(b)
a % b a.mod(b)
a..b a.rangeTo(b)

上の表の演算子はコンパイラが変換先の式に置き換える。

変換先
a in b b.contains(a)
a !in b !b.contains(a)

in, !in も同じなんだけど、レシーバと引数の順番が反対。

変換先
a[i] a.get(i)
a[i, j] a.get(i, j)
a[i1, ..., in] a.get(i1, ..., in)
a[i] = b a.set(i, b)
a[i, j] = b a.set(i, j, b)
a[i1, ..., in] = b a.set(i1, ..., in, b)

[]括弧は適切な数の引数の get()set() に置き換えられる。

変換先
a() a.invoke()
a(i) a.invoke(i)
a(i, j) a.invoke(i, j)
a(i1, ..., in) a.invoke(i1, ..., in)

()括弧は適切な引数の invoke() に置き換えられる1

変換先
a += b a.plusAssign(b)
a -= b a.minusAssign(b )
a *= b a.timesAssign(b )
a /= b a.divAssign(b)
a %= b a.modAssign(b)

代入演算子(a += b)に対してはコンパイラは以下のステップを実行する。

  • もし変換先の関数が存在するなら

    • もし対応する二項演算子(plusAssign() に対してなら plus())が存在するなら、エラーを報告する(曖昧なので)。
    • 戻り値の型が Unit かどうか確認し、そうでなければエラーを報告。
    • a.plusAssign(b) のコードを生成
  • そうでなければ a = a + b _ を生成しようとする。(これには型チェックを含む。つまり _a + ba のサブ型でなければならない。)

注:Kotlin では代入は式ではない。

変換先
a == b a?.equals(b) ?: b === null
a != b !(a?.equals(b) ?: b === null)

注:===!== はオーバーロードできない。なのでそれらの変換ルールはない。

== 演算子は特別。null かどうかを調べるために複雑な式に変換される。そして null == nulltrue である。

変換先
a > b a.compareTo(b) > 0
a < b a.compareTo(b) < 0
a >= b a.compareTo(b) >= 0
a <= b a.compareTo(b) <= 0

全ての比較は compareTo() に変換される。戻り値には Int が要求される。

関数の接中辞呼び出し

関数の接中辞呼び出しによって、独自の接中辞演算子のように振る舞わせることができる。

Null 安全

Nullable 型と Null にならない型

Kotlin の型システムは null 参照2を排除することを狙っている。

Java を含む多くのプログラミング言語の共通の落とし穴の1つは、null 参照例外を引き起こす null 参照のメンバへのアクセスだ。Java では NullPointerException または略して NPE を投げる。

Kotlin で NPE が起こり得るのは、

  • 明示的に throw NullPointerException() を呼んだとき
  • 外部の Java コードが起こしたとき
  • 初期化に関係する何らかの不整合があったとき(コンストラクタ内では有効な未初期化の this がどこかで使われた3

Kotlin では、型システムは null を持ちうる参照とそうでない参照を区別する。例えば String 型の通常の変数は null を入れられない。

var a: String = "abc"
a = null // コンパイルエラー

null を許可するには nullable な StringString? と書く)として変数を宣言する。

var b: String? = "abc"
b = null // ok

a のプロパティにアクセスしたりメソッドを呼んだりしても、NPE を起こさないことが保証されている。なので安全に次のように書ける。

val l = a.length

けど b の同じプロパティにアクセスしたくても、安全ではないので、コンパイラはエラーを報告する。

val l = b.length // error: 変数 'b' は null がありうる

んなこと言ったってプロパティにアクセスする必要があるよね?それには幾つかの方法がある。

null かどうかチェックする

まず明示的に null かどうかチェックして、そうだった場合とそうでない場合を別々に扱うことができる。

val l = if (b != null) b.length else -1

コンパイラはチェックを行ったことを追跡していて、if の中で length を呼ぶのを許可する。もっと複雑な条件もサポートする。

if (b != null && b.length > 0)
  print("String of length ${b.length}")
else
  print("Empty string")

これは b が変更されない場合にだけ動作することに注意(つまりチェックと利用の間でローカル変数が変更されないか、オーバーライドできない val プロパティか)。そうでないとチェックの後で null に変わっちゃうかもしれないから。

安全な呼び出し

2つ目のやり方は安全な呼び出し演算子を使う方法。? と書く。

b?.length

これは bnull でなければ b.length を返し、そうでなければ null を返す。この式の型は Int? である。

これはチェイン呼び出しを行うのに便利。例えば Bob という従業員が部署に配属されている(またはされていない)かもしれないとする。その部署には他の従業員がボスとしている(またはいない)かもしれないとする。もしありえるなら、Bob の部署のボスの名前を取得したいとすると、

bob?.department?.head?.name

これは途中のどれかが null なら null を返す。

エルビス演算子

nullable な参照 r があって、「rnull でなければそれを使うけど、そうじゃないなら別の null じゃない x を使うよ」って場合は if を使うとこう書くことになる。

val l: Int = if (b != null) b.length else -1

このif 式はエルビス演算子4 ?: を使って表現できる。

val l = b?.length ?: -1

エルビス演算子の左側が null でなければそれを返し、そうでなければ右側を返す。右側は左側が null の場合しか評価されないことに注意。

Kotlin では returnthrow も式5なので、これらはエルビス演算子の右側に使える。これは例えば関数の引数チェックでとても便利だ。

fun foo(node: Node): String? {
  val parent = node.getParent() ?: return null
  val name = node.getName() ?: throw IllegalArgumentException("name expected")
  // ...
}

!!演算子

3つ目の選択肢は NPE 大好きっ子のためのもの。b!! って書くと null でない b の値(つまりここの例では String)を返し、bnull なら NPE を投げる。

val l = b!!.length()

もし NPE が欲しいならこれを使えばいい。けど明らかに墓穴を掘ることになり、暗闇を抜けて青空を仰ぎ見ることはできない。

例外

例外クラス

Kotlin では全ての例外クラスは Throwable の子孫。全ての例外はメッセージとスタックトレースと、オプションで原因を持つ。

例外オブジェクトを投げるには、throw 式を使う。

throw MyException("Hi There!")

例外をキャッチするには try 式を使う。

try {
  // なんかのコード
}
catch (e: SomeException) {
  // 例外処理
}
finally {
  // オプションでfinallyブロック。例外が起こっても起こらなくても実行される。
}

ゼロ個以上の数の catch ブロックが持てる。finally は省略されるかもしれない。けど少なくとも1つの catchfinally がなければならない。

try は式である

try は式であり、値を返すかもしれない。

val a: Int? = try { parseInt(input) } catch (e: NumberFormatException) { null }

try の戻り値は try ブロックの最後の式か、catch ブロックの最後の式になる。finally ブロックの内容は式の結果に影響しない。

検査例外

Kotlin には検査例外(checked exception)はない。これには多くの理由があるのだけど、簡単な例を挙げよう。

以下は StringBuilder クラスによって実装されている JDK のインターフェースの例。

Appendable append(CharSequence csq) throws IOException;

これが何を言っているのか?何か(StringBuilder やある種のログ、コンソールなど)に文字列を追加するたび、IOException をキャッチしないといけないと言っている。なぜか?これは IO を実行するかもしれないから(WriterAppendable を実装している)・・・。それでこういうコードをそこら中にばら撒くことになる。

try {
  log.append(message)
}
catch (IOException e) {
  // Must be safe
}

これはイケてない。(Effective Java の65項「例外を無視するな」を参照)

Bruce Eckel は Java に検査例外っていんの? の中で言っている。

小さなプログラムの検査では次の結論を導く。要求される例外の仕様は開発者の生産性とコードの質の両方を向上させうる。しかし巨大なソフトウェアプロジェクトの経験は異なる結果を示唆している - 生産性を低下させ、ほとんどまたは全くコードの質を向上させない。

他の批評はこんなの。

Java との相互運用性

Java との相互運用性についてはJava との相互運用の検査例外を参照

次の章へ

次はKotlin 文法 - アノテーション、リフレクション、型安全なビルダー、動的型へ GO!


  1. C++のファンクタと同じものが作れる。実はラムダは中身が invoke()メソッドで実行されるオブジェクトを生成してる。

  2. 10億ドルの過ちとして知られている」という記述が原文にある。

  3. 別スレッドで初期化途中のオブジェクトにアクセスする場合のことっぽい。

  4. Goovy 由来の機能。名前は Elvis Presley の髪型から。"If you look at it sideways, you'll recognize.(横から見りゃ誰だかわかる)"

  5. じゃあこれらが何の値を返すのか?動作からして何も(Unit さえ)返さないはずなので、式ではなく文だと思うのだが・・・実際に式として利用できる。val a:Int = throw IllegalArgumentException("dummy") とか書いても(val a:Int 部分に到達しないという警告は出るが)コンパイルを通る。このとき a の型は何でもいい。式として使えるが、それが何も返さないことをコンパイラが把握できるので「吾輩は式である。戻り値の型はない。」ってことみたい。なので戻り値を利用する if や when の中でも使える。

Tags