継承を行うことなく新しい機能(関数やプロパティ)をクラスへ追加する「拡張機能」についてまとめます。
何処にでも手軽に定義できるため、とても重宝する仕組みですが、乱用するとプログラムが無法地帯になるので、管理は重要です。そのために、十分な理解が必要になります。
ちなみに、Android Jetpackは、この「拡張機能」を活用して作られた部分が多くあります。「拡張機能」無くして、成り立たないシステムです。
※環境:Android Studio Hedgehog | 2023.1.1 Patch 1
Kotlin 1.8.10
目次
拡張関数と拡張プロパティとは
Kotlinは既存のクラス(または、インターフェイス)に対して、継承を行うことなく新しい関数やプロパティを追加する仕組みを持っています。
これをKotlinの「拡張機能(Extensions)」といいます。
そして、前者を「拡張関数(Extension functions)」、後者を「拡張プロパティ(Extension properties)」と呼びます。
拡張機能は次の特徴を持ち、これらが利点になっています。
- どこでも定義可能
- 継承不可のクラスへ追加可能
拡張関数の作成
構文
Kotlinの一般的な関数の構文とほぼ同じです。しかし、次の点が異なります。
- 拡張関数名の前に対象クラス名が付く
- 対象クラスのインスタンス(this)が参照可能
- 対象クラスのプロパティが参照可能
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)が参照可能
- 対象クラスのプロパティが参照可能
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)が参照可能
- 対象クラスのプロパティが参照可能
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)が参照可能です。
プログラム実行時の処理において、Kotlinはリテラルをオブジェクトで扱います。例えば、「2」はInt型のオブジェクトになり、「5.0f」はFloat型のオブジェクトになります。
さらに、Kotlinはこの仕様を関数へ拡張しています。関数の記述は「関数リテラル」であり、処理においては「関数オブジェクト」として扱うことが可能です。
※詳細は「Kotlin:基本データ型はオブジェクト」「Kotlin:関数オブジェクト」を参照
関連記事: