Kotlin 1.2.0 言語機能の感想
Kotlin 1.2.0出ましたね。 主に新しい言語機能についてざっくり感想を述べます。
言語機能
アノテーション上でArrayリテラルをサポート
今までは次のようにarrayOfって書いていたけど、
@Singleton @Component( modules = arrayOf( AppModule::class, DataModule::class, ... ) ) interface AppComponent : AndroidInjector<HogeApplication> {
次のように書けるようになる。
@Singleton @Component( modules = [ AppModule::class, DataModule::class, ... ] ) interface AppComponent : AndroidInjector<HogeApplication> {
アノテーションでのみ利用可能なので、大体Dagger周りとかPermissionDispatcherとかRobolectricとか周りで活躍しそう。
lateinit が top-levelプロパティとローカル変数で利用可能に
nullableじゃないけど宣言時はまだ値が決まらない時に使えそう。 とはいえlateinitは初期化されてない状態で触るとクラッシュするので、使い所が難しい。 Activity, Fragment等のライフサイクルがあるコンポーネントのプロパティとして使う事はあるが(やむを得ず)、 top-levelやローカル変数でのユースケースってそんなにないのではないかという気はする。
公式のコード例が次。特殊〜
// A cycle of three nodes: lateinit var third: Node<Int> val second = Node(2, next = { third }) val first = Node(1, next = { second }) third = Node(3, next = { first })
同じことをDelegateでもできる。
var third by Delegates.notNull<Node<Int>>() val second = Node(2, next = { third }) val first = Node(1, next = { second }) third = Node(3, next = { first })
lateinit varが初期化済みかどうか検査できるようになった
こんな感じでプロパティリファレンスで検査できる。
class A { lateinit var lateinitVar: String fun initialize () { if(!this::lateinitVar.isInitialized){ lateinitVar = "initialized" } } }
ローカル変数では使えないので注意っぽい。lateinitは宣言時には値は決まらないけどあとで必ずnon-nullになるので型宣言はnon-nullで頼む :pray: みたいな機能なので、初期化されてなければ何か処理する、みたいな使い方はあんまり本質的ではない気がする。再代入を防ぐのに使う、というのがありそうではあるが...
インライン関数でデフォルト引数が可能に
今までなかったのか〜。便利。
inline fun <E> Iterable<E>.strings(transform: (E) -> String = { it.toString() }) = map { transform(it) } val defaultStrings = listOf(1, 2, 3).strings() val customStrings = listOf(1, 2, 3).strings { "($it)" }
明示的なキャストの情報を使った型推論
Android API Level 26でfindViewById
がfindViewById<T>
になったので次のコードは動かなくなったんだけど、明示的キャストを型推論に使うから既存コードいじらなくてもいけるぜ的なやつ。助かる。
// findViewById<T>のTがどこにもないのでエラーになってたけど、 // as ButtonのButtonをTとして認識してくれるようになった。 val button = findViewById(R.id.button) as Button
スマートキャストがより賢く
よくわかってないけど、キャストが成功したと解釈できるときはキャスト対象をその後の文脈でキャスト後の型として扱えるようになったみたいな感じ。複雑な検証もいい感じに拾うぜ的なやつ。
val firstChar = (s as? CharSequence)?.firstOrNull() if (firstChar != null) return s.count { it == firstChar } // s: Any is smart cast to CharSequence val firstItem = (s as? Iterable<*>)?.firstOrNull() if (firstItem != null) return s.count { it == firstItem } // s: Any is smart cast to Iterable<*>
もうひとつ、クロージャでキャプチャする変数を、クロージャより手前でいじる場合のみ、クロージャ内でスマートキャストが利くようになった。
var x: String? = null if (flag) x = "Yahoo!" run { if (x != null) { println(x.length) // x is smart cast to String } }
明示的にこれによさそうみたいなコード例が思いつかないけど多分どっかしら恩恵を受けそうな気はする。
Bound Callable Referenceの省略表記
クラスの関数やプロパティの参照は次のように書く。
val showMessage = Hoge::showMessage val finish = this::finish
今回、this::
を::
に省略して書けるようになった。
val finish = ::finish
はい。
破壊的変更: sound smart casts after try blocks
https://youtrack.jetbrains.com/issue/KT-17929で報告された不具合で、次のコードはコンパイルが通り実行時にクラッシュする。
var s: String? s = "Test" try { s = null } catch (ex: Exception) {} s.hashCode() // スマートキャストで非null扱いになる
1.2.0ではこのコードのコンパイルは通らなくなる。プロダクトで1.2.0にしてもエラーにならなかったのでこういうコードってほぼ書かないよな普通と思いつつ安全になってよかった。
非推奨: スーパークラスのcopy関数をオーバーライドするdata classes
そもそもdata classがなにかを継承するケースってあんまりないと思うけど、もしもcopy関数を持ったクラスを継承したdata classを作ると腐ってしまうので非推奨になった。
例えば次のような感じ。
open class A { fun copy(): A { return A() } } data class B(val a: Int) : A()
次のコードの変数bはBクラスが返って欲しいのだけど、実際にはAが返ってしまう。
val b = B(1).copy() println(b.javaClass.simpleName) // A
data classで継承したいケースがほとんどない気がするのとさらにスーパークラスがcopy関数を持ってるケースってほとんどなさそうだけど、コンパイラがエラーにしてくれると安心ではある。1.2.0では警告で、1.3.0からコンパイルエラーになるとのこと。
非推奨: enumクラスでのネストしたクラスの宣言
次のコードで、A.X
にアクセスすると死ぬ。
enum class A { X { val x = 1 class Nested { val y = x // It's allowed and seems to be fine, because a Nested instance can use A.X } val z = Nested() // There wasn't PUTSTATIC A.X yet, fails with NPE } }
これはXのコンストラクタでval zを初期化するときにNestedのコンストラクタでX.xにアクセスするから。nest classはデフォルトでは静的なクラスになるので、まだコンストラクタが走りきってないXに対してアクセスして死んでいる。inner classで宣言するとX.this.xでアクセスしにいくので死なない。1.2.0では警告で、1.3.0からコンパイルエラーになるとのこと。
こんなコード書かねーよ〜〜〜〜とは思う。
非推奨: 名前付き引数で、varargを単一の値で渡す
次のような関数があったとする。
fun print(vararg values: Int) { values.forEach { println(it) } }
次のような呼び出し方に警告が出るようになった。1.3.0でエラー。アノテーションの配列リテラルのスタイルとの一貫性のためらしい。
print(values = 5) // warning
次のように書くようになる。
print(values = *intArrayOf(5))
あんまり感想がない。
Deprecation: Throwableを継承するジェネリック型の内部クラス
なんか型安全じゃなくなるらしい。よくわからないけど大変そう。こちらも1.2.0警告、1.3.0エラー。
Deprecation: valプロパティでバッキングフィールドを更新する
こういうのはダメよと。
val a: Int = 0 get() { field++ return field }
valなのに値変わっとるやんけ!と。1.2.0警告、1.3.0エラー。
まとめ
コルーチンのexperimental外れるのまだかな。