Jul 31, 2023

Structs in C# are fun - Part 2/9: A brief introduction to Value Types vs Reference Types

Leia este post em português

Lisez cet post en french.

  1. Structs in C# are fun
  2. Brief introduction to Value Types vs Reference Types (this post)
  3. Field initialization in structs.
  4. Constructors and struct behavior.
  5. Other scenarios in which struct constructors behavior may surprise you.
  6. Struct with default argument values in constructors, a.k.a, are you not confused yet ?
  7. `required` feature from C# 11 will not save your a** job.
  8. Struct used as default argument values.
  9. Bonus: Struct evolution in C#.

Since the first oficial release (version 1.0, back in January/2002) of C#, developers have been faced with a decision when introducing new types: declaring it as a class (a reference type) or a struct (a value type), represented in the .NET type system by System.Object and System.ValueType respectively.

Picking one over the other has non trivial implications in usability, performance, extensibility to list a few. In this post I want to briefly cover the main differences (more details will be presented in future posts) and clarify one misconception (which I am probably guilt of contributing to disseminate). So without further ado lets get into the most important distinction about the two...

Reference type vs Value type semantics

The most important characteristic distinguishing these 2 kind of types relates to equality and assignment/parameter passing are behaviour. The table below shows these differences (assuming the types in question does not overloads Equals() method or ==/!= operators):


Value Type Reference Type
Assignment semantics
by value, i.e, the instance contents is copied leaving  two independent copies.
by reference, i.e, assignment just copies a reference and changes through any of the references will be observed when accessing the object through the other one.
Equality
(Identity semantics)
two instances are equal if they are instances of the same type and all its fields are equal. two instances are equal if they reference the same object (i.e, exact same reference)

In order to make it easier to visualise, imagine the following code:

After running the code through line 15, we can represent the memory used by v1 and v3, in an overly simplified way, as something like:


i,e, variable v1 is stored at address 0x100 while v3 at address 0x400. Notice though that v3 does not store the actual  instance data of the AReferenceType object. Instead it stores a reference (or, in an oversimplification, a pointer) to the actual object allocated at address 0x1000. In contrast, AValueType instances stores its data inline.

If we inspect the memory state when program reaches line 19, we'd observe something like:

Notice that both variables (v1 & v3) had its contents copied to the newly declared ones (v2 & v4) but since v3 is a reference type, copying its value means copying a reference, a fact that will become evident later.

When the code prints the IntValue fields from v1 & v2 it is evident that the same value will be displayed but after storing the value 71 in v2.IntValue ( at line 21), the memory will look like:

so line 22 will print the values 42 & 71, or, in other words,  v1 & v2 are independent of each other.

Reference types work differently; let's start with line 25 in which v3 and v4 IntValues are displayed; in order to figure out which values will be passed to Console.WriteLine() method, first the value of  v3 (i.e, 0x1000) is read from the location at address 0x400 (v3) and then the contents at that address (actually the four bytes that makes up an int in C#) is read, producing the value 42; next the same process is repeated for v4; since v4 references the same object as v3 (0x1000) that will produce the same result.

Since a similar process  is applied when changing reference type state, after executing line 26, the memory will look like:

 

and line 27 will print 71 & 71 i.e, changes applied to v4 are observable through v3.

Misconceptions

