2012-04-03 27 views
5

Biorąc pod uwagę dwie listy różnych typów, czy można uczynić te typy wymienialnymi między sobą lub porównywalnymi (np. Z TypeConverter lub podobnym), aby zapytanie LINQ mogło je porównać? Widziałem inne podobne pytania na SO, ale nic, co wskazuje na to, że typy można wymieniać między sobą, aby rozwiązać problem.LINQ: Użyj .Except() na kolekcjach różnych typów, czyniąc je wymienialnymi/porównywalnymi?

Rodzaje Kolekcja:

public class Data 
{ 
    public int ID { get; set; } 
} 

public class ViewModel 
{ 
    private Data _data; 

    public ViewModel(Data data) 
    { 
     _data = data; 
    } 
} 

Pożądany Wykorzystanie:

public void DoMerge(ObservableCollection<ViewModel> destination, IEnumerable<Data> data) 
    { 
     // 1. Find items in data that don't already exist in destination 
     var newData = destination.Except(data); 

     // ... 
    } 

Wydaje się logiczne, że skoro wiem, jak porównać wystąpienie ViewModel do instancji dane powinny być w stanie zapewnić niektóre logiki porównania, które LINQ następnie użyłby dla zapytań takich jak .Except(). czy to możliwe?

+1

Zła, stara pętla 'for', kiedyś była tak użyteczna, ale, niestety, nigdy nie uszczęśliwia ludzi z jednym liniowcem. – Marc

+2

@Marc: Nie zgadzam się z sentymentem, który wyrażasz. Mamy teraz sposoby pisania kodu, które wyraźniej wyrażają intencje, nie martwiąc się o mechanizm. 'for' wyraża mechanizmy i ukrywa intencje. Oparte na LINQ jedno-liniowce, które często dekodujesz (tak, nie zawsze) lepiej wyrażają intencje i ukrywają mechanizmy. Prowadzi to do kodu, który jest łatwiejszy do zrozumienia i utrzymania. – jason

+1

@Jason, podczas gdy byłem niepoważny, wszelkie funkcje, które rzucasz na projekcję taką jak ty, zapewniają jedynie założenie intencji. – Marc

Odpowiedz

4

Najprościej jest dostarczenie projekcję z Data do ViewModel tak że można powiedzieć

var newData = destination.Except(data.Select(x => f(x))); 

gdzie f odwzorowuje Data do ViewModel. Będziesz także potrzebował IEqualityComparer<Data>.

4

Zakładam, że dostarczenie projekcji od Data do ViewModel jest problematyczne, więc oferuję inne rozwiązanie oprócz Jason'a.

Oprócz użycia zestawu hash (jeśli dobrze pamiętam), można uzyskać podobną wydajność, tworząc własny hashset. Zakładam również, że identyfikujesz obiekty Data jako równe, gdy ich IDs są równe.

var oldIDs = new HashSet<int>(data.Select(d => d.ID)); 
var newData = destination.Where(vm => !oldIDs.Contains(vm.Data.ID)); 

Możecie mieć inne zastosowanie dla kolekcji „OldData” gdzie indziej w sposobie, w tym przypadku, co chcesz to zrobić w zamian. Albo wdrożyć IEquatable<Data> na swojej klasy danych lub utworzyć niestandardowy IEqualityComparer<Data> dla zestawu hash:

var oldData = new HashSet<Data>(data); 
//or: var oldData = new HashSet<Data>(data, new DataEqualityComparer()); 
var newData = destination.Where(vm => !oldData.Contains(vm.Data)); 
+0

Dlaczego miałoby to być problematyczne? 'ViewModel' ma konstruktor, który pobiera' Dane'! – jason

+0

Mam na myśli "problematyczny" dla wydajności, lub filozoficznie, nie w sensie ustalania, jak napisać kod. Pod względem wydajności klasa 'ViewModel' może wymagać wiele innych kosztów związanych z jej budową. Filozoficznie, wydaje się dziwne stworzenie grupy obiektów, abyśmy mogli użyć 'Z wyjątkiem' do zaznaczenia niektórych obiektów, które nie spełniają określonych kryteriów. – phoog

+0

'points.All (point => point.IsGoodPoint)' evalute to "true". – jason

3

Jeśli używasz to:

var newData = destination.Except(data.Select(x => f(x))); 

Musisz projektu „dane” do samego rodzaju zawartej w " destination”, ale przy użyciu kodu poniżej ciebie mógł pozbyć się tego ograniczenia:

//Here is how you can compare two different sets. 
class A { public string Bar { get; set; } } 
class B { public string Foo { get; set; } } 

IEnumerable<A> setOfA = new A[] { /*...*/ }; 
IEnumerable<B> setOfB = new B[] { /*...*/ }; 
var subSetOfA1 = setOfA.Except(setOfB, a => a.Bar, b => b.Foo); 

//alternatively you can do it with a custom EqualityComparer, if your not case sensitive for instance. 
var subSetOfA2 = setOfA.Except(setOfB, a => a.Bar, b => b.Foo, StringComparer.OrdinalIgnoreCase); 

//Here is the extension class definition allowing you to use the code above 
public static class IEnumerableExtension 
{ 
    public static IEnumerable<TFirst> Except<TFirst, TSecond, TCompared>(
     this IEnumerable<TFirst> first, 
     IEnumerable<TSecond> second, 
     Func<TFirst, TCompared> firstSelect, 
     Func<TSecond, TCompared> secondSelect) 
    { 
     return Except(first, second, firstSelect, secondSelect, EqualityComparer<TCompared>.Default); 
    } 

    public static IEnumerable<TFirst> Except<TFirst, TSecond, TCompared>(
     this IEnumerable<TFirst> first, 
     IEnumerable<TSecond> second, 
     Func<TFirst, TCompared> firstSelect, 
     Func<TSecond, TCompared> secondSelect, 
     IEqualityComparer<TCompared> comparer) 
    { 
     if (first == null) 
      throw new ArgumentNullException("first"); 
     if (second == null) 
      throw new ArgumentNullException("second"); 
     return ExceptIterator<TFirst, TSecond, TCompared>(first, second, firstSelect, secondSelect, comparer); 
    } 

    private static IEnumerable<TFirst> ExceptIterator<TFirst, TSecond, TCompared>(
     IEnumerable<TFirst> first, 
     IEnumerable<TSecond> second, 
     Func<TFirst, TCompared> firstSelect, 
     Func<TSecond, TCompared> secondSelect, 
     IEqualityComparer<TCompared> comparer) 
    { 
     HashSet<TCompared> set = new HashSet<TCompared>(second.Select(secondSelect), comparer); 
     foreach (TFirst tSource1 in first) 
      if (set.Add(firstSelect(tSource1))) 
       yield return tSource1; 
    } 
} 

Niektórzy mogą twierdzić, że to pamięć nieefektywne ze względu na zastosowanie HashSet. Ale faktycznie metoda Enumerable.Except ramy robi to samo z podobną klasą wewnętrzną o nazwie "Set" (przyjrzałem się przez dekompilację).

Powiązane problemy