Pregunta

Estoy escribiendo un programa de la siguiente manera:

  • Buscar todos los archivos con la extensión correcta en un directorio determinado
  • Foreach, encuentra todas las apariciones de una cadena dada en esos archivos
  • Imprime cada línea

Me gustaría escribir esto de una manera funcional, como una serie de funciones de generador (las cosas que llaman a rendimiento rendimiento y solo devuelven un elemento a la vez cargados perezosamente), por lo que mi código se leería así:

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

Todo está bien, pero lo que también me gustaría hacer es imprimir algunas estadísticas al final. Algo como esto:

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

El problema es que al escribir el código en un estilo 'puramente funcional' como el de arriba, cada elemento está cargado perezosamente.
Solo se sabe cuántos archivos coinciden en total hasta que se completa el bucle foreach final, y dado que solo un elemento se genera rendimiento a la vez, el código no tiene ningún lugar para realizar un seguimiento de cuántos cosas que se han encontrado anteriormente. Si invocas el método matchingLines.Count () de LINQ, ¡volverá a enumerar la colección!

Puedo pensar en muchas maneras de resolver este problema, pero todas parecen ser algo feas. Me parece que es algo que la gente está obligada a hacer antes, y estoy seguro de que habrá un bonito patrón de diseño que muestra la mejor manera de hacer esto.

¿Alguna idea? Saludos

¿Fue útil?

Solución

De forma similar a otras respuestas, pero con un enfoque un poco más genérico ...

... ¿por qué no crea una clase Decorator que puede envolver una implementación IEnumerable existente y calcular la estadística a medida que pasa otros elementos?

Aquí hay una clase de Counter que acabo de juntar, pero también puedes crear variaciones para otros tipos de agregación.

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

Puede crear tres instancias de Counter :

  1. Uno para envolver GetAllFiles () contando el número total de archivos;
  2. Uno para envolver GetMatches () contando el número de archivos coincidentes; y
  3. Uno para envolver GetMatchingLines () contando el número de líneas coincidentes.

La clave de este enfoque es que no estás acumulando múltiples responsabilidades en tus clases / métodos existentes: el método GetMatchingLines () solo maneja la coincidencia, no estás pidiéndole que realice un seguimiento de las estadísticas. también.

Aclaración en respuesta a un comentario de Mitcham :

El código final se vería así:

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

Tenga en cuenta que esto sigue siendo un enfoque funcional: las variables utilizadas son inmutables (más parecidas a enlaces que a las variables), y la función general no tiene efectos secundarios.

Otros consejos

Diría que es necesario encapsular el proceso en una clase de 'Matcher' en la que sus métodos capturan estadísticas a medida que avanzan.

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

Me detendré allí, ya que se supone que estoy programando cosas de trabajo, pero la idea general está ahí. Todo el punto del programa funcional "puro" es no tener efectos secundarios, y este tipo de cálculo estático es un efecto secundario.

Puedo pensar en dos ideas

  1. Pase un objeto de contexto y devuélvalo (cadena + contexto) desde sus enumeradores: la solución puramente funcional

  2. use el almacenamiento local de subprocesos para sus estadísticas ( CallContext ), puede ser elegante y admitir una pila de contextos. así tendrías un código como este.

    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 está feliz de dar la vuelta a su código, es posible que esté interesado en Push LINQ. La idea básica es revertir el " tirar " modelo de IEnumerable < T > y conviértalo en un " push " Modelo con observadores: cada parte de la canalización hace que sus datos pasen de manera efectiva más allá de cualquier número de observadores (que utilizan controladores de eventos) que generalmente forman nuevas partes de la tubería. Esto proporciona una manera realmente fácil de conectar múltiples agregados a los mismos datos.

Ver esta entrada de blog para obtener más detalles. Hace un tiempo di una charla sobre esto en Londres: mi página de charlas tiene algunos enlaces para la muestra Código, la cubierta de diapositivas, video, etc.

Es un pequeño proyecto divertido, pero requiere un poco de atención.

Tomé el código de Bevan y lo reformulé hasta quedar satisfecho. Cosas divertidas.

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

Salida como se espera: 21, 3, 3

Suponiendo que esas funciones son las suyas, lo único que se me ocurre es el patrón de Visitante, que pasa a una función de visitante abstracta que le devuelve la llamada cuando sucede cada cosa. Por ejemplo: pase un ILineVisitor a GetFileContents (que supongo que divide el archivo en líneas). ILineVisitor tendría un método como OnVisitLine (String line), luego podría implementar ILineVisitor y hacer que mantenga las estadísticas adecuadas. Enjuague y repita con un ILineMatchVisitor, IFileVisitor, etc. O podría usar un solo IVisitor con un método OnVisit () que tiene una semántica diferente en cada caso.

Cada una de sus funciones necesitaría llevar a un Visitante y llamar a OnVisit () en el momento adecuado, lo que puede parecer molesto, pero al menos el visitante podría usarse para hacer muchas cosas interesantes, aparte de lo que usted hace. Lo estoy haciendo aquí. De hecho, realmente podría evitar escribir GetMatchingLines pasando un visitante que comprueba la coincidencia en OnVisitLine (línea de cadena) en GetFileContents.

¿Es esta una de las cosas feas que ya habías considerado?

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top