2016-11-10 3 views
10

J'ai rencontré un problème étrange et je me demande ce que je devrais faire à ce sujet.Assurez-vous que l'exécution différée ne sera exécutée qu'une seule fois ou sinon

J'ai cette classe qui retourne un IEnumerable<MyClass> et c'est une exécution différée. En ce moment, il y a deux consommateurs possibles. L'un d'eux trie le résultat.

Voir l'exemple suivant:

public class SomeClass 
{ 
    public IEnumerable<MyClass> GetMyStuff(Param givenParam) 
    { 
     double culmulativeSum = 0; 
     return myStuff.Where(...) 
         .OrderBy(...) 
         .TakeWhile(o => 
         { 
          bool returnValue = culmulativeSum < givenParam.Maximum; 
          culmulativeSum += o.SomeNumericValue; 
          return returnValue; 
         }; 
    } 
} 

Les consommateurs appellent l'exécution différée qu'une seule fois, mais si elles devaient l'appeler plus que cela, le résultat serait erroné que le culmulativeSum ne serait pas remis à zéro. J'ai trouvé le problème par inadvertance avec des tests unitaires. Le moyen le plus simple pour moi de résoudre le problème serait d'ajouter .ToArray() et de supprimer l'exécution différée au prix d'un peu de temps système.

Je pourrais également ajouter un test unitaire dans la catégorie des consommateurs pour m'assurer qu'ils ne l'appellent qu'une seule fois, mais cela n'empêcherait aucun nouveau consommateur de coder à l'avenir de ce problème potentiel.

Une autre chose qui m'est venue à l'esprit était de faire jeter l'exécution suivante. Quelque chose comme

return myStuff.Where(...) 
     .OrderBy(...) 
     .TakeWhile(...) 
     .ThrowIfExecutedMoreThan(1); 

Il est évident que cela n'existe pas. Serait-ce une bonne idée de mettre en œuvre une telle chose et comment le feriez-vous? Sinon, s'il y a un gros éléphant rose que je ne vois pas, le signaler sera apprécié. (Je sens qu'il ya une parce que cette question est d'un scénario très basique: |)

EDIT:

Voici un mauvais exemple d'utilisation des consommateurs:

public class ConsumerClass 
{ 
    public void WhatEverMethod() 
    { 
     SomeClass some = new SomeClass(); 
     var stuffs = some.GetMyStuff(param); 
     var nb = stuffs.Count(); //first deferred execution 
     var firstOne = stuff.First(); //second deferred execution with the culmulativeSum not reset 
    } 
} 
+0

Parlez-vous d'une situation où plusieurs threads appellent cette fonction en même temps? Sinon, je ne vois pas comment cela pourrait vous donner un mauvais résultat. culmulativeSum est remis à zéro en haut et il semble être local à la fonction. – JuanR

+2

Voilà pourquoi c'est une mauvaise idée de laisser les méthodes LINQ avoir des effets secondaires. Bien que le code semble simple, ce que vous faites n'est pas vraiment un «scénario de base», et certainement pas un scénario recommandé. – Groo

+1

@Juan: le 'cumulativeSum' est défini sur' 0' chaque fois que vous appelez 'GetMyStuff', mais PAS chaque fois que vous enumerez le résultat. Parce que chaque fois que vous énumérez, vous n'évaluez que la partie LINQ après le 'return'. Par conséquent, chaque heure future que vous énumérez ne récupérera rien car 'cumulativeSum' est déjà plus grand que le maximum. Démonstration: http://ideone.com/VgLbTe. – mellamokb

Répondre

11

Vous pouvez résoudre le problème de résultat incorrect en tournant simplement votre méthode en iterator:

double culmulativeSum = 0; 
var query = myStuff.Where(...) 
     .OrderBy(...) 
     .TakeWhile(...); 
foreach (var item in query) yield return item; 

Il peut être encapsulé dans une méthode simple d'extension:

public static class Iterators 
{ 
    public static IEnumerable<T> Lazy<T>(Func<IEnumerable<T>> source) 
    { 
     foreach (var item in source()) 
      yield return item; 
    } 
} 

Alors tout ce que vous devez faire de tels scénarios consiste à entourer le corps de la méthode d'origine avec appel Iterators.Lazy, par exemple:

return Iterators.Lazy(() => 
{ 
    double culmulativeSum = 0; 
    return myStuff.Where(...) 
      .OrderBy(...) 
      .TakeWhile(...); 
}); 
+0

Impressionnant !, l'ensemble de 'GetMyStuff' est différé. – AXMIM

+0

Dans le scénario exact décrit dans la question, cette réponse est très appropriée. Mais gardez juste à l'esprit que l'énumérable est encore évalué plusieurs fois. Dans de nombreux cas, ce ne serait pas idéal. Ma réponse ci-dessous permet à la fois l'évaluation paresseuse et mise en cache. –

+1

@AndrewHanlon L'évaluation paresseuse et mise en cache sont des concepts différents. Bien sûr, la mise en cache pourrait être utile, mais cela devrait être la décision du consommateur, pas l'implémenteur. Si la requête n'avait pas d'effets secondaires, elle serait évaluée plusieurs fois de toute façon. Ce que j'essaie de garder et d'utiliser l'évaluation paresseuse seulement pour éviter les effets secondaires de l'implémentation, que le consommateur ne connaît pas. Le consommateur peut toujours faire 'ToList',' ToArray' ou appeler une méthode comme la vôtre si nécessaire. L'implémentation elle-même ne nécessite pas de mise en cache, donc ne devrait pas le faire. –

6

Vous pouvez utiliser la classe suivante:

public class JustOnceOrElseEnumerable<T> : IEnumerable<T> 
{ 
    private readonly IEnumerable<T> decorated; 

