65.9K
CodeProject 正在变化。 阅读更多。
Home

带延迟的枚举集合

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.43/5 (9投票s)

2014 年 3 月 3 日

CPOL

6分钟阅读

viewsIcon

23118

downloadIcon

284

本文将向您展示如何在每次迭代之间带有延迟地枚举集合。

引言

首先,我想说这篇文章仅供初学者阅读,因为有经验的人可能会觉得它太简单了,我先说明这一点 微笑 | :)。来吧,我们开始吧。每个人都会枚举一个序列(集合),但有时在枚举时,您需要在每次迭代之间有一个延迟,我们将看看如何用各种 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 接口
    • 添加了可下载的源文件和演示项目。
© . All rights reserved.