concat 不去重,union 去重:concat 仅首尾拼接、保留所有重复;union 基于 equals/gethashcode 去重,只留首次出现元素,且需注意比较器行为与 null 处理。

Concat 不去重,Union 去重,别指望 Concat 自动 dedupe
直接说结论:Concat 就是简单拼接,两个 IEnumerable<t></t> 首尾相接,重复元素照单全收;Union 才会用 Equals + GetHashCode 去重,且只保留第一个出现的元素。如果你想要“合并+去重”,别试 Concat,它做不到。
常见错误现象:list1.Concat(list2).ToList() 返回 200 个元素,但实际有 50 个重复,你却以为 Union 是默认行为。
-
Concat时间复杂度 O(1)(延迟执行,不遍历),但最终枚举时仍是 O(n+m) -
Union必须遍历全部元素,内部用HashSet<t></t>缓存,时间复杂度 O(n+m),空间多占一份哈希表 - 两者都要求元素类型可比较:引用类型默认按引用,值类型按字段逐个比;自定义类需重写
Equals和GetHashCode,否则Union无效
用 Union 去重时,null 和 IEqualityComparer 很关键
默认 Union 对引用类型用 ReferenceEqualityComparer,所以两个内容相同的 new Person("a") 实例不会被当成重复——除非你提供自定义比较逻辑。
使用场景:合并用户列表、去重 API 返回的 DTO、合并配置项并避免重复键。
- 含
null元素?默认Union允许一个null(把它当唯一值),但多个null会被去成一个 - 想按某字段去重?必须传
IEqualityComparer<t></t>,比如Union(other, StringComparer.OrdinalIgnoreCase)或自定义PersonNameComparer - 别用
Union(list2).OrderBy(...)这种链式调用去“先去重再排序”——Union已是 O(n+m),后面再OrderBy又来一遍 O(n log n),不如先拼再 distinct 再 sort,看需求权衡
示例:list1.Union(list2, StringComparer.OrdinalIgnoreCase) —— 合并两组字符串,忽略大小写去重。
Concat 和 Union 都不改变原集合,但要注意返回类型和延迟执行
两者都返回 IEnumerable<t></t>,不是 List<t></t> 或数组。你不调用 ToList()、ToArray() 或遍历它,什么都不会发生。
容易踩的坑:在循环里反复调用 Concat 构建链,比如 result = result.Concat(item) 循环 1000 次——这会产生嵌套 1000 层的迭代器,枚举时栈深、性能差、调试难。
- 要累积拼接?改用
List<t>.AddRange()</t>或Enumerable.ToList().AddRange() - 需要多次复用结果?立刻
.ToList()或.ToArray(),别留着IEnumerable到处传 - Union 的去重逻辑只在首次枚举时运行,后续枚举仍走缓存(因为内部用了
HashSet),但 Concat 每次枚举都重新链接源序列
别混淆 Union 和 Distinct + Concat 的组合效果
list1.Union(list2) ≠ list1.Concat(list2).Distinct(),虽然结果常一样,但行为不同。
关键差异在语义和实现路径:
-
Union是“取并集”,边遍历list1边建哈希表,再遍历list2时只加不在表里的元素 → 顺序是list1全部 +list2中的新元素 -
Concat(...).Distinct()是“先拼再筛”,先生成完整序列(含所有重复),再整体去重 → 顺序仍是list1+list2,但去重依据是整个拼接后序列的首次出现位置 - 如果
list1本身有重复,Union会保留这些重复(因为它只对跨集合去重),而Distinct会把list1内部也去重
所以,真要“合并两集合且全局去重”,用 Union 更准;若要“拼起来再统一清理所有重复”,才用 Concat().Distinct()。
复杂点在于:Union 的“跨集合去重”逻辑依赖比较器的稳定性,而自定义 IEqualityComparer 如果没处理好 null 或异常,会在第一次枚举时才暴露问题——这时候堆栈已经很深了。











