.NET StringBuilder - проверьте, завершены ли строки

Каков наилучший (самый короткий и быстрый) способ проверить, заканчивается ли StringBuilder определенной строкой?

Если я хочу проверить только один char, это не проблема sb[sb.Length-1] == 'c', но как проверить, заканчивается ли она длинной строкой?

Я могу думать о чем-то вроде цикла из "some string".Length и читать символы один за другим, но, возможно, существует нечто более простое?:)

В конце я хочу иметь метод расширения следующим образом:

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

Ответ 1

Чтобы избежать накладных расходов на производительность для генерации полной строки, вы можете использовать перегрузку 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.

Ответ 2

В msdn вы можете найти тему как искать текст в объекте StringBuilder. Доступны два варианта:

  • Вызов ToString и поиск возвращаемого объекта String.
  • Используйте свойство 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;
    }
}

Ответ 3

TL; DR

Если вы хотите получить кусок или весь контент StringBuilder в объекте String, вы должны использовать его функцию ToString. Но если вы еще не создали свою строку, лучше обрабатывать StringBuilder как массив символов и работать таким образом, чем создавать кучу строк, которые вам не нужны.

Строковые операции в массиве символов могут усложняться локализацией или кодировкой, поскольку строка может быть кодирована многими способами (например, UTF8 или Unicode), но ее символы (System.Char) должны быть 16-битными UTF16.

Я написал следующий метод, который возвращает индекс строки, если он существует в StringBuilder и -1 в противном случае. Вы можете использовать это для создания других общих методов String, таких как Contains, StartsWith и EndsWith. Этот метод предпочтительнее других, потому что он должен правильно обрабатывать локализацию и корпус и не заставляет вас называть ToString на StringBuilder. Он создает одно значение для мусора, если вы укажете, что этот случай следует игнорировать, и вы можете исправить это, чтобы максимизировать экономию памяти, используя Char.ToLower вместо предварительного вычисления нижнего регистра строки, как это делается в приведенной ниже функции. EDIT: Кроме того, если вы работаете со строкой, закодированной в 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 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 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."

Это четырнадцать полных объектов, которые мы создали в этой области, но если вы не понимаете, что каждый отдельный оператор добавления создает целую новую строку каждый раз, когда вы можете подумать, что всего пять. Так что же происходит с девятью другими струнами? Они томятся в памяти, пока сборщик мусора не решит их забрать.

Итак, теперь, на мой взгляд: если вы пытаетесь найти что-то об объекте StringBuilder, и вы не хотите называть ToString(), это, вероятно, означает, что вы еще не создали эту строку. И если вы пытаетесь выяснить, заканчивается ли компоновщик "Foo", он расточительно вызывает sb.ToString(sb.Length - 1, 3) == "Foo", потому что вы создаете еще один строковый объект, который становится сиротой и устарел в тот момент, когда вы сделали вызов.

Я предполагаю, что вы запускаете агрегирующий текст цикла в свой StringBuilder, и вы хотите закончить цикл или просто сделать что-то другое, если последние несколько символов - это какое-то ожидаемое значение, которое вы ожидаете.

Ответ 4

Я даю вам то, о чем вы просили (с ограничениями, которые вы указали), но не лучший способ сделать это. Что-то вроде:

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

Ответ 5

    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 ] );
    }