C# StringBuilder
Você aprendeu em Strings que uma string nunca muda: cada += cria uma string nova. Para juntar meia dúzia de pedaços, isso não faz diferença. Mas, para montar um texto grande, pedaço por pedaço, num laço que roda milhares de vezes (um relatório, uma nota fiscal, um arquivo), o programa passa a maior parte do tempo copiando texto. Para esses casos, o .NET tem o StringBuilder ("construtor de strings"): um texto que pode mudar. Nesta aula você vai ver como usá-lo, quanto ele economiza e quando não vale a pena.
- Por que
+=repetido muitas vezes é lento, e como oStringBuilderresolve. - Criar um
StringBuilder(comusing System.Text;) e acrescentar texto comAppendeAppendLine. - Mudar o texto no lugar:
sb[0] = 'A',Insert,Replace,Remove,Clear. - Transformar em string com
ToString()e encadear chamadas. - Quando usar o
StringBuilder, quando não usar, e os erros reais.
Todos os programas desta aula foram rodados com o .NET 10 na máquina fictícia da Ana (usuária ana, computador notebook), na pasta ~/cs, com dotnet run arquivo.cs.
A ideia: o quadro branco
Imagine que você precisa escrever uma lista de 100 compras e, a cada item novo, tem de passar tudo a limpo numa folha nova, porque a folha antiga não pode ser riscada. No item 100, você já copiou 99 itens, depois 98, depois 97… milhares de linhas copiadas para uma lista de 100.
É isso que o += faz com uma string: como ela é imutável, cada += copia o texto inteiro para uma string nova. O StringBuilder é um quadro branco: você escreve no fim, apaga, troca uma palavra, e ninguém precisa passar nada a limpo. Quando terminar, tira uma "foto" do quadro (ToString()) e tem a string final.
1. O primeiro StringBuilder
using System.Text;
StringBuilder sb = new StringBuilder();
sb.Append("Olá");
sb.Append(", ");
sb.Append("Ana");
sb.Append('!');
Console.WriteLine(sb.ToString());
Console.WriteLine(sb.Length);
$ dotnet run sb1.cs Olá, Ana! 9
- using System.Text;
- O
StringBuildermora no "pacote"System.Text("texto"), que não vem incluído sozinho num programa de arquivo único. Esta linha, no topo do arquivo, avisa que vamos usar nomes de lá (aula Namespaces e using). Sem ela, o erro é oCS0246. - new StringBuilder()
- Cria um quadro branco vazio. O
new("novo") cria um objeto, assunto da aula Classes e Objetos. Por enquanto, leia como "me dê um StringBuilder novo". - sb.Append("Olá")
- Append = "acrescentar no fim". Ao contrário dos métodos de string, ele muda o próprio
sb: não é preciso escreversb = sb.Append(…). Aceita strings,char('!'), números ebool. - sb.ToString()
- Tira a "foto": cria uma
stringcomum com o conteúdo do quadro. A partir dela, você usa tudo o que já conhece de strings. - sb.Length
- Quantos caracteres há no quadro agora, como o
Lengthda string.
O nome sb é um costume (das iniciais de StringBuilder). Num programa de verdade, prefira um nome que diga o que está sendo montado: nota, relatorio, lista. E, como o tipo já aparece no new, muita gente escreve var nota = new StringBuilder(); (aula var).
Missão: a lista de compras só tem arroz. Acrescente feijão e café com mais dois Append para aparecer arroz, feijão, café (as vírgulas e os espaços também precisam ser acrescentados).
arroz, feijão, café
using System.Text;
var lista = new StringBuilder();
lista.Append("arroz");
Console.WriteLine(lista);
using System.Text;
var lista = new StringBuilder();
lista.Append("arroz");
lista.Append(", feijão");
lista.Append(", café");
Console.WriteLine(lista);
Repare que Console.WriteLine(lista) funcionou sem o ToString(): quando recebe algo que não é string, o WriteLine chama o ToString() sozinho. Para guardar o texto numa variável string, porém, o ToString() é obrigatório.
2. AppendLine e chamadas encadeadas
AppendLine acrescenta o texto e uma quebra de linha no fim. E, como cada Append devolve o próprio StringBuilder, dá para chamar um atrás do outro:
using System.Text;
var nota = new StringBuilder();
nota.AppendLine("Lojinha da Ana");
nota.AppendLine("--------------");
nota.Append("Arroz: ").Append(5.90m).AppendLine();
nota.AppendLine($"Feijão: {8.5m:C}");
nota.Append("Itens: ").Append(2);
Console.WriteLine(nota.ToString());
$ dotnet run sb2.cs Lojinha da Ana -------------- Arroz: 5,90 « o número entrou com a vírgula da cultura pt-BR Feijão: R$ 8,50 « interpolação com formato, dentro do AppendLine Itens: 2
- AppendLine("Lojinha da Ana")
- O texto e uma quebra de linha. É o jeito mais comum de montar textos de várias linhas.
- .Append("Arroz: ").Append(5.90m).AppendLine()
- Três chamadas encadeadas, da esquerda para a direita.
AppendLine()vazio acrescenta só a quebra de linha. - AppendLine($"Feijão: {8.5m:C}")
- A interpolação e os formatos (aulas Interpolação e Formatar Números) funcionam normalmente dentro do
Append.
3. Mudar o texto no lugar
Diferente da string, o StringBuilder deixa trocar letras, inserir e apagar sem criar outro texto:
using System.Text;
var sb = new StringBuilder("ana souza");
sb[0] = 'A';
sb[4] = 'S';
Console.WriteLine(sb);
sb.Insert(3, " M.");
Console.WriteLine(sb);
sb.Replace("Souza", "Lima");
Console.WriteLine(sb);
sb.Remove(3, 3);
Console.WriteLine(sb);
sb.Clear();
Console.WriteLine($"[{sb}] {sb.Length}");
$ dotnet run sb3.cs Ana Souza « sb[0] = 'A' trocou a letra (numa string, isso é o erro CS0200) Ana M. Souza « Insert(3, …): enfiou o texto na posição 3 Ana M. Lima « Replace: trocou todas as ocorrências Ana Lima « Remove(3, 3): apagou 3 caracteres a partir da posição 3 [] 0 « Clear: apagou tudo
- new StringBuilder("ana souza")
- Já começa com um texto no quadro.
- sb[0] = 'A'
- Troca o caractere da posição 0. Numa string, isso não compila (aula Strings); no
StringBuilder, é o normal. - Insert, Replace, Remove
- Os mesmos nomes dos métodos de string (aula Métodos de String), mas aqui eles mudam o próprio quadro. Atenção ao
Remove(3, 3): o segundo número é a quantidade a apagar. - Clear()
- Apaga tudo. O quadro continua existindo, vazio, pronto para ser reusado.
4. Por que ele é mais rápido
A diferença aparece com muitas repetições. O programa abaixo junta 100 mil letras x, primeiro com += e depois com StringBuilder. Ele usa um laço for, que repete o bloco entre chaves (aula for), e um Stopwatch ("cronômetro") para medir o tempo:
using System.Diagnostics;
using System.Text;
var relogio = Stopwatch.StartNew();
string texto = "";
for (int i = 0; i < 100_000; i++)
{
texto += "x";
}
Console.WriteLine($"string com +=: {relogio.ElapsedMilliseconds} ms");
relogio.Restart();
var sb = new StringBuilder();
for (int i = 0; i < 100_000; i++)
{
sb.Append('x');
}
string resultado = sb.ToString();
Console.WriteLine($"StringBuilder: {relogio.ElapsedMilliseconds} ms");
Console.WriteLine(texto == resultado);
$ dotnet run tempo.cs string com +=: 952 ms « quase um segundo StringBuilder: 0 ms « menos de um milésimo de segundo True « o mesmo texto no fim
Os tempos mudam de computador para computador e de uma execução para outra, mas a proporção, não. Com +=, a volta número 50 mil copia 50 mil letras para uma string nova; somando todas as voltas, são uns 5 bilhões de letras copiadas. O StringBuilder guarda o texto num espaço que cresce aos poucos (ele dobra de tamanho quando enche), e cada Append só escreve a letra nova no fim.
Esse espaço reservado é a capacidade (sb.Capacity); um StringBuilder novo começa com 16. Se você já sabe que o texto vai ser grande, pode reservar de uma vez: new StringBuilder(1000). É um ajuste fino; na maioria dos programas, não precisa.
Quando usar (e quando não)
| Situação | Use | Por quê |
|---|---|---|
| Juntar poucos pedaços, uma vez | + ou $"…" | Mais simples de ler. O compilador já otimiza a + b + c numa linha só. |
| Juntar os itens de uma lista com um separador | string.Join(", ", itens) | Uma linha, e já é rápido (aula Métodos de String). |
| Montar um texto dentro de um laço | StringBuilder | Evita copiar o texto a cada volta. |
| Montar um texto grande em várias etapas (relatório, nota fiscal, HTML) | StringBuilder | Dá para acrescentar, inserir e trocar no caminho. |
| Texto fixo de várias linhas | """…""" | Não é preciso montar nada (aula Escapes). |
O StringBuilder também tem o seu Join: sb.AppendJoin(", ", itens) acrescenta os itens com o separador, sem criar a string intermediária.
using System.Text;
string[] itens = { "arroz", "feijão", "café" };
var sb = new StringBuilder();
sb.AppendJoin(", ", itens);
Console.WriteLine(sb);
Console.WriteLine(string.Join(", ", itens));
$ dotnet run join.cs arroz, feijão, café arroz, feijão, café
Quando dá errado
error CS0246: O nome do tipo ou do namespace "StringBuilder" não pode ser encontrado
var sb = new StringBuilder();
sb.Append("oi");
Console.WriteLine(sb);
$ dotnet run eb1.cs /home/ana/cs/eb1.cs(1,14): error CS0246: O nome do tipo ou do namespace "StringBuilder" não pode ser encontrado (está faltando uma diretiva using ou uma referência de assembly?) Ocorreu uma falha no build. Corrija os erros de build e execute novamente.
O que significa: o compilador não sabe o que é StringBuilder, porque ele está em System.Text, e esse pacote não foi "chamado". A própria mensagem dá a pista: "está faltando uma diretiva using?".
Como consertar: ponha using System.Text; na primeira linha do arquivo (ou escreva o nome completo, new System.Text.StringBuilder()).
error CS0029: Não é possível converter implicitamente tipo "System.Text.StringBuilder" em "string"
using System.Text;
var sb = new StringBuilder();
sb.Append("oi");
string texto = sb;
$ dotnet run eb2.cs /home/ana/cs/eb2.cs(5,16): error CS0029: Não é possível converter implicitamente tipo "System.Text.StringBuilder" em "string" Ocorreu uma falha no build. Corrija os erros de build e execute novamente.
O que significa: o quadro branco não é uma string. Para virar uma, é preciso tirar a "foto".
Como consertar: string texto = sb.ToString();.
error CS0019: O operador "==" não pode ser aplicado a operandos dos tipos "StringBuilder" e "string"
using System.Text;
var sb = new StringBuilder("oi");
Console.WriteLine(sb == "oi");
$ dotnet run eb3.cs /home/ana/cs/eb3.cs(4,19): error CS0019: O operador "==" não pode ser aplicado a operandos dos tipos "StringBuilder" e "string" Ocorreu uma falha no build. Corrija os erros de build e execute novamente.
O que significa: o == que compara conteúdo é o da string. Um StringBuilder e uma string são tipos diferentes, e o C# não deixa comparar os dois assim.
Como consertar: transforme em string antes: sb.ToString() == "oi".
System.ArgumentOutOfRangeException: Index was out of range.
using System.Text;
var sb = new StringBuilder("oi");
sb[5] = '!';
$ dotnet run eb4.cs Unhandled exception. System.ArgumentOutOfRangeException: Index was out of range. Must be non-negative and less than the size of the collection. (Parameter 'index') at System.Text.StringBuilder.set_Chars(Int32 index, Char value) at Program.<Main>$(String[] args) in /home/ana/cs/eb4.cs:line 4
O que significa: "o índice estava fora do intervalo; precisa ser não negativo e menor que o tamanho". O quadro tem 2 caracteres (posições 0 e 1); a posição 5 não existe. O sb[…] = troca letras que já existem, não acrescenta.
Como consertar: para acrescentar no fim, sb.Append('!'); para trocar, uma posição menor que sb.Length.
sb = sb.Append(…), ou sb.ToString() numa linha sozinha
O que significa: não é erro, mas mostra uma confusão com a string. Append já muda o sb, e o sb = é desnecessário. Já o ToString() numa linha sozinha cria a string e joga fora.
Como consertar: sb.Append(…); sozinho, e string texto = sb.ToString(); quando precisar da string.
Referência
| Código | O que faz |
|---|---|
using System.Text; | permite escrever só StringBuilder |
new StringBuilder(), new StringBuilder("texto") | cria vazio ou com um texto inicial |
sb.Append(x) | acrescenta x (texto, char, número…) no fim |
sb.AppendLine(x), sb.AppendLine() | acrescenta x e uma quebra de linha / só a quebra |
sb.AppendJoin(sep, itens) | acrescenta os itens separados por sep |
sb.Insert(pos, x) | insere x na posição pos |
sb.Replace(velho, novo) | troca todas as ocorrências |
sb.Remove(pos, quantidade) | apaga quantidade caracteres a partir de pos |
sb.Clear() | apaga tudo |
sb[i], sb[i] = 'c' | lê / troca o caractere da posição i |
sb.Length | quantidade de caracteres |
sb.ToString() | cria a string final |
Append, AppendLine, Insert, Replace, Remove e Clear devolvem o próprio StringBuilder, e por isso podem ser encadeados. A tabela completa está em string e StringBuilder.
Exercícios
1. O pacote
Complete a primeira linha do arquivo:
2. Acrescentar com quebra de linha
Complete para acrescentar "Total" e pular a linha:
3. A string final
Complete para guardar o texto do sb numa string:
4. Depois de var sb = new StringBuilder("oi"); sb.Append("!");, o que sb.ToString() devolve?
5. Em qual caso o StringBuilder faz mais diferença?
6. Por que string s = sb; não compila?
Missão final: a tabuada
Missão: o laço for já repete o bloco 5 vezes, com i valendo 1, 2, 3, 4 e 5 (você vai estudá-lo em for). Dentro dele, use AppendLine com interpolação para montar a tabuada do 7, de 7 x 1 = 7 até 7 x 5 = 35. Calcule o resultado com 7 * i dentro das chaves.
7 x 1 = 7 7 x 2 = 14 7 x 3 = 21 7 x 4 = 28 7 x 5 = 35
using System.Text;
var tabuada = new StringBuilder();
for (int i = 1; i <= 5; i++)
{
// acrescente aqui uma linha da tabuada
}
Console.Write(tabuada.ToString());
using System.Text;
var tabuada = new StringBuilder();
for (int i = 1; i <= 5; i++)
{
tabuada.AppendLine($"7 x {i} = {7 * i}");
}
Console.Write(tabuada.ToString());
Repare no Console.Write (sem Line): o último AppendLine já pôs a quebra de linha no fim. Depois, troque o 5 por 10 e o 7 por outro número.
Resumo
- Strings são imutáveis:
+=num laço copia o texto todo a cada volta. OStringBuilderé um texto que pode mudar. - Precisa de
using System.Text;(senão,CS0246). Crie comnew StringBuilder(). Appendacrescenta no fim;AppendLineacrescenta e quebra a linha. Eles mudam o próprioStringBuildere podem ser encadeados.sb[i] = 'c',Insert,Replace,RemoveeClearmudam o texto no lugar.ToString()cria a string final.string s = sb;dáCS0029;sb == "oi"dáCS0019.- Use em laços e textos grandes. Para poucos pedaços,
+ou$"…"; para listas,string.Join.