Элементы обновления C# Generic List‹T›

Я использую List<T>, и мне нужно обновить свойства объектов, которые есть в списке.

Как это сделать наиболее эффективно/быстрее? Я знаю, что просмотр индекса List<T> будет медленнее по мере роста этого списка и что List<T> не самая эффективная коллекция для обновления.

Печально, лучше бы:

  • Удалить объект соответствия, а затем добавить новый?
  • Просмотрите индексы списка, пока не найдете соответствующий объект, а затем обновите свойства объекта?
  • Если у меня есть коллекция, давайте 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)
    {
    }
}

person J_PT    schedule 06.11.2018    source источник
comment
Не совсем понятно, что вы имеете в виду под элементами обновления, можете просто заменить существующий объект продукта новым? В любом случае, если у вас нет прямого доступа к существующим объектам на основе того, что делает их совпадениями, то линейная производительность списка будет проблемой в любом случае. Что именно заставляет один производственный объект соответствовать другому? Код товара? Почему тогда ты не пользуешься словарем?   -  person Lasse V. Karlsen    schedule 06.11.2018
comment
самый эффективный/быстрый способ сделать это? не так ясно, как вы думаете, потому что это зависит от вашего контекста. Сколько объектов живет в вашем списке? Как часто вы добавляете/удаляете элементы? Как часто вы получаете доступ к элементу? Сказав это, просто выберите то, что работает для вас, и подумайте о производительности, если вы столкнетесь с серьезными задержками.   -  person HimBromBeere    schedule 06.11.2018
comment
Поскольку у вас есть идентификатор для каждого продукта, вы должны использовать словарь для повышения производительности. Словарь‹int, Product›, где ключ ProductId   -  person Ricardo Alves    schedule 06.11.2018
comment
Я бы не стал удалять существующий объект и добавлять новый, если вы в конечном итоге сохраните список и захотите заменить объект, вместо этого я вместо этого перезапишу элемент в найденном индексе новым объектом, это быстрее, потому что список делает не нужно перемещать другие элементы из-за удаления.   -  person Lasse V. Karlsen    schedule 06.11.2018
comment
Спасибо за комментарии, речь идет об использовании List‹T›, а не других коллекций. Допустим, СПИСОК содержит миллионы объектов. Как выполнить десятки или даже сотни обновлений в минуту для существующих элементов в этом списке быстрее, чем вы можете, используя 1 поток для задания?   -  person J_PT    schedule 06.11.2018
comment
Список постоянно пополняется новыми товарами, проблем с добавлением новых товаров нет. Обновление существующих продуктов в списке‹› требует двух вещей: 1- поиск по индексу продукта (нет Product.Id для сопоставления, но у нас уже есть расширение, которое выполняет сопоставление продукта для обновления). 2 - Обновите продукт (здесь мы обнаружили, что копирование свойств продукта для обновления в списке «Продукты» является медленным процессом), поэтому, используя список, подобный этому, мы можем обновить весь объект за 1 операцию. вместо того, чтобы отображать все свойства, а затем сохранять их в список?   -  person J_PT    schedule 06.11.2018


Ответы (3)


Ваш вариант использования обновляет List<T>, который может содержать миллионы записей, а обновленные записи могут быть вложенным списком или просто одной записью.

Ниже приведена схема:

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

Содержит ли Product первичный ключ, что означает, что каждый объект Product может быть однозначно идентифицирован, нет дубликатов и каждое обновление предназначено для одной уникальной записи?

Если Да, то лучше расположить List<T> в форме Dictionary<int,T>, что будет означать, что для IEnumerable<T> каждое обновление будет иметь временную сложность O(1), и это будет означать, что все обновления могут выполняться в зависимости от размер IEnumerable<T>, который, я не ожидаю, будет очень большим, и хотя потребуется дополнительное выделение памяти для другой структуры данных, но это будет очень быстрое решение. @JamieLupton уже предоставил решение в аналогичных строках

