読者です 読者をやめる 読者になる 読者になる

C# 3.0 と while(true) と Iterator

.NET

前置き

よくある(ファイルなどから)複数行テキストを一行ずつ読み込む場合のソースコードですが

using (StreamReader sr = new StreamReader(filePath)) {
    string line = string.Empty;
    while((line != sr.ReadLine()) != null) {
        // do something...
    }
}

とするよりも

using (StreamReader sr = new StreamReader(filePath)) {
    for (string line = sr.ReadLine(); line != null; line = sr.ReadLine()) {
        // do something...
    }
}

の方が好きなんですが仲間はいるかなぁ?
行数の問題ではなく、一時変数 line の有効スコープが必要なループ内だけで収まってるのが好きなんです。 ソースコードとしては↓が一番読みやすいんでしょうけどね。

using (StreamReader sr = new StreamReader(filePath)) {
    string line = string.Empty;
    while(true) {
        line = sr.ReadLine();
        if(line != null) {
            // do something...
        } else {
            break;
        }
    }
}

昔、一つ目の書き方はいいけど二つ目はだめだといわれたことがありました。
はっきりいって読みやすさ、読みづらさは同レベルだと思うんだけど。
一つ目はWebなどで割りと一般的に紹介されているからでしょうね。
ああ、マイノリティ

でもあれですね。今の自分の好みで云うと、Pythonで、ジェレネータに ストリームからの取り出しを押し込めてしまう。これで木鞠決まりw。 まあ while ループにはなりませんが、forの形態も違いますしね。

Python に generator があるように,C# 2.0 以降では yield 文が使えるので,これを活用するのはまあ当然として,さらにその先の話でも.

3 年分のコードを清算する

2005 年に*1 C# 2.0 のベータ版を使い出して以来,それなりの数の yield 文を書き散らかしてきた.そのうちかなりは,以下どちらかの generic method で置き換えることができたんじゃないかと思う.

static IEnumerable<TResult> Generate<TElement, TResult>(
    TElement element,
    Func<TElement, TResult> func)
{
    while (true)
    {
        yield return func(element);
    }
}

static IEnumerable<TResult> Generate<TElement, TResult>(
    Func<TElement> initializer,
    Func<TElement, TResult> func) where TElement:IDisposable
{
    using (var element = initializer())
    {
        while (true)
        {
            yield return func(element);
        }
    }
}

Generate メソッドの中に停止条件は書かなくて良い.それは外に書ける.
元ネタのソースコードで説明しよう.

var lines = Generate(() => new StreamReader(filepath), sr => sr.ReadLine())
                .TakeWhile(line => line != null);

foreach (var line in lines)
{
    // do something...
}

つまりこれで十分というわけだ.わざわざ停止条件を Generate メソッドの中に書くのは特殊化しすぎ.汝,パーツを愛せ.
でも,この Generate ってメソッド,まだ分解できるよね?

Repeat, Repeat, Repeat, ...

上に定義した Generate メソッドの正体が「同一要素の無限リピート」+「射影」ということに気付けばあとは簡単.「射影」は C# 3.0 で既にあるので,必要なのは無限に同一要素を返す Repeat の方.つまり答えはこう.

static partial class EnumerableEx
{
    public static IEnumerable<T> Repeat<T>(T element)
    {
        while (true)
        {
            yield return element;
        }
    }
    public static IEnumerable<T> Repeat<T>(Func<T> elementInitializer)
        where T : IDisposable
    {
        using (var element = elementInitializer())
        {
            while (true)
            {
                yield return element;
            }
        }
    }
}

てなわけで結局こうなる.

var lines = EnumerableEx.Repeat(() => new StreamReader(filePath))
                        .Select(sr => sr.ReadLine())
                        .TakeWhile(line => line != null);

使用例

//ランダムに 0 から 9 までの数を返す無限リスト
var randomDigit = EnumerableEx.Repeat(new Random())
                              .Select(rand => rand.Next(0, 10));

