Substitua vários elementos de string em C #

86

Existe um jeito melhor de fazer isso...

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

Eu estendi a classe string para mantê-la em um único trabalho, mas existe uma maneira mais rápida?

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

Apenas por diversão (e para interromper os argumentos nos comentários), eu apresentei uma ideia básica comparando os vários exemplos abaixo.

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

A opção regex pontua terrivelmente; a opção de dicionário surge mais rápido; a versão longa do stringbuilder replace é ligeiramente mais rápida do que a mão curta.

Chris McKee
fonte
1
Com base no que você tem em seus benchmarks, parece que a versão do dicionário não está fazendo todas as substituições, o que eu suspeito que está tornando-a mais rápida do que as soluções StringBuilder.
sapo
1
@toad Hi de 2009; Adicionei um comentário abaixo em abril sobre esse erro gritante. A essência foi atualizada embora eu tenha pulado D. A versão do dicionário é ainda mais rápida.
Chris McKee de
Possível duplicata de Alternative to String.Replace várias vezes?
Tot Zam
1
@TotZam pelo menos verifique as datas antes de sinalizar coisas; isso é de 2009, isso é de 2012
Chris McKee
Visto que muitas respostas aqui parecem estar relacionadas com o desempenho, acredito que deva ser apontado que a resposta de Andrej Adamanko é provavelmente a mais rápida para muitas substituições; certamente mais rápido do que encadear .Replace (), especialmente em uma string de entrada grande, conforme declarado em sua resposta.
pessoa

Respostas:

123

Mais rápido - não. Mais eficaz - sim, se você usar a StringBuilderclasse. Com sua implementação, cada operação gera uma cópia de uma string que, em certas circunstâncias, pode prejudicar o desempenho. Strings são objetos imutáveis , então cada operação retorna apenas uma cópia modificada.

Se você espera que esse método seja ativado ativamente em múltiplos Stringsde comprimento significativo, pode ser melhor "migrar" sua implementação para a StringBuilderclasse. Com ele, qualquer modificação é realizada diretamente nessa instância, evitando operações de cópia desnecessárias.

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
fonte
2
Para maior clareza, a resposta do dicionário é a mais rápida stackoverflow.com/a/1321366/52912
Chris McKee
3
Em seu benchmark em gist.github.com/ChrisMcKee/5937656 o teste de dicionário não está completo: ele não faz todas as substituições e "" substitui "", não "". Não fazer todas as substituições pode ser o motivo pelo qual é mais rápido no benchmark. A substituição do regex também não está completa. Mas o mais importante, sua string TestData é muito curta. Como os estados de resposta aceitos, a string deve ter um comprimento significativo para que o StringBuilder seja vantajoso. Você poderia repetir o benchmark com strings de 10kB, 100kB e 1 MB?
Leif
É um bom ponto; do jeito que está, estava sendo usado para limpeza de urls, então os testes a 100kb - 1 MB não seriam realistas. Vou atualizar o benchmark, então está usando a coisa toda, no entanto, isso foi um erro.
Chris McKee,
Para melhor desempenho, faça um loop sobre os personagens e substitua-os você mesmo. No entanto, isso pode ser entediante se você tiver mais de uma sequência de caracteres (localizá-los obriga você a comparar vários caracteres de uma vez, ao passo que substituí-los requer alocação de mais memória e movimentação do resto da sequência).
Chayim Friedman
13

isso será mais eficiente:

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
fonte
Muito difícil de ler. Tenho certeza de que você sabe o que isso faz, mas um Junior Dev coçará a cabeça com o que realmente acontece. Eu concordo- também procuro sempre a mão curta de escrever algo- Mas foi apenas para minha própria satisfação. Outras pessoas estavam pirando com a pilha de bagunça.
Piotr Kula
3
Na verdade, isso é mais lento. BenchmarkOverhead ... 13ms StringClean-user151323 ... 2843ms StringClean-TheVillageIdiot ... 2921ms Varia nas repetições, mas a resposta ganha gist.github.com/anonymous/5937596
Chris McKee
12

Se você está simplesmente atrás de uma solução bonita e não precisa economizar alguns nanossegundos, que tal um pouco de açúcar 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
fonte
Semelhante ao exemplo C na Síntese (se você olhar acima, a declaração linq mais feia está no comentário)
Chris McKee
1
É interessante que você defina uma declaração funcional como "mais feia" do que uma procedimental.
TimS de
não vou discutir sobre isso; sua mera preferência. Como você disse, linq é simplesmente açúcar sintático; e como eu disse eu já colocaria o equivalente acima do código :)
Chris McKee
11

Talvez um pouco mais legível?

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

Adicione também a sugestão do New In Town sobre StringBuilder ...

Paolo Tedesco
fonte
5
Seria mais legível assim:private static Dictionary<string, string> _replacements = new Dictionary<string, string>() { {"&", "and"}, {",", ""}, {" ", " "} /* etc */ };
ANeves pensa que SE é o mal
2
ou é claro ... private static readonly Dictionary <string, string> Replacements = new Dictionary <string, string> () {{"&", "and"}, {",", ""}, {"", ""} / * etc * /}; public static string Clean (esta string s) {return Replacements.Keys.Aggregate (s, (current, toReplace) => current.Replace (toReplace, Replacements [toReplace])); }
Chris McKee
2
-1: Usar um dicionário não faz qualquer sentido aqui. Basta usar um List<Tuple<string,string>>. Isso também muda a ordem em que as substituições são feitas E não é tão rápido quanto, por exemplo s.Replace("a").Replace("b").Replace("c"). Não use isso!
Thomas
6

Existe uma coisa que pode ser otimizada nas soluções sugeridas. Ter muitas chamadas para Replace()faz com que o código faça várias passagens na mesma string. Com strings muito longas, as soluções podem ser lentas devido à perda de capacidade do cache da CPU. Pode ser que se deva considerar a substituição de várias strings em uma única passagem .

Andrej Adamenko
fonte
1
Muitas respostas parecem preocupadas com o desempenho, caso em que esta é a melhor. E é simples porque é apenas uma sobrecarga documentada de String.Replace onde você retorna um valor esperado com base na correspondência, neste exemplo, usando um dicionário para combiná-los. Deve ser simples de entender.
pessoa
4

Outra opção usando o linq é

[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
fonte
Você pode declarar var removeList = new List<string> { /*...*/ };então basta chamar removeList.ForEach( /*...*/ );e simplificar seu código. Observe também que ele não responde totalmente à pergunta porque todas as strings encontradas são substituídas por String.Empty.
Tok
2

Estou fazendo algo semelhante, mas no meu caso estou fazendo a serialização / desserialização, então preciso poder ir nas duas direções. Eu descobri que usar uma string [] [] funciona quase de forma idêntica ao dicionário, incluindo a inicialização, mas você pode ir na outra direção também, retornando os substitutos aos seus valores originais, algo que o dicionário realmente não está configurado para fazer.

Editar: você pode usar Dictionary<Key,List<Values>>para obter o mesmo resultado que string [] []

SidDemure
fonte
-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
fonte
2
Você deve considerar adicionar contexto às suas respostas. Como uma breve explicação do que está fazendo e, se for relevante, por que você escreveu dessa maneira.
Neil