使 HashSet<string> 不区分大小写

114

我有一个方法,其中包含一个HashSet参数。现在我需要在其中执行不区分大小写的Contains操作:

public void DoSomething(HashSet<string> set, string item)
{
    var x = set.Contains(item);
    ... 
}

有没有一种方法可以使现有的 HashSet 不区分大小写(不创建新的 HashSet)?

我正在寻找性能最佳的解决方案。

编辑

由于 Contains 方法可能会被调用多次,所以无法使用 IEnumerable 扩展方法,因为它们的性能比本机 HashSet 的 Contains 方法差。

解决方案

既然我的问题的答案是 NO,即不可能,我已经创建并使用了以下方法:

public HashSet<string> EnsureCaseInsensitive(HashSet<string> set)
{
    return set.Comparer == StringComparer.OrdinalIgnoreCase
           ? set
           : new HashSet<string>(set, StringComparer.OrdinalIgnoreCase);
}

7
你可能需要创建一个新的... - It'sNotALie.
可能是重复问题:https://dev59.com/63E85IYBdhLWcg3wqlaE(请参考user414076的答案) - Esoteric Screen Name
1
你需要在一开始就决定HashSet是否考虑大小写,通过提供一个比较器来实现。然而,值得注意的是,使用不区分大小写的比较器时,集合{"A","a"}只会包含一个元素。 - spender
7个回答

186

HashSet<T>构造函数有一个重载,允许您传入自定义的IEqualityComparer<string>。在静态的StringComparer类中已经为您定义了一些忽略大小写的比较器。

例如:

var set = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
set.Add("john");
Debug.Assert(set.Contains("JohN"));
您需要在构建 HashSet<T> 时进行此更改。一旦存在一个,就无法更改它所使用的 IEqualityComparer<T>

只要您知道,默认情况下(如果在 HashSet<T> 构造函数中没有传递任何 IEqualityComparer<T>),它将使用 EqualityComparer<T>.Default

编辑

我发布答案后,问题似乎已经更改了。 如果您必须在现有的区分大小写的 HashSet<string> 中进行不区分大小写的搜索,则必须执行线性搜索:

set.Any(s => string.Equals(s, item, StringComparison.OrdinalIgnoreCase));

这是无法避免的。


如果您只是进行单个查找,则这比仅在哈希集合上循环更糟糕。 - Dave Bish
@DaveBish 我相信 OP 在我回答之后更改了他的问题,以说“不要创建新的”...(发布后很快进行的编辑实际上不算作编辑)。- 如果 OP 必须使用 现有的 HashSet<T> 进行此操作,则当然必须进行线性时间搜索。 - Timothy Shields
1
这不是我想说的。如果他只对哈希集执行一次查找 - 创建一个新的比线性扫描更昂贵。(Op没有指定) - Dave Bish
3
这就是我编辑我的回答并加入了LINQ线性扫描的原因。 :) - Timothy Shields
2
这里有一个替代方案,但我更喜欢一个更清晰的LINQ解决方案。你可以像这样使用Enumerable.Contains<TSource>(this IEnumerable<TSource> source, TSource value, IEqualityComparer<TSource> comparer)set.Contains(item, StringComparison.OrdinalIgnoreCase)。它将执行通常相同的线性搜索,尽管Resharper会生成一个“可能意外的线性搜索集合”警告。 - Corio
出于好奇,使用new HashSet<string>(StringComparer.OrdinalIgnoreCase)是否可能导致大小写不同的两个项存在于集合中?比较是仅在跳转到散列指向的桶之后才执行的。因此, 'a'和'A'是否有可能指向不同的桶,因此无法发现已经存在另一个相同的项,然后将其添加到集合中? - Adrian

11

如果你无法依赖于不区分大小写的 HashSet 来实现不区分大小写的功能,那么你需要在函数内部重新创建一个。

最简洁的代码 - 使用现有集合的 构造函数

var insensitive = new HashSet<string>(
   set, StringComparer.InvariantCultureIgnoreCase);

请注意,复制 HashSet 的成本与遍历所有项一样昂贵,因此如果您的函数只执行一次搜索,则通过遍历所有项会更便宜(O(n))。 如果您的函数多次调用以进行单个不区分大小写的搜索,则应尝试将适当的 HashSet 传递给它。


4
假设您有这个扩展方法:
public static HashSet<T> ToHashSet<T>(this IEnumerable<T> source)
{
    return new HashSet<T>(source);
}

您可以直接使用以下代码:
set = set.Select(n => n.ToLowerInvariant()).ToHashSet();

或者,您可以只执行以下操作:
set = new HashSet(set, StringComparer.OrdinalIgnoreCase); 
//or InvariantCultureIgnoreCase or CurrentCultureIgnoreCase

1
如果您只是进行单个查找,则这比仅在哈希集合上循环更糟糕。 - Dave Bish
它会占用大量内存并进行大量哈希计算,然后在一次查找后将所有工作都丢弃。循环遍历整个哈希集并进行不区分大小写的比较可以在恒定的内存中运行,并且不必计算哈希值。无论如何,两者都需要触及set的全部内容。 - user395760
因为创建一个新的哈希集至少需要遍历整个内容! - Dave Bish
@DaveBish 最受欢迎的答案也恰好做到了这一点... 它也需要重建它... - It'sNotALie.
我也在那个帖子上发表了评论 :) - Dave Bish

4
HashSet旨在根据其哈希函数和相等比较器快速查找元素。您所要求的实际上是查找与“某些其他”条件匹配的元素。想象一下,您有一个Set<Person>对象,仅使用Person.Name进行比较,并且您需要找到具有某个给定值的Person.Age的元素。
关键是您需要迭代集合的内容以查找匹配的元素。如果您经常这样做,您可能会创建一个不同的Set,在您的情况下使用不区分大小写的比较器,但是然后您必须确保此影子集与原始集保持同步。
到目前为止,答案本质上是上述变体,我想添加这一点以澄清基本问题。

2
HashSet的构造函数可以接受替代的IEqualityComparer,以覆盖如何确定相等性。这里可以查看构造函数的列表(链接)StringComparer类包含一堆字符串的静态IEqualityComparers实例。特别是,您可能会对StringComparer.OrdinalIgnoreCase感兴趣。这里是StringComparer的文档(链接)
请注意,另一个构造函数接受IEnumerable,因此您可以使用IEqualityComparer从旧的HashSet构造新的HashSet
因此,总体而言,您需要按以下方式转换HashSet
var myNewHashSet = new HashSet(myOldHashSet, StringComparer.OrdinalIgnoreCase);

-1

如果您想保留原始的大小写敏感版本,您可以使用 LINQ 进行不区分大小写的查询:

var contains = set.Any(a => a.Equals(item, StringComparison.InvariantCultureIgnoreCase));

-2

现在您可以使用

set.Contains(item, StringComparer.OrdinalIgnoreCase);

无需重新创建您的 HashSet


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接