C#
LINQ

【C#】デレマスを例題にしたLINQ入門

はじめに(読み飛ばしてもいいよ)

  • こんな投稿をしてしまって、各方面の方々、申し訳ございません。

  • 最近、スクレイピング等で文字列を操作する機会が多く、LINQに触り始めました。

  • LINQ覚えたての人の備忘録なので温かい目でご覧ください。

  • 問題と実行結果を見ながら解いていくと筆者が喜びます。

  • GitHubに今回の問題を上げましたのでよろしければやってみてください。

  • もっと良い回答があれば初心者の筆者にわかるように教えて頂きたいです。

LINQとは?

統合言語クエリ (LINQ, Language INtegrated Query, リンクと発音する)とは、.NET Framework 3.5において、様々な種類のデータ集合に対して標準化された方法でデータを問い合わせる(クエリ)ことを可能にするために、言語に統合された機能のことである。開発ツールはVisual Studio 2008から対応している。

らしいですよ。

.NET Framework 3.5がリリースされたのは、2007年11月19日なのでほぼ10年経ってますね。

知ったのも触ったのも最近なんで、もっと簡単に説明するとSQLクエリっぽくデータを捌けるって感じです。

良くわからないと思うので、問題ベースにコードを見てみましょう。

問題1

問題1は文字列と文字列を連結させます。

3人のdatasに「ちゃん」をつけてあげて出力してください。

問題1、問題2は、foreachでもLINQでも可とします。

foreachでもできますが、LINQの.Selectを使うともっと上手くできるはずです。

MellowYellow.cs
    /// <summary>
    /// メインエントリ
    /// </summary>
    /// <param name="args"></param>
    static void Main(string[] args)
    {
        // ----------------- 問題1--------------------
        Question1();
    }

    /// <summary>
    /// 問題1 : 難易度 DEBUT
    /// -----------------------------------
    /// [入力] : { "水本", "椎名", "中野" }
    /// [出力] : { "水本ちゃん", "椎名ちゃん", "中野ちゃん" }
    /// [説明] : 全部に「ちゃん」を付けてね!
    /// </summary>

    public static void Question1()
    {
        //input datas
        var datas = new[] { "水本", "椎名", "中野" };

        //ここに処理を追加してみて

        // ----------------- foreach版--------------------

        // ----------------- Linq版--------------------
        return;
    }

問題1の実行結果

1.png

問題1の回答

MellowYellow.cs
    /// <summary>
    /// メインエントリ
    /// </summary>
    /// <param name="args"></param>
    static void Main(string[] args)
    {
        // ----------------- 問題1--------------------
        Question1();
    }

    /// <summary>
    /// 問題1 : 難易度 DEBUT
    /// -----------------------------------
    /// [入力] : { "水本", "椎名", "中野" }
    /// [出力] : { "水本ちゃん", "椎名ちゃん", "中野ちゃん" }
    /// [説明] : 全部に「ちゃん」を付けてね!
    /// </summary>

    public static void Question1()
    {
        //input datas
        var datas = new[] { "水本", "椎名", "中野" };
        // ----------------- foreach版--------------------
        //do something(foreach)
        // 結果を受け取るリストを用意
        var forResultDatas = new List<string>();
        // ちゃん付けしてリストに入れる
        foreach (var data in datas)
            forResultDatas.Add(data + "ちゃん");
        //output datas
        // ちゃん付けで表示
        foreach (var forResultData in forResultDatas)
            Console.WriteLine(forResultData);

        // ----------------- Linq版--------------------
        //do something(LINQ)
        // ちゃん付けした列挙を取得
        var linqResultDatas = datas.Select(x => x + "ちゃん");
        // ちゃん付けで表示
        //output datas
        foreach (var linqResultData in linqResultDatas)
            Console.WriteLine(linqResultData);
    }

foreach版では、結果のListを用意するので行数が増えてることがわかると思います。

LINQ版では、.Selectが使えるので、文字列を連結した列挙を取得することができます。

