具有实现 IComparable 的元素的 SortedSet 无法正确删除元素

SortedSet with elements implementing IComparable does not Remove Elements properly

我有一个排序集,我的数据结构包含一个 id(字符串)和一个日期。我想使用 id 避免重复,并使用日期对集合中的元素进行排序,所以我让我的数据结构以这种方式实现 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) 使用 id 删除一个元素,其中 sPlayerId 是代码中其他地方指定的 id。

事实是有时候元素没有被移除,RemoveWhere(...) returns 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" ids 列表是调用 RemoveWhere SortedSet 元素的所有 ids 的打印。 removed是RemoveWhere返回的值,playerCount是调用RemoveWhere后SortedSet的Length,"Leave for user"指定被移除元素的id,其他可以忽略。

如您所见,在本例中,id 为 jor15、jor7 和 jor10 的元素未被删除,尽管它们存在于 SortedSet 中。每次我尝试时,插入顺序和日期都不同,因此其他元素会失败。即使有时所有元素都被成功删除。我想如果我将使用相同的插入顺序和日期,结果将是相同的,但我必须对代码进行过多更改才能对其进行测试。是的,我很懒 ;)

我成功地将 CompareTo 函数更改为:

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

并在需要时使用 OrderBy 通过 DJoinDate 进行排序,但我想知道为什么我以前的 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 再次指出的那样:),我的 PreLobbyPlayer class 的第二个版本的 CompareTo 方法仍然给出不一致的结果。您可以按照已接受的答案及其评论中的解释进行操作。基本上,您可能会以包含具有相同 ID 的 PreLobbyPlayers 的 SortedSet 结束,这对我来说并不好。 SortedSet 使用相同的排序逻辑来避免重复,并且可能会省略一些元素之间的比较(我不是在抱怨 SortedSet 的实现,它是正常且高效的)。对于这种情况,我无法找到一致的 CompareTo(PreLobbyPlayer other) 实现,欢迎提出想法和建议。

我的最终解决方案是仅使用 id 来避免重复,并在需要时使用日期和 LINQ 的 OrderBy 方法对集合进行排序。对我来说,这是可以接受的,因为 SortedSet 将包含不超过 100 个元素,并且当我需要按日期排序的集合时,逻辑上只有一种情况。

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

If dates are equal, get the first, but we don't
prevent insertion of two different players with the same date

什么是"first"?考虑这两个例子:a.CompareTo(b)b.CompareTo(a)。他们提供一致的结果吗?

您的CompareTo还有另一个不一致之处:a={Id1,DateA}b={Id2,DateB}c={Id1,DateC},其中DateA<DateB<DateC。使用您的代码,您将拥有:a<bb<ca=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);
}

如果正在比较具有相同 DJoinDate(但不同 ID)的 2 PreLobbyPlayer 个对象 a 和 b,则比较结果是随机的。
根据哪个是'this',哪个是'other',有时a会比b大,有时b会比a大。

如果 DJoinDate 相同,您可以只比较 id,这样顺序将始终相同。

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