在日常开发中,我们经常需要处理集合中的重复数据。本文将全面介绍C#中List去重的各种优雅方式,帮助你在不同场景下选择最合适的方案。

前言

数据去重是数据处理中的常见需求,主要目的包括:

· 🚫 避免数据重复导致的统计错误
· 💾 减少内存占用和提高性能
· ✅ 保证数据的唯一性约束
· 📈 提升数据质量和用户体验

本文将带你全面掌握C#中List去重的各种技巧,从基础用法到高级方案,应有尽有!

  1. 基础值类型去重

使用 LINQ 的 Distinct() 方法

// 整数去重
List<int> numbers = new List<int> { 1, 2, 2, 3, 4, 4, 5 };
List<int> distinctNumbers = numbers.Distinct().ToList();
Console.WriteLine(string.Join(", ", distinctNumbers));
// 输出: 1, 2, 3, 4, 5

// 字符串去重
List<string> names = new List<string> { "Alice", "Bob", "Alice", "Charlie" };
List<string> distinctNames = names.Distinct().ToList();
Console.WriteLine(string.Join(", ", distinctNames));
// 输出: Alice, Bob, Charlie
  1. 自定义对象去重

首先定义示例类

public class Person
{
    public int Id { get; set; }
    public string Name { get; set; }
    public int Age { get; set; }
    
    public override string ToString() => $"Id: {Id}, Name: {Name}, Age: {Age}";
}

// 准备测试数据
List<Person> people = new List<Person>
{
    new Person { Id = 1, Name = "Alice", Age = 25 },
    new Person { Id = 1, Name = "Alice", Age = 26 }, // 重复ID
    new Person { Id = 2, Name = "Bob", Age = 30 },
    new Person { Id = 3, Name = "Charlie", Age = 35 }
};

方法一:重写 Equals 和 GetHashCode

public class Person
{
    public int Id { get; set; }
    public string Name { get; set; }
    public int Age { get; set; }
    
    // 基于Id去重
    public override bool Equals(object obj)
    {
        return obj is Person person && Id == person.Id;
    }
    
    public override int GetHashCode()
    {
        return Id.GetHashCode();
    }
}

// 使用
List<Person> distinctPeople = people.Distinct().ToList();
foreach (var person in distinctPeople)
{
    Console.WriteLine(person);
}

方法二:使用自定义比较器(推荐)

public class PersonComparer : IEqualityComparer<Person>
{
    private readonly bool _compareName;
    
    public PersonComparer(bool compareName = false)
    {
        _compareName = compareName;
    }
    
    public bool Equals(Person x, Person y)
    {
        if (ReferenceEquals(x, y)) return true;
        if (x is null || y is null) return false;
        
        if (_compareName)
            return x.Id == y.Id && x.Name == y.Name;
        else
            return x.Id == y.Id;
    }
    
    public int GetHashCode(Person obj)
    {
        if (_compareName)
            return HashCode.Combine(obj.Id, obj.Name);
        else
            return obj.Id.GetHashCode();
    }
}

// 使用自定义比较器
List<Person> distinctById = people.Distinct(new PersonComparer()).ToList();
List<Person> distinctByIdAndName = people.Distinct(new PersonComparer(true)).ToList();

Console.WriteLine("基于ID去重:");
foreach (var person in distinctById)
{
    Console.WriteLine($"  {person}");
}

Console.WriteLine("基于ID和Name去重:");
foreach (var person in distinctByIdAndName)
{
    Console.WriteLine($"  {person}");
}
  1. 高性能去重方案

使用 HashSet(推荐用于大数据量)

// 基本值类型 - 最高效
List<int> numbers = new List<int> { 1, 2, 2, 3, 4, 4, 5 };
HashSet<int> uniqueNumbers = new HashSet<int>(numbers);
List<int> distinctNumbers = uniqueNumbers.ToList();

// 自定义对象
HashSet<Person> uniquePeople = new HashSet<Person>(people, new PersonComparer());
List<Person> distinctPeople = uniquePeople.ToList();

// 内联比较器(无需定义类,简洁用法)
var distinctPeopleInline = new HashSet<Person>(people, 
    EqualityComparer<Person>.Create((x, y) => x?.Id == y?.Id, 
        x => x?.Id.GetHashCode() ?? 0))
    .ToList();

Console.WriteLine($"原始数据: {people.Count} 条");
Console.WriteLine($"去重后: {distinctPeople.Count} 条");
  1. .NET 6+ 的新特性

使用 DistinctBy 方法(最优雅的方式)

