C#/배열

[C#]배열 중복 값 제거(Distinct)

DevStory 2021. 9. 26.

배열에서 중복 값을 제거한다는 의미는 고유한 값만 가진다는 의미입니다.

 

하지만, C#에서는 배열의 값을 제거할 수 없기 때문에 중복 값이 없는 새로운 배열을 생성해야 합니다.

 

이러한 경우 Distinct() 메서드를 사용하여 중복 값이 제거된 새로운 배열을 쉽게 생성할 수 있습니다.

 

이번 포스팅에서는 Distinct() 메서드 사용 방법과 객체 배열에서 중복 값 제거 방법을 정리하였습니다.

 


Distinct() 메서드

Distinct() 메서드를 사용하기 위해 using문에 다음 코드를 추가합니다.

using System.Linq;

 

다음은 int형 배열을 Distinct() 메서드를 사용하여 중복 값을 제거하는 코드입니다.

int[] numArray = { 1, 2, 2, 3, 3, 3, 4, 5, 5 };

int[] distArray = numArray.Distinct().ToArray();

 

실행 결과

numArray 배열에서 중복 값이 제거된 새로운 배열이 생성되었습니다.

 

다음은 string형 배열을 Distinct() 메서드를 사용하여 중복 값을 제거하는 코드입니다.

string[] strArray = { "A", "B", "B", "C", "ABC", "ABC" };

string[] distArray = strArray.Distinct().ToArray();

실행 결과

 

다음은 제네릭 컬렉션인 List에서 중복 값을 제거하는 코드입니다.

List<string> list = new List<string>() { "A", "B", "B", "ABC", "ABC" };

List<string> distList = new List<string>(); 
distList = list.Distinct().ToList();

실행 결과

Distinct() 메서드 실행 결과가 List 형식으로 반환해야 하므로 ToList() 메서드를 호출합니다.


객체 배열에서 문제점

다음 코드는 Person 클래스 타입의 List에서 Distinct() 메서드를 호출한 코드입니다.

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

class Program
{
    static void Main(string[] args)
    {
        List<Person> personList = new List<Person>() {
            new Person() { Name = "Steve", Age = 20 },
            new Person() { Name = "Bill", Age = 20 },
            new Person() { Name = "Steve", Age = 25 },
            new Person() { Name = "Nick", Age = 20 },
            new Person() { Name = "Bill", Age = 20 },
        };

        Console.WriteLine("=====[중복 제거 전]=====");
        foreach (Person obj in personList)
        {
            Console.WriteLine($"Name : {obj.Name} / Age: {obj.Age}");
        }

        List<Person> distPersonList = personList.Distinct().ToList();

        Console.WriteLine("\n=====[중복 제거 후]=====");
        foreach (Person obj in distPersonList)
        {
            Console.WriteLine($"Name : {obj.Name} / Age: {obj.Age}");
        }
    }
}

실행 결과

Name이 Bill이고 Age가 20인 중복된 객체가 있는데도 중복 값이 제거되지 않았습니다.

 

Distinct() 메서드는 객체의 참조 값을 비교하고 속성(Name과 Age)은 비교하지 않으므로 객체 배열에서 중복 값이 제거되지 않습니다.

위 그림처럼 personList 객체는 Heap 영역에 생성된 참조값(주소)으로 비교합니다.

 

Name, Age 속성이 동일한 객체가 존재하더라도 참조값(주소)이 다르므로 중복 값이 제거되지 않습니다.

 

객체 배열에서 중복 값을 제거하기 위해서 다음 4가지 방법을 사용할 수 있습니다.

  • IEqualityComparer 상속
  • Equals() 및 GetHashCode() 메서드 재정의
  • 익명 타입 사용
  • IEquatable 상속

위 4가지 방법이 대표적인 방법이며, 순서대로 설명하도록 하겠습니다.

반응형

IEqualityComparer 상속

다음 코드는 IEqualityComparer 인터페이스를 상속받은 PersonComparer 클래스를 구현하여 중복 값이 제거된 객체 배열을 생성하는 코드입니다.

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

public class PersonComparer : IEqualityComparer<Person>
{
    public bool Equals(Person x, Person y)
    {
        // 참조 값 동일한지 비교
        if(object.ReferenceEquals(x, y))
        {
            return true;
        }
        
        // 객체 참조 중 하나라도 null이면 false 반환 
        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;
        }

        // 문자열은 null이 가능하므로 예외 처리
        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> personList = new List<Person>() {
            new Person() { Name = "Steve", Age = 20 },
            new Person() { Name = "Bill", Age = 20 },
            new Person() { Name = "Steve", Age = 25 },
            new Person() { Name = "Nick", Age = 20 },
            new Person() { Name = "Bill", Age = 20 },
        };

        Console.WriteLine("=====[중복 제거 전]=====");
        foreach (Person obj in personList)
        {
            Console.WriteLine($"Name : {obj.Name} / Age: {obj.Age}");
        }

        List<Person> distPersonList = personList.Distinct(new PersonComparer()).ToList();

        Console.WriteLine("\n=====[중복 제거 후]=====");
        foreach (Person obj in distPersonList)
        {
            Console.WriteLine($"Name : {obj.Name} / Age: {obj.Age}");
        }
    }
}

실행 결과

IEqualityComparer 인터페이스를 상속받은 PersonComparer 클래스에 Equals() 및 GetHashCode() 메서드를 구현합니다.

 

