C#/LINQ

[C#]LINQ 차집합 구하는 방법 - Except 메서드

DevStory 2022. 7. 13.

Except 메서드

LINQ의 Except() 메서드는 첫 번째 데이터에는 존재하지만 두 번째 데이터에는 없는 요소를 반환합니다. 예를 들어 다음 소스 코드를 봅시다.

int[] intArrayA = { 0, 2, 4, 5 };
int[] intArrayB = { 1, 2, 3, 5 };

int 타입의 배열인 intArrayA와 intArrayB가 존재합니다. 첫 번째 데이터인 intArrayA에는 존재하지만 두 번째 데이터인 intArrayB에 없는 요소는 0, 4입니다.

 

배열을 그림으로 나타내면 다음과 같습니다.


오버로드된 두 가지 버전

LINQ의 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);

Except() 메서드는 Enumerable 클래스에서 구현되었으며 IEnumerable 인터페이스의 확장 메서드라는 것을 알 수 있습니다. 두 가지 버전의 차이점은 IEqualityComparer 인터페이스 사용 유무입니다.

 

IEqualityComparer 인터페이스가 존재한다는 의미는 Except() 메서드를 비교기(Comparer)와 함께 사용할 수 있다는 것입니다. 비교기를 사용하는 방법은 아래 예제에서 확인할 수 있습니다.


예제 1. int 타입의 List

다음 예제는 int 타입의 List에서 Except() 메서드를 사용합니다. 질의 구문에는 except절이 없으므로 질의 구문과 메서드 구문을 조합하여 사용합니다.

class Program
{
  static void Main(string[] args)
  {
    List<int> intArrayA = new List<int>() { 0, 2, 4, 5 };
    List<int> intArrayB = new List<int>() { 1, 2, 3, 5 };

    // 1. 질의 구문(Query Syntax) + 메서드 구문(Method Syntax)
    List<int> linqQueryResult = (from   num in intArrayA
                                 select num).Except(intArrayB).ToList();

    // 2. 메서드 구문(Method Syntax)
    List<int> linqMethodResult = intArrayA.Except(intArrayB).ToList();

    Console.WriteLine("질의 구문 + 메서드 구문");
    foreach (int num in linqQueryResult)
      Console.Write(num + " ");

    Console.WriteLine("\n메서드 구문");
    foreach (int num in linqMethodResult)
      Console.Write(num + " ");
  }
}

[실행 결과]

질의 구문 + 메서드 구문
0 4
메서드 구문
0 4

예제 2. string 타입의 List

다음 예제는 string 타입의 List에서 Except() 메서드를 사용합니다. 사용 방법은 int 타입에서 설명한 방법과 동일합니다.

class Program
{
  static void Main(string[] args)
  {
    List<string> strArrayA = new List<string>() { "one", "two", "three" };
    List<string> strArrayB = new List<string>() { "One", "Two", "Three" };

    // 1. 질의 구문(Query Syntax) + 메서드 구문(Method Syntax)
    List<string> linqQueryResult = (from   str in strArrayA
                                    select str).Except(strArrayB).ToList();

    // 2. 메서드 구문(Method Syntax)
    List<string> linqMethodResult = strArrayA.Except(strArrayB).ToList();

    Console.WriteLine("질의 구문 + 메서드 구문");
    foreach (string str in linqQueryResult)
      Console.Write(str + " ");

    Console.WriteLine("\n메서드 구문");
    foreach (string str in linqMethodResult)
      Console.Write(str + " ");
  }
}

[실행 결과]

질의 구문 + 메서드 구문
one two three
메서드 구문
one two three

기본 비교자는 문자열을 비교할 때 대소문자를 구분합니다. 따라서 IEqualityComparer를 매개변수로 사용하는 Except() 메서드를 사용하여 대소문자를 구분하지 않도록 합니다.

class Program
{
  static void Main(string[] args)
  {
    List<string> strArrayA = new List<string>() { "one", "two", "three" };
    List<string> strArrayB = new List<string>() { "One", "Two", "Three" };

    // 1. 질의 구문(Query Syntax) + 메서드 구문(Method Syntax)
    List<string> linqQueryResult = (from   str in strArrayA
                                    select str)
                                    .Except(strArrayB, StringComparer.OrdinalIgnoreCase)
                                    .ToList();

    // 2. 메서드 구문(Method Syntax)
    List<string> linqMethodResult = strArrayA
        .Except(strArrayB, StringComparer.OrdinalIgnoreCase)
    .ToList();

    Console.WriteLine("질의 구문 + 메서드 구문");
    foreach (string str in linqQueryResult)
      Console.Write(str + " ");

    Console.WriteLine("\n메서드 구문");
    foreach (string str in linqMethodResult)
      Console.Write(str + " ");
  }
}

예제 3. 사용자 정의 클래스

Except() 메서드는 사용자가 정의한 클래스 타입에서도 사용할 수 있지만 약간 다른 방식으로 동작합니다.

 

Person이라는 사용자 정의 클래스가 있습니다.

public class Person
{
  public string Name { get; set; }
  public int Age { get; set; }
  public override string ToString()
  {
    return "Name: " + Name + ", Age: " + Age;
  }
}

그리고 Person 타입의 두 개의 List 객체가 존재합니다.

객체 personA 기준으로 Except() 메서드를 호출하면 동일한 요소를 제외하고 다음 두 개의 요소가 반환되어야 합니다.

하지만 다음 예제를 실행하면 예상하는 결과와 달리 personA의 모든 요소가 출력됩니다.

public class Person
{
  public string Name { get; set; }
  public int Age { get; set; }
  public override string ToString()
  {
    return "Name: " + Name + ", Age: " + Age;
  }
}