問題2

さっきのに問題2を追加しました。

問題2は、文字列の末尾が"お"のデータだけを抽出します。

5人のデータから末尾が"お"の人を出力してください。

問題1、問題2は、foreachでもLINQでも可とします。

LINQ版では、.Whereを使えば上手に行くはずです。

MellowYellow.cs
    /// <summary>
    /// メインエントリ
    /// </summary>
    /// <param name="args"></param>
    static void Main(string[] args)
    {
        // ----------------- 問題1--------------------
        //Question1();
        // ----------------- 問題2--------------------
        Question2();
    }

    /// <summary>
    /// 問題2 : 難易度 DEBUT
    /// -----------------------------------
    /// [入力] : { "うづき", "りん", "みお", "みく", "なお" };
    /// [出力] : { "みお", "なお" }
    /// [説明] : "_お"っていう名前だけ取得してね!
    /// </summary>
    public static void Question2()
    {
        //input datas
        var datas = new[] { "うづき", "りん", "みお", "みく", "なお" };

        //ここに処理を追加してみて

        // ----------------- foreach版--------------------

        // ----------------- Linq版--------------------
        return;
    }

問題2の実行結果

2.png

問題2の回答

MellowYellow.cs
    /// <summary>
    /// メインエントリ
    /// </summary>
    /// <param name="args"></param>
    static void Main(string[] args)
    {
        // ----------------- 問題1--------------------
        //Question1();
        // ----------------- 問題2--------------------
        Question2();
    }

    /// <summary>
    /// 問題2 : 難易度 DEBUT
    /// -----------------------------------
    /// [入力] : { "うづき", "りん", "みお", "みく", "なお" };
    /// [出力] : { "みお", "なお" }
    /// [説明] : "_お"っていう名前だけ取得してね!
    /// </summary>
    public static void Question2()
    {
        //input datas
        var datas = new[] { "うづき", "りん", "みお", "みく", "なお" };

        // ----------------- foreach版--------------------
        //do something(foreach)
        // 結果を受け取るリストを用意
        var forResultDatas = new List<string>();
        foreach (var data in datas)
        {
            // 末尾が"お"のdataをListに入れる
            if (data.EndsWith("お"))
                forResultDatas.Add(data);
        }
        //output datas
        // 文字列の末尾が"お"のデータを表示
        foreach (var forResultData in forResultDatas)
            Console.WriteLine(forResultData);

        // ----------------- Linq版--------------------
        //do something(LINQ)
        // 文字列の末尾が"お"の列挙を取得
        var linqResultDatas = datas.Where(x => x.EndsWith("お"));
        //output datas
        // 文字列の末尾が"お"のデータを表示
        foreach (var linqResultData in linqResultDatas)
            Console.WriteLine(linqResultData);
    }

LINQ版の方がコードが短く、可読性も上がったことがわかると思います。

.Whereが使えるので、要素の中から指定した条件に合致するものを直感的に取り出せました。

問題3

さっきのに問題3を追加しました。

問題3は、LIKE検索を実装するためのコードです。

5人のdatasから検索をかけて絞り込みます。

この辺りからforeach版では苦しいので回答は、LINQのみの記載となってます。

