[.NET] Linq et type dynamique, attention au piège en .NET

Problème

Le code suivant affiche « Numbers greater than 3 are : 6 7 » au lieu du « Numbers greater than 3 are : 4 5 6 7 » attendu.

Savez-vous pourquoi ?

class Program
{
    static void Main(string[] args)
    {
        List<Int32> allNumbers = new List<Int32>() { 1, 2, 3, 4, 5, 6, 7 };

        List<dynamic> greaterNumbers = new List<dynamic>();
    
        // Finding all numbers from the "allNumbers" list that are greater 
        // than "currentNumber"
        for (Int32 currentNumber = 0; currentNumber < 5; currentNumber++)
        {
            dynamic item = new ExpandoObject();
            item.Number = currentNumber;
            item.GreaterNumbers = allNumbers.Where(e => e > currentNumber);
            greaterNumbers.Add(item);
        }
        // Here greaterNumbers contains 5 items
    
        // We want to display numbers greater than 3
        dynamic greaterThan3 = greaterNumbers
                               .FirstOrDefault(i => i.Number == 3);
    
        if (greaterThan3 != null)
        {
            Console.Write("Numbers greater than 3 are : ");
            foreach (Int32 greaterNumber in greaterThan3.GreaterNumbers)
            {
                Console.Write(greaterNumber);
                Console.Write(' ');
            }
            Console.WriteLine();
        }
    }
}

Explication

Where, est une méthode d’extension proposée par LINQ. Cette méthode s’exécute en différé, c’est-à-dire que le Where ne sera exécuté que lorsqu’on voudra accéder à son résultat. Cette exécution différée est causée par la présence du mot clé « yield » dans l’implémentation de la méthode Where.

Exemple concret de l’utilisation du yield tiré de http://weblogs.asp.net/dixin/archive/2010/03/16/understanding-linq-to-objects-6-deferred-execution.aspx :

Méthode à exécution immédiate :

public static IEnumerable<string> GetMessages()
{
    // string[] collection is IEnumerable<string>.
    return new string[]             
    { 
        // ToString() executes immediately.
        1.ToString(CultureInfo.InvariantCulture), 
        // ToString() executes immediately.
        2.ToString(CultureInfo.InvariantCulture),
        // ToString() executes immediately.
        3.ToString(CultureInfo.InvariantCulture) 
    };
}

La même méthode mais avec une exécution différée :

// Returns a collection which can be iterated.
public static IEnumerable<string> GetMessages()
{
    // The multiple yield returns execute when iterated in a foreach:

    // MoveNext() is invoked and returns true.
    // ToString() is deferred.
    yield return 1.ToString(CultureInfo.InvariantCulture); 
    // Iteration 1 gets item "1" from collection by calling Current.
    
    // MoveNext() is invoked and returns true.
    // ToString() is deferred.
    yield return 2.ToString(CultureInfo.InvariantCulture); 
    // Iteration 2 gets item "2" from collection by calling Current.
    
    // MoveNext() is invoked and returns true.
    // ToString() is deferred.
    yield return 3.ToString(CultureInfo.InvariantCulture); 
    // Iteration 3 gets item "3" from collection by calling Current.
    
    // MoveNext() is invoked and returns false, 
    // because there is no more yield return to reach.
}

Dans la 2eme méthode, les ToString() ne seront exécutés que lors de l’utilisation du résultat de la méthode GetMessages().

Dans notre cas, le résultat produit par allNumbers.Where(e => e > currentNumber) n’est pas utilisé immédiatement, la méthode n’est donc pas exécutée tout de suite.

Le résultat est utilisé lors du foreach (Int32 greaterNumber in greaterThan3.GreaterNumbers)

A ce moment, la dernière valeur de currentNumber est 5 (dernière itération du for), le code exécuté est doncitem.GreaterNumbers = allNumbers.Where(e => e > 5)et retourne une IEnumerable qui contient les valeurs 6 et 7.

La méthode Where de LINQ retourne une IEnumerable qui sera stockée dans la propriété GreatNumbers.

Ici on utilise un objet dynamic qui sera évalué au runtime. La propriété GreatNumbers n’est donc pas typée au moment de la compilation ce qui constitue un piège.

Dans le cas d’un objet « classique », on aurait probablement typé cette propriété en List<Int32>, ce qui aurait provoqué une erreur à la compilation (problème de cast entre IEnumerable<T> et List<T>).

Pour provoquer l’exécution de notre Where au moment où on l’appelle et donc obtenir le résultat que l’on souhaite, il faut simplement rajouter un .ToList() sur le résultat du Where() :

for (Int32 currentNumber = 0; currentNumber < 5; currentNumber++)
{
    dynamic item = new ExpandoObject();
    item.Number = currentNumber;
    item.GreaterNumbers = allNumbers.Where(e => e > currentNumber)
                                    .ToList();
    greaterNumbers.Add(item);
}

Voici la liste des méthodes de LINQ qui retournent immédiatement un résultat (non différé) :

  • Agrégation : Aggregate, Average, Count, LongCount, Sum
  • Evaluation : ElementAt, ElementAtOrDefault, FirstOrDefault, LastOrDefault, First, Last, SingleOrDefault, Max, Min
  • Transformation _: ToArray, ToDictionary, ToList
  • Autre : Contains, SequenceEqual

Il y a un problème de performance si on appelle n fois greaterThan3.GreaterNumbers puisque le Where sera également appelé n fois


Voir également