visible true

技術的なメモを書く

Kotlin 1.2.0 言語機能の感想

Kotlin 1.2.0出ましたね。 主に新しい言語機能についてざっくり感想を述べます。

via What's New in Kotlin 1.2

言語機能

アノテーション上で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でfindViewByIdfindViewById<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外れるのまだかな。