Kotlin:拡張関数と拡張プロパティ

投稿日:  更新日:

継承を行うことなく新しい機能(関数やプロパティ)をクラスへ追加する「拡張機能」についてまとめます。

何処にでも手軽に定義できるため、とても重宝する仕組みですが、乱用するとプログラムが無法地帯になるので、管理は重要です。そのために、十分な理解が必要になります。

ちなみに、Android Jetpackは、この「拡張機能」を活用して作られた部分が多くあります。「拡張機能」無くして、成り立たないシステムです。

※環境:Android Studio Hedgehog | 2023.1.1 Patch 1
    Kotlin 1.8.10

スポンサーリンク

拡張関数と拡張プロパティとは

Kotlinは既存のクラス(または、インターフェイス)に対して、継承を行うことなく新しい関数やプロパティを追加する仕組みを持っています。

これをKotlinの「拡張機能(Extensions)」といいます。

そして、前者を「拡張関数(Extension functions)」、後者を「拡張プロパティ(Extension properties)」と呼びます。

拡張機能は次の特徴を持ち、これらが利点になっています。

  • どこでも定義可能
  • 継承不可のクラスへ追加可能
スポンサーリンク

拡張関数の作成

構文

Kotlinの一般的な関数の構文とほぼ同じです。しかし、次の点が異なります。

  • 拡張関数名の前に対象クラス名が付く
  • 対象クラスのインスタンス(this)が参照可能
  • 対象クラスのプロパティが参照可能
 構文1 
fun 対象クラス.拡張関数(引数: タイプ = デフォルト値, ...): 戻りタイプ {
    // 関数の処理
    // ・対象クラスのインスタンス(this)が参照可能
    // ・対象クラスのプロパティが参照可能
    return 戻り値
}

※thisを「レシーバーオブジェクト」と呼ぶ
※戻りタイプは明確な場合に限り省略可能
 構文2 
fun 対象のクラス.拡張関数(引数: タイプ = デフォルト値, ...): 戻りタイプ = 演算式

※戻りタイプは明確な場合に限り省略可能

サンプル

Stringクラスに拡張関数divideを追加するサンプルです。

devideは文字列へ等間隔に文字(ch)を挿入します。

fun String.divide(num: Int = 4, ch: Char = '_'): String {
    return if(num > 0) {    // 分割可能->chの挿入
        StringBuilder(this).apply {
            for(_offset in (this.length - num) downTo 1 step num)
                insert(_offset, ch)  // _offset:左端からの距離
        }.toString()
    }
    else                    // 分割不可->処理しない
        this
}

2進数の表示を4文字ずつ区切り、アンダーバー(’_’)を区切り文字として挿入します。2進数の4文字が16進数の1文字に当たるためです。

        val _value1 = 0xD2
        Log.i(TAG, "0x${_value1.toString(16)}は10進の${_value1}")
        Log.i(TAG, "0x${_value1.toString(16)}は 2進の${_value1.toString(2).divide()}")
...   I  0xd2は10進の210
...   I  0xd2は 2進の1101_0010
スポンサーリンク

拡張プロパティの作成(valタイプ)

valを用いた読み出し専用の拡張プロパティです。

構文

Kotlinの一般的なプロパティの構文とほぼ同じです。しかし、次の点が異なります。

  • プロパティ名の前に対象のクラス名が付く
  • 初期値は設定できない(※)
  • 対象クラスのインスタンス(this)が参照可能
  • 対象クラスのプロパティが参照可能
※プロパティ用の変数が確保されるわけでは無いため

 構文1 
val 対象のクラス.拡張プロパティ: タイプ
    get() {
        // プロパティの参照
        // ・対象クラスのインスタンス(this)が参照可能
        // ・対象クラスのプロパティが参照可能
        return プロパティ値
    }
 構文2 
val 対象のクラス.拡張プロパティ: タイプ 
    get() = プロパティ値の演算

先にも述べましたが、初期値(赤字)は指定できないため、以下の構文はエラーです。

