Skip to content

拡張

Kotlinの_拡張_は、継承や_Decorator_のようなデザインパターンを使用することなく、クラスやインターフェースに新しい機能を追加できます。これらは、直接変更できないサードパーティ製ライブラリを扱う際に役立ちます。一度作成すれば、これらの拡張は、あたかも元のクラスやインターフェースのメンバーであるかのように呼び出すことができます。

拡張の最も一般的な形式は、拡張関数拡張プロパティです。

重要なこととして、拡張は拡張するクラスやインターフェースを変更しません。拡張を定義しても、新しいメンバーを追加するわけではありません。同じ構文を使って、新しい関数を呼び出したり、新しいプロパティにアクセスできるようにするだけです。

レシーバー

拡張は常にレシーバーで呼び出されます。レシーバーは、拡張されるクラスまたはインターフェースと同じ型を持つ必要があります。拡張を使用するには、レシーバーの後に .と関数またはプロパティ名を付けてプレフィックスとして付けます。

例えば、標準ライブラリのappendLine()拡張関数はStringBuilderクラスを拡張します。したがって、この場合、レシーバーはStringBuilderインスタンスであり、_レシーバー型_はStringBuilderです。

kotlin
fun main() { 
    // builder is an instance of StringBuilder
    val builder = StringBuilder()
        // Calls .appendLine() extension function on builder
        .appendLine("Hello")
        .appendLine()
        .appendLine("World")
    println(builder.toString())
    // Hello
    //
    // World
}

拡張関数

独自の拡張関数を作成する前に、Kotlinの標準ライブラリに既に探しているものがあるかどうかを確認してください。 標準ライブラリは、以下のような多くの便利な拡張関数を提供します。

独自の拡張関数を作成するには、その名前にレシーバー型と .をプレフィックスとして付けます。この例では、.truncate()関数はStringクラスを拡張するため、レシーバー型はStringです。

kotlin
fun String.truncate(maxLength: Int): String {
    return if (this.length <= maxLength) this else take(maxLength - 3) + "..."
}

fun main() {
    val shortUsername = "KotlinFan42"
    val longUsername = "JetBrainsLoverForever"

    println("Short username: ${shortUsername.truncate(15)}") 
    // KotlinFan42
    println("Long username:  ${longUsername.truncate(15)}")
    // JetBrainsLov...
}

.truncate()関数は、呼び出された文字列を指定されたmaxLengthで切り詰め、省略記号...を追加します。文字列がmaxLengthより短い場合、関数は元の文字列を返します。

この例では、.displayInfo()関数はUserインターフェースを拡張します。

kotlin
interface User {
    val name: String
    val email: String
}

fun User.displayInfo(): String = "User(name=$name, email=$email)"

// Inherits from and implements the properties of the User interface
class RegularUser(override val name: String, override val email: String) : User

fun main() {
    val user = RegularUser("Alice", "[email protected]")
    println(user.displayInfo()) 
    // User(name=Alice, [email protected])
}

.displayInfo()関数は、RegularUserインスタンスのnameemailを含む文字列を返します。このようにインターフェースに拡張を定義することは、インターフェースを実装するすべての型に一度だけ機能を追加したい場合に便利です。

この例では、.mostVoted()関数はMap<String, Int>クラスを拡張します。

kotlin
fun Map<String, Int>.mostVoted(): String? {
    return maxByOrNull { (key, value) -> value }?.key
}

fun main() {
    val poll = mapOf(
        "Cats" to 37,
        "Dogs" to 58,
        "Birds" to 22
    )

    println("Top choice: ${poll.mostVoted()}") 
    // Dogs
}

.mostVoted()関数は、呼び出されたマップのキーと値のペアを反復処理し、maxByOrNull()関数を使用して最大値を含むペアのキーを返します。マップが空の場合、maxByOrNull()関数はnullを返します。mostVoted()関数は、maxByOrNull()関数が非null値を返す場合にのみkeyプロパティにアクセスするために、セーフコール?.を使用します。

ジェネリックな拡張関数

ジェネリックな拡張関数を作成するには、関数名の前にジェネリック型パラメータを宣言して、レシーバー型式で利用できるようにします。この例では、.endpoints()関数はList<T>を拡張しており、Tは任意の型です。

kotlin
fun <T> List<T>.endpoints(): Pair<T, T> {
    return first() to last()
}

fun main() {
    val cities = listOf("Paris", "London", "Berlin", "Prague")
    val temperatures = listOf(21.0, 19.5, 22.3)

    val cityEndpoints = cities.endpoints()
    val tempEndpoints = temperatures.endpoints()

    println("First and last cities: $cityEndpoints")
    // (Paris, Prague)
    println("First and last temperatures: $tempEndpoints") 
    // (21.0, 22.3)
}

.endpoints()関数は、呼び出されたリストの最初と最後の要素を含むペアを返します。関数本体内では、first()関数とlast()関数を呼び出し、to中置関数を使用して返された値をPairに結合します。

ジェネリクスに関する詳細については、ジェネリック関数を参照してください。

Null許容レシーバー

null許容レシーバー型で拡張関数を定義できます。これにより、その値がnullであっても変数に対して呼び出すことができます。レシーバーがnullの場合、thisnullになります。関数内でnull許容性を正しく処理するようにしてください。例えば、関数本体内でthis == nullチェック、セーフコール?.、またはエルビス演算子?:を使用します。

この例では、toString()関数をnullチェックなしで呼び出すことができます。なぜなら、そのチェックは既に拡張関数内で実行されているからです。

kotlin
fun main() {
    // Extension function on nullable Any
    fun Any?.toString(): String {
        if (this == null) return "null"
        // After null check, `this` is smart-cast to non-nullable Any
        // So this call resolves to the regular toString() function
        return toString()
    }
    
    val number: Int? = 42
    val nothing: Any? = null
    
    println(number.toString())
    // 42
    println(nothing.toString()) 
    // null
}

拡張関数とメンバー関数

拡張関数とメンバー関数の呼び出しは同じ表記であるため、コンパイラはどちらを使用すべきかをどのように判断するのでしょうか? 拡張関数は_静的に_ディスパッチされます。つまり、コンパイラはコンパイル時にレシーバー型に基づいてどの関数を呼び出すかを決定します。例えば:

kotlin
fun main() {
    open class Shape
    class Rectangle: Shape()
    
    fun Shape.getName() = "Shape"
    fun Rectangle.getName() = "Rectangle"
    
    fun printClassName(shape: Shape) {
        println(shape.getName())
    }
    
    printClassName(Rectangle())
    // Shape
}

この例では、パラメータshapeShape型として宣言されているため、コンパイラはShape.getName()拡張関数を呼び出します。拡張関数は静的に解決されるため、コンパイラは実際のインスタンスではなく、宣言された型に基づいて関数を選択します。

したがって、例ではRectangleインスタンスを渡していますが、変数がShape型として宣言されているため、.getName()関数はShape.getName()に解決されます。

あるクラスがメンバー関数を持ち、かつ、同じレシーバー型、同じ名前で、互換性のある引数を持つ拡張関数が定義されている場合、メンバー関数が優先されます。例えば:

kotlin
fun main() {
    class Example {
        fun printFunctionType() { println("Member function") }
    }
    
    fun Example.printFunctionType() { println("Extension function") }
    
    Example().printFunctionType()
    // Member function
}

ただし、拡張関数は、同じ名前でも_異なる_シグネチャを持つメンバー関数をオーバーロードできます。

kotlin
fun main() {
    class Example {
        fun printFunctionType() { println("Member function") }
    }
    
    // Same name but different signature
    fun Example.printFunctionType(index: Int) { println("Extension function #$index") }
    
    Example().printFunctionType(1)
    // Extension function #1
}

この例では、.printFunctionType()関数にIntが渡されるため、コンパイラはシグネチャに一致する拡張関数を選択します。コンパイラは引数を取らないメンバー関数を無視します。

匿名拡張関数

拡張関数に名前を付けずに定義できます。これは、グローバルな名前空間を汚染したくない場合や、拡張動作をパラメータとして渡す必要がある場合に役立ちます。

例えば、名前を付けずに、データクラスを1回限りの関数で拡張して送料を計算したいとします。

kotlin
fun main() {
    data class Order(val weight: Double)
    val calculateShipping = fun Order.(rate: Double): Double = this.weight * rate
    
    val order = Order(2.5)
    val cost = order.calculateShipping(3.0)
    println("Shipping cost: $cost") 
    // Shipping cost: 7.5
}

拡張動作をパラメータとして渡すには、型アノテーション付きのラムダ式を使用します。例えば、名前付き関数を定義せずに、数値が範囲内にあるかどうかをチェックしたいとします。

kotlin
fun main() {
    val isInRange: Int.(min: Int, max: Int) -> Boolean = { min, max -> this in min..max }

    println(5.isInRange(1, 10))
    // true
    println(20.isInRange(1, 10))
    // false
}

この例では、isInRange変数はInt.(min: Int, max: Int) -> Boolean型の関数を保持しています。この型は、minmaxパラメータを取りBooleanを返すIntクラスの拡張関数です。

ラムダ本体{ min, max -> this in min..max }は、関数が呼び出されるInt値がminmaxパラメータ間の範囲内にあるかどうかをチェックします。チェックが成功した場合、ラムダはtrueを返します。

