Manipulação de Strings em C# e .NET: O guia completo

A manipulação de strings é uma das tarefas mais frequentes no dia a dia de quem programa em C#, porque quase toda aplicação lê, valida, formata ou transforma texto. Neste guia, você vai conhecer as técnicas essenciais e as boas práticas para trabalhar com o tipo string no .NET, desde as operações básicas até cuidados de performance.

O que você vai aprender

Seja em um pequeno utilitário de linha de comando, seja em uma aplicação corporativa, o texto aparece em todo lugar. Por isso, vale entender como a plataforma trata esse tipo antes de sair escrevendo código.

Ao longo do artigo, passamos pelos seguintes tópicos:

  1. Noções básicas sobre strings em C#
  2. Imutabilidade
  3. Concatenação e interpolação
  4. Comparação
  5. Substrings e pesquisa
  6. Substituição e remoção de caracteres
  7. Divisão e junção
  8. StringBuilder
  9. Expressões regulares
  10. Codificação e decodificação
  11. Considerações de performance

Os exemplos usam a versão LTS atual da plataforma, o .NET 10 com C# 14, mas quase tudo funciona em versões anteriores. Quando algum recurso depender de uma versão específica, o texto avisa.

Noções básicas sobre strings em C#

Uma string em C# é uma sequência de caracteres representada pela classe System.String. Além disso, a palavra-chave string é apenas um alias para essa classe, então as duas formas são equivalentes.

Internamente, cada caractere é um char em UTF-16. Dessa forma, a propriedade Length retorna a quantidade de unidades de código UTF-16, e não necessariamente a quantidade de letras que o usuário enxerga na tela.

string greeting = "Hello, World!";
Console.WriteLine(greeting);        // Saída: Hello, World!
Console.WriteLine(greeting.Length); // Saída: 13

Embora seja um tipo de referência, a string se comporta como um valor no uso cotidiano. Por exemplo, o operador == compara o conteúdo, e não o endereço de memória dos objetos.

Para textos com aspas, barras invertidas ou várias linhas, o C# 11 trouxe os raw string literals, delimitados por três aspas. Na prática, eles eliminam as sequências de escape e deixam JSON, SQL e padrões de Regex muito mais legíveis.

string json = """
    {
        "name": "John",
        "active": true
    }
    """;

Imutabilidade na manipulação de strings

O conceito mais importante da manipulação de strings no .NET é a imutabilidade. Uma vez criada, uma string não pode ter seus caracteres alterados.

Assim, qualquer operação que parece modificar o texto, como Replace, Trim ou ToUpperInvariant, na verdade devolve uma nova instância. O valor original continua exatamente igual.

string original = "Hello";
string modified = original + ", World!";

Console.WriteLine(original); // Saída: Hello
Console.WriteLine(modified); // Saída: Hello, World!

Essa característica traz vantagens claras. Como ninguém altera o conteúdo, você pode compartilhar a mesma string entre métodos e threads sem nenhum risco de efeito colateral.

Por outro lado, cada nova instância exige uma alocação na memória. Por isso, a imutabilidade tem impacto direto na performance quando você faz muitas alterações seguidas, assunto que aparece mais adiante.

Se quiser se aprofundar nesse tema, o artigo sobre strings imutáveis em C# detalha o comportamento da memória e os motivos dessa decisão de design.

Concatenação e interpolação

Concatenar é juntar duas ou mais strings em uma só. Para isso, o C# oferece o operador +, o método string.Concat e a interpolação.

Concatenação com o operador + e String.Concat

O operador + é a forma mais direta de juntar valores. Já o método string.Concat faz o mesmo trabalho, mas recebe as partes como argumentos.

string firstName = "John";
string lastName = "Smith";

string fullName = firstName + " " + lastName;
Console.WriteLine(fullName); // Saída: John Smith

// Usando String.Concat
string fullNameConcat = string.Concat(firstName, " ", lastName);
Console.WriteLine(fullNameConcat); // Saída: John Smith

Quando todas as partes estão em uma única expressão, o compilador já converte o + em uma chamada a string.Concat. Portanto, nesse caso, as duas formas têm o mesmo custo.

Interpolação na manipulação de strings

A interpolação é a forma moderna e legível de compor textos a partir de valores. Basta prefixar a string com $ e colocar as expressões entre chaves.

string name = "Anna";
int age = 25;

string message = $"My name is {name} and I am {age} years old.";
Console.WriteLine(message); // Saída: My name is Anna and I am 25 years old.

decimal price = 1234.5m;
Console.WriteLine($"Price: {price:N2}"); // Saída depende da cultura atual

Dentro das chaves, você também pode aplicar especificadores de formato, como N2 ou yyyy-MM-dd. Desde o C# 10, o compilador usa interpolated string handlers, o que reduz alocações intermediárias na manipulação de strings interpoladas.

Comparação de strings

Comparar textos parece simples, mas é uma das fontes mais comuns de bugs. A diferença entre maiúsculas, minúsculas e acentos depende da regra de comparação escolhida.

O operador == e o método Equals sem parâmetros fazem uma comparação ordinal, ou seja, byte a byte. Então, "hello" e "Hello" são diferentes.

string first = "hello";
string second = "Hello";

// Comparação sensível a maiúsculas
bool areEqual = first == second;
Console.WriteLine(areEqual); // Saída: False

// Comparação insensível a maiúsculas
bool areEqualIgnoreCase = string.Equals(first, second, StringComparison.OrdinalIgnoreCase);
Console.WriteLine(areEqualIgnoreCase); // Saída: True

A documentação oficial de boas práticas para comparar strings no .NET traz uma recomendação clara: sempre use uma sobrecarga que receba um StringComparison. Dessa forma, a intenção fica explícita no código.

Para identificadores, chaves, nomes de arquivos e protocolos, prefira Ordinal ou OrdinalIgnoreCase, que também são as opções mais rápidas. Por sua vez, CurrentCulture serve para ordenar ou exibir dados ao usuário, respeitando as regras do idioma.

Um cuidado importante envolve métodos como IndexOf(string) e StartsWith(string). Sem o parâmetro de comparação, eles usam a cultura atual, enquanto Contains e Equals usam a regra ordinal.

Esse comportamento inconsistente pode gerar diferenças entre máquinas com idiomas distintos. Por exemplo, em turco, a letra "i" maiúscula não vira "I", o que já causou falhas de segurança em comparações de URIs.

Substrings e pesquisa

Extrair e localizar trechos de texto é outra parte central da manipulação de strings. Para isso, você usa Substring, IndexOf, Contains, StartsWith e EndsWith.

string text = "C# is amazing!";

// Extrair um trecho a partir do índice 6, com 7 caracteres
string excerpt = text.Substring(6, 7);
Console.WriteLine(excerpt); // Saída: amazing

// Verificar se existe um trecho
bool contains = text.Contains("amazing", StringComparison.Ordinal);
Console.WriteLine(contains); // Saída: True

// Encontrar a posição do trecho
int position = text.IndexOf("amazing", StringComparison.Ordinal);
Console.WriteLine(position); // Saída: 6

O método Substring recebe o índice inicial e, opcionalmente, a quantidade de caracteres. Se os valores ultrapassarem o tamanho do texto, ele lança uma ArgumentOutOfRangeException.

Como alternativa, o C# 8 introduziu os ranges, que deixam o código mais curto. Assim, text[6..13] retorna o mesmo resultado do exemplo acima.

Quando o IndexOf não encontra o trecho, ele retorna -1. Por isso, sempre valide o retorno antes de usar o índice em outra operação.

Existe também o LastIndexOf, que procura a última ocorrência em vez da primeira. Ele é útil, por exemplo, para separar a extensão de um nome de arquivo a partir do último ponto.

Já StartsWith e EndsWith verificam apenas o começo ou o fim do texto. Assim, a manipulação de strings com prefixos de rotas, códigos de produtos ou extensões dispensa o cálculo manual de índices.

Substituindo e removendo caracteres

Os métodos Replace e Remove modificam o conteúdo, sempre devolvendo uma nova string. Além deles, a família Trim remove espaços ou caracteres das extremidades.

string sentence = "I love programming in C#.";

// Substituir um trecho por outro
string updated = sentence.Replace("C#", "JavaScript");
Console.WriteLine(updated); // Saída: I love programming in JavaScript.

// Remover 5 caracteres a partir do índice 2
string shortened = sentence.Remove(2, 5);
Console.WriteLine(shortened); // Saída: I programming in C#.

// Remover espaços das extremidades
string input = "   balta.io   ";
Console.WriteLine(input.Trim()); // Saída: balta.io

O Replace substitui todas as ocorrências encontradas, não apenas a primeira. Além disso, desde o .NET Core 2.0, existe uma sobrecarga que aceita StringComparison para ignorar maiúsculas na busca.

Já o Remove trabalha com posições. Então, ele recebe o índice inicial e a quantidade de caracteres que devem sair do texto.

Vale lembrar que nenhum desses métodos altera a variável original. Portanto, na manipulação de strings, você sempre precisa atribuir o resultado a uma variável para aproveitar a mudança.

Maiúsculas, espaços e preenchimento

Outra tarefa frequente é padronizar o formato do texto antes de salvar ou exibir. Para isso, a classe String oferece métodos como ToUpperInvariant, ToLowerInvariant, PadLeft e PadRight.