val 対象のクラス.拡張プロパティ: タイプ = 初期値
    get() { ... }

サンプル

Stringクラスに拡張プロパティisJpeg/isPNG/isGifを追加するサンプルです。

isJpeg/isPNG/isGifは、格納された文字列の末尾が「画像の拡張子」に等しければtrue、等しくなければfalseを返します。

val String.isPNG: Boolean
    get() {
        return this.contains(Regex(".*\\.png$"))
    }
val String.isJpeg
    get() = this.contains(Regex(".*\\.jpg$"))
val String.isGif: Boolean
    get() = this.contains(Regex(".*\\.gif$"))

ファイル拡張子からファイル名が示す画像フォーマットを判別します。

        val _filename = "sample.jpg"
        // val _filename = "sample.png"
        // val _filename = "sample.gif"
        when {
            _filename.isJpeg -> Log.i(TAG, "File:${_filename} is Jpeg.")
            _filename.isPNG  -> Log.i(TAG, "File:${_filename} is PNG.")
            _filename.isGif  -> Log.i(TAG, "File:${_filename} is Gif.")
            else -> Log.i(TAG, "File:${_filename} is UnKnown.")
        }
... I  File:sample.jpg is Jpeg.
スポンサーリンク

拡張プロパティの作成(varタイプ)

varを用いた読み書き可能な拡張プロパティです。

構文

Kotlinの一般的なプロパティの構文とほぼ同じです。しかし、次の点が異なります。

  • プロパティ名の前に対象のクラス名が付く
  • 初期値は設定できない(※)
  • バッキングフィールドは無い(※)
  • 対象クラスのインスタンス(this)が参照可能
  • 対象クラスのプロパティが参照可能
※プロパティ用の変数が確保されるわけでは無いため

 構文1 
var 対象のクラス.拡張プロパティ: タイプ
    get() {
        // プロパティの参照
        // ・対象クラスのインスタンス(this)が参照可能
        // ・対象クラスのプロパティが参照可能
        return プロパティ値
    }
    set(value) {
        // プロパティの設定
        // ・対象クラスのインスタンス(this)が参照可能
        // ・対象クラスのプロパティが参照可能
        // ・バッキングフィールドは無い
    }
	
※thisを「レシーバーオブジェクト」と呼ぶ
 構文2 
var 対象のクラス.拡張プロパティ: タイプ 
    get() = プロパティ値の演算
    set(value) {
        // プロパティの設定
        // ・対象クラスのインスタンス(this)が参照可能
        // ・対象クラスのプロパティが参照可能
        // ・バッキングフィールドは無い
    }
	
※thisを「レシーバーオブジェクト」と呼ぶ

先にも述べましたが、初期値(赤字)は指定できないため、以下の構文はエラーです。

var 対象のクラス.拡張プロパティ: タイプ = 初期値
    get() { ... }
    set(value) { ... }

サンプル

StringBuilderクラスに拡張プロパティfirstLetterを追加するサンプルです。

firstLetterは格納された文字列の頭文字を表します。

var StringBuilder.firstLetter: Char
    get() {             // getter関数
        return get(0)
    }
    set(value) {        // setter関数
        set(0, value)
    }

英文の頭文字を小文字⇒大文字へ変換します。

        val _builder = StringBuilder("this is a pen.")
        Log.i(TAG, "Before Sentence = \"${_builder}\"")
        _builder.firstLetter = _builder.firstLetter.uppercaseChar()
        // _builder.set(0, _builder.first().uppercaseChar())  // 拡張プロパティなしで書くと...
        Log.i(TAG, "After  Sentence = \"${_builder}\"")
... I  Before Sentence = "this is a pen."
... I  After  Sentence = "This is a pen."
スポンサーリンク

定義の場所とスコープ

拡張機能の定義の場所は、トップレベル・クラス・関数の3箇所が考えられます。

package com.example.xxx.module_a

