Kotlinの拡張関数のガイドライン

2017.10.23

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

はじめに

Kotlinいいですよね。

少し前の- 第6回Kotlin勉強会 @ Sansan - connpass で拡張関数のガイドラインの発表がありました。とても良い資料だと思います。最近だんだんKotlin事例が多くなってきたなので、再掲してもっと広まってほしいです。

ちょっとわかりにくい所を少し補足的に付け足したいと思います。

interfaceに拡張関数に書く

スライドの「interface内で拡張関数定義」の部分の補足を足したい思います。

逆にわかりにくくなっているかも?

interfaceを実装しクラス内で使えるようにする

拡張関数は好きなところにかけます。静的に解決されるので注意が必要です。 オレオレ拡張関数の無法地帯だと、思わぬ罠にはまります。 それは、スライドで言われている通りです。

JavaでUtilクラスレベルのものは、グローバルなところに書いてOKだと思いますが、コンテキストが必要な場合は、他の人が使用すとき混乱してしまうかもしれません。

たとえば、Intに時間に関する拡張関数を書いたとします。

val Int.seconds get() = this * 1000
val Int.minutes get() = this.seconds * 60

1.minutes // => 60000

一見良さそうに見えますが、暗黙の前提としてms(ミリ秒)である必要があります。

要件によって、人によっては、s(秒)が基準で6.minutesの期待値が60かもしれません。しかし、すでに存在するのでどちらかを諦めるか、別の名前にするかです。

前提が必要な場合は、interfaceに閉じ込める方法がベターです。 英語力のなさが露呈しますが、以下のようなinterfaceをつくります。

// msであることを前提とする、拡張関数をまとめる
interface TimeMsInterface {
    val Int.seconds get() = this * 1000
    val Int.minutes get() = this.seconds * 60
}

使用する場合は、interfaceを実装する。そのclass内で使えるようになります。

class Hoge : TimeMsInterface {
    val foo = 1.minutes
    fun hiyo() = 1.seconds
}

また、拡張関数をつかえるスコープを限定するとIDEにムダに保管されることもありません。

一時的に拡張関数をつかえるようにする

クラスで実装するまででなく、一時的に使い場合があります。

スライドで乗っている方法だと

interface TimeMsInterface {
    val Int.seconds get() = this * 1000
    val Int.minutes get() = this.seconds * 60
}

fun calcTime(f: TimeMsInterface.() -> Int) = object : TimeMsInterface {}.f()

calcTime { 1.minutes } // => 60000

???っとなると思いますので、補足をします。

//object式で無名クラスのオブジェクトを作成をします
val timeMsInterface = object : TimeMsInterface {}

//runをつかってレシーバー付ラムダなのでthisがTimeMsInterfaceになることで、TimeMsInterfaceの内部を使用することが出来る
timeMsInterface.run { 1.minutes }  // => 60000

このまま関数化する

fun calcTime(f: TimeMsInterface.() -> Int): Int {
    val timeMsInterface = object : TimeMsInterface {}
    return timeMsInterface.run(f)
}

f: TimeMsInterface.() -> Int は、レシーバーがTimeMsInterfaceになるということだけど、TimeMsInterfaceに一時的な拡張関数f()を作ると考えても良いので。

fun calcTime(f: TimeMsInterface.() -> Int): Int {
    val timeMsInterface = object : TimeMsInterface {}
    return timeMsInterface.f() //TimeMsInterfaceに一時的なf()メソッドを使用
}

わざわざ2行にする必要もないので

fun calcTime(f: TimeMsInterface.() -> Int) = object : TimeMsInterface 
{}.f()

返り値がInt限定にしてるので、Intを返す拡張関数しか使えません。少し発展させてどの拡張関数でも問題ないようにします。

fun<T> calcTime(f: TimeMsInterface.() -> T) = object : TimeMsInterface {}.f()

まとめ

拡張関数はとっても便利だけど、オレオレ度控えめで便利に使っていきましょう。