Kotlin の拡張関数について


この記事は、モバイルファクトリー Advent Calendar 2015 の 14 日目の記事です。

昨日は akihiro さんのiOSアプリのレイアウト事始め でした。

今日は Java との相互運用性を強く意識した JVM で動作する言語 Kotlin の紹介と、Kotlin の拡張関数という機能を取り上げて説明したいと思います。

TL;DL

  • Kotlin はとてもかわいくて、素晴らしい言語
  • Kotlin の拡張関数は static メソッドとして実装されている
  • みんなで Kotlin 使おう

Kotlin とは ?

Kotlin (ことりん) は IntelliJ IDEA で有名な JetBrains 社が開発している Java との相互運用性を持ったプログラミング言語です。オープンソースで開発されています。

Java の反省を活かして開発された言語で、Java と同等のプログラムがより安全に・短く・直感的に記述できることが特徴です。

日本語の小鳥に響きが似ているため、かわいい言語と言われることがあります 1

拡張関数とは ?

ここで言う拡張関数とは、既存のクラスに対して後からメソッドを追加することを指します。拡張メソッド 2 や、エクステンションと呼ばれることもあります。

たとえば、Ruby のような動的言語だと、一度定義したクラスに対してでも簡単にメソッドを追加することができます。

class AdventCalendar
  def year
    2015
  end

  def month
    12
  end

  def to_s
    [year, month, day].join('/')
  end
end

class AdventCalendar
  def day
    14
  end
end

calendar = AdventCalendar.new
p calendar.to_s #=> 2015/12/14

しかし、静的言語においては、一度定義したクラスの定義は、後から変更を加える事ができないことが ほとんど です。これを可能にするのが拡張関数です。

もちろん、Kotlin から Java のクラスとして定義したクラスを書き換えることはできません。そのため Kotlin では、既存のクラスに対してのメソッドを別途定義し、見かけ上はメソッドとして呼んでいるように見える 実装がされています 3

Kotlin での拡張関数とその実装

Kotlin では、既存のクラスに対して以下のように拡張関数を追加することができます。
たとえば Kotlin の数値に対して、その数値だけ繰り返す処理 times を追加する場合を考えてみます。

Main.kt
@file:kotlin.jvm.JvmName("Main")

fun Int.times(callback: (Int) -> Unit) {
    repeat(this) { callback(it) }
}

fun main(args: Array<String>) {
    3.times { println(it) }
}

times は引数に実行する処理 callback を受け取り、自身の数値回数繰り返し callback を呼び出します。repeattimes と同じ動作をする関数で、Kotlin の標準関数です。

上記 Main.kt をコンパイルすると *.class ファイルが生成できます。Kotlin のランタイムに依存しているため、クラスファイルは直接 Java では実行できませんが、kotlin コマンドで実行することができます。

$ kotlinc -version
info: Kotlin Compiler version 1.0.0-beta-2423
info: PERF: INIT: Compiler initialized in 517 ms

$ kotlinc Main.kt
info: PERF: INIT: Compiler initialized in 561 ms
info: PERF: ANALYZE: 1 files (9 lines) in 660 ms
info: PERF: GENERATE: 1 files (9 lines) in 92 ms

$ kotlin Main
0
1
2

$ ls
META-INF
Main$main$1.class
Main.class
Main.kt

せっかくなので、デコンパイルして実装を見てみましょう。

$ jad Main.class
Parsing Main.class...Parsing inner class Main$main$1.class... Generating Main.jad

Main.jad が生成されたのでそれを見てみます。

Main.jad
// Decompiled by Jad v1.5.8g. Copyright 2001 Pavel Kouznetsov.
// Jad home page: http://www.kpdus.com/jad.html
// Decompiler options: packimports(3)
// Source File Name:   Main.kt

import kotlin.Unit;
import kotlin.io.ConsoleKt;
import kotlin.jvm.functions.Function1;
import kotlin.jvm.internal.Intrinsics;
import kotlin.jvm.internal.Lambda;

public final class Main
{

    public static final void times(int $receiver, Function1 callback)
    {
        Intrinsics.checkParameterIsNotNull(callback, "callback");
        int index = 0;
        int i = $receiver - 1;
        if(index <= i)
            do
            {
                int it = index;
                callback.invoke(Integer.valueOf(it));
                Unit _tmp = Unit.INSTANCE;
                if(index == i)
                    break;
                index++;
            } while(true);
    }

    public static final void main(String args[])
    {
        Intrinsics.checkParameterIsNotNull(args, "args");
        public static final class main._cls1 extends Lambda
            implements Function1
        {

            public volatile Object invoke(Object obj)
            {
                invoke(((Number)obj).intValue());
                return Unit.INSTANCE;
            }

            public final void invoke(int it)
            {
                ConsoleKt.println(it);
            }

            public static final main._cls1 INSTANCE = new main._cls1();


        }

        times(3, (Function1)main._cls1.INSTANCE);
    }
}

以下の行より、Kotlin では、拡張関数が static メソッドとして実装されており、既存のクラスに対して一切の変更を加えずに実現されていることが分かります。

public static final void times(int $receiver, Function1 callback)

つまり、拡張関数内では private なメンバを呼ぶことが許されません

実際に times の処理を行っているのは以下の所です。元の Kotlin のコードでは repeat 関数を読んでいるだけですが、inline アノテーションが付与されているため、処理内容が times 内にインライン展開されています。

        Intrinsics.checkParameterIsNotNull(callback, "callback");
        int index = 0;
        int i = $receiver - 1;
        if(index <= i)
            do
            {
                int it = index;
                callback.invoke(Integer.valueOf(it));
                Unit _tmp = Unit.INSTANCE;
                if(index == i)
                    break;
                index++;
            } while(true);

Kotlin のクロージャも綺麗に Java 4 で扱えるように変換されていますね。綺麗に簡潔に記述できる Kotlin すごい!!

まとめ

拡張関数以外にも、Kotlin には便利な機能がたくさんあります。Java との相互運用性にかなり配慮されて作られていて、Java コードから Kotlin への自動変換も可能であるので、機会があればぜひ使ってみてください。

明日は、kazuman519 さんが Advent Calendar に新しい風を吹かしてくれます。期待ですね!!


  1. https://ja.wikipedia.org/wiki/Kotlin 

  2. https://msdn.microsoft.com/ja-jp/library/bb383977.aspx 

  3. C# の拡張メソッドの実装とほとんど同じですが、Kotlin の方がよりスマートに定義できるようになっています 

  4. ここで言う Java は、Java 8 以前のことを指します