CodeGym /课程 /C# SELF /集合: HashSet<T>

集合: HashSet<T>

C# SELF
第 27 级, 课程 4
可用

1. 入门

想象一下:你在开发一个网店系统,需要保存所有卖出去过的商品唯一编码的列表。或者更酷点,你写一个社交网络App,需要快速检查用户名是不是已经存在。再比如,你想统计一大段文本里有多少个不同的单词?

这些场景下我们都需要一个集合,每个元素只能出现一次。这时候HashSet<T>就闪亮登场了。

HashSet<T>就是一个存储无序唯一元素的集合。重点就是唯一。如果你试图加一个已经存在的元素到HashSet里,它会直接无视你的操作,不会加重复的。就像“只收独一无二的人”的俱乐部:你已经进来了,第二次就不让进了。

HashSet<T>的主要特点:

  • 唯一性:保证集合里每个元素只出现一次。
  • 性能:查找、添加和删除元素都超快。平均这些操作都是常数时间(O(1)),不管元素多少!这多亏了叫做哈希的机制。
  • 无序:和List<T>不一样,HashSet<T>里的元素没有顺序。你不能通过索引拿到某个元素(比如“第五个元素”)。
  • 基于哈希表:内部HashSet<T>用哈希表存元素,这就是它快的原因。我们现在不深挖哈希表怎么工作(那是进阶课题),你可以想象每个元素会“变成”一个特殊的数字码(哈希),以后就能超快找到它。

如果你用List<T>来存唯一元素,每次都得遍历整个列表,确认没有才加进去。大列表的话会超级慢。HashSet<T>直接秒加!

2. 程序员为啥要用HashSet<T>?

唯一集合的秘密

编程里经常有这种需求:要存不重复的元素。比如你解析App用户的email地址列表,想确保没有重复。或者收集文件夹里所有唯一的文件名。最简单的办法——用一个不能加重复元素的集合。

当然,你也可以用List<T>,手动检查元素是否存在再加:

var users = new List<string>();
if (!users.Contains("alice@example.com"))
    users.Add("alice@example.com");

但这个方法在数据量大时很拉胯——Contains在List里要遍历所有元素,用户一多,程序就慢得像老掉牙的Windows XP电脑。

HashSet<T>干了啥

HashSet<T>保证每个元素只存一次。它底层用哈希表(和字典类似),所以添加、查找、删除都超快——基本都是常数时间,不用遍历所有元素。

3. HashSet<T>的基本用法

声明和创建

要用它,不用引入额外库——这个类就在System.Collections.Generic命名空间里。

using System.Collections.Generic;

var emails = new HashSet<string>();

你也可以直接在构造函数里传初始值:

var fruits = new HashSet<string> { "苹果", "香蕉", "梨", "香蕉" };
// "香蕉"出现两次,但只会存一次!

添加元素

用Add方法加元素。如果之前没有这个元素,方法返回true。如果已经有了——啥都不干,返回false。

bool added = emails.Add("alice@example.com"); // true,元素加进去了
added = emails.Add("alice@example.com");      // false,已经有了,没加

有趣:你可以用同一个值疯狂调用Add一百遍——HashSet不会生气,只会礼貌地无视重复。

检查是否存在:Contains

要查元素在不在,用Contains方法:

if (emails.Contains("alice@example.com"))
    Console.WriteLine("这个email已经有了!");

删除元素

删除也很快:

emails.Remove("alice@example.com");

如果没有这个元素——没事,方法直接返回false。

4. 实战例子

我们来升级一下我们在课程里一直在写的学生CRM。

需求

假设系统要求每个用户必须有唯一的用户名(登录名)。加新用户前要检查唯一性,如果重复就要提示。

代码例子

using System;
using System.Collections.Generic;

class Program
{
    static void Main()
    {
        // 用来存唯一用户名的集合
        var userNames = new HashSet<string>();

        while (true)
        {
            Console.Write("输入用户名(或者输入leave退出):");
            string name = Console.ReadLine();

            if (name == "leave")
                break;

            if (userNames.Add(name))
            {
                Console.WriteLine("用户名添加成功!");
            }
            else
            {
                Console.WriteLine("错误:这个名字已经被占用,换一个吧。");
            }
        }

        Console.WriteLine("用户列表:");
        foreach (var user in userNames)
            Console.WriteLine($"- {user}");

        // 注意!输出顺序可能是乱的。
    }
}

