16

检查是否StringBuilder以特定字符串结尾的最佳(最短和最快)方法是什么?

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

我可以考虑像"some string".Length一个一个循环并读取字符之类的东西,但也许存在更简单的东西?:)

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

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

5 回答 5

27

为了避免生成完整字符串的性能开销,您可以使用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.

于 2013-07-10T20:26:11.177 回答
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;
    }
}
于 2013-07-10T20:42:18.043 回答
2

TL;博士

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

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

我编写了以下方法,如果它存在于字符串中,则返回字符串的索引,StringBuilder否则返回 -1。您可以使用它来创建其他常用String方法,如ContainsStartsWithEndsWith。这种方法比其他方法更可取,因为它应该正确处理本地化和大小写,并且不会强制您调用ToString. StringBuilder如果您指定应该忽略大小写,它会创建一个垃圾值,您可以通过使用 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对象而不是连接字符串的主要原因是由于字符串是不可变的,因此会产生内存开销。当您在不使用 aStringBuilder的情况下进行过多的字符串操作时,您看到的性能损失通常是收集您在此过程中创建的所有垃圾字符串的结果。

以此为例:

string firstString = "1st", 
       secondString = "2nd", 
       thirdString = "3rd", 
       fourthString = "4th";
string all = firstString;
all += " & " + secondString;
all += " &" + thirdString;
all += "& " + fourthString + ".";

如果你要运行它并在内存分析器中打开它,你会发现一组看起来像这样的字符串:

“一号”、“二号”、“三号”、“四号”、
“&”、“&2nd”、“1st&2nd”
“&”、“&3rd”、“1st & 2nd &3rd”
“&”、“&4th”、“&4th”。
“第一、二、三、四。”

那是我们在该范围内创建的总共 14 个对象,但是如果您没有意识到每次您可能认为只有 5 个时,每个加法运算符都会创建一个全新的字符串。那么其他九根弦会怎样呢?它们在内存中逐渐消失,直到垃圾收集器决定将它们捡起来。

所以现在我的观点是:如果你试图找出关于一个StringBuilder对象的一些东西并且你不想调用ToString(),这可能意味着你还没有完成构建该字符串。如果您试图找出构建器是否以“Foo”结尾,那么调用它是一种浪费,sb.ToString(sb.Length - 1, 3) == "Foo"因为您正在创建另一个字符串对象,该对象在您调用的那一刻就变得孤立和过时了。

我的猜测是,您正在运行一个将文本聚合到您的循环中StringBuilder,并且您想要结束循环,或者如果最后几个字符是您期望的某个标记值,则只是做一些不同的事情。

于 2013-07-11T01:24:33.700 回答
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 ] );
    }
于 2019-09-09T22:40:45.140 回答
0

我给你你所要求的(有你所说的限制),但不是最好的方法。就像是:

StringBuilder sb = new StringBuilder("Hello world"); bool hasString = sb.Remove(1,sb.Length - "world".Length) == "world";

于 2013-07-10T20:35:42.050 回答