C# Enumerable类 之 集合操作
总目录
前言
在 C# 中,System.Linq.Enumerable 类是 LINQ(Language Integrated Query)的核心组成部分,它提供了一系列静态方法,用于操作实现了 IEnumerable 接口的集合。通过这些方法,我们可以轻松地对集合进行查询、转换、排序和聚合等操作。
本文属于 C# Enumerable类 使用详解 中的一个章节,着重介绍 C# Enumerable 类中集合操作这部分的内容。
一、概览
| 方法 | 描述 | 示例 | 
|---|---|---|
| Concat | 序列合并(不去重) | list1.Concat(list2); | 
| Union和UnionBy | 序列并集(去重) | list1.Union(list2); | 
| Except和ExceptBy  | 序列差集 | list1.Except(list2); | 
| Intersect和IntersectBy  | 序列交集 | list1.Intersect(list2); | 
二、Concat :合并(不去重)
 
1. 什么是 Concat
Concat 是 LINQ 提供的一个扩展方法,用于将两个序列连接在一起,并返回一个包含所有元素的新序列。需要注意的是,Concat 方法不会去除重复项,如果需要去重可以使用 Union 方法。
2. Concat 方法 基本信息
1) Concat
public static IEnumerable<TSource> Concat<TSource>(this IEnumerable<TSource> first,IEnumerable<TSource> second)
- 参数: 
  - first:第一个要连接的序列。
- second:第二个要连接的序列。
 
- 返回值:Concat方法返回一个IEnumerable<T>对象,其中T是输入序列中元素的类型。
2)工作原理
Concat 方法通过惰性求值和迭代器模式,将两个序列依次遍历并返回一个新的序列,过程中不会创建新的集合对象,而是逐个返回两个序列中的元素。
3)使用场景
适用于合并多个集合、处理流式数据、数据预处理等场景。
3. 使用示例
示例 1:基本连接
假设我们有两个整数列表,我们希望将它们连接在一起形成一个新的列表。
class Program
{
    static void Main()
    {
        List<int> list1 = new List<int> { 1, 2, 3 };
        List<int> list2 = new List<int> { 4, 5, 6 };
        // 使用 Concat 连接两个列表
        var concatenatedList = list1.Concat(list2);
        
        Console.WriteLine(string.Join(",",concatenatedList));
        // 输出:1,2,3,4,5,6
    }
}
示例 2:不同类型的序列
虽然 Concat 方法要求两个序列的元素类型相同,但可以通过泛型和隐式转换来处理不同类型的数据。
class Program
{
    static void Main()
    {
        List<string> list1 = new List<string> { "Apple", "Banana" };
        List<object> list2 = new List<object> { 1, 2.5, true };
        // 使用 Concat 连接两个不同类型的列表
        var concatenatedList = list1.Cast<object>().Concat(list2);
        Console.WriteLine(string.Join(",",concatenatedList));
        // 输出:Apple,Banana,1,2.5,True
    }
}
示例 3:空序列
Concat 方法也可以用于将一个非空序列与一个空序列连接。
class Program
{
    static void Main()
    {
        List<int> list1 = new List<int> { 1, 2, 3 };
        List<int> list2 = new List<int>();
        // 使用 Concat 连接非空序列和空序列
        var concatenatedList = list1.Concat(list2);
        Console.WriteLine(string.Join(",",concatenatedList));
        // 输出:1,2,3
    }
}
示例 4:合并多个集合
当需要合并多个集合时,可以多次使用 Concat 方法,或者使用 Union 方法来去除重复项。
class Program
{
    static void Main()
    {
        List<int> list1 = new List<int> { 1, 2, 3 };
        List<int> list2 = new List<int> { 3, 4, 5, 6 };
        List<int> list3 = new List<int> { 6, 7, 8, 9 };
        // 使用 Concat 合并多个集合
        var concatenatedList = list1.Concat(list2).Concat(list3);
        Console.WriteLine(string.Join(",", concatenatedList));
        // 输出:1,2,3,3,4,5,6,6,7,8,9
        // 使用 Union 去除重复项
        var unionedList = list1.Union(list2).Union(list3);
        Console.WriteLine(string.Join(",", unionedList));
        // 输出:1,2,3,4,5,6,7,8,9
    }
}
示例 5:处理流式数据
由于 Concat 方法采用惰性求值,因此非常适合处理流式数据或无限序列。
class Program
{
    static void Main()
    {
        IEnumerable<int> streamData = GenerateStreamData();
        List<int> additionalData = new List<int> { 10, 20, 30 };
        // 使用 Concat 连接流式数据和附加数据
        var concatenatedStream = streamData.Concat(additionalData);
        // 输出结果
        Console.WriteLine("Concatenated Stream Data (First 10 elements):");
        foreach (var item in concatenatedStream.Take(10))
        {
            Console.Write(item + " ");
        }
    }
    static IEnumerable<int> GenerateStreamData()
    {
        int i = 0;
        while (true)
        {
            yield return i++;
        }
    }
}
输出结果:
Concatenated Stream Data (First 10 elements):
0 1 2 3 4 5 6 7 8 9
示例 6:数据预处理
在某些情况下,你可能需要对数据进行预处理,然后再将其与其他数据连接起来。
using System;
using System.Collections.Generic;
using System.Linq;
class Program
{
    static void Main()
    {
        List<int> rawData = new List<int> { 1, 2, 3, 4, 5 };
        List<int> processedData = rawData.Select(x => x * 2).ToList();
        List<int> additionalData = new List<int> { 10, 20, 30 };
        // 使用 Concat 连接预处理后的数据和其他数据
        var concatenatedData = processedData.Concat(additionalData);
        // 输出结果
        Console.WriteLine("Concatenated Data:");
        foreach (var item in concatenatedData)
        {
            Console.Write(item + " ");
        }
    }
}
输出结果:
Concatenated Data:
2 4 6 8 10 10 20 30
4. 注意事项
尽管 Concat 方法非常有用,但在实际应用中也有一些需要注意的地方:
- 空集合处理: 如果源集合为空,Concat方法将返回一个空的结果集。因此,在使用Concat方法之前,通常不需要检查集合是否为空。
- 数据类型一致:适用于需要合并的集合对象的数据类型是一样的情况。
三、Union 和 UnionBy:并集(去重)
 

