Help us understand the problem. What is going on with this article?

Kotlin の Extensions について色々試す

More than 3 years have passed since last update.

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 は、そのへんどうしているのだろう?

試す

まずは簡単なの

hello.kt
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);
}

HelloKthoge() という static メソッドが追加されている!

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

あれ、ということは、適当なクラス内で定義したら。。。

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.classHoge.classjavap で覗いてみる。

> 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() メソッドを参照できる?

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

class Hoge {

    fun method() {
        "hoge".hoge()
    }

    fun String.hoge() {
        println("<$this>")
    }
}
実行結果
<hoge>

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

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

他のパッケージで宣言されたものはどうやって利用する?

foo.kt
package foo

fun String.hoge() {
    println("[$this]")
}
hello.kt
import foo.hoge

fun main(args : Array<String>) {
    "extensions".hoge()
}
実行結果
[extensions]
  • 他のパッケージで宣言された拡張メソッドは、 import すれば使えるようになる。
  • ただし、拡張メソッドがクラス内に宣言されていると import はできないっぽい。
foo.kt
package foo

class Foo {
    fun String.hoge() {
        println("[$this]")
    }
}
hello.kt
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 に定義されたものしかダメらしい。

ということは、 Fooobject に宣言すればイケる?

foo.kt
package foo

object Foo {
    fun String.hoge() {
        println("{$this}")
    }
}
hello.kt
import foo.Foo.hoge

fun main(args : Array<String>) {
    "extensions".hoge()
}
実行結果
{extensions}

イケた!

参照できる範囲にいっぱい拡張メソッドがあったらどうなるの?

foo.kt
package foo

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

fun String.hoge() {
    println("foo.hoge")
}
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")
}
実行結果
Hoge.method.hoge

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

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

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")
}
実行結果
Hoge.hoge

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

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

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.Hoge.hoge

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

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

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")
}
実行結果
Foo.hoge

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

import 宣言を削除する。

hello.kt
import foo.*

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

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

fun String.hoge() {
    println("hoge")
}
実行結果
hoge

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

どんどん削除していく。

hello.kt
import foo.*

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

class Hoge {
    fun method() {
        "extensions".hoge()
    }
}
実行結果
foo.hoge

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

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

まとめ

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

参考

opengl-8080
ただのSE。Java好き。
tis
創業40年超のSIerです。
https://www.tis.co.jp/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした