【LINQ】遅延評価・LINQのメリットを考えてみた【C#】

結論から言うと

あくまで私の考えですが、

遅延評価 のメリットは LINQ が実現できることだと私は思います。
そして、LINQ のメリットは コレクションに対する処理を宣言的に分かりやすく書けること だと私は考えています。

たまに、「自分にはLINQを使うメリットがいまいち分からない」という声を聞きますが、別に無理してまで使わなくてもいいと思います。
大抵はLINQを使わなくてもforeachでコレクションを回すときに処理をすれば同じような処理になるからです。

LINQの処理の流れを確認してみる

LINQを使わなくてもforeachでコレクションを回すときに処理をすれば同じような処理になる

というように書きましたが、具体例を挙げて考えてみます。

例: Select拡張メソッドの場合

Select拡張メソッドは MoveNext メソッド内部で、
要素に変形をさせるデリゲート Func<TSource, TResult> selector に要素を渡し、
返ってきた変形後の値を Current プロパティにセットするオペレータです。

実装はだいたい以下のようになっています。

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

namespace aridai
{
    public static class MyEnumerable
    {
        public static IEnumerable<TResult> Select<TSource, TResult>(this IEnumerable<TSource> source, Func<TSource, TResult> selector)
        {
            //  このコードはあくまで実験用
            //  本当は色々とチェックや最適化が掛かったコードになっている
            //  sourceが配列やリストの場合はソースのコレクションの列挙を高速化したりしてくれている
            //  詳しくはソースを読めばいいと思われる
            //  https://referencesource.microsoft.com/#System.Core/System/Linq/Enumerable.cs

            return new MySelectEnumerable<TSource, TResult>(source, selector);
        }
    }

    public class MySelectEnumerable<TSource, TResult> : IEnumerable<TResult>
    {
        private IEnumerable<TSource> source;

        private Func<TSource, TResult> selector;

        internal MySelectEnumerable(IEnumerable<TSource> source, Func<TSource, TResult> selector)
        {
            this.source = source;
            this.selector = selector;
        }

        public IEnumerator<TResult> GetEnumerator()
        {
            return new MySelectEnumerator<TSource, TResult>(source, selector);
        }

        IEnumerator IEnumerable.GetEnumerator()
            => GetEnumerator();
    }

    public class MySelectEnumerator<TSource, TResult> : IEnumerator<TResult>
    {
        private IEnumerable<TSource> source;

        private Func<TSource, TResult> selector;

        private IEnumerator<TSource> enumerator;

        private int state = 1;

        public TResult Current { get; private set; }

        object IEnumerator.Current => Current;

        public MySelectEnumerator(IEnumerable<TSource> source, Func<TSource, TResult> selector)
        {
            this.source = source;
            this.selector = selector;
        }

        public bool MoveNext()
        {
            switch (state)
            {
                case 1:
                    enumerator = source.GetEnumerator();
                    state = 2;
                    goto case 2;

                case 2:
                    if (enumerator.MoveNext())
                    {
                        //  Func<TSource, TResult> selectorで変形させて
                        //  Currentプロパティにセットする
                        Current = selector(enumerator.Current);
                        return true;
                    }

                    else
                    {
                        state = -1;
                        return false;
                    }
            }

            return false;
        }

        public void Dispose() { }

        public void Reset() => throw new NotSupportedException();
    }
}

Select拡張メソッドを使ったコードはこのようになります。

using System;
using System.Linq;

namespace aridai
{
    public static void Main()
    {
        var source = Enumerable.Range(1, 10);
        foreach (var e in source.Select(n => n * n))
        {
            Console.WriteLine(e);

            /*
                出力
                1
                4
                9
                16
                25
                36
                49
                64
                81
                100
            */
        }
    }
}

当然ですが、Select拡張メソッドに渡したデリゲートを通して変形された値が返ってきています。
このコードをSelect拡張メソッドを使わずに書くとこうなります。

using System;
using System.Linq;

namespace aridai
{
    public static void Main()
    {
        public static void Main()
        {
            var source = Enumerable.Range(1, 10);
            foreach (var e in source)
            {
                var e2 = e * e;
                Console.WriteLine(e2);
            }
        }
    }
}

この2つの書き方、本質的にはやっていることは同じです。
値の変形を MySelectEnumerator.MoveNext メソッド内でやるか、ループを回したときにやるかの違いしかありません。

何が言いたいのかというと、LINQを利用したコードとループを回したときに処理を加えるコードでは、処理のされ方とほとんど変わらない ということです。

IEnumerable<T>として扱える

ただ、LINQで処理する場合、コレクションに対する処理を予約することができる というメリットがあります。
(予約というか、そういう要素の返し方をするイテレータパターンを実装したクラスを返すといったほうがいいかもしれませんけど...)

予約 というのは

オペレータは「コレクションに対して処理をするようなIEnumerable<T>」を返してくれるのだが、
実際に処理をされるのはループ中にOperatorEnumeratorのMoveNextメソッドが呼ばれたとき

ということです。
言ってしまえば、これ以外の違いはほとんどありません。

もし遅延評価されなかったら?

LINQを実現するためには 遅延評価 が必要不可欠です。
もし 遅延評価 されずに、LINQのような書き方がしたい場合、オペレータを通すごとに、リストに要素を詰め直すような実装が必要になります。
例えば以下のような場合

using System;
using System.Linq;

namespace aridai
{
    public static void Main()
    {
        var source = Enumerable.Range(1, 10);
        source.Where(n => n % 2 == 0).Select(n => n * n);
    }
}

Where 拡張メソッドを通るとき、Select 拡張メソッドを通るとき、2回もリストへの要素の詰め直しが必要となり、無駄ですね。

まとめ

今回の内容をまとめると

  • 遅延評価 のメリットは LINQ が実現できること
  • LINQ のメリットは コレクションに対する処理を宣言的に分かりやすく書けること
  • LINQ を使わずにループを回すときに処理した場合と評価のされるタイミングはほとんど変わらない
  • LINQ を使うと、コレクションに対する処理を予約することができる

ということです。

LINQ を使う大きなメリットは コードが分かりやすくなること です。
だから、必ずしも LINQ を使わなくてはならないということはないと言うのが私の考えです。

2017/06/26 12:29:18
コメントを投稿する