2013-07-10 11 views
11

Jaki jest najlepszy (najkrótszy i najszybszy) sposób sprawdzenia, czy StringBuilder kończy się określonym ciągiem znaków?.NET StringBuilder - sprawdź, czy kończy się ciągiem

Jeśli chcę sprawdzić tylko jeden znak, to nie jest problem sb[sb.Length-1] == 'c', ale jak sprawdzić, czy kończy się dłuższym łańcuchem?

Mogę myśleć o czymś w rodzaju zapętlenia z "some string".Length i czytać znaki jeden po drugim, ale może istnieje coś prostszego? :)

Na koniec chcę mieć metodę rozszerzenia takiego:

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

Odwróć ciąg znaków i zrób 'StartsWith (" world ")' maybe? – PoweredByOrange

+0

Dlaczego boisz się 'ToString'? W ten sposób korzystasz z StringBuilders – banging

+0

@PoweredByOrange Hm ... StringBuilder nie ma metody StartsWith i odwrotnie, będzie to bardziej kosztowne pod względem wydajności, po prostu zacznij sprawdzać od końca, char char. –

Odpowiedz

21

Aby uniknąć napowietrznej wydajności generowania pełny ciąg, można użyć przeciążenie ToString(int,int) że potrzebny zakres indeksu.

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

Edit: Prawdopodobnie byłoby pożądane, aby określić przeciążenie, że trwa StringComparison argument:

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

Edycja: Jak zauważył Tim S w komentarzach, jest wada w mojej odpowiedzi (i wszystkie inne odpowiedzi, które zakładają równość opartą na znakach), która wpływa na pewne porównania Unicode. Unicode nie wymaga dwóch (pod) łańcuchów, aby ta sama sekwencja znaków była uważana za równą. Na przykład, wstępnie skomponowany znak é powinien być traktowany jako równy znakowi e, po którym następuje znak łączenia 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 

Jeśli chcesz obsługiwać te przypadki prawidłowo, to może być najłatwiej po prostu zadzwonić StringBuilder.ToString(), a następnie skorzystać z wbudowanego w String.EndsWith.

+0

Whoops, mój faul ... Przegapiłem, że ToString ma przeciążenie indeksami początkowymi i końcowymi! Dzięki, to jest odpowiedź! :) –

+1

Przyjemna opcja, aby mieć StringComparer, ale kompilator nie puści z linią end.Equals (test, porównanie); on chce StringComparison zamiast porównania :) –

+2

W niektórych porównaniach kulturowych to nie zadziała poprawnie. Na przykład. przykład z http://msdn.microsoft.com/en-us/library/t9h2fbth.aspx, używając StringBuilder all return 'False'. zobacz http://ideone.com/mVHhWR –

0

Daję ci to, o co prosiłeś (z określonymi ograniczeniami), ale nie jest to najlepszy sposób na zrobienie tego. Coś jak:

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

+0

Wystąpiły dwa problemy z rozwiązaniem ... 1) Nie chcę niczego usuwać z początkowego StringBuilder. 2) Usuń zwraca StringBuilder, więc nie może być użyty jako sb == "string"; –

4

Na msdn można znaleźć temat na how to search text in the StringBuilder object. Dwie dostępne opcje to:

  1. Wywołanie funkcji ToString i przeszukiwanie zwróconego obiektu String.
  2. Właściwość Chars służy do sekwencyjnego przeszukiwania zakresu znaków.

Ponieważ pierwsza opcja nie wchodzi w rachubę. Musisz iść z nieruchomością 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; 
    } 
} 
+0

Tak, to jest dobre rozwiązanie ... kiedy będę mieć trochę czasu, sprawdzę różnicę wydajności za pomocą pętli ToString i For. W każdym razie myślę, że ToString (int, int) używa tej samej pętli w środku, więc wydajność będzie mniej więcej taka sama ... ale to tylko założenie :) –

+0

Daj nam znać wyniki twoich benchmarków – Hemario

+0

