2010-03-05 5 views
10

NOTE: droit avant de poster cette question, il me est apparu il y a une meilleure façon de faire ce que je voulais accomplir (et je me sens assez stupide à ce sujet):Qu'est-ce qui me manque dans cette chaîne de prédicats?

IEnumerable<string> checkedItems = ProductTypesList.CheckedItems.Cast<string>(); 
filter = p => checkedItems.Contains(p.ProductType); 

Alors OK, oui, je déjà réaliser cela. Cependant, je poste la question de toute façon, parce que je ne comprends toujours pas pourquoi ce que j'étais (stupidement) en essayant à faire ne fonctionnait pas.


Je pensais que ce serait extrêmement facile. Il s'avère que cela me donne mal à la tête.

L'idée de base: afficher tous les éléments dont la valeur de la propriété ProductType est vérifiée dans un CheckedListBox.

La mise en œuvre:

private Func<Product, bool> GetProductTypeFilter() { 
    // if nothing is checked, display nothing 
    Func<Product, bool> filter = p => false; 

    foreach (string pt in ProductTypesList.CheckedItems.Cast<string>()) { 
     Func<Product, bool> prevFilter = filter; 
     filter = p => (prevFilter(p) || p.ProductType == pt); 
    } 

    return filter; 
} 

Cependant, disent les articles "Capitaux propres" et "ETF" sont tous deux sélectionnés dans ProductTypesList (un CheckedListBox). Ensuite, pour une raison quelconque, le code suivant renvoie uniquement des produits de type « ETF »:

var filter = GetProductTypeFilter(); 
IEnumerable<Product> filteredProducts = allProducts.Where(filter); 

Je l'ai deviné aurait pu avoir quelque chose à voir avec une messiness auto-référentielle où filter est réglé, essentiellement, se ou autre chose. Et je pensais que peut-être en utilisant ...

filter = new Func<Product, bool>(p => (prevFilter(p) || p.ProductType == pt)); 

... ferait l'affaire, mais pas de chance. Quelqu'un peut-il voir ce qui me manque ici?

Répondre

9

Je crois que vous avez un problème de fermeture modifié ici. Le paramètre pt est lié à l'expression lambda mais change à mesure que la boucle progresse. Il est important de réaliser le quand une variable est référencée dans un lambda c'est la variable qui est capturée, pas la valeur de la variable.

Dans les boucles, cela a une ramification très importante - parce que la variable de boucle change et n'est pas redéfinie. En créant une variable à l'intérieur de la boucle, vous créez une nouvelle variable pour chaque itération, ce qui permet au lambda de les capturer indépendamment.

La mise en œuvre souhaitée serait:

foreach (string pt in ProductTypesList.CheckedItems.Cast<string>()) { 
    string ptCheck = pt; 
    Func<Product, bool> prevFilter = filter; 
    filter = p => (prevFilter(p) || p.ProductType == ptCheck); 
} 

Eric Lippert a écrit au sujet de cette situation particulière:

A Aussi, voir la question Access to Modified Closure (2) pour une bonne explication de ce qui se passe avec les variables de fermeture.Il y a aussi une série d'articles sur le blog The Old New Thing qui a une perspective intéressante sur ce point:

+0

Eh bien, c'est tout à fait logique. Merci! –

2

Il a à voir avec les fermetures . La variable pt se référera toujours à la dernière valeur de la boucle for.

Considérons l'exemple suivant où la sortie est celle attendue car elle utilise une variable qui est comprise dans la boucle for.

public static void Main(string[] args) 
{ 
    var countries = new List<string>() { "pt", "en", "sp" }; 

    var filter = GetFilter(); 

    Console.WriteLine(String.Join(", ", countries.Where(filter).ToArray())); 
} 

private static Func<string, bool> GetFilter() 
{ 
    Func<string, bool> filter = p => false; 

    foreach (string pt in new string[] { "pt", "en" }) 
    { 
     Func<string, bool> prevFilter = filter; 

     string name = pt; 

     filter = p => (prevFilter(p) || p == name); 
    } 

    return filter; 
} 
2

Puisque vous en boucle et régler le type de filtre lui-même, vous définissez le type de produit au dernier pt dans chaque cas. Il est une fermeture modifiée et puisqu'il est retard lié, vous devez copier sur chaque boucle, comme ceci:

foreach (string pt in ProductTypesList.CheckedItems.Cast<string>()) { 
    var mypt = pt; 
    Func<Product, bool> prevFilter = filter; 
    filter = p => (prevFilter(p) || p.ProductType == mypt); 
} 

Cela devrait se traduire par le bon résultat, sinon on utilise la dernière pt pour tous les contrôles à l'égalité.

Questions connexes