4/2(土) Kotlin 1.0リリース記念勉強会 in 京都 - connpass に参加してきて、個人的に仕組みがかなり気になった Extensions について色々試してみた。
Extensions とは
fun main(args : Array<String>) {
"extensions".hoge()
}
fun String.hoge() {
println("<$this>")
}
<extensions>
-
fun 型名.メソッド名() {}
というふうに、関数の前に型名.
を付けることで、既存のクラスにメソッドを追加することができる。
何故こんな仕組みが必要?
公式サイトによると、以下のことがモチベーション(動機)となっているらしい。
Java では、 *Utils
と名付けられたクラスがよく使われる(StringUtils
とか FileUtils
とか)。
有名ドコロとしては、 java.util.Collections
とかがある。
これを使うと、以下のようにコードがごちゃごちゃになってつらい。
Collections.swap(list, Collections.binarySearch(list, Collections.max(otherList)), Collections.max(list))
static インポートを使えば、以下のようにすることもできる。
swap(list, binarySearch(list, max(otherList)), max(list))
これは幾分かマシだが、 IDE による強力なコード補完の恩恵が受けにくくなるという問題がある。
補足
- Eclipse の場合、コンテンツ・アシストのお気に入りに登録して、 static インポートの必要数を1にすれば、コード補完をかけることもできる。
- しかし、「クラスごとに設定が必要」「全ての該当するクラスが候補として列挙される」という問題がどうしても残る。
できれば、以下のように書けると嬉しい。
list.swap(list.binarySearch(otherList.max()), list.max())
ということで、この Extensions という仕組みが用意されたらしい。
*Utils
を使った実装をシンプルに記述できて、かつ IDE によるコード補完の恩恵も最大限受けたい、ということが動機らしい。
(オブジェクトを自由に拡張したい、というのが根本的な目的ではないっぽい)
何が気になった?
- Kotlin は静的型付け言語のはずなのに、メソッドを追加できるというのは不思議な感じがした。
- 追加されたメソッドを使った実装を書いた場合、コンパイラはどこで「そのメソッドが追加された」ことを知ることができるのだろうか?
- 例えば Groovy も任意のメソッドを追加できるけど、コンパイル時はチェックせずに、実行時にメソッドがなければエラーになる。
- Kotlin は、そのへんどうしているのだろう?
試す
まずは簡単なの
fun main(args : Array<String>) {
"extensions".hoge()
}
fun String.hoge() {
println("<$this>")
}
これを kotlinc
でコンパイルする。
> 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 は、本当にクラスにメソッドを追加しているわけではなく、引数に対象オブジェクトを受け取るメソッド呼び出しのシンタックスシュガー的なものになっているらしい。
あれ、ということは、適当なクラス内で定義したら。。。
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()
メソッドを参照できる?
fun main(args : Array<String>) {
Hoge().method()
}
class Hoge {
fun method() {
"hoge".hoge()
}
fun String.hoge() {
println("<$this>")
}
}
<hoge>
コンパイルできて動いた。
どうやら、参照できる範囲内に拡張メソッドの定義が無いとコンパイルエラーになるらしい。
他のパッケージで宣言されたものはどうやって利用する?
package foo
fun String.hoge() {
println("[$this]")
}
import foo.hoge
fun main(args : Array<String>) {
"extensions".hoge()
}
[extensions]
- 他のパッケージで宣言された拡張メソッドは、
import
すれば使えるようになる。 - ただし、拡張メソッドがクラス内に宣言されていると
import
はできないっぽい。
package foo
class Foo {
fun String.hoge() {
println("[$this]")
}
}
import foo.Foo.hoge
fun main(args : Array<String>) {
"extensions".hoge()
}
> 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
に宣言すればイケる?
package foo
object Foo {
fun String.hoge() {
println("{$this}")
}
}
import foo.Foo.hoge
fun main(args : Array<String>) {
"extensions".hoge()
}
{extensions}
イケた!
参照できる範囲にいっぱい拡張メソッドがあったらどうなるの?
package foo
object Foo {
fun String.hoge() {
println("Foo.hoge")
}
}
fun String.hoge() {
println("foo.hoge")
}
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")
}
Hoge.method.hoge
一番近くの奴(method()
内のローカル宣言)が採用された。
じゃあ、これを消したら次はどれが採用される?
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")
}
Hoge.hoge
同じクラス内で宣言したものが採用された。
次は、これを削除するとどうなる?
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.Hoge.hoge
コンパニオンオブジェクト内で宣言したものが採用された。ここまではある意味予想通り。
これを削除したら、次はどれが採用される?
import foo.*
import foo.Foo.hoge
fun main(args : Array<String>) {
Hoge().method()
}
class Hoge {
fun method() {
"extensions".hoge()
}
}
fun String.hoge() {
println("hoge")
}
Foo.hoge
import
した object
で宣言したものが採用された(同じファイル内でトップレベルで宣言した奴ではなかった)。
import
宣言を削除する。
import foo.*
fun main(args : Array<String>) {
Hoge().method()
}
class Hoge {
fun method() {
"extensions".hoge()
}
}
fun String.hoge() {
println("hoge")
}
hoge
今度は import
先ではなく、トップレベルのものが採用された。
どんどん削除していく。
import foo.*
fun main(args : Array<String>) {
Hoge().method()
}
class Hoge {
fun method() {
"extensions".hoge()
}
}
foo.hoge
最後に採用されたのは、 import
した別パッケージにトップレベルで宣言されたものだった。
import
先のオブジェクトが絡んできたあたりで、優先順序がよく分からなくなった気がする(まぁ、こんなややこしいことは普通しないので、気にする必要はないと思うけど)。
まとめ
- 拡張用のメソッドは、そのメソッドを利用できるところから参照できる場所になければならない。
- 参照できる範囲に該当するメソッドがなければ、コンパイルエラーになる。
-
import
で、他パッケージ(オブジェクト)に宣言された拡張用のメソッドを読み込むことができる。