【问题标题】:How can I improve performance of an AddRange method on a custom BindingList?如何提高自定义 BindingList 上的 AddRange 方法的性能?
【发布时间】:2017-04-10 19:08:40
【问题描述】:

我有一个自定义 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我想总结为:

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

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

【问题讨论】:

  • 内部列表调整大小不太可能仅对 30000 个项目花费“不可接受的时间”。最可能的真正问题是每次添加新项目时都会引发更改事件,并且您有一些处理该事件的处理程序(例如某些 UI 控件使用列表)。您可以通过创建绑定列表并用 30k 项填充它来轻松测试它,而无需执行任何其他操作。你会看到它最多需要几毫秒。 PS:它也不会随着每个新项目调整列表的大小,当达到限制时它会增加一倍的大小。
  • @Evk 经过一些测试,您是正确的。我的问题是更改事件。感谢您的确认!

标签: c# bindinglist addrange


【解决方案1】:

CSharpie 在他的answer 中解释说,性能不佳是由于每个Add 之后触发ListChanged 事件,并展示了一种为您的自定义BindingList 实现AddRange 的方法。

另一种方法是实现AddRange 功能作为BindingList&lt;T&gt; 的扩展方法。基于 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-subclass。

【讨论】:

  • 这是一个很棒的主意。像魅力一样为我工作。谢谢!
【解决方案2】:

您可以在构造函数中传入一个 List 并使用List&lt;T&gt;.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&lt;T&gt; 不会挂钩PropertyChanged 事件。您可以通过在 AddRange 之后为每个项目调用私有方法 HookPropertyChanged 来绕过此问题。然而,这只有在vals(您的方法参数)是一个集合时才有意义。否则,您将冒着枚举可枚举两次的风险。

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

【讨论】:

  • 我不喜欢创建不同的 baseList 的想法,因为我必须覆盖所有其他功能,例如添加、删除等。在这种特定情况下,我绑定到列表并且需要BindingList的变更通知。我以为我过去曾测试过禁用 ListChanged 事件,但没有帮助,但我会再次检查。
  • @Rachel 我不明白你为什么需要覆盖任何功能。使用我的方法时到底什么不起作用?
  • 你不是使用List&lt;T&gt; 作为你的物品的基本持有者,而不是BindingList&lt;T&gt; 的默认实现吗?因此,如果我使用 myList.Add(something) 添加项目,它将被添加到 BindingList&lt;T&gt; 而不是 _baseList
  • @Rachel 是的,但so does BindingList 如果您不将 List 传递给构造函数。 Bindinglist 环绕它的 baseList。我的代码所做的只是获取对它的引用,以便我们可以修改 Capactiy。我仍然打电话给Add 而不是_baseList.Add
  • 那么_bindingList 有什么意义呢?我认为我们需要更改BindingList.Capacity,而不是_baseList.Capacity。不过,我正在使用 RaiseListChangedEvents 属性再次进行测试,我认为您可能会在这里找到一些东西
猜你喜欢
  • 2018-09-03
  • 2020-03-05
  • 1970-01-01
  • 2022-10-05
  • 1970-01-01
  • 1970-01-01
  • 2021-04-28
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多