string code = "abc-123";

// Converter sem depender da cultura atual
Console.WriteLine(code.ToUpperInvariant()); // Saída: ABC-123

// Completar à esquerda com zeros até 6 caracteres
string invoiceNumber = "42".PadLeft(6, '0');
Console.WriteLine(invoiceNumber); // Saída: 000042

// Verificar se o valor está nulo, vazio ou só com espaços
string? userInput = "   ";
Console.WriteLine(string.IsNullOrWhiteSpace(userInput)); // Saída: True

Os métodos com o sufixo Invariant ignoram o idioma da máquina. Por isso, eles são a escolha mais segura para códigos, chaves e identificadores, enquanto ToUpper sem parâmetros usa a cultura atual.

Já o PadLeft e o PadRight são úteis para gerar números de documentos ou alinhar colunas em relatórios de texto. Em ambos os casos, o método recebe o tamanho total desejado e o caractere de preenchimento.

Por fim, string.IsNullOrWhiteSpace resume em uma única chamada três verificações comuns na validação de formulários. Com isso, a manipulação de strings vindas do usuário fica mais curta e menos sujeita a esquecimentos.

Manipulação de strings com Split e Join

Dividir e unir textos é muito comum ao processar arquivos CSV, parâmetros de linha de comando ou listas digitadas pelo usuário. Nesse caso, os métodos Split e string.Join resolvem a maior parte dos cenários.

string data = "apple,banana,orange";

// Dividir pelo separador
string[] fruits = data.Split(',');
foreach (var fruit in fruits)
{
    Console.WriteLine(fruit);
}

// Unir com outro separador
string joined = string.Join(" | ", fruits);
Console.WriteLine(joined); // Saída: apple | banana | orange

O Split também aceita opções úteis. Por exemplo, StringSplitOptions.RemoveEmptyEntries descarta itens vazios, enquanto StringSplitOptions.TrimEntries remove espaços de cada parte.

string tags = " csharp, dotnet , ,azure ";
string[] cleanTags = tags.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
// Resultado: ["csharp", "dotnet", "azure"]

Com essa combinação, você evita um laço extra só para limpar os valores. Na prática, isso deixa a manipulação de strings vindas de entradas externas mais segura e concisa.

StringBuilder na manipulação de strings

Para operações intensivas, a classe StringBuilder, do namespace System.Text, é a ferramenta certa. Ao contrário da string, ela mantém um buffer mutável e cresce conforme a necessidade.

using System.Text;

var builder = new StringBuilder();
builder.Append("Hello");
builder.Append(", ");
builder.Append("World!");

Console.WriteLine(builder.ToString()); // Saída: Hello, World!

A grande vantagem aparece em laços. Enquanto o operador += cria uma string nova a cada volta, o StringBuilder acrescenta o conteúdo no mesmo buffer e só gera a string final no ToString.

var report = new StringBuilder(capacity: 256);

for (int i = 1; i <= 3; i++)
{
    // Append encadeado, sem strings intermediárias
    report.Append("item ").Append(i).Append(';');
}

Console.WriteLine(report); // Saída: item 1;item 2;item 3;

Informar a capacidade inicial também ajuda, porque evita realocações do buffer interno. Além disso, o StringBuilder permite alterar caracteres individuais por índice, algo impossível com a string comum.

O artigo string, System.String e StringBuilder compara essas opções em mais detalhes, caso você queira revisar a diferença entre os tipos.

Expressões regulares

As expressões regulares são poderosas para encontrar padrões, validar formatos e extrair informações de textos. No .NET, elas ficam no namespace System.Text.RegularExpressions.

using System.Text.RegularExpressions;

string input = "Contact: 123-456-7890";
string pattern = @"\d{3}-\d{3}-\d{4}";

Match match = Regex.Match(input, pattern);
if (match.Success)
{
    Console.WriteLine(match.Value); // Saída: 123-456-7890
}

Repare no @ antes do padrão. Sem ele, o compilador interpretaria \d como uma sequência de escape inválida, o que é um erro comum em exemplos de Regex.

Desde o .NET 7, você também pode usar o atributo GeneratedRegex. Com ele, um source generator cria o código da expressão em tempo de compilação, o que melhora o desempenho e facilita a depuração.

using System.Text.RegularExpressions;

public static partial class PhoneValidator
{
    // O source generator cria a implementação em tempo de compilação
    [GeneratedRegex(@"^\d{3}-\d{3}-\d{4}$")]
    private static partial Regex PhoneRegex();

    public static bool IsValid(string phone) => PhoneRegex().IsMatch(phone);
}