//今から 10 秒以内に限り,true を返し続ける無限リスト
var timer = EnumerableEx.Repeat(DateTime.Now.AddSeconds(10.0))
                        .TakeWhile(time => DateTime.Now <= time)
                        .Select(time => true);

暇潰し編

ここから先はただの遊び.
EnumerableEx.Repeat の初期化子をとるバージョンから where T : IDisposable という制約を消してみる.ただし T が IDisposable だったときの動作は変えないまま.どういうときに便利かというとこういうの.

//最初の要素を評価した時刻からちょうど 10 秒間,終了時刻までの残り時間を返し続ける無限リスト
var resttime = EnumerableEx.Repeat(() => DateTime.Now.AddSeconds(10.0))
                           .Select(time => time - DateTime.Now)
                           .TakeWhile(span => span.TotalSeconds > 0);

boxing を回避しつつの制約外しのテクニックは滅多に出番がないので,練習がてらにやってみる.まあ値型で IDisposable ってのはまずなかろうけど.

static partial class EnumerableEx
{
    public static IEnumerable<T> Repeat<T>(T element)
    {
        while (true)
        {
            yield return element;
        }
    }
    public static IEnumerable<T> Repeat<T>(Func<T> elementInitializer)
    {
        return RepeatImpl<T>.Default.Repeat(elementInitializer);
    }

    private class RepeatImpl<T>
    {
        public virtual IEnumerable<T> Repeat(Func<T> elementInitializer)
        {
            var element = elementInitializer();
            while (true)
            {
                yield return element;
            }
        }
        public static readonly RepeatImpl<T> Default;
        static RepeatImpl()
        {
            Type t = typeof(T);
            if (typeof(IDisposable).IsAssignableFrom(t))
            {
                var implType = typeof(RepeatImplWithUsing<>).MakeGenericType(t);
                Default = Activator.CreateInstance(implType) as RepeatImpl<T>;
            }
            else
            {
                Default = new RepeatImpl<T>();
            }
        }
    }
    private sealed class RepeatImplWithUsing<U> : RepeatImpl<U> where U : IDisposable
    {
        public override IEnumerable<U> Repeat(Func<U> elementInitializer)
        {
            using (var element = elementInitializer())
            {
                while (true)
                {
                    yield return element;
                }
            }
        }
    }
}

デリゲート好きな人のための別解.

static partial class EnumerableEx
{
    public static IEnumerable<T> Repeat<T>(T element)
    {
        while (true)
        {
            yield return element;
        }
    }
    public static IEnumerable<T> Repeat<T>(Func<T> elementInitializer)
    {
        return RepeatImpl<T>.Repeat(elementInitializer);
    }

    private static class RepeatImpl<T>
    {
        public static IEnumerable<T> Repeat(Func<T> elementInitializer)
        {
            return _repeat(elementInitializer);
        }
        private static readonly Func<Func<T>, IEnumerable<T>> _repeat;

        static RepeatImpl()
        {
            Type t = typeof(T);
            if (typeof(IDisposable).IsAssignableFrom(t))
            {
                Func<Func<IDisposable>, IEnumerable<IDisposable>> dummy
                    = RepeatWithUsing<IDisposable>;

                _repeat = Delegate.CreateDelegate(
                             typeof(Func<Func<T>, IEnumerable<T>>),
                             dummy.Method.GetGenericMethodDefinition().MakeGenericMethod(t)
                          ) as Func<Func<T>, IEnumerable<T>>;
            }
            else
            {
                _repeat = RepeatWithoutUsing;
            }
        }
        private static IEnumerable<T> RepeatWithoutUsing(Func<T> elementInitializer)
        {
            var element = elementInitializer();
            while (true)
            {
                yield return element;
            }
        }
        private static IEnumerable<U> RepeatWithUsing<U>(Func<U> elementInitializer)
            where U : IDisposable
        {
            using (var element = elementInitializer())
            {
                while (true)
                {
                    yield return element;
                }
            }
        }
    }
}

過去の関連記事

*1:ひょっとしたら 2004 年だったかも