October 30, 2018

Kotlin1.3の新機能

この記事ではKotlin1.3にて追加された新機能についての解説です。 量がとても多く全てに触れることは出来ないので主要機能と個人的にピックアップしたい機能を中心に解説していきたいと思います。

量が多いのもあってか、IntelliJではKotlinのmigration toolを用意してくれています。

Tools > Kotlin > Enable migrations detection (experimental)
Kotlin migration settings

変更点

Parameterless main

Kotlin1.3からmain関数の引数が不要になりました。Androidをやっているとあまり旨味がないですが、kts(kotlin script)を書いている人からすると、ちょっと便利になりました。

  • Kotlin1.3まで
fun main(args: Array<String>) {
}
  • Kotlin1.3から
fun main() {
}

when

whenの中でローカル変数を定義出来るようになりました。 今までは引数で比較をした後、もう一度whenの中で値を取得し直さなければならなかったのですが、1.3からはwhenの比較部分にローカル変数を定義出来るようになりました。これは地味に便利です。

fun Request.getBody() =
  when (val response = executeRequest()) {
    is Success -> response.body
    is HttpError -> throw HttpException(response.status)
  }

Experimental features

experimentalの機能が正式に追加されました。
以前からCoroutineを使っている方は馴染みがあると思いますが、今後のバージョンで追加予定の新しい機能などは、experimentalの機能で提供されることになります。JetBrainsとしてはなるべく早くEarly adapterの人達に検証してもらって、フィードバックを活かして正式リリースを向かえたいという意図があるそうです。

Coroutine

今までexperimental featureとして提供されていた、coroutineはKotlin1.3からはついにexperimentalが外れ、正式リリースとなりました。 そのため、package構成が変更されているのでご注意下さい。

  • Kotlin1.3まで
import kotlinx.coroutines.experimental.*
  • Kotlin1.3から
import kotlinx.coroutines.*

Experimental annotation

なんとExperimental APIは自作ライブラリにも追加することが出来ます!!
例えば、Kotlinでライブラリを作る時にHogeNewAPIという機能をexperimentalで提供したい場合は以下のように自作annotationに対してExperimental annotationを付けます、

@Experimental
annotation class HogeNewAPI

もちろん、このannotationはクラスにも関数にも付けることができます。

// クラスにも付けられる
@HogeNewAPI
class Foo {}
                                         
// 関数にも付けられる
@HogeNewAPI
fun Foo.bar() = ""

ただしexperimental annotationを使用する場合は、このように呼び出し側の関数にもannotationを付けなければなりません。

@HogeNewAPI
fun doSomething() {
    val foo = Foo()
    foo.bar()
}
                                           
@HogeNewAPI                         
fun main() {
    doSomething()
}

そこで、UseExperimentalというannotationで囲ってあげると、以降の関数から呼び出し側にannotationを追加しなくても大丈夫です。

@UseExperimental(HogeNewAPI::class)
fun doSomething() {
    val foo = Foo()
    foo.bar()
}
fun main() {
    doSomething()
}

Contracts

1.3からコンパイラに暗黙的に意味を与えられるContractという便利なAPIが追加されました。Kotlinを使っているとrunやlet等のスコープ関数はよく使うと思います。
しかし、1.2までは以下のように変数を定義してrun内で値を代入してもprintlnの部分で"variable hoge must be initialized"となり、コンパイルエラーになっていました。

fun main(args: Array<String>) {
    var hoge: Int
    run {
        hoge = 46
    }
    println(hoge)
}

このコードはrunの中で暗黙的に実行内容が変わるため、人間が見たら変数の初期化がされていることは一目瞭然ですが、コンパイラはrunの中で変数が本当に初期化されているのかは実行してみないとわかりません。そのため、Kotlin1.2まではこのコードはコンパイルエラーで実行出来ませんでした。 runの実装を見てみましょう。

  • Kotlin1.3まで
public inline fun <T, R> T.run(block: T.() -> R): R {
    return block()
}
  • Kotlin1.3から
public inline fun <T, R> T.run(block: T.() -> R): R {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return block()
}

