いろいろ備忘録日記

主に .NET とか Go とか Flutter とか Python絡みのメモを公開しています。

Linq入門記-34 (LINQ To Object, Except)


今回は、Except拡張メソッドについてです。


Except拡張メソッドは、差集合を生成します。
つまり、2つのシーケンスの内、片方だけに存在している要素のみが抽出されます。
OracleでいうMINUS, SQLServerでいうEXCEPTと同じです。


ちなみに、Union拡張メソッドは和集合、Intersect拡張メソッドは積集合を生成します。


Except拡張メソッドの書式は以下の通りです。

public static IEnumerable<TSource> Except<TSource>(
	this IEnumerable<TSource> first,
	IEnumerable<TSource> second
)

public static IEnumerable<TSource> Except<TSource>(
	this IEnumerable<TSource> first,
	IEnumerable<TSource> second,
	IEqualityComparer<TSource> comparer
)


一つ目の書式が一番よく利用される形のものですね。
2つめの書式は、一つ目の分にIEqualityComparer comparerが追加されています。
キーの同値性を独自の方法で決定する際に利用します。


Except拡張メソッドを利用する上で、注意点があります。
以下、MSDNライブラリからの引用です。

このメソッドは遅延実行を使用して実装されます。アクションの実行に必要なすべての情報を格納するオブジェクトがすぐに返されます。このメソッドによって表されるクエリは、オブジェクトの GetEnumerator メソッドを直接呼び出すか、foreach (Visual C# の場合) または For Each (Visual Basic の場合) を使用することによってオブジェクトが列挙されるまで実行されません。
2 つの集合の差集合は、2 番目の集合には含まれない最初の集合のメンバーとして定義されます。

メモ
このメソッドは、second に含まれない、first の要素を返します。また、first に含まれない、second の要素は返しません。


つまり、

var numbers1 = new int[]{ 1, 2, 3 };
var numbers2 = new int[]{ 1, 2, 4 };

numbers1.Except(numbers2);

とした場合、結果は

3, 4

ではなく

3

となり、左側のシーケンスだけに存在する要素が抽出されます。
右側のシーケンスからは抽出されません。


両方のシーケンスに対して、どちらか片方しか存在しないものを抽出するには

var numbers1 = new int[]{ 1, 2, 3 };
var numbers2 = new int[]{ 1, 2, 4 };

var result = (numbers1.Except(numbers2)).Concat((numbers2.Except(numbers1)))

とします。


以下、サンプルです。
以下のサンプルでは、基本型のシーケンスに対してのExceptと、カスタムデータのシーケンスに対してのExceptの
両方を実行してみています。

    #region LinqSamples-31
    public class LinqSamples31 : IExecutable
    {
        class Person
        {
            public string Name { get; set; }
            
            public override string ToString()
            {
                return string.Format("[NAME = {0}]", Name);
            }
        }
        
        class PersonComparer : EqualityComparer<Person>
        {
            public override bool Equals(Person p1, Person p2)
            {
                if (Object.Equals(p1, p2))
                {
                    return true;
                }
                
                if (p1 == null || p2 == null)
                {
                    return false;
                }
                
                return (p1.Name == p2.Name);
            }
            
            public override int GetHashCode(Person p)
            {
                return p.Name.GetHashCode();
            }
        }
        
        public void Execute()
        {
            //
            // 引数なしのExcept拡張メソッドを利用.
            // この場合、既定のIEqualityComparer<T>を用いて比較が行われる。
            //
            // Except拡張メソッドは、差集合を求める。
            // (Unionは和集合、Intersectは積集合となる。)
            // 
            // このExcept拡張メソッドには、以下の仕様がある。
            //
            //     ・差集合の対象となるのは、1番目の集合のみであり、2番目の集合からは抽出されない。
            //         →つまり、引数で指定する方のシーケンスからは抽出されない。
            //     ・以下MSDNの記述を引用。
            //       「このメソッドは、second に含まれない、first の要素を返します。また、first に含まれない、second の要素は返しません。」
            // 
            var numbers1 = new int[]
                          {
                            1, 2, 3, 4, 5
                          };
                          
            var numbers2 = new int[]
                           {
                             1, 2, 3, 6, 7
                           };
                          
            Console.WriteLine("EXCEPT = {0}", JoinElements(numbers1.Except(numbers2)));
            
            //
            // 引数にIEqualityComparer<T>を指定して、Except拡張メソッドを利用。
            // この場合、引数に指定したComparerを用いて比較が行われる。
            //
            var people1 = new Person[]
                          { 
                                new Person { Name = "gsf_zero1" }, 
                                new Person { Name = "gsf_zero2" }, 
                                new Person { Name = "gsf_zero1" },
                                new Person { Name = "gsf_zero3" }
                          };

            var people2 = new Person[]
                          { 
                                new Person { Name = "gsf_zero4" }, 
                                new Person { Name = "gsf_zero5" }, 
                                new Person { Name = "gsf_zero6" },
                                new Person { Name = "gsf_zero1" }
                          };
                          
            Console.WriteLine("EXCEPT = {0}", JoinElements(people1.Except(people2, new PersonComparer())));
        }
        
        string JoinElements<T>(IEnumerable<T> elements)
        {
            return string.Join(",", elements.Select(item => item.ToString()));
        }
    }
    #endregion


実行結果は以下の通りです。

  EXCEPT = 4,5
  EXCEPT = [NAME = gsf_zero2],[NAME = gsf_zero3]

================================
過去の記事については、以下のページからご参照下さい。