Kotlin の Extensions について色々試す

[4/2(土) Kotlin 1.0リリース記念勉強会 in 京都 - connpass](http://kanjava.connpass.com/event/27758/) に参加してきて、個人的に仕組みがかなり気になった Extensions について色々試してみた。

# Extensions とは
```kotlin
fun main(args : Array<String>) {
    "extensions".hoge()
}

fun String.hoge() {
    println("<$this>")
}
```

```text:実行結果
<extensions>
```

- `fun 型名.メソッド名() {}` というふうに、関数の前に `型名.` を付けることで、既存のクラスにメソッドを追加することができる。

# 何故こんな仕組みが必要？
[公式サイトによると](https://kotlinlang.org/docs/reference/extensions.html#motivation)、以下のことがモチベーション（動機）となっているらしい。

Java では、 `*Utils` と名付けられたクラスがよく使われる（`StringUtils` とか `FileUtils` とか）。
有名ドコロとしては、 `java.util.Collections` とかがある。

これを使うと、以下のようにコードがごちゃごちゃになってつらい。

```java
Collections.swap(list, Collections.binarySearch(list, Collections.max(otherList)), Collections.max(list))
```

static インポートを使えば、以下のようにすることもできる。

```java
swap(list, binarySearch(list, max(otherList)), max(list))
```

これは幾分かマシだが、 IDE による強力なコード補完の恩恵が受けにくくなるという問題がある。

**補足**

- Eclipse の場合、コンテンツ・アシストのお気に入りに登録して、 static インポートの必要数を１にすれば、コード補完をかけることもできる。
- しかし、「クラスごとに設定が必要」「全ての該当するクラスが候補として列挙される」という問題がどうしても残る。

できれば、以下のように書けると嬉しい。

```java
list.swap(list.binarySearch(otherList.max()), list.max())
```

ということで、この Extensions という仕組みが用意されたらしい。

`*Utils` を使った実装をシンプルに記述できて、かつ IDE によるコード補完の恩恵も最大限受けたい、ということが動機らしい。
（オブジェクトを自由に拡張したい、というのが根本的な目的ではないっぽい）

# 何が気になった？
- Kotlin は静的型付け言語のはずなのに、メソッドを追加できるというのは不思議な感じがした。
- 追加されたメソッドを使った実装を書いた場合、コンパイラはどこで「そのメソッドが追加された」ことを知ることができるのだろうか？
- 例えば Groovy も任意のメソッドを追加できるけど、コンパイル時はチェックせずに、実行時にメソッドがなければエラーになる。
- Kotlin は、そのへんどうしているのだろう？

# 試す
## まずは簡単なの
```kotlin:hello.kt
fun main(args : Array<String>) {
    "extensions".hoge()
}

fun String.hoge() {
    println("<$this>")
}
```

これを `kotlinc` でコンパイルする。

```text
> kotlinc hello.kt

> dir /b
hello.kt
HelloKt.class
META-INF
```

`HelloKt.class` というファイルが吐き出されるので、これを `javap` で見る。

```
> javap HelloKt.class
Compiled from "hello.kt"
public final class HelloKt {
  public static final void main(java.lang.String[]);
  public static final void hoge(java.lang.String);
}
```

`HelloKt` に `hoge()` という static メソッドが追加されている！

Extensions は、本当にクラスにメソッドを追加しているわけではなく、引数に対象オブジェクトを受け取るメソッド呼び出しのシンタックスシュガー的なものになっているらしい。

## あれ、ということは、適当なクラス内で定義したら。。。
```kotlin:hello.kt
fun main(args : Array<String>) {
    "extensions".hoge()
}

class Hoge {
    fun String.hoge() {
        println("<$this>")
    }
}
```

```
> kotlinc hello.kt
hello.kt:2:18: error: unresolved reference: hoge
    "extensions".hoge()
```

`hoge()` メソッドが見つからずにコンパイルエラーになった。

`"extensions".hoge()` を消してコンパイルできるようにしてから、 `HelloKt.class` と `Hoge.class` を `javap` で覗いてみる。

```
> javap HelloKt.class
Compiled from "hello.kt"
public final class HelloKt {
  public static final void main(java.lang.String[]);
}

> javap Hoge.class
Compiled from "hello.kt"
public final class Hoge {
  public final void hoge(java.lang.String);
  public Hoge();
}
```

`hoge()` メソッドは `Hoge` クラスの方に**インスタンスメソッド**として追加されていて、 `HelloKt` からは直接参照できないようになっている。

## ということは、 Hoge クラス内なら `hoge()` メソッドを参照できる？
```kotlin:hello.kt
fun main(args : Array<String>) {
    Hoge().method()
}

class Hoge {
    
    fun method() {
        "hoge".hoge()
    }
    
    fun String.hoge() {
        println("<$this>")
    }
}
```

```text:実行結果
<hoge>
```

コンパイルできて動いた。

どうやら、参照できる範囲内に拡張メソッドの定義が無いとコンパイルエラーになるらしい。

## 他のパッケージで宣言されたものはどうやって利用する？
```kotlin:foo.kt
package foo

fun String.hoge() {
    println("[$this]")
}
```

```kotlin:hello.kt
import foo.hoge

fun main(args : Array<String>) {
    "extensions".hoge()
}
```

```text:実行結果
[extensions]
```

- 他のパッケージで宣言された拡張メソッドは、 `import` すれば使えるようになる。
- ただし、拡張メソッドがクラス内に宣言されていると `import` はできないっぽい。

```kotlin:foo.kt
package foo

class Foo {
    fun String.hoge() {
        println("[$this]")
    }
}
```

```kotlin:hello.kt
import foo.Foo.hoge

fun main(args : Array<String>) {
    "extensions".hoge()
}
```

```text:コンパイル結果
> kotlinc hello.kt foo\foo.kt
hello.kt:1:16: error: cannot import 'hoge', functions and properties can be imported only from packages or objects
import foo.Foo.hoge
               ^
hello.kt:4:18: error: unresolved reference: hoge
    "extensions".hoge()
                 ^
```

コンパイルエラーになった。
関数のインポートは、パッケージ（トップ）レベルか、 `object` に定義されたものしかダメらしい。

ということは、 `Foo` を `object` に宣言すればイケる？

```kotlin:foo.kt
package foo

object Foo {
    fun String.hoge() {
        println("{$this}")
    }
}
```

```kotlin:hello.kt
import foo.Foo.hoge

fun main(args : Array<String>) {
    "extensions".hoge()
}
```

```text:実行結果
{extensions}
```

イケた！

## 参照できる範囲にいっぱい拡張メソッドがあったらどうなるの？
```kotlin:foo.kt
package foo

object Foo {
    fun String.hoge() {
        println("Foo.hoge")
    }
}

fun String.hoge() {
    println("foo.hoge")
}
```

```kotlin:hello.kt
import foo.*
import foo.Foo.hoge

fun main(args : Array<String>) {
    Hoge().method()
}

class Hoge {
    companion object Hoge {
        fun String.hoge() {
            println("Hoge.Hoge.hoge")
        }
    }
    
    fun method() {
        fun String.hoge() {
            println("Hoge.method.hoge")
        }
        
        "extensions".hoge()
    }
    
    fun String.hoge() {
        println("Hoge.hoge")
    }
}

fun String.hoge() {
    println("hoge")
}
```

```text:実行結果
Hoge.method.hoge
```

一番近くの奴（`method()` 内のローカル宣言）が採用された。

じゃあ、これを消したら次はどれが採用される？

```kotlin:hello.kt
import foo.*
import foo.Foo.hoge

fun main(args : Array<String>) {
    Hoge().method()
}

class Hoge {
    companion object Hoge {
        fun String.hoge() {
            println("Hoge.Hoge.hoge")
        }
    }
    
    fun method() {
        "extensions".hoge()
    }
    
    fun String.hoge() {
        println("Hoge.hoge")
    }
}

fun String.hoge() {
    println("hoge")
}
```

```text:実行結果
Hoge.hoge
```

同じクラス内で宣言したものが採用された。

次は、これを削除するとどうなる？

```kotlin:hello.kt
import foo.*
import foo.Foo.hoge

fun main(args : Array<String>) {
    Hoge().method()
}

class Hoge {
    companion object Hoge {
        fun String.hoge() {
            println("Hoge.Hoge.hoge")
        }
    }
    
    fun method() {
        "extensions".hoge()
    }
}

fun String.hoge() {
    println("hoge")
}
```

```text:実行結果
Hoge.Hoge.hoge
```

コンパニオンオブジェクト内で宣言したものが採用された。ここまではある意味予想通り。

これを削除したら、次はどれが採用される？

```kotlin:hello.kt
import foo.*
import foo.Foo.hoge

fun main(args : Array<String>) {
    Hoge().method()
}

class Hoge {
    fun method() {
        "extensions".hoge()
    }
}

fun String.hoge() {
    println("hoge")
}
```

```text:実行結果
Foo.hoge
```

`import` した `object` で宣言したものが採用された（同じファイル内でトップレベルで宣言した奴ではなかった）。

`import` 宣言を削除する。

```kotlin:hello.kt
import foo.*

fun main(args : Array<String>) {
    Hoge().method()
}

class Hoge {
    fun method() {
        "extensions".hoge()
    }
}

fun String.hoge() {
    println("hoge")
}
```

```text:実行結果
hoge
```

今度は `import` 先ではなく、トップレベルのものが採用された。

どんどん削除していく。

```kotlin:hello.kt
import foo.*

fun main(args : Array<String>) {
    Hoge().method()
}

class Hoge {
    fun method() {
        "extensions".hoge()
    }
}
```

```text:実行結果
foo.hoge
```

最後に採用されたのは、 `import` した別パッケージにトップレベルで宣言されたものだった。

`import` 先のオブジェクトが絡んできたあたりで、優先順序がよく分からなくなった気がする（まぁ、こんなややこしいことは普通しないので、気にする必要はないと思うけど）。

# まとめ
- 拡張用のメソッドは、そのメソッドを利用できるところから参照できる場所になければならない。
- 参照できる範囲に該当するメソッドがなければ、コンパイルエラーになる。
- `import` で、他パッケージ（オブジェクト）に宣言された拡張用のメソッドを読み込むことができる。

# 参考
- [Extensions - Kotlin Programming Language](https://kotlinlang.org/docs/reference/extensions.html)