To trochę gadatliwe, ale lubię że ta metoda nie tworzy żadnych śmieci. –

2

TL; DR

Jeśli celem jest, aby dostać kawałek lub całość TREŚCI StringBuilder „s w String obiektu, należy użyć jego ToString funkcję.Ale jeśli nie jesteś jeszcze gotowy do tworzenia łańcucha, lepiej traktować StringBuilder jako tablicę znaków i działać w ten sposób, niż tworzyć kilka ciągów, których nie potrzebujesz.

Operacje na łańcuchach znaków w tablicy znaków mogą być skomplikowane przez lokalizację lub kodowanie, ponieważ łańcuch może być zakodowany na wiele sposobów (na przykład UTF8 lub Unicode), ale jego znaki (System.Char) mają być 16-bitowym UTF16 wartości.

Napisałem następującą metodę, która zwraca indeks ciągu, jeśli istnieje w ramach StringBuilder i -1 w przeciwnym razie. Za jego pomocą można utworzyć inne typowe metody String, takie jak Contains, i EndsWith. Ta metoda jest lepsza od innych, ponieważ powinna odpowiednio obsługiwać lokalizację i obudowę i nie zmuszać użytkownika do dzwonienia pod numer ToString pod numerem StringBuilder. Tworzy jedną wartość śmieci, jeśli określisz, że wielkość liter powinna zostać zignorowana, i możesz to naprawić, aby zmaksymalizować oszczędność pamięci za pomocą funkcji Char.ToLower zamiast wstępnego obliczania małych liter ciągu, tak jak to robię w poniższej funkcji. EDYCJA: Ponadto, jeśli pracujesz z ciągiem zakodowanym w UTF32, będziesz musiał porównać dwa znaki naraz zamiast jednego.

Prawdopodobnie lepiej używać ToString, chyba że zamierzasz zapętlać, pracować z dużymi ciągami i manipulować lub formatować.

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

Głównym powodem jeden powszechnie używa StringBuilder obiekt zamiast łączenie ciągów jest ze względu na pamięć napowietrznych ponieść ponieważ ciągi są niezmienne. Wydajność, którą widzisz, gdy wykonujesz nadmierne manipulacje strunami bez używania StringBuilder jest często wynikiem zbierania wszystkich ciągów śmieci, które utworzyłeś po drodze.

Weź to na przykład:

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

Jeśli było uruchomić to i otwórz go w profilera pamięci, można znaleźć zbiór łańcuchów, które wyglądają mniej więcej tak:

 
"1st", "2nd", "3rd", "4th", 
" & ", " & 2nd", "1st & 2nd" 
" &", "&3rd", "1st & 2nd &3rd" 
"& ", "& 4th", "& 4th." 
"1st & 2nd &3rd& 4th." 

To czternaście obiektów, które stworzyliśmy w tym zakresie, ale jeśli nie zdajesz sobie sprawy, że każdy operator dodający tworzy cały nowy ciąg za każdym razem, gdy myślisz, że jest ich tylko pięć. Co dzieje się z dziewięcioma innymi strunami? Odkładają się w pamięci, dopóki śmieciarz nie zdecyduje się ich odebrać.

Więc teraz do rzeczy: jeśli próbujesz znaleźć coś o obiekcie StringBuilder i nie chcesz dzwonić pod numer ToString(), prawdopodobnie oznacza to, że nie skończyłeś jeszcze budować tego łańcucha. A jeśli próbujesz dowiedzieć się, czy budowniczy kończy się na "Foo", marnowanie czasu na wywoływanie sb.ToString(sb.Length - 1, 3) == "Foo", ponieważ tworzysz kolejny obiekt typu string, który zostaje osierocony i przestarzały w momencie, w którym wykonałeś połączenie.

Zgaduję, że uruchamiasz pętlę agregującą tekst do swojej StringBuilder i chcesz zakończyć pętlę lub po prostu zrobić coś innego, jeśli kilka ostatnich znaków jest wartością wartą wartownika, którego się spodziewasz.

Powiązane problemy