В случае, если Product повторяется, первичный ключ отсутствует, тогда приведенное выше решение недействительно, тогда идеальным способом просмотра List<T> является двоичный поиск, временная сложность которого составляет O(logN).

Теперь, поскольку размер IEnumerable<T> сравнительно мал, скажем, M, поэтому общая временная сложность будет O(M*logN), где M намного меньше, чем N, и ею можно пренебречь.

List<T> поддерживает API бинарного поиска, который предоставляет индекс элемента, который затем можно использовать для обновления объекта по соответствующему индексу, проверьте пример здесь

Лучшим вариантом, по моему мнению, для такого большого количества записей будет параллельная обработка вместе с бинарным поиском.

Теперь, поскольку безопасность потоков является проблемой, что я обычно делаю, так это делю List<T> на List<T>[], поскольку тогда каждый блок может быть назначен отдельному потоку, простым способом является использование MoreLinq пакетного API, где вы можете получить количество системных процессоров. используя Environment.ProcessorCount, а затем создайте IEnumerable<IEnumerable<T>> следующим образом:

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;
    }
}

Теперь вы можете создать Task[], где каждый Task назначается каждому элементу List<T>, на List<T>[], сгенерированном выше, затем двоичный поиск для каждого подраздела. Хотя это повторяется, но будет использовать возможности параллельной обработки и двоичного поиска. Каждый Task может быть запущен, а затем мы можем подождать, используя Task.WaitAll(taskArray), чтобы дождаться завершения обработки задачи.

Кроме того, если вы хотите создать Dictionary<int,T>[] и, таким образом, использовать параллельную обработку, это будет самым быстрым.

Окончательную интеграцию List<T>[] в List<T> можно выполнить с помощью Linq Aggregation или SelectMany следующим образом:

List<T>[] splitListArray = Fetch splitListArray;

// Process  splitListArray

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

Другим вариантом может быть использование Parallel.ForEach вместе с потокобезопасной структурой данных, такой как ConcurrentBag<T>, или может быть ConcurrentDictionary<int,T> в случае, если вы заменяете полный объект, но если его свойство обновляется, тогда будет работать простой List<T>. Parallel.ForEach внутренне используйте разделитель диапазона, аналогичный тому, что я предложил выше

Упомянутые выше решения в идеале зависят от вашего варианта использования, вы сможете использовать комбинацию для достижения наилучшего результата. Дайте мне знать, если вам нужен конкретный пример

person Mrinal Kamboj    schedule 06.11.2018
comment
спасибо за ваш ответ, мы не можем использовать словари, список‹T› не отсортирован, двоичный поиск не поможет - person J_PT; 06.11.2018
comment
Почему словари нельзя использовать, это из-за дублирования данных, нескольких записей на ключ. Я могу понять проблему бинарного поиска, но разделение диапазона всегда возможно, что можно использовать для параллельной обработки. - person Mrinal Kamboj; 06.11.2018
comment
Дайте мне знать, если вам нужна помощь, особенно в отношении создания раздела диапазона и параллелизма задач. - person Mrinal Kamboj; 07.11.2018
comment
спасибо, у нас были хорошие результаты с этим решением, почти готово... спасибо за очень полезный вклад. - person J_PT; 07.11.2018
comment
Рад, что был полезен - person Mrinal Kamboj; 07.11.2018

Вы можете рассмотреть возможность использования словаря вместо списка, если вам нужен быстрый поиск. В вашем случае это будет идентификатор продукта (который, как я предполагаю, уникален). Словарь 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);
            }
        }
    }
