2016-02-01 6 views
6

Korzystanie z GroupBy() i Count() > 1 Próbuję znaleźć zduplikowane wystąpienia mojej klasy na liście.GroupBy na złożonym obiekcie (np. Lista <T>)

Klasa wygląda następująco:

public class SampleObject 
{ 
    public string Id; 
    public IEnumerable<string> Events; 
} 

I to jest jak ja instancji i grupowej listy:

public class Program 
{ 
    private static void Main(string[] args) 
    { 
     var items = new List<SampleObject>() 
     { 
      new SampleObject() { Id = "Id", Events = new List<string>() { "ExampleEvent" } }, 
      new SampleObject() { Id = "Id", Events = new List<string>() { "ExampleEvent" } } 
     }; 

     var duplicates = items.GroupBy(x => new { Token = x.Id, x.Events }) 
         .Where(g => g.Count() > 1) 
         .Select(g => g.Key) 
         .ToList(); 
    } 
} 

duplicates zawiera żadnych elementów. Jak mogę utworzyć pracę grupową?

+2

Domyślnie listy nie są porównywane wartości ich pozycji. –

+1

@SergeyBerezovskiy - Co nie jest problemem tutaj. Problemem jest brak nadpisań 'GetHashCode' i' Equals'. – Enigmativity

+3

@Enigmativity 'new {Token = x.Id, x.Events}' nie ma nic, aby przesłonić Equals i GetHashCode z 'SampleObject'. Problem tutaj jest całkowicie spowodowany przez porównanie "x.Events" –

Odpowiedz

7

Aby uzyskać obiekty do pracy z wieloma operatorami LINQ, takich jak GroupBy lub Distinct, trzeba albo wdrożyć GetHashCode & Equals, czy należy podać niestandardową comparer.

W twoim przypadku z nieruchomością na liście prawdopodobnie potrzebujesz porównywalnika, chyba że zrobiłeś listę tylko do odczytu.

Spróbuj porównywarka:

public class SampleObjectComparer : IEqualityComparer<SampleObject> 
{ 
    public bool Equals(SampleObject x, SampleObject y) 
    { 
     return x.Id == y.Id && x.Events.SequenceEqual(y.Events); 
    } 

    public int GetHashCode(SampleObject x) 
    { 
     return x.Id.GetHashCode()^x.Events.Aggregate(0, (a, y) => a^y.GetHashCode()); 
    } 
} 

Teraz ten kod działa:

var items = new List<SampleObject>() 
    { 
     new SampleObject() { Id = "Id", Events = new List<string>() { "ExampleEvent"} }, 
     new SampleObject() { Id = "Id", Events = new List<string>() { "ExampleEvent" } } 
    }; 

    var comparer = new SampleObjectComparer(); 

    var duplicates = items.GroupBy(x => x, comparer) 
        .Where(g => g.Count() > 1) 
        .Select(g => g.Key) 
        .ToList(); 
1

List<T> ma zastąpiona Equals + GetHashCode, to dlaczego GroupBy nie działa zgodnie z oczekiwaniami. Jedna z dwóch właściwości anonimowego typu odnosi się do listy, kiedy GroupBy musi porównywać dwie listy: używa się Object.RefernceEquals, która sprawdza tylko, czy oba mają takie same referencje, a nie, czy oba zawierają przykładowe elementy.

Można zapewnić zwyczaj IEqualityComparer<T>:

public class IdEventComparer : IEqualityComparer<SampleObject> 
{ 
    public bool Equals(SampleObject x, SampleObject y) 
    { 
     if (object.ReferenceEquals(x, y)) 
      return true; 
     if (x == null || y == null) 
      return false; 
     if(x.Id != y.Id) 
      return false; 
     if (x.Events == null && y.Events == null) 
      return true; 
     if (x.Events == null || y.Events == null) 
      return false; 

     return x.Events.SequenceEqual(y.Events); 
    } 

    public int GetHashCode(SampleObject obj) 
    { 
     if(obj == null) return 23; 
     unchecked 
     { 
      int hash = 23; 
      hash = (hash * 31) + obj.Id == null ? 31 : obj.Id.GetHashCode(); 

      if (obj.Events == null) return hash; 
      foreach (string item in obj.Events) 
      { 
       hash = (hash * 31) + (item == null ? 0 : item.GetHashCode()); 
      } 
      return hash; 
     } 
    } 
} 

Wtedy można go używać na wiele sposobów LINQ jak również GroupBy:

var duplicates = items.GroupBy(x => x, new IdEventComparer()) 
    .Where(g => g.Count() > 1) 
    .Select(g => g.Key) 
    .ToList(); 
1

GroupBy() będzie wykonać porównanie domyślną, powodując to, aby znaleźć listy nie są równe.

Zobacz następujący kod:

listy
var eventList1 = new List<string>() { "ExampleEvent" }; 
var eventList2 = new List<string>() { "ExampleEvent" }; 

Console.WriteLine(eventList1.GetHashCode()); 
Console.WriteLine(eventList2.GetHashCode()); 
Console.WriteLine(eventList1.Equals(eventList2)); 

Dwa "równe", prawda? Zostanie jednak wydrukowane:

796641852 
1064243573 
False 

Tak więc nie są one uważane za równe, a zatem nie są zgrupowane.

Musisz podać niestandardowego porównywalnika, który porówna odpowiednie właściwości obiektów. Zauważ, że jak wcześniej pokazano, List<T>.GetHashCode() nie reprezentuje poprawnie pozycji na liście.

Można to zrobić jako taki (z Good GetHashCode() override for List of Foo objects respecting the order i LINQ GroupBy on multiple ref-type fields; Custom EqualityComparer):

public class SampleObjectComparer : IEqualityComparer<SampleObject> 
{ 
    public bool Equals(SampleObject a, SampleObject b) 
    { 
     return a.Id == b.Id 
      && a.Events.SequenceEqual(b.Events); 
    } 

    public int GetHashCode(SampleObject a) 
    { 
     int hash = 17; 

     hash = hash * 23 + a.Id.GetHashCode(); 

     foreach (var evt in a.Events) 
     { 
      hash = hash * 31 + evt.GetHashCode(); 
     }   

     return hash; 
    } 
} 

i używać go tak:

var eventList1 = new List<string>() { "ExampleEvent" }; 
var eventList2 = new List<string>() { "ExampleEvent" }; 

var items = new List<SampleObject>() 
{ 
    new SampleObject() { Id = "Id", Events = eventList1 }, 
    new SampleObject() { Id = "Id", Events = eventList2 } 
}; 

var duplicates = items.GroupBy(x => x, new SampleObjectComparer()) 
       .Where(g => g.Count() > 1) 
       .Select(g => g.Key) 
       .ToList(); 

Console.WriteLine(duplicates.Count);