我正在编写一个自定义控件,基本上是一个图表/图形,绘制最多1024个双值.

我目前将这些值存储在一个List中,当我得到一个超过1024计数的新值时,我会删除列表中的最后一个,我相信这不是最有效的方法,性能和分配明智.

    private readonly List<double> myValues = new(1025);

    public void AddValue(double newValue) {
        myValues.Insert(0, newValue);
        if (myValues.Count > 1024) myValues.RemoveAt(1024);
    }

这是做这件事的最佳/正确方法吗?

我也在考虑使用一个直接的double []数组,并在每次加法时将元素从1复制到—1,但我感觉这也不正确.

    private readonly double[] myArray = new double[1024];

    public void AddValue2(double newValue) {
        Array.Copy(myArray, 0, myArray, 1, 1023);
        myArray[0] = newValue;
    }

有何建议或建议,说明什么是最有效的实施方式?

推荐答案

一个有效而简单的实现方法是使用一个具有前索引和后索引的数组来实现Circular Buffer.

如果将底层数组设置为比要存储的最大项目数大一个,则代码会简化.这允许你安排事情,如果front_index == back_index,那么缓冲区是空的.

下面是一个示例实现.它有添加元素、移除最老元素、索引元素从最老到最新以及枚举元素从最老到最新的方法.希望代码是相当自解释的:

public sealed class CircularBuffer<T> : IEnumerable<T>
{
    /// <summary>Constructor.</summary>
    /// <param name="capacity">The maximum capacity of the buffer.</param>
    public CircularBuffer(int capacity)
    {
        // We will use a buffer with a size one greater than the capacity.
        // The reason for this is to simplify the logic - we can use "front == back" to indicate an empty buffer.

        _buffer = new T[capacity + 1];
    }

    public int Capacity => _buffer.Length - 1;

    /// <summary>The number of elements currently stored in the buffer.</summary>
    public int Count
    {
        get
        {
            int result = _back - _front;

            if (result < 0)
                result += _buffer.Length;

            return result;
        }
    }

    public bool IsEmpty => this.Count == 0;
    public bool IsFull  => nextSlot(_back) == _front;

    /// <summary>Removes all elements.</summary>
    public void Empty()
    {
        _front = _back = 0;
        Array.Clear(_buffer, 0, _buffer.Length); // Destroy any old references so they can be GCed.
    }

    /// <summary>Add an element to the buffer, overwriting the oldest element if the buffer is full.</summary>
    /// <param name="newItem">The element to add.</param>
    public void Add(T newItem)
    {
        _buffer[_back] = newItem;
        _back = nextSlot(_back);

        if (_back != _front) // Buffer is not full?
            return;

        _front         = nextSlot(_front); // Bump the front, overwriting the current front.
        _buffer[_back] = default!;         // Remove the old front value.
    }

    /// <summary>Return the item at index, where 0 is the oldest thing in the buffer </summary>
    public T this[int index] => _buffer[(index + _front) % _buffer.Length];

    /// <summary>Removes and returns the oldest element from the buffer.</summary>
    /// <returns>The element that was removed from the buffer.</returns>
    /// <exception cref="InvalidOperationException">Thrown if the buffer is empty.</exception>

    public T RemoveOldestElement()
    {
        if (IsEmpty)
            throw new InvalidOperationException("Cannot remove an element from an empty buffer.");

        T result = _buffer[_front];
        _buffer[_front] = default!; // Zap the front element.
        _front          = nextSlot(_front);

        return result;
    }

    /// <summary>Returns elements in oldest to newest order. </summary>
    public IEnumerator<T> GetEnumerator()
    {
        for (int i = _front; i != _back; i = nextSlot(i))
        {
            yield return _buffer[i];
        }
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return GetEnumerator(); // Implement in terms of the typesafe enumerator.
    }

    int nextSlot(int slot)
    {
        return (slot + 1) % _buffer.Length;
    }