Para organizar e reaproveitar padrões em projetos maiores, vale ler o artigo sobre como criar, organizar e reutilizar expressões regulares em C#.

Codificação e decodificação de strings

Muitas vezes você precisa transformar texto em bytes, seja para gravar em arquivos, enviar pela rede ou calcular um hash. Para isso, a classe Encoding converte nos dois sentidos.

using System.Text;

string text = "Olá, Mundo!";

// Codificar em bytes UTF-8
byte[] bytes = Encoding.UTF8.GetBytes(text);
Console.WriteLine(bytes.Length); // Saída: 13

// Decodificar de volta para string
string decoded = Encoding.UTF8.GetString(bytes);
Console.WriteLine(decoded); // Saída: Olá, Mundo!

Repare que o texto tem 11 caracteres, mas gera 13 bytes. Isso acontece porque, em UTF-8, letras acentuadas como "á" ocupam dois bytes.

Por isso, sempre use a mesma codificação na ida e na volta. Caso contrário, os acentos viram caracteres estranhos, um problema clássico de integrações entre sistemas.

Performance na manipulação de strings

Como a string é imutável, a manipulação de strings em excesso pode gerar muitas alocações e pressionar o garbage collector. No entanto, a própria documentação lembra que essas operações são bem otimizadas e só pesam em laços com milhares de repetições.

Algumas regras práticas ajudam a manter o código rápido:

  • Use StringBuilder para modificações frequentes, especialmente dentro de laços.
  • Evite += em laços longos, porque cada volta cria uma nova string.
  • Prefira string.Concat ou string.Join quando já tiver todas as partes em uma coleção.
  • Use StringComparison.Ordinal ou OrdinalIgnoreCase em comparações que não dependem de idioma.
  • Prefira string.IsNullOrWhiteSpace em vez de combinar Trim com comparação de texto vazio.

Em cenários de alta performance, ReadOnlySpan<char> permite fatiar textos sem alocar novas strings. Por exemplo, text.AsSpan(6, 7) aponta para o mesmo trecho da memória original.

Além disso, o .NET 8 trouxe o tipo SearchValues<T>, que otimiza buscas repetidas por um conjunto fixo de caracteres ou palavras. Contudo, essas técnicas só valem a pena quando um profiler mostrar que a manipulação de strings é, de fato, o gargalo.

Boas práticas para manipulação de strings

Depois de conhecer as ferramentas, vale reunir os hábitos que evitam bugs no dia a dia. Primeiro, trate valores nulos explicitamente, porque chamar um método em uma string nula lança NullReferenceException.

Em seguida, deixe a regra de comparação sempre visível. Com isso, outra pessoa entende de imediato se a intenção era respeitar ou ignorar maiúsculas.

Também prefira interpolação para montar mensagens e raw string literals para blocos grandes de texto. Ambos deixam a manipulação de strings mais legível do que uma sequência de +.

Por fim, não otimize antes da hora. Comece com o código mais claro e só troque por StringBuilder ou Span quando houver necessidade comprovada.

Conclusão

Dominar a manipulação de strings em C# é essencial para escrever código eficiente e fácil de manter. Afinal, quase toda aplicação depende de texto em algum ponto do fluxo.

Neste guia, você viu como a imutabilidade influencia cada operação e quando usar concatenação, interpolação, Split, Join, Replace e Substring. Além disso, conheceu o StringBuilder, as expressões regulares e a classe Encoding.

Agora, explore esses métodos nos seus projetos e escolha a ferramenta certa para cada caso de uso. Assim, a manipulação de strings deixa de ser um ponto de dúvida e passa a ser uma habilidade sólida no seu repertório.

FAQ

Por que a string é imutável no C#?

A imutabilidade garante segurança no compartilhamento entre métodos e threads. Além disso, ela permite otimizações como o string interning, que reaproveita instâncias com o mesmo conteúdo.

Quando devo usar StringBuilder em vez de concatenação?

Use StringBuilder quando houver muitas modificações seguidas, principalmente em laços. Para poucas partes em uma única expressão, a interpolação ou o operador + são suficientes.

Qual a diferença entre Ordinal e CurrentCulture?

Ordinal compara os valores binários dos caracteres e é mais rápido. Já CurrentCulture aplica as regras do idioma atual e serve para exibir ou ordenar dados ao usuário.

O operador == compara o conteúdo das strings?

Sim. Em strings, o operador == compara o conteúdo usando a regra ordinal, e não a referência dos objetos.

Como evitar problemas com acentos ao converter strings em bytes?

Use a mesma codificação, como Encoding.UTF8, tanto para codificar quanto para decodificar. Misturar codificações diferentes corrompe os caracteres acentuados.