带延迟的枚举集合






4.43/5 (9投票s)
本文将向您展示如何在每次迭代之间带有延迟地枚举集合。
引言
首先,我想说这篇文章仅供初学者阅读,因为有经验的人可能会觉得它太简单了,我先说明这一点 。来吧,我们开始吧。每个人都会枚举一个序列(集合),但有时在枚举时,您需要在每次迭代之间有一个延迟,我们将看看如何用各种 C# 版本实现这一点。
背景
写这篇文章的灵感来自于这个 Stackoverflow 问题,我觉得应该把那个答案变成一篇文章,这样未来的读者就不需要重复发明轮子了。我不确定我是否在重复发明轮子。
Using the Code
糟糕的代码
我们将从糟糕的代码开始。
private void EnumerateWithDelay(IEnumerable<object> sequence, Action<object> action)
{
foreach (var item in sequence)
{
action(item);
Thread.Sleep(1000);//Sleep a second
}
}
上面的代码有什么问题?为什么它很糟糕?不是因为它不能工作。它能工作,它会遍历序列并添加 1 秒的延迟用于演示目的,当然也可以是任何时间。它之所以被认为是糟糕的,是因为它阻塞了线程,这从来都不是个好主意。如果您需要从 UI 线程调用此方法怎么办?UI 将会冻结,显然没有人喜欢这样。
让我们看看如何在不阻塞线程的情况下编写它。
使用 C# 5.0
C# 5.0 搭配 .NET 4.5 提供了一个很棒的特性 async/await,这让生活变得更轻松。我将使用这个特性来实现功能。
private async void EnumerateWithDelayAsync(IEnumerable<object> sequence, Action<object> action)
{
foreach (var item in sequence)
{
action(item);
await Task.Delay(1000);//Asynchronously wait a second
}
}
我不会过多解释 async/await... 已经有很多文章介绍了。上面的代码的作用是枚举一个序列,每次迭代它都会异步等待一秒。此方法在等待时不会阻塞调用线程。很简单,不是吗?
那么接下来呢?我们看到了 C#5.0 中对我们需求的实现。如果我们想在 C# 5.0 之前实现这一点,当时这些 async/await 特性还不可用怎么办?别担心,我们现在就来做。
使用 C# 4.0
以下代码需要 C# 4.0 和 .NET 4.0 才能编译。请注意,我仅使用 .NET 4.0 来将操作表示为 Task
,以便您可以等待它或附加 continuations 等等。可能 .NET 2.0 就足够了。
此实现依赖于一个事实,即 Timer
在它们到期之前不会阻塞调用线程,这是实现此类事物最常用的方法。事实上,.NET 4.5 中的 TPL (Task Parallel Library) 中的 Task.Delay
方法仅使用 Timer
来实现。
所以我们需要一个序列,一个 Action<T>
和一个延迟来等待每次迭代。我们将此类包装在一个类中,因为我们需要访问一些状态。PeriodicEnumerator<T>
是我们将要实现的类,它定义了一个带有所需参数的构造函数,并仅将它们存储在字段中以供将来使用。
Enumerate
是实际开始枚举的方法。它仅获取序列的实际枚举器,使用没有给定延迟的计时器立即启动它,并返回一个未完成的任务。
请注意,第二个参数是计时器。更改为 Timeout.Infinite
,这意味着计时器只会触发一次,而不是重复触发。
public Task Enumerate()
{
//Validation part removed
enumerator = sequence.GetEnumerator();
timer.Change(0, Timeout.Infinite);
return completionSource.Task;
}
这里我们重要的一个方法是 TimerCallback
,它实际上执行迭代。
private void TimerCallback(object state)
我们检查序列中是否还有更多元素?如果没有,我们就将任务状态设置为已完成,释放计时器,然后返回。
if (!enumerator.MoveNext())
{
completionSource.SetResult(null);
timer.Dispose();
return;
}
如果元素可用,那么我们获取当前元素并再次调用 action
来设置计时器,但这次是带有延迟,这个过程会一直持续到我们到达序列的末尾。
T current = enumerator.Current;
if (synchronizationContext != null)
{
synchronizationContext.Send((x) => action(current), null);
}
else
{
action(current);
}
timer.Change(period, Timeout.Infinite);
您可能已经注意到我使用了 SynchronizationContext
,它支持在特定上下文中执行回调。当您处理 UI 应用程序时,这将非常有用。
完整代码
public class PeriodicEnumerator<T> : IDisposable
{
private IEnumerable<t> sequence;
private Action<t> action;
private int period;
private System.Threading.Timer timer;
private SynchronizationContext synchronizationContext;
private IEnumerator<t> enumerator;
private TaskCompletionSource<object> completionSource = new TaskCompletionSource<object>();
public PeriodicEnumerator(IEnumerable<t> sequence, Action<t> action, int period)
: this(sequence, action, period, null)
{
}
public PeriodicEnumerator(IEnumerable<t> sequence, Action<t> action, int period, SynchronizationContext synchronizationContext)
{
if (sequence == null)
{
throw new ArgumentNullException("sequence");
}
if (action == null)
{
throw new ArgumentNullException("action");
}
if (period <= 0)
{
throw new ArgumentOutOfRangeException("period", "period should be greater than zero.");
}
this.sequence = sequence;
this.action = action;
this.period = period;
this.synchronizationContext = synchronizationContext;
this.timer = new System.Threading.Timer(TimerCallback);
}
public Task Enumerate()
{
if (this.enumerator != null)
{
throw new InvalidOperationException("Enumeration already started");
//To avoid multiple enumerations, better create new instance
}
enumerator = sequence.GetEnumerator();
timer.Change(0, Timeout.Infinite);
return completionSource.Task;
}
private void TimerCallback(object state)
{
if (!enumerator.MoveNext())
{
completionSource.SetResult(null);
timer.Dispose();
return;
}
try
{
T current = enumerator.Current;
if (synchronizationContext != null)
{
synchronizationContext.Send((x) => action(current), null);
}
else
{
action(current);
}
timer.Change(period, Timeout.Infinite);
}
catch (Exception ex)
{
completionSource.SetException(ex);
timer.Dispose();
}
}
public void Dispose()
{
timer.Dispose();
}
}
使用场景
static void ConsoleAppSample()
{
var periodicEnumerator = new PeriodicEnumerator<int>(Enumerable.Range(1, 5), (x) => Console.WriteLine(x), 1000);
Task enumerationTask = periodicEnumerator.Enumerate();
enumerationTask.Wait();//Optionally wait for completion
Console.WriteLine("Completed");
Console.Read();
}
static void SynchronizationContextSample()//applicable for any UI apps
{
var periodicEnumerator = new PeriodicEnumerator<int>(Enumerable.Range(1, 5), (x) => textbox.Text = x.ToString(), 1000,SynchronizationContext.Current);
Task enumerationTask = periodicEnumerator.Enumerate();
}
PeriodicEnumerator 的一些用法
这段代码看起来很简单,不是吗?简单而强大。您几乎可以用它做任何事情。让我们看看它的一些用法。
显示当前时间
在我们的应用程序中显示当前时间非常常见,通常在标签或状态栏中,我将向您展示如何借助我们的 PeriodicEnumerator
实现此功能。
private PeriodicEnumerator<DateTime> updateTimeEnumerator;
private void StartUpdatingTime()
{
if (updateTimeEnumerator == null)
{
updateTimeEnumerator = new PeriodicEnumerator<DateTime>(GetCurrentTimeSequence(),
(time) => lblCurrentTime.Text = string.Format("Current time: {0}", time),
1000,
SynchronizationContext.Current);
updateTimeEnumerator.Enumerate();
}
}
我们只是声明了一个 PeriodicEnumerator<DateTime>
并用序列 GetCurrentTimeSequence
初始化它,它是一个 IEnumerable<DateTime>
,一个 Action<DateTime>
,它会更新一个标签,1000 毫秒(1 秒)的延迟来更新时间,并且最后是 UI SynchronizationContext
,因为我们要更新 UI。
我能听到你在说:等等!那个奇怪的方法 GetCurrentTimeSequence
是什么,你还没有向我们展示?嗯,那个方法没什么可展示的。它只是一个永不停止的序列,实现如下:
private IEnumerable<DateTime> GetCurrentTimeSequence()
{
while (true)
{
yield return DateTime.Now;
}
}
上面的方法使用了一个称为迭代器的语言特性。如果您还不了解,可以阅读一下。
好了。我们完成了!上面代码在 Windows Form 中渲染的输出如下所示:

