使 HashSet < string > 不区分大小写

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

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

是否有办法使现有的 HashSet 不区分大小写(不要创建新的) ?

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

剪辑

包含可以多次调用。因此,IEnumable 扩展对我来说是不可接受的,因为它的性能低于原生 HashSet Containsmethod。

解决方案

因为,我的问题的答案是否定的,这是不可能的,所以我创建并使用了以下方法:

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

Assuming you've got this extension method:

public static HashSet<T> ToHashSet<T>(this IEnumerable<T> source)
{
return new HashSet<T>(source);
}

You can just use this:

set = set.Select(n => n.ToLowerInvariant()).ToHashSet();

Or, you could just do this:

set = new HashSet(set, StringComparer.OrdinalIgnoreCase);
//or InvariantCultureIgnoreCase or CurrentCultureIgnoreCase

If you want to leave the original, case-sensitive version in place, you could just query it with linq with case insensitivity:

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

The constructor of HashSet can take an alternative IEqualityComparer that can override how equality is determined. See the list of constructors here.

The class StringComparer contains a bunch of static instances of IEqualityComparers for strings. Particularly, you're probably interested in StringComparer.OrdinalIgnoreCase. Here is the documentation of StringComparer.

Note that another constructor takes in an IEnumerable, so you can construct a new HashSet from your old one, but with the IEqualityComparer.

So, all together, you want to convert your HashSet as follows:

var myNewHashSet = new HashSet(myOldHashSet, StringComparer.OrdinalIgnoreCase);

The HashSet<T> constructor has an overload that lets you pass in a custom IEqualityComparer<string>. There are a few of these defined for you already in the static StringComparer class, a few of which ignore case. For example:

var set = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
set.Add("john");
Debug.Assert(set.Contains("JohN"));

You'll have to make this change at the time of constructing the HashSet<T>. Once one exists, you can't change the IEqualityComparer<T> it's using.


Just so you know, by default (if you don't pass in any IEqualityComparer<T> to the HashSet<T> constructor), it uses EqualityComparer<T>.Default instead.


Edit

The question appears to have changed after I posted my answer. If you have to do a case insensitive search in an existing case sensitive HashSet<string>, you will have to do a linear search:

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

There's no way around this.

You can not magically make case-sensetive HashSet (or Dictionary) to behave in case-insensitive way.

You have to recreate one inside your function if you can not rely on incoming HashSet to be case-insensitive.

Most compact code - use constructor from existing set:

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

Note that copying HashSet is as expensive as walking through all items, so if your function does just on search it would be cheaper (O(n)) to iterate through all items. If your function called multiple times to make single case-insensitive search you should try to pass proper HashSet to it instead.

The HashSet is designed to quickly find elements as per its hashing function and equality comparator. What you are asking for is really to find an element matching "some other" condition. Imagine that you have a Set<Person> objects that uses only Person.Name for comparison and you need to find an element with some given value of Person.Age.

The point is you need to iterate over the contents of the set to find the matching elements. If you are going to be doing this often you might create a different Set, in you case using a case-insensitive comparator but then you would have to make sure that this shadow set is in sync with the original.

The answers so far are essentially variations of the above, I thought to add this to clarify the fundamental issue.

You can now use

set.Contains(item, StringComparer.OrdinalIgnoreCase);

without needing to re-create you HashSet