あんなこといいな♪
できたらいいな♪
あんな型こんな型いっぱいあるけど~♪
「はい、拡張関数!」
拡張関数の定義と呼び出し方法
拡張(Extension)とは、既存の型に関数やプロパティを追加する機能です。
細かいことは抜きにして構文をチェックしておきましょう。例としてList<String>から呼び出し、MutableListを返す拡張関数を定義します。
fun List<String>.select(n: Int): MutableList<String>{
return this.filter { it.length == n }.toMutableList()
}
新たに定義したselect
は内部でfilter
メソッドを使い、引数に取ったn文字の要素のみを、新たなMutableListに入れて返す関数です。この関数定義構文が通常の関数と異なる点は、
- 呼び出し元(レシーバ)の型を指定する
- 内部ではレシーバを「this」で参照する
これだけです。それでは実際に使用してみましょう。
fun main() {
val list = listOf("野比のび太","しずか","スネ夫","ジャイアン")
//5文字の要素だけを抜き出す
println(list.select(5))
}
//[野比のび太, ジャイアン]
Listから5文字の要素のみを抜き出したMutableListができあがりました。
ラムダ式で同じことをやろうとすると、その都度処理を記述する必要があります。使い捨ての処理ならそれがメリットにもなりますが、拡張関数は何度か使用する処理を、継承無しで利用できるのが大きな特徴です。
拡張関数の良いところ
継承不要であるということは、単純に「新たなクラスを宣言する必要が無い」こと以外にも良いところがあります。それが、StringやIntなどの基本的な型にも機能を追加できるという点です。
これらの型はopenなクラスではないため、継承することができません。APIリファレンスを確認してみましょう。
//「open」が付いていないclassは、それ自体がfinalなクラスである
class String : Comparable<String>, CharSequence
class Int : Number, Comparable<Int>
class Double : Number, Comparable<Double>
そのため例えばString型としての機能はそのままに独自のメソッドを追加したい場合、拡張関数を使うのが最も手っ取り早い手段となります。例を挙げてみるとこんな感じ。
//shiftVowel(母音をズラす)拡張関数
fun String.shiftVowel(): String{
val vowel = listOf('あ','い','う','え','お')
var result = ""
for(i in this) {
if (vowel.contains(i)) {
result += (i - 1) //その文字が母音なら-1する
} else result += i
}
return result
}
fun main() {
val str = "おかねのいらない世界"
println(str.shiftVowel())
}
//ぉかねのぃらなぃ世界
ひらがなの母音を「-1」するメソッドなどString型には元々存在しません。しかし拡張関数を使うことで、お手軽に新たな機能をString型に付け加え、しかもそれをいつでも利用することができます。
拡張関数は既存の型に何らかの変更を加えるというものではなく、まるで関数というタグを付けるように、ただ外から機能を付け加えるだけのものというイメージを持っておくと、この後の説明が分かりやすいかもしれません。
拡張関数の制限
ただ外から機能を付け加えるだけの拡張関数は実装が簡単で、様々な型に応用できます。しかしその反面、当然ですがいくつかの制限があります。
プライベートなメンバにアクセスできない
あくまでも拡張関数は「外」であるということを忘れないようにしてください。クラス内部からしか参照できないprivateメンバにはアクセスできません。
同じ処理ならメンバが優先される
同じ名前、同じパラメータ、同じ戻り値の型を持つメソッドが既にその型に実装されている場合、拡張関数を定義することは可能ですが、呼び出されるのは常に元々存在するメソッドです。
//常に文字列の最初の文字を返す拡張関数
fun String.get(n: Int): Char = this[0]
fun main() {
val str = "タケコプター"
println(str.get(2))
}
//コ (元々のgetメソッドが呼び出される)
しかし同じパッケージに属する場合、拡張関数は拡張関数でオーバーライドすることができます。
//countはString型の拡張関数として実装されているメソッド
fun String.count(): Int = 50
fun main() {
val str = "タケコプター"
println(str.count())
}
//50 (拡張関数は拡張関数で上書きできる)
そのメソッドが拡張関数かどうかは、APIリファレンスに掲載されています。Extension機能を使う際には名前が被っていないか、上書きしたければそのメソッドが拡張関数かどうかを確認しておきましょう。
拡張プロパティを加える
Etensionで定義できるのは、なにも関数だけではありません。
val String.size: Int
get() = this.length
fun main() {
val str = "恐怖のジャイアンディナーショー"
println(str.size)
}
//15
本来はlength
プロパティで参照する文字数を、新たに定義したsize
プロパティでも参照できるようになりました。これが拡張プロパティです。
拡張プロパティの制限
拡張プロパティも関数と同様、あくまでクラスに付け加えられるだけの値です。そのため全ての拡張プロパティは算出プロパティであり、インスタンスのバッキングフィールドを参照できないことには注意してください。
つまり下のような「=」を使った代入は不可です。ゲッターは「field」の値を参照することができません。
val String.size: Int = this.length
簡単に言えば「プロパティとしての値を保存しておく場所が無い」ということ。バッキングフィールドについてはこちらをご覧ください。
ジェネリック型を扱う拡張関数
今回の締めとしてジェネリック型を扱う拡張関数を定義します。
//echo関数はTから呼び出し、Tをそのまま返す関数
fun <T> T.echo(): T{
println(this)
return this
}
LinuxやPHPでお馴染みのecho
ですが、Kotlinには存在しません。少し寂しい気もするので、ここで登場してもらいましょう。
この関数は呼び出し元の型としてジェネリック型「T」を指定し、どんな型からも同じように呼び出せる関数となっています。
実際の処理は標準出力するだけの単純な関数ですが、println
とは違い、戻り値を「this」(レシーバをそのまま返す)とすることで、メソッドチェーンに対応した標準出力用の関数として利用できます。
fun main() {
val list = mutableListOf(1,2,3)
list.apply{ add(4) }.echo().apply{ add(5) }.echo()
}
//[1, 2, 3, 4]
//[1, 2, 3, 4, 5]
参考文献
Extensionに関する公式ドキュメント。
上記ページは日本語ドキュメントもあります。
Kotlin入門までの助走読本の著者の1人でもある山本純平さんのスライド。Extensionについて分かりやすく解説されているのでオススメ。