C#再入門 はじめてのLINQ その2 LINQのJoinとGroupJoinの違い

masakiが2016/06/08 18:41:04に投稿

LINQのJoinとGroupJoinの違い

LINQのJoinは何度も使っているうちにだいたい感触がつかめたが、Joinは内部結合しかできない。
LINQで外部結合するにはどうすればよいのかぐぐってみると「GroupJoinを使え」と出てきた。
前回IEnumerableの主要メソッドの解説をした時にGroupJoinについては後回しだった。
ちょうどよい機会なのでGroupJoinの使い方とJoinとの違いついて実際プログラムを書いて確かめた。

サンプルプログラム

登場するクラスは以下のとおり。

    /// <summary>
    /// 楽器
    /// </summary>
    public class Instrument : HasId
    {
        public Instrument() : base() { } 
    }

    /// <summary>
    /// 人
    /// </summary>
    public class Person : HasId
    {
        public Person() : base() { }
    }

    /// <summary>
    /// 担当楽器
    /// </summary>
    public class InstrumentPlayer : IEquatable<InstrumentPlayer>
    {
        public Person Person { get; set; }
        public Instrument Instrument  { get; set; }
        public bool Equals(InstrumentPlayer o)
        {
            return (this.Person == o.Person) && (this.Instrument == o.Instrument);
        }
    }
  • Instrument ... 楽器を表す。
  • Person ... 人を表す。
  • InstrumentPlayer ... 担当楽器を表す。楽器と人を結びつける。

