Desenvolvimento - C#

.Net Framework Inside : Strings (System.String)

A primeira coisa que devemos ter em mente quando nos referimos às strings é que este tipo de objeto é imutável, ou seja, todas as vezes que alterarmos seu conteúdo o .Net Framework irá alocar um novo espaço de memória, copiar o conteúdo da string anterior e adicionar o novo conteúdo...

por Guilherme Bacellar Moralez



A primeira coisa que devemos ter em mente quando nos referimos às strings é que este tipo de objeto é imutável, ou seja, todas as vezes que alterarmos seu conteúdo o .Net Framework irá alocar um novo espaço de memória, copiar o conteúdo da string anterior e adicionar o novo conteúdo.

Vamos a um exemplo simples:

string nomeUsuario; // 0

nomeUsuario = "Guilherme"; // 1

nomeUsuario += "Daniel"; // 2

O que o .Net faz neste caso é criar um espaço de memória quando executamos a linha (1) e armazenar a palavra “Guilherme” nela. Ao chegar à linha (2) o .Net irá alocar um novo espaço de memória e armazenar a palavra “Daniel”, deixando o espaço de memória anterior para trás.

Pode soar um pouco estranho, mas a quantidade de elementos que o .Net Framework utiliza contempla os caracteres de controle que são utilizados no armazenamento.

Calculando o total de memória utilizada temos que o espaço de 63 Bytes (33 bytes “Guilherme” + 30 bytes “Daniel”) total foi utilizado para a operação.

Agora peguemos um exemplo um pouco mais complexo:

string query; // 0

query = "SELECT * "; // 1

query += "FROM tb_Usuario "; // 2

query += "WHERE IdUsuario = 25"; // 3

Neste exemplo podemos verificar a gravidade do problema uma vez que a quantidade de memória em utilização será cumulativa:

· Par executar a linha (1) são gastos: 33 bytes.

· Para executar a linha (2) são gastos: 73 bytes (33 bytes para a linha 1 e 40 bytes para a linha 2)

· Para executar a linha (3) são gastos: 117 bytes (73 bytes para a linha 2 e 44 bytes para linha 3)

No total, tão sido gastos 177 bytes para a execução do trecho de código acima.

Pode parecer que 177 bytes não representam muito em consumo de memória, mas quando lidamos com sistemas Cliente/Servidor ou distribuídos ou quando são necessárias várias execuções de uma rotina com concatenação o problema começa a se tornar acentuado e em casos mais graves pode chegar a causar lentidão no aplicativo ou até o esgotamento de memória do computador.

Por sua característica de imutabilidade é comum pensarmos em strings como objetos “value type” quando na verdade são objetos “reference type”.

