Base Class Library – Parte 02 – Tipos por Valor e por Referência, Stack, Heap, Boxing e Unboxing

Olá pessoal, nesta parte da série de artigos sobre o BCL veremos as principais diferenças e conceitos entre tipos por valor, tipos por referência, stack e heap, além de vermos o que é boxing, o que é unboxing e como realizar estas operações.

Esta é uma série de artigos sobre a Base Class Library, para ver os artigos anteriores desta série clique neste link.

Introdução

O CLR suporta dois tipos de dados: os Value Types (tipos por valor) e os Reference Types (tipos por referência).

Tipos por Valor – São variáveis que diretamente contém seu próprio valor em memória, sendo assim, cada variável mantém a cópia de seus dados e, consequentemente, operações que utilizam essa variável não afetarão o seu valor. Os tipos por valor são divididos em dois outros tipos: built-in e user-defined types.

O primeiro se trata de valores fornecidos com o próprio .NET Framework, que são os tipos comuns do nosso uso diário, caso de inteiros, decimais, floats, doubles, etc. Já o segundo são tipos criados e definidos por nós dentro de componentes ou aplicações, como estruturas de dados (structs) ou enumeradores. Os structs são semelhantes a uma classe e podem conter membros para armazenar os dados e/ou funções para desempenhar alguma operação. Já os enumeradores são constantes que fixam a escolha de alguns valores fornecidos por ele, o que impossibilita que seja passado algo diferente do esperado, ou seja, de passar algum valor que seu método/classe não saiba trabalhar.

Tipos por Referência – São variáveis que, ao invés de conter o valor em si, contém uma referência (também chamado de “ponteiro”) para um objeto em um local específico da memória. Assim é possível termos duas ou mais variáveis apontando para o mesmo lugar na memória e uma operação, como atribuição de valor por exemplo, poderá alterar o conteúdo deste objeto, já que ele está, de certa forma, “compartilhado”.

Os tipos por referência também são divididos em dois outros tipos: built-in e user-defined types, sendo que o primeiro se destina a classes, interfaces e delegates, e o segundo a tipos como o dynamic, object e ao string.

Para ilustrar as explicações dos tipos acima, confira uma imagem com o diagrama deles.

Tipos do Framework

Créditos da imagem aqui. Reparem a clara alusão ao ponteiro aos dados nos tipos por referência.

Se pensarmos bem, já que todos os tipos herdam de object e object é um tipo por referência, em sua essência, todos os tipos são tipos por referência!

Agora falando da diferença entre os tipos: o local onde cada um é armazenado na memória é completamente diferente do outro. Enquanto os tipos por valor são armazenados na Stack, os tipos por referência são armazenados na Heap. Se você traduzir notará que ambas as palavras são sinônimas da palavra “pilha”, por isso é necessária uma explicação de cada termo (e porque também é importante sabermos que são locais distintos da memória).

Stack – O stack é um bloco de memória alocado para cada variável, em tempo de execução. Ou seja, durante a execução, quando uma função é chamada, todas as variáveis utilizadas por ela são colocadas na stack. Assim que a função é retornada ou o escopo de um bloco é finalizado, o mais rápido possível os valores da stack são descartados, liberando desta forma a memória que estava sendo ocupada. Como explicado anteriormente, quando uma variável que é tipo por valor for passada em uma função uma cópia do valor é passado e, se esta função alterar este valor, não refletirá no valor original. Como os tipos de dados dentro do .NET Framework são uma estrutura, eles serão também armazenados na stack.

Heap – Já os tipos por referência são armazenados na memória heap, em um local completamente diferente da stack. Enquanto a referência a este dado é colocada na memória stack (não confundir, o dado em si fica na memória heap, enquanto que a referência a este dado vai pra memória stack).

Isso acontece quando utilizamos o operador new (New em VB.NET) no código, que retornará o endereço de memória do objeto. Isso acontece no momento em que precisamos criar efetivamente o objeto para utilizá-lo (conhecido pela palavra instanciação). Para entender melhor o processo confira o código abaixo, usando uma classe, que é um tipo por referência.

Dim clienteUm As New Cliente()
Dim clienteDois As Cliente = clienteUm
Cliente clienteUm = new Cliente();
Cliente clienteDois = clienteUm;

Quando atribuímos o clienteUm ao clienteDois recém criado, é copiado a referência ao objeto que, por sua vez, aponta para uma determinada seção na memória Heap. No código acima, quando ser efetuado uma mudança, seja ela no objeto clienteUm ou clienteDois, refletirá no mesmo objeto da memória Heap. Através da imagem abaixo podemos visualizar as memórias (Stack e Heap) em conjunto quando armazenam tipos por referência:

Stack&Heap

Como explicando antes, todos os objetos são, em sua essência, tipos por referência. Além deles temos a Interface, que será explicada com detalhes e exemplos na próxima parte desta série de artigos.

Importante: Como comentado antes, todos os tipos do .NET são tipos por referência, já que todos derivam de System.Object, e o Object é um tipo por referência. Mas e quanto aos tipos por valor? Para não haver confusão, a Microsoft criou uma possibilidade de diferenciar um do outro: segundo a documentação da Microsoft a maioria dos tipos por valor são representados através de estruturas de dados (structs, que são obviamente tipos por valor, enquanto as classes são tipos por referência).

Por exemplo: os tipos integer, decimal, datetime são representados System.Int32 (Int16 para o tipo smallint), System.Decimal e System.DateTime respectivamente.

