【问题标题】:C# Generic List<T> update itemsC# Generic List<T> 更新项目
【发布时间】:2018-11-06 11:19:50
【问题描述】:

我正在使用List&lt;T&gt;,我需要更新列表中的对象属性。

高效/更快的方法是什么?我知道随着列表的增长,扫描List&lt;T&gt; 的索引会变慢,而且List&lt;T&gt; 并不是进行更新的最有效集合。

那伤心,不如:

  • 删除匹配对象然后添加一个新对象?
  • 扫描列表索引,直到找到匹配的对象,然后更新对象的属性?
  • 如果我有一个集合,让我们使用 IEnumerable,并且我想将该 IEnumerable 更新到列表中,最好的方法是什么。

存根代码示例:

public class Product
{
    public int ProductId { get; set; }
    public string ProductName { get; set; }
    public string Category { get; set; }
}

public class ProductRepository
{
    List<Product> product = Product.GetProduct();
    public void UpdateProducts(IEnumerable<Product> updatedProduct)
    {
    }
    public void UpdateProduct(Product updatedProduct)
    {
    }
}

【问题讨论】:

  • 这里的“更新项目”是什么意思并不完全清楚,可以您只需用新的产品对象替换现有的产品对象吗?无论哪种方式,如果您无法根据使它们匹配的任何内容直接访问现有对象,那么无论哪种方式,列表的线性性能都将是一个问题。究竟是什么让一个生产对象匹配另一个?产品编号?那你为什么不用字典呢?
  • “最有效/更快的方法?”没有你想的那么清楚,因为这取决于你的上下文。您的列表中有多少对象?您多久添加/删除项目?您多久访问一次项目?话虽如此,这只是选择了适合您的方法,并在遇到严重滞后时考虑性能。
  • 由于每个产品都有一个 ID,因此您应该使用字典来提高性能。 Dictionary 其中键是 ProductId
  • 我不会删除现有对象并添加新对象,如果您最终保留列表并想要替换对象,我会改为用新对象覆盖找到的索引处的元素,这更快,因为列表不必因为删除而在其他项目周围移动。
  • 感谢 cmets,这是关于使用 List 而不是其他集合。假设 LIST 有数百万个对象,如何以最快的速度对列表中的现有项目执行数十甚至数百次更新,因为您可以使用 1 个线程进行作业?

标签: c# generic-list


【解决方案1】:

如果您想要快速查找,可以考虑使用 Dictionary 而不是 List。在您的情况下,它将是产品 ID(我假设它是唯一的)。 Dictionary MSDN

例如:

public class ProductRepository
    {
        Dictionary<int, Product> products = Product.GetProduct();
        public void UpdateProducts(IEnumerable<Product> updatedProducts)
        {
            foreach(var productToUpdate in updatedProducts)
            {
                UpdateProduct(productToUpdate);
            }

            ///update code here...
        }
        public void UpdateProduct(Product productToUpdate)
        {
            // get the product with ID 1234 
            if(products.ContainsKey(productToUpdate.ProductId))
            {
                var product = products[productToUpdate.ProductId];
                ///update code here...
                product.ProductName = productToUpdate.ProductName;
            }
            else
            {
                //add code or throw exception if you want here.
                products.Add(productToUpdate.ProductId, productToUpdate);
            }
        }
    }

【讨论】:

  • 如果 Update 被替换并且 Dictionary 键中的 Id 不保持不变会发生什么情况,OP 并不建议只有非 Key 属性会改变
  • 我已经更新了上面的 UpdateProduct 代码。仅当您拥有不变的唯一密钥时,此方法才有效。如果 ProductID 发生变化,那么您可以使用“ID”,即数据库中的主键或类似的东西。
  • 这对于特定用例 +1 来说是一个不错的选择,但对于数百万条记录而言,我更愿意将它与并行处理的强大功能结合起来
  • 感谢您的回答,但我们无法使用字典。
【解决方案2】:

您的用例正在更新一个List&lt;T&gt;,它可以包含数百万条记录,并且更新的记录可以是一个子列表或只是一条记录

以下是架构:

public class Product
{
    public int ProductId { get; set; }
    public string ProductName { get; set; }
    public string Category { get; set; }
}

Product 是否包含主键,这意味着每个 Product 对象都可以唯一标识,并且没有重复项,并且每次更新都针对单个唯一记录?

如果,那么最好以Dictionary&lt;int,T&gt; 的形式排列List&lt;T&gt;,这意味着对于IEnumerable&lt;T&gt;,每次更新都是O(1) 的时间复杂度和这意味着所有更新都可以根据IEnumerable&lt;T&gt; 的大小来完成,我不希望它很大,尽管需要为不同的数据结构分配额外的内存,但这将是一个非常快速的解决方案.@JamieLupton 已经提供了类似的解决方案

如果Product被重复,没有主键,那么上面的解决方案是无效的,那么扫描List&lt;T&gt;的理想方法是Binary Search,其时间复杂度是O(logN)

现在由于IEnumerable&lt;T&gt;的大小相对较小,比如M,所以整体时间复杂度为O(M*logN),其中M远小于N,可以忽略。

List&lt;T&gt; 支持 Binary Search API,它提供元素索引,然后可以使用它来更新相关索引处的对象,请查看example 这里

