Desenvolvimento - C#
Reference Types e sua diferença para Value Types em C#
Veremos neste artigo a diferença entre Value Types e Reference types, assim como veremos os principais Built-in Reference Types. Aprenderemos a trabalhar com a classe String e StringBuilder e notar as diferenças em performance e aprender boas práticas para se trabalhar com Strings. Veremos como criar arrays, trabalhar com Streams e utilizar tratamento de erros na leitura e escrita de arquivos.
por André BaltieriIntrodução
Reference Types armazenam endereços ao invés de dados em si e também são conhecidos como Ponteiros. Estes endereços são armazenados em um lugar na memória chamado Stack.
O dado no qual o endereço se refere fica armazenado em um local da memória chamado Heap. O runtime gerencia esta memória utilizada pelo Heap através de um processo chamado Garbage Collector. O Garbage Collector “varre” a memória periodicamente, jogando fora tudo que não estão mais sendo utilizados (Referenciados).
Nota: O Garbage Collector é feito automaticamente, e é aconselhável que continue assim, mas a partir da versão 2.0 do .NET Framework, você pode chamar o Garbage Collector a qualquer momento, utilizando a sintaxe GC.Collect().
Comparando o comportamento entre um Reference e um Value Type
Quando estamos utilizando um Reference type e o assinamos para uma variável, seu dado não é copiado, isto é, atribuindo uma variável Reference Type para outra, criamos uma simples cópia de seu endereço, mas estaremos modificando o mesmo valor que está alocado no Heap. Veja o exemplo na Listagem 1.
using System;
using System.Collections.Generic;
using System.Text;
namespace ReferenceTypes
{
class Program
{
static void Main(string[] args)
{
Numeros n1 = new Numeros(0);
Numeros n2 = n1;
n1.val += 1;
n2.val += 2;
Console.WriteLine("N1 = {0}\nN2 = {1}", n1, n2);
Console.ReadKey();
}
}
struct Numeros
{
public int val;
public Numeros(int _val)
{ val = _val; }
public override string ToString()
{ return val.ToString(); }
}
}
Listagem 1 – Value Types
O programa acima deve mostrar N1
= 1 e N2 = 2, pois Struct é um Value Type,e sua cópia resulta em dois valores
distintos. No entanto, se modificarmos a Struct para uma classe, como na Listagem
2, os valores passam a ser N1=3 e N2=3, pois uma classe é um Reference
Type, e sendo assim estaremos sempre alterando um único valor.
namespace ReferenceTypes
{
class Program
{
static void Main(string[] args)
{
Numeros n1 = new Numeros(0);
Numeros n2 = n1;
n1.val += 1;
n2.val += 2;
Console.WriteLine("N1 = {0}\nN2 = {1}", n1, n2);
Console.ReadKey();
}
}
class Numeros
{
public int val;
public Numeros(int _val)
{ val = _val; }
public override string ToString()
{ return val.ToString(); }
}
}
Listagem 2 – Reference Types
Built-in Reference Types
Existem aproximadamente 2500 Reference Types no .NET Framework. Tudo que não deriva de System.ValueType é um Reference Type. A Figura 1 mostra os Reference Types mais comuns:
Figura 1 – Reference Types mais comuns
Strings e StringBuilder
Tipos são mais do que simples recipientes para os dados, eles também fornecem membros para a manipulação dos mesmo. Neste caso, o System.String fornece um conjunto de membros para se trabalhar com textos. A Listagem 3 mostra um simples exemplo de busca e substituição de caracteres em um texto:
using System;
using System.Collections.Generic;
using System.Text;
namespace ReferenceTypes
{
class Program
{
static void Main(string[] args)
{
// C#
string s = "Consuma energia de forma inteligente.";
s = s.Replace("inteligente", "eficaz");
Console.WriteLine(s);
Console.ReadKey();
}
}
}
Listagem 3 – Buscando e substituindo em textos.
Podemos notar que o texto foi alterado de “Consuma energia
de forma inteligente.” para “Consuma energia de forma eficaz.”.
Strings do tipo System.String são imutáveis no .NET Framework, isto
significa, que cada vez que alteramos uma string, uma nova string é criada e a
string antiga é abandonada. O programa da Listagem 4 por exemplo, cria 4
strings em memória:
using System;
using System.Collections.Generic;
using System.Text;
namespace ReferenceTypes
{
class Program
{
static void Main(string[] args)
{
// C#
string s;
s = "Pêra";
s += " Uva";
s += " Maçã";
s += " Salada Mista";
Console.WriteLine(s);
Console.ReadKey();
}
}
}
Listagem 4 – Atribuindo textos a strings.
Note que somente a última string possui a referência, as outras três são “varridas”
pelo Garbage Collector durante o runtime. Evitantoa utilização destas strings
temporárias, evitamos o uso do Garbage Collector, e sendo assim, aumentamos a
performance da aplicação. Podemos tomar algumas medidas para precaver esta
criação desnecessária de strings temporárias como:
Figura 2 – Operadores de String.
Criando e Ordenando Arrays
Arrays são declarados utilizando parênteses (VB) ou colchetes (C#) como parte de sua declaração e assim como o tipo String, os arrays possuem membros para manipulação de seus dados. Veja o exemplo na Listagem 6.
using System;
using System.Collections.Generic;
using System.Text;
namespace ReferenceTypes
{
class Program
{
static void Main(string[] args)
{
// C#
// Declaração e inicialização do array.
int[] ar = { 3, 1, 2 };
// Chamada do método de ordenação.
Array.Sort(ar);
// Exibindo o resultado.
Console.WriteLine("{0}, {1}, {2}", ar[0], ar[1], ar[2]);
Console.ReadKey();
}
}
}
Listagem 6 – Criando, ordenando e exibindo os valores de um Array.
Notamos que no programa acima, fornecemos os valores 3, 1 e 2 respectivamente como entrada no array, e após utilizar o método Sort(), os valores foram exibidos em ordem crescente na tela.
Utilizando Streams
Stream é um outro tipo muito comum e muito utilizado no .NET Framework. Normalmente utilizamos Streams para ler ou escrever em disco e para comunicação em rede. O namespace System.IO é o objeto base para se trabalhar com Streams. Para se trabalhar em rede, precisamos importar também os namespaces System.Network.Sockets e se precisarmos encriptar um Stream, necessitamos do namespace System.Security.Cryptography. A Figura 3 mostra a lista de Streams mais utilizados:
Figura 3 – Tipos de Stream mais utilizados.
As classes mais simples do Stream são StreamReader e StreamWriter, que tem a função de ler e escrever arquivos texto. A Listagem 7 demostra a utilização destas classes. Para este exemplo, é necessário importar o namespace System.IO.
using System;
using System.Collections.Generic;
using System.Text;
using System.IO;
namespace ReferenceTypes
{
class Program
{
static void Main(string[] args)
{
// C#
try
{
StreamReader sr = new StreamReader(@"C:\boot.ini");
Console.WriteLine(sr.ReadToEnd());
Console.ReadKey();
}
catch (Exception ex)
{
// Se não for possível ler o arquivo, mostra um erro
Console.WriteLine("Erro ao ler arquivo: " + ex.Message);
Console.ReadKey();
}
}
}
}
Listagem 7 – Lendo um arquivo com a classe StremReader.
Note que é extremamente importante a utilização do Try/Catch (Tratamento de Erros) já que poderão ocorrer situações onde o arquivo não existe ou não pode ser lido.
A classe StreamWriter é utilizada para gravar informações em um arquivo texto, e seu exemplo pode ser visto na Listagem 8.
using System;
using System.Collections.Generic;
using System.Text;
using System.IO;
namespace ReferenceTypes
{
class Program
{
static void Main(string[] args)
{
// C#
StreamWriter sw = new StreamWriter(@"C:\newtextfile.txt");
try
{
sw.WriteLine("Escrevi uma linha no arquivo");
Console.WriteLine("Arquivo alterado com sucesso!");
Console.ReadKey();
}
catch (Exception ex)
{
Console.WriteLine("Erro ao escrever no arquivo: " + ex.Message);
Console.ReadKey();
}
finally
{
sw.Close();
}
}
}
}
Listagem 8 – Escrevendo um arquivo com a classe StreamWriter.
Note que na declaração do StremWriter, podemos informar se vamos adicionar uma linha ao fim de um arquivo existente ou se vamos sobrescrever o arquivo. Também é importante saber que se o arquivo informado na declaração do StreamWriter não existir, o mesmo será criado.
Nos exemplos acima, utilizamos os blocos Try/Catch para tratamento de erro. Caso algum erro ocorra na leitura ou gravação de um arquivo (Coisa muito comum), o bloco Catch será disparado, e caso nenhum erro ocorra, o bloco Catch não será disparado. O bloco Finally será disparado independentemente de ter havido erro ou não. Para melhor entendimento, tome o exemplo de escrita de arquivo, onde abrimos um arquivo para escrever dentro dele. Porém, se ocorrer ou não erros, este arquivo deve ser fechado, caso contrário, o mesmo ficará indisponível para ser utilizado, pois se encontra em uso pelo seu aplicativo. O mesmo se aplica a banco de dados, onde em alguns casos, precisamos nos assegurar que a conexão será fechada, mesmo que um erro ocorra.
Conclusão
Podemos ver notar que há uma grande diferença entre Value Types e Reference types, sendo que os Reference Types contém apenas o endereço do dado na memória. Vimos também, os principais Built-in Reference Types. Aprendemos a trabalhar com a classe String e StringBuilder e pudemos notar grandes diferenças em performance e aprender boas práticas para se trabalhar com Strings. Vimos rapidamente como criar arrays, trabalhar com Streams e utilizar tratamento de erros na leitura e escrita de arquivos.
Referências
·
MSDN Brasil –
André Baltieri - Trabalha com desenvolvimento de aplicações web a mais de 7 anos, e com ASP.NET desde 2003. É líder da comunidade Inside .NET (http://www.insidedotnet.com.br/) e do projeto Learn MVC .NET (http://learn-mvc.net/). Bacharelando em Sistemas de Informação, atualmente trabalha com desenvolvimento e suporte de aplicações web em ASP.NET/C# em projetos internacionais e ministra treinamentos e consultorias sobre a plataforma .NET.