そのforeach本当に要りますか?~for/foreach等をLINQに書き換える~

  • 43
    Like
  • 0
    Comment

C#Adventcallenderの8日目ですよ~
昨日はなんか難しそうなでも面白い記事だったので今日は簡単?な記事を

僕は新しいものが大好きです。
もちろんC#7も心待ちにしています。

僕が初めてC#に触れたのはC#3.5の時なのでその最先端のLINQももちろん触っていて
今ではLINQ星人です。最近はRXなんて形態も出てきましたね。

新卒の就活でも意図的にC#が使える仕事を選んでありがたいことにC#っていうかUnity使ってます。


\def\textlarge#1{%
  {\rm\Large #1}
}

\def\texthuge#1{%
  {\rm\huge #1}
}

$\texthuge{が}$

$\textlarge{だがしかし}$

Unityちゃん界隈だとLINQではなくforを多く使う人が若干多めな傾向にあるような気がします。
(アセット買いあさってみるとわりとforが多い)
個人的にはとてももやもやーっとしますのでforeachからLINQへの置き換えを書いていきます。
全ての例でforeachにしますが大丈夫ですよね。

速度がどうとかいう議論はほかでやってください。C++とかCとかアセンブラとかおすすめですよ。

LINQを使うメリット

  1. 記述が簡潔になる
  2. あなただけがわかる謎のメソッドを追加しなくて済む
  3. 条件を少し変えただけで新しいメソッドを作らなくて済む
  4. 世界共通で使える
  5. 考え方は言語をまたいで使える

foreach/forを使う最大のデメリット

for/foreach/if/switch/whileって昔々にできた古い言葉ですごく抽象度が高いんですね。

例えばforは繰り返しで使う構文ですが、そもそもなぜ繰り返しをするのかを記述できない
なのでなぜ繰り返すのかデータの加工なのか抜き出しなのかそういう記述ができるLINQや他言語メソッド群を優先的に使うべきだと思います

同様にifも分岐ですが、なぜ分岐したいのかがわからない
ここでは書きませんが例えばポリモーフィズムという方法をとっても良いしファクトリメソッドでも良いわけです

本当にこの文脈でそれを使うのが一番具体的なの?という意味ではこの辺の話も似たようなものかなと思います。
おめえ本当に今null使う必要あるん?というお話
null安全でない言語は、もはやレガシー言語だ

準備

前提知識

  • C#の基本文法はわかる
  • ラムダ式はわかる

共通コード

とりあえずこんなクラスを基底クラスとして始めます。
ようするにPositiveは1~9の配列、Negativeは-1~9の配列です。
Writeは配列表示するだけStartは両方実行してみるテスト用です

Legacyメソッドに古い方法LinqメソッドにLINQの方法を記述していきます。

    abstract class Base
    {
        protected int[] Positives = new[] {1, 2, 3, 4, 5, 6, 7, 8, 9};
        protected int[] Negatives = new[] {-1, -2, -3, -4, -5, -6, -7, -8, -9};

        protected abstract void Legacy();

        protected abstract void Linq();

        public void Write<T>(IEnumerable<T> array)
        {
            Console.WriteLine(string.Join(" ",array));
        }

        public void Start()
        {
            Console.WriteLine(GetType());
            Console.WriteLine("Legacy");
            Legacy();
            Console.WriteLine("LINQ");
            Linq();
            Console.WriteLine();

        }
    }

基本的な置き換え

明日から使えるLINQの置き換えです。
すぐできます今から使うと幸せになります。

配列から条件に合う物をだけを抜き出す

  • 与えられたファイルパスの配列から拡張子がpngだけのパスの配列を返す
  • ユーザーの中で年齢が20歳以下のユーザーの配列を返す

配列から条件に合う物をだけを抜き出すには別のリストを用意してforとifを使って条件に合ったものを…やめましょう。
Whereをつかえば一行です。

たとえば、3の倍数だけを抜き出すような操作は次の1行で書けます。

Where

    class Where : Base
    {
        protected override void Legacy()
        {
            List<int> list = new List<int>();
            foreach (var i in Positives)
            {
                if (i%3 == 0)
                {
                    list.Add(i);
                }
            }
            Write(list);
        }

        protected override void Linq()
        {
            IEnumerable<int> list = Positives.Where(i => i%3 == 0);
            Write(list);
        }
    }

配列のすべての値に一定の操作をする

  • 入力配列のユーザーidからユーザー情報を配列で返す
  • 入力配列の金額から消費税込みの金額の配列を返す

配列のすべての値に一定の操作をするいわゆる射影と呼ばれる操作、これをするには別の配列をつくってそこにforeachをつかって順番に計算した値を入れて…
ではなくSelectを使いましょう。

たとえば、配列の中身をそれぞれ二乗した配列を作る操作はこの一行で書けます。

Select
    class Select : Base
    {
        protected override void Legacy()
        {
            int[] list = new int[Positives.Length];
            for (int i = 0; i < list.Length; i++)
            {
                list[i] = Positives[i]* Positives[i];
            }


            Write(list);
        }

        protected override void Linq()
        {
            var list = Positives.Select( i => i * i);

            Write(list);
        }
    }

配列の中から条件に合う任意の要素を一つ取り出したい

  • ユーザーの配列から年収が400万円以上のユーザーを一人選び出す
  • ユーザーIDが10であるユーザーを検索する
    • これは一意性が担保されている例です