詳細については、ラムダ式と匿名関数を参照してください。

拡張プロパティ

Kotlinは拡張プロパティをサポートしており、これは、作業しているクラスを汚染することなく、データ変換を実行したり、UI表示ヘルパーを作成したりするのに役立ちます。

拡張プロパティを作成するには、拡張したいクラスの名前の後に .とプロパティ名を記述します。

例えば、名と姓を持つユーザーを表すデータクラスがあり、アクセス時にメール形式のユーザー名を返すプロパティを作成したいとします。コードは次のようになります。

kotlin
data class User(val firstName: String, val lastName: String)

// An extension property to get a username-style email handle
val User.emailUsername: String
    get() = "${firstName.lowercase()}.${lastName.lowercase()}"

fun main() {
    val user = User("Mickey", "Mouse")
    // Calls extension property
    println("Generated email username: ${user.emailUsername}")
    // Generated email username: mickey.mouse
}

拡張は実際にクラスにメンバーを追加しないため、拡張プロパティがバッキングフィールドを持つ効率的な方法はありません。そのため、拡張プロパティには初期化子は許可されていません。その動作は、明示的にゲッターとセッターを提供することによってのみ定義できます。例えば:

kotlin
data class House(val streetName: String)

// Doesn't compile because there is no getter and setter
// var House.number = 1
// Error: Initializers are not allowed for extension properties

// Compiles successfully
val houseNumbers = mutableMapOf<House, Int>()
var House.number: Int
    get() = houseNumbers[this] ?: 1
    set(value) {
        println("Setting house number for ${this.streetName} to $value")
        houseNumbers[this] = value
    }

fun main() {
    val house = House("Maple Street")

    // Shows the default
    println("Default number: ${house.number} ${house.streetName}") 
    // Default number: 1 Maple Street
    
    house.number = 99
    // Setting house number for Maple Street to 99

    // Shows the updated number
    println("Updated number: ${house.number} ${house.streetName}") 
    // Updated number: 99 Maple Street
}

この例では、ゲッターはエルビス演算子を使用して、houseNumbersマップに家の番号が存在する場合はその番号を返し、そうでない場合は1を返します。ゲッターとセッターの書き方について詳しく知るには、カスタムゲッターとセッターを参照してください。

コンパニオンオブジェクト拡張

クラスがコンパニオンオブジェクトを定義している場合、そのコンパニオンオブジェクトに対して拡張関数やプロパティを定義することもできます。コンパニオンオブジェクトの通常のメンバーと同様に、クラス名を修飾子として使用するだけで呼び出すことができます。コンパイラはデフォルトでコンパニオンオブジェクトをCompanionと名付けます。

kotlin
class Logger {
    companion object { }
}

fun Logger.Companion.logStartupMessage() {
    println("Application started.")
}

fun main() {
    Logger.logStartupMessage()
    // Application started.
}

メンバーとしての拡張の宣言

あるクラスの内部で、別のクラスの拡張を宣言することができます。このような拡張には複数の_暗黙のレシーバー_があります。暗黙のレシーバーとは、thisで修飾することなくメンバーにアクセスできるオブジェクトのことです。

  • 拡張を宣言するクラスが_ディスパッチレシーバー_です。
  • 拡張関数のレシーバー型が_拡張レシーバー_です。

ConnectionクラスにHostクラスの拡張関数printConnectionString()がある次の例を考えてみましょう。

kotlin
class Host(val hostname: String) {
    fun printHostname() { print(hostname) }
}

class Connection(val host: Host, val port: Int) {
    fun printPort() { print(port) }

    // Host is the extension receiver
    fun Host.printConnectionString() {
        // Calls Host.printHostname()
        printHostname() 
        print(":")
        // Calls Connection.printPort()
        // Connection is the dispatch receiver
        printPort()
    }

    fun connect() {
        /*...*/
        // Calls the extension function
        host.printConnectionString() 
    }
}

fun main() {
    Connection(Host("kotl.in"), 443).connect()
    // kotl.in:443
    
    // Triggers an error because the extension function isn't available outside Connection
    // Host("kotl.in").printConnectionString()
    // Unresolved reference 'printConnectionString'.
}

この例では、printConnectionString()関数をConnectionクラスの内部で宣言しているため、Connectionクラスがディスパッチレシーバーです。拡張関数のレシーバー型はHostクラスであるため、Hostクラスが拡張レシーバーです。

ディスパッチレシーバーと拡張レシーバーに同じ名前のメンバーがある場合、拡張レシーバーのメンバーが優先されます。ディスパッチレシーバーに明示的にアクセスするには、修飾this構文を使用します。

