GroupBy是查找数组重复元素最直接高效的方法:先GroupBy(x=>x)按值分组,再Where(g=>g.Count()>1)筛选重复组,最后Select(g=>g.Key)提取重复值。

用 GroupBy 找出重复元素最直接
想快速拿到数组里重复出现的值,GroupBy 是最贴切的工具——它天然按值聚类,之后筛出组内数量大于 1 的键就行。别先写循环或字典手动计数,LINQ 这步能一步到位。
常见错误是只调用 GroupBy 却忘了 Where 和 Select 组合:分组本身返回的是 IGrouping<TKey, TElement>,不进一步提取 Key 就没法得到“重复的元素”这个结果。
-
GroupBy(x => x)按元素自身分组(适用于值类型或重写了Equals/GetHashCode的引用类型) - 必须接
.Where(g => g.Count() > 1)过滤出真正重复的组 - 最后用
.Select(g => g.Key)提取重复值,不是g.First()或g.ToList() - 如果原数组是
int[]、string[]这类简单类型,直接用上面三步;如果是自定义对象,得确保Equals和GetHashCode行为合理,否则分组会失效
var arr = new[] { 1, 2, 2, 3, 4, 4, 4 };
var duplicates = arr.GroupBy(x => x)
.Where(g => g.Count() > 1)
.Select(g => g.Key)
.ToArray(); // 结果:[2, 4]
Distinct + Count 导致性能翻车
有人会先用 Distinct() 拿到所有唯一值,再对每个值调用 Count(x => x == target) 判断是否重复。这在小数组里看不出问题,但数据量一上去就明显变慢——时间复杂度从 O(n) 退化成 O(n²),因为每次 Count 都要遍历整个原数组。
更隐蔽的问题是:如果数组很大且重复项少,这种写法还会触发多次枚举,而 LINQ 默认是延迟执行,但反复调用 Count 会让底层集合被反复读取,尤其在 IEnumerable<T> 来源不是数组而是数据库查询或文件流时,后果更严重。
- 避免嵌套遍历逻辑,尤其是外层
Distinct()+ 内层Count() - 如果必须用
Count,至少先把源转成List<T>或数组缓存一次:var list = source.ToList(),再操作 - 真要计数优先考虑
GroupBy,它内部只遍历一次,聚合过程高效
空值和引用类型容易漏判重复
当数组元素是 string 或自定义类时,null 值默认会被归到同一组(因为 GroupBy 对 null 的键处理是统一的),但如果你期望把多个 null 当作“重复”,得确认业务逻辑是否真需要它——有时 null 只是缺省值,不该参与去重判断。
更常见的是自定义对象没重写 Equals 和 GetHashCode,导致两个内容完全相同的对象被当成不同元素分到不同组里。这时候 GroupBy 看起来“没找出重复”,其实是比较逻辑失效了。
- 对可空值类型(如
int?),GroupBy(x => x)能正确处理多个null归为一组 - 对
string,.NET 默认的字符串比较已足够,但要注意大小写敏感性:GroupBy(x => x, StringComparer.OrdinalIgnoreCase) - 对自定义类,要么重写
Equals/GetHashCode,要么传入自定义IEqualityComparer<T>实现 - 如果只是临时比某几个字段,用匿名类型分组更安全:
GroupBy(x => new { x.Name, x.Age })
要保留原始顺序或首次出现位置?别只依赖 GroupBy
GroupBy 本身不保证输出顺序与原数组一致——虽然多数情况下它会保持“首次出现的组优先”,但这不是规范承诺的行为。如果你的需求是“按第一次出现重复的元素顺序返回”,或者要同时拿到重复值及其索引,就得额外处理。
另一个隐藏需求是:只想知道哪些元素重复了,但不想让结果里有重复值本身(比如 [2,2,4,4,4] 返回 [2,4] 而不是 [2,2,4,4,4]),这点 GroupBy + Select(g => g.Key) 已满足;但如果还要知道每个重复值出现了几次、在哪些位置,就得补上 Select 投影。
- 要保留首次出现顺序,加
.OrderBy(g => arr.ToList().IndexOf(g.Key))(注意:IndexOf在大数组里有性能代价) - 要带出现次数,用
.Select(g => new { Value = g.Key, Count = g.Count() }) - 要带所有索引位置,用
.Select(g => new { Value = g.Key, Indices = g.Select((x, i) => Array.IndexOf(arr, x)).Where(i => i >= 0).Distinct() })(不推荐,效率低;应改用带索引的遍历) - 真要精确控制顺序和上下文,不如用
for循环配合Dictionary<T, List<int>>,虽然代码多几行,但意图清晰、可控性强
重复检测看着简单,实际卡点往往不在语法,而在对数据语义的理解——比如 null 算不算有效值、大小写要不要忽略、对象相等性怎么定义。这些细节不提前想清楚,光套 LINQ 写法,很容易跑通测试却在线上出错。


















