LINQ to Objects에서 작동하지 않는 구별


120
class Program
{
    static void Main(string[] args)
    {
        List<Book> books = new List<Book> 
        {
            new Book
            {
                Name="C# in Depth",
                Authors = new List<Author>
                {
                    new Author 
                    {
                        FirstName = "Jon", LastName="Skeet"
                    },
                     new Author 
                    {
                        FirstName = "Jon", LastName="Skeet"
                    },                       
                }
            },
            new Book
            {
                Name="LINQ in Action",
                Authors = new List<Author>
                {
                    new Author 
                    {
                        FirstName = "Fabrice", LastName="Marguerie"
                    },
                     new Author 
                    {
                        FirstName = "Steve", LastName="Eichert"
                    },
                     new Author 
                    {
                        FirstName = "Jim", LastName="Wooley"
                    },
                }
            },
        };


        var temp = books.SelectMany(book => book.Authors).Distinct();
        foreach (var author in temp)
        {
            Console.WriteLine(author.FirstName + " " + author.LastName);
        }

        Console.Read();
    }

}
public class Book
{
    public string Name { get; set; }
    public List<Author> Authors { get; set; }
}
public class Author
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public override bool Equals(object obj)
    {
        return true;
        //if (obj.GetType() != typeof(Author)) return false;
        //else return ((Author)obj).FirstName == this.FirstName && ((Author)obj).FirstName == this.LastName;
    }

}

이것은 "LINQ in Action"의 예를 기반으로합니다. 목록 4.16.

이것은 Jon Skeet을 두 번 인쇄합니다. 왜? Author 클래스에서 Equals 메서드를 재정의하려고 시도했습니다. 여전히 Distinct가 작동하지 않는 것 같습니다. 내가 무엇을 놓치고 있습니까?

편집 : 나는 == 및! = 연산자 과부하도 추가했습니다. 여전히 도움이되지 않습니다.

 public static bool operator ==(Author a, Author b)
    {
        return true;
    }
    public static bool operator !=(Author a, Author b)
    {
        return false;
    }

답변:


159

LINQ Distinct는 사용자 지정 개체와 관련하여 그다지 똑똑하지 않습니다.

목록을보고 두 개의 다른 개체가 있는지 확인하기 만하면됩니다 (구성원 필드에 대해 동일한 값이 있는지 상관하지 않습니다).

한 가지 해결 방법은 여기에 표시된대로 IEquatable 인터페이스를 구현하는 입니다.

이렇게 Author 클래스를 수정하면 작동합니다.

public class Author : IEquatable<Author>
{
    public string FirstName { get; set; }
    public string LastName { get; set; }

    public bool Equals(Author other)
    {
        if (FirstName == other.FirstName && LastName == other.LastName)
            return true;

        return false;
    }

    public override int GetHashCode()
    {
        int hashFirstName = FirstName == null ? 0 : FirstName.GetHashCode();
        int hashLastName = LastName == null ? 0 : LastName.GetHashCode();

        return hashFirstName ^ hashLastName;
    }
}

DotNetFiddle로 사용해보십시오


22
IEquatable은 괜찮지 만 불완전합니다. 당신이해야 항상 implemement Object.Equals ()와 Object.GetHashCode () 함께; IEquatable <T> .Equals는 Object.Equals를 재정의하지 않으므로 프레임 워크에서 자주 발생하며 항상 제네릭이 아닌 컬렉션에서 발생하는 강력하지 않은 형식 비교를 수행 할 때 실패합니다.
AndyM

Rex M이 제안한대로 IEqualityComparer <T>를 사용하는 Distinct의 재정의를 사용하는 것이 더 낫습니까? 함정에 빠지고 싶지 않다면해야 할 일을 의미합니다.
Tanmoy

3
@ Tanmoy에 따라 다릅니다. Author가 일반적으로 일반 객체처럼 동작하도록하려면 (즉, 참조 같음 만) Distinct 목적으로 이름 값을 확인하려면 IEqualityComparer를 사용합니다. 당신이 경우 항상 저자 오브젝트가 이름 값을 기준으로 비교하려면, 다음 GetHashCode을 무시하고 같음, 또는 IEquatable를 구현합니다.
Rex M

3
나는 구현 IEquatable(및 오버라이드 Equals/ GetHashCode)하지만 내 중단 점의 아무도는 Linq에 이러한 방법에서 해고되지 않습니다 Distinct?
PeterX