class Program
{
  static void Main(string[] args)
  {
    List<Person> personA = new List<Person>
    {
      new Person{Name ="Bob", Age = 20},
      new Person{Name ="Tom", Age = 25},
      new Person{Name ="Sam", Age = 30}
    };

    List<Person> personB = new List<Person>
    {
      new Person{Name ="Ella", Age = 15},
      new Person{Name ="Bob",  Age = 20}
    };

    // 1. 질의 구문(Query Syntax) + 메서드 구문(Method Syntax)
    List<Person> linqQueryResult = (from   person in personA
                                    select person)
                                    .Except(personB)
                                    .ToList();

    // 2. 메서드 구문(Method Syntax)
    List<Person> linqMethodResult = personA.Except(personB).ToList();

    Console.WriteLine("질의 구문 + 메서드 구문");
    foreach (Person person in linqQueryResult)
      Console.WriteLine(person.ToString());

    Console.WriteLine("\n메서드 구문");
    foreach (Person person in linqMethodResult)
      Console.WriteLine(person.ToString());
  }
}

[실행 결과]

질의 구문 + 메서드 구문
Name: Bob, Age: 20
Name: Tom, Age: 25
Name: Sam, Age: 30

메서드 구문
Name: Bob, Age: 20
Name: Tom, Age: 25
Name: Sam, Age: 30

위 문제를 해결하기 위해 익명 타입과 비교자를 사용하는 방식을 알아봅시다.


예제 4. 익명 타입 사용

익명 타입을 사용하는 방법은 Select절을 사용하여 객체의 모든 요소를 익명 타입으로 생성합니다.

public class Person
{
  public string Name { get; set; }
  public int Age { get; set; }
}

class Program
{
  static void Main(string[] args)
  {
    List<Person> personA = new List<Person>
    {
      new Person{Name ="Bob", Age = 20},
      new Person{Name ="Tom", Age = 25},
      new Person{Name ="Sam", Age = 30}
    };

    List<Person> personB = new List<Person>
    {
      new Person{Name ="Ella", Age = 15},
      new Person{Name ="Bob",  Age = 20}
    };

    // 1. 질의 구문(Query Syntax) + 메서드 구문(Method Syntax)
    var linqQueryResult = (from person in personA
                           select new { person.Name, person.Age })
                           .Except(personB.Select(item => new { item.Name, item.Age }));

    // 2. 메서드 구문(Method Syntax)
    var linqMethodResult = personA.Select(item => new { item.Name, item.Age })
        .Except(personB.Select(item => new { item.Name, item.Age }));

    Console.WriteLine("질의 구문 + 메서드 구문");
    foreach (var person in linqQueryResult)
      Console.WriteLine(person.ToString());

    Console.WriteLine("\n메서드 구문");
    foreach (var person in linqMethodResult)
      Console.WriteLine(person.ToString());
  }
}

[실행 결과]

질의 구문 + 메서드 구문
{ Name = Tom, Age = 25 }
{ Name = Sam, Age = 30 }

메서드 구문
{ Name = Tom, Age = 25 }
{ Name = Sam, Age = 30 }

예제 5. IEqualityComparer 인터페이스 구현

IEqualityComparer 인터페이스 구현 클래스가 필요합니다. 따라서 IEqualityComparer 인터페이스를 구현하는 PersonComparer이라는 클래스를 추가합니다.

public class Person
{
  public string Name { get; set; }
  public int Age { get; set; }
  public override string ToString()
  {
    return "Name: " + Name + ", Age: " + Age;
  }
}

public class PersonComparer : IEqualityComparer<Person>
{
  public bool Equals(Person x, Person y)
  {
    if (object.ReferenceEquals(x, y))
    {
      return true;
    }
    if (object.ReferenceEquals(x, null) || object.ReferenceEquals(y, null))
    {
      return false;
    }
    return x.Name == y.Name && x.Age == y.Age;
  }
  
  public int GetHashCode(Person obj)
  {
    if (obj == null)
    {
      return 0;
    }
    int NameHashCode = obj.Name == null ? 0 : obj.Name.GetHashCode();
    int AgeHashCode = obj.Age.GetHashCode();
    return NameHashCode ^ AgeHashCode;
  }
}

class Program
{
  static void Main(string[] args)
  {
    List<Person> personA = new List<Person>
    {
      new Person{Name ="Bob", Age = 20},
      new Person{Name ="Tom", Age = 25},
      new Person{Name ="Sam", Age = 30}
    };

    List<Person> personB = new List<Person>
    {
      new Person{Name ="Ella", Age = 15},
      new Person{Name ="Bob",  Age = 20}
    };

    PersonComparer personComparer = new PersonComparer();

    // 1. 질의 구문(Query Syntax) + 메서드 구문(Method Syntax)
    List<Person> linqQueryResult = (from   person in personA
                                    select person)
                                    .Except(personB, personComparer)
                                    .ToList();

    // 2. 메서드 구문(Method Syntax)
    List<Person> linqMethodResult = personA.Except(personB, personComparer).ToList();

    Console.WriteLine("질의 구문 + 메서드 구문");
    foreach (Person person in linqQueryResult)
      Console.WriteLine(person.ToString());

    Console.WriteLine("\n메서드 구문");
    foreach (Person person in linqMethodResult)
      Console.WriteLine(person.ToString());
  }
}

[실행 결과]

질의 구문 + 메서드 구문
Name: Tom, Age: 25
Name: Sam, Age: 30

메서드 구문
Name: Tom, Age: 25
Name: Sam, Age: 30
반응형

댓글