Sempre que nos deparamos com uma situação de manipulação de strings, vem aquela dúvida de qual o melhor modo de manipular a string. Se você não tem essa dúvida, melhor começar a ter.
Alguns devs usam StringBuilder como silver bullet sempre que precisam manipular textos, outros partem para o Regex e outros usam as funções nativas da string. Seja como for sua abordagem, é bom refletir sobre o motivo da escolha. Nunca escolha uma só por escolher.
Recentemente passei por uma situação de comparação de strings onde precisava retirar os acentos dos textos antes de compará-los. Felizmente, alguém já havia implementado um método extensão para isso.
public static string RemoveCharacters(this string text) { if (string.IsNullOrEmpty(text)) return text; var aux = text; var symbolTable = new Dictionary<char, char[]>(); symbolTable.Add('a', new char[] { 'à', 'á', 'ä', 'â', 'ã' }); symbolTable.Add('c', new char[] { 'ç' }); symbolTable.Add('e', new char[] { 'è', 'é', 'ë', 'ê' }); symbolTable.Add('i', new char[] { 'ì', 'í', 'ï', 'î' }); symbolTable.Add('o', new char[] { 'ò', 'ó', 'ö', 'ô', 'õ' }); symbolTable.Add('u', new char[] { 'ù', 'ú', 'ü', 'û' }); symbolTable.Add('A', new char[] { 'À', 'Á', 'Ä', 'Â', 'Ã' }); symbolTable.Add('C', new char[] { 'Ç' }); symbolTable.Add('E', new char[] { 'È', 'É', 'Ë', 'Ê' }); symbolTable.Add('I', new char[] { 'Ì', 'Í', 'Ï', 'Î' }); symbolTable.Add('O', new char[] { 'Ò', 'Ó', 'Ö', 'Ô', 'Õ' }); symbolTable.Add('U', new char[] { 'Ù', 'Ú', 'Ü', 'Û' }); foreach (var key in symbolTable.Keys) { foreach (var symbol in symbolTable[key]) { aux = aux.Replace(symbol, key); } } return aux; }
Ao me deparar com esse código eu comecei a me perguntar se essa era a melhor maneira de resolver esse problema. Foi então que resolvi fazer alguns testes para entender qual a melhor abordagem.
Primeira coisa que fiz foi implementar o código abaixo, usando StringBuilder. A lógica é a mesma do método anterior, exceto que eu evito criar várias strings e utilizo o Replace do próprio builder para isso, gerando a string somente no final.
public static string RemoveCharactersStringbuiler(this string text) { if (string.IsNullOrEmpty(text)) return text; var aux = new StringBuilder(text); var symbolTable = new Dictionary<char, char[]>(); symbolTable.Add('a', new char[] { 'à', 'á', 'ä', 'â', 'ã' }); symbolTable.Add('c', new char[] { 'ç' }); symbolTable.Add('e', new char[] { 'è', 'é', 'ë', 'ê' }); symbolTable.Add('i', new char[] { 'ì', 'í', 'ï', 'î' }); symbolTable.Add('o', new char[] { 'ò', 'ó', 'ö', 'ô', 'õ' }); symbolTable.Add('u', new char[] { 'ù', 'ú', 'ü', 'û' }); symbolTable.Add('A', new char[] { 'À', 'Á', 'Ä', 'Â', 'Ã' }); symbolTable.Add('C', new char[] { 'Ç' }); symbolTable.Add('E', new char[] { 'È', 'É', 'Ë', 'Ê' }); symbolTable.Add('I', new char[] { 'Ì', 'Í', 'Ï', 'Î' }); symbolTable.Add('O', new char[] { 'Ò', 'Ó', 'Ö', 'Ô', 'Õ' }); symbolTable.Add('U', new char[] { 'Ù', 'Ú', 'Ü', 'Û' }); foreach (var key in symbolTable.Keys) { foreach (var symbol in symbolTable[key]) { aux.Replace(symbol, key); } } return aux.ToString(); }
Na sequência, implementei uma versão do método utilizando Regex. Na teoria, a busca por Regex é otimizado para varrer textos e encontrar patterns.
public static string RemoveCharactersRegex(this string text) { if (string.IsNullOrEmpty(text)) return text; var symbolTable = new Dictionary<string, string> { { "a", "à|á|ä|â|ã" }, { "c", "ç" }, { "e", "è|é|ë|ê" }, { "i", "ì|í|ï|î" }, { "o", "ò|ó|ö|ô|õ" }, { "u", "ù|ú|ü|û" }, { "A", "À|Á|Ä|Â|Ã" }, { "C", "Ç" }, { "E", "È|É|Ë|Ê" }, { "I", "Ì|Í|Ï|Î" }, { "O", "Ò|Ó|Ö|Ô|Õ" }, { "U", "Ù|Ú|Ü|Û" } }; foreach (var key in symbolTable.Keys) { text = Regex.Replace(text, symbolTable[key], key); } return text; }
Rodei, então, testes com cada uma das implementações. O teste consiste em dar Replace de caracteres especiais em um texto simples, uma única vez. Cada teste foi executado separadamente, um teste por execução do programa. O projeto de teste é um projeto Console usando .Net Core 3.1. Os resultados estão na tabela baixo.
Método | Tempo de execução |
Método Original | 1,98 ms |
Método com StringBuilder | 2,07 ms |
Método com Regex | 19,37 ms |
Como podemos ver, o tempo de execução foi pior com o Regex e tempos semelhantes entre a implementação original e a implementação com StringBuilder.
Nessa ideia, resolvi olhar para a implementação base e ver se poderia ganhar alguma performance com ela. Tirei o uso de Dictionary e troquei o foreach por um for.
public static string RemoveCharactersOptimized(this string text) { if (string.IsNullOrEmpty(text)) return text; var symbolTable = new char[][] { new char[] { 'a', 'à', 'á', 'ä', 'â', 'ã' }, new char[] { 'c', 'ç' }, new char[] { 'e', 'è', 'é', 'ë', 'ê' }, new char[] { 'i', 'ì', 'í', 'ï', 'î' }, new char[] { 'o', 'ò', 'ó', 'ö', 'ô', 'õ' }, new char[] { 'u', 'ù', 'ú', 'ü', 'û' }, new char[] { 'A', 'À', 'Á', 'Ä', 'Â', 'Ã' }, new char[] { 'C', 'Ç' }, new char[] { 'E', 'È', 'É', 'Ë', 'Ê' }, new char[] { 'I', 'Ì', 'Í', 'Ï', 'Î' }, new char[] { 'O', 'Ò', 'Ó', 'Ö', 'Ô', 'Õ' }, new char[] { 'U', 'Ù', 'Ú', 'Ü', 'Û' }, }; for (var i = 0; i < symbolTable.Length; i++) { for (var j = 1; j < symbolTable[i].Length; j++) { text = text.Replace(symbolTable[i][j], symbolTable[i][0]); } } return text; }
Rodando o teste novamente, obtive um tempo de 0,6 ms, um terço do tempo da implementação original.
Método | Tempo de execução |
Método Original | 1,98 ms |
Método com StringBuilder | 2,07 ms |
Método com Regex | 19,37 ms |
Método Otimizado | 0,6 ms |
Pode parecer fácil a mudança, mas a otimização não foi do nada. Primeiro, tirei o uso de Dictionary para não demandar uso do GC. Já a troca do foreach pelo for foi motivado pois a instrução foreach executa o método MoveNext para iterar pela lista. Assim, otimizei o código para usar somente tipos primitivos.
Então, para a minha situação, eu pude otimizar a execução do Replace dos caracteres com acentos.
Mas claro que como bom dev, quero ver até onde vai essa melhoria, então refiz os testes com um texto bem maior.
O novo resultado pode ser visto na tabela abaixo.
Método | Tempo de execução |
Método Original | 1,97 ms |
Método com StringBuilder | 3,61 ms |
Método com Regex | 19,31 ms |
Método Otimizado | 0,64 ms |
Com exceção da versão com StringBuilder que ganhou 1,5 ms a mais, as demais implementações mantiveram um tempo constate e o método otimizado continuou sendo o mais rápido.
Essa não é a única solução existente e talvez não seja a melhor, mas foi a solução utilizada para o meu caso e fui capaz de otimizar ela. O link para o projeto está no meu Github. O tempo de execução que você encontrará na sua máquina será diferente do meu, mas o esperado é que o padrão seja o mesmo.
Até a próxima!