Except默认按引用比较自定义类数组,需用IEqualityComparer、lambda比较器或C#12的ExceptBy;基础类型可直接使用,注意差集方向敏感、null处理及类型一致性。

用 Except 得到差集,但默认只比较引用或简单值
直接调用 Except 对两个 int[] 或 string[] 没问题,它会按值比对;但一旦是自定义类数组(比如 User[]),默认行为是比引用——哪怕两个对象字段完全一样,也会被当成“不同”。这不是 bug,是 LINQ 的设计逻辑:没指定怎么判等,就走 EqualityComparer<t>.Default</t>。
实操建议:
- 基础类型(
int、string、DateTime等)可直接用array1.Except(array2) - 自定义类必须提供比较逻辑:实现
IEqualityComparer<t></t>,或传入 lambda 匿名比较器(C# 12 起支持更简洁的ExceptBy) - 注意返回的是
IEnumerable<t></t>,不是新数组——需要.ToArray()或.ToList()才能二次使用
ExceptBy 更适合按字段取差集(C# 12+)
如果你只想看 “ID 不在另一个集合里” 或 “Name 不重复”,ExceptBy 比写完整比较器轻量得多。它不比整个对象,只比你挑出来的键(key selector)。
常见错误现象:用 Except 处理对象数组却没重写 Equals/GetHashCode,结果空手而归;或者误以为 ExceptBy(x => x.Id) 能自动去重本数组——它只控制“和谁比”,不改变源数据顺序或重复性。
示例:
var onlyInA = usersA.ExceptBy(usersB, u => u.Id); // 取出 usersA 中 Id 不在 usersB.Id 集合里的项
使用场景:
- 同步数据前找待插入/删除记录
- 日志比对中过滤已处理的请求 ID
- 避免为简单字段比对专门写一个
IEqualityComparer
差集方向敏感,a.Except(b) ≠ b.Except(a)
这是最容易忽略的逻辑点:差集不是对称操作。就像数学里的 A − B,只关心“A 有而 B 没有”的部分,反过来要另算。
性能影响很小,但语义错不得。比如你本想找出“数据库里有、本地缓存没有”的脏数据,却写了 cache.Except(db),结果得到一堆空列表——因为缓存本来就该是子集。
实操建议:
- 把“基准集合”放左边,即你想从中筛选出“差异项”的那个数组
- 需要双向差异?老老实实调两次:
a.Except(b)和b.Except(a) - 别依赖顺序:LINQ
Except不保证返回顺序和原数组一致(虽然当前实现通常保持,但规范不承诺)
空值、null 引用、装箱类型要特别当心
string[] 里混了 null?int? 数组里有 null?Except 默认把 null 当相等处理(符合 EqualityComparer.Default 规则),但如果你用 ExceptBy(x => x.Name),而 x.Name 是 null,就会抛 NullReferenceException。
还有装箱陷阱:new object[]{1, "hello"} 和 new int[]{1} 混用 Except,类型不匹配直接报错或返回全集——因为泛型推导失败,实际走的是 object 层级比较。
实操建议:
- 提前用
.Where(x => x != null)过滤,或在 key selector 里加空值判断:u => u?.Id ?? 0 - 确保两个数组元素类型一致,尤其涉及
var或隐式转换时 - 调试时打印下
array1.Length、array2.Length和array1.Except(array2).Count(),比盲目改逻辑更快定位是不是空值捣鬼
差集看着简单,真正卡住人的永远是那几个隐性条件:null 怎么算、对象怎么比、方向有没有搞反、类型到底对不对齐。写完别急着跑,先拿两行真实数据打个断点看看结果长啥样。


















