C# の yield return で再帰呼び出しを行うには

今回は C# の yield return 文を再帰メソッドの中で用いるときの書き方をメモしておきます。

概要

C# には yield return という構文があります。これは IEnumerable<T> を返すようなメソッドの中に記述し、 foreach などの呼び出し元に対して、コレクション要素を返すものです。

IEnumerable<int> GetNumber()
{
    yield return 1;
    yield return 2;
    yield return 3;
}

たとえば上の GetNumber メソッドを foreach で使うことができます。

// 1, 2, 3 を順に出力
foreach (var n in GetNumber())
{
    Console.WriteLine(n);
}

GetNumber 内で List<T> などに格納して返してもいいのですが、その場合すべての要素を列挙し終えるまで GetNumber を抜けないため、要素数が多い場合などは yield return のほうが望ましいでしょう。

まぁ、詳細な説明はグーグル先生にお任せするとして、今回は再帰メソッドでの使い方をご紹介します。

再帰呼び出しでの yield return

今回の例は一つの XmlElement インスタンスを渡して、自分を含む配下のすべての XmlElement を返すメソッドです。

素直に再帰メソッドを考えれば下記のように書きたくなります。

private static IEnumerable<XmlElement> GetXmlElements(XmlElement node)
{
    yield return node; // とりあえず自分を返す
    if (!node.HasChildNodes) yield break; // 子ノードがなければ抜ける
    foreach (XmlNode child in node.ChildNodes)
    {
        if (child.NodeType == XmlNodeType.Element) // 子ノードが XmlElement なら
        {
            return GetXmlElements(child as XmlElement); // ここが再帰呼び出し!でも動かない。
        }
    }
}

yield return を使うメソッドで強制的に抜けたい場合は yield break を使いましょう。このへんはコレクションらしいですね。

しかしこのコードは return GetXmlElements(child as XmlElement); の部分がコンパイルできません。 yield return を使用しているメソッド中では return が書けないからです。

かといって return しなければ、子ノードに対する呼び出しの結果は返されないまま終わってしまいます。

ということでどうするかと言うと、再帰呼び出しで戻ってくるコレクションをさらに foreachyield return で戻します

書き換えると下記のようになります。

private static IEnumerable<XmlElement> GetXmlElements(XmlElement node)
{
    yield return node; // とりあえず自分を返す
    if (!node.HasChildNodes) yield break; // 子ノードがなければ抜ける
    foreach (XmlNode child in node.ChildNodes)
    {
        if (child.NodeType == XmlNodeType.Element) // 子ノードが XmlElement なら
        {
            var subElements = GetXmlElements(child as XmlElement); // 再帰呼び出し
            foreach (var subElement in subElements)
            {
                yield return subElement; // 子ノードと含まれる要素を返す
            }
        }
    }
}

少々ややこしいですが、読めないほどではありませんね。

あとがき

ツリー構造を走査するときに便利な再帰呼び出し。yield return を使ってリッチに書いてみてはいかがでしょう?

kenzauros