Kotlinではコレクション操作メソッドが豊富で、ボイラープレートコードを排除したスマートなコーディングができて大変快適ですね。しかし、既存クラスの中にはコレクション的なクラスなのだが、Collectionでない、というかIterableでないクラスが存在します。Collectionだと思ってmapを使おうとしたら使えない!!ってなるあれです。
でもまあ、当然ですが、Kotlinだと拡張関数が使えるので、拡張関数でforEachとか定義すれば同じ書き方ができます。
KTXで提供されているViewGroupに対するforEachみたいなものですね。
inline fun ViewGroup.forEach(action: (view: View) -> Unit) {
for (index in 0 until childCount) {
action(getChildAt(index))
}
}
また、IterableやSequenceに変換する拡張関数を定義しておけば、これらをレシーバーとする一連の拡張関数群が使えるようになります。
ここでは、CollectionでないけどCollectionのように扱いたくなるクラスをCollection的に扱う方法を紹介します。(すでにあるかもしれないが調べてない)
※安易に拡張関数を作ると混乱するので、使う際は各プロジェクト等のルールに従ってください。ご利用は自己責任で。
基本的な作業
基本的な作業は、Iteratorを定義することにつきます。
Iteratorはnext()とhasNext()の2つのメソッドを定義すればよいだけですので簡単に作れます。
クラスとして定義してもいいですが、iterator()メソッドをoperatorとして定義すればそのままfor文で使えるようになるので以下のように定義してしまいましょう。
要するに、以下のように定義すると
operator fun Hoge.iterator(): Iterator<Fuga> = object : Iterator {
override fun next(): Fuga = ...
override fun hasNext(): Boolean = ...
}
以下のように書けるようになります。
for (fuga in hoge) { ... }
残念ながらforEachはこのままでは使えませんので、拡張関数の定義が必要です。が、iterator()を定義していれば一行です。
inline fun Hoge.forEach(action: (Node) -> Unit): Unit = iterator().forEach(action)
※iterator()のインスタンスを作成する分、多少効率は悪いとも言えますので、インスタンスを作成しないで簡単に書き下せる場合は、そちらで定義した方がよいと思います。
また、任意のコレクション操作を実現したい場合はIterableに変換してしまえばOKです。
Iterableは iterator() メソッド一つのインターフェースなので、以下のような定義になります。
fun Hoge.asIterable(): Iterable<Fuge> = Iterable { iterator() }
asIterable()はoperatorではないので、この名前でないといけないと言うことはありません。
Iterableに変換してしまえば以下のようにCollection操作メソッドが使えます。
hoge.asIterable()
.filter { ... }
.map { ... }
.toSet()
forEachIndexed()も使えます。
hoge.asIterable().forEachIndexed { index, fuga ->
}
Iterableに変換する代わりにSequenceに変換するというのもありだと思います。Sequenceもiterator()メソッド一つのインターフェースで、Iterableと同じです。
fun Hoge.asSequence(): Sequence<Fuge> = Sequence { iterator() }
IterableからSequenceはasSequence()で、SequenceからIterableはasIterable()で簡単に相互変換可能なので使いやすい方を定義すればよいと思います。
sequence関数/iterator関数
@sdkei さんから教えていただきました。(ありがとうございます)
sequence関数を使うとイテレート処理ほぼそのままでSequenceを作ることができます。
先のViewGroupでやるとこうです。
fun ViewGroup.asSequence(): Sequence<View> = sequence {
for (index in 0 until childCount) {
yield(getChildAt(index))
}
}
残念ながら iterable 関数はないみたいですが、iterator関数はあります。使い方は全く同じ。
operator fun ViewGroup.iterator(): Iterator<View> = iterator {
for (index in 0 until childCount) {
yield(getChildAt(index))
}
}
sequence/iterator関数が受け取るのは、SequenceScopeをレシーバーとするサスペンドラムダです。
SequenceScopeはyieldとyieldAll(引数はIterator/Iterable/Sequence)というサスペンド関数を持っており、yieldに値を渡した順に取り出されるSequenceを作ることができます。coroutinesをこういう風に使うこともできるのかと感心しました。
Iteratorとしての実装を考えるより、イテレート処理を書けば、それがそのままSequenceやIteratorになるので直感的かつスマートに実装することができますね。
事例集
結局のところ、Iteratorさえ定義してしまえば、他はほぼコピペみたいなものなので、以降はIteratorの作り方集みたいになります。asIterableとかの定義は省略するので適宜補完してください。
org.w3c.dom
DOMを操作する際、特に読み込みではループを多用することになりますが、これらもCollectionだったりIteratorだったりはないので、while文などを使う必要があります。これらは拡張関数を用意すると便利です。
Node
Nodeは getNextSibling() を使って隣接ノードを見ていくループをよく使いますね。
しかし、そのまま使おうとすると do ~ while を使ったりとなかなかスマートに使えないですね。
var node: Node? = ...
do {
...
node = node.nextSigling
} while(node != null)
iterator()の定義
operator fun Node.iterator(): Iterator<Node> = object : Iterator<Node> {
private var node: Node? = this@iterator
override fun next(): Node = node?.also { node = it.nextSibling }
?: throw NoSuchElementException()
override fun hasNext(): Boolean = node != null
}
これでスマートにいけます。
iterator関数を使うなら以下ですが、iterator関数に片引き数がないとyieldで!!が必要になるのでちょっとややこしい
operator fun Node.iterator(): Iterator<Node> = iterator<Node> {
var node: Node? = this@iterator
while(node != null) {
yield(node)
node = node.nextSibling
}
}
Elementだけ取りたいんや
node.asIterable()
.mapNotNull { it as? Element }
で、十分すね。
Iterator書いた後に気づいたorz
NodeList
Nodeの子要素のイテレートをしたい場合、getFirstChild()で最初の子要素を取り出して、getNextSigling()でイテレートするか、getChildNodes() でNodeListを取り出して、これをイテレートするかになります。
NodeListは以下のようなインターフェースです。
public interface NodeList {
public Node item(int index);
public int getLength();
}
for文を書くだけならこんな感じで行けます。
for (i in 0 until nodeList.length) {
val node = nodeList.item(i)
...
}
Iteratorにするにはこう
operator fun NodeList.iterator(): Iterator<Node> = object : Iterator<Node> {
private var index = 0
override fun next(): Node = item(index++)
override fun hasNext(): Boolean = index < length
}
iterator関数を使うなら
operator fun NodeList.iterator(): Iterator<Node> = iterator {
for (i in 0 until length) yield(item(i))
}
NodeListのイテレートはchildNodesに対して行うことが多いのと、隣接ノードでのイテレートと区別するため、以下のような拡張関数を定義しておけばいいかなと思います。
fun Node.siblings(): Iterable<Node> = Iterable { iterator() }
fun Node.siblingElements(): Iterable<Element> = siblings().mapNotNull { it as? Element }
fun NodeList.asIterable(): Iterable<Node> = Iterable { iterator() }
fun NodeList.asElementIterable(): Iterable<Element> = asIterable().mapNotNull { it as? Element }
fun Node.children(): Iterable<Node> = childNodes.asIterable()
fun Node.childElements(): Iterable<Element> = childNodes.asElementIterable()
NamedNodeMap
NodeからgetAttributes()でAttributesを取り出してイテレートする場合があります。
これは NamedNodeMap なのでこいつに対しても定義します。NamedNodeMapは名前をキーに取り出すMapとしての機能がありますが、イテレートするために必要なメソッドはNodeListと同一なので以下のようになります。
operator fun NamedNodeMap.iterator() = object : Iterator<Node> {
private var index = 0
override fun next(): Node = item(index++)
override fun hasNext(): Boolean = index < length
}
iterator関数を使うなら
operator fun NamedNodeMap.iterator(): Iterator<Node> = iterator {
for (i in 0 until length) yield(item(i))
}
Enumration
NetworkInterface.getNetworkInterfaces()の戻り値はEnumration<NetworkInterface>であり、EnumrationはCollectionでもIteratorでもありません。以下のようにIteratorのメソッド名が違うだけの Java1.0 からあるレガシーなインターフェースです。それだけに特にコードを書かずとも変換する手段が提供されているので、それを利用しましょう。
public interface Enumeration<E> {
boolean hasMoreElements();
E nextElement();
}
Javaの世界でもCollectionsにArrayListに詰め替えるメソッドが存在しますし、
public static <T> ArrayList<T> list(Enumeration<T> e) {
ArrayList<T> l = new ArrayList<>();
while (e.hasMoreElements())
l.add(e.nextElement());
return l;
}
KotlinではCollectionsJVM.ktにて上記Collections.listメソッドを使った
@kotlin.internal.InlineOnly
public inline fun <T> java.util.Enumeration<T>.toList(): List<T> = java.util.Collections.list(this)
という拡張関数が用意されていますので、toList() してしまえばあとはListとして扱えます。
さらに言えば、Java9以降ならasIterator()のデフォルトメソッドがあったりするので、使える環境ならこれを使えばよいですね。
default Iterator<E> asIterator() {
return new Iterator<>() {
@Override public boolean hasNext() {
return hasMoreElements();
}
@Override public E next() {
return nextElement();
}
};
}
まとめ
XML扱う処理書いてて、Iterator作ればええやんと気づいたので書き始めたのだが、結構バリエーションあるかなと思いきやネタ切れ、思いついたら追記するかも。