3 coisas que você deve saber sobre Strings em C#/.NET

Autor(a):

No .NET, strings são imutáveis. Isso significa que uma vez que um objeto string é criado, ele não pode ser modificado.

Se você precisa alterar uma string adicionando outra string a ela, o .NET na verdade não anexa a nova string à string existente.

Em vez disso, ele cria um novo objeto string que contém a combinação das duas strings e então descarta a string antiga.

Esse comportamento é eficiente e seguro para pequenas ou algumas manipulações, mas se torna um problema de desempenho e memória quando feito repetidamente, como em um loop.

Como o StringBuilder pode ajudar aqui?

O StringBuilder é um objeto dinâmico que permite expandir o número de caracteres na string que ele contém sem criar um novo objeto para cada concatenação.

Por baixo dos panos, o StringBuilder mantém um array de caracteres.

Quando você anexa uma string a uma instância de StringBuilder, ele simplesmente copia os caracteres adicionados para o final do array interno.

Se o array ficar sem espaço, o StringBuilder aloca automaticamente um novo array maior e copia os caracteres nele.

Isso acontece com muito menos frequência do que a imutabilidade de strings forçaria, tornando o StringBuilder muito mais eficiente para operações de concatenação, especialmente em loops.

Veja o exemplo abaixo:

// Usando concatenação com string
string result = "";
for (int i = 0; i < 1000; i++)
{
    result += "a"; // Cria um novo objeto de string a cada iteração do loop;
}

// Usando StringBuilder
var builder = new StringBuilder();
for (int i = 0; i < 1000; i++)
{
    builder.Append("a"); // Anexa ao array de caracteres existente
}
string result = builder.ToString(); // Converte pra string após finalizar o loop;

O .NET oferece várias maneiras de comparar strings, incluindo verificações simples de igualdade (==), string.Equals, string.Compare e métodos como string.StartsWith ou string.Contains.

Cada um desses métodos pode opcionalmente receber uma enumeração StringComparison como parâmetro, que especifica como a comparação deve ser realizada.

As opções de StringComparison incluem:

Comparação ordinal (Ordinal, OrdinalIgnoreCase): Essas comparações são baseadas nos valores binários dos caracteres nas strings e são o tipo mais rápido de comparação. Elas são insensíveis à cultura, tornando-as ideais para comparar strings para processamento interno, caminhos de arquivos, strings legíveis por máquina (como tags XML) e quando o desempenho é crucial.

Comparação sensível à cultura (CurrentCulture, CurrentCultureIgnoreCase, InvariantCulture, InvariantCultureIgnoreCase): Essas comparações consideram o contexto cultural das strings, o que é essencial ao comparar strings que são exibidas ao usuário ou quando os resultados da comparação dependem de regras culturais específicas (como ordenação em uma interface do usuário).

As comparações ordinais são mais rápidas do que as comparações sensíveis à cultura porque comparam diretamente o valor numérico Unicode de cada caractere nas strings.

Não há necessidade de aplicar regras culturais, que podem variar amplamente e envolver lógica complexa como o tratamento de caracteres especiais, marcas de acentuação ou conversões de caixa baseadas em culturas específicas.

Vamos ver um exemplo prático:

string string1 = "hello world";
string string2 = "Hello World";

bool areEqual = string.Equals(string1, string2, StringComparison.OrdinalIgnoreCase);
// areEqual é true porque a comparação é case-insensitive.

Span é um tipo alocado na pilha que pode apontar para regiões contínuas de memória representando partes de arrays, strings ou memória não gerenciada.

Ele oferece a capacidade de trabalhar com uma parte dos dados sem alocar nova memória para essa parte.

Isso é particularmente útil para strings porque, como mencionado anteriormente, strings são imutáveis no .NET.

Principais vantagens do Span

Redução de Alocações:

Como o Span pode referenciar uma parte de um array ou string, ele elimina a necessidade de criar novas substrings ou segmentos de array quando você só precisa trabalhar com parte dos dados. Isso pode reduzir significativamente o número de alocações, diminuindo assim a pressão sobre o Coletor de Lixo (GC) e melhorando o desempenho do aplicativo.

Eficiência de Memória:

O Span possibilita um uso mais eficiente da memória ao permitir operações em partes dos dados sem duplicar as estruturas de dados subjacentes. Isso é particularmente benéfico em aplicações críticas de desempenho, como parsers ou pipelines de processamento, onde é comum precisar ler ou manipular pequenas porções de um conjunto de dados maior de cada vez.

Versatilidade:

O Span pode ser usado com qualquer tipo de memória contígua, não apenas arrays ou strings. Isso inclui memória não gerenciada, o que abre possibilidades para cenários de alto desempenho que anteriormente eram mais complicados ou ineficientes no .NET.

public class SpanVsSubstring
{
    private const string testString = "Esta é uma string de teste mais longa para demonstração.";

    [Benchmark]
    public string UseSubstring()
    {
        return testString.Substring(10, 5); // Extrair"longa"
    }

    [Benchmark]
    public ReadOnlySpan<char> UseSpan()
    {
        ReadOnlySpan<char> span = testString.AsSpan(10, 5);
        return span; // "longa"
    }
}