新たに、contractというコードが追加されています。このcontractを簡単に説明すると関数の振る舞いをコンパイラに対して伝えることが出来ます。
callsInPlaceは第一引数の関数が何回呼ばれるのかを定義出来ます。 呼び出し回数はkotlin.contracts.ContractBuilderに定義されているInvocationKindを指定できます。なお1.3では以下の4つを指定できます。

InvocationKind意味
AT_MOST_ONCE一度だけ呼ばれるか、一回も呼ばれない
AT_LEAST_ONCE少なくとも一回呼ばれる
EXACTLY_ONCE一度だけ呼ばれる
UNKNOWN何回呼ばれるかわからない

runではEXACTLY_ONCEが指定されています。これにより、コンパイラはこの関数が1度だけ実行される事がわかるので、変数に値が代入されたことをコンパイル時に認識することが出来るようになり、1.3以降では先程のコードのコンパイルが通ります。

もう一つ例を挙げます。

fun main(args: Array<String>) {  val hoge : String? = ""
  if(!hoge.isNullOrEmpty()) {
     hoge.first()
  }
}

このコードも1.2まではコンパイルが通りません。人間が見れば直前のif文内のisNullOrEmptyでnullチェックを行っているので、hogeはnon-nullなのがわかりますがコンパイラはわかりません。そのため、smart castは効かずにhogeがnullableと判断されてしまいます。
これも、contractを使えばhogeを解決出来ます。

  • kotlin1.3から
public inline fun CharSequence?.isNullOrEmpty(): Boolean {
    contract {
        returns(false) implies (this@isNullOrEmpty != null)
    }
    return this == null || this.length == 0
}

新しくimpliesが出てきました。 impliesは戻り値の状態によってimpliesで定義した状態を定義することが出来ます。 この場合はisNullOrEmptyの戻り値がfalseの場合にthisをnon-nullとコンパイラに伝えることが出来ます。 そのため、先程のコードは1.3からコンパイルが通るようになります。

1.3からcontractが追加された既存のメソッド達です

  • run, let, with, apply, also, takeIf, takeUnless, synchronized
  • isNullOrEmpty, isNullOrBlank,
  • kotlin.test: assertTrue, assertFalse, assertNotNull
  • check, checkNotNull, require, requireNotNull

assert系にもcontractが追加されているので、testコードもスッキリすると思います。

BuilderInference

1.3までcoroutineで使われていたbuildSequencesequenceに置き換えられました。 @BuilderInferenceが追加されたためです。(1.3からはbuildSequenceにもBuilderInferenceはついています)
BuilderInference annotationを使うとlamda式の型パラメータを省略することが出来ます。自作関数に追加することも可能です。

  • Kotlin1.3まで
fun <T> buildList(
    init: MutableList<T>.() -> Unit
): List<T> {
    return mutableListOf<T>().apply(init)
}

fun main() {
    val list = buildList<Int> {
        add(1)
        add(2)
    }
}
  • Kotlin1.3から
@UseExperimental(ExperimentalTypeInference::class)
fun <T> buildList(
    @BuilderInference init: MutableList<T>.() -> Unit
): List<T> {
    return mutableListOf<T>().apply(init)
}

fun main() {
    val list = buildList {
        add(1)
        add(2)
    }
}

BuilderInference annotationを引数に追加すると、mainで使っているbuildListの型パラメータが不要になっています。ただし、前述で説明したとおりexperimentalになるのでUseExperimental annotationが必要となります。

inline class

ついにclassにinlineが追加できるようになりました!
これが出来ると何が便利かというと、例えば番組オブジェクトを取得するAPIがあるとします。番組を取得するAPIには一意のid(今回はInt型)を持っています。名前を付けるだけならtype aliasで対応出来ていたのですが、あくまでaliasなので型としての強制力は無く不十分でした。type aliasは引数というよりはコールバックに対して名前を付ける方が適しているように思います。
type aliasとinline classの使い分けに関してはこちら に記述されています。

typealias ChannelId = Int

class ProgramRepository() {
    suspend fun getPrograms(channelId: ChannelId, count: Int) : Result<List<Program>> {
        return ...
    }
}

fun main() {
  repository.getPrograms(10, 1234) // typealiasに型の強制力はないので、idなのかcountなのかわからない 
}

