はじめに
Kotlin文法 - 分解宣言、範囲、型チェックとキャストの続き。
Kotlin ReferenceのOther章This expressions, Equality, Operator overloading, Null Safety, Exceptionsの大雑把日本語訳。適宜説明を変えたり端折ったり補足したりしている。
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 === b は a と b が同じオブジェクトを指している時だけ true になる。
構造の等価性
構造の等価性は == 演算子(否定は !=)によってチェックできる。a == b は以下のように翻訳される。
a?.equals(b) ?: (b === null)
つまり a が null でなければ equals(Any?) 関数が呼ばれ、そうでなければ(つまり a が null)、b が null かどうかがチェックされる。
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-- でもステップは同じ(inc が dec になるだけ)。
前置の --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[i_1, ..., i_n] | a.get(i_1, ..., i_n) |
a[i] = b | a.set(i, b) |
a[i, j] = b | a.set(i, j, b) |
a[i_1, ..., i_n] = b | a.set(i_1, ..., i_n, b) |
[]括弧は適切な数の引数の get() や set() に置き換えられる。
式 | 変換先 |
---|---|
a() | a.invoke() |
a(i) | a.invoke(i) |
a(i, j) | a.invoke(i, j) |
a(i_1, ..., i_n) | a.invoke(i_1, ..., i_n) |
()括弧は適切な引数の 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 + b は a のサブ型でなければならない。)
注:Kotlinでは代入は式ではない。
式 | 変換先 |
---|---|
a == b | a?.equals(b) ?: b === null |
a != b | !(a?.equals(b) ?: b === null) |
注:=== と !== はオーバーロードできない。なのでそれらの変換ルールはない。
== 演算子は特別。null かどうかを調べるために複雑な式に変換される。そして null == null は true である。
式 | 変換先 |
---|---|
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 な String (String? と書く)として変数を宣言する。
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
これは b が null でなければ b.length を返し、そうでなければ null を返す。この式の型は Int? である。
これはチェイン呼び出しを行うのに便利。例えばBobという従業員が部署に配属されている(またはされていない)かもしれないとする。その部署には他の従業員がボスとしている(またはいない)かもしれないとする。もしありえるなら、Bobの部署のボスの名前を取得したいとすると、
bob?.department?.head?.name
これは途中のどれかが null なら null を返す。
エルビス演算子
nullableな参照 r があって、「r が null でなければそれを使うけど、そうじゃないなら別のnullじゃない x を使うよ」って場合は if を使うとこう書くことになる。
val l: Int = if (b != null) b.length else -1
このif 式はエルビス演算子4 ?: を使って表現できる。
val l = b?.length ?: -1
エルビス演算子の左側が null でなければそれを返し、そうでなければ右側を返す。右側は左側が null の場合しか評価されないことに注意。
Kotlinでは return や throw も式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)を返し、b が null ならNPEを投げる。
val l = b!!.length()
もしNPEが欲しいならこれを使えばいい。けど明らかに墓穴を掘ることになり、暗闇を抜けて青空を仰ぎ見ることはできない。
例外
例外クラス
Kotlinでは全ての例外クラスは Throwable の子孫。全ての例外はメッセージとスタックトレースと、オプションで原因を持つ。
例外オブジェクトを投げるには、throw 式を使う。
throw MyException("Hi There!")
例外をキャッチするには try 式を使う。
try {
// なんかのコード
}
catch (e: SomeException) {
// 例外処理
}
finally {
// オプションでfinallyブロック。例外が起こっても起こらなくても実行される。
}
ゼロ個以上の数の catch ブロックが持てる。finally は省略されるかもしれない。けど少なくとも1つの catch か finally がなければならない。
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を実行するかもしれないから(Writer も Appendable を実装している)・・・。それでこういうコードをそこら中にばら撒くことになる。
try {
log.append(message)
}
catch (IOException e) {
// Must be safe
}
これはイケてない。(Effective Javaの65項「例外を無視するな」を参照)
Bruce Eckelは Javaに検査例外っていんの? の中で言っている。
小さなプログラムの検査では次の結論を導く。要求される例外の仕様は開発者の生産性とコードの質の両方を向上させうる。しかし巨大なソフトウェアプロジェクトの経験は異なる結果を示唆している - 生産性を低下させ、ほとんどまたは全くコードの質を向上させない。
他の批評はこんなの。
- Javaの検査例外は誤ちだった (Rod Waldhoff)
- 検査例外のトラブル (Anders Hejlsberg)
Javaとの相互運用性
Javaとの相互運用性についてはJavaとの相互運用の検査例外を参照
次の章へ
次はKotlin文法 - アノテーション、リフレクション、型安全なビルダー、動的型へGO!
-
C++のファンクタと同じものが作れる。実はラムダは中身がinvoke()メソッドで実行されるオブジェクトを生成してる。 ↩
-
別スレッドで初期化途中のオブジェクトにアクセスする場合のことっぽい。 ↩
-
Goovy由来の機能。名前はElvis Presleyの髪型から。"If you look at it sideways, you'll recognize.(横から見りゃ誰だかわかる)" ↩
-
じゃあこれらが何の値を返すのか?動作からして何も(Unitさえ)返さないはずなので、式ではなく文だと思うのだが・・・実際に式として利用できる。val a:Int = throw IllegalArgumentException("dummy") とか書いても(val a:Int 部分に到達しないという警告は出るが)コンパイルを通る。このときaの型は何でもいい。式として使えるが、それが何も返さないことをコンパイラが把握できるので「吾輩は式である。戻り値の型はない。」ってことみたい。なので戻り値を利用する if や when の中でも使える。 ↩