Strings imutáveis em C#: Por que strings não podem ser alteradas
Strings imutáveis são strings cujo conteúdo não muda depois da criação, e no C# todo objeto string funciona assim. Você concatena, substitui caracteres e manipula texto de várias maneiras, mas cada operação cria uma nova string em vez de alterar a existente.
Esse comportamento confunde muita gente no início. Afinal, por que um tipo tão usado se comporta de forma diferente dos outros objetos que você cria no dia a dia?
Neste artigo, você vai entender o que significa uma string ser imutável, por que o .NET tomou essa decisão de design e como ela impacta a forma de escrever código. Além disso, vai ver as ferramentas certas para manipular texto sem desperdiçar memória.
O que são strings imutáveis
Em programação, imutabilidade significa que um objeto não pode ser alterado depois de criado. No caso das strings em C#, isso tem duas consequências diretas.
Primeiro, você não consegue modificar o conteúdo de uma string após a criação. Segundo, qualquer operação que pareça modificar uma string, na verdade, devolve uma nova instância.
Veja um exemplo simples:
string original = "Hello";
string modified = original.Replace("H", "J");
Console.WriteLine(original); // Saída: Hello
Console.WriteLine(modified); // Saída: Jello
Note que original permanece igual mesmo após a chamada ao método Replace. Isso acontece porque o método criou uma nova instância de string e a atribuiu a modified.
A documentação oficial sobre strings no C# reforça esse ponto. Métodos como ToUpperInvariant, Replace, Substring e Trim sempre retornam uma nova string, enquanto a instância original continua a mesma.
Strings imutáveis e tipos de referência
Um detalhe que surpreende muitos desenvolvedores é que string é um tipo de referência. Ou seja, a variável guarda uma referência para um objeto na heap, assim como acontece com qualquer classe.
Mesmo assim, as strings imutáveis se comportam como tipos de valor no uso diário. Por exemplo, os operadores == e != comparam o conteúdo das strings, e não as referências:
string a = "hello";
string b = "h";
// Concatena e gera uma nova instância em b
b += "ello";
Console.WriteLine(a == b); // True
Console.WriteLine(object.ReferenceEquals(a, b)); // False
O conteúdo das duas variáveis é igual, por isso a comparação com == retorna True. No entanto, a e b apontam para instâncias diferentes, então ReferenceEquals retorna False.
string e String são o mesmo tipo
A palavra-chave string é apenas um alias para o tipo System.String. Por isso, as duas formas compilam para a mesma IL e têm exatamente o mesmo comportamento imutável.
// A palavra-chave string é um alias para System.String
string a = "hello";
String b = "hello";
Console.WriteLine(a == b); // True
Console.WriteLine(typeof(string) == typeof(String)); // True
Ainda assim, a documentação recomenda preferir a palavra-chave string no seu código. Ela segue o padrão dos outros tipos nativos, como int e bool, e funciona sem a diretiva using System;.
O que acontece com as referências
Como toda "modificação" cria um novo objeto, é preciso atenção ao trabalhar com várias referências para a mesma string. Observe:
string first = "Hello ";
string second = first;
// first passa a apontar para uma nova string
first += "World";
Console.WriteLine(second); // Saída: Hello
Antes da concatenação, as duas variáveis apontavam para o mesmo objeto. Depois dela, first passou a apontar para uma string nova, enquanto second continuou com a original.
Além disso, a string antiga que só first referenciava fica disponível para o Garbage Collector assim que nenhuma variável aponta mais para ela. Se você quer entender esse processo em detalhes, vale ler sobre o que é e como funciona o Garbage Collector.
Strings imutáveis em outras linguagens
O C# não está sozinho nessa escolha. Segundo a documentação da Microsoft, Java e JavaScript também trabalham com strings imutáveis, e métodos como Replace e Trim retornam um novo valor nessas linguagens.
Além disso, a string do C# usa UTF-16 como codificação em memória, de forma parecida com a String do Java. Por outro lado, ela difere bastante das strings de C e C++, porque não termina com um caractere nulo e não se converte em ponteiro.
Portanto, se você já programou em Java ou JavaScript, o modelo mental é praticamente o mesmo. Já quem vem de C ou C++ precisa se acostumar com a ideia de que alterar texto sempre significa criar um novo objeto.
Por que o C# usa strings imutáveis
A imutabilidade não é um acidente nem uma limitação técnica. Na verdade, ela é uma decisão de design que traz benefícios concretos em memória, concorrência e previsibilidade.
Eficiência de memória com o interning
O runtime do .NET usa uma técnica chamada interning de strings para otimizar o uso de memória. Quando dois literais têm o mesmo valor, eles apontam para a mesma posição na memória.
string first = "CSharp";
string second = "CSharp";
Console.WriteLine(object.ReferenceEquals(first, second));
// Saída: True
Esse compartilhamento só é seguro porque estamos falando de strings imutáveis. Afinal, se fosse possível alterar uma delas, a mudança afetaria todas as outras variáveis que compartilham a mesma instância.
Você também pode usar os métodos string.Intern e string.IsInterned para trabalhar com o pool de strings em tempo de execução. O artigo sobre string interning no .NET aprofunda esse mecanismo e mostra quando ele vale a pena.
Segurança em threads
Como nenhuma thread consegue alterar o conteúdo de uma string, as strings imutáveis são naturalmente seguras para uso concorrente. Assim, várias threads podem ler a mesma instância sem locks e sem risco de ver dados pela metade.
string shared = "Immutable";
Parallel.For(0, 10, _ =>
{
// Todas as threads leem a mesma instância com segurança
Console.WriteLine(shared);
});
Em um tipo mutável, esse mesmo código exigiria sincronização. Com strings, por outro lado, você compartilha o valor livremente entre tarefas, filas e caches.
Previsibilidade e simplicidade
Manter as strings imutáveis também facilita prever o comportamento do código. Com isso, você evita efeitos colaterais quando passa uma string entre métodos ou a guarda em coleções.
void UpdateText(string input)
{
// Apenas a variável local passa a apontar para outra string
input = "Updated";
}
string message = "Original";
UpdateText(message);
Console.WriteLine(message); // Saída: Original
Nesse exemplo, o método apenas troca a referência da variável local input. Portanto, a string original permanece igual depois da chamada.
Essa previsibilidade vale também para coleções. Como o conteúdo de uma string nunca muda, o hash calculado a partir dele também não muda, e isso torna as strings chaves confiáveis em um Dictionary<string, T> ou em um HashSet<string>.
O custo das strings imutáveis
Toda decisão de design tem um preço. No caso das strings imutáveis, o custo aparece quando você faz muitas alterações seguidas no mesmo texto.
Cada + ou += cria uma nova string, e a anterior vira lixo para o Garbage Collector. Em um laço com poucas iterações, isso quase não faz diferença.
Contudo, em um laço com milhares de iterações, o código passa a alocar milhares de objetos intermediários. Como cada nova string copia todo o conteúdo anterior, o trabalho cresce rapidamente com o tamanho do texto.
// Ineficiente: cada iteração cria uma nova string
string result = "";
for (int i = 0; i < 10; i++)
{
result += i + ", ";
}
Vale lembrar, no entanto, que esse custo não é um defeito das strings imutáveis em si. Ele surge do uso de uma ferramenta pensada para valores estáveis em um cenário de alterações constantes.
Na prática, a maior parte do código de negócio monta poucas strings por requisição, e nesses casos a diferença nem aparece. O problema surge em laços longos, geração de relatórios, logs muito verbosos e processamento de arquivos grandes.
Por isso, conhecer as alternativas certas é tão importante quanto entender o conceito. A seguir, você vai ver como trabalhar com strings imutáveis de forma eficiente.
Como trabalhar bem com strings imutáveis
A imutabilidade não precisa custar performance. Na prática, o .NET oferece ferramentas pensadas justamente para os cenários em que você precisa montar ou transformar muito texto.
Use StringBuilder para modificações frequentes
Ao trabalhar com texto dinâmico, use o StringBuilder. Diferente da string, ele é mutável e foi otimizado para modificações frequentes.
using System.Text;
var builder = new StringBuilder("Hello");
builder.Append(" World");
builder.Replace("World", "CSharp");
Console.WriteLine(builder.ToString());
// Saída: Hello CSharp
O StringBuilder escreve em buffers internos e só gera a string final na chamada a ToString(). Dessa forma, o laço do exemplo anterior fica muito mais eficiente:
using System.Text;
// Eficiente: o texto cresce em um buffer interno
var builder = new StringBuilder();
for (int i = 0; i < 10; i++)
{
builder.Append(i).Append(", ");
}
string result = builder.ToString();
Se você quer comparar os dois tipos lado a lado, o artigo sobre string, System.String e StringBuilder mostra as diferenças com mais exemplos. Além disso, quem já está no .NET 11 pode conhecer o novo StringBuilder.MoveChunks, que transfere o conteúdo sem copiar caracteres.
Use interpolação para poucas partes
Para montar uma string a partir de poucas partes conhecidas, a interpolação é a opção mais legível. Ela também gera um único resultado, sem as strings intermediárias de uma sequência de +=.
string name = "Maria";
int orders = 3;
// Interpolação monta o texto final de uma vez
string message = $"{name} has {orders} pending orders.";
Console.WriteLine(message); // Saída: Maria has 3 pending orders.
Desde o C# 10, o compilador transforma a interpolação em chamadas a um manipulador de strings interpoladas. Com isso, o código evita boa parte das alocações que versões antigas faziam ao formatar cada valor.
Prefira string.Join e string.Concat para coleções
Quando o texto vem de uma coleção, o caminho mais simples costuma ser string.Join ou string.Concat. Esses métodos calculam o tamanho final e montam o resultado de uma só vez.
string[] words = ["The", "quick", "brown", "fox"];
// Junta os elementos sem separador
string together = string.Concat(words);
// Junta os elementos com um espaço entre eles
string sentence = string.Join(' ', words);
Console.WriteLine(sentence); // Saída: The quick brown fox
Segundo a documentação, string.Join é a ferramenta certa sempre que você precisa de uma saída com delimitador, como valores separados por vírgula.
Use string.Create para montar strings sem buffers extras
Em código de alta performance, o método string.Create permite preencher os caracteres de uma nova string por meio de um callback. Esse callback recebe um Span<char> gravável e um objeto de estado.
// Cria uma string de 5 caracteres a partir da letra 'a'
string result = string.Create(5, 'a', (span, firstChar) =>
{
for (int i = 0; i < span.Length; i++)
{
span[i] = (char)(firstChar + i);
}
});
Console.WriteLine(result); // Saída: abcde
Assim, você monta o conteúdo direto na memória da nova string, sem alocar buffers intermediários. Depois que o método retorna, a string volta a ser imutável como qualquer outra.
Leia trechos com ReadOnlySpan
Nem toda operação precisa gerar uma nova string. Quando você só quer ler um trecho, o método AsSpan devolve um ReadOnlySpan<char> que aponta para os caracteres originais.
string fullName = "John Smith";
// Nenhuma nova string é criada aqui
ReadOnlySpan<char> firstName = fullName.AsSpan(0, 4);
Console.WriteLine(firstName.ToString()); // Saída: John
Como o span é somente leitura, ele respeita a imutabilidade da string. Ao mesmo tempo, ele evita a alocação que um Substring faria.
Aproveite as strings imutáveis a seu favor
Além de entender o custo, vale usar a imutabilidade como aliada. Ela protege o seu código contra alterações que ninguém pretendia fazer.
using System.Security.Cryptography;
using System.Text;
string password = "SecurePassword";
// O hash gera um novo valor e não altera a senha original
string hashedPassword = HashPassword(password);
Console.WriteLine(password); // Saída: SecurePassword
static string HashPassword(string value)
{
byte[] hash = SHA256.HashData(Encoding.UTF8.GetBytes(value));
return Convert.ToHexString(hash);
}
Nesse exemplo, nenhum método chamado consegue alterar a variável password por dentro. Portanto, você não precisa criar cópias defensivas antes de repassar o valor.
O exemplo serve apenas para ilustrar a imutabilidade. Em um sistema real, armazene senhas com algoritmos próprios para isso, que usam salt e várias iterações.
Leve a ideia das strings imutáveis para os seus tipos
A própria documentação do System.Text.Json cita o System.String como exemplo de tipo imutável. Ou seja, o mesmo princípio que torna as strings imutáveis seguras pode ser aplicado às classes que você cria.
No C#, você consegue isso com records, propriedades somente leitura ou setters init. Dessa forma, o objeto recebe os valores na criação e não muda mais depois disso.
// Record posicional: as propriedades são init-only
public record Customer(string Name, string Email);
var customer = new Customer("Maria", "maria@example.com");
// Para "alterar", cria-se uma nova instância com with
var updated = customer with { Email = "maria@newdomain.com" };
Repare que a expressão with funciona exatamente como os métodos da string. Ela devolve uma nova instância e mantém a original intacta.
Nunca altere strings com código unsafe
Em tese, é possível escrever diretamente na memória de uma string usando ponteiros em um bloco unsafe. No entanto, as boas práticas de código unsafe da Microsoft classificam isso como comportamento indefinido.
O motivo está no interning. Se você alterar um literal internado, a mudança aparece em todos os outros lugares que usam o mesmo valor.
// Exemplo do que NÃO fazer
string text = "Hello";
unsafe
{
fixed (char* pointer = text)
{
pointer[0] = '_';
}
}
// Pode imprimir "_ello", porque o literal é compartilhado
Console.WriteLine("Hello");
Em vez disso, a recomendação oficial é usar string.Create quando a lógica de cópia é complexa. Para os demais casos, use ToString(), StringBuilder, new string(...) ou interpolação.
Em outras palavras, as strings imutáveis são um contrato do runtime. Quebrar esse contrato pode gerar bugs difíceis de rastrear em partes do sistema que não têm relação com o código alterado.
Boas práticas com strings imutáveis
Para fechar, vale reunir as recomendações em uma lista curta e fácil de consultar:
- Para montar poucas strings de uma vez,
+e interpolação resolvem bem. - Em laços ou quando o número de partes é grande ou desconhecido, prefira
StringBuilder. - Para juntar coleções, use
string.Joinestring.Concat. - Em código sensível a performance, considere
string.CreateeReadOnlySpan<char>. - Compare strings com
==oustring.Equals, e não comReferenceEquals. - Nunca altere o conteúdo de uma string com ponteiros.
Se você quer ir além e dominar o restante da API de texto do .NET, o guia completo de manipulação de strings em C# cobre formatação, comparação, busca e muito mais.
Conclusão
Entender as strings imutáveis em C# é essencial para escrever código eficiente, previsível e seguro em ambientes com várias threads.
Embora a imutabilidade pareça limitante a princípio, ela oferece vantagens significativas, especialmente em concorrência e em otimização de memória com o interning.
Ao usar ferramentas como StringBuilder, string.Join, string.Create e spans, você aproveita esses benefícios sem pagar o custo das alocações excessivas.
Além disso, o mesmo raciocínio das strings imutáveis vale para os seus próprios tipos, com records e propriedades init.
Por fim, projetar o seu código com a imutabilidade em mente ajuda a maximizar tanto o desempenho quanto a manutenibilidade.
FAQ
Por que as strings são imutáveis em C#?
Porque a imutabilidade permite compartilhar instâncias com segurança, tanto pelo interning quanto entre threads. Além disso, ela evita efeitos colaterais quando você passa strings entre métodos.
Se a string é imutável, como o operador += funciona?
O operador += cria uma nova string com o conteúdo combinado e atribui essa nova instância à variável. A string antiga continua igual e fica disponível para o Garbage Collector quando ninguém mais a referencia.
String é tipo de valor ou tipo de referência?
A string é um tipo de referência. Mesmo assim, os operadores == e != comparam o conteúdo, por isso ela parece um tipo de valor no uso diário.
Quando devo usar StringBuilder em vez de concatenação?
Use StringBuilder quando montar texto em laços ou quando o número de partes for grande ou desconhecido. Para juntar poucas partes de uma vez, a concatenação ou a interpolação continuam adequadas.
É possível alterar uma string existente?
Somente com código unsafe, e a Microsoft classifica isso como comportamento indefinido. Para montar strings de forma eficiente, use string.Create ou StringBuilder.