.NET StringBuilder - 检查是否以字符串结尾

17

如何最好(最短、最快)地检查StringBuilder是否以特定字符串结尾?

如果我只想检查一个字符,那不是问题sb[sb.Length-1] == 'c',但如何检查它是否以更长的字符串结尾呢?

我可以考虑从"some string".Length开始循环并逐个读取字符,但也许存在更简单的方法? :)

最后我想要像这样的扩展方法:

StringBuilder sb = new StringBuilder("Hello world");
bool hasString = sb.EndsWith("world");

反转字符串,然后使用 StartsWith("world") - Arian Motamedi
你为什么害怕 ToString?这就是你使用 StringBuilders 的方式。 - banging
@PoweredByOrange 嗯... StringBuilder 没有 StartsWith 方法,而且反转它会比从末尾逐个字符检查更耗费性能。 - Alex Dn
1
因为如果我有非常大的字符串,我不想每次迭代整个 StringBuilder 中的 char 数组,所以我在进行优化。 - Alex Dn
5个回答

31
为避免生成完整字符串的性能开销,您可以使用带有索引范围的 ToString(int,int) 重载。
public static bool EndsWith(this StringBuilder sb, string test)
{
    if (sb.Length < test.Length)
        return false;

    string end = sb.ToString(sb.Length - test.Length, test.Length);
    return end.Equals(test);
}

编辑:可能希望定义一个接受StringComparison参数的重载函数:

public static bool EndsWith(this StringBuilder sb, string test)
{
    return EndsWith(sb, test, StringComparison.CurrentCulture);
}

public static bool EndsWith(this StringBuilder sb, string test, 
    StringComparison comparison)
{
    if (sb.Length < test.Length)
        return false;

    string end = sb.ToString(sb.Length - test.Length, test.Length);
    return end.Equals(test, comparison);
}

编辑2:正如评论中Tim S所指出的那样,我的回答存在一个缺陷(以及所有假设基于字符相等性的其他答案),这会影响某些Unicode比较。Unicode不要求两个(子)字符串具有相同的字符序列才能被视为相等。例如,预组合字符é应被视为等于字符e后跟组合标记U+0301

Thread.CurrentThread.CurrentCulture = new CultureInfo("en-US");

string s = "We met at the cafe\u0301";
Console.WriteLine(s.EndsWith("café"));    // True 

StringBuilder sb = new StringBuilder(s);
Console.WriteLine(sb.EndsWith("café"));   // False

如果您想正确处理这些情况,最简单的方法可能是调用 StringBuilder.ToString(),然后使用内置的 String.EndsWith

糟糕,我的失误...我错过了ToString带有起始和结束索引的重载!谢谢,这就是答案! :) - Alex Dn
1
有StringComparer是个不错的选择,但编译器不会让你用line end.Equals(test, comparison)。他想要使用StringComparison而不是comparison :) - Alex Dn
3
在某些文化对比中,这种方法不起作用。例如,来自http://msdn.microsoft.com/en-us/library/t9h2fbth.aspx的示例,使用StringBuilder将全部返回“False”。请参见http://ideone.com/mVHhWR。 - Tim S.
@AlexDn:谢谢,是我自己的错没有尝试编译。已经修复了。 - Douglas
@AlexDn:你说得对(在String.Compare中),你的观点很公正。实际上,框架似乎在是否提供这样的重载方面存在不一致性。EndsWith有它,但是Equals,出于某种原因,却没有。 - Douglas
显示剩余4条评论

7
在 MSDN 上,您可以找到有关 如何在 StringBuilder 对象中搜索文本 的主题。您可以选择以下两个选项之一:
  1. 调用 ToString 并搜索返回的 String 对象。
  2. 使用 Chars 属性顺序搜索一定范围内的字符。
由于第一个选项不可行,因此您必须使用 Chars 属性。
public static class StringBuilderExtensions
{
    public static bool EndsWith(this StringBuilder sb, string text)
    {
        if (sb.Length < text.Length)
            return false;

        var sbLength = sb.Length;
        var textLength = text.Length;
        for (int i = 1; i <= textLength; i++)
        {
            if (text[textLength - i] != sb[sbLength - i])
                return false;
        }
        return true;
    }
}

1
是的,这是很好的解决方案……等我有时间了,我会检查一下使用ToString和For循环的性能差异。无论如何,我认为ToString(int, int)内部使用相同的循环,所以性能应该差不多……但这只是猜测 :) - Alex Dn
1
这个方法有点啰嗦,但我喜欢它不会创建任何垃圾。 - user180326
1
实际上,当我考虑这个问题时,你的方法在处理大字符串时可能会更快,因为一旦出现不相等的字符,你就可以结束循环...而使用ToString,则需要在比较之前获取所有字符的数量。 - Alex Dn
2
我也想避免使用 ToString(),但是:for 循环有最好情况和最坏情况的场景。在最坏情况(最后一个测试字符不同)下,for 循环速度慢了7倍,在最好情况(第一个测试字符不同)下,向后甚至向前循环速度快了77倍或者140倍(取决于循环方向)。我测试了发布版本,每个方法版本调用1m次,测试字符串长度为1000。 - Bitterblue

2

简短版

如果您的目标是获得StringBuilder内容的一部分或全部,并将其放入String对象中,您应该使用其ToString函数。但如果您还没有完成创建字符串,则最好将StringBuilder视为字符数组并以此方式操作,而不是创建大量您不需要的字符串。

对字符数组进行字符串操作可能会因本地化或编码而变得复杂,因为字符串可以以许多方式进行编码(例如UTF8或Unicode),但其字符(System.Char)应为16位UTF16值。