그리고 Distinct() 메서드에 PersonComparer 클래스의 인스턴스를 전달합니다.

Distinct() 메서드는 두 가지 버전이 존재하는데, IEqualityComparer 인터페이스를 상속받은 PersonComparer 클래스의 인스턴스를 전달하여 PersonComparer 클래스에서 구현한 Equals() 및 GetHashCode() 메서드를 호출하도록 합니다.


Equals() 및 GetHashCode() 메서드 재정의

Person 클래스에서 Equals() 및 GetHashCode() 메서드를 재정의합니다.

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

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

    public override int GetHashCode()
    {
        int NameHashCode = this.Name == null ? 0 : this.Name.GetHashCode();

        int AgeHashCode = this.Age.GetHashCode();

        return NameHashCode ^ AgeHashCode;
    }
}

class Program
{
    static void Main(string[] args)
    {
        List<Person> personList = new List<Person>() {
            new Person() { Name = "Steve", Age = 20 },
            new Person() { Name = "Bill", Age = 20 },
            new Person() { Name = "Steve", Age = 25 },
            new Person() { Name = "Nick", Age = 20 },
            new Person() { Name = "Bill", Age = 20 },
        };

        Console.WriteLine("=====[중복 제거 전]=====");
        foreach (Person obj in personList)
        {
            Console.WriteLine($"Name : {obj.Name} / Age: {obj.Age}");
        }

        List<Person> distPersonList = personList.Distinct().ToList();

        Console.WriteLine("\n=====[중복 제거 후]=====");
        foreach (Person obj in distPersonList)
        {
            Console.WriteLine($"Name : {obj.Name} / Age: {obj.Age}");
        }
    }
}

실행 결과

Distinct() 메서드가 Person() 클래스에서 재정의된 Equals() 및 GetHashCode() 메서드를 호출합니다.


익명 타입 사용

다음 코드는 Linq의 Select() 메서드를 사용하여 중복 값이 제거된 var 타입의 객체를 생성합니다.

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

class Program
{
    static void Main(string[] args)
    {
        List<Person> personList = new List<Person>() {
            new Person() { Name = "Steve", Age = 20 },
            new Person() { Name = "Bill", Age = 20 },
            new Person() { Name = "Steve", Age = 25 },
            new Person() { Name = "Nick", Age = 20 },
            new Person() { Name = "Bill", Age = 20 },
        };

        Console.WriteLine("=====[중복 제거 전]=====");
        foreach (Person obj in personList)
        {
            Console.WriteLine($"Name : {obj.Name} / Age: {obj.Age}");
        }

        var distPerson = personList.Select(person => new { person.Name, person.Age }).Distinct().ToList();

        Console.WriteLine("\n=====[중복 제거 후]=====");
        foreach (var obj in distPerson)
        {
            Console.WriteLine($"Name : {obj.Name} / Age: {obj.Age}");
        }
    }
}

실행 결과

Linq의 Select() 메서드를 사용하여 personList 객체에서 Name과 Age의 속성의 값을 추출합니다.

 

추출된 값은 Distinct() 메서드를 사용하여 중복 값을 제거합니다.

 

익명 타입으로 중복 값을 제거하는 방법의 단점은 중복 값이 제거된 타입이 var라는 단점이 있습니다.


IEquatable 인터페이스 상속

IEquatable 인터페이스를 상속받은 Person 클래스를 구현합니다.

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

    public bool Equals(Person other)
    {
        return Name.Equals(other.Name) && Age.Equals(other.Age);
    }
    
    public override int GetHashCode()
    {
        return Name.GetHashCode() ^ Age.GetHashCode();
    }
}

class Program
{
    static void Main(string[] args)
    {
        List<Person> personList = new List<Person>() {
            new Person() { Name = "Steve", Age = 20 },
            new Person() { Name = "Bill", Age = 20 },
            new Person() { Name = "Steve", Age = 25 },
            new Person() { Name = "Nick", Age = 20 },
            new Person() { Name = "Bill", Age = 20 },
        };

        Console.WriteLine("=====[중복 제거 전]=====");
        foreach (Person obj in personList)
        {
            Console.WriteLine($"Name : {obj.Name} / Age: {obj.Age}");
        }

        List<Person> distPersonList = personList.Distinct().ToList();

        Console.WriteLine("\n=====[중복 제거 후]=====");
        foreach (Person obj in distPersonList)
        {
            Console.WriteLine($"Name : {obj.Name} / Age: {obj.Age}");
        }
    }
}

실행 결과


정리

  • 기존 배열에서 중복 값 삭제가 불가능하므로 중복 값이 제거된 새로운 배열을 생성해야 합니다.
  • Distinct() 메서드는 중복 값이 제거된 새로운 배열을 생성합니다.
  • 객체는 참조 값으로 비교되므로 아래 4가지 방법을 사용하여 중복 값이 제거된 배열을 생성합니다.
    ☞ IEqualityComparer 상속, Equals() 및 GetHashCode() 메서드 재정의, 익명 타입 사용, IEquatable 상속

 

IEqualityComparer와 IEquatable의 차이

IEqualityComparer<T>는 동일한 타입의 두 객체를 비교를 목적으로 구현된 인터페이스입니다.

IEquatable<T>는 다른 타입의 객체도 비교하기 위해 구현된 인터페이스입니다.

반응형

댓글