読者です 読者をやめる 読者になる 読者になる

Kotlin Advent Calendar 2012 (全部俺)

JavaプログラマのためのKotlin入門

21日目:その他の機能いろいろ

f:id:ngsw_taro:20121220192158j:plain

今日でKotlin言語の文法や機能の説明は最後です。今日ご紹介するのは、これまでに紹介していない残りの機能についてです。中にはまだ実装されていない(正しくコンパイルされない)機能もありますが、それらは今後のマイルストーンで実装されていくでしょう。では、始めます。

キャスト

キャスト、それは型の強制変換術です。時としてそれは、プログラムにスタックトレースを吐かせ死に追いやる恐ろしい術です。NULL安全機構を導入している心配性のKotlinは、このキャストとどうやって付き合うのでしょうか。

型チェック

まずは道具を準備しましょう。いわゆる is-a 関係(つまり継承関係)をチェックすることにより、むりやり型を合わせようとして失敗することを未然に防げます。Javaではキーワード instanceof を使用しましたが、Kotlinではより簡潔で直感的なキーワード is を使用します。その否定形は !is です。is(あるいは !is)によって評価された式は Boolean値を返すので、if式やwhen式などで使用できます。次のコードはwhen式のパターンマッチによる型チェックの例です。

val hoge : Any = "kotlin is fun!"
  
val result = when(hoge) {
  is Int     -> "im Int"
  is Char    -> "im Char"
  !is String -> "im not String"
  is String  -> "im String"
  else       -> throw AssertionError()
}
  
println(result) // im String

スマートキャスト

ではキャストの方法について紹介していきます。いくつかやり方があるうちの1つ目がスマートキャストです。これは。型チェックを済ませたあとのブロックでは、その型に自動的にキャストされる仕組みです。具体的には次のコードを見てください。

val hoge : Any = "kotlin is fun!"
  
if(hoge is String) {
  println(hoge.toUpperCase()) // KOTLIN IS FUN!
}

変数 hoge が型 String であるかif によりチェックしており、そのブロック内で hoge が String のように振る舞うことができます。Javaでは型チェックをした後、明示的にキャストをしないとならない場面ですが、その手間をKotlinでは省けます。

演算子 as による安全でないキャスト

次に紹介するのは演算子 as を使用した、キャストっぽいキャストです。この方法は、演算子 as に続けて変換したい型を指定します。ただし、キャストに失敗した場合には実行時例外を投げますのでご注意ください。

println*1.toUpperCase()) // KOTLIN IS FUN!

演算子 as? による安全なキャスト

慎重なKotlinには、やはり安全なキャストが用意されています。演算子 as? を、先に説明した as のように使用するやり方です。しかしこの場合は、キャストに失敗したときには例外はスローされず null が返されます。

val hoge : Any = "kotlin is fun!"
val i : Int? = hoge as? Int
println(i) // null

これは面白い機能です。NULL安全により、次のようなキャストからの関数呼び出しも安全に失敗してくれます。

val piyo : Any = 123
println*2 // null

クラスオブジェクトとジェネリクス制約

まず最初に言いますが、この機能は現時点では実装されていませんこちらの issue に対応しています。

さて、いきなりですが次のクラスオブジェクトとトレイトを組み合わせたファクトリ関数の例を見てください。

trait Factory {
  fun create() : T
}

class Coffee private() {
  class object : Factory {
    override fun create() = Coffee()
  }
}

これは昨日の例でも見たクラスオブジェクトとトレイトの上手い使い方の典型です。コーヒーにラベルを貼る必要が出たとして、便宜的に次のような関数を用意します。

fun getLabeledCoffee(label : String, coffee : Coffee?) =
  Pair(label, coffee ?: Coffee.create())

引数にラベルを表す文字列とコーヒーを取ります。ラベルとコーヒーのペアを生成して返しますが、引数のコーヒーに null が渡された場合には、ファクトリ関数を使ってコーヒーを作成します。

将来的に緑茶などにも対応する必要があるので、この関数を汎用的に改造します。直感次のようなコードを書きました。

fun <T> getLabeledObject(label : String, t : T?) =
  Pair(label, t ?: T.create())

これは文法的に正しくありません。型引数 T に対して関数 create を呼び出そうとしているのがおかしいです。Tにはどのような型が来るのかわからないからです。

では、型引数 Tに対して「トレイト Factory をミックスインしたクラスオブジェクトを持つ型である」という制約を設定しましょう。これは17日目に解説した型引数に上限制約を設ける話に似ていますが、同じでないことに注意してください*3

fun  getLabeledObject(label : String, t : T?)
  where class object T : Factory = Pair(label, t ?: T.create())

上記のようにclass object 型引数 : トレイトを where節に記述すると、上手く行きます。

例外

基本的にKotlinの例外機構はJavaと同じです。例外クラスはクラス Exception のサブクラスです。文法的な話をするならば、やはり try-catch-finally はありますが、これらはなので値を返します。例外をスローしたいときはキーワード throw を使用します。

Javaとの違いは、チェック例外がなく、すべて非チェック例外であることです。チェック例外はプログラマの作業を増やしAPIを使い難いものにしたり、作業を強制されるのでcatch節でつい握り潰してしまったりと、あまりいいことがないからです*4

列挙型

列挙型は、現在開発中の機能であることに注意してください。

Kotlinには列挙型(enumがあります。Javaのそれと同じような使い方ができます。

enum class Currency {
  JPY
  USD
  GBP
}

コンストラクタに値を渡す場合は次のように記述します。

enum class Currency(val country : String) {
  JPY : Currency("Japan")
  USD : Currency("USA")
  GBP : Currency("UK")
}

さらに、列挙型定数(例えば上記の JPYなど)は匿名サブクラスのインスタンスとなることもできます。

import java.math.BigDecimal
import kotlin.math.times

enum class Currency(val country : String) {
  JPY : Currency("Japan") {
    override fun toJPY(money : BigDecimal) = money
  }
  
  USD : Currency("USA") {
    override fun toJPY(money : BigDecimal) =
      money * BigDecimal(84.19)
  }
  
  abstract fun toJPY(money : BigDecimal) : BigDecimal
}

Kotlinの列挙型は機能が豊富です。クラスのように open 宣言することで、他の列挙型から継承できるように設定できます。また、トレイトをミックスインすることも可能です。

まとめと次回予告

今日はいろいろ学びました。キャスト、クラスオブジェクトのジェネリクス制約、例外、列挙型についてです。キャストには危険なキャストと安全なキャストがありました。クラスオブジェクトのジェネリクス制約はまだ実装されていませんが非常に便利な機能です。例外、列挙型はJavaのそれらと似ていますが、改善されています。

明日は...なに書こうw 

日記

早く3連休来い!

*1:hoge as String

*2:piyo as? String)?.toUpperCase(

*3:単なる型引数に対する上限制約では、上記の例においては t.create() はできるようになりますが、T.create()はできません。

*4:Effective Java 第2版「項目59 チェックされる例外を不必要に使用するのを避ける」「項目65 例外を無視しない」を参照してください。