Kotlin:関数オブジェクト

投稿日:  更新日:

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)はクラスに属さないメソッド(関数)を処理できません。

処理(代入や受け渡し)ができるようにするために、関数オブジェクト用のクラス作って、その中に「関数の記述そのものが記載されたメソッド」を格納しているのです。

スポンサーリンク

関連記事:

クロージャ(Closure)は新しい概念ではなく、関数型プログラミングに古くから存在していました。 手続き型やオブジェクト指向プログラミングにも、だいぶ前から採用が進んでいます。 Kotlinもクロージャが使える言語の1つです。 ...
Androidのアプリを作成しているとSAM変換が頻繁に登場します。 プログラムが簡素に記述できることから、プログラマーに好評なようです。 Kotlinが関数を第一級オブジェクトとして扱える恩恵です。 しかし、私はSAM変換に出合うと、いつもプログラムの論理が把握できなくて戸惑います。なので、苦手です。あまり好きではありません。 苦手を克服するために、もっとSAM変換の理解を深めたいと思い、まとめてみました。 ...
近頃の携帯端末はクワッドコア(プロセッサが4つ)やオクタコア(プロセッサが8つ)が当たり前になりました。 サクサク動作するアプリを作るために、この恩恵を使わなければ損です。 となると、必然的に非同期処理(マルチスレッド)を使うことになります。 JavaのThreadクラス、Android APIのAsyncTaskクラスが代表的な手法です。 Kotlinは上記に加えて「コルーチン(Coroutine)」が使えるようになっています。 今回は、このコルーチンについて、まとめます。 ...
Android Studio Giraffe(2023.07)になって、ビルドスクリプトの推奨がKotlin DSLになりました。 この機会に、ビルドスクリプトと記述言語についてまとめます。 ※環境:Android Studio Giraffe | 2022.3.1    :Android Gradle Plugin 8.1.0    :Gradle 8.0 ...
Kotlinにおいて、各クラスのインスタンスに自動生成されるequals()関数は、classとdata classで比較対象に違いがあります。 紛らさしいので、まとめます。 ※環境:Android Studio Giraffe | 2022.3.1 Patch 1     Kotlin 1.8.10 ...
KotlinはJavaのプリミティブ型に当たるデータ型を持ちません。しかし、同等な記述が基本データ型を用いて出来ます。 Kotlinにおいて、基本データ型がどのように扱われているのか、まとめます。 ※環境:Android Studio Giraffe | 2022.3.1 Patch 1     Kotlin 1.8.10 ...
継承を行うことなく新しい機能(関数やプロパティ)をクラスへ追加する「拡張機能」についてまとめます。 何処にでも手軽に定義できるため、とても重宝する仕組みですが、乱用するとプログラムが無法地帯になるので、管理は重要です。そのために、十分な理解が必要になります。 ちなみに、Android Jetpackは、この「拡張機能」を活用して作られた部分が多くあります。「拡張機能」無くして、成り立たないシステムです。 ※環境:Android Studio Hedgehog | 2023.1.1 Patch 1     Kotlin 1.8.10 ...
スコープ関数はKotlinの標準ライブラリで提供されています。 そのことから、Kotlinを特徴付ける重要な構文であることが分かります。 ただし、「プログラムの動作を定義する構文」ではなく、「プログラムのコードの品質を上げる構文」です。 同様な動作はスコープ関数を使わなくても記述できます。しかし、スコープ関数を使えばスマートな(洗練された)記述になります。 積極的に使いたいと思いますが、applyとlet以外はあまり使う機会がありません。また、稀にwithなどが登場すると、コードの流れが理解できずに戸惑います。 ですので、スコープ関数について、まとめました。 ※環境:Android Studio Hedgehog | 2023.1.1 Patch 2     Kotlin 1.9.0     Compose Compiler 1.5.1 ...
スポンサーリンク