Over the years I have seem multiple articles, interviewes and even some books claiming that one of the main difference between value types vs reference types is that value types are always stored on the stack while reference types are always stored in the managed heap (as I mentioned above, most likely I am guilt of propagating this misconception also :(). 

This misconception is so spread that Eric Lippert (which worked as one of the designers of C# in the past) wrote 2 blog posts to debunk it.

As Eric mentions, the fact that value types are usualy allocated on the stack is an implementation detail, but IMO, one that will hardly change; that said this implementation  behavior is important in scenarios where keeping heap allocations (and consequently GC pressure) low is crutial to achieve predictable performance characteristics.

As always, all feedback is welcome.

Have fun!

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!

Les structures en C# sont amusantes - Partie 2/9 : Une brève introduction aux types de valeur par rapport aux types de référence

Leia este post em português

Read this post in English.

  1. Les structures en C# sont amusantes
  2. Brève introduction aux Value Types vs Reference Types (cet post).
  3. Initialisation des champs dans les structures.
  4. Comportement des constructeurs dans structures.
  5. Des autres scénarios dans lesquels le comportement des constructeurs de structure peut vous surprendre.
  6.  Struct avec des valeurs d'argument par défaut dans les constructeurs, ou, n'êtes-vous pas encore confus ?
  7. Le modificateur `required`de C # 11 ne sauvegardera pas votre c*l emploi.
  8. Structure utilisée comme valeurs d'argument défaut.
  9. Bonus: L'evolution des structures en C#.

Depuis la première version officielle (version 1.0, en janvier 2002) de C#, les développeurs ont été confrontés à une décision lors de l'introduction de nouveaux types : le déclarer comme une class (un type référence) ou une struct (un type valeur), représenté dans le système de type .NET par System.Object et System.ValueType respectivement.

Choisir l'un sur l'autre a des implications non négligeables en termes de convivialité, de performances, d'extensibilité, pour n'en citer que quelques-unes. Dans cet article, je souhaite couvrir brièvement les principales différences (plus de détails seront présentés dans les prochains articles) et clarifier une idée fausse (que je suis probablement coupable d'avoir contribué à diffuser). Alors sans plus tarder, entrons dans la distinction la plus importante entre les deux ...

Reference type versus Value type sémantique

La caractéristique la plus importante qui distingue ces 2 types concerne la manière dont l'égalité et le attribution/paramètres sont gérés. Le tableau ci-dessous montre ces différences (en supposant que les types en question ne surchargent pas la méthode Equals() ou les opérateurs ==/!=):


Value Type Reference Type
Sémantique d'attribution
par valeur, c'est-à-dire que le contenu de l'instance est copié en laissant deux copies indépendantes.
par référence, c'est-à-dire que l'attribution copie simplement une référence et que les modifications via l'une des références seront observées lors de l'accès à l'objet via l'autre.
Égalité
(Sémantique de l'identité)
deux instances sont égales si ce sont des instances du même type et si tous ses champs sont égaux. deux instances sont égales si elles référencent le même objet (c'est-à-dire exactement la même référence)

Afin de faciliter la visualisation, imaginez le code suivant :

Après avoir exécuté le code jusqu'à la ligne 15, nous pouvons représenter la mémoire utilisée par les variables v1 et v3, d'une manière trop simplifiée, comme quelque chose comme :


donc la variable v1 est stockée à l'adresse 0x100 tandis que v3 à l'adresse 0x400. Notez cependant que v3 ne stocke pas les données d'instance réelles de l'objet AReferenceType. Au lieu de cela, il stocke une référence (ou, dans une simplification, un pointeur) à l'objet réel alloué à l'adresse 0x1000. En revanche, les instances AValueType stockent leurs données en ligne.

Si nous inspectons l'état de la mémoire lorsque le programme atteint la ligne 19, nous observerons quelque chose comme:

Notez que les deux variables (v1 et v3) avaient leur contenu copié dans les nouvelles déclarées (v2 et v4) mais puisque v3 est un type de référence, copier sa valeur signifie copier une référence, un fait qui deviendra évident plus tard.

Lorsque le code imprime les champs IntValue de v1 & v2, il est évident que la même valeur sera affichée mais après avoir stocké la valeur 71 dans v2.IntValue ( à la ligne 21), la mémoire ressemblera à
:


donc la ligne 22 imprimera les valeurs 42 et 71, ou, en d'autres termes, v1 et v2 sont indépendants l'un de l'autre.

Reference types fonctionnent différemment ; commençons par la ligne 25 dans laquelle le IntValue des variables
v3 et v4 sont affichées ; afin de déterminer quelles valeurs seront transmises à la méthode Console.WriteLine(), d'abord la valeur de la variable v3 (c'est-à-dire 0x1000) est lue à partir de l'emplacement à l'adresse 0x400 (v3), puis le contenu à cette adresse (en fait le quatre octets qui composent un int en C#) est lu, produisant la valeur 42 ; ensuite, le même processus est répété pour v4, mais puisque la variable v4 fait référence au même objet que la variable v3 (0x1000), cela produira le même résultat.

Puisqu'un processus similaire est appliqué lors du changement d'état du type de référence, après l'exécution de la ligne 26, la mémoire ressemblera à
:

 

et la ligne 27 affichera 71 et 71, c'est-à-dire que les modifications appliquées à la variable v4 sont observables jusqu'à la variable v3.

Idée fausse

Au fil des ans, j'ai paru plusieurs articles, interviews et même certains livres affirmant que l'une des principales différences entre les types de valeur et les types de référence est que les types de valeur sont toujours stockés sur la pile tandis que les types de référence sont toujours stockés dans le tas géré (comme je mentionné ci-dessus, je suis très probablement coupable de propager cette idée fausse aussi :().

Cette idée fausse est tellement répandue qu'Eric Lippert (qui a travaillé comme l'un des concepteurs de C # dans le passé) a écrit 2 articles de blog pour la démystifier.

Comme le mentionne Eric, le fait que les types valeur soient généralement alloués sur la pile est un détail d'implémentation, mais a mon avis, qui ne changera jamais ; Cela dit, ce comportement de mise en œuvre est important dans les scénarios où il est essentiel de maintenir les allocations de tas (et par conséquent la pression du GC) à un niveau bas pour obtenir des caractéristiques de performances prévisibles.

Comme toujours, tous les commentaires sont bienvenus

Have fun!