1. 什么是 Union/UnionBy
- Union是 LINQ 提供的一个扩展方法,用于将两个序列合并成一个新的序列,并去除重复的元素。这个方法非常有用,特别是在需要合并多个集合并确保结果中没有重复项时。
- UnionBy是 .NET 6 及以上版本中引入的一个扩展方法,用于合并两个序列并根据指定的键选择器函数去除重复项。与 Union 方法不同的是,- UnionBy允许你通过一个自定义的键来确定元素是否相同,而不是直接比较整个对象。
2. Union/UnionBy 方法 基本信息
1) Union/UnionBy
public static IEnumerable<TSource> Union<TSource>(
    this IEnumerable<TSource> first,
    IEnumerable<TSource> second
)
public static IEnumerable<TSource> Union<TSource>(
    this IEnumerable<TSource> first,
    IEnumerable<TSource> second,
    IEqualityComparer<TSource> comparer
)
- 参数: 
  - first:第一个要合并的序列。
- second:第二个要合并的序列。
- comparer(可选):一个 IEqualityComparer<TSource>实现,用于比较元素是否相等。
 
- 返回值:Union方法返回一个IEnumerable<T>对象,其中T是输入序列中元素的类型。
public static IEnumerable<TSource> UnionBy<TSource, TKey>(
    this IEnumerable<TSource> first,
    IEnumerable<TSource> second,
    Func<TSource, TKey> keySelector
)
public static IEnumerable<TSource> UnionBy<TSource, TKey>(
    this IEnumerable<TSource> first,
    IEnumerable<TSource> second,
    Func<TSource, TKey> keySelector,
    IEqualityComparer<TKey> comparer
)
- 参数: 
  - first:第一个要合并的序列。
- second:第二个要合并的序列。
- keySelector:一个函数,用于从每个元素中提取键值。
- comparer(可选):一个 IEqualityComparer 实现,用于比较键值是否相等。
 
