2013-06-13 3 views
5

La méthode LINQ Join() avec Nullable<int> pour TKey ignore les correspondances null. Qu'est-ce qui me manque dans la documentation? Je sais que je peux passer à SelectMany(), je suis juste curieux de savoir pourquoi cette opération d'égalité fonctionne comme SQL et pas comme C# car aussi proche que je peux dire, le EqualityComparer<int?>.Default fonctionne exactement comme je l'attend pour des valeurs nulles.LINQ Joindre une clé 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; 
    } 
} 

Répondre

4

Enumerable.Join utilise JoinIterator (classe privée) pour itérer sur les éléments correspondants. JoinIterator utilise Lookup<TKey, TElement> pour créer des clés de recherches séquence:

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

partie intéressante ici est sauter les clés qui sont null. C'est pourquoi sans fournir de valeur par défaut, vous n'avez qu'une seule correspondance.


On dirait que j'ai trouvé la raison d'un tel comportement. Recherche utilise EqualityComparer par défaut, qui retournera 0 tant pour la clé qui est null et la clé qui est 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 

Peut-être sautée pour éviter nulls correspondant null et 0 clés.

+0

Ai-je manqué cela dans la documentation n'importe où? – ryancerium

+0

@ryancerium juste vérifié msdn, on dirait qu'il n'y a aucune mention de ce comportement. Mais je regarde des sources avec Reflector et vois cette implémentation. –

+0

Cela me semble une omission assez sérieuse dans la documentation. Merci d'avoir examiné cela pour moi. – ryancerium

0

Je pense qu'il est fait de cette façon de faire correspondre le comportement des bases de données où you can't join on null keys, they are just ignored. There are workarounds pour passer cette limitation qui malheureusement ne peut pas être écrit dans LINQ.

Vous devrez écrire votre requête de manière à ce qu'aucune des clés ne soit réellement nulle. Vous pouvez le faire simplement en enveloppant la valeur dans un autre objet qui peut être comparé pour l'égalité (par exemple, un tuple ou un objet anonyme).

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

Solution de contournement astucieuse. – ryancerium

+0

Oups, je suppose que j'ai mal interprété votre question. Je pensais que vous demandiez pourquoi il se comportait de cette façon, pas pourquoi l'implémentation LINQ-to-Objects se comportait comme les implémentations LINQ-to-Entities/-SQL. La réponse à cela, la cohérence. –