C#再入門 はじめてのLINQ その1 IEnumerableの主要メソッド

masakiが2016/06/08 18:39:43に投稿

はじめに

C#はver1.1(VisualStudio2003)の頃に触ったきりで、それ以降C#がどのように進化したかを知らないでいた。
今回、触れる機会があったのでその進化した分を学習する。

LINQ

LINQとは

以下、Wikipedia 統合言語クエリより抜粋。

統合言語クエリ (LINQ, Language INtegrated Query, リンクと発音する)とは、.NET Framework 3.5において、様々な種類のデータ集合に対して標準化された方法でデータを問い合わせる(クエリ)ことを可能にするために、言語に統合された機能のことである。開発ツールはVisual Studio 2008から対応している。
言語仕様
LINQに対応する言語は、LINQ の能力をより発揮させるために新しい言語仕様が併せて導入されている。例えば、クエリ式、拡張メソッド、ラムダ式、匿名型などがそうである。C# における例は C# 3.0からの仕様 を参照されたい。
LINQはサードパーティによるものを含め、あらゆる種類のデータソースに対して適用することができる。これは、標準クエリ演算子に対応する機能を拡張メソッドとしてデータソースに追加することで実現している。
従来では同種のデータ型やオブジェクトの集合に対して列挙やソート、フィルタを効率的に扱うために配列 (Arrayクラス) やコレクションオブジェクトが用いられた。一方、データベースやXML上のデータ集合はADO.NETによってデータセットとして取り扱われており異なる操作が必要であった。LINQによって、これらのオブジェクトやデータセットを区別せず共通的に扱うことが可能となった。
例えば、マイクロソフトによるものでは次のような実装がある。
LINQ to Objects (あらゆるコレクション/列挙子をLINQクエリで操作可能にする)
LINQ to XML (XLinq)
LINQ to SQL (DLinq / SQL Server専用)

以下、MSDN LINQ to Objectsより抜粋。

"LINQ to Objects" という用語は、LINQ to SQL や LINQ to XML などの中間 LINQ プロバイダーまたは API を使用せずに、LINQ クエリを任意の IEnumerable コレクションまたは IEnumerable<T> コレクションと直接組み合わせて使用することを意味します。LINQ を使用して、List<T>、Array、または Dictionary<TKey, TValue> などの任意の列挙可能なコレクションを照会できます。このコレクションは、ユーザー定義のコレクションでも、.NET Framework API から返されたコレクションでもかまいません。


つまり、配列やコレクションオブジェクトもデータベース上のデータも区別しないで、 IEnumerable コレクションまたは IEnumerable コレクションで扱うことができるようになった。
 
クエリ式とメソッド式

LINQはクエリ構文とメソッド構文で記述することができる。ざっくり比較すると

  • クエリ式は単なる糖衣構文にすぎない。
  • メソッド構文ではラムダ式を使える。

(参考)


ラムダ式が使えると便利なのでここではメソッド構文で書くことにする。

LINQtoObjectsの例

    public class Person : IEquatable<Person>
    {
        public string Name { get; set; }
        public int Age { get; set; }

        public bool Equals(Person o)
        {
            return this.Name == o.Name && this.Age == o.Age;
        }

        public Person Older(Person o)
        {
            return this.Age >= o.Age ? this : o;
        }
    }

    public class Part : IEquatable<Part>
    {
        public string Name { get; set; }
        public List<string> Instrument { get; set; }

        public bool Equals(Part o)
        {
            // Partはソートしてから比較
            return this.Name == o.Name && this.Instrument.OrderBy(_ => _).SequenceEqual(o.Instrument.OrderBy(_ => _));
        }
    }

    public class Member : IEquatable<Member>
    {
        public string Name { get; set; }
        public List<string> Instrument { get; set; }
        public int Age { get; set; }
        public bool Equals(Member o)
        {
            return this.Name == o.Name && this.Age == o.Age && this.Instrument.OrderBy(_ => _).SequenceEqual(o.Instrument.OrderBy(_ => _));
        }

    }

 

