Есть ли причина, по которой существующий элемент не будет найден в списке‹T› в этом кодовом блоке?

У нас есть сетка, привязанная к List<T> элементам. Всякий раз, когда пользователь нажимает «Обновить», изменения извлекаются из базы данных, и связанный список обновляется. Я столкнулся с проблемой, когда в сетку добавляются повторяющиеся элементы, и я не могу понять, почему.

Вызов базы данных возвращает два значения: List<int> идентификаторов записей, которые изменились, и List<MyClass> обновленных данных для измененных записей. Существующий код, который я отлаживаю, который определяет, что нужно обновить, выглядит примерно так:

public void FindUpdates(IList<MyClass> existingRecords,
                    IList<MyClass> updatedRecords,
                    List<int> updatedIds,
                    out IDictionary<int, int> existing,
                    out IDictionary<int, int> updated,
                    out List<int> updates,
                    out List<int> removes,
                    out List<int> adds)
{
    updates = new List<int>();
    removes = new List<int>();
    adds = new List<int>();

    existing = FindUpdated(existingRecords, updatedIds);
    updated = FindUpdated(updatedRecords, updatedIds);

    if (updatedIds != null)
    {
        // split add/update and remove
        foreach (int id in updatedIds)
        {
            if (!existing.ContainsKey(id))
                adds.Add(id);
            else if (updated.ContainsKey(id))
                updates.Add(id);
            else
                removes.Add(id);
        }

        WriteToLog(updatedIds, adds, updates, removes);
    }
}

private IDictionary<int, int> FindUpdated(IList<MyClass> records, List<int> updatedIds)
{
    IDictionary<int, int> foundItems = new Dictionary<int, int>();

    if (records != null && updatedIds != null)
    {
        for (int i = 0; i < records.Count; i++)
        {
            IMyClass r = records[i] as IMyClass ;
            if (r != null && !r.IsDisposed)
            {
                if (updatedIds.Contains(r.Id))
                {
                    foundItems.Add(r.Id, i);
                }
            }
        }
    }

    return foundItems;
}

В результате вызова FindUpdates() я получаю Dictionary<Id, Data> существующих записей, Dictionary<Id, Data> обновленных записей для их замены и List<int> идентификаторов, для которых элементы должны быть добавлены, удалены или обновлены из источника данных.

Иногда запись добавляется в сетку дважды, и я не могу понять, где этот код работает неправильно.

Я вытащил файл журнала из одного из этих экземпляров и ясно вижу следующую последовательность событий:

  • Пункт №2 добавлен в Список данных
  • Через 20 минут элемент № 2 снова добавляется в список данных.

WriteToLog() из 2-го добавления говорит мне, что

  • updatedIds содержит значения 1, 2 и 3
  • adds содержит 1 и 2
  • updates содержит 3

Основываясь на других записях журнала, я ясно вижу, что элемент № 2 был добавлен ранее и никогда не удалялся, поэтому он должен быть в переменной existingRecords, отображаемой в переменной updates, а не в adds. Кроме того, элемент № 2 был успешно обновлен несколько раз между первым и вторым добавлением, поэтому теоретически код должен работать. У меня также есть скриншот пользовательского интерфейса, показывающий обе копии элемента № 2 в сетке данных.

Примечания...

  • IsDisposed имеет значение true только в переопределенном методе .Dispose() элемента. Я не думаю, что это произошло бы.

    Изменить. С тех пор я добавил отчет в журнал и могу убедиться, что IsDisposed не установлено на true, когда это происходит.

  • Это случалось несколько раз с несколькими разными пользователями, так что это не разовая вещь. Однако я не могу воспроизвести проблему по требованию.

  • Сетка записей может быть довольно большой, в среднем насчитывая несколько тысяч элементов.

  • Я не исключаю, что вызов БД возвращает недопустимые значения или списки, в которых нет одинаковых элементов, однако я не понимаю, как это может повлиять на результат.

  • Один раз, когда я смог увидеть эту ошибку в действии, мы запускали несколько тестов, и другие пользователи довольно часто изменяли запись № 2.

  • Все это работает в фоновом потоке

  • Судя по журналу, это было запущено только один раз. Раньше он запускался за минуту до, а затем через 2 минуты.

  • Из файла журнала я вижу, что элемент № 2 был обновлен несколько раз правильно, прежде чем был неправильно добавлен во второй раз, поэтому этот код работал с существующим набором данных несколько раз ранее.

Есть ли что-нибудь в коде, показанном выше, что может привести к этому? Или, возможно, это редкая известная проблема в C#, о которой я не знаю?


