BlockingFileObserver - 一个 IEnumerable<string> 的实现
BlockingFileObserver - 一个 IEnumerable<string> 的实现

引言
你是否曾经有过需要监视目录以查找新文件的经历?对我来说,当然有过,无论是监视我的 IIS SMTP 服务的BadMail目录,还是监视来自其他进程的传入文件,等等。
现在,通常我会设置我的 FileSystemWatcher
,注册事件等等,然后愉快地阻塞并等待传入的文件。然而,在某个时候,我发现这种方法很乏味,坦白说……很无聊。我想要做的是使用一个更友好的接口来实现这种“功能”。
我理想中想要的是一个 IEnumerable<span class="code-keyword"><string>
实例,我可以对其进行枚举,对其执行 LINQ 查询,并总体上拥有一个更通用的接口来处理。
我想要的是这样
foreach(string fileName in BlockingFileObserver.Create
(@"D:\projects", "*.txt", FileOperation.None | FileOperation.Create))
Console.WriteLine("File Notification: {0}", fileName);
在上面的代码中,我做了一个非常有趣的假设。“我的线程应该阻塞,直到生成器中的下一个项出来。”嗯……做梦去吧。
这就是本文随附的小玩具项目所要做的。它包含一个名为 BlockingFileObserver
的类,该类是 IEnumerable<span class="code-keyword"><string>
的子类,它将处理目录中文件更改的监视,并在文件到达时通知您,在下一个文件到来之前一直阻塞。
项目中提供的类比上面的代码更进一步。BlockingFileObserver
很好用,直到你需要关闭它。上面的语法将一直运行下去。停止包含的控制台应用程序的唯一方法是按 Ctrl-C。因此,我添加了另一个功能,其中包含一个 out Timer stopTimer
和一个 long millis
参数,以指示我们希望在经过特定时间间隔后停止枚举。这种语法如下所示
foreach(string fileName in BlockingFileObserver.Create
(@"D:\projects", "*.txt", FileOperation.None | FileOperation.Create,
out timer, 30000))
Console.WriteLine("File Notification: {0}", fileName);
我花了一些时间为自己证明为什么我需要 out Timer stopTimer
参数。我得出的结论是,这是将 Timer
的事件暴露给调用者的唯一方法,以防它也需要超时通知。每当我把一些 while(true) { // 阻塞执行 ... }
代码放到外面时,我都想在它停止运行时得到通知。
BlockingFileObserver
上总共有四个工厂方法,暴露了大多数可能的参数组合
public static BlockingFileObserver Create(string folderPath);
public static BlockingFileObserver Create
(string folderPath, string fileTypeFilter);
public static BlockingFileObserver Create
(String folderPath, FileOperation opType);
public static BlockingFileObserver Create
(String folderPath, string fileTypeFilter, FileOperation opType);
public static BlockingFileObserver Create
(String folderPath, string fileTypeFilter, FileOperation opType,
out Timer stopTimer, long millis);
FileOperation
是项目中包含的一个 enum
类型,定义如下
/// <summary>
/// Defines the three basic file type operations, and
/// one more, None, meaning an unspecified value
/// </summary>
[Flags]
public enum FileOperation
{
Create = 1,
Delete = 2,
Change = 4,
None = 8
}
由于它被 [Flags]
装饰,所以它的值可以一起使用 '|' 运算符,例如 FileOperation.None | FileOperation.Delete
等等。这给接口带来了一点额外的灵活性,允许您使用非常熟悉的语法——如果不是非常漂亮的话——来指定您感兴趣的通知类型。
背景
最重要的是,我们将创建此类枚举的能力归功于 yield
关键字。简单来说,当在方法或属性 getter 中使用时,yield
允许执行流程返回到调用者,当调用者请求生成器的下一个元素时,执行将从最后一个 yield return
语句之后继续。也许这比描述更容易说明。这就是 yield
在 BlockingFileObserver
中的用法
public IEnumerator<string> GetEnumerator()
{
// ok, do the existing files first,
// but only if we've .None in our FileOperation ...
if ((FileOperation.None & this.FileOperation) > 0)
{
foreach (string fileName in Directory.GetFiles
(this.FolderPath, this.FilterString))
yield return fileName;
}
...
}
</string>
每次我们命中 yield return
语句时,执行就会继续到调用者的端。紧接着,执行将从这里恢复。这让我可以设置一个 FileSystemWatcher
,注册我需要的事件,并阻塞我的线程的执行,直到我收到事件通知。
为了阻塞直到收到事件,我不得不深入研究 System.Threading
命名空间。在那里,我找到了 ManualResetEvent
类。
ManualResetEvent
有两种状态:已触发或未触发。它有用之处在于,如果您调用 .WaitOne()
,您之后的下一行代码将在有人将 ManualResetEvent
的状态设置为已触发时执行。您将阻塞,直到收到继续的信号。我使用它来阻塞,直到我从内部 FileSystemWatcher
收到文件通知事件。这看起来像这样(这些是前一段代码片段之后的几行,仍在 BlockingFileObserver
的 GetEnumerator()
方法中)
// ok, now I need to create a FileSystemWatcher here ...
m_Watcher = new FileSystemWatcher(this.FolderPath, this.FilterString);
// Init the watcher, register for events etc.
...
// Do I need to keep listening ?
if (0 == keepListening)
yield break;
// start the watcher ...
m_Watcher.EnableRaisingEvents = true;
// Now I need to start waiting for events ...
while (true)
{
m_ResetEvent.WaitOne();
m_ResetEvent.Reset();
if (this.Active)
yield return m_LastFile;
else
{
yield break;
break;
}
}
这里的“魔法”在 while(true)
循环中。我们立即进入循环,阻塞,直到 ManualResetEvent
被触发。我们重置它,以便它返回到未触发状态,然后继续返回下一个可用值,或者使用 yield
break 退出循环。
事件在 FileSystemWatcher
的事件处理程序代码中被触发
#region FileSystemWatcher & eventing fields & methods
/// <summary>
/// Will observe our folder for new/deleted/updated files and notify us ...
/// </summary>
private FileSystemWatcher m_Watcher = null;
/// <summary>
/// Will help us block execution until the FileSystemWatcher
/// posts a new event ...
/// </summary>
private ManualResetEvent m_ResetEvent = new ManualResetEvent(false);
/// <summary>
/// Little "buffer" variable, to hold the last returned file name
/// </summary>
private string m_LastFile = string.Empty;
private void Watcher_PostEvent(object sender, FileSystemEventArgs e)
{
// ok, store the file name ...
m_LastFile = e.Name;
// post an event ...
m_ResetEvent.Set();
}
#endregion
ManualResetEvent
成员在 Active
属性的 set
部分也被触发。这是必需的,与 GetEnumerator()
方法中我 while
循环的“特定”语法相结合,以确保我不会遇到 Active
属性设置为 false
,但我仍在等待最后一个 FileSystemWatcher
事件然后退出。
private bool m_Active = true;
public bool Active
{
get
{
return m_Active;
}
set
{
m_Active = value;
// In case I'm blocking right now ... notify
if (!m_Active)
m_ResetEvent.Set();
}
}
Using the Code
该类真的很小,我认为将其放在单独的 ClassLibrary
项目中没有用,所以它包含在项目中的唯一一个 .cs 文件,即 Program.cs 中。我在其中仔细定义了区域,因此通过简单的复制粘贴将其提取出来应该不会太麻烦。
除此之外,使用该类非常直接。使用四个可用的 .Create(...) static
方法之一,来获取一个实例,该实例的行为将与其他任何 IEnumerable<span class="code-keyword"><string>
类似。
class Program
{
static void Main(string[] args)
{
Timer timer;
// Enumerate ...
foreach(string fileName in BlockingFileObserver.Create
(@"D:\projects", "*.txt", FileOperation.None |
FileOperation.Create, out timer, 30000))
Console.WriteLine("File Notification: {0}", fileName);
Console.WriteLine("Finished enumerating");
Console.ReadLine();
}
}
关注点
最后一个有趣的细节是也注册了枚举超时功能的 .Create
实现。在那里,我实例化了 out Timer stopTimer
参数,并为其提供了一个匿名委托来处理其 CallBack
。有趣的是,由于在创建时我对想要停用的 BlockingFileObserver
实例有引用,我可以在我的匿名委托的正文中! 我认为这非常漂亮……
public static BlockingFileObserver Create
(String folderPath, string fileTypeFilter, FileOperation opType,
out Timer stopTimer, long millis)
{
BlockingFileObserver enumerator =
Create(folderPath, fileTypeFilter, opType);
// Create the new timer ...
stopTimer = new Timer
(new TimerCallback(
delegate(object sender)
{
enumerator.Active = false;
}),
null,
millis,
Timeout.Infinite);
return enumerator;
}
我认为这是一个强大的面向服务型“监听器”的编程范例。将所有阻塞的复杂性隐藏在 IEnumerable<span class="code-keyword"><T>
接口后面非常通用,但最重要的是,在我看来,它非常漂亮。