2
@PeterX 나도 이것을 발견했다. GetHashCode및에 중단 점이 있습니다.Equals foreach 루프 중에 적중되었습니다. var temp = books.SelectMany(book => book.Authors).Distinct();는를 반환 하기 때문입니다. IEnumerable즉, 요청이 즉시 실행되지 않고 데이터가 사용될 때만 실행됩니다. 당신이 바로이 발사의 예를 원하는 경우, 추가 .ToList()애프터 .Distinct()당신은에서 중단 점을 볼 수 EqualsGetHashCodeforeach 문 전합니다.
JabberwockyDecompiler

70

Distinct()메서드는 참조 유형에 대한 참조 동등성을 확인합니다. 즉, 동일한 값을 포함하는 다른 개체가 아니라 문자 그대로 복제 된 동일한 개체를 찾습니다.

IEqualityComparer를 사용 하는 오버로드 가 있으므로 주어진 개체가 다른 개체와 같은지 여부를 결정하기 위해 다른 논리를 지정할 수 있습니다.

Author가 일반적으로 일반 객체처럼 동작하도록하려면 (즉, 참조 같음 만), Distinct의 목적으로 이름 값으로 같음을 확인하려면 IEqualityComparer를 사용하십시오 . 항상 Author 개체를 이름 값을 기준으로 비교하려면 GetHashCode 및 Equals재정의 하거나 IEquatable을 구현하십시오 .

IEqualityComparer인터페이스 의 두 멤버 는 EqualsGetHashCode입니다. 두 Author개체가 같은지 여부를 결정하는 논리는 이름 및 성 문자열이 동일한 경우로 나타납니다.

public class AuthorEquals : IEqualityComparer<Author>
{
    public bool Equals(Author left, Author right)
    {
        if((object)left == null && (object)right == null)
        {
            return true;
        }
        if((object)left == null || (object)right == null)
        {
            return false;
        }
        return left.FirstName == right.FirstName && left.LastName == right.LastName;
    }

    public int GetHashCode(Author author)
    {
        return (author.FirstName + author.LastName).GetHashCode();
    }
}

1
감사합니다! 귀하의 GetHashCode () 구현은 내가 여전히 놓친 것을 보여주었습니다. {property being used for comparison} .GetHashCode ()가 아닌 {passed-in object} .GetHashCode ()를 반환했습니다. 그것은 차이를 만들어 내고 왜 여전히 실패했는지 설명합니다. 두 개의 다른 참조는 두 개의 다른 해시 코드를 가질 것입니다.
pelazem

44

구현해야만 용액 IEquatable, EqualsGetHashCodeLINQs 사용하는 GroupBy방법 및 IGrouping 중 첫 번째 항목을 선택한다.

var temp = books.SelectMany(book => book.Authors)
                .GroupBy (y => y.FirstName + y.LastName )
                .Select (y => y.First ());

foreach (var author in temp){
  Console.WriteLine(author.FirstName + " " + author.LastName);
}

1
성능을 고려할 때 위의 방법을 고려할 때 동일한 속도로 수행됩니까?
Biswajeet 2015 년

메서드 구현으로 복잡하게 만드는 것보다 훨씬 좋으며 EF를 사용하면 작업이 SQL 서버에 위임됩니다.
Zapnologica 2015 년

이 방법이 작동하는 동안, 때문에 사물의 수에 성능 문제가있을 것 그룹화되고
Bellash

@Bellash 그것을 작동시키고 빨리 만드십시오. 물론이 그룹화로 인해 더 많은 작업이 수행 될 수 있습니다. 그러나 때로는 원하는 것보다 더 많은 것을 구현하는 것이 번거 롭습니다.
Jehof

2
이 솔루션을 선호하지만 groupby에서 "새"개체를 사용하여 : .GroupBy(y => new { y.FirstName, y.LastName })
Dave de Jong

32

사용자 정의 데이터 유형 목록에서 고유 한 값을 가져 오는 또 다른 방법이 있습니다.

YourList.GroupBy(i => i.Id).Select(i => i.FirstOrDefault()).ToList();

확실히, 그것은 별개의 데이터 세트를 제공 할 것입니다


21

Distinct()열거 형의 개체에 대한 기본 같음 비교를 수행합니다. Equals()및을 재정의하지 않은 경우 GetHashCode()에 기본 구현을 사용합니다.object 참조를 비교 .