MellowYellow.cs
    /// <summary>
    /// メインエントリ
    /// </summary>
    /// <param name="args"></param>
    static void Main(string[] args)
    {
        // ----------------- 問題1--------------------
        //Question1();
        // ----------------- 問題2--------------------
        //Question2();
        // ----------------- 問題3--------------------
        //引数は4パターン試してね! 
        //完全一致パターン
        var ret3a = Question3("りん");
        //表示
        foreach (var a in ret3a)
            Console.Write(a);
        //前方一致パターン
        var ret3b = Question3("み%");
        //表示
        foreach (var b in ret3b)
            Console.Write(b);
        //後方一致パターン
        var ret3c = Question3("%お");
        //表示
        foreach (var c in ret3c)
            Console.Write(c);
        //部分一致パターン
        var ret3d = Question3("%づき%");
        //表示
        foreach (var d in ret3d)
            Console.Write(d);
    }

    /// <summary>
    /// 問題3 : 難易度 REGULAR
    /// -----------------------------------
    /// [入力] : LIKE検索文字列
    /// [説明] : LIKE検索を実装しよう!
    ///          入力が「みお」の場合→ {"みお"}(完全一致)
    ///          入力が「み%」の場合→ {"みお","みく"}(前方一致)
    ///          入力が「%お」の場合→ {"みお","なお"}(後方一致)
    ///          入力が「%づき%」の場合→ {"うづき"}(部分一致)
    ///          ※それぞれ、IEnumerable型で返して下さい!
    /// </summary>
    /// <param name="v"></param>
    /// <returns></returns>
    private static IEnumerable<string> Question3(string value)
    {
        //data sources
        var datas = new[] { "うづき", "りん", "みお", "みく", "なお" };

        //ここに処理を追加してみて
        return null;
    }

問題3の実行結果

3.png

問題3の回答

MellowYellow.cs
    /// <summary>
    /// メインエントリ
    /// </summary>
    /// <param name="args"></param>
    static void Main(string[] args)
    {
        // ----------------- 問題1--------------------
        //Question1();
        // ----------------- 問題2--------------------
        //Question2();
        // ----------------- 問題3--------------------
        //引数は4パターン試してね! 
        //完全一致パターン
        var ret3a = Question3("りん");
        //表示
        foreach (var a in ret3a)
            Console.Write(a);
        //前方一致パターン
        var ret3b = Question3("み%");
        //表示
        foreach (var b in ret3b)
            Console.Write(b);
        //後方一致パターン
        var ret3c = Question3("%お");
        //表示
        foreach (var c in ret3c)
            Console.Write(c);
        //部分一致パターン
        var ret3d = Question3("%づき%");
        //表示
        foreach (var d in ret3d)
            Console.Write(d);
    }

    /// <summary>
    /// 問題3 : 難易度 REGULAR
    /// -----------------------------------
    /// [入力] : LIKE検索文字列
    /// [説明] : LIKE検索を実装しよう!
    ///          入力が「みお」の場合→ {"みお"}(完全一致)
    ///          入力が「み%」の場合→ {"みお","みく"}(前方一致)
    ///          入力が「%お」の場合→ {"みお","なお"}(後方一致)
    ///          入力が「%づき%」の場合→ {"うづき"}(部分一致)
    ///          ※それぞれ、IEnumerable型で返して下さい!
    /// </summary>
    /// <param name="v"></param>
    /// <returns></returns>
    private static IEnumerable<string> Question3(string value)
    {
        //data sources
        var datas = new[] { "うづき", "りん", "みお", "みく", "なお" };

        //部分一致
        //一文字目が%かつ末尾が%の場合
        if (value.StartsWith("%") && value.EndsWith("%"))
        {
            // valueから%を除いたものとdatasの中に存在してるのが一致してる列挙を取得
            var partMatchedCase = datas.Where(a => a.Contains(value.Trim('%')));
            foreach (var matchedD in partMatchedCase)
                yield return matchedD;
        }
        //前方一致
        //末尾が%の場合
        else if (value.EndsWith("%"))
        {
            // valueから%を除いたものとdatasの中に存在してる一文字目が一致してる列挙を取得
            var forwardMatchedCase = datas.Where(b => b.StartsWith(value.Trim('%')));
            foreach (var matchedB in forwardMatchedCase)
                yield return matchedB;
        }
        //後方一致
        //一文字目が%の場合
        else if (value.StartsWith("%"))
        {
            // valueから%を除いたものとdatasの中に存在してる末尾が一致してる列挙を取得
            var backMatchedCase = datas.Where(c => c.EndsWith(value.Trim('%')));
            foreach (var matchedC in backMatchedCase)
                yield return matchedC;

        }
        //完全一致
        // それ以外
        else
        {
            // valueとdatasの中に存在してる文字列が一致してる列挙を取得
            var matchedCase = datas.Where(d => d.Equals(value));
            foreach (var matchedA in matchedCase)
                yield return matchedA;
        }

    }