この場合、programがprogramのidとcountの数を取る場合、どちらもint型なので代入の際に順番が入れ替わってもわかりません。かと言って、区別するためだけにclassを引数に取ると毎回classを作成するコストが高いのでベストプラクティスとは言えませんでした。
しかし、1.3からclassのinlineが使えるようになったため、以下のように気軽に引数にclassを使えるようになりました。ただし、inline classのprimaryコンストラクタは1つのみです。


inline class ChannelId(val channelId: Long)
inline class Count(val count: Long)

class ProgramRepository() {
    suspend fun getPrograms(channelId: ProgramId, count: Count) : Result<List<Program>> {
        return ...
    }
}

fun main() {
  repository.getProgram(ProgramId(1234), Count(10)) // 順番を間違えない
}

inline classはまだexperimentalです。そのためコンパイラではlintが表示されます。非表示にしたい場合はgradleに以下のoptionを追加しましょう。

compileKotlin {
    kotlinOptions.freeCompilerArgs += ["-XXLanguage:+InlineClasses"]
}

Unsigned integers

ついにKotlinにもunsignedが追加されました。他の言語を触っていれば特に問題なく使えると思います。

  • kotlin.UByte: an unsigned 8-bit integer, ranges from 0 to 255
  • kotlin.UShort: an unsigned 16-bit integer, ranges from 0 to 65535
  • kotlin.UInt: an unsigned 32-bit integer, ranges from 0 to 2^32 - 1
  • kotlin.ULong: an unsigned 64-bit integer, ranges from 0 to 2^64 - 1

uをつけれれば、unsignedの定義も今までどおり出来ます。便利ですね。unsignedも一応experimentalです。

// You can define unsigned types using literal suffixes
val uint = 42u 
val ulong = 42uL
val ubyte: UByte = 255u

// hex
val hexUByte: UByte = 0xFFu  // 255
val hexUShort: UShort = 0xFFFFu  // 65535
val hexUInt: UInt = 0xFFFF_FFFFu  // 4294967295
val hexULong: ULong = 0xFFFF_FFFF_FFFF_FFFFu  // 18446744073709551615

// You can convert signed types to unsigned and vice versa via stdlib extensions:
val int = uint.toInt()
val byte = ubyte.toByte()
val ulong2 = byte.toULong()

// String to unsigned
val uByte: UByte = "255".toUByte()
val uShort: UShort = "65535".toUShort()
val uInt: UInt = "4294967295".toUInt()
val uLong: ULong = "18446744073709551615".toULong()

// Unsigned array list 
val uByteArray: UByteArray = ubyteArrayOf(1.toUByte(), 2.toUByte(), 3.toUByte())
val uShortArray: UShortArray = ushortArrayOf(1.toUShort(), 2.toUShort(), 3.toUShort())
val uIntArray: UIntArray = uintArrayOf(1u, 2u, 3u)
val uLongArray: ULongArray = ulongArrayOf(1uL, 2uL, 3uL)

// Range
(0u .. UInt.MAX_VALUE).forEach {
    println(it)
}

// Range of unsigned Long
(0uL .. ULong.MAX_VALUE).forEach {
    println(it)
}

// example
val x = 20u + 22u
val y = 1u shl 8

@JvmStatic, @JvmField, @JvmDefault

今までinterfaceには@JvmField, @JvmStatic annotationを付けることは出来ませんでした。そもそも、interfaceにstatic変数、メソッドを持てるのがJava1.8からなので、もし使う場合はJVM targetを1.8にする必要があります。

interface Hoge {
    companion object {
        @JvmField
        val foo: Int = 46

        @JvmStatic
        fun bar() {
            println("Hello, world!")
        }
    }
}

同様に@JvmDefaultも用意されています。Java1.8のinterfaceのdefault関数が定義出来ます。
ただし、こちらはexperimentalです。

interface Hoge {
    companion object {
        @JvmDefault
        fun foo(): Int = 46
    }
}

Nested declarations in annotation classes

annotation classにenumとcompanion objectをネスト出来るようになりました。
こんな感じのコードや

annotation class Foo(val direction: Direction) {
    enum class Direction { UP, DOWN, LEFT, RIGHT }

    annotation class Bar

    companion object {
        fun foo(): Int = 42
        val bar: Int = 42
    }
}

