Zastąp wiele elementów ciągu w C #

86

Czy jest lepszy sposób na zrobienie tego ...

MyString.Trim().Replace("&", "and").Replace(",", "").Replace("  ", " ")
         .Replace(" ", "-").Replace("'", "").Replace("/", "").ToLower();

Rozszerzyłem klasę string, aby ograniczyć ją do jednego zadania, ale czy jest szybszy sposób?

public static class StringExtension
{
    public static string clean(this string s)
    {
        return s.Replace("&", "and").Replace(",", "").Replace("  ", " ")
                .Replace(" ", "-").Replace("'", "").Replace(".", "")
                .Replace("eacute;", "é").ToLower();
    }
}

Dla zabawy (i żeby zatrzymać argumenty w komentarzach) podałem streszczenie porównując różne przykłady poniżej.

https://gist.github.com/ChrisMcKee/5937656

Opcja wyrażenia regularnego ma straszne wyniki; opcja słownika pojawia się najszybciej; Długa zwojowa wersja wymiany stringbuildera jest nieco szybsza niż krótka ręka.

Chris McKee
źródło
1
Na podstawie tego, co masz w testach porównawczych, wygląda na to, że wersja słownikowa nie wykonuje wszystkich zamienników, co, jak podejrzewam, czyni ją szybszą niż rozwiązania StringBuilder.
ropucha
1
@toad Cześć od 2009 roku; W kwietniu dodałem poniżej komentarz dotyczący tego rażącego błędu. Streszczenie zostało zaktualizowane, chociaż pominąłem D. Wersja słownika jest nadal szybsza.
Chris McKee,
1
@TotZam przynajmniej sprawdź daty przed oznaczeniem rzeczy; to jest z 2009, czyli z 2012
Chris McKee
Ponieważ wiele odpowiedzi wydaje się związanych z wydajnością, uważam, że należy podkreślić, że odpowiedź Andreja Adamanko będzie prawdopodobnie najszybsza w przypadku wielu zmian ; z pewnością szybszy niż łańcuch .Replace (), zwłaszcza na dużym ciągu wejściowym, jak stwierdzono w jego odpowiedzi.
osoba 27

Odpowiedzi:

123

Szybciej - nie. Bardziej efektywne - tak, jeśli będziesz korzystać z StringBuilderzajęć. W przypadku implementacji każda operacja generuje kopię ciągu, który w pewnych okolicznościach może pogorszyć wydajność. Ciągi znakówniezmiennymi obiektami, więc każda operacja zwraca tylko zmodyfikowaną kopię.

Jeśli spodziewasz się, że ta metoda będzie aktywnie wywoływana w wielu Stringsznaczących długościach, może być lepszym rozwiązaniem „migracja” jej implementacji do StringBuilderklasy. Dzięki niemu wszelkie modyfikacje są wykonywane bezpośrednio na tej instancji, dzięki czemu oszczędzasz niepotrzebnych operacji kopiowania.

public static class StringExtention
{
    public static string clean(this string s)
    {
        StringBuilder sb = new StringBuilder (s);

        sb.Replace("&", "and");
        sb.Replace(",", "");
        sb.Replace("  ", " ");
        sb.Replace(" ", "-");
        sb.Replace("'", "");
        sb.Replace(".", "");
        sb.Replace("eacute;", "é");

        return sb.ToString().ToLower();
    }
}
BC2
źródło
2
Dla jasności słownikową odpowiedzią jest najszybszy stackoverflow.com/a/1321366/52912
Chris McKee,
3
W twoim benchmarku na gist.github.com/ChrisMcKee/5937656 test słownika nie jest kompletny: nie wykonuje wszystkich zastąpień i „zastępuje”, a nie „”. Niewykonanie wszystkich wymian może być przyczyną, dla której jest najszybszy w teście porównawczym. Zamiana wyrażenia regularnego również nie została zakończona. Ale co najważniejsze, Twój ciąg TestData jest bardzo krótki. Podobnie jak w przypadku zaakceptowanej odpowiedzi, ciąg musi mieć znaczną długość, aby StringBuilder miał przewagę. Czy mógłbyś powtórzyć benchmark z ciągami 10kB, 100kB i ​​1MB?
Leif
To dobra uwaga; w obecnej postaci był używany do czyszczenia adresów URL, więc testy przy 100kb - 1mb byłyby nierealne. Zaktualizuję benchmark, więc używa całości, to był błąd.
Chris McKee,
Aby uzyskać najlepszą wydajność, zapętl znaki i wymień je samodzielnie. Jednak może to być uciążliwe, jeśli masz więcej niż pojedyncze ciągi znaków (znalezienie ich zmusza cię do porównania wielu znaków jednocześnie, podczas gdy ich zastąpienie wymaga przydzielenia większej ilości pamięci i przeniesienia reszty ciągu).
Chayim Friedman
13

będzie to bardziej wydajne:

public static class StringExtension
{
    public static string clean(this string s)
    {
        return new StringBuilder(s)
              .Replace("&", "and")
              .Replace(",", "")
              .Replace("  ", " ")
              .Replace(" ", "-")
              .Replace("'", "")
              .Replace(".", "")
              .Replace("eacute;", "é")
              .ToString()
              .ToLower();
    }
}
TheVillageIdiot
źródło
Naprawdę trudne do odczytania. Jestem pewien, że wiesz, co to robi, ale młodszy Dev podrapie się po głowie, słysząc, co się naprawdę dzieje. Zgadzam się - ja też zawsze szukam krótkiej ręki, żeby coś napisać - Ale to tylko dla własnej satysfakcji. Inni ludzie wariowali na stosie bałaganu.
Piotr Kula
3
To jest faktycznie wolniejsze. BenchmarkOverhead ... 13ms StringClean-user151323 ... 2843ms StringClean-TheVillageIdiot ... 2921ms Różni się przy powtórkach, ale odpowiedź wygrywa gist.github.com/anonymous/5937596
Chris McKee
12

Jeśli szukasz po prostu ładnego rozwiązania i nie musisz oszczędzać kilku nanosekund, co powiesz na cukier LINQ?

var input = "test1test2test3";
var replacements = new Dictionary<string, string> { { "1", "*" }, { "2", "_" }, { "3", "&" } };

var output = replacements.Aggregate(input, (current, replacement) => current.Replace(replacement.Key, replacement.Value));
TimS
źródło
Podobnie jak w przykładzie C w Gist (jeśli spojrzysz powyżej, brzydsza instrukcja linq znajduje się w komentarzu)
Chris McKee
1
Ciekawe, że określenie funkcjonalne definiuje się jako „Brzydsze” niż proceduralne.
TimS,
nie będę się o to kłócić; jego tylko preferencja. Jak mówisz, linq to po prostu cukier syntaktyczny; i jak powiedziałem, już wstawiłem odpowiednik nad kodem :)
Chris McKee
11

Może trochę bardziej czytelny?

    public static class StringExtension {

        private static Dictionary<string, string> _replacements = new Dictionary<string, string>();

        static StringExtension() {
            _replacements["&"] = "and";
            _replacements[","] = "";
            _replacements["  "] = " ";
            // etc...
        }

        public static string clean(this string s) {
            foreach (string to_replace in _replacements.Keys) {
                s = s.Replace(to_replace, _replacements[to_replace]);
            }
            return s;
        }
    }

Dodaj także sugestię New In Town dotyczącą StringBuilder ...

Paolo Tedesco
źródło
5
Byłoby bardziej czytelne w ten sposób:private static Dictionary<string, string> _replacements = new Dictionary<string, string>() { {"&", "and"}, {",", ""}, {" ", " "} /* etc */ };
ANeves uważa, że ​​SE jest złem
2
lub oczywiście ... private static readonly Dictionary <string, string> Replacements = new Dictionary <string, string> () {{"&", "and"}, {",", ""}, {"", ""} / * etc * /}; public static string Clean (this string s) {return Replacements.Keys.Aggregate (s, (current, toReplace) => current.Replace (toReplace, Replacements [toReplace])); }
Chris McKee,
2
-1: Używanie słownika nie ma tutaj znaczenia. Po prostu użyj List<Tuple<string,string>>. To również zmienia kolejność zamian jest brana ORAZ nie jest tak szybka jak np s.Replace("a").Replace("b").Replace("c"). Nie używaj tego!
Thomas
6

W proponowanych rozwiązaniach jest jedna rzecz, którą można zoptymalizować. Posiadanie wielu wywołań Replace()powoduje, że kod wykonuje wiele przejść przez ten sam ciąg. W przypadku bardzo długich łańcuchów rozwiązania mogą być powolne z powodu braku pojemności pamięci podręcznej procesora. Można rozważyć zastąpienie wielu ciągów w jednym przebiegu .

Andrej Adamenko
źródło
1
Wiele odpowiedzi wydaje się być zaniepokojonych wydajnością, w którym to przypadku jest to najlepsze. Jest to proste, ponieważ jest to po prostu udokumentowane przeciążenie String.Replace, gdzie zwracasz oczekiwaną wartość na podstawie dopasowania, w tym przykładzie, używając słownika do ich dopasowania. Powinien być łatwy do zrozumienia.
osoba 27
4

Inną opcją korzystania z linq jest

[TestMethod]
public void Test()
{
  var input = "it's worth a lot of money, if you can find a buyer.";
  var expected = "its worth a lot of money if you can find a buyer";
  var removeList = new string[] { ".", ",", "'" };
  var result = input;

  removeList.ToList().ForEach(o => result = result.Replace(o, string.Empty));

  Assert.AreEqual(expected, result);
}
Luiz Felipe
źródło
Możesz zadeklarować, var removeList = new List<string> { /*...*/ };a następnie po prostu zadzwoń removeList.ForEach( /*...*/ );i uprość swój kod. Zauważ również, że nie daje pełnej odpowiedzi na pytanie, ponieważ wszystkie znalezione ciągi są zastępowane String.Empty.
Tok
2

Robię coś podobnego, ale w moim przypadku robię serializację / deserializację, więc muszę być w stanie przejść w obu kierunkach. Uważam, że użycie ciągu [] [] działa prawie identycznie jak słownik, łącznie z inicjalizacją, ale można też pójść w innym kierunku, przywracając zamienniki do ich oryginalnych wartości, do czego słownik naprawdę nie jest przystosowany.

Edycja: możesz użyć Dictionary<Key,List<Values>>, aby uzyskać taki sam wynik jak ciąg [] []

sidDemure
źródło
-1
string input = "it's worth a lot of money, if you can find a buyer.";
for (dynamic i = 0, repl = new string[,] { { "'", "''" }, { "money", "$" }, { "find", "locate" } }; i < repl.Length / 2; i++) {
    input = input.Replace(repl[i, 0], repl[i, 1]);
}
user7718176
źródło
2
Powinieneś rozważyć dodanie kontekstu do swoich odpowiedzi. Jak krótkie wyjaśnienie, co robi i, jeśli ma to znaczenie, dlaczego napisałeś to tak, jak to zrobiłeś.
Neil