// トップレベル
fun String.extFunc_top_none(){ Log.i(TAG, "top-none") }
public fun String.extFunc_top_public(){ Log.i(TAG, "top-public") }
private fun String.extFunc_top_private(){ Log.i(TAG, "top-private") }
internal fun String.extFunc_top_internal(){ Log.i(TAG, "top-internal") }

class Sample() {

    // クラス
    fun String.extFunc_class_none(){ Log.i(TAG, "class-none") }
    public fun String.extFunc_class_public(){ Log.i(TAG, "class-public") }
    private fun String.extFunc_class_private(){ Log.i(TAG, "class-private") }
    internal fun String.extFunc_class_internal(){ Log.i(TAG, "class-internal") }
    protected fun String.extFunc_class_protected(){ Log.i(TAG, "class-protected") }

    fun localFunc() {
	    // 関数
        fun String.extFunc_func_none(){ Log.i(TAG, "function-none") }
        ...
    }

    ...
}

定義の場所とアクセス修飾子により、スコープ(有効な範囲)は異なります。

定義の場所アクセス修飾子スコープ条件
トップレベルなし(public)どこからでもパッケージが異なる場合
 ⇒ importの記述が必要
privateファイル内
internalモジュール内(※)
クラス(※)なし(public)
private
internal
protected
定義したクラス
関数定義した関数定義の場所より後方で有効
※モジュール内:一括でビルドが行われる範囲
※クラス:インターフェースなど抽象クラスを含む

この特徴を活かして、拡張機能の利用に制限をかけることが出来ます。

例えば、interfaceへ定義を行うと、定義した拡張機能はinterfaceを実装したクラス内の利用に限定されます。

スポンサーリンク

機能の継承

スーパークラスで定義した拡張機能は、サブクラスへ機能の継承が可能です。

継承の仕様は一般的なクラスの関数・プロパティと変わりません。ただし、super修飾子を付けてスーパークラスの拡張機能を参照することは出来ません。

継承の有無はアクセス修飾子に従います。

スーパークラス
Mod=A
Pkg=A
サブクラス
Mod=A
Pkg=A
サブクラス
Mod=A
Pkg=B
サブクラス
Mod=B
Pkg=A
サブクラス
Mod=B
Pkg=B
なし(public)
private××××
internal××
protected
※Mpd:モジュール名、Pkg:パッケージ名
(例:Mod=A,Mod=A⇒同一モジュール、Pkg=A、Pkg=B⇒異なるパッケージ)
※○:継承有、×:継承無
ackage com.example.xxx.module_a

open class SuperSample() {

    // 拡張関数の定義
    fun String.extFunc_none(){ Log.i(TAG, "class-none") }
    public fun String.extFunc_public(){ Log.i(TAG, "class-public") }
    private fun String.extFunc_private(){ Log.i(TAG, "class-private") }
    internal fun String.extFunc_internal(){ Log.i(TAG, "class-internal") }
    protected fun String.extFunc_protected(){ Log.i(TAG, "class-protected") }
	
	// メンバー関数の実装
    open fun sampleFunc() { ... }
}
package com.example.xxx.module_a

private val STR = "Hello World !!"

class SubSample : SuperSample() {

    private fun test() {
        STR.extFunc_none()			// 継承有
        STR.extFunc_public()		// 継承有
        // STR.extFunc_private() 	// 継承無⇒スーパークラス内でのみ有効
        STR.extFunc_internal()		// 継承有
        STR.extFunc_protected()		// 継承有
    }

    override fun sampleFunc() { ... }
}

また、オーバーライド(上書き)したければ、拡張機能の定義の前にopen修飾子が必要です。

open class SuperClass(open val str: String) {
    fun String.extFuncA(){ Log.i(TAG, "${str} (Super-FuncA)") }
    fun String.extFuncB(){ Log.i(TAG, "${str} (Super-FuncB)") }
    open fun String.extFuncC(){ Log.i(TAG, "${str} (Super-FuncC)") }
    open fun String.extFuncD(){ Log.i(TAG, "${str} (Super-fundD)") }