配列の中から条件に合う要素をもつデータをどれでもいいから一つ取り出したいことがあります。
(それはもしかしたら一意性が保証されているかもしれないし、そうでないかもしれません)
そんな時はforとifを使って条件に合う要素があったらそれを返せば…いえいえ

そんなときはFirst/FirstOrDefaultやLast/Single等を状況によって使用します。

例えば、

First

        protected override void Legacy()
        {

            int upper5 = 0;

            foreach (var i in Positives)
            {
                if (i >= 5)
                {
                    upper5 = i;
                }
            }

            Console.WriteLine(upper5);
        }

        protected override void Linq()
        {
            int upper5 = Positives.First(i => i >= 5);
            Console.WriteLine(upper5);
        }

Firstは配列の中で条件に合う要素のうち最初の一つの要素を取得するメソッドです
Lastはもちろん配列の中で条件に合う要素のうち最後の一つの要素を取得するメソッドです
Singleは条件に合うものが複数あった場合例外を送出します。

それぞれのメソッドは条件に合う要素がなければ例外を送出します。

もし例外ではなくその型のデフォルト値(default(T))を返してほしい場合は
FirstOrDefault/LastOrDefault/SingleOrDefaultを使用します。(決してtry/catchを使用してはいけません。)

基本的にはFirstが速度の面で若干有利です。ですから特に理由がない場合Firstの利用をお勧めします。
ただし、結局はO(n)の操作になるので、「ユーザーIDが10であるユーザーを検索する」というような主キーとなるようなものがある場合そもそも主キーをKeyとしたDictionaryを検討してください。(Dictionaryの検索はO(1)に近いです。)

foreachの中身を読みやすくする置き換え

  • 敵の中からHPが0以下になっている敵のみに死亡処理をする
  • アイテムリストから売却フラグが立っているアイテムだけ売却する

今度はforeachではなくifとcontinue/breakが必要ですか?という置き換えです。

先程のWhereやSelectをちゃんと使ってあげれば、continueもbreakもほとんど必要なくなりますよ。
という感じです。

と言うかですね。この2つのキーワードについては書く必要に迫られたときは一旦立ち止まってください。
LINQで大抵回避できます。そしてそちらのほうが大抵わかりやすい書き方ができます。ネストも減りますしね。
そして本当の処理部分だけを書いたforeachを記述してください

NoContinue

        protected override void Legacy()
        {
            foreach (var i in Positives)
            {
                // ここみただけだと 
                //この条件に合うと・・・
                //どうなるの?除外?それとも使用される?それとも別の何か?
                if (i%2 == 0) 
                {   //ネストも深くなる
                    continue; //ここで初めて除外がわかる
                }

                Console.Write(i + " "); //重要な処理は最後まで行かないとわからない
            }
        }

        protected override void Linq()
        {
            var evens = Positives.Where(i => i%2 != 0); //変数名もつけられる
            foreach (var i in odds)
            {
                Console.Write(i + " ");
            }
        }

2つのリストの統合 #

ここまで来てリストを2つ一緒にループさせるときはもう役立たずじゃんと思う人もいるかもしれません。
そういう状況があまり思いつかないのですが、その用途であればZipで可能です。(なおUnityには未実装・・・おい・・・)

Zip

    class Zip : Base
    {
        protected override void Legacy()
        {
            List<string> list = new List<string>();

            for (int i = 0; i < Positives.Length; i++)
            {
                list.Add(Positives[i] + ":" + Negatives[i]);
            }
            Write(list);
        }

        protected override void Linq()
        {
            var list = Positives.Zip(Negatives, (p, n) => p + ":" + n);
            Write(list);
        }
    }

ちなみにSelectにはSelect(this IEnumerable,Func)というオーバーロードがありFuncのintに何番目かのインデックスが入ります。

他に2つ以上のリストを扱う方法としてリストの結合はConcat,もっと複雑な統合はJoinを使います。

その他の簡潔に書くためのTips

その他覚えておいたほうが良いTipsです。
簡単に示します。

リストが空かどうかの判別 ###

リストが空かどうかはAnyを使って判別します。
いままでLengthやCount()を利用していた人も多いのでは?

list.Any()はlistに値が一つでもあればtrueひとつもなければfalseです
例えばこのように書くとlistが空の場合何も処理をせずに戻すコードになります。

Any

void Foo(){
    if(!list.Any()) return;
    //略
}

キャストしたい

リストの中身をキャストしたい時もあります。
贅沢を言えばリストの中から特定の形のデータだけ抜き出したいとか・・・

そういうときにはOfTypeを使います。
Castというメソッドもありますがそちらはキャストが失敗したら例外を出します。
用途によって使い分けてください。

こんなのもあったので参考にどうぞ
【C#】Where(it => it is Xxx).Select(it => it as Xxx)って書かず、OfType使おう!【LINQ】

重複を除去したい

わりと面倒くさいこの作業はDistinctを利用します。

データをひっくり返したい

Reverseを使います。
UI作成とかにどうぞ

おわりに

どんどん新しい知識増やしていかないとstaticおじさんになっちゃいますね。
この知識もまだ使えるけどいつごみになるかわかんないですしね。

最近はRxやnull安全とか
言語も色々出てくるしツールも多くて若いけど大変です。

C#7はどうなるのかしら、あとC#6にそろそろ対応するはずのUnityちゃんはよ!!
まってるんだから!

この記事で使われたメソッドはここに簡単に一覧にしています。
LINQチートシート的なもの

This post is the No.8 article of C# Advent Calendar 2016