간단한 솔루션은 추가하는 것입니다 올바른 구현을 Equals()하고GetHashCode() 있습니다 (예 : 도서 및 저자)를 비교하는 객체 그래프에 참여하는 모든 클래스를.

IEqualityComparer인터페이스를 구현할 수있는 편리 Equals()하고 GetHashCode()별도의 클래스에서 당신이 비교의 다른 방법을 사용하는 경우 비교해야하거나 클래스의 내부에 액세스 할 수없는 경우.


참여하는 대상에 대한이 화려한 성약에 대해 대단히 감사합니다.
suhyura

11

Equals ()를 재정의했지만 GetHashCode ()도 재정의했는지 확인하십시오.


+1은 GetHashCode ()를 강조합니다. 기본 HashCode 구현을 추가하지 마십시오<custom>^base.GetHashCode()
Dani

8

위의 답변이 잘못되었습니다 !!! MSDN에 명시된 Distinct는 명시된대로 기본 Equator를 반환합니다 . Default 속성은 T 유형이 System.IEquatable 인터페이스를 구현하는지 여부를 확인하고, 그렇다면 해당 구현을 사용하는 EqualityComparer를 반환합니다. 그렇지 않으면 T에서 제공하는 Object.Equals 및 Object.GetHashCode의 재정의를 사용하는 EqualityComparer를 반환합니다.

즉, Equals를 오버라이드하는 한 괜찮습니다.

코드가 작동하지 않는 이유는 firstname == lastname을 확인하기 때문입니다.

참조 https://msdn.microsoft.com/library/bb348436(v=vs.100).aspxhttps://msdn.microsoft.com/en-us/library/ms224763(v=vs.100).aspx


0

계산 된 Hash를 기반으로 고유성을 확인하는 목록에서 확장 방법을 사용할 수 있습니다. IEnumerable을 지원하도록 확장 방법을 변경할 수도 있습니다.

예:

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

List<Employee> employees = new List<Employee>();
employees.Add(new Employee{Name="XYZ", Age=30});
employees.Add(new Employee{Name="XYZ", Age=30});

employees = employees.Unique(); //Gives list which contains unique objects. 

연장 방법 :

    public static class LinqExtension
        {
            public static List<T> Unique<T>(this List<T> input)
            {
                HashSet<string> uniqueHashes = new HashSet<string>();
                List<T> uniqueItems = new List<T>();

                input.ForEach(x =>
                {
                    string hashCode = ComputeHash(x);

                    if (uniqueHashes.Contains(hashCode))
                    {
                        return;
                    }

                    uniqueHashes.Add(hashCode);
                    uniqueItems.Add(x);
                });

                return uniqueItems;
            }

            private static string ComputeHash<T>(T entity)
            {
                System.Security.Cryptography.SHA1CryptoServiceProvider sh = new System.Security.Cryptography.SHA1CryptoServiceProvider();
                string input = JsonConvert.SerializeObject(entity);

                byte[] originalBytes = ASCIIEncoding.Default.GetBytes(input);
                byte[] encodedBytes = sh.ComputeHash(originalBytes);

                return BitConverter.ToString(encodedBytes).Replace("-", "");
            }

-1

다음 두 가지 방법으로이를 달성 할 수 있습니다.

1. Enumerable.Distinct Method 와 같이 IEquatable 인터페이스를 구현 하거나이 게시물에서 @skalb의 답변을 볼 수 있습니다.

2. 개체에 고유 키가없는 경우 개체의 모든 속성을 그룹화하고 첫 번째 개체를 선택한 후 고유 한 개체 목록을 얻기 위해 GroupBy 메서드를 사용할 수 있습니다.

예를 들어 아래와 같이 나를 위해 일하고 있습니다.

var distinctList= list.GroupBy(x => new {
                            Name= x.Name,
                            Phone= x.Phone,
                            Email= x.Email,
                            Country= x.Country
                        }, y=> y)
                       .Select(x => x.First())
                       .ToList()

MyObject 클래스는 다음과 같습니다.

public class MyClass{
       public string Name{get;set;}
       public string Phone{get;set;}
       public string Email{get;set;}
       public string Country{get;set;}
}

삼. 개체에 고유 한 키가있는 경우 그룹 별에서만 사용할 수 있습니다.

예를 들어 내 개체의 고유 키는 Id입니다.

var distinctList= list.GroupBy(x =>x.Id)
                      .Select(x => x.First())
                      .ToList()
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.