person Jamie Lupton    schedule 06.11.2018
comment
Что произойдет, если Update является заменой, а Id в ключе словаря не останется прежним, OP не предполагает, что изменятся только неключевые атрибуты - person Mrinal Kamboj; 06.11.2018
comment
Я обновил код UpdateProduct выше. Этот подход будет работать только в том случае, если у вас есть уникальный ключ, который не меняется. Если ProductID изменится, вы можете использовать «ID», то есть первичный ключ из базы данных или что-то подобное. - person Jamie Lupton; 06.11.2018
comment
Это был бы хороший вариант для конкретного варианта использования +1, хотя в идеале для миллионов записей я бы предпочел совместить его с мощностью параллельной обработки. - person Mrinal Kamboj; 06.11.2018
comment
Спасибо за ваш ответ, но мы не можем использовать словари. - person J_PT; 06.11.2018

Что такое эффективность?

Если не существует буквально тысяч элементов, выполняющих foreach или for или любой другой тип циклической операции, скорее всего, будет отображаться разница только в миллисекундах. Действительно? Следовательно, вы потратили больше времени (стоимостью программиста XX долларов в час, чем стоимость работы конечного пользователя), пытаясь найти лучшее.

Поэтому, если у вас есть буквально тысячи записей, я бы порекомендовал найти эффективность путем параллельной обработки списка с помощью Parallel.Foreach, который может обрабатывать больше записей, чтобы сэкономить время из-за накладных расходов на многопоточность.


ИМХО, если количество записей больше 100, это означает, что используется база данных. Если задействована база данных, напишите процедуру обновления и закройте ее; Мне было бы трудно написать одноразовую программу для выполнения конкретного обновления, которое можно было бы сделать более простым способом в указанной базе данных.

person ΩmegaMan    schedule 06.11.2018
comment
Для параллельной обработки в первую очередь требуется параллельная коллекция, которая изменит саму базовую структуру данных, ближайшей из которых является ConcurrentBag<T>. - person Mrinal Kamboj; 06.11.2018
comment
@MrinalKamboj Это неверно, Parallel.ForEach не заставляет вас автоматически использовать параллельную коллекцию. Он хочет только получить доступ/обновить свойства элементов списка, не манипулируя самой коллекцией. - person Peter Wurzinger; 06.11.2018
comment
@PeterWurzinger, только когда вы не обновляете, а просто читаете, здесь вы изменяете исходную коллекцию, поэтому требуется безопасность потоков - person Mrinal Kamboj; 06.11.2018
comment
Он изменяет не коллекцию, а свойства содержащихся в ней элементов. Когда идете с List<T>, это нормально. dotnetfiddle.net/oHNNYt РЕДАКТИРОВАТЬ: полное объяснение см. в этом SO-ответе: stackoverflow.com/questions/11232167/ - person Peter Wurzinger; 06.11.2018
comment
Это ваше предположение, ОП четко не указал, на самом деле ОП предлагает удалить объект соответствия, а затем добавить новый, и на самом деле ссылка, которую вы разместили, также использует lock, когда изменяется весь объект / заменены. Наиболее распространенный способ, который делают люди, — это замена одного объекта другим, а не только обновление свойств, что желательно, но может быть немного утомительным и может привести к логическим ошибкам. В противном случае Parallel.ForEach также работает с использованием разделителя диапазона, который я тоже предложил в своем ответе. - person Mrinal Kamboj; 06.11.2018
comment
Мне нужно обновить свойства объектов - это то, чего он хочет достичь, и удалить объект совпадения, а затем добавить новый - это возможный вариант, который он хочет обсудить с точки зрения эффективности. Я не вижу никакого намерения или необходимости изменять саму коллекцию, поэтому использование Parallel.ForEach на простом List<T> без оболочки параллелизма кажется мне законным подходом. РЕДАКТИРОВАТЬ: использование Parallel.ForEach подразумевает потенциальное использование нескольких потоков, что, скорее всего, противоречит ограничению использования только 1 потока (как указано в комментарии). - person Peter Wurzinger; 06.11.2018
comment
Это верно, Питер Вурцингер. Кроме того, все эти объекты находятся в объектах памяти, в этой части процесса не развивалась БД. это очень большие коллекции в памяти, не отсортированные. - person J_PT; 06.11.2018