Além disso, quando utilizamos os operadores de comparação (== no C#) não há uma comparação de endereços de memórias, mas o método (Equals) é invocado para realizar a comparação. Essa característica auxilia muito no entendimento do código mas também pode provocar uma pequena confusão uma vez que com os demais objetos “reference type” ocorre a comparação de ponteiros.

Calculando o Tamanho das Strings

Utilizaremos o principio da serialização a fim de calcular o tamanho dos objetos string.

/// <summary>

/// Calcula o Tamanho em Bytes de uma String

/// </summary>

/// <param name="fonte">String para o Calculo</param>

/// <returns>Tamanho em Bytes</returns>

public long CalculaTamanhoString(string fonte)

{

// Verifica os Parâmetros

if (fonte == null || fonte.Equals(string.Empty)) { return 0; }

// Cria Objetos

System.IO.MemoryStream memoryStream = new MemoryStream();

System.Runtime.Serialization.Formatters.Binary.BinaryFormatter formatter =

new System.Runtime.Serialization.Formatters.Binary.BinaryFormatter();

// Serializa

formatter.Serialize(memoryStream, fonte);

// Retorna Tamanho

return memoryStream.Length;

}

A serialização binária retorna a exata representação do código do objeto em memória a fim de permitir a restauração posterior deste objeto e desta forma conseguimos obter o tamanho (length) do fluxo (stream) binário que contém o objeto.

Trabalhando com Strings

Existem basicamente 3 formas de se trabalhar com strings: Concatenação, String.Format() e StringBuilder.

Iremos tratar cada uma delas detalhadamente neste momento e utilizaremos nosso programa de exemplos para explorar mais das características de cada um.

Concatenação

Provavelmente a forma mais intuitiva, prática e pouco eficiente de se trabalhar com strings.

Ex:

string query; // 0

int idUsuario = 25; // 1

query = "SELECT * "; // 2

query += "FROM tb_Usuario "; // 3

query += "WHERE IdUsuario = " + idUsuario.ToString(); // 4

A Concatenação torna-se especialmente útil (devido a sua simplicidade) para rotinas simples como concatenar duas ou três strings uma única vez. Ex:

string nome = "Guilherme"; // 0

string sobreNome = "Bacellar"; // 1

string ultimoNome = "Moralez"; // 2

string nomeCompleto = nome + " " + sobreNome + " " + ultimoNome; // 3

Contudo, para rotinas mais complexas ou com um maior número de iterações, a concatenação torna-se progressivamente mais lenta com o aumento da quantidade de texto nas operações.

String.Format()

O String.Format() é talvez um dos recursos mais práticos do objeto string (System.String) e permite a formatação intuitiva de strings complexas, porém tornar-se onerosa em performance uma vez que é necessário a análise da string em busca dos operadores troca. Podemos utilizar os operadores de troca “{<número>}” inúmeras vezes em uma única string.

Ex:

string query; // 0

int idUsuario = 25; // 1

query = string.Format("SELECT * FROM tb_Usuario WHERE IdUsuario = {0}", idUsuario); // 2

O método Format() fornece ainda uma sobrecarga com suporte aos provedores de formato (FormatProvider) através da interface (IFormatProvider), que nos permite especificar (quando necessário) configurações especiais de formato (como números, moeda, data, etc) aos operadores do String.Format().

Sendo a mais lenta das formas de manipulação de string, o String.Format() é recomendado apenas para iterações muito simples e em pequenas quantidades e ainda possui os mesmos problemas de alocação de memória que a concatenação possui. Sua vantagem não se estende além da facilidade de leitura do resultado do método.

StringBuilder

Sendo a mais complexa e mais eficiente das formas de manipulação de string, o StringBuilder (System.Text.StringBuilder) oferece vantagens em performance e utilização racional dos recursos de memória do computador.

Ex:

System.Text.StringBuilder query = new StringBuilder(); // 0

int idUsuario = 25; // 1

query.Append("SELECT * "); // 2

query.Append("FROM tb_Usuario "); // 3

query.Append("WHERE IdUsuario = "); // 4

query.Append(idUsuario.ToString()); // 5

Apesar da aparente complexidade do código o objeto StringBuilder foi especialmente desenhado para a utilização em processos que necessitam alterar uma string muitas vezes . Ex: Criação de strings DML (Data Manipulation Language - Insert, Update, Delete, Select) para bancos de dados.

Medindo a Performance

Nosso aplicativo de exemplos é composto de um Formulário que contém as rotinas que serão necessárias para a comparação entre a concatenação, o string.Format() e o StringBuilder.

O formulário é composto de um texto que será utilizado nos testes, da quantidade de iterações que serão utilizadas para a medição e de botões de acionamento dos testes.

Os resultados dos testes em um computador (Intel Pentium 4 3.06GHz com 1.5 Gb de Memória Ram) rodando Windows Vista RC2 foram:

Texto

Total de Iterações

Concatenação

String.Format()

StringBuilder

ABC*2

1.000

4,8 ms

13,6 ms

0 ms

ABC*2

10.000

417 ms

1.966 ms

0,97 ms

ABC*2

100.000

*1

*1

8,78 ms

ABCDEF*3

1.000

8,7 ms

27, 34 ms

0 ms

ABCDEF*3

10.000

1.313 ms

4,813 ms

0,97 ms

ABCDEF*3

100.000

*1

*1

15,62 ms

Benchmark com Strings*4

1.000

25,39 ms

109 ms

0,97 ms

Benchmark com Strings*4

10.000

7.288 ms

18.300 ms

2,92 ms

Benchmark com Strings*4

100.000

*1

*1

34,18 ms

*1 – Não respondeu após 60 segundos de teste (60.000 ms)
*2 – A string “ABC” ocupa um total de 27 bytes
*3 – A string “ABCDEF” ocupa um total de 30 bytes
*4 – A string “Benchmark com Strings” ocupa um total de 45 bytes

Conclusão

Depois de realizarmos o benchmark podemos concluir que o objeto StringBuilder é sempre a melhor solução para o tratamento de strings, contudo para operações muito simples como a junção de duas strings a concatenação simples é a opção mais indicada, mas, para operações com várias iterações ou operações com vários conjuntos de strings o objeto StringBuilder deve ser a opção utilizada.

Clique aqui e faça o download do código

Guilherme Bacellar Moralez

Guilherme Bacellar Moralez - Bacharel em Ciências da Computação, Desenvolvedor .NET há 4 anos, MCAD, Autor de Livros e Artigos, Arquiteto de Software do Projeto D.NET (Framework de Desenvolvimento .NET).
Blog:
http://dotnetmax.bacellar.org/