- 返回值:UnionBy方法返回一个IEnumerable<TSource>对象,其中TSource是输入序列中元素的类型。
2)工作原理
- Union方法通过惰性求值和迭代器模式,将两个序列合并成一个新的序列,并在遍历过程中自动去除重复元素。
- UnionBy方法通过指定的键选择器函数从两个序列中提取键值,并根据这些键值合并序列,去除重复项,最终返回一个包含唯一键值元素的新序列。
3)使用场景
适用于合并多个集合、处理流式数据、数据预处理、复杂数据结构合并等场景。
3. 使用示例
示例 1:基本合并
假设我们有两个整数列表,我们希望将它们合并在一起形成一个新的列表,并去除重复项。
class Program
{
    static void Main()
    {
        List<int> list1 = new List<int> { 1, 2, 3 };
        List<int> list2 = new List<int> { 3, 4, 5 };
        // 使用 Union 合并两个列表并去除重复项
        var unionedList = list1.Union(list2);
        Console.WriteLine(string.Join(",", unionedList));// 输出:1,2,3,4,5
        // 使用 UnionBy 合并两个列表并去除重复项
        unionedList = list1.UnionBy(list2, x => x);
        Console.WriteLine(string.Join(",", unionedList));// 输出:1,2,3,4,5
    }
}
在这个例子中,我们使用 Union / UnionBy 方法将两个整数列表合并在一起,并打印了结果。注意,数字 3 只出现了一次。
示例 2:自定义对象
当处理自定义对象时,Union 方法默认使用对象的 Equals 和 GetHashCode 方法来判断元素是否相同。如果你有更复杂的比较需求,可以提供自定义的 IEqualityComparer<T> 实现。
- 使用默认的 Equals和GetHashCode方法,并不能实现对象内容的比较
class Person
{
    public string Name { get; set; }
    public int Age { get; set; }
    public override string ToString()
    {
        return $"{Name} ({Age})";
    }
}
class Program
{
    static void Main()
    {
        List<Person> list1 = new List<Person>
        {
            new Person { Name = "Alice", Age = 30 },
            new Person { Name = "Bob", Age = 25 }
        };
        List<Person> list2 = new List<Person>
        {
            new Person { Name = "Charlie", Age = 30 },
            new Person { Name = "Bob", Age = 25 }
        };
        // 使用 Union 合并两个列表并去除重复项
        var unionedPeople = list1.Union(list2);
        Console.WriteLine(string.Join(",",unionedPeople));
        // 输出:Alice (30),Bob (25),Charlie (30),Bob (25)
    }
}
- 重写 对象的 Equals和GetHashCode方法,可以实现对象内容的比较。使用Union时,默认就会调用重写后的方法判断对象是否相等
class Person
{
    public string Name { get; set; }
    public int Age { get; set; }
    public override bool Equals(object obj)
    {
        if (obj is Person other)
        {
            return Name == other.Name && Age == other.Age;
        }
        return false;
    }
    public override int GetHashCode()
    {
        return HashCode.Combine(Name, Age);
    }
    public override string ToString()
    {
        return $"{Name} ({Age})";
    }
}
class Program
{
    static void Main()
    {
        List<Person> list1 = new List<Person>
        {
            new Person { Name = "Alice", Age = 30 },
            new Person { Name = "Bob", Age = 25 }
        };
        List<Person> list2 = new List<Person>
        {
            new Person { Name = "Charlie", Age = 30 },
            new Person { Name = "Bob", Age = 25 }
        };
        // 使用 Union 合并两个列表并去除重复项
        var unionedPeople = list1.Union(list2);
        Console.WriteLine(string.Join(",",unionedPeople));
        // 输出:Alice (30),Bob (25),Charlie (30)
    }
}
- 使用自定义的 PersonComparer来比较Person对象,并确保合并后的结果中没有重复项。
class Person
{
    public string Name { get; set; }
    public int Age { get; set; }
    public override string ToString()
    {
        return $"{Name} ({Age})";
    }
}
class PersonComparer : IEqualityComparer<Person>
{
    public bool Equals(Person x, Person y)
    {
        if (x == null || y == null) return false;
        return x.Name == y.Name && x.Age == y.Age;
    }
    public int GetHashCode(Person obj)
    {
        return HashCode.Combine(obj.Name, obj.Age);
    }
}
class Program
{
    static void Main()
    {
        List<Person> list1 = new List<Person>
        {`在这里插入代码片`
            new Person { Name = "Alice", Age = 30 },
            new Person { Name = "Bob", Age = 25 }
        };
        List<Person> list2 = new List<Person>
        {
            new Person { Name = "Charlie", Age = 30 },
            new Person { Name = "Bob", Age = 25 }
        };
        // 使用 Union 合并两个列表并去除重复项
        var unionedPeople = list1.Union(list2,new PersonComparer());
        Console.WriteLine(string.Join(",",unionedPeople));
        // 输出:Alice (30),Bob (25),Charlie (30)
    }
}
- 使用UnionBy,通过指定键选择器函数来确定哪些对象应该被视为相同的。并确保合并后的结果中没有重复项。
class Person
{
    public string Name { get; set; }
    public int Age { get; set; }
    public override string ToString()
    {
        return $"{Name} ({Age})";
    }
}
class Program
{
    static void Main()
    {
        List<Person> list1 = new List<Person>
        {
            new Person { Name = "Alice", Age = 30 },
            new Person { Name = "Bob", Age = 25 }
        };
        List<Person> list2 = new List<Person>
        {
            new Person { Name = "Charlie", Age = 30 },
            new Person { Name = "Bob", Age = 25 }
        };
        // 使用 UnionBy 合并两个列表并根据年龄去重
        var unionedPeople = list1.UnionBy(list2, p => p.Age);
        Console.WriteLine(string.Join(",", unionedPeople));
        // 输出:Alice (30),Bob (25)
    }
}
示例 3:处理无限序列
Union /UnionBy方法采用惰性求值(Lazy Evaluation),这意味着它不会立即执行合并操作,而是等到实际遍历时才会计算结果。这使得 Union /UnionBy 可以处理无限序列或延迟执行复杂的查询。
using System;
using System.Collections.Generic;
using System.Linq;
class Program
{
    static void Main()
    {
        IEnumerable<int> infiniteNumbers = Enumerable.Range(0, int.MaxValue).Select(i => i * 2);
        List<int> finiteNumbers = new List<int> { 1, 3, 5 };
        // 使用 Union 合并无限序列和有限序列
        var unionedSequence = infiniteNumbers.Union(finiteNumbers);
		// var unionedSequence = infiniteNumbers.UnionBy(finiteNumbers, x => x);
        
        // 限制输出数量
        Console.WriteLine("Unioned Sequence (First 10 elements):");
        foreach (var item in unionedSequence.Take(10))
        {
            Console.Write(item + " ");
        }
    }
}
输出结果:
Unioned Sequence (First 10 elements):
0 2 4 6 8 10 12 14 16 18
在这个例子中,我们展示了如何使用 Union /UnionBy 方法合并一个无限序列和一个有限序列,并通过 Take 方法限制输出的数量,从而避免无限循环。
示例 4:自定义比较器
当处理复杂的数据结构时,可以通过自定义比较器来实现更灵活的合并逻辑。
class Person
{
    public string Name { get; set; }
    public int Age { get; set; }
    public override bool Equals(object obj)
    {
        if (obj is Person other)
        {
            return Name == other.Name && Age == other.Age;
        }
        return false;
    }
    public override int GetHashCode()
    {
        return HashCode.Combine(Name, Age);
    }
    public override string ToString()
    {
        return $"{Name} ({Age})";
    }
}
class PersonComparerByName : IEqualityComparer<Person>
{
    public bool Equals(Person x, Person y)
    {
        if (x == null || y == null) return false;
        return x.Name == y.Name;
    }
    public int GetHashCode(Person obj)
    {
        return obj.Name?.GetHashCode() ?? 0;
    }
}
class Program
{
    static void Main()
    {
        List<Person> list1 = new List<Person>
        {
            new Person { Name = "Alice", Age = 30 },
            new Person { Name = "Bob", Age = 25 }
        };
        List<Person> list2 = new List<Person>
        {
            new Person { Name = "Alice", Age = 31 },
            new Person { Name = "Charlie", Age = 30 }
        };
        // 使用 Union 合并两个列表并根据名字去重
        var unionedPeople = list1.Union(list2, new PersonComparerByName());
        Console.WriteLine(string.Join(",", unionedPeople));
        // 输出:Alice (30),Bob (25),Charlie (30)
    }
}
在这个例子中,我们展示了如何使用自定义的 PersonComparerByName 来根据名字去重合并 Person 对象。
class Person
{
    public string Name { get; set; }
    public int Age { get; set; }
    public override string ToString()
    {
        return $"{Name} ({Age})";
    }
}
class CustomAgeComparer : IEqualityComparer<int>
{
    public bool Equals(int x, int y)
    {
        // 自定义比较逻辑:年龄相差不超过1岁视为相同
        return Math.Abs(x - y) <= 1;
    }
    public int GetHashCode(int obj)
    {
        return 0;
    }
}
class Program
{
    static void Main()
    {
        List<Person> list1 = new List<Person>
        {
            new Person { Name = "Alice", Age = 30 },
            new Person { Name = "Bob", Age = 25 }
        };
        List<Person> list2 = new List<Person>
        {
            new Person { Name = "Charlie", Age = 30 },
            new Person { Name = "David", Age = 26 }
        };
        // 使用 UnionBy 合并两个列表并根据年龄和自定义比较器去重
        var unionedPeople = list1.UnionBy(list2, p => p.Age, new CustomAgeComparer());
        Console.WriteLine(string.Join(",", unionedPeople));
        // 输出:Alice (30),Bob (25)
    }
}
示例 5:多个集合的合并
当需要合并多个集合时,可以多次使用 Union 方法。
class Program
{
    static void Main()
    {
        List<int> list1 = new List<int> { 1, 2, 3 };
        List<int> list2 = new List<int> { 4, 5, 6 };
        List<int> list3 = new List<int> { 7, 8, 9 };
        // 使用 Union 合并多个集合
        var unionedLists = list1.Union(list2).Union(list3);
		// var unionedLists = list1.UnionBy(list2, x => x).UnionBy(list3, x => x);
		
        Console.WriteLine(string.Join(",", unionedLists));
        // 输出:1,2,3,4,5,6,7,8,9
    }
}
4. 注意事项
尽管 Union/UnionBy 方法非常有用,但在实际应用中也有一些需要注意的地方:
- 空集合处理: 如果源集合为空,Union/UnionBy方法将返回一个空的结果集。因此,在使用Union/UnionBy方法之前,通常不需要检查集合是否为空。
- 数据类型一致:适用于需要合并的集合对象的数据类型是一样的情况。
四、Except 和 ExceptBy :差集
 

