覚えたら書く

IT関係のデベロッパとして日々覚えたことを書き残したいです。twitter: @yyoshikaw

Kotlin - 拡張関数・拡張プロパティ

前回のエントリ で、文字列の前後に prefixとpostfixの文字列を付与する decorate というしょうもない関数を定義してみました。

この関数の呼び出しは以下のようになるのですが

decorate({ベースの文字列}, {prefix}, {postfix})

Kotlinの 拡張関数(extention function)という機能により、Stringのメンバ関数の様にして扱う事ができます。 結果的に以下の様に呼び出せるようになります。

<ベースの文字列>.decorate(<prefix>, <postfix>)


もともと定義した decorate 関数は以下の様なものでした

fun decorate(src: String, prefix: String, postfix: String): String {
    return StringBuilder(prefix)
        .append(src)
        .append(postfix)
        .toString()
}

これを拡張関数として定義し直すと以下の様になります

fun String.decorate(prefix: String, postfix: String): String {
    return StringBuilder(prefix)
        .append(this)
        .append(postfix)
        .toString()
}


拡張関数の構文は、以下ようになります。

fun <レシーバ型>.関数名: <戻り値型> {
    // ここに処理を記述。 ここでの this は レシーバオブジェクトを意味する
}

追加する関数の名前の前に、クラスまたはインターフェースの名前を置くだけです。このクラス名を レシーバ型(receivcer type)と呼び、拡張関数を呼び出す値は、レシーバーオブジェクト(receiver object)と呼ばれます。


実際に 定義してみた拡張関数を呼び出してみると以下の様になります。(Stringのメンバの様に呼び出している事が分かります)

val decoratedStr = "Kotlin".decorate("<b>", "</b>")
println("Kotlin -> $decoratedStr")

実行結果は以下の通りです。

Kotlin -> <b>Kotlin</b>

この例では、 String がレーシバ型で、"Kotlin"という文字列がレシーバオブジェクト ということになります。


拡張関数を定義する時の制限事項の一つに、カプセル化を破れない。というのがあります。
拡張関数では、拡張しているクラスのメソッドやプロパティに、そのクラス自身に定義されているメソッドのように直接アクセス可能です。
しかし、拡張関数はクラスに定義されたメソッドとは異なり、クラスの private または protected なメンバにアクセスすることは許されません。

また、拡張関数を定義した時に、自動的にプロジェクト全体を横断して利用可能になるわけではありません。
他のクラスや関数と同様にインポートが必要になります。


メンバが優先される

Sample というクラスを定義してメンバに hello という関数を定義します。
その上で同名の hello という拡張関数を定義します。

package net.yyuki.sandbox.expand

class Sample {
    fun hello() {
        println("Hello World")
    }
}

fun Sample.hello() {
    println("Hello World Expand")
}

このSampleのhello関数を呼び出してみます

import net.yyuki.sandbox.expand.hello
import net.yyuki.sandbox.expand.Sample

fun main() {
    val sample = Sample()
    sample.hello()
}

実行結果は以下の通りです

Hello World

メンバの関数の実行が優先されている事が分かります


ただし、ここから私の理解が追いついていないだけなのですが、
String には文字列を大文字化する toUpperCase というメソッドが存在しています。

例えば、これと同名の toUpperCase という拡張関数を String に定義します。
文字列の先頭を取り出すという関数名とは全く無関係な処理内容にしています。

package net.yyuki.sandbox.expand

fun String.toUpperCase(): String = this.first().toString()

で、これを以下のように呼び出すと

import net.yyuki.sandbox.expand.toUpperCase

fun main() {
    println("Kotlin".toUpperCase())
}

実行結果は以下の通りです。(通常の String の toUpperCase が実行されていない)

K

元のメンバの関数が実行されずに、拡張関数が呼ばれます。
この動きはいまいち何でこうなのかは理解できていないです。


拡張プロパティ

関数の構文ではなく、プロパティの構文を使ってクラスを拡張する方法も提供されています。
ただし、いかなる状態も持つことはできません。状態を格納するための場所がないためです。
つまり、Javaオブジェクトの既存インスタンスに付加的なフィールドを追加するのは不可能ということになります。

文字列の末尾の文字を取り出す拡張プロパティ lastChar を定義すると以下の様になります

val String.lastChar: Char
    get() = get(lastIndex)

上記では読み取り専用のプロパティの例でしたが、ミュータブルな書き込み可能な拡張プロパティの定義も可能です

var StringBuilder.lastChar: Char
    get() = get(lastIndex)
    set(value) {
        this.setCharAt(lastIndex , value)
    }

拡張プロパティを呼び出すコードは以下の通りです

import net.yyuki.sandbox.expand.lastChar
import java.lang.StringBuilder

fun main() {
    val lastCh = StringBuilder()
        .append("Abcdefg")
        .lastChar

    println("Last char = $lastCh")


    val sb = StringBuilder()
        .append("Abcdefg")
    sb.lastChar = '@'

    println("StringBuilder = $sb")
}

実行結果は以下の通りです

Last char = g
StringBuilder = Abcdef@


Javaライブラリ拡張

Kotlinで利用されている コレクションは Javaのコレクションライブラリです。
それにも関わらず、以下の様なListの先頭要素の取得、最終要素の取得する関数や、Setの最大値を取得する関数 が用意されています

val list = listOf("1st", "2nd", "3rd", "4th")

val firstElement = list.first() // Listの先頭要素を取得
val lastElement = list.last()   // Listの最終要素を取得

println("$list -> first=$firstElement, last=$lastElement")    // [1st, 2nd, 3rd, 4th] -> first=1st, last=4th


val set = setOf(10, 30, -1, 100, 1000, 5)
val maxNumber = set.max()    // コレクションの最大値を取得

println("$set -> max=$maxNumber")    // [10, 30, -1, 100, 1000, 5] -> max=1000

これらのfirst, last, max といった関数は、まさに拡張関数として宣言されたものです。
これら以外にも多くの拡張関数がKotlinの標準ライブラリに宣言されています。


まとめ

Kotlin では、Javaと違ってクラスを継承したりしなくても、クラスを拡張する能力が提供されていることが分かりました。



関連エントリ

blog.y-yuki.net