アルゴリズムを考えるのが難しい問題ですね。

データの検索が簡単で見やすくできるのは捗りますね。

問題4

さっきのに問題4を追加しました。

問題4は、列挙を「分割」する方法ですね。

今回は、cuteを2個づつにしたいです。

MellowYellow.cs
    /// <summary>
    /// メインエントリ
    /// </summary>
    /// <param name="args"></param>
    static void Main(string[] args)
    {
        // ----------------- 問題1--------------------
        //Question1();
        // ----------------- 問題2--------------------
        //Question2();
        // ----------------- 問題3--------------------
        //引数は4パターン試してね! 
        //完全一致パターン
        //var ret3a = Question3("りん");
        //表示
        //foreach (var a in ret3a)
        //  Console.Write(a);
        //前方一致パターン
        //var ret3b = Question3("み%");
        //表示
        //foreach (var b in ret3b)
        //  Console.Write(b);
        //後方一致パターン
        //var ret3c = Question3("%お");
        //表示
        //foreach (var c in ret3c)
        //  Console.Write(c);
        //部分一致パターン
        //var ret3d = Question3("%づき%");
        //表示
        //foreach (var d in ret3d)
        //  Console.Write(d);
        // ----------------- 問題4--------------------
        // 列挙を作るよ
        IEnumerable<string> cute = new[] { "まゆ", "ちえり", "ゆかり", "きょうこ", "さえ" };
        // 分割した列挙を入れるよ
        var ret4a = Divide(cute, 2);
        // 表示
        foreach (var r in ret4a)
            Console.WriteLine(string.Join(",", r));

    }


    /// <summary>
    /// 問題4 難易度 : REGULAR
    /// -----------------------------------
    /// [説明] : 列挙を「分割」してみよう。
    /// [入力] : datas = {"まゆ","ちえり","ゆかり","きょうこ","さえ"},
    ///          value = 2 (区切る単位の数)
    /// [出力] :   {"まゆ,"ちえり"},
    ///            {"ゆかり","きょうこ"},
    ///            {"さえ"}
    /// </summary>
    /// <param name="value"></param>
    /// <returns></returns>

    private static IEnumerable<IEnumerable<string>> Divide(IEnumerable<string> datas, int value)
    {   
        //ここに処理を追加してみて
        return null;

    }

問題4の実行結果

4.png

問題4の回答

MellowYellow.cs
    /// <summary>
    /// メインエントリ
    /// </summary>
    /// <param name="args"></param>
    static void Main(string[] args)
    {
        // ----------------- 問題1--------------------
        //Question1();
        // ----------------- 問題2--------------------
        //Question2();
        // ----------------- 問題3--------------------
        //引数は4パターン試してね! 
        //完全一致パターン
        //var ret3a = Question3("りん");
        //表示
        //foreach (var a in ret3a)
        //  Console.Write(a);
        //前方一致パターン
        //var ret3b = Question3("み%");
        //表示
        //foreach (var b in ret3b)
        //  Console.Write(b);
        //後方一致パターン
        //var ret3c = Question3("%お");
        //表示
        //foreach (var c in ret3c)
        //  Console.Write(c);
        //部分一致パターン
        //var ret3d = Question3("%づき%");
        //表示
        //foreach (var d in ret3d)
        //  Console.Write(d);
        //Console.WriteLine();
        // ----------------- 問題4--------------------
        // 列挙を作るよ
        IEnumerable<string> cute = new[] { "まゆ", "ちえり", "ゆかり", "きょうこ", "さえ" };
        // 分割した列挙を入れるよ
        var ret4a = Divide(cute, 2);
        // 表示
        foreach (var r in ret4a)
            Console.WriteLine(string.Join(",", r));

    }

    /// <summary>
    /// 問題4 難易度 : REGULAR
    /// -----------------------------------
    /// [説明] : 列挙を「分割」してみよう。
    /// [入力] : datas = {"まゆ","ちえり","ゆかり","きょうこ","さえ"},
    ///          value = 2 (区切る単位の数)
    /// [出力] :   {"まゆ,"ちえり"},
    ///            {"ゆかり","きょうこ"},
    ///            {"さえ"}
    /// </summary>
    /// <param name="value"></param>
    /// <returns></returns>

    private static IEnumerable<IEnumerable<string>> Divide(IEnumerable<string> datas, int value)
    {   
        // datesがある間ループ
        while (datas.Any())
        {
            // valueの分だけデータを貰って
            yield return datas.Take(value);
            // valueの分だけスキップ
            datas = datas.Skip(value);
        }
    }