IEnumerable<T>インターフェースを実装したList<T>クラスで上記クラスのコレクションを作成し、VisualStudio2015の単体テストフレームワークMicrosoft.VisualStudio.TestTools.UnitTestingを使って、IEnumerableの主要メソッドの動作を確認する。
 

using Microsoft.VisualStudio.TestTools.UnitTesting;
using ConsoleApplication1;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ConsoleApplication1.Tests
{
    [TestClass()]
    public class PersonTests
    {
        private Person john40 = new Person { Name = "John", Age = 40 };
        private Person paul73 = new Person { Name = "Paul", Age = 73 };
        private Person george58 = new Person { Name = "George", Age = 58 };
        private Person ringo75 = new Person { Name = "Ringo", Age = 75 };
        private Person sean40 = new Person { Name = "Sean", Age = 40 };
        private Person yoko83 = new Person { Name = "Yoko", Age = 83 };

        private List<Person> persons;
        string rythmGuitar = "rhythm guitar";
        string bass = "bass";
        string leadGuitar = "lead guitar";
        string drums = "drums";

        private Part johnPart;
        private Part paulPart;
        private Part georgePart;
        private Part ringoPart;

        [TestInitialize]
        public void TestInitialize()
        {
            persons = new List<Person> { john40, paul73, george58, ringo75 };
            johnPart = new Part { Name = "John", Instrument = new List<string> { rythmGuitar, leadGuitar, bass } };
            paulPart = new Part { Name = "Paul", Instrument = new List<string> { bass, leadGuitar } };
            georgePart = new Part { Name = "George", Instrument = new List<string> { leadGuitar, rythmGuitar, } };
            ringoPart = new Part { Name = "Ringo", Instrument = new List<string> { drums, } };
        }

        [TestMethod()]
        public void PersonEqualsTest()
        {
            Assert.IsTrue(john40.Equals(john40));
            Assert.IsFalse(john40.Equals(paul73));
        }

        [TestMethod()]
        public void PersonPartEqualsTest()
        {
            Assert.IsTrue(johnPart.Equals(johnPart));
            Assert.IsFalse(johnPart.Equals(paulPart));
        }

        // 順序

        [TestMethod()]
        public void リスト1とリスト2の順序が同じであることを確認するTest()
        {
            Assert.IsTrue(persons.SequenceEqual(new List<Person> { john40, paul73, george58, ringo75 }));
        }

        [TestMethod()]
        public void リストを逆順に並べ替える()
        {
            Assert.IsTrue(persons.AsEnumerable().Reverse().SequenceEqual(new List<Person> { ringo75, george58, paul73, john40 }));
        }

        [TestMethod()]
        public void リストを特定の条件で並べ替えるTest()
        {
            Assert.IsTrue(persons.OrderBy(_ => _.Name).SequenceEqual(new List<Person> { george58, john40, paul73, ringo75, }));
            Assert.IsTrue(persons.OrderByDescending(_ => _.Name).SequenceEqual(new List<Person> { ringo75, paul73, john40, george58, }));
        }

        [TestMethod()]
        public void リストを複数の条件で並べ替えるTest()
        {
            var george17 = new Person { Name = "George", Age = 17 };
            persons.Add(george17);
            Assert.IsTrue(persons.OrderBy(_ => _.Name).ThenBy(_ => _.Age).SequenceEqual(new List<Person> { george17, george58, john40, paul73, ringo75, }));
            Assert.IsTrue(persons.OrderBy(_ => _.Name).ThenByDescending(_ => _.Age).SequenceEqual(new List<Person> { george58, george17, john40, paul73, ringo75, }));
        }

        // 集計

        [TestMethod()]
        public void リストの要素を数えるTest()
        {
            Assert.AreEqual(persons.Count(), 4);
        }

        [TestMethod()]
        public void リストの要素の特定フィルドの最大値を求めるTest()
        {
            Assert.IsTrue(persons.Max(_ => _.Age).Equals(ringo75.Age));
        }

        [TestMethod()]
        public void リストの要素の特定フィルドの最小値を求めるTest()
        {
            Assert.IsTrue(persons.Min(_ => _.Age).Equals(john40.Age));
        }

        [TestMethod()]
        public void リストの要素の特定フィルドの合計を求めるTest()
        {
            Assert.IsTrue(persons.Sum(_ => _.Age).Equals(john40.Age + paul73.Age + george58.Age + ringo75.Age));
        }

        [TestMethod()]
        public void リストの要素の特定フィルドの平均値を求めるTest()
        {
            Assert.IsTrue(persons.Average(_ => _.Age).Equals(((double)persons.Sum(_ => _.Age)) / 4));
        }

        [TestMethod()]
        public void リストの要素を先頭から畳み込むTest()
        {
            Assert.IsTrue(persons.Aggregate((_, _next) => _.Older(_next)).Equals(ringo75));
        }

        // 抽出

        [TestMethod()]
        public void リストから条件に合う要素を抽出してリストにするTest()
        {
            Assert.IsTrue(persons.Where(_ => _.Name == george58.Name).SequenceEqual(new List<Person> { george58 }));
        }


        [TestMethod()]
        public void リストから先頭要素を取り出すTest()
        {
            Assert.IsTrue(persons.First().Equals(john40));

            // 条件に合う先頭要素を取り出す
            persons.Add(sean40);
            Assert.IsTrue(persons.First(_ => _.Age == sean40.Age).Equals(john40));

            // 条件に合う要素が見つからなければ例外を起こす
            try
            {
                persons.First(_ => _.Age < 10);
                Assert.Fail();
            }
            catch { Assert.IsTrue(true); }

            // 条件に合う先頭要素が見つからなければNullを返す
            Assert.IsTrue(persons.FirstOrDefault(_ => _.Age == sean40.Age).Equals(john40));
            Assert.IsNull(persons.FirstOrDefault(_ => _.Age < 10));
        }

        [TestMethod()]
        public void リストから末尾要素を取り出すTest()
        {
            Assert.IsTrue(persons.Last().Equals(ringo75));

            // 条件に合う末尾要素を取り出す
            persons.Add(sean40);
            Assert.IsTrue(persons.Last(_ => _.Age == sean40.Age).Equals(sean40));

            // 条件に合う要素が見つからなければ例外を起こす
            try
            {
                persons.Last(_ => _.Age < 10);
                Assert.Fail();
            }
            catch { Assert.IsTrue(true); }

            // 条件に合う末尾要素が見つからなければNullを返す
            Assert.IsTrue(persons.LastOrDefault(_ => _.Age == sean40.Age).Equals(sean40));
            Assert.IsNull(persons.LastOrDefault(_ => _.Age < 10));
        }

        [TestMethod()]
        public void リストから任意位置の要素を取り出すTest()
        {
            Assert.IsTrue(persons.ElementAt(2).Equals(george58));

            // 任意位置の要素が見つからなければ例外を起こす
            try
            {
                new List<Person> { george58 }.ElementAt(2);
                Assert.Fail();
            }
            catch { Assert.IsTrue(true); }

            // 任意位置の要素が見つからなければNullを返す
            Assert.IsTrue(persons.AsEnumerable().ElementAtOrDefault(2).Equals(george58));
            Assert.IsNull(new List<Person> { george58 }.ElementAtOrDefault(2));

        }

        [TestMethod()]
        public void リストから唯一の要素を取り出すTest()
        {
            Assert.IsTrue(persons.Single(_ => _.Name == george58.Name).Equals(george58));

            // 一つだけでなければ例外を起こす
            try {
                persons.Single(_ => _.Name != george58.Name);
                Assert.Fail();
            } catch { Assert.IsTrue(true); }
        }

        [TestMethod()]
        public void リストの特定件数読み飛ばしてリストにするTest()
        {
            Assert.IsTrue(persons.Skip(2).SequenceEqual(new List<Person> { /*john40, paul73,*/ george58, ringo75, }));
        }

        [TestMethod()]
        public void リストの特定条件を満たすあいだ読み飛ばしてリストにするTest()
        {
            Assert.IsTrue(persons.SkipWhile(_ => _.Age != george58.Age).SequenceEqual(new List<Person> { /*john40, paul73,*/ george58, ringo75, }));
        }

        [TestMethod()]
        public void リストの先頭から特定件数だけリストにするTest()
        {
            Assert.IsTrue(persons.Take(2).SequenceEqual(new List<Person> { john40, paul73, /*george58, ringo75,*/ }));
        }

        [TestMethod()]
        public void リストの先頭から特定条件をみたすあいだリストにするTest()
        {
            Assert.IsTrue(persons.TakeWhile(_ => _.Age != george58.Age).SequenceEqual(new List<Person> { john40, paul73, /*george58, ringo75,*/ }));
        }

        [TestMethod()]
        public void リストが空の場合はデフォルト値を詰めたリストを返すTest()
        {
            Assert.IsTrue(new List<Person> { }.DefaultIfEmpty(yoko83).SequenceEqual(new List<Person> { yoko83 }));
            Assert.IsFalse(persons.DefaultIfEmpty(yoko83).SequenceEqual(new List<Person> { yoko83 }));
        }


        // 存在確認

        [TestMethod()]
        public void リストに指定要素が含まれているかを確認するTest()
        {
            var yoko83 = new Person { Name = "Yoko", Age = 83 };
            Assert.IsTrue(persons.Contains(john40));
            Assert.IsFalse(persons.Contains(yoko83));
        }

        [TestMethod()]
        public void リストの指定した条件の要素が1つ以上存在することを確認するTest()
        {
            Assert.IsTrue(persons.Any(_ => _.Age > 60)); // paul73,ringo75
            Assert.IsFalse(persons.Any(_ => _.Age < 10));
        }

        [TestMethod()]
        public void リストのすべての要素が指定した条件に合致することを確認するTest()
        {
            Assert.IsTrue(persons.All(_ => _.Age >= 40));
            Assert.IsFalse(persons.All(_ => _.Age < 50)); // john40
        }

        // 連結、ユニーク、和、差、積

        [TestMethod()]
        public void リスト1とリスト2を連結するTest()
        {
            var persons2 = new List<Person> { sean40, yoko83, john40 };
            // personsとpersons2を連結した結果、john40が重複している
            Assert.IsTrue(persons.Concat(persons2).SequenceEqual(new List<Person> { john40, paul73, george58, ringo75, sean40, yoko83, john40 }));
        }

        [TestMethod()]
        public void リストから重複要素を削除するTest()
        {
            var persons2 = new List<Person> { sean40, yoko83, sean40 }; // sean40が重複
            Assert.IsTrue(persons2.Distinct().SequenceEqual(new List<Person> { sean40, yoko83 }));
        }

        [TestMethod()]
        public void リスト1とリスト2の和集合Test()
        {
            var persons2 = new List<Person> { sean40, yoko83, john40 };
            // personsとpersons2を連結し重複を除く
            Assert.IsTrue(persons.Union(persons2).SequenceEqual(new List<Person> { john40, paul73, george58, ringo75, sean40, yoko83, }));
        }

        [TestMethod()]
        public void リスト1とリスト2の差集合Test()
        {
            var persons2 = new List<Person> { sean40, yoko83, john40 };
            // persons2に含まれないpersonsの要素
            Assert.IsTrue(persons.Except(persons2).SequenceEqual(new List<Person> { paul73, george58, ringo75, }));
        }

        [TestMethod()]
        public void リスト1とリスト2の積集合Test()
        {
            var persons2 = new List<Person> { sean40, yoko83, john40 };
            // persons2に含まれるpersonsの要素
            Assert.IsTrue(persons.Intersect(persons2).SequenceEqual(new List<Person> { john40, }));
        }

        // 新たなリストを作る

        [TestMethod()]
        public void リストの各要素を加工してリストにするTest()
        {
            var correct = new List<Tuple<String, int>> {
                    new Tuple<String, int>(john40.Name, john40.Age),
                    new Tuple<String, int>(paul73.Name, paul73.Age),
                    new Tuple<String, int>(george58.Name, george58.Age),
                    new Tuple<String, int>(ringo75.Name, ringo75.Age),
                };
            Assert.IsTrue(persons.Select(_ => new Tuple<String, int>(_.Name, _.Age))
                .SequenceEqual(correct));
        }

        [TestMethod()]
        public void リスト1とリスト2の同じインデックス要素をつきあわせてリスト3にするTest()
        {
            var parts = new List<string> { rythmGuitar, bass, leadGuitar, drums, };
            var correct = new List<Tuple<string, int, string>> {
                    new Tuple<string, int, string>(john40.Name, john40.Age, rythmGuitar),
                    new Tuple<string, int, string>(paul73.Name, paul73.Age, bass),
                    new Tuple<string, int, string>(george58.Name, george58.Age, leadGuitar),
                    new Tuple<string, int, string>(ringo75.Name, ringo75.Age, drums),
            };

            // personsとpartsの同じインデックス要素同士をつきあわせる
            Assert.IsTrue(persons.Zip(parts, (p, i) => new Tuple<string, int, string>(p.Name, p.Age, i))
                .SequenceEqual(correct));

        }

        [TestMethod()]
        public void リストの要素のリストを展開するTest()
        {
            var parts = new List<Part> { johnPart, paulPart, georgePart, ringoPart };
            var correct = johnPart.Instrument.Concat(paulPart.Instrument.Concat(georgePart.Instrument.Concat(ringoPart.Instrument)));
            Assert.IsTrue(parts.SelectMany(_ => _.Instrument).SequenceEqual(correct));
        }

        [TestMethod()]
        public void リスト1とリスト2をつき合わせるTest()
        {
            var parts1 = new List<Part> { johnPart, paulPart, georgePart, ringoPart };
            var correct1 = new List<Member> {
                new Member { Name = john40.Name, Age = john40.Age, Instrument = johnPart.Instrument },
                new Member { Name = paul73.Name, Age = paul73.Age, Instrument = paulPart.Instrument },
                new Member { Name = george58.Name, Age = george58.Age, Instrument = georgePart.Instrument },
                new Member { Name = ringo75.Name, Age = ringo75.Age, Instrument = ringoPart.Instrument },
            };

            // persons1とpartsをNameでつきあわせる
            var result1 = persons.Join(
                parts1,
                _ => _.Name,
                _ => _.Name,
                (prs, pts) => new Member { Name = prs.Name, Age = prs.Age, Instrument = pts.Instrument }
                );
            Assert.IsTrue(result1.SequenceEqual(correct1));

            // Personはsean40, george58, paul73, ringo75
            // PartはjohnPart, paulPart, georgePart
            // の場合(2つのリストの要素の数も順序も異なる)
            var correct2 = new List<Member> {
                new Member { Name = paul73.Name, Age = paul73.Age, Instrument = paulPart.Instrument },
                new Member { Name = george58.Name, Age = george58.Age, Instrument = georgePart.Instrument },
            };

            var result2 = new List<Person> { sean40, george58, paul73, ringo75 }.Join(
                new List<Part> { johnPart, paulPart, georgePart },
                _ => _.Name,
                _ => _.Name,
                (prs, pts) => new Member { Name = prs.Name, Age = prs.Age, Instrument = pts.Instrument }
                );
            Assert.IsTrue(result2.OrderBy(_ => _.Name).SequenceEqual(correct2.OrderBy(_ => _.Name)));
        }

        // Groupby
        // GroupJoin
        // についてはまた次回
    }
}