    public JustOnceOrElseEnumerable(IEnumerable<T> decorated) 
    { 
     this.decorated = decorated; 
    } 

    private bool CalledAlready; 

    public IEnumerator<T> GetEnumerator() 
    { 
     if (CalledAlready) 
      throw new Exception("Enumerated already"); 

     CalledAlready = true; 

     return decorated.GetEnumerator(); 
    } 

    IEnumerator IEnumerable.GetEnumerator() 
    { 
     if (CalledAlready) 
      throw new Exception("Enumerated already"); 

     CalledAlready = true; 

     return decorated.GetEnumerator(); 
    } 
} 

à decorate une dénombrable afin qu'il ne peut être recensée qu'une fois. Après cela, il y aurait une exception.

Vous pouvez utiliser cette classe comme ceci:

return new JustOnceOrElseEnumerable(
    myStuff.Where(...) 
    ... 
    ); 

S'il vous plaît noter que je ne recommande pas cette approche car elle viole le contrat de l'interface IEnumerable et donc le Liskov Substitution Principle. Il est légal pour les consommateurs de ce contrat de supposer qu'ils peuvent énumérer l'énumérable autant de fois qu'ils le souhaitent. Au lieu de cela, vous pouvez utiliser un énumérable mis en cache qui met en cache le résultat de l'énumération. Cela garantit que l'énumérable est seulement énuméré une fois et que toutes les tentatives d'énumération suivantes liront à partir du cache. Voir this answer ici pour plus d'informations.

+0

Bonnes choses et bonne recommandation. C'est en fait exagéré pour mon scénario, mais je pourrais l'utiliser ailleurs sur le chemin. – AXMIM

4

La réponse d'Ivan est très appropriée pour le problème sous-jacent dans l'exemple d'OP - mais pour le cas général, j'ai déjà abordé cette question par le passé en utilisant une méthode d'extension similaire à celle ci-dessous. Cela garantit que le Enumerable a une seule évaluation, mais est également reporté:

public static IMemoizedEnumerable<T> Memoize<T>(this IEnumerable<T> source) 
{ 
    return new MemoizedEnumerable<T>(source); 
} 

private class MemoizedEnumerable<T> : IMemoizedEnumerable<T>, IDisposable 
{ 
    private readonly IEnumerator<T> _sourceEnumerator; 
    private readonly List<T> _cache = new List<T>(); 

    public MemoizedEnumerable(IEnumerable<T> source) 
    { 
     _sourceEnumerator = source.GetEnumerator(); 
    } 

    public IEnumerator<T> GetEnumerator() 
    { 
     return IsMaterialized ? _cache.GetEnumerator() : Enumerate(); 
    } 

    private IEnumerator<T> Enumerate() 
    { 
     foreach (var value in _cache) 
     { 
      yield return value; 
     } 

     while (_sourceEnumerator.MoveNext()) 
     { 
      _cache.Add(_sourceEnumerator.Current); 
      yield return _sourceEnumerator.Current; 
     } 

     _sourceEnumerator.Dispose(); 
     IsMaterialized = true; 
    } 

    IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); 

    public List<T> Materialize() 
    { 
     if (IsMaterialized) 
      return _cache; 

     while (_sourceEnumerator.MoveNext()) 
     { 
      _cache.Add(_sourceEnumerator.Current); 
     } 

     _sourceEnumerator.Dispose(); 
     IsMaterialized = true; 

     return _cache; 
    } 

    public bool IsMaterialized { get; private set; } 

    void IDisposable.Dispose() 
    { 
     if(!IsMaterialized) 
      _sourceEnumerator.Dispose(); 
    } 
} 

public interface IMemoizedEnumerable<T> : IEnumerable<T> 
{ 
    List<T> Materialize(); 

    bool IsMaterialized { get; } 
} 

Exemple d'utilisation:

void Consumer() 
{ 
    //var results = GetValuesComplex(); 
    //var results = GetValuesComplex().ToList(); 
    var results = GetValuesComplex().Memoize(); 

    if(results.Any(i => i == 3)) 
    { 
     Console.WriteLine("\nFirst Iteration"); 
     //return; //Potential for early exit. 
    } 

    var last = results.Last(); // Causes multiple enumeration in naive case.   

    Console.WriteLine("\nSecond Iteration"); 
} 

IEnumerable<int> GetValuesComplex() 
{ 
    for (int i = 0; i < 5; i++) 
    { 
     //... complex operations ...   
     Console.Write(i + ", "); 
     yield return i; 
    } 
} 
  • Naive: ✔ différés, ✘ énumération unique.
  • ToList: ✘ Différé, ✔ Enumération simple.
  • Mémo: ✔ Différé, ✔ Enumération simple.

.

Édité pour utiliser la terminologie appropriée et étoffer l'implémentation.