如何提高自定义 BindingList 上 AddRange 方法的性能?

How can I improve performance of an AddRange method on a custom BindingList?

我有一个自定义 BindingList,我想为其创建一个自定义 AddRange 方法。

public class MyBindingList<I> : BindingList<I>
{
    ...

    public void AddRange(IEnumerable<I> vals)
    {
        foreach (I v in vals)
            Add(v);
    }
}

我的问题是大型集合的性能很糟糕。我现在正在调试的案例是尝试添加大约 30,000 条记录,并且花费了无法接受的时间。

在线查看此问题后,问题似乎在于 Add 的使用每次添加都会调整数组的大小。 This answer 我想总结为:

If you are using Add, it is resizing the inner array gradually as needed (doubling)

我可以在我的自定义 AddRange 实现中做什么来指定 BindingList 需要根据项目计数调整大小,而不是让它在添加每个项目时不断地重新分配数组?

您可以在构造函数中传入一个列表并使用List<T>.Capacity

但我敢打赌,最显着的加速将来自添加范围时的暂停事件。所以我在我的示例代码中包含了这两件事。

可能需要一些微调来处理一些最坏的情况,而不是什么。

public class MyBindingList<I> : BindingList<I>
{
    private readonly List<I> _baseList;

    public MyBindingList() : this(new List<I>())
    {

    }

    public MyBindingList(List<I> baseList) : base(baseList)
    {
        if(baseList == null)
            throw new ArgumentNullException();            
        _baseList = baseList;
    }

    public void AddRange(IEnumerable<I> vals)
    {
        ICollection<I> collection = vals as ICollection<I>;
        if (collection != null)
        {
            int requiredCapacity = Count + collection.Count;
            if (requiredCapacity > _baseList.Capacity)
                _baseList.Capacity = requiredCapacity;
        }

        bool restore = RaiseListChangedEvents;
        try
        {
            RaiseListChangedEvents = false;
            foreach (I v in vals)
                Add(v); // We cant call _baseList.Add, otherwise Events wont get hooked.
        }
        finally
        {
            RaiseListChangedEvents = restore;
            if (RaiseListChangedEvents)
                ResetBindings();
        }
    }
}

您不能使用 _baseList.AddRange,因为 BindingList<T> 不会挂接 PropertyChanged 事件。您可以通过在 AddRange 之后为每个项目调用私有方法 HookPropertyChanged 来仅使用反射来绕过它。然而,这只有在 vals (您的方法参数)是一个集合时才有意义。否则你冒着枚举两次的风险。

这是您在不编写自己的 BindingList 的情况下最接近 "optimal" 的方法。 这应该不会太难,因为您可以从 BindingList 复制源代码并根据需要更改部分。

CSharpie 在他的 中解释说,性能不佳是由于 ListChanged 事件在每次 Add 后触发,并展示了一种实现 AddRange 的方法你的习惯 BindingList.

另一种方法是将 AddRange 功能实现为 BindingList<T> 的扩展方法。基于 CSharpies 实施:

/// <summary>
/// Extension methods for <see cref="System.ComponentModel.BindingList{T}"/>.
/// </summary>
public static class BindingListExtensions
{
  /// <summary>
  /// Adds the elements of the specified collection to the end of the <see cref="System.ComponentModel.BindingList{T}"/>,
  /// while only firing the <see cref="System.ComponentModel.BindingList{T}.ListChanged"/>-event once.
  /// </summary>
  /// <typeparam name="T">
  /// The type T of the values of the <see cref="System.ComponentModel.BindingList{T}"/>.
  /// </typeparam>
  /// <param name="bindingList">
  /// The <see cref="System.ComponentModel.BindingList{T}"/> to which the values shall be added.
  /// </param>
  /// <param name="collection">
  /// The collection whose elements should be added to the end of the <see cref="System.ComponentModel.BindingList{T}"/>.
  /// The collection itself cannot be null, but it can contain elements that are null,
  /// if type T is a reference type.
  /// </param>
  /// <exception cref="ArgumentNullException">values is null.</exception>
  public static void AddRange<T>(this System.ComponentModel.BindingList<T> bindingList, IEnumerable<T> collection)
  {
    // The given collection may not be null.
    if (collection == null)
      throw new ArgumentNullException(nameof(collection));

    // Remember the current setting for RaiseListChangedEvents
    // (if it was already deactivated, we shouldn't activate it after adding!).
    var oldRaiseEventsValue = bindingList.RaiseListChangedEvents;

    // Try adding all of the elements to the binding list.
    try
    {
      bindingList.RaiseListChangedEvents = false;

      foreach (var value in collection)
        bindingList.Add(value);
    }

    // Restore the old setting for RaiseListChangedEvents (even if there was an exception),
    // and fire the ListChanged-event once (if RaiseListChangedEvents is activated).
    finally
    {
      bindingList.RaiseListChangedEvents = oldRaiseEventsValue;

      if (bindingList.RaiseListChangedEvents)
        bindingList.ResetBindings();
    }
  }
}

这样,根据您的需要,您甚至可能不需要编写自己的 BindingList-子类。