就这么简单,我们就保证了唯一性。不用手动检查——HashSet全帮你搞定。

5. HashSet<T>内部怎么工作的?哈希码有啥用

类比:存储格子

想象你有一大堆用户名卡片,还有一张编号0到1000的桌子。每个用户名你都放到一个格子里,格子的编号是一个函数(GetHashCode)算出来的。如果卡片重复——它们会在同一个格子里,你就能很快知道用户名已经存在。

GetHashCode函数

HashSet<T>比较元素时,不是直接比值,而是先用GetHashCode()方法算出哈希码。大多数内置类型(int、string、double等)都已经实现得很高效了。

有趣的知识:如果你要存自定义类对象到HashSet<T>,一定要给这些类实现正确的相等比较和唯一码(Equals和GetHashCode方法),这样唯一性才靠谱。这个我们后面课会详细讲。

HashSet<T>常见新手错误

刚学用唯一集合时,大家常掉进的坑:以为HashSet<T>会按添加顺序存元素。其实不是!哈希集合不保证顺序,顺序可能是随机的。如果你在意顺序——得用别的集合,比如SortedSet<T>,但那是另一个故事了。

第二个常见错误——想用索引访问:

string name = userNames[0]; // 错误!HashSet<T>没有索引。

和数组或列表不一样,这里不能用编号取元素。只能用foreach遍历。

第三个常见混淆是序列化或保存哈希集合到文件时——因为顺序不确定,每次程序运行后元素顺序可能都不一样。

6. 集合操作:并集、交集、差集

HashSet<T>有一堆方法,让你像做数学集合那样操作它。比如:并集、交集、差集和对称差。

主要方法如下:

方法 作用
UnionWith(other)
把other里的所有元素加到哈希集合里。
IntersectWith(other)
只保留同时在本集合和other里的元素。
ExceptWith(other)
把other里的元素从当前集合里删掉。
SymmetricExceptWith(other)
只保留只在本集合或other里、但不同时在两边的元素。

例子:交集和并集

举个例子。有两个名字集合:

var groupA = new HashSet<string> { "安雅", "鲍里斯", "薇拉" };
var groupB = new HashSet<string> { "薇拉", "格列布", "达莎" };

// 找出两个组都有谁
var common = new HashSet<string>(groupA); // 复制内容,否则groupA会变!
common.IntersectWith(groupB);

Console.WriteLine("两个组都有:");
foreach (var name in common)
    Console.WriteLine(name); // 输出"薇拉"

// 合并两个组所有学生,谁都不丢:
var all = new HashSet<string>(groupA);
all.UnionWith(groupB);

Console.WriteLine("所有学生:");
foreach (var name in all)
    Console.WriteLine(name); // "安雅", "鲍里斯", "薇拉", "格列布", "达莎"

7. 其他方法和属性

Count——看看集合里有多少元素:

Console.WriteLine(userNames.Count);

Clear——全删掉(就像现实里的CTRL+A, DELETE):

userNames.Clear();

SetEquals、IsSubsetOf、IsSupersetOf——检查集合是否相等、是否是子集、是否是超集等等。如果你玩(或者写)“数学家谁更牛”这种游戏很有用。

if (groupA.IsSubsetOf(groupB))
    Console.WriteLine("A组所有人都在B组里");

8. 在HashSet<T>里存自定义对象

前面说过,标准类型已经会算哈希和判断相等了。

但如果你想存比如用户对象,就得按某个属性(比如登录名)来判断相等:

class User
{
    public string Login { get; set; }

    public override bool Equals(object obj)
    {
        if (obj is User other)
            return Login == other.Login;
        return false;
    }

    public override int GetHashCode()
    {
        return Login.GetHashCode();
    }
}

// 现在可以这样:
var users = new HashSet<User>();
users.Add(new User { Login = "alice" });
users.Add(new User { Login = "bob" });
users.Add(new User { Login = "alice" }); // 不会加进去!

如果不重写Equals和GetHashCode方法,HashSet会把所有实例都当成不同的(即使登录名一样),因为默认是比内存地址。

2
任务
C# SELF, 第 27 级, 课程 4
已锁定
检查字符串唯一性
检查字符串唯一性
1
调查/小测验
主要集合概览第 27 级,课程 4
不可用
主要集合概览
集合类型和generics
评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION