SortedSet с элементами, реализующими IComparable, не удаляет элементы должным образом

У меня есть отсортированный набор с моей структурой данных, которая содержит идентификатор (строку) и дату. Я хочу избежать дублирования с использованием идентификатора и отсортировать элементы в наборе по дате, поэтому я сделал свою структуру данных, реализующую интерфейс IComparable<T> таким образом:

public class PreLobbyPlayer : IComparable<PreLobbyPlayer>
{
    public int CompareTo(PreLobbyPlayer other)
    {
        // First avoid duplicate players
        int compResult = this.SPlayerId.CompareTo( other.SPlayerId);
        if ( compResult == 0 )
            return compResult;

        // Then compare join dates
        compResult = this.DJoinDate.CompareTo(other.DJoinDate);
        // If dates are equal, get the first, but we don't 
        // prevent insertion of two different players with the same date
        return compResult == 0 ? -1 : compResult;
    }
}

Затем я пытаюсь удалить один элемент, используя идентификатор с помощью метода LINQ RemoveWhere(p => p.SPlayerId == sPlayerId), где sPlayerId — это идентификатор, указанный в другом месте кода.

Дело в том, что иногда элемент не удаляется, RemoveWhere(...) возвращает 0 и элемент все еще находится в SortedSet<PreLobbyPlayer>

Я кодирую некоторую отладочную информацию, и вот результат:

jor0|jor11|jor12|jor8|jor5|jor14|jor9|jor3|jor13|jor15|jor10|jor7|jor4|jor1|jor2|

Leave for user jor6, playerCount is: 15, removed is 1, prevSOwnerPlayerId is jor0

jor0|jor11|jor12|jor8|jor5|jor14|jor9|jor13|jor3|jor10|jor15|jor4|jor7|jor1|jor2|

Leave for user jor7, playerCount is: 15, removed is 0, prevSOwnerPlayerId is jor0

jor0|jor11|jor12|jor8|jor14|jor9|jor13|jor3|jor10|jor15|jor4|jor7|jor1|jor2|

Leave for user jor5, playerCount is: 14, removed is 1, prevSOwnerPlayerId is jor0

jor0|jor11|jor12|jor8|jor14|jor9|jor3|jor13|jor15|jor10|jor7|jor1|jor2|

Leave for user jor4, playerCount is: 13, removed is 1, prevSOwnerPlayerId is jor0

jor0|jor12|jor8|jor14|jor9|jor13|jor3|jor10|jor15|jor7|jor1|jor2|

Leave for user jor11, playerCount is: 12, removed is 1, prevSOwnerPlayerId is jor0

jor0|jor12|jor8|jor14|jor9|jor3|jor13|jor15|jor10|jor7|jor1|

Leave for user jor2, playerCount is: 11, removed is 1, prevSOwnerPlayerId is jor0

jor0|jor12|jor8|jor14|jor9|jor13|jor3|jor10|jor15|jor7|jor1|

Leave for user jor15, playerCount is: 11, removed is 0, prevSOwnerPlayerId is jor0

jor0|jor8|jor14|jor9|jor13|jor3|jor10|jor15|jor7|jor1|

Leave for user jor12, playerCount is: 10, removed is 1, prevSOwnerPlayerId is jor0

jor0|jor8|jor14|jor9|jor3|jor13|jor15|jor10|jor7|

Leave for user jor1, playerCount is: 9, removed is 1, prevSOwnerPlayerId is jor0

jor0|jor8|jor9|jor13|jor3|jor10|jor15|jor7|

Leave for user jor14, playerCount is: 8, removed is 1, prevSOwnerPlayerId is jor0

jor8|jor9|jor3|jor13|jor15|jor10|jor7|

Leave for user jor0, playerCount is: 7, removed is 1, prevSOwnerPlayerId is jor0

jor8|jor13|jor3|jor10|jor15|jor7|

Leave for user jor9, playerCount is: 6, removed is 1, prevSOwnerPlayerId is jor8

jor8|jor3|jor13|jor15|jor10|jor7|

Leave for user jor10, playerCount is: 6, removed is 0, prevSOwnerPlayerId is jor8

jor8|jor13|jor15|jor10|jor7|

Leave for user jor3, playerCount is: 5, removed is 1, prevSOwnerPlayerId is jor8

jor8|jor10|jor15|jor7|

Leave for user jor13, playerCount is: 4, removed is 1, prevSOwnerPlayerId is jor8

jor15|jor10|jor7|

Leave for user jor8, playerCount is: 3, removed is 1, prevSOwnerPlayerId is jor8

список идентификаторов "jorxx" - это печать всех идентификаторов элементов SortedSet после вызова RemoveWhere. remove — это значение, возвращаемое RemoveWhere, playerCount — это длина SortedSet после вызова RemoveWhere, «Leave for user» указывает идентификатор удаленного элемента, а остальные можно игнорировать.

Как видите, в этом случае элементы с идентификаторами jor15, jor7 и jor10 не удаляются, хотя они присутствуют в SortedSet. Каждый раз, когда я пытаюсь, порядок вставки и даты меняются, поэтому другие элементы не работают. Даже бывают случаи, когда все элементы успешно удаляются. Я предполагаю, что если я буду использовать тот же порядок вставки и даты, результаты будут такими же, но мне нужно слишком сильно изменить код, чтобы проверить его. Да, я ленивый ;)

Я заставил его работать, изменив функцию CompareTo на:

return this.SPlayerId.CompareTo(other.SPlayerId);

и упорядочивание с помощью DJoinDate с использованием OrderBy, когда это необходимо, но я хотел бы знать, почему моя прежняя реализация CompareTo нарушает логику SortedSet.

Редактировать:

Как указал PetSerAl, мой метод CompareTo не давал согласованных результатов, когда даты считались равными.

Я изменил CompareTo на:

public class PreLobbyPlayer : IComparable<PreLobbyPlayer>
{
    public int CompareTo(PreLobbyPlayer other)
    {
        // First avoid duplicate players
        int compIdResult = this.SPlayerId.CompareTo( other.SPlayerId);
        if ( compIdResult == 0 )
            return compIdResult;

        // Then compare join dates
        int compDateResult = this.DJoinDate.CompareTo(other.DJoinDate);
        // If dates are equal, return the id comparison result to give consistent results.
        return compDateResult == 0 ? compIdResult : compDateResult;
    }
}

и работал как шарм. Спасибо.

Редактировать:

Как снова указал @PetSerAl :), моя вторая версия метода CompareTo для класса PreLobbyPlayer по-прежнему дает противоречивые результаты. Вы можете следить за объяснением в принятом ответе и его комментариях. По сути, вы можете закончить с SortedSet, содержащим PreLobbyPlayers с тем же идентификатором, и это нехорошо для меня. SortedSet использует ту же логику упорядочения, чтобы избежать дублирования и может опустить некоторые сравнения между элементами (я не жалуюсь на реализацию SortedSet, она нормальная и эффективная). Я не смог найти последовательную реализацию CompareTo (PreLobbyPlayer other) для этого случая, идеи и предложения приветствуются.

Мое окончательное решение использует только идентификатор, чтобы избежать дублирования и упорядочить набор, используя дату и метод LINQ OrderBy, когда это необходимо. Для меня это приемлемо, потому что SortedSet будет содержать не более 100 элементов и в логике есть только один случай, когда мне нужна коллекция, упорядоченная по дате.

public class PreLobbyPlayer : IComparable<PreLobbyPlayer>
{
   public int CompareTo(PreLobbyPlayer other)
   {
       return this.SPlayerId.CompareTo( other.SPlayerId);
   }
   ...
}

person George Sideburns    schedule 09.04.2015    source источник
comment
какой ужасный результат, не могли бы вы немного упростить результат?   -  person zinking    schedule 09.04.2015


Ответы (2)


Если даты совпадают, получаем первый, но мы не предотвращаем
вставку двух разных игроков с одинаковой датой

Что такое "первый"? Рассмотрим два примера: a.CompareTo(b) и b.CompareTo(a). Дают ли они стабильный результат?

В ваших CompareTo есть еще одно несоответствие: a={Id1,DateA}, b={Id2,DateB}, c={Id1,DateC}, где DateA<DateB<DateC. С вашим кодом у вас будет: a<b, b<c и a=c. Таким образом, ваша реализация CompareTo не всегда предотвращает добавление двух элементов с одинаковыми SPlayerId в SortedSet.

var a=new PreLobbyPlayer { SPlayerId=1,DJoinDate=DateTime.Today };
var b=new PreLobbyPlayer { SPlayerId=2,DJoinDate=DateTime.Today.AddDays(1) };
var c=new PreLobbyPlayer { SPlayerId=1,DJoinDate=DateTime.Today.AddDays(2) };
var set=new SortedSet<PreLobbyPlayer>();
set.Add(b);
set.Add(a);
set.Add(c);
foreach(var current in set) {
    Console.WriteLine("{0}: {1}",current.SPlayerId,current.DJoinDate);
}
person user4003407    schedule 09.04.2015
comment
во-первых это, и может быть вы нашли ключ этой проблемы, я проверю, спасибо. - person George Sideburns; 09.04.2015
comment
Вы правы, я изменил метод CompareTo на это: - person George Sideburns; 09.04.2015
comment
Я вижу несоответствие в том, что b больше и меньше одновременно, чем две вещи, считающиеся равными, но я не понимаю, как можно одновременно иметь a и c в SortedSet‹PreLobbyPlayer› - person George Sideburns; 10.04.2015
comment
@GeorgeSideburns Если SortedSet сначала сравнить a с b и b с c, то он просто предполагает, что a<c и никогда не утруждает себя их сравнением. - person user4003407; 10.04.2015
comment
Хорошо, теперь я вижу, спасибо за ваше объяснение и ваше время :) - person George Sideburns; 11.04.2015

Если сравниваются 2 объекта PreLobbyPlayer a и b с одинаковыми DJoinDate (но разными идентификаторами), результат сравнения будет случайным.
В зависимости от того, какой из них является «этим», а какой «другим», иногда быть больше, чем b, а иногда b будет больше, чем a.

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

public int CompareTo(PreLobbyPlayer other)
{
    // First avoid duplicate players
    int idCompare = this.SPlayerId.CompareTo(other.SPlayerId);
    if (idCompare == 0) return 0;

    // Then compare join dates
    int dateCompare = this.DJoinDate.CompareTo(other.DJoinDate);
    // If dates are equal, get the result from the first comparison 
    return dateCompare == 0 ? idCompare : dateCompare;
}
person Dennis_E    schedule 09.04.2015
comment
Предположительно, если идентификаторы совпадают, то и даты совпадают. Если нет, это вызовет другие проблемы. Если это так, код можно сделать немного проще, выполнив сначала сравнение дат. - person ; 09.04.2015
comment
Я согласен с вами Dennis_E, я хотел бы дать вам +1, но у меня пока недостаточно репутации :( - person George Sideburns; 09.04.2015
comment
@GeorgeSideburns не беспокойтесь об этом ;) - person Dennis_E; 09.04.2015