1. 什么是 Except/ExceptBy
- Except方法用于计算两个序列的差集,即返回第一个序列中存在但第二个序列中不存在的所有元素。默认情况下,它使用对象的默认比较器(如 EqualityComparer.Default)来比较元素是否相等。你也可以提供自定义的比较器来控制比较逻辑。
- ExceptBy是 .NET 6 及以上版本中引入的一个扩展方法,用于计算两个序列的差集,但与- Except不同的是,它允许你通过指定的键选择器函数来确定元素是否相同。这意味着你可以根据某个特定属性或计算结果来进行差集操作,而不是直接比较整个对象。
2. Except/ExceptBy 方法 基本信息
1) Except/ExceptBy
public static IEnumerable<TSource> Except<TSource>(
    this IEnumerable<TSource> first,
    IEnumerable<TSource> second
)
public static IEnumerable<TSource> Except<TSource>(
    this IEnumerable<TSource> first,
    IEnumerable<TSource> second,
    IEqualityComparer<TSource> comparer
)
- 参数: 
  - first:第一个要计算差集的序列。
- second:第二个要计算差集的序列。
- comparer(可选):一个 IEqualityComparer<TSource>实现,用于比较元素是否相等。
 
- 返回值:Except方法返回一个IEnumerable<TSource>对象,其中TSource是输入序列中元素的类型。
public static IEnumerable<TSource> ExceptBy<TSource, TKey>(
    this IEnumerable<TSource> first,
    IEnumerable<TKey> second,
    Func<TSource, TKey> keySelector
)
public static IEnumerable<TSource> ExceptBy<TSource, TKey>(
    this IEnumerable<TSource> first,
    IEnumerable<TKey> second,
    Func<TSource, TKey> keySelector,
    IEqualityComparer<TKey> comparer
)
- 参数: 
  - first:第一个要计算差集的序列。
