1

我正在编写如下程序:

  • 在给定目录中查找所有具有正确扩展名的文件
  • Foreach,在这些文件中查找给定字符串的所有出现
  • 打印每一行

我想以一种功能性的方式编写它,作为一系列生成器函数(调用yield return并且一次只返回一个项目的东西延迟加载),所以我的代码将如下所示:

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

这一切都很好,但我还想做的是在最后打印一些统计数据。像这样的东西:

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

问题是,像上面那样以“纯功能”风格编写代码,每个项目都是延迟加载的。
在最终的 foreach 循环完成之前,您只知道总共有多少文件匹配,并且因为一次只yield编辑一个项目,所以代码没有任何地方可以跟踪它之前找到了多少东西。如果您调用 LINQ 的matchingLines.Count()方法,它将重新枚举集合!

我可以想出很多方法来解决这个问题,但它们似乎都有些难看。它让我印象深刻,因为人们以前一定会做过,而且我相信会有一个很好的设计模式来展示做这件事的最佳实践方式。

有任何想法吗?干杯

4

6 回答 6

2

我会说您需要将该过程封装到一个“Matcher”类中,您的方法在其中捕获统计信息,因为它们正在进行。

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

我会停在那里,因为我应该编码工作的东西,但总体思路就在那里。“纯”函数式程序的全部意义在于没有副作用,而这种静态计算是一种副作用。

于 2009-01-07T02:43:18.457 回答
2

我能想到两个想法

  1. 传入一个上下文对象并从您的枚举器返回(字符串+上下文) - 纯功能解决方案

  2. 使用线程本地存储为您统计(CallContext),您可以花哨并支持一堆上下文。所以你会有这样的代码。

    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();
    } 
    
于 2009-01-07T02:49:48.833 回答
2

与其他答案类似,但采取了更通用的方法......

...为什么不创建一个装饰器类,它可以包装现有的 IEnumerable 实现并在传递其他项目时计算统计信息。

这是Counter我刚刚汇总的一个类 - 但您也可以为其他类型的聚合创建变体。

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

您可以创建三个实例Counter

  1. 一个用来GetAllFiles()计算文件总数;
  2. 一个用于GetMatches()计算匹配文件的数量;和
  3. 一个用于GetMatchingLines()计算匹配行数。

这种方法的关键是您没有将多个职责分层到现有的类/方法上——该GetMatchingLines()方法只处理匹配,您也没有要求它跟踪统计信息。

澄清回应评论Mitcham

最终代码如下所示:

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

请注意,这仍然是一种函数式方法——使用的变量是不可变的(更像是绑定而不是变量),并且整个函数没有副作用。

于 2009-01-07T07:44:17.070 回答
1

如果您乐于颠倒您的代码,您可能会对 Push LINQ 感兴趣。基本思想是反转“拉”模型IEnumerable<T>并将其转变为带有观察者的“推”模型——管道的每个部分有效地将其数据推送到任意数量的观察者(使用事件处理程序),这些观察者通常形成新的部分管道。这提供了一种非常简单的方法来将多个聚合连接到相同的数据。

有关更多详细信息,请参阅此博客条目。不久前我在伦敦做了一次演讲——我的演讲页面有一些示例代码、幻灯片、视频等的链接。

这是一个有趣的小项目,但确实需要一些时间。

于 2009-01-07T07:23:24.833 回答
1

我采用了 Bevan 的代码并对其进行了重构,直到我满意为止。好玩的东西。

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

按预期输出:21、3、3

于 2009-01-08T02:07:51.710 回答
0

假设这些函数是你自己的,我唯一能想到的就是访问者模式,它传入一个抽象的访问者函数,当每件事发生时都会回调你。例如:将 ILineVisitor 传递给 GetFileContents (我假设将文件分成几行)。ILineVisitor 将有一个类似 OnVisitLine(String line) 的方法,然后您可以实现 ILineVisitor 并使其保持适当的统计信息。冲洗并使用 ILineMatchVisitor、IFileVisitor 等重复。或者您可以使用单个 IVisitor 和 OnVisit() 方法,在每种情况下都有不同的语义。

每个函数都需要一个访问者,并在适当的时候调用它的 OnVisit(),这可能看起来很烦人,但至少访问者可以用来做很多有趣的事情,而不仅仅是你在这里做的事情. 事实上,您实际上可以通过将检查 OnVisitLine(String line) 中匹配的访问者传递给 GetFileContents 来避免编写 GetMatchingLines。

这是你已经考虑过的丑陋的事情之一吗?

于 2009-01-07T02:41:00.443 回答