テスト準備。

    [TestClass()]
    public class InstrumentPlayerTests
    {
        // 担当楽器は以下のとおり
        // John : RythmGuitar, LeadGuitar, Bass
        // Paul : LeadGuitar, Bass
        // George : RythmGuitar, LeadGuitar
        // Ringo : Drums
        // Yoko :  (担当楽器なし)
        // 
        // 和太鼓の担当はいない。

        Instrument rythmGuitar = new Instrument { Name = "RythmGuitar" };
        Instrument leadGuitar = new Instrument { Name = "LeadGuitar" };
        Instrument bass = new Instrument { Name = "Bass" };
        Instrument drums = new Instrument { Name = "Drums" };
        Instrument wadaiko = new Instrument { Name = "和太鼓" };
        List<Instrument> instruments;
        Person john = new Person { Name = "John" };
        Person paul = new Person { Name = "Paul" };
        Person george = new Person { Name = "George" };
        Person ringo = new Person { Name = "Ringo" };
        Person yoko = new Person { Name = "Yoko" };
        List<Person> persons;
        List<InstrumentPlayer> instrumentPlayers;

        [TestInitialize]
        public void TestInitialize()
        {
            instruments = new List<Instrument> { rythmGuitar, leadGuitar, bass, drums, wadaiko };
            persons = new List<Person> { john, paul, george, ringo, yoko };
            instrumentPlayers = new List<InstrumentPlayer> {
                new InstrumentPlayer { Person = john, Instrument = rythmGuitar},
                new InstrumentPlayer { Person = john, Instrument = leadGuitar},
                new InstrumentPlayer { Person = john, Instrument = bass},
                new InstrumentPlayer { Person = paul, Instrument = leadGuitar},
                new InstrumentPlayer { Person = paul, Instrument = bass},
                new InstrumentPlayer { Person = george, Instrument = rythmGuitar},
                new InstrumentPlayer { Person = george, Instrument = leadGuitar},
                new InstrumentPlayer { Person = ringo, Instrument = drums},
            };
        }

Instrument、Person、InstrumentPlayerにデータを投入。

personsとinstrumentPlayers、instrumentsとinstrumentPlayersを
Join(内部結合)させてみる。

        [TestMethod()]
        public void リストとリストをつき合わせる_内部結合Test()
        {
            var personPlay = persons // personsと
                .Join(instrumentPlayers,// instrumentPlayersをつきあわせ、
                    _ => _.Id,
                    _ => _.Person.Id, // お互いに存在するPersonについて要素を抽出し
                    (Person, itp) => new { Person, itp.Instrument }) // 新しいオブジェクトを作り出し
                .OrderBy(_ => _.Person.Id).ThenBy(_ => _.Instrument.Id); // PeronのId、InstrumentのIdをキーにして並べる。

            Assert.AreEqual(personPlay.Count(), 8);
            Assert.AreEqual(personPlay.ElementAt(0).Person, john);
            Assert.AreEqual(personPlay.ElementAt(0).Instrument, rythmGuitar);
            Assert.AreEqual(personPlay.ElementAt(1).Person, john);
            Assert.AreEqual(personPlay.ElementAt(1).Instrument, leadGuitar);
            Assert.AreEqual(personPlay.ElementAt(2).Person, john);
            Assert.AreEqual(personPlay.ElementAt(2).Instrument, bass);
            Assert.AreEqual(personPlay.ElementAt(3).Person, paul);
            Assert.AreEqual(personPlay.ElementAt(3).Instrument, leadGuitar);
            Assert.AreEqual(personPlay.ElementAt(4).Person, paul);
            Assert.AreEqual(personPlay.ElementAt(4).Instrument, bass);
            Assert.AreEqual(personPlay.ElementAt(5).Person, george);
            Assert.AreEqual(personPlay.ElementAt(5).Instrument, rythmGuitar);
            Assert.AreEqual(personPlay.ElementAt(6).Person, george);
            Assert.AreEqual(personPlay.ElementAt(6).Instrument, leadGuitar);
            Assert.AreEqual(personPlay.ElementAt(7).Person, ringo);
            Assert.AreEqual(personPlay.ElementAt(7).Instrument, drums);

            var InstrumentPlayedBy = instruments // instrumentsと
                .Join(instrumentPlayers, // instrumentPlayersをつきあわせ、
                    _ => _.Id,
                    _ => _.Instrument.Id, // お互いに存在するInstrumentについて要素を抽出し
                    (Instrument, itp) => new { Instrument, itp.Person }) // 新しいオブジェクトを作り出し
                .OrderBy(_ => _.Instrument.Id).ThenBy(_ => _.Person.Id); // InstrumentのId、PeronのIdをキーにして並べる。

            Assert.AreEqual(InstrumentPlayedBy.Count(), 8);
            Assert.AreEqual(InstrumentPlayedBy.ElementAt(0).Instrument, rythmGuitar);
            Assert.AreEqual(InstrumentPlayedBy.ElementAt(0).Person, john);
            Assert.AreEqual(InstrumentPlayedBy.ElementAt(1).Instrument, rythmGuitar);
            Assert.AreEqual(InstrumentPlayedBy.ElementAt(1).Person, george);
            Assert.AreEqual(InstrumentPlayedBy.ElementAt(2).Instrument, leadGuitar);
            Assert.AreEqual(InstrumentPlayedBy.ElementAt(2).Person, john);
            Assert.AreEqual(InstrumentPlayedBy.ElementAt(3).Instrument, leadGuitar);
            Assert.AreEqual(InstrumentPlayedBy.ElementAt(3).Person, paul);
            Assert.AreEqual(InstrumentPlayedBy.ElementAt(4).Instrument, leadGuitar);
            Assert.AreEqual(InstrumentPlayedBy.ElementAt(4).Person, george);
            Assert.AreEqual(InstrumentPlayedBy.ElementAt(5).Instrument, bass);
            Assert.AreEqual(InstrumentPlayedBy.ElementAt(5).Person, john);
            Assert.AreEqual(InstrumentPlayedBy.ElementAt(6).Instrument, bass);
            Assert.AreEqual(InstrumentPlayedBy.ElementAt(6).Person, paul);
            Assert.AreEqual(InstrumentPlayedBy.ElementAt(7).Instrument, drums);
            Assert.AreEqual(InstrumentPlayedBy.ElementAt(7).Person, ringo);
        }

内部結合結果リストのイメージ

title

次に、personsとinstrumentPlayers、instrumentsとinstrumentPlayersを
GroupJoin(左外部結合)させてみる。

        [TestMethod()]
        public void リストとリストをつき合わせる_左外部結合Test()
        {
            var personPlay = persons // personsと
                .GroupJoin(instrumentPlayers, // instrumentPlayersをつきあわせ、
                    _ => _.Id,
                    _ => _.Person.Id, // personsに存在するPersonについて要素を抽出し
                    (Person, InstrumentPlayers) => new { Person, InstrumentPlayers })  // 新しいオブジェクトを作り出し
                .OrderBy(_ => _.Person.Id);  // PeronのIdをキーにして並べる。

            Assert.AreEqual(personPlay.Count(), 5); // personsに存在するすべてのPersonが含まれている。
            Assert.AreEqual(personPlay.ElementAt(0).Person, john);
            Assert.AreEqual(personPlay.ElementAt(1).Person, paul);
            Assert.AreEqual(personPlay.ElementAt(2).Person, george);
            Assert.AreEqual(personPlay.ElementAt(3).Person, ringo);
            Assert.AreEqual(personPlay.ElementAt(4).Person, yoko);

            Assert.AreEqual(personPlay.ElementAt(0).InstrumentPlayers.Count(), 3); // ひとつのPersonに関連するInstrumentPlayerが束になって含まれている。
            Assert.AreEqual(personPlay.ElementAt(1).InstrumentPlayers.Count(), 2);
            Assert.AreEqual(personPlay.ElementAt(2).InstrumentPlayers.Count(), 2);
            Assert.AreEqual(personPlay.ElementAt(3).InstrumentPlayers.Count(), 1);
            Assert.AreEqual(personPlay.ElementAt(4).InstrumentPlayers.Count(), 0); // YokoはInstrumentを持たない。

            var instrumentsPlayedByJohn = personPlay.ElementAt(0).InstrumentPlayers.Select(_ => _.Instrument).OrderBy(_ => _.Id); // Johnについて確認
            Assert.AreEqual(instrumentsPlayedByJohn.ElementAt(0), rythmGuitar);
            Assert.AreEqual(instrumentsPlayedByJohn.ElementAt(1), leadGuitar);
            Assert.AreEqual(instrumentsPlayedByJohn.ElementAt(2), bass);

            var InstrumentPlayedBy = instruments
                .GroupJoin(instrumentPlayers,
                _ => _.Id,
                _ => _.Instrument.Id,
                (Instrument, InstrumentPlayers) => new { Instrument, InstrumentPlayers })
                .OrderBy(_ => _.Instrument.Id);
            Assert.AreEqual(InstrumentPlayedBy.Count(), 5);
            Assert.AreEqual(InstrumentPlayedBy.ElementAt(0).Instrument, rythmGuitar); // ひとつのPersonに関連するInstrumentPlayerが束になって含まれている。
            Assert.AreEqual(InstrumentPlayedBy.ElementAt(1).Instrument, leadGuitar);
            Assert.AreEqual(InstrumentPlayedBy.ElementAt(2).Instrument, bass);
            Assert.AreEqual(InstrumentPlayedBy.ElementAt(3).Instrument, drums);
            Assert.AreEqual(InstrumentPlayedBy.ElementAt(4).Instrument, wadaiko); // wadaikoはPlayerを持たない。

            Assert.AreEqual(InstrumentPlayedBy.ElementAt(0).InstrumentPlayers.Count(), 2);
            Assert.AreEqual(InstrumentPlayedBy.ElementAt(1).InstrumentPlayers.Count(), 3);
            Assert.AreEqual(InstrumentPlayedBy.ElementAt(2).InstrumentPlayers.Count(), 2);
            Assert.AreEqual(InstrumentPlayedBy.ElementAt(3).InstrumentPlayers.Count(), 1);
            Assert.AreEqual(InstrumentPlayedBy.ElementAt(4).InstrumentPlayers.Count(), 0);

            var personsPlayedRythmGuitar = InstrumentPlayedBy.ElementAt(0).InstrumentPlayers.Select(_ => _.Person).OrderBy(_ => _.Id); // RythmGuitarについて確認
            Assert.AreEqual(personsPlayedRythmGuitar.ElementAt(0), john);
            Assert.AreEqual(personsPlayedRythmGuitar.ElementAt(1), george);
        }

左外部結合結果リストのイメージ

title

疑問

GroupJoinの結果リストをフラットなリストにしなかった理由は、Join先の相手がいないという表現を「== null」でしたくなかったからだろうか?
ツリーなら「Count() == 0」で表現できるからツリーにしたのだろうか?