person Rachel    schedule 05.03.2014    source источник
comment
Вы спрашиваете, является ли List.Contains() потокобезопасным? Если несколько потоков работают с общим состоянием, которое существует вне мьютекса/семафора/блокировки какого-либо типа, я бы потратил свое время на это, а не обвинял структуру данных.   -  person 48klocs    schedule 05.03.2014
comment
@Рэйчел: Извини. Слепое заклинание. :/   -  person Jon    schedule 05.03.2014
comment
@Jon Нет проблем, я все равно отредактировал заголовок, чтобы, надеюсь, устранить путаницу и лучше соответствовать тому, что я действительно хочу знать :)   -  person Rachel    schedule 05.03.2014
comment
@Rachel В вашем методе FindUpdated эта строка: else if (updated.ContainsKey(id)) не должна быть такой? (с оператором отрицания (!) else if (!updated.ContainsKey(id))   -  person Selman Genç    schedule 05.03.2014
comment
@ Selman22 Нет, это правильно. Сначала он находит все новые элементы, затем все добавленные элементы, а все остальное удаляется. List<MyClass> содержит комбинацию добавлений и обновлений, а List<int> содержит идентификаторы для всех объявлений, обновлений и удалений.   -  person Rachel    schedule 05.03.2014
comment
Надеюсь поле ID это не просто номер записи? Если это так, то со многими/большинством механизмов баз данных rec_no не сохраняется/не остается согласованным между запросами.   -  person Wonderbird    schedule 06.03.2014
comment
Это не выглядит неправильно. Это не так, как я бы это написал - я не уверен, почему FindUpdated не возвращает, например, Set<int>, или почему вы не используете foreach - но это совсем не выглядит алгоритмически неправильным. Меня беспокоит то, что вы сказали, что это работает в фоновом потоке. Есть ли в каком-либо другом потоке код, который может изменять объекты в выходных параметрах? Поскольку вы упомянули потоки, такие вещи просто воняют ошибкой параллельного обновления. Хотя я признаю, что путь, по которому это могло произойти, выглядит крайне неправдоподобно, поскольку вы в основном работаете с локальными данными.   -  person Matthew Walton    schedule 13.05.2014
comment
Какую сетку используете? У меня были десятки проблем с потокобезопасностью и списками со сторонними сетками.   -  person Iain Galloway    schedule 13.05.2014
comment
@MatthewWalton Я дважды проверил, и все выходные параметры создаются и инициализируются из блока кода, который вызывает FindUpdates, поэтому никакой другой код не должен получать к ним доступ.   -  person Rachel    schedule 13.05.2014
comment
@IainGalloway Я использую DevExpress GridView, WinForms версии 9.3, и он привязан к коллекции existingRecords. Поскольку я привязываюсь к коллекции, я не думаю, что элемент управления пользовательского интерфейса повлияет на это, но сталкивались ли вы с чем-то подобным в прошлом?   -  person Rachel    schedule 13.05.2014
comment
Абсолютно, особенно с элементами управления DevExpress!   -  person Iain Galloway    schedule 13.05.2014
comment
Вы говорите: На основании других записей в журнале я ясно вижу, что элемент № 2 был добавлен ранее и никогда не удалялся: Вы уверены? Структуры журналов могут изменить порядок записей в журнале. Вы можете добавить статический счетчик в свою запись журнала, который вы увеличиваете с помощью interlocked.increment(), чтобы убедиться, что № 2 не был удален раньше.   -  person jeromerg    schedule 13.05.2014
comment
@jrg Да, я уверен. Временные метки в записях журнала достаточно далеки друг от друга, и я знаю, что это не так, и последовательность других событий журнала правильная.   -  person Rachel    schedule 13.05.2014
comment
Пробовали ли вы написать класс записи со словарями в качестве свойств вместо всех этих выходных параметров? Не имея возможности видеть весь код, мне интересно, является ли это проблемой изменчивости.   -  person Parrish Husband    schedule 13.05.2014


Ответы (2)


Нет никаких причин, по которым существующий элемент не будет найден в List<T> с помощью приведенного выше кода.

У меня должен был быть тревожный звоночек в голове, когда я заметил, что выходные значения включали Dictionary<int,int> значений, содержащих идентификатор элемента и индекс элемента в существующем списке.

Происходило то, что элементы удалялись с использованием existingRecords[existing[id]], где existing[id] возвращал индекс элемента в пределах existingRecords. Чтобы это работало, элементы должны быть удалены из самого большого индекса вниз. Если меньший индекс удаляется раньше, чем больший, то больший индекс становится неправильным на 1 позицию, и неправильный элемент удаляется.

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

Мое краткосрочное решение состояло в том, чтобы отсортировать коллекцию removes, чтобы убедиться, что она отсортирована по индексу каждого элемента в порядке убывания. Моим долгосрочным решением будет переписать код. :)

person Rachel    schedule 15.05.2014

List.Contains(r) 

попытается найти объект a в списке, который a.Equals(r). если MyClass не переопределяет Equals, вы не можете быть уверены, что два разных объекта одного и того же класса равны. Я не совсем уверен в этом: но я думаю, что equals использует GetHashCode(). Если вы решите переопределить один из них, вы должны переопределить другой.

вот MSDN об этом: http://msdn.microsoft.com/en-us/library/bhkz42b3(v=vs.110).aspx

person dorhanner    schedule 05.03.2014
comment
Это список целых чисел. - person LarsTech; 05.03.2014
comment
Это правильно, что .Contains() сравнивает объекты по ссылке, однако в моем случае у меня есть List<int>, который должен правильно сравнивать целые числа по их значению. - person Rachel; 05.03.2014
comment
Я не понимаю, почему это получает плюсы. Я не использую List.Contains(r), я использую List.Contains(r.Id), а Id — это общий int, который всегда оценивается одинаково. - person Rachel; 13.05.2014