- second:第二个要计算差集的序列(注意这里的元素类型是键类型- TKey)。
- keySelector:一个函数,用于从每个元素中提取键值。
- comparer(可选):一个 IEqualityComparer 实现,用于比较键值是否相等。
 
- 返回值:UnionBy方法返回一个IEnumerable<TSource>对象,其中TSource是输入序列中元素的类型。
2)工作原理
- Except:计算两个序列的差集,返回第一个序列中存在但第二个序列中不存在的所有元素,并使用默认或自定义的比较器来判断元素是否相等。
- ExceptBy:根据指定的键选择器函数提取键值,并计算两个序列的差集,返回第一个序列中存在但第二个序列中不存在的所有元素,去除具有相同键值的重复项。
3)使用场景
适用于数据过滤、数据预处理、多个集合的差集计算等场景。
3. 使用示例
示例 1:基本差集计算
假设我们有两个整数列表,我们希望计算第一个列表中存在但第二个列表中不存在的所有元素。
class Program
{
    static void Main()
    {
        List<int> list1 = new List<int> { 1, 2, 3, 4, 5 };
        List<int> list2 = new List<int> { 3, 4, 6 };
        // 使用 Except 计算差集
        var exceptList = list1.Except(list2);
        Console.WriteLine(string.Join(",", exceptList));// 输出:1,2,5
        exceptList = list1.ExceptBy(list2, x => x);
        Console.WriteLine(string.Join(",", exceptList));// 输出:1,2,5
    }
}
示例 2:自定义对象
当处理自定义对象时,Union 方法默认使用对象的 Equals 和 GetHashCode 方法来判断元素是否相同。如果你有更复杂的比较需求,可以提供自定义的 IEqualityComparer<T> 实现。
- 使用默认的 Equals和GetHashCode方法,并不能实现对象内容的比较
class Person
{
    public string Name { get; set; }
    public int Age { get; set; }
    public override string ToString()
    {
        return $"{Name} ({Age})";
    }
}
class Program
{
    static void Main()
    {
        List<Person> list1 = new List<Person>
        {
            new Person { Name = "Alice", Age = 30 },
            new Person { Name = "Bob", Age = 25 },
            new Person { Name = "Charlie", Age = 35 }
        };
        List<Person> list2 = new List<Person>
        {
            new Person { Name = "Bob", Age = 25 },
            new Person { Name = "David", Age = 40 }
        };
        // 使用 Except 计算差集
        var exceptPeople = list1.Except(list2);
        Console.WriteLine(string.Join(",", exceptPeople));
        // 输出:Alice (30),Bob (25),Charlie (35)
    }
}
- 重写 对象的 Equals和GetHashCode方法,可以实现对象内容的比较。使用Except时,默认就会调用重写后的方法判断对象是否相等
class Person
{
    public string Name { get; set; }
    public int Age { get; set; }
    public override bool Equals(object obj)
    {
        if (obj is Person other)
        {
            return Name == other.Name && Age == other.Age;
        }
        return false;
    }
    public override int GetHashCode()
    {
        return HashCode.Combine(Name, Age);
    }
    public override string ToString()
    {
        return $"{Name} ({Age})";
    }
}
class Program
{
    static void Main()
    {
        List<Person> list1 = new List<Person>
        {
            new Person { Name = "Alice", Age = 30 },
            new Person { Name = "Bob", Age = 25 }
        };
        List<Person> list2 = new List<Person>
        {
            new Person { Name = "Charlie", Age = 30 },
            new Person { Name = "Bob", Age = 25 }
        };
        // 使用 Except 计算差集
        var exceptPeople = list1.Except(list2);
        Console.WriteLine(string.Join(",", exceptPeople));
        // 输出:Alice (30)
    }
}
- 使用自定义的 PersonComparer来比较Person对象。
class Person
{
    public string Name { get; set; }
    public int Age { get; set; }
    public override string ToString()
    {
        return $"{Name} ({Age})";
    }
}
class PersonComparer : IEqualityComparer<Person>
{
    public bool Equals(Person x, Person y)
    {
        if (x == null || y == null) return false;
        return x.Name == y.Name && x.Age == y.Age;
    }
    public int GetHashCode(Person obj)
    {
        return HashCode.Combine(obj.Name, obj.Age);
    }
}
class Program
{
    static void Main()
    {
        List<Person> list1 = new List<Person>
        {
            new Person { Name = "Alice", Age = 30 },
            new Person { Name = "Bob", Age = 25 }
        };
        List<Person> list2 = new List<Person>
        {
            new Person { Name = "Charlie", Age = 30 },
            new Person { Name = "Bob", Age = 25 }
        };
        // 使用 Except 计算差集并使用自定义比较器
        var exceptPeople = list1.Except(list2, new PersonComparer());
        Console.WriteLine(string.Join(",", exceptPeople));
        // 输出:Alice (30)
    }
}
- 使用ExceptBy,通过指定键选择器函数来确定哪些对象应该被视为相同的。并确保合并后的结果中没有重复项。
class Person
{
    public string Name { get; set; }
    public int Age { get; set; }
    public override string ToString()
    {
        return $"{Name} ({Age})";
    }
}
class Program
{
    static void Main()
    {
        List<Person> list1 = new List<Person>
        {
            new Person { Name = "Alice", Age = 30 },
            new Person { Name = "Bob", Age = 25 },
            new Person { Name = "Charlie", Age = 35 }
        };
        List<Person> list2 = new List<Person>
        {
            new Person { Name = "Bob", Age = 25 },
            new Person { Name = "David", Age = 40 }
        };
        // 使用 ExceptBy 计算差集并排除特定年龄
        var exceptPeople = list1.ExceptBy(list2.Select(x => x.Age), p => p.Age);
        Console.WriteLine(string.Join(",", exceptPeople));
        // 输出:Alice (30),Charlie (35)
    }
}
示例 3:处理无限序列
Except /ExceptBy方法采用惰性求值(Lazy Evaluation),这意味着它不会立即执行合并操作,而是等到实际遍历时才会计算结果。这使得 Except /ExceptBy 可以处理无限序列或延迟执行复杂的查询。
class Program
{
    static void Main()
    {
        IEnumerable<int> infiniteNumbers = Enumerable.Range(0, int.MaxValue).Select(i => i * 2);
        List<int> finiteNumbers = new List<int> { 1, 3, 5 };
        // 使用 Union 合并无限序列和有限序列
        //var unionedSequence = infiniteNumbers.Except(finiteNumbers);
        var unionedSequence = infiniteNumbers.ExceptBy(finiteNumbers, x => x);
        // 限制输出数量
        Console.WriteLine("Unioned Sequence (First 10 elements):");
        foreach (var item in unionedSequence.Take(10))
        {
            Console.Write(item + " ");
        }
    }
}
输出结果:
Unioned Sequence (First 10 elements):
0 2 4 6 8 10 12 14 16 18
在这个例子中,我们展示了如何使用 Except /ExceptBy 方法合并一个无限序列和一个有限序列,并通过 Take 方法限制输出的数量,从而避免无限循环。
示例 4:自定义比较器
class Person
{
    public string Name { get; set; }
    public int Age { get; set; }
    public override string ToString()
    {
        return $"{Name} ({Age})";
    }
}
class CustomAgeComparer : IEqualityComparer<int>
{
    public bool Equals(int x, int y)
    {
        // 自定义比较逻辑:年龄相差不超过1岁视为相同
        return Math.Abs(x - y) <= 1;
    }
    public int GetHashCode(int obj)
    {
        return 0;
    }
}
class Program
{
    static void Main()
    {
        List<Person> list1 = new List<Person>
        {
            new Person { Name = "Alice", Age = 30 },
            new Person { Name = "Bob", Age = 25 },
            new Person { Name = "Charlie", Age = 35 }
        };
        List<Person> list2 = new List<Person>
        {
            new Person { Name = "Charlie", Age = 30 },
            new Person { Name = "David", Age = 26 }
        };
        // 使用 UnionBy 合并两个列表并根据年龄和自定义比较器去重
        var unionedPeople = list1.ExceptBy(list2.Select(x=>x.Age), p => p.Age, new CustomAgeComparer());
        Console.WriteLine(string.Join(",", unionedPeople));
        // 输出:Charlie (35)
    }
}
示例 5:多个集合的合并
当需要合并多个集合时,可以多次使用 Except/ExceptBy 方法。
class Program
{
    static void Main()
    {
        List<int> list1 = new List<int> { 1, 2, 3, 4, 5 };
        List<int> list2 = new List<int> { 3, 4, 6 };
        List<int> list3 = new List<int> { 4, 7, 8 };
        // 使用 Union 合并多个集合
        //var unionedLists = list1.Except(list2).Except(list3);
         var unionedLists = list1.ExceptBy(list2, x => x).ExceptBy(list3, x => x);
        Console.WriteLine(string.Join(",", unionedLists));
        // 输出:1,2,5
    }
}
4. 注意事项
尽管 Except/ExceptBy  方法非常有用,但在实际应用中也有一些需要注意的地方:
- 空集合处理: 如果源集合为空,Except/ExceptBy方法将返回一个空的结果集。因此,在使用Except/ExceptBy方法之前,通常不需要检查集合是否为空。
- 数据类型一致:适用于需要取差集的集合对象的数据类型是一样的情况。
五、Intersect 和 IntersectBy :交集
 

