8月24日(土)に開催された「Kotlin Fest 2019」において、サイバーエージェントはひよこスポンサーを務めました。
サイバーエージェントからは「タップル誕生」のAndroidエンジニア・佐藤(@stsn_jp )と「Amebaブログ」のサーバーサイドエンジニア・荻野(@youta1119 )の2名が登壇いたしましたので、今日は登壇資料の共有と「タップル誕生」の古澤(@mzkii)による「Kotlin型 実践入門」のセッションについての解説を行います。
「Kotlin型実践入門」
佐藤隼(@stsn_jp )
Smart Cast
多くの場合、Kotlin ではコンパイル時に必要に応じて型推論が行われるため、明示的にキャスト演算子を使用する必要はありません。
Smart Cast とは、if 式 / when 式 / is 演算子 / as 演算子などを使用した後に型推論してくれる機能のことを指します。この機能によって、Java 特有の冗長な記述を減らすことが可能です。
例えば、if 式で型チェックを行ったとします。するとブロック内では Smart Cast によって明示的に型を指定する必要がなくなります。
//if ブロック内では String であることが保証されているため length メソッドが呼び出せる
fun demo(x: Any) {
if (x is String) {
print(x.length)
}
}
また Smart Cast は複雑な条件式においても有効です。例えば、以下のように複数の条件式が組み合わさっている場合でも効果を発揮します。
// x が String と保証されているため length メソッドが if 式の中で呼び出せる
if (x is String && x.length > 0) {
print(x.length)
}
ただし、型チェックからその変数が使用されるまでの間に何らかの変更が加わる可能性がある場合、Smart Cast は機能しないことに注意して下さい。
例えば、以下のクラスメソッドがあった場合、一見型チェック後の obj は String として扱えそうですが、コンパイルレベルでは String であることが保証されないため、明示的にキャストしなければコンパイルエラーになります。
class Hoge {
private var obj: Any = "a"
fun test() {
// 型チェックと,length メソッドを呼び出す間に obj に対して変更が加わる可能性があるため
// length メソッド呼び出し時に明示的に String にキャストしないとコンパイルエラー
if (obj is String) {
print(obj.length)
}
}
}
この場合、val としてローカル変数を新しく切り出したり、
class Hoge {
private var obj: Any = "a"
fun test() {
val obj = obj
// val によって変更されないことが保証されるので OK
if (obj is String) {
print((obj.length)
}
}
}
スコープ関数を使ったりすることで Smart Cast が使えるようになります。
class Hoge {
private var obj: Any = "a"
fun test() {
// let スコープの中では it は毎回同じ obj を返すので OK
obj.let {
if (it is String) {
print(obj.length)
}
}
}
}
また、Kotlin の Smart Cast については以下の公式ドキュメントが参考になります。
https://kotlinlang.org/docs/reference/typecasts.html#smart-casts
Contracts
Kotlin 1.3 から Contracts という機能が実装されました。この機能によって、関数の振る舞いや効果を定義することが出来るようになりました。例えば Kotlin 1.3 以前では、isNullOrEmpty() を呼び出したあとに Smart Cast を使用することは出来ませんでした。
fun foo(s: String?) {
if (!s.isNullOrEmpty()) {
print(s.length) // error!!
}
}
Contracts を使うことによって、関数の呼び出し結果を元にして Smart Cast させる事が可能です。Kotlin 1.3 以降の CharSequence?.isNullOrEmpty() は以下のように定義されています。
public inline fun CharSequence?.isNullOrEmpty(): Boolean {
contract {
returns(false) implies (this@isNullOrEmpty != null)
}
return this == null || this.length == 0
}
returns(false) implies (this@isNullOrEmpty != null) の意味は、「isNullOrEmpty が false を返す時 isNullOrEmpty は NonNull である」という意味になります。
したがって、isNullOrEmpty を使った先程の例では Smart Cast が効きコンパイルが通ります。
fun foo(s: String?) {
if (!s.isNullOrEmpty()) {
print(s.length) // OK
}
}
また、Kotlin には assertTrue というメソッドが用意されていますが、こちらの内部実装を見ると Contracts が使用されていることが分かります。
/** Asserts that the given [block] returns `true`. */
fun assertTrue(message: String? = null, block: () -> Boolean): Unit = assertTrue(block(), message)
/** Asserts that the expression is `true` with an optional [message]. */
fun assertTrue(actual: Boolean, message: String? = null) {
contract { returns() implies actual }
return asserter.assertTrue(message ?: "Expected value to be true.", actual)
}
contract { returns() implies actual } によって、assertTrue が正常に return する時に actual は True であることがコンパイルレベルで保証されます。
(※ asserter.assertTrue に渡す actual が False の時は例外が発生するため、正常に終了できる == actual は True)
したがって以下のように記述することが可能です。
assertTrue(obj != null)
print(obj.length)
また Contract は自分で実装することも可能です。checkObj は引数 obj が NonNull か否かを Boolean で返す関数です。
@UseExperimental(ExperimentalContracts::class)
fun checkObj(obj: Any?): Boolean {
contract {
returns(true) implies (obj != null)
}
return obj != null
}
if (checkObj(obj)) {
obj.javaClass // obj is not null
}
現在 Contract は Experimental な機能であるため、@UseExperimental(ExperimentalContracts::class) が必要になります。
また、Kotlin の Contract については以下の公式ドキュメントが参考になります。
https://kotlinlang.org/docs/reference/whatsnew13.html#contracts
Any、Unit、Nothing
Kotlin では新しく Any、Unit、Nothing という特殊な型が登場しました。ここでは、これらの型を使ってどのようなメリットがあるのか解説します。
Any
Any はすべてのクラスのスーパークラスであり、Java における java.lang.Object クラスに相当します。
java.lang.Object と違うところは、
- スレッドを操作するための java.lang.Object.wait メソッドや java.lang.Object.notify メソッドが使えない
- Object の実行時クラスを取得するための java.lang.Object.getClass メソッドが Kotlin では拡張関数で定義されている
などが挙げられます。
Unit
Unit は戻り値が空であることを表し、Java における void 型に相当します。Kotlin では、メソッドが値を返さない場合は Unit を省略することができます。
fun empty(): Unit {
println("hoge")
}
fun empty() {
println("hoge")
}
Nothing
Nothing 型は特殊な型になっていて、すべての Kotlin におけるクラスのサブクラスを表します。
使い道の例としては、絶対に正常終了することのない関数の戻り値として使うことができ、値が存在しないことを表します。例えば、以下の2つの関数が存在したとして、それぞれの戻り値の型が String と Unit だとします。
fun getName(): String {
return "name"
}
fun fail(): Unit {
throw RuntimeException()
}
val name: Any = if (isFriend()) {
getName()
else {
fail()
}
この場合、変数 name の型は String ではなく Any になってしまいます。
なぜかというと、String と Unit というそれぞれ異なった型が返却されるため、name の型はそれらの親の型になってしまうからです。
この場合 name の型は Any なのですが、Nothing 型の特徴をうまく使えば name を String にすることができます。
fun getName(): String {
return "name"
}
fun fail(): Nothing {
throw RuntimeException()
}
val name: String = if (isFriend()) {
getName()
else {
fail()
}
fail の戻り値を Unit から Nothing にすることで、Nothing は String のサブクラスとなるので、変数 name の型は、Nothing の親クラスである String として扱うことができます。
Nothing?
Kotlin では Nothing? 型も扱うことができます。Nothing は Unit と同じく値として存在しないので、実質的には null のみを許容する型になります。
interface Show {
fun supports(a: Any?): Boolean
fun show(a: A): String
}
object NullShow : Show<Nothing?> {
override fun supports(a: Any?): Boolean = a == null
// Nothing? なので null のみを許容する
override fun show(a: Nothing?): String = ""
}
ジェネリクス
Java と同様に、Kotlin でもクラスは型パラメータを持つことができます。
Java において、総称型(ジェネリクス) は不変でしたが、Kotlin では型パラメータに対して共変性(out 修飾子) と不変性(in 修飾子) を持たせることで、柔軟に親子関係を制限することが出来ます。
例えば Number 型が存在したとして、Int は Number を継承している関係(Number <– Int) の時、任意のクラス A<T> に対して A<Number> <– A<Int> の関係であるならば、クラス A を共変と言います。
// 共変
val a: A<Int> = A<Int>()
val b: A<Number> = a
逆に、Int は Number を継承している関係(Number <– Int) の時、任意のクラス A<T> に対して A<Int> <– A<Number> の関係であるならば、クラス A を反変と言います。
// 反変
val a: A<Number> = A<Number>()
val b: A<Int> = a
A<Int> と A<Number> が関連付けられていない場合は、クラス A を不変と言います。
// 不変
val a: A<Int> = A<Int>()
具体的に、例を挙げて説明したいと思います。例えば、以下のインターフェースがあるとします。
interface Mapper<T> {
fun map(s: String): T
}
これを継承した IntMapper クラスを作ります。
class IntMapper : Mapper<Int> {
override fun map(s: String): Int = s.toInt()
}
fun hoge(mapper: Mapper<Number>) {
mapper.map("10")
}
val mapper = IntMapper()
hoge(mapper)
この時、Mapper<Number> を引数に取る hoge に対して、IntMapper() のインスタンスを渡すことは出来るのでしょうか?
答えは NO です。Int と Number クラスには何の関係性も定義されていないため、hoge の呼び出し箇所でコンパイルエラーになります。
この場合は共変性を利用します。Mapper インターフェースに対して out 修飾子を使って改めて定義してみます。
interface Mapper<out T> {
fun map(s: String): T
}
こうすることで、Mapper<Number> <– Int<Mapper> の関係性が出来るので hoge メソッドを呼び出せるようになります。
reified
reified とは、inline function 内で型変数にアクセスするための修飾子のことです。
以下のコード例は、reified 修飾子を使ったサンプルです。
inline fun <reified T> hoge(obj: Any) {
println(T::class.java)
if (obj is T) {
println("obj is T")
}
}
hoge<MainActivity>(requireActivity())
hoge 呼び出し時に型情報を渡し、関数内で渡されたインスタンスが T であるかをチェックすることができます。
また、拡張関数とジェネリクスを組み合わせることで、メソッド呼び出し時に柔軟に型制限できるようになります。例えば、以下の A というクラスが存在したとして、そのクラスに対して isNull という拡張関数を定義します。
class A<T>(val value: T)
fun <T: Any> A<T?>.isNull(): Boolean {
return value != null
}
こうすることで、プリミティブ型など絶対に null でないことが分かりきっているケースにおいては、isNull メソッドを呼び出せないように制限することができます。
val a1: A<Int?> = A(null)
a1.isNull() // true
val a2: A = A(10)
// コンストラクタで渡されたクラスの型は
// nullable ではないため呼び出せない
a2.isNull()
sealed class
sealed class は内部的には abstract クラスになっていて、制限されたクラスの階層を表すために使用されます。例えば以下のような Either クラスが存在するとします。
sealed class Either {
object Left : Either()
object Right : Either()
}
この sealed class のインスタンスを obj とすれば、when 式内で型チェックをする場合に else 句を省略することが可能です。
// sealed class によって Either クラスのサブクラスには
// Left と Right のみ存在することがコンパイルレベルで保証される
when (obj) {
Left -> println("is Left")
Right -> println("is right")
// else -> println("else")
}
ちなみに、sealed class があるなら sealed interface が存在しても良いのではないかと思いつく方もいると思いますが、sealed interface だと容易に継承ができてしまい、型による制限を破ってしまうため用意されていません。
関数型
Kotlin では、関数を第1級オブジェクトとして扱うことが出来るので、変数・関数の引数・戻り値などに関数型を使うことが出来ます。例えば、Int 型を受け取り String 型で返す関数を受け取る関数 hoge は以下のように定義します。
fun hoge(body: (Int) -> String) {
body(10)
}
hoge { it.toString() }
Kotlin では、Function0 ~ Function22 までの関数インターフェースと、FunctionN インターフェースが用意されていて、上記のコードはコンパイル時に Function 関数インターフェースに変換されます。
また関数は変数として扱うことも出来ます。以下の例では、Int 型変数を2つ受け取り Int 型を返す関数を変数 a に代入して呼び出しています。
fun add(a1: Int, a2: Int): Int {
return a1 + a2
}
add(10, 20)
val a: (Int, Int) -> Int = ::add
a(10, 20)
SAM 変換
SAMとは、Single Abstract Method のことで、一つの抽象メソッドを持つインターフェースを指します。Kotlin においては、Java 定義の SAM インターフェースを引数に取る関数に対してラムダ式を渡すと、SAM インターフェースに変換されるので、簡潔に書くことができます。
以下のように Runnable インターフェースが存在したとして、それを引数に取る SamTest.foo() メソッドについて考えてみます。
public interface Runnable {
public abstract void run();
}
public class SamTest {
public static void foo(Runnable a, Runnable b) {
// do nothing
}
fun test() {
SamTest.foo({}) {}
}
この例では、Runnable インターフェースに対して SAM 変換が機能するので、 run メソッドの実装は省略することができます。
ただし、以下のように引数として Runnable を受け取ることはできません。
fun test(r: Runnable) {
SamTest.foo(r) {} // コンパイルエラー
なぜかというと、foo メソッドのように複数の SAM を引数に取る場合は、一つでも実態を使ってしまうと意図した変換が行われないからです。この場合、test メソッドの引数の r は既に実体化されているので foo メソッドの呼び出し部分でコンパイルエラーになる訳です。
また、Kotlin 1.3.40 から SAM 変換の問題点を解決するために、新しい型推論アルゴリズムを開発しています。現在はまだ experimental ですが、設定で有効化することもできます。
最後に
Kotlin の型についてざっくりとおさらいしてみました。
Smart Cast や型推論、Sealed Class などの Kotlin 独自の言語機能を使えば、Java のような冗長な記述がなくなりとても書きやすくなります。
また、Kotlin Fest 2019 のセッション資料は公式でまとめられていますのでぜひご覧ください。
この記事を読んで Kotlin に興味を持った方はチャレンジしてもらえると幸いです。
来年も Kotlin Fest を楽しみましょう!Have a Nice Kotlin!
サイバーエージェントではAndroidエンジニアを大募集中です!
カジュアル面談からスタートさせたい方はコチラ
おまけ
サイバーエージェントは今年もブースを出展し、寿司打大会を開催しました。参加者の皆さま、ありがとうございました。
https://twitter.com/kotlin_fest/status/1165112495138361344?s=20
そして上位に入賞された皆様おめでとうございます
https://twitter.com/ca_developers/status/1165160567843115008?s=20