我需要进行大量高性能的不区分大小写的字符串比较,并意识到我的做法 .ToLower().Trim() 真的很愚蠢,因为所有新字符串都被分配了
所以我仔细研究了一下,这种方式似乎更好:
String.Compare(txt1,txt2, StringComparison.OrdinalIgnoreCase)
这里唯一的问题是我想忽略前导或尾随空格,即 Trim(),但如果我使用 Trim,我在字符串分配方面也会遇到同样的问题。我想我可以检查每个字符串,看看它是否 StartsWith(" ") 或 EndsWith(" "),然后再修剪。或者找出每个字符串的索引、长度并传递给字符串。比较覆盖
public static int Compare
(
string strA,
int indexA,
string strB,
int indexB,
int length,
StringComparison comparisonType
)
但这看起来相当混乱,如果我不为两个字符串上的尾随和前导空白的每个组合创建一个非常大的 if-else 语句,我可能必须使用一些整数......那么有什么优雅的解决方案的想法吗?
这是我目前的建议:
public bool IsEqual(string a, string b)
{
return (string.Compare(a, b, StringComparison.OrdinalIgnoreCase) == 0);
}
public bool IsTrimEqual(string a, string b)
{
if (Math.Abs(a.Length- b.Length) > 2 ) // if length differs by more than 2, cant be equal
{
return false;
}
else if (IsEqual(a,b))
{
return true;
}
else
{
return (string.Compare(a.Trim(), b.Trim(), StringComparison.OrdinalIgnoreCase) == 0);
}
}
类似这样的事情应该做到:
public static int TrimCompareIgnoreCase(string a, string b) {
int indexA = 0;
int indexB = 0;
while (indexA < a.Length && Char.IsWhiteSpace(a[indexA])) indexA++;
while (indexB < b.Length && Char.IsWhiteSpace(b[indexB])) indexB++;
int lenA = a.Length - indexA;
int lenB = b.Length - indexB;
while (lenA > 0 && Char.IsWhiteSpace(a[indexA + lenA - 1])) lenA--;
while (lenB > 0 && Char.IsWhiteSpace(b[indexB + lenB - 1])) lenB--;
if (lenA == 0 && lenB == 0) return 0;
if (lenA == 0) return 1;
if (lenB == 0) return -1;
int result = String.Compare(a, indexA, b, indexB, Math.Min(lenA, lenB), true);
if (result == 0) {
if (lenA < lenB) result--;
if (lenA > lenB) result++;
}
return result;
}
示例:
string a = " asdf ";
string b = " ASDF \t ";
Console.WriteLine(TrimCompareIgnoreCase(a, b));
输出:
0
您应该根据简单的修剪和比较一些真实数据来对其进行分析,以查看您将要使用它的用途是否确实存在任何差异。
我会使用你的代码
String.Compare(txt1,txt2, StringComparison.OrdinalIgnoreCase)
并根据需要添加任何
.Trim()
调用。这将在大多数情况下保存您的初始选项 4 个字符串 (.ToLower().Trim()
),并始终保存两个字符串 (.ToLower()
)。
如果您在此之后遇到性能问题,那么您的“混乱”选项可能是最好的选择。
首先确保您确实需要优化此代码。也许创建字符串的副本不会明显影响您的程序。
如果确实需要优化,可以尝试在第一次存储字符串时处理字符串,而不是在比较它们时处理(假设它发生在程序的不同阶段)。例如,存储字符串的修剪版本和小写版本,以便在比较它们时可以简单地检查等效性。
你不能只修剪(并可能使其小写)每个字符串一次(在获取它时)吗?做更多的事情听起来像是过早的优化......
我注意到您的第一个建议仅比较相等性而不是排序,这可以进一步节省效率。
public static bool TrimmedOrdinalIgnoreCaseEquals(string x, string y)
{
//Always check for identity (same reference) first for
//any comparison (equality or otherwise) that could take some time.
//Identity always entails equality, and equality always entails
//equivalence.
if(ReferenceEquals(x, y))
return true;
//We already know they aren't both null as ReferenceEquals(null, null)
//returns true.
if(x == null || y == null)
return false;
int startX = 0;
//note we keep this one further than the last char we care about.
int endX = x.Length;
int startY = 0;
//likewise, one further than we care about.
int endY = y.Length;
while(startX != endX && char.IsWhiteSpace(x[startX]))
++startX;
while(startY != endY && char.IsWhiteSpace(y[startY]))
++startY;
if(startX == endX) //Empty when trimmed.
return startY == endY;
if(startY == endY)
return false;
//lack of bounds checking is safe as we would have returned
//already in cases where endX and endY can fall below zero.
while(char.IsWhiteSpace(x[endX - 1]))
--endX;
while(char.IsWhiteSpace(y[endY - 1]))
--endY;
//From this point on I am assuming you do not care about
//the complications of case-folding, based on your example
//referencing the ordinal version of string comparison
if(endX - startX != endY - startY)
return false;
while(startX != endX)
{
//trade-off: with some data a case-sensitive
//comparison first
//could be more efficient.
if(
char.ToLowerInvariant(x[startX++])
!= char.ToLowerInvariant(y[startY++])
)
return false;
}
return true;
}
当然,如果没有匹配的哈希码生成器,什么是相等检查器:
public static int TrimmedOrdinalIgnoreCaseHashCode(string str)
{
//Higher CMP_NUM (or get rid of it altogether) gives
//better hash, at cost of taking longer to compute.
const int CMP_NUM = 12;
if(str == null)
return 0;
int start = 0;
int end = str.Length;
while(start != end && char.IsWhiteSpace(str[start]))
++start;
if(start != end)
while(char.IsWhiteSpace(str[end - 1]))
--end;
int skipOn = (end - start) / CMP_NUM + 1;
int ret = 757602046; // no harm matching native .NET with empty string.
while(start < end)
{
//prime numbers are our friends.
ret = unchecked(ret * 251 + (int)(char.ToLowerInvariant(str[start])));
start += skipOn;
}
return ret;
}
问题是,如果需要做,就需要做。我认为您的任何不同解决方案都不会产生影响。在每种情况下都需要进行多次比较才能找到空白或将其删除。
显然,删除空格是问题的一部分,所以你不应该担心这一点。
如果您使用 unicode 字符并且可能比复制字符串慢,那么在比较之前将字符串小写是一个错误。
关于过早优化的警告是正确的,但我假设您已经对此进行了测试,并发现大量时间被浪费在复制字符串上。在这种情况下,我会尝试以下方法:
int startIndex1, length1, startIndex2, length2;
FindStartAndLength(txt1, out startIndex1, out length1);
FindStartAndLength(txt2, out startIndex2, out length2);
int compareLength = Math.Max(length1, length2);
int result = string.Compare(txt1, startIndex1, txt2, startIndex2, compareLength);
FindStartAndLength 是一个查找“修剪”字符串的起始索引和长度的函数(未经测试,但应该给出总体思路):
static void FindStartAndLength(string text, out int startIndex, out int length)
{
startIndex = 0;
while(char.IsWhiteSpace(text[startIndex]) && startIndex < text.Length)
startIndex++;
length = text.Length - startIndex;
while(char.IsWhiteSpace(text[startIndex + length - 1]) && length > 0)
length--;
}
您可以实现自己的
StringComparer
。这是一个基本的实现:
public class TrimmingStringComparer : StringComparer
{
private StringComparison _comparisonType;
public TrimmingStringComparer()
: this(StringComparison.CurrentCulture)
{
}
public TrimmingStringComparer(StringComparison comparisonType)
{
_comparisonType = comparisonType;
}
public override int Compare(string x, string y)
{
int indexX;
int indexY;
int lengthX = TrimString(x, out indexX);
int lengthY = TrimString(y, out indexY);
if (lengthX <= 0 && lengthY <= 0)
return 0; // both strings contain only white space
if (lengthX <= 0)
return -1; // x contains only white space, y doesn't
if (lengthY <= 0)
return 1; // y contains only white space, x doesn't
if (lengthX < lengthY)
return -1; // x is shorter than y
if (lengthY < lengthX)
return 1; // y is shorter than x
return String.Compare(x, indexX, y, indexY, lengthX, _comparisonType);
}
public override bool Equals(string x, string y)
{
return Compare(x, y) == 0;
}
public override int GetHashCode(string obj)
{
throw new NotImplementedException();
}
private int TrimString(string s, out int index)
{
index = 0;
while (index < s.Length && Char.IsWhiteSpace(s, index)) index++;
int last = s.Length - 1;
while (last >= 0 && Char.IsWhiteSpace(s, last)) last--;
return last - index + 1;
}
}
备注:
Trim
和 ToLower
更好)GetHashCode
方法未实现,所以不要将其用作字典中的键使用现代版本的 .NET 和
Span<char>
,现在可以很容易地做到这一点,而无需牺牲性能:
public static bool EqualsIgnoreLeadingTrailingWhitespaces(
string a,
string b,
StringComparison comparison = StringComparison.OrdinalIgnoreCase)
{
if (ReferenceEquals(a, b))
return true;
if (a is null || b is null)
return false;
// Memory allocation free trimming
ReadOnlySpan<char> s1 = a.AsSpan().Trim();
ReadOnlySpan<char> s2 = a.AsSpan().Trim();
return s1.Equals(s2, comparison);
}
上面比较两个字符串是否相等,但可以通过使用 CompareTo() 而不是 Equals() 轻松用于字符串排序。