1. 什么是 Intersect/IntersectBy
- Intersect方法用于计算两个序列的交集,即返回两个序列中共有的所有元素。默认情况下,它使用对象的默认比较器(如- EqualityComparer<T>.Default)来比较元素是否相等。你也可以提供自定义的比较器来控制比较逻辑。
- IntersectBy是 .NET 6 及以上版本中引入的一个扩展方法,用于计算两个序列的交集,但与- Intersect不同的是,它允许你通过指定的键选择器函数来确定元素是否相同。这意味着你可以根据某个特定属性或计算结果来进行交集操作,而不是直接比较整个对象。
2. Intersect/IntersectBy 方法 基本信息
1) Intersect/IntersectBy
public static IEnumerable<TSource> Intersect<TSource>(
    this IEnumerable<TSource> first,
    IEnumerable<TSource> second
)
public static IEnumerable<TSource> Intersect<TSource>(
    this IEnumerable<TSource> first,
    IEnumerable<TSource> second,
    IEqualityComparer<TSource> comparer
)
- 参数: 
  - first:第一个要计算交集的序列。
- second:第二个要计算交集的序列。
- comparer(可选):一个 IEqualityComparer<TSource>实现,用于比较元素是否相等。
 
- 返回值:Except方法返回一个IEnumerable<TSource>对象,其中TSource是输入序列中元素的类型。
public static IEnumerable<TSource> IntersectBy<TSource, TKey>(
    this IEnumerable<TSource> first,
    IEnumerable<TKey> second,
    Func<TSource, TKey> keySelector
)
public static IEnumerable<TSource> IntersectBy<TSource, TKey>(
    this IEnumerable<TSource> first,
    IEnumerable<TKey> second,
    Func<TSource, TKey> keySelector,
    IEqualityComparer<TKey> comparer
)
- 参数: 
  - first:第一个要计算交集的序列。