@Foo(Foo.Direction.UP)
class Example() {
    init {
        println("foo: ${Foo.foo()}")
    }
}

こんな感じのコードが書けるようになりました。

annotation class Hoge {
    annotation class Foo(val foo: String)
    annotation class Bar(val foo: Int, val bar: Double = 1.0)
}

@Hoge.Foo("convenience")
@Hoge.Bar(foo = 10)
class Example {

}

Functions with big arity

1.3まで関数の引数には22個までしかとれなかったのですが、1.3からはたくさん引数を取れるようになりました。試してみたら数えるのが大変になったのでやめました。本当にたくさんとれます。そもそも22個以上引数を取ることが少ないと思うのであまり実感は無いですが、ライブラリ等を作っていると便利と感じる時が来るかもしれません。

Standard library

Random

Multiplatformを意識して、kotlin自体にrandomが実装されました。今まではjava.util.Randamが使われていたので注意しましょう。

import kotlin.random.Random

val number = Random.nextInt(46)  // number is in range [0, limit)
println(number)

assosiateWith

collections classにassociateWithという関数が追加されました。 元々associateという関数があったのですが、わざわざ自分でpairを作成する必要がありました。associateWithでは、pairを作らずそのままlamdaの中でmapが作成されます。

  • 1.3まで
val keys = 'a'..'f'
val map = keys.associate { it to it.toString().capitalize() }
map.forEach { println(it) }
  • 1.3から
val keys = 'a'..'f'
val map = keys.associateWith { it.toString().capitalize() }
map.forEach { println(it) }

isNullOrEmpty, orEmpty

collections, maps, arrays達にもisNullOrEmptyが追加されました。 今まではnullの場合orEmptyがempty listを返して来たので本質とは異なるチェックをする必要がありましたが、null checkにorEmptyを使う必要は無くなりました。便利です。

Copying elements between two existing arrays

Arrayの拡張関数として、copyInto が追加されました。パッと良い例が思いつかないのですが、簡単にarrayに挿入出来るのは便利です。

val sourceArr = arrayOf("k", "o", "t", "l", "i", "n")
val targetArr = sourceArr.copyInto(arrayOfNulls<String>(6), 3, startIndex = 3, endIndex = 6)
println(targetArr.contentToString()) // [null, null, null, l, i, n]

sourceArr.copyInto(targetArr, startIndex = 0, endIndex = 3)
println(targetArr.contentToString()) // [k, o, t, l, i, n]

ifEmpty

collections, maps, object arrays, char sequences, sequencesに対してifEmptyが追加されました。 kotlinのlist操作がより強力になって、かなり便利になりました。

fun printAllUppercase(data: List<String>) {
    val result = data
    .filter { it.all { c -> c.isUpperCase() } }
        .ifEmpty { listOf("<no uppercase>") }
    result.forEach { println(it) }
}

printAllUppercase(listOf("foo", "Bar")) // <no uppercase>
printAllUppercase(listOf("FOO", "BAR")) // FOO BAR

ifBlank

CharSequenceとStringにifBlankが追加されています。blankならlamdaに渡したdefault valueが表示されます。ifEmptyとは異なり文字列が空ではなくすべて空白であるかどうかを確認します。

val s = "    \n"
println(s.ifBlank { "<blank>" }) // <blank>
println(s.ifBlank { null }) // null

val s2 = "abc"
println(s.ifBlank { "xyz" }) // abc

Boolean

1.3まではBooleanがCompanion objectをとれなかったので、実はBooleanには拡張関数を追加することが出来ませんでした。1.3から取れるようになっています。
あまりBooleanに拡張関数を追加することが無いと思うので、そもそも気付いていなかった人のほうが多そうですが…

fun Boolean.Companion.isHoge(): Boolean = true

まとめ

全てには触れていないので、他にも追加されたAPIや変更がたくさんあります。 今までずっと痒いと思っていた所に手が届いている印象です。この記事も途中から便利しか言ってませんw
コア機能の追加が忙しくて後回しになってた、やりたい事にようやく手を付けられた感じがしました。(推測)
Kotlinも1.3になりますます盛り上がりを見せています。Kotlin multi platformの進化にも期待です。

Have a nice Kotlin!

© AAkira 2023