2013-06-13 15 views
5

Metoda LINQ Join() z Nullable<int> dla TKey pomija null dopasowania klawiszy. Czego brakuje mi w dokumentacji? Wiem, że mogę przełączyć się na SelectMany(), jestem po prostu ciekawy, dlaczego ta operacja równości działa jak SQL, a nie jak C#, ponieważ tak blisko, jak mogę powiedzieć, EqualityComparer<int?>.Default działa dokładnie tak, jak oczekiwałbym tego dla wartości null.LINQ Przyłącz się do klucza Nullable

http://msdn.microsoft.com/en-us/library/bb534675.aspx

using System; 
using System.IO; 
using System.Linq; 
using System.Collections.Generic; 

public class dt 
{ 
    public int? Id; 
    public string Data; 
} 

public class JoinTest 
{ 
    public static int Main(string [] args) 
    { 
     var a = new List<dt> 
     { 
      new dt { Id = null, Data = "null" }, 
      new dt { Id = 1, Data = "1" }, 
      new dt { Id = 2, Data = "2" } 
     }; 

     var b = new List<dt> 
     { 
      new dt { Id = null, Data = "NULL" }, 
      new dt { Id = 2, Data = "two" }, 
      new dt { Id = 3, Data = "three" } 
     }; 

     //Join with null elements 
     var c = a.Join(b, 
      dtA => dtA.Id, 
      dtB => dtB.Id, 
      (dtA, dtB) => new { aData = dtA.Data, bData = dtB.Data }).ToList(); 
     // Output: 
     // 2 two 
     foreach (var aC in c) 
      Console.WriteLine(aC.aData + " " + aC.bData); 
     Console.WriteLine(" "); 

     //Join with null elements converted to zero 
     c = a.Join(b, 
      dtA => dtA.Id.GetValueOrDefault(), 
      dtB => dtB.Id.GetValueOrDefault(), 
      (dtA, dtB) => new { aData = dtA.Data, bData = dtB.Data }).ToList(); 

     // Output: 
     // null NULL 
     // 2 two 
     foreach (var aC in c) 
      Console.WriteLine(aC.aData + " " + aC.bData); 

     Console.WriteLine(EqualityComparer<int?>.Default.Equals(a[0].Id, b[0].Id)); 
     Console.WriteLine(EqualityComparer<object>.Default.Equals(a[0].Id, b[0].Id)); 
     Console.WriteLine(a[0].Id.Equals(b[0].Id)); 

     return 0; 
    } 
} 

Odpowiedz

4

Enumerable.Join wykorzystuje JoinIterator (klasa prywatny) iteracyjne nad pasujących elementów. JoinIterator wykorzystuje Lookup<TKey, TElement> tworzenia wyszukiwań kluczy sekwencji:

internal static Lookup<TKey, TElement> CreateForJoin(
    IEnumerable<TElement> source, 
    Func<TElement, TKey> keySelector, 
    IEqualityComparer<TKey> comparer) 
{ 
    Lookup<TKey, TElement> lookup = new Lookup<TKey, TElement>(comparer); 
    foreach (TElement local in source) 
    { 
     TKey key = keySelector(local); 
     if (key != null) // <--- Here 
     { 
      lookup.GetGrouping(key, true).Add(local); 
     } 
    } 
    return lookup; 
} 

interesujących części o przeskakuje klucze, które są null. Dlatego bez podania wartości domyślnej masz tylko jedno dopasowanie.


Wygląda na to, że znalazłem przyczynę takiego zachowania. Lookup używa domyślnego EqualityComparer, która będzie zwracać 0 zarówno dla klucza, który jest null i klucz, który jest 0:

int? keyA = 0; 
var comparer = EqualityComparer<int?>.Default; 
int hashA = comparer.GetHashCode(keyA) & 0x7fffffff; // from Lookup class 
int? keyB = null; 
int hashB = comparer.GetHashCode(keyB) & 0x7fffffff; 
Console.WriteLine(hashA); // 0 
Console.WriteLine(hashB); // 0 

Ewentualnie null pomijane, aby uniknąć dopasowywania null i 0 klucze.

+0

Czy tęskniłem za tym w dokumentacji gdziekolwiek? – ryancerium

+0

@ryancerium właśnie sprawdzone msdn, wygląda na to, że nie ma wzmianki o tym zachowaniu. Ale szukam źródeł z Reflectorem i widzę tę implementację .. –

+0

To wydaje się dość poważnym pominięciem w dokumentacji do mnie. Dzięki, że mi się przyglądasz. – ryancerium

0

myślę to zrobić w taki sposób, aby dopasować gdy zachowanie baz you can't join on null keys, they are just ignored. There are workarounds, aby ominąć to ograniczenie, którego niestety nie można zapisać w LINQ.

Będziesz musiał napisać zapytanie w taki sposób, że żaden z kluczy nie jest rzeczywiście pusty. Możesz to zrobić po prostu przez zawijanie wartości w innym obiekcie, który można porównać dla równości (np. Krotki lub obiekt anonimowy).

//Join with null elements 
var c = a.Join(b, 
    dtA => Tuple.Create(dtA.Id), 
    dtB => Tuple.Create(dtB.Id), 
    (dtA, dtB) => new { aData = dtA.Data, bData = dtB.Data }).ToList(); 
+0

Sprytne obejście. – ryancerium

+0

Ups, domyślam się, że źle odczytałem twoje pytanie. Myślałem, że pytasz, dlaczego tak się zachowywało, a nie dlaczego implementacja LINQ-to-Object zachowywała się jak implementacje LINQ-to-Entities/-SQL. Odpowiedź na to, spójność. –