Kotlinの拡張関数について

採用はこちら

Kotlinの拡張関数(Extension Function)とは、既存のクラスのソースコードを変更することなく、そのクラスに新しい関数を追加したように利用できる仕組みです。

Javaでは既存のクラスに新しい機能を追加したい場合、一般的に次のような方法が使われます。

  • クラスを継承してサブクラスを作る
  • ユーティリティクラスを作る(例:StringUtilsなど)

しかしKotlinでは、拡張関数を使うことで、既存クラスに直接メソッドが追加されたかのような自然な書き方が可能になります。

目次

拡張関数の基本構文

拡張関数は次の形式で定義します。

fun クラス名.関数名(): 戻り値型 {
    処理
}

例えば、String クラスに関数を追加する場合は次のように書きます。

fun String.addHello(): String {
    return "Hello $this"
}

この関数は次のように呼び出すことができます。

fun main() {
    val text = "Kotlin"
    println(text.addHello())
}

出力

Hello Kotlin

このように、String クラスに addHello() というメソッドが追加されたように利用できます。

拡張関数の中の this

拡張関数の内部では、this は拡張対象のオブジェクト(レシーバ)を指します。

例えば次の例を見てみましょう。

fun String.firstChar(): Char {
    return this[0]
}

使用例

println("Kotlin".firstChar())

出力

K

この場合、this"Kotlin" を指しています。

拡張関数の実際の仕組み

拡張関数は「クラスに本当にメソッドを追加している」わけではありません。

実際には、拡張関数はレシーバを第1引数として受け取る通常の関数のような形で扱われます

そのため、呼び出しは静的に解決されるという特徴があります。

例えば次のコードがあります。

fun String.addHello(): String {
    return "Hello $this"
}

呼び出し

text.addHello()

これは概念的には次のような形に近いイメージになります。

addHello(text)

つまり、オブジェクトの実行時型ではなく、変数の静的型によって呼ばれる拡張関数が決まるという点が重要です。

静的ディスパッチ

拡張関数は動的ディスパッチではなく静的ディスパッチです。

つまり、実行時のオブジェクト型ではなく、変数の型で呼び出される関数が決まります

例を見てみます。

open class Shape
class Rectangle : Shape()

fun Shape.getName() = "Shape"
fun Rectangle.getName() = "Rectangle"

fun printName(shape: Shape) {
    println(shape.getName())
}

fun main() {
    printName(Rectangle())
}

出力

Shape

Rectangle を渡しているにもかかわらず、Shape.getName() が呼ばれます。
これは、printName() の引数の型が Shape だからです。

メンバー関数との優先順位

拡張関数は、既存のメンバー関数をオーバーライドすることはできません

同じ名前の関数が存在する場合、クラス側のメンバー関数が常に優先されます

class Sample {
    fun hello() {
        println("member function")
    }
}

fun Sample.hello() {
    println("extension function")
}

fun main() {
    Sample().hello()
}

出力

member function

拡張関数はあくまで「補助的な関数」であり、クラスのメンバー関数を置き換えることはできません。

privateメンバーへのアクセス

拡張関数はクラスの外側で定義されるため、privateメンバーにはアクセスできません

class User(private val name: String)

fun User.printName() {
    println(name) // コンパイルエラー
}

拡張プロパティ

Kotlinでは、関数だけでなくプロパティの拡張も可能です。

val String.lastChar: Char
    get() = this[length - 1]

使用例

println("Kotlin".lastChar)

出力

n

ただし、拡張プロパティには制限があります。

拡張プロパティはバッキングフィールド(実際のデータを保持するフィールド)を持つことができません

そのため、定義できるのは getter / setter の処理のみです。

Nullable型への拡張

拡張関数は Nullable型 に対しても定義できます。

fun String?.isNullOrEmptySafe(): Boolean {
    return this == null || this.isEmpty()
}

使用例

val text: String? = null
println(text.isNullOrEmptySafe())

この場合、thisnull になる可能性があるため、null チェックが必要になります。

なお、Kotlin標準ライブラリにはすでに isNullOrEmpty() が存在します。

ジェネリック拡張関数

拡張関数はジェネリック型にも対応しています。

fun <T> List<T>.second(): T {
    require(size >= 2) { "List must contain at least 2 elements." }
    return this[1]
}

使用例

val list = listOf("A", "B", "C")
println(list.second())

出力

B

クラス内部での拡張関数

拡張関数はクラス内部でも定義できます。

class Printer {

    fun String.printHello() {
        println("Hello $this")
    }

    fun run() {
        "Kotlin".printHello()
    }
}

この場合、printHello()Printer クラスのスコープ内でのみ利用可能です。

Kotlin標準ライブラリと拡張関数

Kotlinの標準ライブラリは拡張関数を大量に使用しています。

代表例として次のような関数があります。

let
apply
also
run
map
filter
flatMap
takeIf
takeUnless

これらの多くは、コレクション操作やオブジェクト操作を簡潔に書くために利用されています。

なお、with は拡張関数ではなく、通常の関数です。

拡張関数の主な用途

拡張関数は次のような場面で特に役立ちます。

ユーティリティ関数の整理

Javaのように Utils クラスを大量に作る必要がなくなります。

コードの可読性向上

メソッドチェーン形式で処理を自然に記述できます。

DSL構築

Kotlinでは、拡張関数を利用して次のようなDSLを構築できます。

  • Kotlin HTML DSL
  • Gradle Kotlin DSL
  • Jetpack Compose

ただし、DSLは拡張関数だけで作られているわけではなく、次の機能と組み合わせて構築されます。

  • ラムダ with receiver
  • 高階関数
  • インライン関数
  • 型安全ビルダー

まとめ

Kotlinの拡張関数には次の特徴があります。

  • 既存クラスを変更せず機能を追加できる
  • クラスにメソッドが追加されたように記述できる
  • 実際にはクラスにメソッドが追加されるわけではない
  • 呼び出しは静的に解決される
  • メンバー関数が存在する場合はメンバー関数が優先される
  • privateメンバーにはアクセスできない
  • 拡張プロパティは定義できるがフィールドは持てない

Kotlinでは標準ライブラリ自体が拡張関数を多用しており、Kotlinらしいコードを書くうえで非常に重要な言語機能となっています。

以上、Kotlinの拡張関数についてでした。

最後までお読みいただき、ありがとうございました。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!
目次