    int _front;
    int _back;

    /// <summary>The underlying buffer. This has a length one greater than the actual capacity.</summary>
    readonly T[] _buffer;
}

[增编]

上面的代码是我们从. Net 3.5开始就一直在使用的代码.在 那次比一百快了一些.然而,在后来的. NET版本中,情况有所改善.

我比较了上面的CircularBufferQueue<T>上的简单扩展方法,以提供与CircularBuffer.Add()相同的功能:

public static class QueueExt
{
    public static void Add<T>(this Queue<T> self, T item, int limit)
    {
        if (self.Count >= limit) // Consider throwing if self.Count > limit.
            self.Dequeue();

        self.Enqueue(item);
    }
}

测试的基准代码(有点简单化)是:

using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;

namespace TestQueue;

[MemoryDiagnoser]
[SimpleJob(RuntimeMoniker.Net48)]
[SimpleJob(RuntimeMoniker.Net80)]
public class UnderTest
{
    [Benchmark]
    public void ViaQueue()
    {
        _queue.Clear();

        for (int i = 0; i < 100_000; i++)
        {
            _queue.Add(i, 1000);
        }
    }

    [Benchmark]
    public void ViaCircularArray()
    {
        _buffer.Empty();

        for (int i = 0; i < 100_000; i++)
        {
            _buffer.Add(i);
        }
    }

    static readonly Queue<double> _queue = new Queue<double>(1000);

    static readonly CircularBuffer<double> _buffer = new CircularBuffer<double>(1000);
}

在我的系统上的结果:

| Method           | Runtime            | Mean     | Error   | StdDev  |
|----------------- |------------------- |---------:|--------:|--------:|
| ViaQueue         | .NET 8.0           | 220.5 us | 1.35 us | 1.12 us |
| ViaCircularArray | .NET 8.0           | 380.1 us | 1.67 us | 1.56 us |
| ViaQueue         | .NET Framework 4.8 | 403.6 us | 7.87 us | 7.36 us |
| ViaCircularArray | .NET Framework 4.8 | 390.5 us | 2.43 us | 2.15 us |

对于.NET 4.8,CircularArray稍微快一些.然而,对于.NET 8.0来说,Queue<T>速度明显快.

因此,我实际上建议您使用内置的Queue<T>来代替(带有适当的前端).

Queue<T>的唯一缺点是你不能访问它的内部Capacity,所以你必须通过限制—或者你可以直接将它包装在一个包含容量的类中.

Csharp相关问答推荐

我可以将Expressc操作设置为在另一个Expressc操作完成后自动发生吗?

C# Json重新初始化动态类型

利用.NET 8中的AddStandardResilienceDeliveries和AddStandardHedgingDeliveries实现Resiliency

EF Core:看不到任何查询日志(log)?

EF Core Fluent API中定义的多对多关系

如何从HttpContext获取请求正文

C#.NET依赖项注入顺序澄清

.NET 6控制台应用程序,RabbitMQ消费不工作时,它的程序文件中的S

如何在Windows 11任务调度程序中每1分钟重复一次任务?

在.NET MAUI.NET 8中如何防止按钮点击时出现灰色反馈

System.Text.Json .NET 8多形态语法化

在.NET 7.0 API控制器项目中通过继承和显式调用基类使用依赖项注入

源代码生成器:CS8795分部方法';Class1.GetS2(字符串)';必须有实现部分,因为它有可访问性修饰符?

Azure Functions v4中的Serilog控制台主题

如何让游戏对象在切换场景时被销毁,但在开始新游戏时重新锁定

从Base64转换为不同的字符串返回相同的结果

C#中类库项目的源代码生成器

将字符串类型日期输入(yyyy-mm-ddthh:mm:ss)转换为MM/dd/yyyy格式

.NET EF Core Automapper项目到筛选不起作用

从具有泛型类型约束的类继承-Blazor