Domanda

Sto scrivendo un programma come segue:

  • Trovare tutti i file con l'estensione corretta in una determinata directory
  • Foreach, trovare tutte le occorrenze di una stringa data in quei file
  • La stampa di ciascuna linea

Mi piacerebbe scrivere questo in modo funzionale, come una serie di generatore di funzioni (cose che chiamare yield return e restituire solo un elemento alla volta caricati in modo pigro), quindi il mio codice vorresti leggere in questo modo:

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 );

Questo è tutto bene, ma quello che mi piacerebbe fare è stampare alcune statistiche alla fine.Qualcosa di simile a questo:

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

Il problema è che scrivere il codice in un 'puramente funzionale' stile come sopra, ogni elemento è pigramente caricato.
Si conosce solo il numero di file che corrispondono in totale, fino all'ultimo ciclo foreach completa, e perché solo una voce che è sempre yielded in un momento, il codice non ha alcun posto per tenere traccia di quante cose si è trovato in precedenza.Se si richiama del LINQ matchingLines.Count() metodo, si ri-enumerare la collezione!

Posso pensare a molti modi per risolvere questo problema, ma tutti sembrano essere un po ' brutto.Mi colpisce come qualcosa che le persone sono tenuti ad avere fatto prima, e sono sicuro che ci sarà un bel design pattern, che mostra una best practice modo di fare questo.

Tutte le idee?Cheers

È stato utile?

Soluzione

In una vena simile ad altre risposte, ma prendendo un po ' più generico di approccio ...

...perché non creare un Decoratore classe che può avvolgere un esistente IEnumerable attuazione e calcolare la statistica che passa in altri articoli.

Ecco un Counter classe ho messo insieme - ma si potrebbe creare variazioni per altri tipi di aggregazione troppo.

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;
}

È possibile creare tre istanze di Counter:

  1. Uno per avvolgere GetAllFiles() contare il numero totale di file;
  2. Uno per avvolgere GetMatches() contare il numero di file corrispondenti;e
  3. Uno per avvolgere GetMatchingLines() contare il numero di linee di corrispondenza.

La chiave di questo approccio è che non hai la stratificazione di più responsabilità sul vostro esistente classi/metodi - la GetMatchingLines() metodo solo gestisce la corrispondenza, non stai chiedendo di tenere traccia delle statistiche così.

Chiarimento in risposta a un commento di Mitcham:

Il codice sarebbe simile a questa:

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);

Si noti che questo è ancora un approccio di tipo funzionale - le variabili utilizzate sono immutabile (di più come associazioni che variabili), e in generale la funzione non ha effetti collaterali.

Altri suggerimenti

Direi che è necessario per definire il processo in un 'Matcher' di classe in cui i vostri metodi statistiche di acquisizione in corso.

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;
        }
     }
  }
}

Mi fermo qui perché dovrei essere lavoro di codifica roba, ma l'idea generale è lì.L'intero punto di 'puro' programma funzionale è quello di non avere effetti collaterali, e questo tipo di schema statico di calcolo è un effetto collaterale.

Posso pensare a due idee

  1. Passare in un contesto in oggetto e ritorno (stringa + contesto) dal enumeratori, puramente funzionale soluzione

  2. utilizzare thread local storage per le statistiche (CallContexta ), può essere di fantasia e il supporto di una pila di contesti.così si avrebbe un codice come questo.

    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();
    } 
    

Se sei felice di girare il tuo codice a testa in giù, si potrebbe essere interessati a Spingere LINQ.L'idea di base è di invertire il modello "pull" di IEnumerable<T> e di trasformarlo in un "push" modello con osservatori - ogni parte della pipeline spinge efficacemente i suoi dati passato qualsiasi numero di osservatori (utilizzando gestori di eventi), che in genere si formano nuove parti della pipeline.Questo fornisce un modo molto semplice per collegare più aggregati agli stessi dati.

Vedere questo blog per qualche dettaglio in più.Ho fatto un discorso su di esso, a Londra, un po ' di tempo fa - il mio pagina di colloqui ha un paio di link per un esempio di codice, le slide, video, etc.

E 'un po' di divertimento in progetto, ma ci vuole un po ' di ottenere la vostra testa intorno.

Ho preso Bevan codice e refactoring intorno fino a quando mi è stato contenuto.Roba di divertimento.

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);
        }
    }
}

Uscita come previsto:21, 3, 3

Supponendo che tali funzioni sono proprio, l'unica cosa che posso pensare è il Visitor pattern, passando in un astratto visitatore funzione che ti richiama quando ogni cosa che accade.Per esempio:passare un ILineVisitor in GetFileContents (che sto assumendo spezza il file in linee).ILineVisitor sarebbe un metodo come OnVisitLine(linea Stringa), si potrebbe implementare il ILineVisitor e renderlo mantenere la stats.Sciacquare e ripetere l'operazione con un ILineMatchVisitor, IFileVisitor etc.Oppure si potrebbe usare un singolo IVisitor con l'articolovisita l'() metodo che ha una diversa semantica in ogni caso.

Le funzioni bisogno di prendere un Visitatore, e la chiamata è l'articolovisita l'() al momento opportuno, che può sembrare fastidioso, ma almeno il visitatore potrebbe essere usato per fare un sacco di cose interessanti, altri di ciò che si sta facendo qui.Infatti, in realtà si potrebbe evitare di scrivere GetMatchingLines passando un visitatore che controlla il match in OnVisitLine(Stringa di linea) in GetFileContents.

È questa una delle brutte cose che avevo già preso in considerazione?

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top