私は最初、遅延評価に悩まされました。

とはいえ、遅延評価は「利用されるタイミングまで実行されない」のでメモリの節約になって良いですね。

処理としては、.Take.Skipを使いました。

.Takeは、要素を指定した数だけ読みます。

.Skipは、要素を指定した数だけ読み飛ばします。

組み合わせることでインデックスを使わずにインデックスっぽく捌くことができました。

インデックスを使ってないので、IndexOutOfRangeException等の例外が出る可能性がありません。

これもデータを捌く上で大きなメリットだと思います。

問題5

問題4で作成したDivideをExtensionsクラスに切り出して、IEnumerableの拡張メソッドにしよう!

ジェネリック(<T>)型に対応させ、string以外の型でも処理できるようにしよう!

返却する最後の列挙数が指定したvalue数に満たない場合、切り捨てを行う処理を追加する。

(例えば、問題4の例でいうと{"さえ"}の個数は指定したvalue(2個)に満たないので、切り捨てることとする。)

MellowYellow.cs
    /// <summary>
    /// メインエントリ
    /// </summary>
    /// <param name="args"></param>
    static void Main(string[] args)
    {
        // ----------------- 問題1--------------------
        //Question1();
        // ----------------- 問題2--------------------
        //Question2();
        // ----------------- 問題3--------------------
        //引数は4パターン試してね! 
        //完全一致パターン
        //var ret3a = Question3("りん");
        //表示
        //foreach (var a in ret3a)
        //  Console.Write(a);
        //前方一致パターン
        //var ret3b = Question3("み%");
        //表示
        //foreach (var b in ret3b)
        //  Console.Write(b);
        //後方一致パターン
        //var ret3c = Question3("%お");
        //表示
        //foreach (var c in ret3c)
        //  Console.Write(c);
        //部分一致パターン
        //var ret3d = Question3("%づき%");
        //表示
        //foreach (var d in ret3d)
        //  Console.Write(d);
        //Console.WriteLine();
        // ----------------- 問題4--------------------
        // 列挙を作るよ
        //IEnumerable<string> cute = new[] { "まゆ", "ちえり", "ゆかり", "きょうこ", "さえ" };
        // 分割した列挙を入れるよ
        //var ret4a = Divide(cute, 2);
        // 表示
        //foreach (var r in ret4a)
        //  Console.WriteLine(string.Join(",", r));
        // ----------------- 問題5--------------------
        // LiPPS ? をてきとーな数に分けてくれー!
        var LiPPS = new[] { "しき", "しゅうこ", "かなで", "高田純次", "みか" };
        // 表示
        foreach (var r in LiPPS)
            Console.WriteLine(string.Join(",", r));

    }
    public static class Extension
    {
        //作ったDivideを
        //①Extensionsクラスに切り出して、IEnumerableの拡張メソッドにする
        //②ジェネリック(<T>)型に対応させ、string以外の型でも処理できるようにする
        // 返却する最後の列挙数が指定したvalue数に満たない場合、
        // 切り捨てを行う処理を追加する。
        // (例えば、前回の例でいうと{"さえ"}の個数は指定したvalue(2個)に満たないので、
        //  切り捨てることとする)

        /// <summary>        
        /// 問題5 難易度 : REGULER+
        /// 対象のシーケンスを分割、余りは切り捨て
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="datas"></param>
        /// <param name="value"></param>
        /// <returns></returns>
        public static IEnumerable<IEnumerable<T>> Divide<T>(this IEnumerable<T> datas, int value)
        {
            // ここに処理を書いて
            return null;
        }
    }