kotlin
class Connection {
    fun Host.getConnectionString() {
        // Calls Host.toString()
        toString()
        // Calls Connection.toString()
        this@Connection.toString()
    }
}

メンバー拡張のオーバーライド

メンバー拡張をopenとして宣言し、サブクラスでオーバーライドできます。これは、各サブクラスで拡張の動作をカスタマイズしたい場合に役立ちます。コンパイラは各レシーバー型を異なる方法で処理します。

レシーバー型解決時間ディスパッチ型
ディスパッチレシーバー実行時仮想
拡張レシーバーコンパイル時静的

Userクラスがopenで、Adminクラスがそれを継承している次の例を考えてみましょう。NotificationSenderクラスはUserAdminクラスの両方にsendNotification()拡張関数を定義し、SpecialNotificationSenderクラスはそれらをオーバーライドします。

kotlin
open class User

class Admin : User()

open class NotificationSender {
    open fun User.sendNotification() {
        println("Sending user notification from normal sender")
    }

    open fun Admin.sendNotification() {
        println("Sending admin notification from normal sender")
    }

    fun notify(user: User) {
        user.sendNotification()
    }
}

class SpecialNotificationSender : NotificationSender() {
    override fun User.sendNotification() {
        println("Sending user notification from special sender")
    }

    override fun Admin.sendNotification() {
        println("Sending admin notification from special sender")
    }
}

fun main() {
    // Dispatch receiver is NotificationSender
    // Extension receiver is User
    // Resolves to User.sendNotification() in NotificationSender
    NotificationSender().notify(User())
    // Sending user notification from normal sender
    
    // Dispatch receiver is SpecialNotificationSender
    // Extension receiver is User
    // Resolves to User.sendNotification() in SpecialNotificationSender
    SpecialNotificationSender().notify(User())
    // Sending user notification from special sender 
    
    // Dispatch receiver is SpecialNotificationSender
    // Extension receiver is User NOT Admin
    // The notify() function declares user as type User
    // Statically resolves to User.sendNotification() in SpecialNotificationSender
    SpecialNotificationSender().notify(Admin())
    // Sending user notification from special sender 
}

ディスパッチレシーバーは仮想ディスパッチを使用して実行時に解決されるため、main()関数の動作を理解しやすくなります。驚くかもしれませんが、Adminインスタンスでnotify()関数を呼び出す際、コンパイラは宣言された型であるuser: Userに基づいて拡張を選択します。これは、拡張レシーバーを静的に解決するためです。

拡張と可視性修飾子

拡張は、他のクラスのメンバーとして宣言された拡張を含め、同じスコープで宣言された通常の関数と同じ可視性修飾子を使用します。

例えば、ファイルのトップレベルで宣言された拡張は、同じファイル内の他のprivateトップレベル宣言にアクセスできます。

kotlin
// File: StringUtils.kt

private fun removeWhitespace(input: String): String {
    return input.replace("\\s".toRegex(), "")
}

fun String.cleaned(): String {
    return removeWhitespace(this)
}

fun main() {
    val rawEmail = "  user @example. com  "
    val cleaned = rawEmail.cleaned()
    println("Raw:     '$rawEmail'")
    // Raw:     '  user @example. com  '
    println("Cleaned: '$cleaned'")
    // Cleaned: '[email protected]'
    println("Looks like an email: ${cleaned.contains("@") && cleaned.contains(".")}") 
    // Looks like an email: true
}

また、拡張がそのレシーバー型の外部で宣言されている場合、レシーバーのprivateまたはprotectedメンバーにアクセスできません。

kotlin
class User(private val password: String) {
    fun isLoggedIn(): Boolean = true
    fun passwordLength(): Int = password.length
}

// Extension declared outside the class
fun User.isSecure(): Boolean {
    // Can't access password because it's private:
    // return password.length >= 8

    // Instead, we rely on public members:
    return passwordLength() >= 8 && isLoggedIn()
}

fun main() {
    val user = User("supersecret")
    println("Is user secure: ${user.isSecure()}") 
    // Is user secure: true
}

拡張がinternalとマークされている場合、そのモジュール内でのみアクセス可能です。

kotlin
// Networking module
// JsonParser.kt
internal fun String.parseJson(): Map<String, Any> {
    return mapOf("fakeKey" to "fakeValue")
}

拡張のスコープ

ほとんどの場合、拡張はパッケージ直下のトップレベルで定義します。

kotlin
package org.example.declarations

fun List<String>.getLongestString() { /*...*/}

宣言パッケージの外で拡張を使用するには、呼び出し側でインポートします。

kotlin
package org.example.usage

import org.example.declarations.getLongestString

fun main() {
    val list = listOf("red", "green", "blue")
    list.getLongestString()
}

詳細については、インポートを参照してください。