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.

Você vai aprender
  • Por que += repetido muitas vezes é lento, e como o StringBuilder resolve.
  • Criar um StringBuilder (com using System.Text;) e acrescentar texto com Append e AppendLine.
  • 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 StringBuilder mora 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 é o CS0246.
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 escrever sb = sb.Append(…). Aceita strings, char ('!'), números e bool.
sb.ToString()
Tira a "foto": cria uma string comum 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 Length da 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.

A capacidade

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çãoUsePor 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 separadorstring.Join(", ", itens)Uma linha, e já é rápido (aula Métodos de String).
Montar um texto dentro de um laçoStringBuilderEvita copiar o texto a cada volta.
Montar um texto grande em várias etapas (relatório, nota fiscal, HTML)StringBuilderDá 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.

Escrevi 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ódigoO 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.Lengthquantidade 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:

using ;

2. Acrescentar com quebra de linha

Complete para acrescentar "Total" e pular a linha:

sb.("Total");

3. A string final

Complete para guardar o texto do sb numa string:

string texto = sb.;

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. O StringBuilder é um texto que pode mudar.
  • Precisa de using System.Text; (senão, CS0246). Crie com new StringBuilder().
  • Append acrescenta no fim; AppendLine acrescenta e quebra a linha. Eles mudam o próprio StringBuilder e podem ser encadeados.
  • sb[i] = 'c', Insert, Replace, Remove e Clear mudam 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.