かずきのBlog@hatena

すきな言語は C# + XAML の組み合わせ。Azure Functions も好き。最近は Go 言語勉強中。日本マイクロソフトで働いていますが、ここに書いていることは個人的なメモなので会社の公式見解ではありません。

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で追加されたイテレータだけどとってもパワフル!!