3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Kotlinのコレクション操作メソッドをCollectionでないクラスでも使う

3
Last updated at Posted at 2019-12-15

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はyieldyieldAll(引数は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作ればええやんと気づいたので書き始めたのだが、結構バリエーションあるかなと思いきやネタ切れ、思いついたら追記するかも。

3
1
3

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?