sortedset基于红黑树实现,add时即完成插入、去重、排序,遍历天然升序;不支持索引访问,自定义类需显式提供比较逻辑;误用于可变对象或随机访问场景易引发语义错误。

SortedSet<t></t> 不是“加完再排序”的容器,它从第一次 Add() 就维持红黑树结构——插入、去重、排序三件事一步完成。选它,就等于选了动态有序 + 去重 + O(log n) 查找;若只是临时排序一次,用 List<t>.OrderBy()</t> 更轻量。
为什么 Add() 后遍历就是升序?不是靠 foreach 时才排
因为底层是红黑树,不是数组或链表。Add() 每次都按 IComparer<t></t> 或 IComparable<t></t> 定义的顺序执行插入+自平衡,节点位置在写入那一刻就确定了。
- 插入顺序是
5, 1, 3,遍历时一定是1, 3, 5 -
Add(3)返回false表示已存在,不会触发任何比较或移动 - 没有
set[0]这种索引访问——编译直接报错,只能用First()、Last()或ElementAt()(后者是 O(n),慎用)
自定义类排序必须显式提供比较逻辑
内置类型如 int、string 默认可用;但自定义类(比如 Person、Order)不实现比较接口,SortedSet<order></order> 会编译失败或运行时报 InvalidOperationException。
- 推荐用
Comparer<t>.Create((a, b) => a.Id.CompareTo(b.Id))</t>:临时、清晰、无副作用 - 实现
IComparable<t></t>:仅当该类型“天然有唯一顺序”且长期稳定(如按身份证号排序的Citizen类) - 如果构造时传了
IComparer<t></t>,它会完全覆盖类自身的IComparable<t></t>实现 - 别用
StringComparer.Ordinal给字符串按长度排序——那是字典序;要按长度,必须用Comparer<string>.Create(...)</string>
SortedSet 和 List/HashSet 混用时的典型误判
性能和语义错配常出现在“以为能替代”的场景:
- 用
SortedSet<t></t>存可变对象(比如未重写GetHashCode和Equals的 class),之后修改影响排序的字段(如item.Score = 999),会导致Contains()失效、Remove()找不到、遍历跳项 -
UnionWith()或IntersectWith()传入List<t></t>,内部仍是逐个Add(),不会批量建树——大数据量时比先转成另一个SortedSet<t></t>再操作慢得多 -
RemoveWhere()是O(n log n),不是O(n):每删一个都要重新平衡树,高频过滤建议改用Where()+ 新建集合 - 需要随机访问或频繁按索引读写?别用
SortedSet<t></t>——选List<t></t>或Span<t></t>;需要极致去重速度且不要顺序?选HashSet<t></t>
Redis ZSet 和 C# SortedSet 别搞混
名字像,但完全无关:SortedSet<t></t> 是纯内存结构;Redis 的 ZSet 是服务端数据结构,C# 中必须通过 StackExchange.Redis 调用原生命令(如 Database.SortedSetAdd())。
-
SortedSetAdd()的score参数必须是double,传int或null会静默失败或插成0.0 -
score不能是NaN、PositiveInfinity等非法浮点值,否则 Redis 直接返回ERR invalid float - 查某 member 排名要用
SortedSetRankAsync(key, member, Order.Descending),返回long?;null表示不存在,不是 -1 - 同分时 Redis 按 member 字典序排,无法干预;如需“同分按时间”,得把时间戳拼进 member 字符串里(如
"user:123|1715674200")
真正难处理的从来不是怎么写第一行 new SortedSet<int>()</int>,而是当业务逻辑开始修改对象字段、跨线程共享实例、或试图把它当数组使的时候——那些红黑树的隐式约束才会突然咬人一口。










