Question

J'écris un programme comme suit:

  • Trouver tous les fichiers avec l'extension correcte dans un répertoire donné
  • Pour chaque recherche, trouver toutes les occurrences d'une chaîne donnée dans ces fichiers
  • Imprimer chaque ligne

J'aimerais écrire ceci de manière fonctionnelle, sous la forme d'une série de fonctions de générateur (les choses qui appellent yield return et ne renvoient qu'un élément à la fois, chargées paresseusement), afin que mon code se lise comme suit:

IEnumerable<string> allFiles = GetAllFiles();
IEnumerable<string> matchingFiles = GetMatches( "*.txt", allFiles );
IEnumerable<string> contents = GetFileContents( matchingFiles );
IEnumerable<string> matchingLines = GetMatchingLines( contents );

foreach( var lineText in matchingLines )
  Console.WriteLine( "Found: " + lineText );

Tout va bien, mais j'aimerais aussi imprimer des statistiques à la fin. Quelque chose comme ça:

Found 233 matches in 150 matching files. Scanned 3,297 total files in 5.72s

Le problème est que si vous écrivez le code dans un style "purement fonctionnel" comme ci-dessus, chaque élément est chargé paresseusement.
Vous savez seulement combien de fichiers correspondent au total jusqu'à la fin de la boucle foreach finale, et comme un seul élément est jamais yield édité à la fois, le code n'a pas d'endroit pour garder trace du nombre de choses qu'il a trouvées précédemment. Si vous appelez la méthode matchingLines.Count() de LINQ, la collection sera à nouveau énumérée!

Je peux penser à plusieurs façons de résoudre ce problème, mais elles semblent toutes un peu laides. Cela me semble être quelque chose que les gens doivent avoir déjà fait auparavant, et je suis sûr qu'il y aura un joli motif de conception qui illustre une meilleure pratique pour y parvenir.

Des idées? A la vôtre

Était-ce utile?

La solution

Dans la même veine que d'autres réponses, mais en adoptant une approche légèrement plus générique ...

... pourquoi ne pas créer une classe Decorator capable d'envelopper une implémentation IEnumerable existante et de calculer la statistique au fur et à mesure qu'elle transmet d'autres éléments.

Voici une classe Counter que je viens de réunir - mais vous pouvez également créer des variantes pour d'autres types d'agrégation.

public class Counter<T> : IEnumerable<T>
{
    public int Count { get; private set; }

    public Counter(IEnumerable<T> source)
    {
        mSource = source;
        Count = 0;
    }

    public IEnumerator<T> GetEnumerator()
    {
        foreach (var T in mSource)
        {
            Count++;
            yield return T;
        }
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        foreach (var T in mSource)
        {
            Count++;
            yield return T;
        }
    }

    private IEnumerable<T> mSource;
}

Vous pouvez créer trois instances de GetAllFiles():

  1. Un pour emballer GetMatches() en comptant le nombre total de fichiers;
  2. Celui à emballer GetMatchingLines() en comptant le nombre de fichiers correspondants; et
  3. Celui à encapsuler Mitcham en comptant le nombre de lignes correspondantes.

La clé de cette approche est que vous ne répartissez pas plusieurs responsabilités sur vos classes / méthodes existantes - la méthode <=> ne gère que la correspondance, vous ne lui demandez pas de suivre également les statistiques.

Clarification en réponse à un commentaire de <=>:

Le code final ressemblerait à quelque chose comme ceci:

var files = new Counter<string>( GetAllFiles());
var matchingFiles = new Counter<string>(GetMatches( "*.txt", files ));
var contents = GetFileContents( matchingFiles );
var linesFound = new Counter<string>(GetMatchingLines( contents ));

foreach( var lineText in linesFound )
    Console.WriteLine( "Found: " + lineText );

string message 
    = String.Format( 
        "Found {0} matches in {1} matching files. Scanned {2} files",
        linesFound.Count,
        matchingFiles.Count,
        files.Count);
Console.WriteLine(message);

Notez qu'il s'agit toujours d'une approche fonctionnelle: les variables utilisées sont immuables (plutôt des liaisons que des variables), et la fonction globale n'a aucun effet secondaire.

Autres conseils

Je dirais que vous devez encapsuler le processus dans une classe "Matcher" dans laquelle vos méthodes capturent des statistiques au fur et à mesure de leur progression.

public class Matcher
{
  private int totalFileCount;
  private int matchedCount;
  private DateTime start;
  private int lineCount;
  private DateTime stop;

  public IEnumerable<string> Match()
  {
     return GetMatchedFiles();
     System.Console.WriteLine(string.Format(
       "Found {0} matches in {1} matching files." + 
       " {2} total files scanned in {3}.", 
       lineCount, matchedCount, 
       totalFileCount, (stop-start).ToString());
  }

  private IEnumerable<File> GetMatchedFiles(string pattern)
  {
     foreach(File file in SomeFileRetrievalMethod())
     {
        totalFileCount++;
        if (MatchPattern(pattern,file.FileName))
        {
          matchedCount++;
          yield return file;
        }
     }
  }
}

Je vais m'arrêter ici car je suis censé coder du travail, mais l'idée générale est là. Le but du programme fonctionnel "pur" est de ne pas avoir d’effets secondaires, et ce type de calcul statique est un effet secondaire.

Je peux penser à deux idées

  1. Transmettez un objet de contexte et renvoyez (chaîne + contexte) à partir de vos énumérateurs - la solution purement fonctionnelle

  2. utilise le stockage local du thread pour vos statistiques ( CallContext ), vous pouvez être chic et prendre en charge une pile de contextes. donc vous auriez un code comme celui-ci.

    using (var stats = DirStats.Create())
    {
        IEnumerable<string> allFiles = GetAllFiles();
        IEnumerable<string> matchingFiles = GetMatches( "*.txt", allFiles );
        IEnumerable<string> contents = GetFileContents( matchingFiles );
        stats.Print()
        IEnumerable<string> matchingLines = GetMatchingLines( contents );
        stats.Print();
    } 
    

Si vous êtes prêt à transformer votre code, vous pouvez être intéressé par Push LINQ. L'idée de base est d'inverser le & "Pull &"; modèle de IEnumerable<T> et transformez-le en " push " modèle avec observateurs - chaque partie du pipeline pousse efficacement ses données au-delà d’un nombre quelconque d’observateurs (à l’aide de gestionnaires d’événements), qui forment généralement de nouvelles parties du pipeline. Cela donne un moyen très simple de relier plusieurs agrégats aux mêmes données.

Voir cette entrée de blog pour plus de détails. Il y a quelque temps, j'ai donné une conférence à ce sujet à Londres. Ma page de conférences contient quelques liens à titre d'exemple. code, le jeu de diapositives, la vidéo, etc.

C’est un petit projet amusant, mais il vous faut un peu de tête.

J'ai pris le code de Bevan et je l'ai remanié jusqu'à ce que je sois satisfait. Des choses amusantes.

public class Counter
{
    public int Count { get; set; }
}

public static class CounterExtensions
{
    public static IEnumerable<T> ObserveCount<T>
      (this IEnumerable<T> source, Counter count)
    {
        foreach (T t in source)
        {
            count.Count++;
            yield return t;
        }
    }

    public static IEnumerable<T> ObserveCount<T>
      (this IEnumerable<T> source, IList<Counter> counters)
    {
        Counter c = new Counter();
        counters.Add(c);
        return source.ObserveCount(c);
    }
}


public static class CounterTest
{
    public static void Test1()
    {
        IList<Counter> counters = new List<Counter>();
  //
        IEnumerable<int> step1 =
            Enumerable.Range(0, 100).ObserveCount(counters);
  //
        IEnumerable<int> step2 =
            step1.Where(i => i % 10 == 0).ObserveCount(counters);
  //
        IEnumerable<int> step3 =
            step2.Take(3).ObserveCount(counters);
  //
        step3.ToList();
        foreach (Counter c in counters)
        {
            Console.WriteLine(c.Count);
        }
    }
}

Résultat attendu: 21, 3, 3

En supposant que ces fonctions soient les vôtres, la seule chose à laquelle je puisse penser est le modèle de visiteur, qui transmet une fonction de visiteur abstrait qui vous rappelle chaque fois que cela se produit. Par exemple: passez un ILineVisitor à GetFileContents (ce qui, je suppose, divise le fichier en lignes). ILineVisitor aurait une méthode comme OnVisitLine (chaîne de caractères), vous pouvez ensuite implémenter ILineVisitor et lui faire conserver les statistiques appropriées. Rincez et répétez avec un ILineMatchVisitor, IFileVisitor, etc. Vous pouvez également utiliser un seul IVisitor avec une méthode OnVisit () qui a une sémantique différente dans chaque cas.

Vos fonctions doivent chacune prendre un visiteur et appeler c'est OnVisit () au moment opportun, ce qui peut paraître ennuyeux, mais au moins le visiteur pourrait être utilisé pour faire beaucoup de choses intéressantes, autres que ce que vous ' re faire ici. En fait, vous pouvez réellement éviter d'écrire GetMatchingLines en transmettant à GetFileContents un visiteur qui recherche la correspondance dans OnVisitLine (ligne de chaîne).

Est-ce l'une des choses laides que vous aviez déjà envisagées?

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top