// 按单个属性去重
List<Person> distinctById = people.DistinctBy(p => p.Id).ToList();

// 按多个属性去重
List<Person> distinctByIdAndName = people
    .DistinctBy(p => new { p.Id, p.Name })
    .ToList();

// 使用元组(性能更好)
List<Person> distinctByTuple = people
    .DistinctBy(p => (p.Id, p.Name))
    .ToList();

// 复杂条件去重
List<Person> distinctByCondition = people
    .DistinctBy(p => new { 
        Category = p.Age < 30 ? "Young" : "Adult", 
        p.Name 
    })
    .ToList();

Console.WriteLine("使用DistinctBy按ID去重:");
foreach (var person in distinctById)
{
    Console.WriteLine($"  {person}");
}
  1. 使用 GroupBy 方法
// 按ID分组取第一个
List<Person> distinctPeople = people
    .GroupBy(p => p.Id)
    .Select(g => g.First())
    .ToList();

// 按多个属性分组
List<Person> distinctByMultiple = people
    .GroupBy(p => new { p.Id, p.Name })
    .Select(g => g.First())
    .ToList();

// 取最后一个或特定元素
List<Person> distinctLast = people
    .GroupBy(p => p.Id)
    .Select(g => g.Last())
    .ToList();

// 按条件选择(如年龄最大的)
List<Person> distinctOldest = people
    .GroupBy(p => p.Id)
    .Select(g => g.OrderByDescending(p => p.Age).First())
    .ToList();

Console.WriteLine("取每个ID中年龄最大的:");
foreach (var person in distinctOldest)
{
    Console.WriteLine($"  {person}");
}
  1. 优雅的扩展方法封装

创建扩展方法类

public static class ListExtensions
{
    /// <summary>
    /// 根据指定键选择器去重
    /// </summary>
    public static List<T> DistinctBy<T, TKey>(this IEnumerable<T> source, Func<T, TKey> keySelector)
    {
        return source.DistinctBy(keySelector, null);
    }
    
    /// <summary>
    /// 根据指定键选择器和比较器去重
    /// </summary>
    public static List<T> DistinctBy<T, TKey>(this IEnumerable<T> source, Func<T, TKey> keySelector, IEqualityComparer<TKey> comparer)
    {
        ArgumentNullException.ThrowIfNull(source);
        ArgumentNullException.ThrowIfNull(keySelector);
        
        var seen = new HashSet<TKey>(comparer);
        return source.Where(item => seen.Add(keySelector(item))).ToList();
    }
    
    /// <summary>
    /// 高性能去重(使用HashSet)
    /// </summary>
    public static List<T> FastDistinct<T>(this IEnumerable<T> source)
    {
        return new HashSet<T>(source).ToList();
    }
    
    /// <summary>
    /// 高性能去重(使用HashSet和比较器)
    /// </summary>
    public static List<T> FastDistinct<T>(this IEnumerable<T> source, IEqualityComparer<T> comparer)
    {
        return new HashSet<T>(source, comparer).ToList();
    }
}

使用扩展方法

// 使用扩展方法
var distinctById = people.DistinctBy(p => p.Id);
var distinctByName = people.DistinctBy(p => p.Name);
var distinctByMultiple = people.DistinctBy(p => new { p.Id, p.Name });

// 高性能去重
var fastDistinct = people.FastDistinct();
var fastDistinctWithComparer = people.FastDistinct(new PersonComparer());

Console.WriteLine("使用扩展方法去重:");
Console.WriteLine($"按ID去重: {distinctById.Count} 条记录");
Console.WriteLine($"高性能去重: {fastDistinct.Count} 条记录");
  1. 性能比较和选择指南

为了帮助大家选择最佳方案,我整理了以下性能对比表格:

方法 时间复杂度 空间复杂度 适用场景 代码简洁性 推荐指数
HashSet O(n) O(n) 大数据量,性能要求高 ★★★☆☆ ★★★★★
Distinct() O(n) O(n) 一般场景 ★★★★★ ★★★★☆
DistinctBy() O(n) O(n) .NET 6+,按属性去重 ★★★★★ ★★★★★
GroupBy O(n) O(n) 需要分组逻辑时 ★★★☆☆ ★★★☆☆
扩展方法 O(n) O(n) 代码复用和封装 ★★★★☆ ★★★★☆

简单性能测试示例