对于如此多的记录,我认为最好的选择是并行处理和二分搜索

既然,线程安全是个问题,我通常做的是将List&lt;T&gt;划分为List&lt;T&gt;[],这样每个单元都可以分配给一个单独的线程,一个简单的方法是使用MoreLinq批处理Api,您可以使用Environment.ProcessorCount 获取系统处理器的数量,然后按如下方式创建IEnumerable&lt;IEnumerable&lt;T&gt;&gt;

var enumerableList = List<T>.Batch(Environment.ProcessorCount).ToList();

另一种方法是遵循自定义代码:

public static class MyExtensions
{
    // data - List<T>
    // dataCount - Calculate once and pass to avoid accessing the property everytime
    // Size of Partition, which can be function of number of processors
    public static List<T>[] SplitList<T>(this List<T> data, int dataCount, int partitionSize)
    {
        int remainderData;    
        var fullPartition = Math.DivRem(dataCount, partitionSize, out remainderData);    
        var listArray = new List<T>[fullPartition];    
        var beginIndex = 0;

        for (var partitionCounter = 0; partitionCounter < fullPartition; partitionCounter++)
        {
            if (partitionCounter == fullPartition - 1)
                listArray[partitionCounter] = data.GetRange(beginIndex, partitionSize + remainderData);
            else
                listArray[partitionCounter] = data.GetRange(beginIndex, partitionSize);    
            beginIndex += partitionSize;
        }    
        return listArray;
    }
}

现在您可以在上面生成的List&lt;T&gt;[] 上创建Task[],其中每个Task 分配给每个元素List&lt;T&gt;,然后对每个子分区进行二进制搜索。虽然它是重复的,但会使用并行处理和二进制搜索的力量。每个Task都可以启动,然后我们可以使用Task.WaitAll(taskArray)等待Task处理完成

除此之外,如果您想创建一个Dictionary&lt;int,T&gt;[] 并因此使用并行处理,那么这将是最快的。

List&lt;T&gt;[]List&lt;T&gt; 的最终集成可以使用 Linq AggregationSelectMany 完成,如下所示:

List<T>[] splitListArray = Fetch splitListArray;

// Process  splitListArray

var finalList = splitListArray.SelectMany(obj => obj).ToList()

另一种选择是使用Parallel.ForEach 以及像ConcurrentBag&lt;T&gt; 这样的线程安全数据结构,或者在替换完整对象的情况下可能是ConcurrentDictionary&lt;int,T&gt;,但是如果它的属性更新,那么简单的List&lt;T&gt; 就可以了. Parallel.ForEach 内部使用类似于我上面建议的范围分区器

上述解决方案理想情况下取决于您的用例,您应该能够使用组合来达到最佳效果。如果您需要具体示例,请告诉我

【讨论】:

  • 感谢您的回答,我们无法使用字典,列表 未排序,二进制搜索无法完成工作
  • 为什么不能使用字典,是不是因为数据重复,每个键有多个记录。二分查找问题我能理解,但是范围分区总是可行的,可以用于并行处理
  • 如果您需要任何帮助,尤其是与创建范围分区和任务并行性有关的帮助,请告诉我
  • 谢谢,我们用这个解决方案取得了很好的效果,几乎完成了......谢谢您的意见非常有帮助。
  • 很高兴它有帮助
【解决方案3】:

究竟什么是效率?

除非确实有数千个项目在执行 foreach、for 或任何其他类型的循环操作,否则很可能只会显示以毫秒为单位的差异。真的吗?因此,您在试图找到最佳方面浪费了更多时间(程序员的成本为每小时 XX 美元,而不是最终用户的成本)。

因此,如果您实际上有数千条记录,我建议通过使用 Parallel.Foreach 方法并行处理列表来提高效率,该方法可以处理更多记录以节省线程开销的时间。


恕我直言,如果记录数大于 100,则表示正在使用数据库。如果涉及数据库,请编写一个更新存储过程并收工;我很难编写一个一次性程序来执行特定更新,这可以在所述数据库中以更简单的方式完成。

【讨论】:

  • 对于并行处理,首先需要的是并发集合,它会改变底层数据结构本身,最接近的是ConcurrentBag&lt;T&gt;
  • @MrinalKamboj 这不正确,Parallel.ForEach 不会自动强制您使用并发集合。他只想访问/更新列表项的属性,而不是操作集合本身。
  • @PeterWurzinger 仅当您不更新只是阅读时,这里您正在修改原始集合,因此需要线程安全
  • 他不是在修改集合,而是在修改包含项目的属性。与List&lt;T&gt; 一起使用时,这很好。 dotnetfiddle.net/oHNNYt 编辑:请参阅此 SO-answer 以获取完整说明:stackoverflow.com/questions/11232167/…
  • 这是您的假设,OP 没有明确指出,实际上 OP 建议 删除匹配对象然后添加一个新对象,实际上您发布的链接也使用lock,当完整对象被修改/替换时。人们做的最常见的方法是用另一个对象替换一个对象,而不仅仅是属性更新,这是可取的,但可能有点乏味,并且可能导致逻辑错误。否则Parallel.ForEach 也可以使用范围分区器,我在回答中也建议过
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2017-08-06
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多