Toda e qualquer estrutura é derivada de System.ValueType e esta, por sua vez, herda de System.Object. Alguns leitores podem estar se perguntando: “como um tipo por valor que herda de um tipo por referência continua sendo um tipo por valor?”. Esta questão é fácil de responder: apesar dos tipos por valor herdarem de um tipo só (Object), que é um tipo por referência, isso não impede que os tipos por valor continuem sendo tipos por valor! A “característica” dele não irá ser alterada. Essa distinção é clara, necessária e obrigatória no ponto de vista do CLR (se você leu a parte 01 ou já tem conhecimento prévio acredito que saiba bem o que é e como o CLR funciona).

Um usuário fez essa mesma pergunta lá no StackOverflow e ninguém mais ninguém menos que Eric Lippert (criador do Visual Basic, VBScript, JScript e do compilador do C#) respondeu, confira neste link a resposta (a 1ª e a sugerida como resposta pela maioria). Ele ainda deu um exemplo que por si só já é a resposta: “Como é possível que cada caixa vermelha (tipos por valor) esteja dentro (herde de) da caixa O (System.Object), que é uma caixa azul (tipo por referência) e ainda seja uma caixa vermelha (um tipo por valor)?”, confira o link para entender toda a resposta dele.

Ainda sobre os tipos, além da classe ValueType fornecer toda a estrutura básica para os tipos por valor do .NET, ela também é tratada de forma diferente pelo compilador, já que seus tipos derivados devem ser colocados na memória Stack, explicada anteriormente.

Essa distinção entre tipos por valor e por referência devem existir, por outros motivos também, como a performance de uma aplicação. Imagine se a cada criação/atribuição de valores de um integer, decimal, datetime, o compilador tivesse que alocar memória? Os tipos por valor são considerados os tipos menos custosos na aplicação, e além disso são alocados na Stack, dessa forma temos muitas melhorias na performance da aplicação como um todo. Além disso, os tipos por valor não ficam sob inspeção do GC (Garbage Collector, ou Coletor de Lixo), e ficam obviamente fora da Heap.

Para mais informações sobre tipos por valor, por referência, stack e heap, recomendo a leitura deste artigo, do C# Corner.

Boxing e Unboxing – É a conversão entre tipos, feita muitas vezes implicitamente pelo .NET Framework para permitir que um método que espere um tipo por referência aceite um tipo por valor, por exemplo.

Explicando em detalhes, pense na seguinte situação: você precisa obter uma referência a uma instância de um tipo por valor. Para exemplificar podemos usar uma coleção de dados, como o ArrayList, que nos permite criar uma coleção de qualquer tipo. Ele aceita como parâmetro de seu método Add um System.Object (tipo por referência). Como tudo no .NET é, direta ou indiretamente, um tipo de System.Object, posso adicionar o que quiser nesta coleção, uma string, um inteiro, um datetime… Veja o exemplo na prática.

Dim colecaoDeObjetos As New ArrayList()
colecaoDeObjetos.Add(1)
colecaoDeObjetos.Add(2)
colecaoDeObjetos.Add(3)
ArrayList colecaoDeObjetos = new ArrayList();
colecaoDeObjetos.Add(1);
colecaoDeObjetos.Add(2);
colecaoDeObjetos.Add(3);

Como descrito anteriormente, o método Add recebe um tipo por referência. Isso significa que este método exige uma referência (um ponteiro) para um objeto na Heap. Mas como podemos ver no código acima foi adicionado um inteiro e tipos por valor não possuem ponteiros para o Heap, já que eles são armazenados na Stack.

E porque o código não dá exceção?

Porque o .NET Framework possibilita que o código acima funcione por conta do Boxing. Na maioria dos casos ele ocorre implicitamente, sem que você nem perceba, como nos códigos acima. O que ocorre no CLR quando o código acima é executado é o seguinte:

  1. A quantidade de memória é alocada no Heap, de acordo com o tamanho do tipo por valor e qualquer overhead a mais, se for o caso;
  2. Os campos do tipo por valor são copiados para a Heap, que foi previamente alocada;
  3. O endereço com a referência é retornado.

Felizmente os compiladores das linguagens VB.NET e C# produzem automaticamente o código necessário para realizar o boxing, sem termos que nos preocupar com essa conversão (nesse caso específico).

O Unboxing, por outro lado, é o inverso do Boxing, mas a operação ocorre mais facilmente que o Boxing. Unboxing é o processo de apenas obter o ponteiro para o tipo por valor bruto, contido dentro de um objeto e, neste caso, não é necessário copiar nenhum tipo de dado na memória.

Boxing e Unboxing comprometem a performance da aplicação, em termos de velocidade e de memória utilizada. Mas desde a versão 2.0 do .NET Framework temos os Generics Collections (Coleções Genéricas), que elimina quaisquer problemas de performance. Veremos sobre Generics futuramente, nesta série de artigos.

Se ficou com dúvidas de como implementar o boxing/unboxing sugiro a leitura deste artigo, da Devmedia, neste link.

Conclusão

Vimos neste artigo conceitos e exemplos dos tipos de dados do .NET Framework, além das diferenças entre stack e heap, além do boxing e unboxing.

Fonte principal de consulta: Por dentro da Base Class Library, de Israel Aece.

Obrigado e até o próximo artigo, onde teremos muito conteúdo sobre as Interfaces e códigos com exemplos de algumas muito usadas.

Expresse sua opinião!

Preencha os seus dados abaixo ou clique em um ícone para log in:

Logotipo do WordPress.com

Você está comentando utilizando sua conta WordPress.com. Sair / Alterar )

Imagem do Twitter

Você está comentando utilizando sua conta Twitter. Sair / Alterar )

Foto do Facebook

Você está comentando utilizando sua conta Facebook. Sair / Alterar )

Foto do Google+

Você está comentando utilizando sua conta Google+. Sair / Alterar )

Conectando a %s