    open fun test() {
        str.extFuncA()
        str.extFuncB()
        str.extFuncC()
        str.extFuncD()
        normal()
    }

    open fun normal() { Log.i(TAG, "${str} (Super-Normal)") }
}
class SubClass(override val str: String) : SuperClass(str) {
    override fun String.extFuncC(){ Log.i(TAG, "${str} (Sub-FuncC)") }
    override fun String.extFuncD(){ Log.i(TAG, "${str} (Sub-fundD)") }

    override fun test() {
        str.extFuncA()          // サブクラスの関数
        str.extFuncB()
        str.extFuncC()
        str.extFuncD()
        normal()

        super.str.extFuncA()    // スーパークラスの関数
        super.str.extFuncB()
        super.str.extFuncC()
        super.str.extFuncD()
        super.normal()
    }

    override fun normal() { Log.i(TAG, "${str} (Sub-Normal)") }
}
        SuperClass("Hello World !!").test()
        SubClass("世界の皆さん、こんにちは !!").test()
... I  Hello World !! (Super-FuncA)
... I  Hello World !! (Super-FuncB)
... I  Hello World !! (Super-FuncC)
... I  Hello World !! (Super-fundD)
... I  Hello World !! (Super-Normal)

... I  世界の皆さん、こんにちは !! (Super-FuncA)	... サブクラスの関数
... I  世界の皆さん、こんにちは !! (Super-FuncB)
... I  世界の皆さん、こんにちは !! (Sub-FuncC)
... I  世界の皆さん、こんにちは !! (Sub-fundD)
... I  世界の皆さん、こんにちは !! (Sub-Normal)

... I  世界の皆さん、こんにちは !! (Super-FuncA)	... スーパークラスの関数
... I  世界の皆さん、こんにちは !! (Super-FuncB)
... I  世界の皆さん、こんにちは !! (Sub-FuncC)        ⇒ サブクラス側の実行
... I  世界の皆さん、こんにちは !! (Sub-fundD)        ⇒ サブクラス側の実行
... I  世界の皆さん、こんにちは !! (Super-Normal)
スポンサーリンク

定義の継承

スーパークラスへ定義した拡張機能は、サブクラスへ定義の継承が可能です。

また、サブクラスで同じ拡張機能を再定義すれば、定義の上書きができます。

open class SuperClass(open val str: String) {}
class SubClass(override val str: String) : SuperClass(str) {}

fun SuperClass.extFunc1() { Log.i(TAG, "${str} (Super-Func1)") }
fun SuperClass.extFunc2() { Log.i(TAG, "${str} (Super-Func2)") }
fun SuperClass.extFunc3() { Log.i(TAG, "${str} (Super-Func3)") }
fun SuperClass.extFunc4() { Log.i(TAG, "${str} (Super-Func4)") }

fun SubClass.extFunc3() { Log.i(TAG, "${str} (Sub-Func3)") }
fun SubClass.extFunc4() { Log.i(TAG, "${str} (Sub-Func4)") }
        SuperClass("Hello World !!").extFunc1()
        SuperClass("Hello World !!").extFunc2()
        SuperClass("Hello World !!").extFunc3()
        SuperClass("Hello World !!").extFunc4()
        SubClass("世界の皆さん、こんにちは !!").extFunc1()
        SubClass("世界の皆さん、こんにちは !!").extFunc2()
        SubClass("世界の皆さん、こんにちは !!").extFunc3()
        SubClass("世界の皆さん、こんにちは !!").extFunc4()
... I  Hello World !! (Super-Func1)
... I  Hello World !! (Super-Func2)
... I  Hello World !! (Super-Func3)
... I  Hello World !! (Super-Func4)

... I  世界の皆さん、こんにちは !! (Super-Func1)    // 継承有
... I  世界の皆さん、こんにちは !! (Super-Func2)    // 継承有
... I  世界の皆さん、こんにちは !! (Sub-Func3)      // 定義の上書き
... I  世界の皆さん、こんにちは !! (Sub-Func4)      // 定義の上書き
スポンサーリンク

定義の重複

