yield 再帰

Composit パターンを使った木構造イテレータを書こうとして、ふと yield で再帰ってどうやるんだ?とちょっと考え込んでしまったのでメモ。わかってしまえば、あーこれ再帰だよねぇと納得ですが、パッと見、再帰に見えない罠。
せっかくなのでファイル/ディレクトリ用のイテレータを書いてみました。LINQ のエサにどうぞ。

ちなみに、C#の言語仕様書にもサンプルが載ってます。あと、Head Firstデザインパターン ―頭とからだで覚えるデザインパターンの基本 にもズバリそのものが出てます。ちょっと横道にそれるけど「Head Firstデザインパターン」は今年読んだ本で一番でした。今年は図書館にずいぶん行ったんで、読んだ本は多いような気がしますが(読みきれないことが多いけどw)、間違いなくこれが一番です。パターン本ですが、オブジェクト指向の原則もいろいろと出てきます。オブジェクト指向をうまく使えないって悩みをちょっぴり解決してくれたかも?というわけでこの本のいいところを列挙。

  • GoF23パターン中の特に使えるものだけを重点解説
  • 読者に考えさせるのがうまい。一方的な説明じゃないのが良い
  • 問題を提起してそれをパターンで解決してみせるので、どこでどう使うってのがよくわかる
  • 同時に、変化をどう許容するかというプログラムを書くうえで最大の問題への対処方法を示している
  • 繰り返し繰り返し疎結合にしろ、継承を使いすぎるな、継承よりコンポジションをなどと現代的なOOの原則を説いている
  • 変な本なので頭に残る(今回も、あ、これ、レストランのメニューと思ったw)。この本が変なのは頭に残るようにとの配慮から
  • パターンとパターンの対話やQ&Aが、パターン間の違いなど様々な疑問を解決してくれる
  • ふざけた本なのに奥が深い。開放閉鎖原則とか依存性反転原則とかKISSとか

その後、メイヤー本も手にとってみたけど、そっちはどうもイマイチ…。厚すぎ。

閑話休題

using System.Collections.Generic;
using System.IO;

namespace FileEnum
{
  /// <summary>FileSystemInfoの行きがけ順(preorder)列挙子</summary>
  public sealed class FileSystemInfoPreorderEnumerator : IEnumerable<FileSystemInfo>
  {
    private DirectoryInfo dir;

    public FileSystemInfoPreorderEnumerator( DirectoryInfo dir )
    {
      this.dir = dir;
    }

    public IEnumerator<FileSystemInfo> GetEnumerator()
    {
      if ( this.dir == null )
        yield break;

      foreach ( FileSystemInfo item in this.dir.GetFileSystemInfos() )
      {
        yield return item;

        DirectoryInfo subDir = item as DirectoryInfo;
        if ( subDir != null )
        {
          foreach ( FileSystemInfo subDirItem in new FileSystemInfoPreorderEnumerator( subDir ) )
          {
            yield return subDirItem;
          }
        }
      }
    }

    System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
    {
      return GetEnumerator();
    }
  }

  public static class FileSystemInfoEnumerator
  {
    public static IEnumerable<FileSystemInfo> Preorder( this DirectoryInfo info )
    {
      return new FileSystemInfoPreorderEnumerator( info );
    }
  }
}

気軽な遊びで書いたものなので、バグってたらすみません。
拡張メソッドはおまけです。クラス名がちょっと長いので付けてみました。行きがけ順以外のイテレータを書いたときに、拡張メソッドの名前を Inorder(), Postorder() などと追加していくとわかりやすいですね。拡張ゲッターが書ければ () が消えてもっときれいだけど、そんなこといってると際限ないですね(^^;
DirectoryInfo の GetDirectories() や GetFiles() を使えば、as を使うよりスマートに書けますが、今回は使いませんでした。
これが再帰?と思ったなら、GetEnumerator() にブレークを張って動かしてみてください。
これを使うサンプルはこちら。

using System;
using System.IO;
using System.Linq;

namespace FileEnum
{
  class Program
  {
    static void Main()
    {
      var dir = new DirectoryInfo( @"D:\VS9Projects" );
      foreach ( var item in dir.Preorder() )
      {
        if ( item is DirectoryInfo ) Console.Write( "[Dir] " );
        Console.WriteLine( item.Name );
      }

      Console.WriteLine();

      var q = from file in dir.Preorder()
              where file.Extension == ".cs"
              select file;

      foreach ( var item in q )
      {
        Console.WriteLine( item.Name );
      }

      Console.ReadKey();
    }
  }
}

「D:\VS9Projects」は私の砂場。適当に変えて実行してください。