public class PerformanceTest
{
    public static void TestDistinctMethods()
    {
        // 生成测试数据
        var largeList = Enumerable.Range(0, 100000)
            .Select(i => new Person { Id = i % 1000, Name = $"Person{i}" })
            .ToList();
        
        var stopwatch = Stopwatch.StartNew();
        
        // 测试不同方法
        var result1 = largeList.Distinct().ToList();
        Console.WriteLine($"Distinct(): {stopwatch.ElapsedMilliseconds}ms");
        stopwatch.Restart();
        
        var result2 = largeList.DistinctBy(p => p.Id).ToList();
        Console.WriteLine($"DistinctBy(): {stopwatch.ElapsedMilliseconds}ms");
        stopwatch.Restart();
        
        var result3 = new HashSet<Person>(largeList).ToList();
        Console.WriteLine($"HashSet: {stopwatch.ElapsedMilliseconds}ms");
        stopwatch.Restart();
        
        var result4 = largeList.GroupBy(p => p.Id).Select(g => g.First()).ToList();
        Console.WriteLine($"GroupBy: {stopwatch.ElapsedMilliseconds}ms");
    }
}
  1. 实际应用场景

场景一:数据库查询结果去重

// 模拟数据库查询结果
var databaseResults = new List<Person>
{
    new Person { Id = 1, Name = "Alice", Age = 25 },
    new Person { Id = 1, Name = "Alice", Age = 25 }, // 重复记录
    new Person { Id = 2, Name = "Bob", Age = 30 },
    new Person { Id = 3, Name = "Charlie", Age = 35 }
};

// 去重处理
var cleanResults = databaseResults
    .DistinctBy(p => p.Id)
    .ToList();

Console.WriteLine($"数据库查询去重: {databaseResults.Count}{cleanResults.Count} 条记录");

场景二:API响应数据合并去重

public class Product
{
    public int Id { get; set; }
    public string Name { get; set; }
    public decimal Price { get; set; }
}

public class ApiService
{
    public List<Product> MergeProducts(List<Product> localData, List<Product> apiData)
    {
        // 合并数据并去重
        var mergedData = localData
            .Concat(apiData)
            .DistinctBy(p => p.Id)
            .ToList();
            
        Console.WriteLine($"数据合并: {localData.Count} + {apiData.Count}{mergedData.Count} 条记录");
        return mergedData;
    }
}

场景三:复杂业务逻辑去重

public class Order
{
    public int CustomerId { get; set; }
    public int ProductId { get; set; }
    public DateTime OrderDate { get; set; }
    public decimal TotalAmount { get; set; }
}

public class OrderService
{
    public List<Order> RemoveDuplicateOrders(List<Order> orders)
    {
        // 基于多个业务规则去重:同一客户、同一产品、同一天只保留金额最大的订单
        return orders
            .GroupBy(o => new { o.CustomerId, o.ProductId, o.OrderDate.Date })
            .Select(g => g.OrderByDescending(o => o.TotalAmount).First())
            .ToList();
    }
}
  1. 最佳实践和建议

🎯 选择策略

  1. 根据数据量选择方法:
    · 小数据量(< 1000):使用 Distinct() 或 DistinctBy()
    · 大数据量(≥ 1000):使用 HashSet
  2. 考虑比较逻辑复杂度:
    · 简单比较:使用 Lambda 表达式
    · 复杂比较:使用自定义比较器
  3. 空值安全处理:
public class SafePersonComparer : IEqualityComparer<Person>
{
    public bool Equals(Person x, Person y)
    {
        if (x is null && y is null) return true;
        if (x is null || y is null) return false;
        return x.Id == y.Id && x.Name == y.Name;
    }
    
    public int GetHashCode(Person obj)
    {
        return obj?.Id.GetHashCode() ?? 0;
    }
}
  1. 线程安全考虑:
    · 单线程:使用普通方法
    · 多线程:考虑使用 ConcurrentDictionary 或适当的同步机制

总结

通过本文的学习,相信你已经掌握了C#中List去重的各种优雅方式。下面是我的终极选择建议:

· ✅ .NET 6+ 项目:优先使用 DistinctBy(),代码最简洁
· ✅ 高性能需求:使用 HashSet 构造器
· ✅ 复杂比较逻辑:使用自定义 IEqualityComparer
· ✅ 代码复用:封装为扩展方法
· ✅ 维护性:为自定义比较器添加单元测试

互动讨论

你在项目中遇到过哪些有趣的数据去重场景?欢迎在评论区分享你的经验和问题!


友情提示:

· 本文所有代码均在 .NET 6+ 环境下测试通过
· 实际使用时请根据具体业务需求调整比较逻辑
· 记得为自定义比较器编写单元测试

希望这篇文章能帮助你在实际开发中更加得心应手地处理数据去重问题!

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