一般的なクラスのメンバと同名の拡張機能は定義できます。

このように名前が重複した場合、一般的なクラスのメンバの実行が優先されます。

class Sample(val str: String) {
    fun func1() { Log.i(TAG, "${str} (Member func1)") }       // メンバーのみ
    fun func2() { Log.i(TAG, "${str} (Member func2)") }
}

fun Sample.func2() { Log.i(TAG, "${str} (Extension func2)") } // メンバーと拡張関数
fun Sample.func3() { Log.i(TAG, "${str} (Extension func3)") } // 拡張関数のみ
        Sample("Hello World !!").func1()
        Sample("Hello World !!").func2()
        Sample("Hello World !!").func3()
... I  Hello World !! (Member func1)
... I  Hello World !! (Member func2)
... I  Hello World !! (Extension func3)

ただし、オーバーロード(引数の違い)は区別され、重複になりません。

class Sample(val str: String) {
    fun func1() { Log.i(TAG, "${str} (Member func1)") }       // メンバーのみ
    fun func2() { Log.i(TAG, "${str} (Member func2)") }
}

fun Sample.func2(value: Int) {								  // メンバーと拡張関数
    Log.i(TAG, "${str} (Extension func2 #${value})") 
}
fun Sample.func3() { Log.i(TAG, "${str} (Extension func3)") } // 拡張関数のみ
        Sample("Hello World !!").func1()
        Sample("Hello World !!").func2(22)
        Sample("Hello World !!").func3()
