2009-07-18 7 views
1

Je suis tombé sur un bug intéressant avec linq à sql. Jetez un oeil sur le code ci-dessous qui est librement traduit à partir d'une requête LINQtoSQL à partir d'un moteur de recherche que j'écris.LINQ to SQL et les durées de vie des objets, les références et les valeurs

Le but de la requête est de trouver tous les groupes qui ont l'ID "Joe", "Jeff", "Jim" dans l'ordre consécutif.

Faites très attention aux variables nommées localKeyword et localInt. Si vous deviez supprimer les déclarations de ces apparemment variables locales inutiles et les remplacer par celles qu'ils mandatent, la requête ne fonctionnerait plus. Je suis encore un débutant avec linq to sql mais on dirait qu'il passe tous les locaux comme références. Cela entraîne la requête ayant uniquement la valeur de variables locales lorsque la requête est évaluée. Dans LINQ to SQL ma requête a fini par ressembler à

SELECT * FROM INDEX ONE, INDEX TWO, INDEX THREE 
    WHERE ONE.ID = 'Jim' and TWO.ID = 'Jim' and 
    TWO.SEQUENCE = ONE.SEQUENCE + 2 and 
    THREE.ID = 'Jim' and 
    THREE.SEQUENCE = ONE.SEQUENCE + 2 and 
    ONE.GROUP == TWO.GROUP and ONE.GROUP == THREE.GROUP 

La requête est bien sûr paraphrasée. Qu'est-ce qui se passe exactement, est-ce un bug? Je demande peut-être mieux comprendre pourquoi cela se produit. Vous devriez trouver le code compile en studio visuel 2008.

using System; 
using System.Collections.Generic; 
using System.Text; 
using System.Linq; 

namespace BreakLINQ 
{ 
    class Program 
    { 
     public struct DataForTest 
     { 
      private int _sequence; 
      private string _ID; 
      private string _group; 

      public int Sequence 
      { 
       get 
       { 
        return _sequence; 
       } 
       set 
       { 
        _sequence = value; 
       } 
      } 
      public string ID 
      { 
       get 
       { 
        return _ID; 
       } 
       set 
       { 
        _ID = value; 
       } 
      } 
      public string Group 
      { 
       get 
       { 
        return _group; 
       } 
       set 
       { 
        _group = value; 
       } 
      } 
     } 
     static void Main(string[] args) 
     { 
      List<DataForTest> elements = new List<DataForTest> 
      { 
       new DataForTest() { Sequence = 0, ID = "John", Group="Bored" }, 
       new DataForTest() { Sequence = 1, ID = "Joe", Group="Bored" }, 
       new DataForTest() { Sequence = 2, ID = "Jeff", Group="Bored" }, 
       new DataForTest() { Sequence = 3, ID = "Jim", Group="Bored" }, 
       new DataForTest() { Sequence = 1, ID = "Jim", Group="Happy" }, 
       new DataForTest() { Sequence = 2, ID = "Jack", Group="Happy" }, 
       new DataForTest() { Sequence = 3, ID = "Joe", Group="Happy" }, 
       new DataForTest() { Sequence = 1, ID = "John", Group="Sad" }, 
       new DataForTest() { Sequence = 2, ID = "Jeff", Group="Sad" }, 
       new DataForTest() { Sequence = 3, ID = "Jack", Group="Sad" } 
      }; 

      string[] order = new string[] { "Joe", "Jeff", "Jim" }; 
      int sequenceID = 0; 
      var query = from item in elements 
         select item; 
      foreach (string keyword in order) 
      { 
       if (sequenceID == 0) 
       { 
        string localKeyword = keyword; 
        query = from item in query 
          where item.ID == localKeyword 
          select item; 
       } 
       else 
       { 
        string localKeyword = keyword; 
        int localSequence = sequenceID; 
        query = from item in query 
          where (from secondItem in elements 
            where secondItem.Sequence == item.Sequence + localSequence && 
             secondItem.ID == localKeyword 
            select secondItem.Group).Contains(item.Group) 
          select item; 
       } 
       sequenceID++; 
      } 
     } 
    } 
} 

La valeur de la requête fois le code terminée doit avoir la valeur { « Joe », « Bored », 1}.

+0

Être pinailler, vous avez l'aide System.Linq; deux fois. –

+0

Je ne vois pas d'éléments LINQ to SQL dans votre code. C'est seulement LINQ aux objets. btw voir: http://stackoverflow.com/questions/1095707/what-is-the-exact-definition-of-a-closure/1095770#1095770 –

Répondre

3

La raison pour laquelle cela ne fonctionne pas sans les variables « mandatement » est que les variables sont capturées par les expressions dans la requête LINQ. Sans les proxies, chaque itération de la boucle fait référence aux deux mêmes variables (keyword, sequenceID), et lorsque la requête est finalement évaluée et exécutée, la valeur substituée pour chacune de ces références est identique; à savoir, quelle que soit la valeur présente dans ces variables lorsque la boucle se termine (ce qui est quand vous voulez que nous évaluons «requête»).

La requête se comporte comme prévu avec les proxies car les variables capturées sont déclarées de manière unique par itération de la boucle; les itérations suivantes ne modifient pas les variables capturées, car elles ne sont plus dans la portée. Les variables proxy ne sont pas inutiles du tout. En outre, ce comportement est inhérent à la conception; laissez-moi voir si je peux trouver un bon lien de référence ...

2
var correctQuery = 
    from o in elements 
    join tw in elements on o.Sequence equals tw.Sequence - 1 
    join th in elements on tw.Sequence equals th.Sequence - 1 
    where 
     o.ID == "Joe" && tw.ID == "Jeff" && th.ID == "Jim" && o.Group == tw.Group && 
     th.Group == tw.Group 
    select new {o.ID, o.Sequence, o.Group}; 
+0

Pour un moteur de recherche, vous avez besoin d'un moyen de le faire avec un tableau arbitraire des éléments, disons new string [] {"John", "joe", "Jeff", "Jim"}; – hannasm

+0

Cependant, j'ai été surpris à quel point cela s'avère être plus propre :) – hannasm

2

Ce n'est pas un bug, c'est "par conception". Qu'est-ce qui se passe sous le capot ici est que vous capturez la variable d'itération d'une boucle for dans une expression lambda. Il est en fait utilisé dans une requête mais sous le capot cela sera traduit en une expression lambda.

Dans une boucle for, il n'y a qu'une seule variable d'itération pour toutes les boucles. Pas un pour chaque itération de la boucle. Ainsi, chaque requête capture la même variable. Une fois exécutée, la requête s'exécutera sur la valeur actuelle, ou dans ce cas la dernière, stockée dans la variable d'itération.

La raison de votre astuce de variable temporaire est qu'il y aura essentiellement une instance de la variable temporaire pour chaque itération de la boucle. Chaque requête capture donc une valeur différente et indépendante.

Un exemple plus concis demo'ing ce problème est la suivante

var list = new List<Func<int>>(); 
foreach (var cur in Enumerable.Range(1,3)) { 
    list.Add(() => cur); 
} 
foreach (var lambda in list) { 
    Console.WriteLine(lambda()); // always prints 3 
} 
Questions connexes