かずきのBlog@hatena

日本マイクロソフトに勤めています。XAML + C#の組み合わせをメインに、たまにASP.NETやJavaなどの.NET系以外のことも書いています。掲載内容は個人の見解であり、所属する企業を代表するものではありません。

LINQは遅延評価?

この間、LINQはメソッドの呼び出しに展開されるから、同名のメソッドを用意しておけば、それが呼ばれるようになるよ!みたいなことを書いた。


もう一回書いてみようと思う。

using System;
using System.Collections.Generic;
using System.Text;
// using System.Linq; 自前Selectとか使うためusingしない

namespace MyLinq
{
    class Program
    {
        static void Main(string[] args)
        {
            var values = new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
            // 偶数だけ取り出して二乗する
            var results = from value in values
                          where value % 2 == 0
                          select value * value;
            foreach (var result in results)
            {
                Console.WriteLine(result);
            }
        }
    }

    // 自前のWhereメソッドとSelectメソッド
    static class MyExtensions
    {
        public static IEnumerable<T> Where<T>(this IEnumerable<T> e, System.Linq.Func<T, bool> f)
        {
            Console.WriteLine("俺様Where!!");
            ICollection<T> ret = new LinkedList<T>();
            foreach (var i in e)
            {
                if (f(i))
                {
                    ret.Add(i);
                }
            }
            return ret;
        }

        public static IEnumerable<U> Select<T, U>(this IEnumerable<T> e, System.Linq.Func<T, U> f)
        {
            Console.WriteLine("俺様Select!!");
            ICollection<U> ret = new LinkedList<U>();
            foreach (var i in e)
            {
                ret.Add(f(i));
            }
            return ret;
        }
    }
}

実行すると↓のようになる

俺様Where!!
俺様Select!!
4
16
36
64
100

ここまではOK。


さて、ちょっと大変なのはここから。
こんなEnumerableを作ってみました。

    // 無限に数字を返すEnumerable
    class InfEnumerable : IEnumerable<int>
    {
        public IEnumerator<int> GetEnumerator()
        {
            return new InfEnumerator();
        }
        System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
        {
            return GetEnumerator();
        }

        class InfEnumerator : IEnumerator<int>
        {
            private int value = 0;
            public int Current
            {
                get { return value; }
            }
            public void Dispose()
            {
            }
            object System.Collections.IEnumerator.Current
            {
                get { return Current; }
            }
            public bool MoveNext()
            {
                value++;
                return true;
            }
            public void Reset()
            {
                value = 0;
            }
        }

    }

0,1,2,3,4,5,6.......とintが許す限り数字をカウントアップしていく輩です。
こいつに対してLINQで問い合わせをしてみると…

本物のSystem.Linqを使う

    class Program
    {
        static void Main(string[] args)
        {
            // 最初の10個の偶数を二乗した結果を表示する
            var values = new InfEnumerable();
            var results = from value in values
                          where value % 2 == 0
                          select value * value;
            int count = 0;
            foreach (var result in results)
            {
                Console.WriteLine(result);
                if (count++ >= 10)
                {
                    break;
                }
            }
        }
    }

実行するとちゃんと結果が出ます。

4
16
36
64
100
144
196
256
324
400
484

これを、俺様SelectとWhereを使うように置き換えると…

俺様Where!!

の時点でとまっちゃいます。
そりゃそうだ、無限ループしてますからね。


ってことで、無限ループのせいでパソコンが5分くらい固まっちゃいました。
やっと復活です!!
無限ループいくない!!


さて、結局のところ↓のようにイテレータを使うと丸くおさまります。

    // 自前のWhereメソッドとSelectメソッド
    static class MyExtensions
    {
        public static IEnumerable<T> Where<T>(this IEnumerable<T> e, System.Linq.Func<T, bool> f)
        {
            foreach (var i in e)
            {
                if (f(i))
                {
                    yield return i;
                }
            }
        }

        public static IEnumerable<U> Select<T, U>(this IEnumerable<T> e, System.Linq.Func<T, U> f)
        {
            foreach (var i in e)
            {
                yield return f(i);
            }
        }
    }

これで実行すると、System.Linqに用意されてるSelectやWhereと同じ結果になります。
yield returnが無かったら書くのメンドクサソウだな…


ちなみに、yield returnを使えばInfEnumerableもこんなにスッキリ。

    // 無限に数字を返すEnumerable
    class InfEnumerable : IEnumerable<int>
    {
        public IEnumerator<int> GetEnumerator()
        {
            for (var i = 0; ; i++)
            {
                yield return i;
            }
        }
        System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
        {
            return GetEnumerator();
        }
    }

C#2.0で追加されたイテレータだけどとってもパワフル!!