2013-06-13 12 views
5

Il metodo LINQ Join() con Nullable<int> per TKey ignora le corrispondenze di chiavi Null. Cosa mi manca nella documentazione? So che posso passare a SelectMany(), sono curioso di sapere perché questa operazione di uguaglianza funziona come SQL e non come C# poiché, per quanto posso dire, lo EqualityComparer<int?>.Default funziona esattamente come mi aspetterei per i valori nulli.LINQ Join su una chiave 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; 
    } 
} 

risposta

4

Enumerable.Join utilizza JoinIterator (classe privata) per iterare su elementi corrispondenti. JoinIterator utilizza Lookup<TKey, TElement> per la generazione di chiavi ricerche sequenza:

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; 
} 

parte interessante qui è saltare chiavi che sono null. Ecco perché senza fornire valore predefinito hai una sola corrispondenza.


Sembra che ho trovato la ragione di un simile comportamento. Ricerca utilizza EqualityComparer predefinita, che restituirà 0 sia per chiave che è null e la chiave che è 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 

Possibilmente nulli saltato evitare corrispondenza null e 0 chiavi.

+0

Mi sono perso nella documentazione ovunque? – ryancerium

+0

@ryancerium appena controllato msdn, sembra che non ci sia alcun accenno a questo comportamento. Ma sto cercando su sorgenti con Reflector e vedo questa implementazione .. –

+0

Sembra un'omissione abbastanza seria nella documentazione per me. Grazie per averlo esaminato per me. – ryancerium

0

Penso che sia fatto in questo modo per abbinare il comportamento delle banche dati in cui you can't join on null keys, they are just ignored. There are workarounds per superare questa limitazione che purtroppo non può essere scritta in LINQ.

È necessario scrivere la query in modo che nessuna delle chiavi sia effettivamente nulla. Puoi farlo semplicemente avvolgendo il valore in un altro oggetto che può essere confrontato per l'uguaglianza (ad es. Una tupla o un oggetto anonimo).

//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

Soluzione intelligente. – ryancerium

+0

Oops, credo di aver letto male la tua domanda. Ho pensato che stavi chiedendo perché si è comportato in questo modo, non perché l'implementazione LINQ-to-Objects si è comportata come implementazioni LINQ-to-Entities/-SQL. La risposta per questo, coerenza. –