問題5の実行結果

3づつ分割した結果

5.png

問題5の回答

MellowYellow.cs
    /// <summary>
    /// メインエントリ
    /// </summary>
    /// <param name="args"></param>
    static void Main(string[] args)
    {
        // ----------------- 問題1--------------------
        //Question1();
        // ----------------- 問題2--------------------
        //Question2();
        // ----------------- 問題3--------------------
        //引数は4パターン試してね! 
        //完全一致パターン
        //var ret3a = Question3("りん");
        //表示
        //foreach (var a in ret3a)
        //  Console.Write(a);
        //前方一致パターン
        //var ret3b = Question3("み%");
        //表示
        //foreach (var b in ret3b)
        //  Console.Write(b);
        //後方一致パターン
        //var ret3c = Question3("%お");
        //表示
        //foreach (var c in ret3c)
        //  Console.Write(c);
        //部分一致パターン
        //var ret3d = Question3("%づき%");
        //表示
        //foreach (var d in ret3d)
        //  Console.Write(d);
        //Console.WriteLine();
        // ----------------- 問題4--------------------
        // 列挙を作るよ
        //IEnumerable<string> cute = new[] { "まゆ", "ちえり", "ゆかり", "きょうこ", "さえ" };
        // 分割した列挙を入れるよ
        //var ret4a = Divide(cute, 2);
        // 表示
        //foreach (var r in ret4a)
        //  Console.WriteLine(string.Join(",", r));
        // ----------------- 問題5--------------------
        // LiPPS ? わぁお!ここで拡張メソッド呼べちゃう! 
        // .Divide(3)っていうのもわかりやすい!
        var LiPPS = new[] { "しき", "しゅうこ", "かなで", "高田純次", "みか" }.Divide(3);
        // 表示
        foreach (var r in LiPPS)
            Console.WriteLine(string.Join(",", r));

    }
    public static class Extension
    {
        // 作ったDivideを
        // Extensionsクラスに切り出して、IEnumerableの拡張メソッドにする
        // ジェネリック(<T>)型に対応させ、string以外の型でも処理できるようにする
        // 返却する最後の列挙数が指定したvalue数に満たない場合、
        // 切り捨てを行う処理を追加する。
        // (例えば、今回の例でいうと{"さえ"}の個数は指定したvalue(2個)に満たないので、
        //  切り捨てることとする)

        /// <summary>   
        /// 問題5 難易度 : REGULER+
        /// 対象のシーケンスを分割、余りは切り捨て
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="datas"></param>
        /// <param name="value"></param>
        /// <returns></returns>
        public static IEnumerable<IEnumerable<T>> Divide<T>(this IEnumerable<T> datas, int value)
        {
            if (datas == null)
                // そもそも分割するdatasが無いのはおかしくね? 
                // Exceptionを出して、使った人に告知してあげようかな
                throw new ArgumentNullException();

            // 要素が無いとbreak
            if (!datas.Any())
                yield break;

            // value分だけをyield return
            yield return datas.Take(value);

            // 上の処理でTakeした分だけSkipしたものから再帰して自分REST@RT
            foreach (var s in datas.Skip(value).Divide(value))
                // きちんと分割できるか判定
                if (value == s.Count())
                    // 分割したものを各自yield return
                    yield return s;
        }
    }

拡張クラスを作ることで車輪の再開発も減ると思います。

業務でも使う機会が多いと思いますので、ぜひ使えるようになりましょう。

まとめ

LINQ素敵!

とっつきやすい内容なのでGitHubからもってきて、ぜひ、やってみてください!