... I  Hello World !! (Member func1)
... I  Hello World !! (Extension func2 #22)
... I  Hello World !! (Extension func3)
スポンサーリンク

関数オブジェクト

Kotlinの一般的な関数は第一級オブジェクト(First class object)です。※第一級オブジェクトについては「Kotlin:関数オブジェクト」を参照

同様に、拡張関数も第一級オブジェクトになります。ですので、生成・代入・演算・受け渡しといったプログラムの基本的な操作が可能です。

拡張関数を関数オブジェクトとして扱う場合のタイプは、次のように定義されます。

対象のクラス.(引数タイプ, ...)->戻りタイプ

以下は、拡張関数の関数オブジェクトを作成して、変数へ代入する例です。

/*
** 整数値を偶数へ変換
**   up: true  ... 一つ大きい偶数へ
**       false ... 一つ小さい偶数へ
*/
fun Int.toEven1(up: Boolean = true): Int {
    return this + if(up) this%2 else -this%2
}

// 「::関数名」で関数オブジェクト
val toEven2: Int.(Boolean)->Int = Int::toEven1

// 無名関数で生成
val toEven3: Int.(Boolean)->Int =
    fun Int.(up: Boolean): Int {
        return this + if(up) this%2 else -this%2
    }
val toEven4: Int.(Boolean)->Int =
    fun Int.(up: Boolean): Int =
        this + if(up) this%2 else -this%2
		
// ラムダ式で生成
val toEven5: Int.(Boolean)->Int = {
    up: Boolean ->this + if(up) this%2 else -this%2
}
val toEven6: Int.(Boolean)->Int = {
    this + if(it) this%2 else -this%2
}

ちなみに、右辺は関数リテラルです。左辺の変数が拡張関数のタイプであるため、関数リテラル内でレシーバーオブジェクト(this)が参照可能です。

サンプルの実行例
    Log.i(TAG, "Int.toEven1 123 = ${123.toEven1()}")
    Log.i(TAG, "Int.toEven1 312 = ${312.toEven1()}")
    Log.i(TAG, "Int.toEven1 231 = ${231.toEven1(false)}")

    Log.i(TAG, "Int.toEven2 123 = ${123.toEven2(true)}")
    Log.i(TAG, "Int.toEven2 312 = ${312.toEven2(true)}")
    Log.i(TAG, "Int.toEven2 231 = ${231.toEven2(false)}")

    Log.i(TAG, "Int.toEven3 123 = ${123.toEven3(true)}")
    Log.i(TAG, "Int.toEven3 312 = ${312.toEven3(true)}")
    Log.i(TAG, "Int.toEven3 231 = ${231.toEven3(false)}")

    Log.i(TAG, "Int.toEven4 123 = ${123.toEven4(true)}")
    Log.i(TAG, "Int.toEven4 312 = ${312.toEven4(true)}")
    Log.i(TAG, "Int.toEven4 231 = ${231.toEven4(false)}")

    Log.i(TAG, "Int.toEven5 123 = ${123.toEven5(true)}")
    Log.i(TAG, "Int.toEven5 312 = ${312.toEven5(true)}")
    Log.i(TAG, "Int.toEven5 231 = ${231.toEven5(false)}")

    Log.i(TAG, "Int.toEven6 123 = ${123.toEven6(true)}")
    Log.i(TAG, "Int.toEven6 312 = ${312.toEven6(true)}")
    Log.i(TAG, "Int.toEven6 231 = ${231.toEven6(false)}")
 ... I  Int.toEven1 123 = 124	1つ大きい偶数へ
 ... I  Int.toEven1 312 = 312	何もしない(元から偶数)
 ... I  Int.toEven1 231 = 230	1つ小さい偶数へ
 
 ... I  Int.toEven2 123 = 124
 ... I  Int.toEven2 312 = 312
 ... I  Int.toEven2 231 = 230
 
 ... I  Int.toEven3 123 = 124
 ... I  Int.toEven3 312 = 312
 ... I  Int.toEven3 231 = 230
 
 ... I  Int.toEven4 123 = 124
 ... I  Int.toEven4 312 = 312
 ... I  Int.toEven4 231 = 230
 
 ... I  Int.toEven5 123 = 124
 ... I  Int.toEven5 312 = 312
 ... I  Int.toEven5 231 = 230
 
 ... I  Int.toEven6 123 = 124
 ... I  Int.toEven6 312 = 312
 ... I  Int.toEven6 231 = 230
〇〇リテラルと〇〇オブジェクト
「リテラル」とは、ソースコードへ直に記述した定数(数値や文字列)のことです。

数値(文字)リテラル

プログラム実行時の処理において、Kotlinはリテラルをオブジェクトで扱います。例えば、「2」はInt型のオブジェクトになり、「5.0f」はFloat型のオブジェクトになります。

さらに、Kotlinはこの仕様を関数へ拡張しています。関数の記述は「関数リテラル」であり、処理においては「関数オブジェクト」として扱うことが可能です。

関数リテラル

※詳細は「Kotlin:基本データ型はオブジェクト」「Kotlin:関数オブジェクト」を参照

スポンサーリンク

関連記事:

KDocのドキュメント記述例を紹介します。 KDocを使えばKotlinのソースコードに書いたコメントからドキュメントを作成することが出来ます。後々、クラスの再利用を考えているなら、ドキュメントを残して再利用性を高めておきましょう! ...
Kotlinは関数を変数に代入したり、引数で受け渡したりできます。 関数を第一級オブジェクトで扱えるためです。 これにより、関数を使った処理の委譲が容易になりました。ArrayOf#forEach( )がその典型的な例です。 以上のような操作で必要となる「関数オブジェクト」について、まとめます。 ...
クロージャ(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 ...
スコープ関数はKotlinの標準ライブラリで提供されています。 そのことから、Kotlinを特徴付ける重要な構文であることが分かります。 ただし、「プログラムの動作を定義する構文」ではなく、「プログラムのコードの品質を上げる構文」です。 同様な動作はスコープ関数を使わなくても記述できます。しかし、スコープ関数を使えばスマートな(洗練された)記述になります。 積極的に使いたいと思いますが、applyとlet以外はあまり使う機会がありません。また、稀にwithなどが登場すると、コードの流れが理解できずに戸惑います。 ですので、スコープ関数について、まとめました。 ※環境:Android Studio Hedgehog | 2023.1.1 Patch 2     Kotlin 1.9.0     Compose Compiler 1.5.1 ...
スポンサーリンク