Jul 31, 2023

Structs em C# - diversão garantida - Parte 2/9: Uma breve introdução a Value types versus Reference types

Read this post in English.

Lisez cet post en french.

  1. Structs em C# - diversão garantida
  2. Rápida introdução à Value Types vs Reference Types (este post).
  3.  Initialização de campos em estruturas.
  4.  Comportamento de construtores em estruturas.
  5.  Outros cenários em que o comportamento de construtores em estruturas podem te surpreender.
  6.  Argumentos default  em construtores de estruturas (você ainda não esta confuso ?).
  7.  Modificador `required` do C# 11 não vai salvar seu c* trabalho.
  8.  Estruturas usadas como valor default de argumentos.
  9.  Bonus: Evolução das estruturas em C#. 


Desde o lançamento oficial da primeira versão do C# (versão 1.0, em janeiro/2002),  desenvolvedores se deparam com uma decisão ao introduzir novos tipos: declará-los como class (Reference Type) ou struct (Value Type), representados no .NET por System.Object e System.ValueType respectivamente.

Escolher um ou o outro tem implicações não triviais com relação à usabilidade, desempenho e extensibilidade, para listar alguns. Neste post, quero cobrir brevemente as principais diferenças (mais detalhes serão apresentados nos posts futuros se necessário) e esclarecer uma concepção equivocada (a qual provavelmente sou culpado por contribuir para sua disseminassão). Então, sem mais delongas, vamos entrar na distinção mais importante sobre os dois...

Semântia de referencia versus de valor

A característica mais importante que distingue esses 2 tipos está relacionada a como a igualdade e a atribuição/passagem de parâmetro são tratadas. A tabela abaixo mostra essas diferenças (supondo que os tipos em questão não sobrecarreguem o método Equals() ou os operadores ==/!=):


Value Type Reference Type
Assignment semantics
por valor, ou seja, o conteúdo da instância é copiado resultando em duas cópias independentes.
por referência, ou seja, a atribuição apenas copia uma referência e  mudanças através de qualquer uma das referências serão observadas ao acessar o objeto através da outra.
Equality
(Identity semantics)
duas instâncias são iguais se forem instâncias do mesmo tipo e todos os seus campos forem iguais. duas instâncias são iguais se referênciarem o mesmo objeto.

Para facilitar a visualização, imagine o seguinte código:

Durante sua execução, ao chegar na linha 15, podemos representar, de uma forma  simplificada, a memória utilizada para o armazenamento das variáveis v1 e v3 como algo:


ou seja, as variávis v1v3 são armazenadas nos endereços 0x100 e 0x400 respectivamente. Observe contudo que a variável v3 não armazena o contéudo da instância do objeto AReferenceType mas sim uma referência (ou, novamente em uma simplificação, um ponteiro) para o objeto instanciado no endereço 0x1000v1 (uma instância da estrutura  AValueType) por sua vez armazena os dados da instância diretamente (sem indireções).

Se inspecionarmos o estado da memória quando o programa executa a linha 19 observaremos algo como:

Observe que ambas as variáveis (v1 & v3) tem seus conteúdos copiados para as variáceis recem declaradas (v2 & v4) mas como v3 é uma refeence type, copiar seu valor implica copiar a referência (endereço) armazenada na mesma, fato que ficará mais evidente a frente.

Desta forma, ao imprimir os campos IntValue de v1 & v2 o mesmo resultado é produzido; contudo,  após armazenar o valor 71 em v2.IntValue (linha 21) o estado da memória pode ser representado como:


e consequentemente a linha 22 do programa imprimirá os valores 42 & 71 evidenciando que
v1 & v2 são independentes um do outro.

Reference types funcionam de uma forma diferente; ao executar a instrução na linha 25 o valor do campo IntValue das variáveis v3 e v4 são impressos. Para determinar quais valores serão passados para o método Console.WriteLine(), primeiramente o valor de v3 (0x1000) é lido da possição de memória 0x400 (v3) e a seguir, o conteúdo desta posição (0x1000) (ou mais precisamente 4 bytes que compõem um int em C#) é lido, resultando no valor 42; a seguir o memo processo é repetido para a variável v4 e como esta referencia o mesmo objeto que v3 (0x1000) o mesmo resultado é prodizido.

Uma vez que um processo similar é aplicado ao se modificar o conteúdo de um Reference Type, após a execução da linha 26 o estado da memória se parecerá com

 

e a linha 27 imprimirá 71 & 71, ou seja, modificações aplicadas ao objeto referenciado por v4 são observadas através da variável v3 (o que faz todo o sentido já que ambas variáveis referenciam o mesmo objeto).

Concepções equivocadas

Ao longo dos anos, li vários artigos, entrevistas e até alguns livros afirmando que uma das principais diferenças entre Reference Types e Value Types é que Value Types são sempre armazenados na pilha, enquanto Reference Types são sempre armazenados no heap.

Esse equívoco é tão difundido que Eric Lippert (o qual trabalhou como um dos designers de C# no passado) escreveu 2 postagens de blog com o objetivo de  acabar com a confusão.

Como Eric menciona, o fato de Value Type serem normalmente alocados na pilha é um detalhe de implementação. Contudo, na minha opnião este é um detalhe que dificilmente mudará uma vez que esse comportamento é essencial em cenários em que manter baixas as alocações de heap (e, consequentemente, a pressão de GC) é crucial.

Como sempre, todo feedback é bem-vindo.

Have fun!

No comments: