为何String.IndexOf仅支持StringComparison,不兼容自定义StringComparer?
Great question! I totally get the frustration of wanting to reuse your carefully crafted comparison logic across different parts of your app—especially when built-in methods like String.IndexOf don't play nice with StringComparer out of the box. Let's break down a clean way to solve this without writing a ton of repetitive extension methods.
Step 1: Refactor Your CustomComparer for Reusability
First, let's tweak your CustomComparer to extract the core string normalization logic into a public static method. This way, both the comparer itself and any future extension methods can share the exact same transformation rules:
using System.Text.RegularExpressions; public class CustomComparer : StringComparer { // Compile the regex once for better performance (balance with startup cost if needed) private static readonly Regex WhitespaceRegex = new Regex(@"\s+", RegexOptions.Compiled | RegexOptions.CultureInvariant); // Make normalization logic public so other code can reuse it public static string NormalizeString(string s) { if (string.IsNullOrEmpty(s)) return string.Empty; // Combine trim and normalization to reduce unnecessary string allocations var trimmed = s.Trim(); if (string.IsNullOrEmpty(trimmed)) return string.Empty; return WhitespaceRegex.Replace(trimmed.ToLowerInvariant(), " "); } public override int Compare(string x, string y) { return StringComparer.Ordinal.Compare(NormalizeString(x), NormalizeString(y)); } public override bool Equals(string x, string y) { // Use string.Equals with Ordinal for consistent, fast comparisons return string.Equals(NormalizeString(x), NormalizeString(y), StringComparison.Ordinal); } public override int GetHashCode(string obj) { return obj == null ? 0 : NormalizeString(obj).GetHashCode(); } }
I made a couple small performance tweaks here:
- Compiled the regex once (instead of every time it's called) to speed up repeated use
- Simplified the
Equalsmethod to usestring.Equalsdirectly (cleaner and more consistent) - Added null/empty checks early to avoid unnecessary processing
Step 2: Build a Reusable IndexOf Extension Method
Now, let's create a single IndexOf extension method that accepts your CustomComparer (or any StringComparer) and reuses the normalization logic. The key challenge here is that normalizing the entire source string first would lose the original character indices—so we need to check substrings of the original string against the normalized target value:
public static class StringExtensions { public static int IndexOf(this string source, string value, StringComparer comparer) { // Match .NET's default behavior for edge cases if (string.IsNullOrEmpty(source)) return -1; if (string.IsNullOrEmpty(value)) return 0; // Handle our CustomComparer specifically using shared normalization logic if (comparer is CustomComparer) { var normalizedTarget = CustomComparer.NormalizeString(value); int targetLength = value.Length; // Iterate through possible substrings in the original source for (int i = 0; i <= source.Length - targetLength; i++) { var substring = source.Substring(i, targetLength); var normalizedSubstring = CustomComparer.NormalizeString(substring); if (normalizedSubstring == normalizedTarget) { return i; // Return the original index, not the normalized one } } return -1; } // Fallback for built-in StringComparers (map to matching StringComparison values) if (comparer == StringComparer.Ordinal) return source.IndexOf(value, StringComparison.Ordinal); if (comparer == StringComparer.OrdinalIgnoreCase) return source.IndexOf(value, StringComparison.OrdinalIgnoreCase); if (comparer == StringComparer.CurrentCulture) return source.IndexOf(value, StringComparison.CurrentCulture); if (comparer == StringComparer.CurrentCultureIgnoreCase) return source.IndexOf(value, StringComparison.CurrentCultureIgnoreCase); if (comparer == StringComparer.InvariantCulture) return source.IndexOf(value, StringComparison.InvariantCulture); if (comparer == StringComparer.InvariantCultureIgnoreCase) return source.IndexOf(value, StringComparison.InvariantCultureIgnoreCase); // Throw for unsupported comparers (or return -1 if you prefer silent failure) throw new NotSupportedException($"StringComparer type {comparer.GetType().Name} is not supported."); } }
How to Use It
Now you can reuse your CustomComparer everywhere, including with IndexOf:
var myComparer = new CustomComparer(); string testSource = " Hello World "; string testValue = "hello world"; // Use with dictionaries (as before) var dict = new Dictionary<string, int>(myComparer); dict.Add(testSource, 1); Console.WriteLine(dict.ContainsKey(testValue)); // True // Use with IndexOf via the extension method int index = testSource.IndexOf(testValue, myComparer); Console.WriteLine(index); // 0 (since the normalized substring matches starting at index 0)
Bonus: Performance Considerations
If you're dealing with very long strings and need faster IndexOf performance, you could:
- Precompute normalized versions of frequently used strings and cache them
- Implement a more optimized substring normalization (e.g., using
Span<char>to avoid extra string allocations) - Use a more efficient whitespace replacement algorithm than regex (if regex is a bottleneck)
But for most common use cases, the approach above will be fast enough while keeping your code clean and reusable.
内容的提问来源于stack exchange,提问作者Isaiah Shiner

