今回はKotlinシリーズの3回目、Extensionについて説明します。
前回のData Class同様、Javaの「なんとなく面倒だな…」と思うところを補填してくれる機能となります。
KotlinのExtensionsのページを見ると、KotlinのデザインチームがなぜExtensionを作ったか書いてあります。Javaでコード書いてる人なら頷ける内容なのではないかと思いますので一読を。
・Extensionとは
iOSアプリをObjective-Cで作ってる人であれば、Objective-Cのカテゴリ機能はご存知かと思います。クラスを継承するほどでもないけど、ちょっとしたメソッドを既存クラスに追加したいときに便利な機能ですよね。
Extension機能も狙いはそこで「既存クラスを継承したクラスを作るほどでもないが、ちょっとしたメソッドやプロパティを追加したい。」ということを実現できる機能です。
今までJavaでそのようなことをするためにはUtilクラスを作ったり、使ったりすることが多かったと思います。
例えば、Javaで用意されているCollectionsクラスがそのようなクラスの代表になります。
Collectionsクラスに用意されているswapメソッドを使って配列の要素を入れ替えを行うとき、以下のようなコードになります。
Collections.swap(list, 1, 3);
もはや見慣れた書き方ではありますが、ちょっとイマイチですよね。
以下のコードのような感じであれば、わかりやすいと思います。
list.swap(1, 3)
KotlinではExtension機能を使うことで、Javaではできない「既存のクラスにちょい足し」を簡単に使うことができます。
先程の例であげたswapメソッドをInt型のMutableListに追加してみます。
//Listのなかを操作するのでMutableListにExtensionする fun MutableList<Int>.swap(index1: Int, index2: Int) { val tmp = this[index1] // 'this' corresponds to the list this[index1] = this[index2] this[index2] = tmp } fun main(args: Array<String>) { // mutableなListは arrayListOf, linkedListOf で作成できる val list = arrayListOf(1,2,3) list.swap(0,2) println(list) }
実行結果は以下のようになります。
[3, 2, 1]
ちなみにdecompileしてみると、以下のようなコードが生成されていることがわかります。
前回のDataClassのときと同様、Kotlinのソースコードをコンパイルする際にいろいろと頑張っています。
public final class MainKt { public static final void swap(List<Integer> $receiver, int index1, int index2) { Intrinsics.checkParameterIsNotNull($receiver, "$receiver"); int tmp = ((Number)$receiver.get(index1)).intValue(); $receiver.set(index1, $receiver.get(index2)); $receiver.set(index2, Integer.valueOf(tmp)); } public static final void main(@NotNull String[] args) { Intrinsics.checkParameterIsNotNull(args, "args"); ArrayList list = CollectionsKt.arrayListOf(new Integer[] { Integer.valueOf(1), Integer.valueOf(2), Integer.valueOf(3) }); swap((List)list, 0, 2); ConsoleKt.println(list); } }
なお、Extension機能ではGenericsを使うことも可能です。
先程のコードを以下のように修正することでInt型に限らず、swapメソッドを利用することができます。
//Listのなかを操作するのでMutableListにExtensionする fun <T> MutableList<T>.swap(index1: Int, index2: Int) { val tmp = this[index1] // 'this' corresponds to the list this[index1] = this[index2] this[index2] = tmp }
実際にExtensionを使うときのことを考えると、いままでの例のように同一のファイルにExtensionの実装を書くことはなく、別ファイルにExtensionの実装を記述するでしょう。
その際はimport文を用いて、別ファイルで定義したExtensionを利用することができます。
先程のswapメソッドを別ファイル(Extensions.kt)に切り出し、extensionパッケージ配下で定義します。
// Extensions.kt package extension; //Listのなかを操作するのでMutableListにExtensionする fun <T> MutableList<T>.swap(index1: Int, index2: Int) { val tmp = this[index1] // 'this' corresponds to the list this[index1] = this[index2] this[index2] = tmp }
swapメソッドを利用する側ではswapメソッドをimportすることで利用が可能となります。
// Main.kt package main; import extension.swap fun main(args: Array<String>) { // mutableなListは arrayListOf, linkedListOf で作成できる val list = arrayListOf(1,2,3) val stringList = arrayListOf("one", "two", "three") list.swap(0,2) println(list) stringList.swap(0,2) println(stringList) }
これでExtension機能をどのように利用したらよいかがなんとなく掴めたかと思います。
Extension機能を有効に活用することでソースの可読性やメンテナンス性を高めることができますので利用を検討してみてください。(ご利用は計画的に…)
・ちょっと気になること…
Extensionの実装を別ファイルに分けた場合、import文でメソッド名(もしくは*)を指定することでimportを行います。
Extensionの実装ファイルに同じメソッド名を持った違うクラスへのExtensionが定義されている場合はどうなるのでしょう??
ちょっと試してみました。
Extensions.ktにIntArrayクラスのExtensionとして、swapメソッドを定義しました。(何もしないメソッドですが….)
この状態ではコンパイルエラーは発生しません。
// Extensions.kt package extension; //Listのなかを操作するのでMutableListにExtensionする fun <T> MutableList<T>.swap(index1: Int, index2: Int) { val tmp = this[index1] // 'this' corresponds to the list this[index1] = this[index2] this[index2] = tmp } fun IntArray.swap(index1: Int, index2: Int) { }
利用するMain.kt側でIntArrayを作成し、swapメソッドを呼ぼうとすると…
package main; import extension.swap fun main(args: Array<String>) { // mutableなListは arrayListOf, linkedListOf で作成できる val list = arrayListOf(1,2,3) val intArray = intArrayOf(1,2,3) list.swap(0,2) intArray.swap(0,2) }
コンパイルエラーも発生せず、普通に実行できます。
Kotlinのソースコード上で見ると不思議な感じですが、生成されたExtensions.ktのJavaクラスをdecompileしてみるとswapメソッドがオーバロードされているだけの話なので、エラーがでないのは当然となります。
想定外の副作用を避けるため、1ファイルで複数のクラスのExtensionを記述することは止めたほうが良いでしょう。