Pergunta

Eu estou escrevendo um programa da seguinte forma:

  • Encontre todos os arquivos com a extensão correta em um determinado diretório
  • Foreach, encontrar todas as ocorrências de uma determinada cadeia nesses arquivos
  • Imprimir cada linha

Eu gostaria de escrever isso de uma forma funcional, como uma série de funções de gerador (coisas que yield return chamada e retornar apenas um item de cada vez preguiçosamente-carregado), de modo que meu código ficaria assim:

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

Esta é tudo muito bem, mas o que eu também gostaria de fazer é imprimir algumas estatísticas no final. Algo parecido com isto:

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

O problema é, escrevendo o código em um estilo 'pura funcional' como acima, cada item é preguiçosamente carregado.
Você só sabe quantos arquivos corresponder, no total, até que os concluída finais de loop foreach, e porque apenas um item é sempre yielded de cada vez, o código não tem qualquer lugar para manter o controle de quantas coisas é encontrado anteriormente. Se você chamar método matchingLines.Count() do LINQ, ele vai voltar a enumerar a coleção!

Não consigo pensar em muitas maneiras de resolver este problema, mas todos eles parecem ser um pouco feio. Parece-me algo que as pessoas são obrigadas a ter feito antes, e tenho certeza que vai haver um padrão de design agradável que mostra a melhor maneira prática de fazer isso.

Todas as idéias? Felicidades

Foi útil?

Solução

Em uma veia similar a outras respostas, mas tomando uma abordagem um pouco mais genérico ...

... por que não criar um Decorator classe que pode envolver uma implementação IEnumerable existente e calcular a estatística que passa outros itens através de.

Aqui está uma classe Counter Eu só joguei junto -. Mas você pode criar variações para outros tipos de agregação demasiado

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

Você pode criar três instâncias de Counter:

  1. Uma para embrulhar GetAllFiles() contagem do número total de arquivos;
  2. Um a GetMatches() envoltório contagem do número de arquivos correspondentes; e
  3. Uma para embrulhar GetMatchingLines() contagem do número de linhas correspondentes.

A chave dessa abordagem é que você não está em camadas múltiplas responsabilidades em suas classes existentes / métodos -. O método GetMatchingLines() lida apenas com a correspondência, você não está pedindo-lhe para acompanhar estatísticas, bem

Clarificação em resposta a um comentário por Mitcham:

O código final seria algo parecido com isto:

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

Note que esta ainda é uma abordagem funcional -. As variáveis ??utilizadas são imutável (mais como ligações de variáveis), e a função global não tem efeitos colaterais

Outras dicas

Eu diria que você precisa encapsular o processo em uma classe 'Matcher', em que os seus métodos de captura de estatísticas à medida que progridem.

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

Eu vou parar lá desde que eu tenho que ser codificação material de trabalho, mas a idéia geral é lá. Todo o ponto de programa funcional 'puro' é não ter efeitos colaterais, e esse tipo de cálculo estática é um efeito colateral.

Eu posso pensar de duas ideias

  1. Passe em um objeto de contexto e de retorno (string + contexto) de seus entrevistadores - a solução puramente funcional

  2. fio de uso de armazenamento local para você as estatísticas ( CallContext ), pode ser fantasia e apoiar uma pilha de contextos. assim você teria um 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();
    } 
    

Se você está feliz para transformar o seu código de cabeça para baixo, que você pode estar interessado em LINQ Push. A idéia básica é a de inverter o modelo de "puxar" de IEnumerable<T> e transformá-lo em um modelo de "push" com observadores - cada parte do gasoduto empurra eficazmente os seus dados passados ??qualquer número de observadores (usando manipuladores de eventos) que normalmente formam novas partes pipeline. Isto dá uma maneira muito fácil de ligar vários agregados aos mesmos dados.

Veja este entrada de blog para mais alguns detalhes. Eu dei uma palestra sobre ele em Londres há um tempo atrás - o meu página de negociações tem algumas ligações para amostra código, o conjunto de slides, vídeo etc.

É um projeto divertido pouco, mas faz exame de um pouco de começar sua cabeça em torno.

Eu levei o código do Bevan e reformulado-lo ao redor até que eu estava contente. Fun coisas.

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

Saída como esperado: 21, 3, 3

Assumindo que essas funções são a sua própria, a única coisa que posso pensar é o padrão Visitor, passando uma função visitante abstrato que chama de volta quando cada coisa acontece. Por exemplo: passar um ILineVisitor em GetFileContents (que eu estou supondo que rompe o arquivo em linhas). ILineVisitor teria um método como OnVisitLine (linha String), você poderia, então, implementar o ILineVisitor e torná-lo manter as estatísticas apropriadas. Enxágüe e repita com um ILineMatchVisitor, IFileVisitor etc. Ou você pode usar um único IVisitor com um método OnVisit () que tem uma semântica diferente em cada caso.

As suas funções seriam cada necessidade de tomar um visitante, e chamá-lo de OnVisit () no momento apropriado, que pode parecer chato, mas pelo menos o visitante poderia ser usado para fazer muitas coisas interessantes, que não seja apenas o que você' está fazendo aqui. Na verdade, você realmente pode evitar escrever GetMatchingLines pela passagem de um visitante que verifica a partida em OnVisitLine (linha String) em GetFileContents.

É este uma das coisas feias que já tinha considerado?

Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top