我编写了以下方法,如果字符串存在于StringBuilder中,则返回其索引;否则返回-1。 您可以使用此方法创建其他常见的String方法,例如ContainsStartsWithEndsWith。 由于此方法应正确处理本地化和大小写,而且不强制您在StringBuilder上调用ToString,因此它比其他方法更可取。 如果您指定应忽略大小写,则它将创建一个垃圾值,并且可以通过使用Char.ToLower来最大化内存节省来修复此问题,而不是像在下面的函数中那样预先计算字符串的小写形式。编辑:此外,如果您使用UTF32编码的字符串,则必须一次比较两个字符而不仅仅是一个。

除非您要循环操作、处理大型字符串并进行操作或格式化,否则最好使用ToString

public static int IndexOf(this StringBuilder stringBuilder, string str, int startIndex = 0, int? count = null, CultureInfo culture = null, bool ignoreCase = false)
{
    if (stringBuilder == null)
        throw new ArgumentNullException("stringBuilder");

    // No string to find.
    if (str == null)
        throw new ArgumentNullException("str");
    if (str.Length == 0)
        return -1;

    // Make sure the start index is valid.
    if (startIndex < 0 && startIndex < stringBuilder.Length)
        throw new ArgumentOutOfRangeException("startIndex", startIndex, "The index must refer to a character within the string.");

    // Now that we've validated the parameters, let's figure out how many characters there are to search.
    var maxPositions = stringBuilder.Length - str.Length - startIndex;
    if (maxPositions <= 0) return -1;

    // If a count argument was supplied, make sure it's within range.
    if (count.HasValue && (count <= 0 || count > maxPositions))
        throw new ArgumentOutOfRangeException("count");

    // Ensure that "count" has a value.
    maxPositions = count ?? maxPositions;
    if (count <= 0) return -1;

    // If no culture is specified, use the current culture. This is how the string functions behave but
    // in the case that we're working with a StringBuilder, we probably should default to Ordinal.
    culture = culture ?? CultureInfo.CurrentCulture;

    // If we're ignoring case, we need all the characters to be in culture-specific 
    // lower case for when we compare to the StringBuilder.
    if (ignoreCase) str = str.ToLower(culture);

    // Where the actual work gets done. Iterate through the string one character at a time.
    for (int y = 0, x = startIndex, endIndex = startIndex + maxPositions; x <= endIndex; x++, y = 0)
    {
        // y is set to 0 at the beginning of the loop, and it is increased when we match the characters
        // with the string we're searching for.
        while (y < str.Length && str[y] == (ignoreCase ? Char.ToLower(str[x + y]) : str[x + y]))
            y++;

        // The while loop will stop early if the characters don't match. If it didn't stop
        // early, that means we found a match, so we return the index of where we found the
        // match.
        if (y == str.Length)
            return x;
    }

    // No matches.
    return -1;
}

通常情况下,我们使用 StringBuilder 对象而非字符串拼接的主要原因是字符串不可变性所带来的内存开销。如果你在进行大量字符串操作时没有使用 StringBuilder,那么看到的性能损失往往是由于一路上创建了许多垃圾字符串。举个例子:
string firstString = "1st", 
       secondString = "2nd", 
       thirdString = "3rd", 
       fourthString = "4th";
string all = firstString;
all += " & " + secondString;
all += " &" + thirdString;
all += "& " + fourthString + ".";

如果你运行这段代码并在内存分析器中打开它,你会发现一组字符串,看起来像这样:
"1st", "2nd", "3rd", "4th", " & ", " & 2nd", "1st & 2nd" " &", "&3rd", "1st & 2nd &3rd" "& ", "& 4th", "& 4th." "1st & 2nd &3rd& 4th."
在那个作用域中我们创建了14个对象,但如果你没有意识到每个加法运算符都会创建一个全新的字符串,你可能会认为只有5个。那么其他九个字符串会发生什么呢?它们在内存中闲置,直到垃圾回收器决定回收它们。
所以现在我的观点就是:如果你想要查找关于StringBuilder对象的信息,而你不想调用ToString(),那么很可能意味着你还没有完成构建字符串的工作。如果你想知道构建器是否以"Foo"结尾,调用sb.ToString(sb.Length - 1, 3) == "Foo"是很浪费的,因为你正在创建另一个字符串对象,一旦你调用完毕,它就变得无用了。
我猜你正在运行一个循环将文本聚合到你的StringBuilder中,并且如果最后几个字符是你期望的标志值,你想结束循环或者做一些不同的事情。

1
    private static bool EndsWith(this StringBuilder builder, string value) {
        return builder.GetLast( value.Length ).SequenceEqual( value );
    }
    private static IEnumerable<char> GetLast(this StringBuilder builder, int count) {
        count = Math.Min( count, builder.Length );
        return Enumerable.Range( builder.Length - count, count ).Select( i => builder[ i ] );
    }

根据源代码,似乎 builder[i] 索引器在当前块开始线性搜索特定块。但如果您只有少量块的话,这样做也许还可以接受。 - Alexey F

0
我会给你你要求的内容(按照你所述的限制),但这并不是最好的方法。类似于:
StringBuilder sb = new StringBuilder("Hello world"); bool hasString = sb.Remove(1,sb.Length - "world".Length) == "world";

你的解决方案有两个问题...1)我不想从初始StringBuilder中删除任何内容。2)Remove会返回StringBuilder,因此它不能用作sb ==“string”。 - Alex Dn

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