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

9

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

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

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

我对此的问题是,使用大型集合时性能非常差。我现在正在调试的情况是尝试添加大约30,000条记录,并且需要花费无法接受的时间。

在网上研究了这个问题后,似乎问题是使用 Add 会导致每次添加都重新调整数组大小。我认为这个回答总结如下:

如果您使用 Add,则它会根据需要逐渐调整内部数组的大小(加倍)

在我的自定义 AddRange 实现中,我应该怎么做才能指定 BindingList 需要调整大小的基础项目数,而不是让它每次添加项目时都重新分配数组?


4
对于仅有3万个项目而言,内部列表调整大小需要“不可接受的时间”的可能性非常小。最有可能的真正问题是,每次添加新项目时它会引发更改事件,并且您有一些处理程序处理该事件(例如,列表由某些UI控件使用)。 您可以通过创建绑定列表并填充30k个项目来轻松测试它,而无需执行其他操作。 最多只需几毫秒即可看到结果。PS:每次添加新项目时它也不会调整列表大小,而当达到限制时会将其大小加倍。 - Evk
1
@Evk 经过一些测试,你是正确的。我的问题出在更改事件上。感谢确认! - Rachel
2个回答

12
CSharpie在他的答案中解释说,性能差是由于每个Add之后都会触发ListChanged事件,他展示了一种实现自定义BindingListAddRange的方法。
另一种选择是将AddRange功能作为BindingList<T>的扩展方法实现。基于CSharpie的实现:
/// <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 子类。


那是一个非常棒的想法。对我非常有效。谢谢! - Umar T.

8

你可以在构造函数中传入一个List,并利用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();
        }
    }
}

由于BindingList<T>不会挂钩PropertyChanged事件,因此您无法使用_baseList.AddRange。您只能通过使用Reflection在AddRange之后为每个项目调用私有方法HookPropertyChanged来绕过此限制。但是,如果vals(您的方法参数)是一个集合,则只有这样做才有意义。否则,您可能会枚举可枚举对象两次。

这是您可以在不编写自己的BindingList的情况下获得“最佳”的方式。这应该不太困难,因为您可以从BindingList复制源代码并更改部分以满足您的需求。


1
@Rachel,我不明白为什么你需要覆盖任何功能。当使用我的方法时,到底哪里出了问题? - CSharpie
1
@Rachel 是的,我知道。但是如果您没有将List传递到构造函数中,BindingList也会这样做。 BindingList会包装其baseList。我的所有代码都是获取对它的引用,以便我们可以修改容量。我仍然调用“Add”,而不是“_baseList.Add”。 - CSharpie
那么_bindingList的作用是什么呢?我认为我们需要修改BindingList.Capacity而不是_baseList.Capacity。不过,我正在使用RaiseListChangedEvents属性进行测试,我认为你可能有点道理。 - Rachel
@Rachel 这里有一个可行的例子。 BindingList 可以与每个 IList<T> 一起使用。但是只有 List<T> 具有 Capacity。您还可以将一个大小不可变的数组传递到 BindingList<T> 中。 - CSharpie
让我们在聊天中继续这个讨论 - CSharpie
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接