Kotlinは関数を変数に代入したり、引数で受け渡したりできます。
関数を第一級オブジェクトで扱えるためです。
これにより、関数を使った処理の委譲が容易になりました。ArrayOf#forEach( )がその典型的な例です。
以上のような操作で必要となる「関数オブジェクト」について、まとめます。
関数オブジェクトとは
第一級オブジェクト(first class object)は生成・代入・演算・受け渡しといったプログラムの基本的な操作に使用できるオブジェクトのことです。
数値オブジェクトのInt・Float・Doubleクラスや、文字列オブジェクトのStringクラスは第一級オブジェクトです。
Kotlinは関数も第一級オブジェクトです。
オブジェクトとして扱われる関数を関数オブジェクトと言います。
関数オブジェクトの生成と代入
関数の定義から「::関数名」で関数オブジェクトが取り出せます。また、関数オブジェクトはラムダ式(無名関数)で生成できます。
生成された関数オブジェクトは型を持ちます。型定義された変数に代入が可能です。
fun plus1(a: Int): Int { return a + 1 } val plus1: (Int)->Int = ::plus1 // 「::関数名」で関数オブジェクト val plus2: (Int)->Int = fun(a: Int): Int = a + 2 // 無名関数で生成 val plus3: (Int)->Int = { a: Int -> a + 3 } // ラムダ式で生成
関数の型 (引数の型)->戻り値の型 例: ()->Unit ... 引数なし、戻り値なし (Int)->Int ... 引数Int、戻り値Int (Int,Int)->Int ... 引数1Int・引数2Int、戻り値Int
ラムダ式 {引数のリスト->関数の本体} ※引数のリストは「,」で区切る ※return省略時の戻り値は最後に評価した式の結果 ※戻り値の型は指定不可(型推論で行う) 例: { a: Int -> a + 2 } ... Int型の引数a、a+2を返す { a: Int, b: Int -> a + b } ... Int型の引数aとb、a+bを返す { a: Int -> ... Int型の引数a、++aを2回行って返す var b = a ++b ++b <=戻り値 } ※例は原形のままのラムダ式です。ラムダ式は不要な部分を省略できます。
関数オブジェクトを受け渡し
関数オブジェクトは引数で受け渡しができます。
以下のサンプルは関数の引数が関数オブジェクトになっています。このように、引数で関数を受け渡しするものを高階関数と呼びます。
class Player { private var playCtrl: ((String)->Unit)? = null ... fun play(title: String) { playCtrl?.invoke(title) // 委譲された関数の実行 } ... fun setPlayCtrl(ctrl: (String)->Unit) { // 関数オブジェクトを引数で受ける playCtrl = ctrl } ... }
fun playCD(title: String) { println("CDで「${title}」を再生する!") } ... val _CDPlayer = Player().apply { setPlayCtrl(::playCD) // 関数オブジェクトを引数で渡す ... } _CDPlayer.play("SampleMusic") ...
関数オブジェクトを返す
関数オブジェクトを戻り値で返すことができます。
fun increment(): ()->Int { var value = 0 fun plus1(): Int { return value++ } return ::plus1 // 子関数オブジェクトを返す }
関数オブジェクトを実行
例えば、数値オブジェクトは「5」という数値がオブジェクトの値として格納されています。
同様に、関数オブジェクトは関数の記述そのものがオブジェクトの値として格納されていると見なせます。
従って、関数オブジェクトは値を格納する箱であり関数ではないので、そのまま実行することは出来ません。
fun exec() { println("plus1 = ${plus1}") // 関数オブジェクトは実行出来ない println("plus2 = ${plus2}") // 同上 println("plus3 = ${plus3}") // 同上 }
plus1 = function plus1 (Kotlin reflection is not available) plus2 = Function1<java.lang.Integer, java.lang.Integer> plus3 = Function1<java.lang.Integer, java.lang.Integer>
関数オブジェクトを実行するにはinvokeを使用します。これにはシンタックスシュガーが用意されています。
fun exec() { println("plus1 = ${plus1.invoke(10)}") // invokeが必要 println("plus2 = ${plus2(10)}") // ( )は上記のシンタックスシュガー println("plus2 = ${plus3(10)}") }
plus1 = 11 plus2 = 12 plus3 = 13
関数オブジェクトの内部的な表現
関数オブジェクト内の関数は、内部的に「SAM型のインターフェースを実装したクラスに属するメソッド(関数)」へ変換されています。ここで定義されたクラスが関数オブジェクトを表しています。
※SAM(Single Absolute Method):メソッド(関数)が一つしかない抽象クラス
内部的とはバイトコードの状態です。バイトコードをソースへ変換してみると、次のようになっていました。
public class plus1の関数オブジェクトextends FunctionReferenceImpl implements Function1<Integer, Integer> // 関数型インターフェイス { ... public final Integer invoke(int p0) { return XXXX.plus1(p0)); // 関数の記述そのものが入っているメソッド } @Override public Integer invoke(Integer num) { // 抽象メソッドの実装 return invoke(num.intValue()); } } // ★XXX.plus1( ) // public final int plus1(int a) { // return a + 1; // }
public interface Function1<P1, R> extends Function<R> { R invoke(P1 p1); }
JVM(Java Virtual Machine)はクラスに属さないメソッド(関数)を処理できません。
処理(代入や受け渡し)ができるようにするために、関数オブジェクト用のクラス作って、その中に「関数の記述そのものが記載されたメソッド」を格納しているのです。
関連記事: