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.
Resolva exercícios e atividades acadêmicas
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.
Encontre o professor particular perfeito
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!