- second:第二个要计算交集的序列(注意这里的元素类型是键类型- TKey)。
- keySelector:一个函数,用于从每个元素中提取键值。
- comparer(可选):一个 IEqualityComparer 实现,用于比较键值是否相等。
 
- 返回值:UnionBy方法返回一个IEnumerable<TSource>对象,其中TSource是输入序列中元素的类型。
2)工作原理
- Intersect:计算两个序列的交集,返回两个序列中共有的所有元素,并使用默认或自定义的比较器来判断元素是否相等。
- IntersectBy:根据指定的键选择器函数提取键值,并计算两个序列的交集,返回第一个序列中具有与第二个序列中相同键值的所有元素。
3)使用场景
适用于数据过滤、数据预处理、多个集合的交集计算等场景。
3. 使用示例
示例 1:基本交集计算
假设我们有两个整数列表,我们希望找出它们共有的元素。
class Program
{
    static void Main()
    {
        List<int> list1 = new List<int> { 1, 2, 3, 4, 5 };
        List<int> list2 = new List<int> { 3, 4, 6 };
        // 使用 Intersect 计算交集
        var intersectList = list1.Intersect(list2);
        Console.WriteLine(string.Join(",", intersectList));// 输出:3,4
        intersectList = list1.IntersectBy(list2, x => x);
        Console.WriteLine(string.Join(",", intersectList));// 输出:3,4
    }
}
示例 2:自定义对象
当处理自定义对象时,Intersect 方法默认使用对象的 Equals 和 GetHashCode 方法来判断元素是否相同。如果你有更复杂的比较需求,可以提供自定义的 IEqualityComparer<T> 实现。
- 使用默认的 Equals和GetHashCode方法,并不能实现对象内容的比较
class Person
{
    public string Name { get; set; }
    public int Age { get; set; }
    public override string ToString()
    {
        return $"{Name} ({Age})";
    }
}
class Program
{
    static void Main()
    {
        List<Person> list1 = new List<Person>
        {
            new Person { Name = "Alice", Age = 30 },
            new Person { Name = "Bob", Age = 25 },
            new Person { Name = "Charlie", Age = 35 }
        };
        List<Person> list2 = new List<Person>
        {
            new Person { Name = "Bob", Age = 25 },
            new Person { Name = "David", Age = 40 }
        };
        // 使用 Intersect 计算交集
        var intersectPeople = list1.Intersect(list2);
        Console.WriteLine(string.Join(",", intersectPeople));
        // 输出:          - 表示 没有交集
    }
}
- 重写 对象的 Equals和GetHashCode方法,可以实现对象内容的比较。使用Except时,默认就会调用重写后的方法判断对象是否相等
class Person
{
    public string Name { get; set; }
    public int Age { get; set; }
    public override bool Equals(object obj)
    {
        if (obj is Person other)
        {
            return Name == other.Name && Age == other.Age;
        }
        return false;
    }
    public override int GetHashCode()
    {
        return HashCode.Combine(Name, Age);
    }
    public override string ToString()
    {
        return $"{Name} ({Age})";
    }
}
class Program
{
    static void Main()
    {
        List<Person> list1 = new List<Person>
        {
            new Person { Name = "Alice", Age = 30 },
            new Person { Name = "Bob", Age = 25 }
        };
        List<Person> list2 = new List<Person>
        {
            new Person { Name = "Charlie", Age = 30 },
            new Person { Name = "Bob", Age = 25 }
        };
        // 使用 Intersect 计算交集
        var intersectPeople = list1.Intersect(list2);
        Console.WriteLine(string.Join(",", intersectPeople));
        // 输出:Bob (25)
    }
}
- 使用自定义的 PersonComparer来比较Person对象。
class Person
{
    public string Name { get; set; }
    public int Age { get; set; }
    public override string ToString()
    {
        return $"{Name} ({Age})";
    }
}
class PersonComparer : IEqualityComparer<Person>
{
    public bool Equals(Person x, Person y)
    {
        if (x == null || y == null) return false;
        return x.Name == y.Name && x.Age == y.Age;
    }
    public int GetHashCode(Person obj)
    {
        return HashCode.Combine(obj.Name, obj.Age);
    }
}
class Program
{
    static void Main()
    {
        List<Person> list1 = new List<Person>
        {
            new Person { Name = "Alice", Age = 30 },
            new Person { Name = "Bob", Age = 25 }
        };
        List<Person> list2 = new List<Person>
        {
            new Person { Name = "Charlie", Age = 30 },
            new Person { Name = "Bob", Age = 25 }
        };
        // 使用 Intersect 计算交集并使用自定义比较器
        var intersectPeople = list1.Intersect(list2, new PersonComparer());
        Console.WriteLine(string.Join(",", intersectPeople));
        // 输出:Bob (25)
    }
}
- 使用ExceptBy,通过指定键选择器函数来确定哪些对象应该被视为相同的。并确保合并后的结果中没有重复项。
class Person
{
    public string Name { get; set; }
    public int Age { get; set; }
    public override string ToString()
    {
        return $"{Name} ({Age})";
    }
}
class Program
{
    static void Main()
    {
        List<Person> list1 = new List<Person>
        {
            new Person { Name = "Alice", Age = 30 },
            new Person { Name = "Bob", Age = 25 },
            new Person { Name = "Charlie", Age = 35 }
        };
        List<Person> list2 = new List<Person>
        {
            new Person { Name = "Bob", Age = 25 },
            new Person { Name = "David", Age = 40 }
        };
        // 使用 IntersectBy 计算交集
        var intersectPeople = list1.IntersectBy(list2.Select(x => x.Age), p => p.Age);
        Console.WriteLine(string.Join(",", intersectPeople));
        // 输出:Bob (25)
    }
}
示例 3:处理无限序列
Intersect /IntersectBy方法采用惰性求值(Lazy Evaluation),这意味着它不会立即执行合并操作,而是等到实际遍历时才会计算结果。这使得 Intersect /IntersectBy 可以处理无限序列或延迟执行复杂的查询。
class Program
{
    static void Main()
    {
        IEnumerable<int> infiniteNumbers = Enumerable.Range(0, int.MaxValue).Select(i => i * 2);
        List<int> finiteNumbers = new List<int> { 4, 6, 8 };
        // 使用 Intersect 计算交集
        var intersectSequence = infiniteNumbers.Intersect(finiteNumbers);
        // 限制输出数量
        Console.WriteLine("Elements common between infiniteNumbers and finiteNumbers (First 3 elements):");
        foreach (var item in intersectSequence.Take(3))
        {
            Console.Write(item + " ");
        }
    }
}
输出结果:
Elements common between infiniteNumbers and finiteNumbers (First 3 elements):
4 6 8
在这个例子中,我们展示了如何使用 Intersect 方法计算一个无限序列和一个有限序列之间的交集,并通过 Take 方法限制输出的数量,从而避免无限循环。
示例 4:自定义比较器
class Person
{
    public string Name { get; set; }
    public int Age { get; set; }
    public override string ToString()
    {
        return $"{Name} ({Age})";
    }
}
class CustomAgeComparer : IEqualityComparer<int>
{
    public bool Equals(int x, int y)
    {
        // 自定义比较逻辑:年龄相差不超过1岁视为相同
        return Math.Abs(x - y) <= 1;
    }
    public int GetHashCode(int obj)
    {
        return 0;
    }
}
class Program
{
    static void Main()
    {
        List<Person> list1 = new List<Person>
        {
            new Person { Name = "Alice", Age = 30 },
            new Person { Name = "Bob", Age = 25 },
            new Person { Name = "Charlie", Age = 35 }
        };
        List<Person> list2 = new List<Person>
        {
            new Person { Name = "Charlie", Age = 30 },
            new Person { Name = "David", Age = 26 }
        };
        // 使用 IntersectBy 取两个列表交集并根据年龄和自定义比较器 比较Person对象
        var intersectPeople = list1.IntersectBy(list2.Select(x => x.Age), p => p.Age, new CustomAgeComparer());
        Console.WriteLine(string.Join(",", intersectPeople));
        // 输出:Alice (30),Bob (25)
    }
}
示例 5:多个集合的合并
当需要合并多个集合时,可以多次使用 Intersect/IntersectBy 方法。
class Program
{
    static void Main()
    {
        List<int> list1 = new List<int> { 1, 2, 3, 4, 5 };
        List<int> list2 = new List<int> { 3, 4, 6 };
        List<int> list3 = new List<int> { 4, 7, 8 };
        // 使用 Union 合并多个集合
        //var unionedLists = list1.Intersect(list2).Intersect(list3);
         var unionedLists = list1.IntersectBy(list2, x => x).IntersectBy(list3, x => x);
        Console.WriteLine(string.Join(",", unionedLists));
        // 输出:4
    }
}
4. 注意事项
尽管 Intersect/IntersectBy方法非常有用,但在实际应用中也有一些需要注意的地方:
- 空集合处理: 如果源集合为空, Intersect/IntersectBy方法将返回一个空的结果集。因此,在使用Intersect/IntersectBy方法之前,通常不需要检查集合是否为空。
- 数据类型一致:适用于需要取差集的集合对象的数据类型是一样的情况。
结语
回到目录页:C#/.NET 知识汇总
 希望以上内容可以帮助到大家,如文中有不对之处,还请批评指正。
参考资料:
 微软官方文档 Enumerable