您可以看到时间开始在状态栏中更新,它将一直这样下去。
动画化控件
在 Winforms 中,动画通常通过计时器实现,我们的 PeriodicEnumerator<T>
已经包装了一个计时器,这使得我们非常容易实现动画。我将向您展示如何实现一个永不结束的动画(只是移动一个控件)。
让我们开始创建一个 PeriodicEnumerator<T>
实例和所需的成员。
private PeriodicEnumerator<Point> moveEnumerator;
private void StartMovingButton()
{
if (moveEnumerator == null)
{
moveEnumerator = new PeriodicEnumerator<point>(GetNextPointSequence(),
(location) => btnStartMovingMe.Location = location,
10,
SynchronizationContext.Current);
moveEnumerator.Enumerate();
}
}
这次,我们有 PeriodicEnumerator<Point>
,因为 Control.Location
属性的类型是 Point
。在上面的代码中,我们创建了一个新的 PeriodicEnumerator
实例,它有一个用迭代器实现的 IEnumerable<Point>
,一个更新按钮 Location
的委托,10 毫秒的计时器延迟,以及 SynchronizationContext
,您此时应该知道我们为什么需要它。
我们通过调用 Enumerate()
来开始枚举。
private IEnumerable<Point> GetNextPointSequence()
{
const int MoveIncrement = 2;
Direction xDirection = Direction.Forward;
Direction yDirection = Direction.Forward;
Size buttonSize = btnStartMovingMe.Size;
while (true)
{
Point newLocation = btnStartMovingMe.Location;
newLocation.X += (xDirection == Direction.Forward) ? MoveIncrement : -MoveIncrement;
newLocation.Y += (yDirection == Direction.Forward) ? MoveIncrement : -MoveIncrement;
if (newLocation.X + buttonSize.Width > btnStartMovingMe.Parent.ClientSize.Width)
{
xDirection = Direction.Reverse;
}
else if (newLocation.X < 0)
{
xDirection = Direction.Forward;
}
if (newLocation.Y + buttonSize.Height > btnStartMovingMe.Parent.ClientSize.Height)
{
yDirection = Direction.Reverse;
}
else if (newLocation.Y < 0)
{
yDirection = Direction.Forward;
}
yield return newLocation;
}
}
private enum Direction
{
Forward = 1,
Reverse = 2,
}
GetNextPointSequence
方法除了返回下一个点(控件的位置)之外,不做任何事情,并进行一些检查,看控件是否超出了父容器的边界;如果是,它就会反转运动方向。
所以我们已经完成了代码,准备好进行动画演示了;让我们看看它的渲染效果。
由于图像质量问题,动画看起来很糟糕,当您运行 EXE 时会很清晰。
各位,这就是全部内容,希望您喜欢。很快将在另一篇文章中再见。欢迎批评和建议。
关注点
使用 CancellationToken
实现取消会很容易,并且可以使用 APM 将此代码转换为 .NET 2.0。我将其留作练习。请尝试一下。
您可能会发现以下文章很有帮助,这些文章与主题相关
历史
- 2014 年 3 月 2 日 - 初始版本
- 2014 年 3 月 3 日 - 添加了 PeriodicEnumerator 的一些用法
- 在状态栏中更新当前时间
- 动画化 Winform 控件
- 实现了 IDisposable 接口
- 添加了可下载的源文件和演示项目。