Como funcionam os tipos por referência, em .NET ?

Os tipos por referência ocupam espaço duas vezes. Uma na stacke outra na heap. Na stack existe um apontamento para o dado correspondente na heap. Se estes tipos por referência forem destruídos, apenas seu ponteiro é destruído. O Garbage Collector percebe os dados na heap que não possuem contra-referências na stack e o excluem na sua limpeza.
Os tipos por referência são especialmente úteis. Eles são responsáveis por guardar tipos mais complexos como classes. Estes tipos normalmente ocupam grande espaço em memória. Quando um tipo por referência é utilizado como parâmetro para um método, este parâmetro aponta para o tipo por referência. E quando um tipo por referência é atribuído por outro, existe um apontamento interno, diferentemente do tipo por valor. Isto é de grande valia para objetos pesados.
    Os tipos por referência mais fundamentais são:
  • System.Object
  • System.String
  • System.Text.StringBuilder
  • System.Array
  • System.IO.Stream
  • System.Exception

Por que utilizar StringBuilder a string?

As strings são classes prontas do .NET para tratamento de arrays de caracteres. Enquanto um char[] utiliza a classe System.Array, um string utiliza a classe System.String. A classe System.String se especializa no tratamento de array de caracteres trazendo uma série de benefícios na sua manipulação.
As strings em .NET e em outras linguagens apresentam uma limitação: as strings são imutáveis. Isto significa que é impossível alterar uma string em .NET. Esta afirmativa é estranha por que é comum a alteração de string, porém internamente a CLR realiza uma manobra para que isto funcione corretamente. Quando uma string é definida, uma posição de memória é alocada na stack para apontar para um endereço da heap que conterá a string. Se esta string for alterada, uma nova entrada será feita na stack e outra cópia será feita na heap. E pelo menos o .NET apaga o endereço de memória da stack que contém a antiga string. Desta forma na próxima passagem do Garbage Collector, todas as strings antigas serão excluídas. Veja o código – comentado – a seguir que mostra este problema:
  public static void Main()
        {
// System.Int32 ocupa 4 Bytes somente na stack
            intvalor1 = 300;
int valor2 = 600;
            intvalor3 = 900;
            stringvalor;          
// System.String ocupa o equivalente a um System.IntPtr na stack.
//Este valor varia de acordo com a plataforma
            /* Como este valor não está instanciado, apenas ocupa a stack, e não aponta para a heap */
            valor = “Esta string é um teste”;     
// Um novo espaço (System.IntPtr) é ocupado na stack
            /* O espaço antigo não é anulado (embora já estivesse)*/
            /* A heap guarda o tamanho da string mais outras partes da classe System.String*/
            valor += “nO valor 1 é de “ + valor1; 
// Um novo espaço (System.IntPtr) é ocupado na stack
            /* O espaço antigo não é anulado*/
            /* A heap guarda o tamanho da string mais outras partes da classe System.String*/
            valor += “nO valor 2 é de “ + valor2; 
// Um novo espaço (System.IntPtr) é ocupado na stack
            /* O espaço antigo não é anulado*/
            /* A heap guarda o tamanho da string mais outras partes da classe System.String*/
            valor += “nO valor 3 é de “ + valor3; 
// Um novo espaço (System.IntPtr) é ocupado na stack
            /* O espaço antigo não é anulado*/
            /* A heap guarda o tamanho da string mais outras partes da classe System.String*/
            // Imprime na tela o valor da string
            Console.Write(valor);
        }
A aplicação citada gasta:
  • 3 System.Int32
    • 4 Bytes cada
    • 12 Bytes da stack
    • Gastos corretamente
  • 5 System.IntPrt
    • O valor varia de acordo com a plataforma
    • Gasta no mínimo 5 bytes da stack
    • Os 5 System.IntPrt representam indiretamente a mesma System.String
  • 5 System.String
Agora, tendo o problema bem claro, então qual é a solução? O .NET oferece um objeto que resolve esta questão: System.Text.StringBuilder. Diferentemente da string, ele tem a função de montar strings e, apenas, uma string é criada na memória após uma manipulação de string. Veja o código que segue, ele resolve o problema apresentado no código anterior:
public staticvoid Main()
        {
// System.Int32 ocupa 4 Bytes somente na stack
            intvalor1 = 300;      
            intvalor2 = 600;
            intvalor3 = 900;
                             
// System.Text.StringBuilder equivale a um System.IntPtr na stack. Este valor varia de acordo com a plataforma
System.Text.StringBuilder sb = newStringBuilder();
            sb.Append(“Esta string é um teste”);
            sb.Append(“nO valor 1 é de “ + valor1);
            sb.Append(“nO valor 2 é de “ + valor2);
            sb.Append(“nO valor 3 é de “ + valor3);
// Um novo espaço (System.IntPtr) é ocupado na stack
            /* A heap guarda o tamanho da string mais outras partes da classe System.String*/
            stringvalor = sb.ToString();
          
            // Imprime na tela o valor da string
            Console.Write(valor);
        }
Desta forma, o resumo dos gastos são:
  • 3 System.Int32
    • 4 Bytes cada
    • 12 Bytes da stack
    • Gastos corretamente
  • 2 System.IntPtr
    • O valor varia de acordo com a plataforma
    • 1 para apontar para um System.String, criado corretamente
    • 1 para apontar para um System.Text.StringBuilder, criado corretamente
  • 1 System.String
  • 1 System.Text.StringBuilder

Thiago Anselme
Thiago Anselme - Gerente de TI - Arquiteto de Soluções

Ele atua/atuou como Dev Full Stack C# .NET / Angular / Kubernetes e afins. Ele possui certificações Microsoft MCTS (6x), MCPD em Web, ITIL v3 e CKAD (Kubernetes) . Thiago é apaixonado por tecnologia, entusiasta de TI desde a infância bem como amante de aprendizado contínuo.

Deixe um comentário