Desenvolvimento - C#
Tipos de dados no .NET
Todas as linguagens da arquitetura .NET têm muita coisa em comum. Uma delas é o conjunto de tipos disponíveis. O sistema de tipos do .NET é bastante característico e substancialmente diferente de outras linguagens como C++ e Visual Basic. Nesta série de dois artigos, irei explorar o sistema de tipos e, em especial, as suas características únicas. Usarei a nomenclatura e exemplos em C#, mas os tipos estão também disponíveis em outras linguagens como o Visual Basic...
por Mauro Sant'AnnaTodas as linguagens da arquitetura .NET têm muita coisa em comum. Uma delas é o conjunto de tipos disponíveis. O sistema de tipos do .NET é bastante característico e substancialmente diferente de outras linguagens como C++ e Visual Basic. Nesta série de dois artigos, irei explorar o sistema de tipos e, em especial, as suas características únicas. Usarei a nomenclatura e exemplos em C#, mas os tipos estão também disponíveis em outras linguagens como o Visual Basic.
Semelhanças com o Delphi
Quem estiver acostumado com o Delphi verá muitas semelhanças, já que tanto o Delphi como o C# foram desenvolvidos pela mesma pessoa, Anders Hejlsberg:
- O modelo de objeto é virtualmente o mesmo: todas as classes são derivadas de um ancestral comum chamado object; a herança é simples, mas permite a implementação de múltiplas interfaces; existe um suporte direto a conceitos de componentes como eventos e propriedades, inclusive as propriedades array.
- Todos os objetos são ponteiros (ou referências).
- Existe um tipo lógico forte e incompatível com inteiro, o bool.
- Existem enumerações fortes e incompatíveis com inteiros e entre si.
- As strings e os arrays dinâmicos (array of type) são muito semelhantes.
- Existe um grande suporte a reflections, algo chamado no Delphi de RTTI (Runtime Type Information). Na verdade as reflections do C# vão além do RTTI do Delphi. Existem, contudo, algumas diferenças em relação ao Delphi:
- Os conjuntos (sets) não são suportados no C#.
- Não existem ponteiros diretamente, mas quando atribuirmos um valor qualquer a uma variável to tipo object, este vira implicitamente um ponteiro.
- As variáveis alocadas dinamicamente não precisam ser liberadas, elas estão sujeitas à “coleta de lixo”.
- Existem tipos inteiros de quatro tamanhos: 1, 2, 4 e 8 bytes. Este último não existe no Delphi.
- Os caracteres e strings são no padrão Unicode e usam dois bytes por caractere.
Sistema de tipos
Dividirei os tipos em três categorias: “por valor”, “por referência” e o tipo “string”, que tem algumas características dos dois. Além dos tipos intrínsecos, é possível definir novos tipos, tanto por valor como por referência.
Tipos por valor
Os tipos por valor têm as seguintes características principais:
- São alocados diretamente na pilha.
- Não precisam ser inicializados com o operador new.
- A variável armazena o valor diretamente.
- A atribuição de uma variável a outra copia o conteúdo, criando efetivamente outra cópia da variável.
- Normalmente usados com tipos de pequeno tamanho (menos que 16 bytes), onde o uso de referências traria um custo muito grande.
- Podem ser automaticamente convertidos para referências em um processo chamado “boxing”, descrito na segunda parte deste artigo. Não podem conter o valor null.
Tipo | Implementação |
byte | Inteiro de 8 bits sem sinal (0 a 255). |
sbyte | Inteiro de 8 bits com sinal (-127 a 128). |
ushort | Inteiro de 16 bits sem sinal (0 a 65 535). |
short | Inteiro de 16 bits com sinal (-32 768 a 32 767). |
uint | Inteiro de 32 bits sem sinal (0 a 4 294 967 295). |
int | Inteiro de 32 bits com sinal (-2 147 483 648 a 2 147 483 647). |
ulong | Inteiro de 64 bits sem sinal (0 a 18 446 744 073 709 551 615). |
long | Inteiro de 64 bits com sinal (-9 223 372 036 854 775 808 a 9 223 372 036 854 775 807). |
double | Ponto flutuante binário IEEE de 8 bytes (±5.0×10-324 a ±1.7×10308), 15 dígitos decimais de precisão. |
float | Ponto flutuante binário IEEE de 4 bytes (±1.5×10-45 a ±3.4×1038), 7 dígitos decimais de precisão. |
decimal | Ponto flutuante decimal de 128 bits. (1.0×10-28 a 7.9×1028), 28 dígitos decimais de precisão. |
bool | Pode ter os valores true e false. Não é compatível com inteiro. |
char | Um único caractere Unicode de 16 bits. Não é compatível com inteiro. |
Tabela 1: Tipos de dados por valor
Os tipos inteiros vêm em quatro tamanhos: um, dois, quatro e oito bytes, com e sem sinal. Os dois tipos de ponto flutuante IEEE não oferecem nenhuma novidade.
O tipo decimal é uma novidade: ele tem 28 dígitos de precisão e as contas são menos propensas a erros de arredondamento comuns aos formatos IEEE e odiados por quem cria software de contabilidade. Por exemplo, com o decimal 14,2 mais 0,2 dá 14,4 ao invés de 14,399999999999999 que você obteria com o tipo double. Quem usou o Turbo Pascal 3.0 talvez se lembre de uma versão especial que suportava o tipo BCD, que tinha características semelhantes. Em uma conversa com Anders Hejlsberg, consegui arrancar dele algumas risadas com a comparação.
O tipo bool armazena os valores true e false. As expressões lógicas como if e while esperam sempre uma expressão do tipo bool.
O tipo char armazena caracteres no padrão Unicode. Este padrão inclui letras em diversos alfabetos além dos “romanos” usados no ocidente. Ele inclui os alfabetos Cirílico, Árabe, Hebraico, Chinês, Japoneses, Coreano e Sânscrito.
Tipos por valor definidos pelo usuário
No C# você pode declarar novos tipos, de maneira semelhante ao Delphi.
enum
Permite declarar uma seqüência de identificadores associados, mas incompatíveis com inteiros e com outras enumerações. Praticamente idêntico às enumerações do Delphi. Exemplo:
enum Days {Sun, Mon, Tue, Wed, Thu, Fri, Sat};
struct
Permite declarar tipos que contém vários valores, identificados por um nome. Semelhante ao record do Delphi. Exemplo:
Listagem 1: Declarando tipos de valores.
public struct Point { public int x, y; public Point(int p1, int p2) { x = p1; y = p2; } }
As structs possuem algumas características em comum com as classes:
- Podem ter métodos.
- Podem ter construtores. Entretanto, existem diferenças em relação às classes:
- Elas são tipos por valor enquanto as classes são tipos por referência.
- Não podemos declarar um construtor que não aceite argumentos.
- Podemos atribuir à variável this, correspondente ao self no Delphi;
- Não suportam herança; elas são implicitamente sealed.
As structs fornecem uma alternativa mais “leve e barata” às classes, onde o custo do uso das classes (alocação dinâmica de memória, métodos virtuais e uso de ponteiros) seria muito caro. Por exemplo, um ponto (coordenada X, Y).
Tipos por referência
Os tipos por referência têm as seguintes características principais:
- São alocados em um heap e sujeitos à “coleta de lixo” (“garbage collection”) quando não forem mais usados.
- Devem ser inicializados com o operador new.
- A variável armazena uma “referência”, uma espécie de ponteiro; o conteúdo em si fica no heap.
- A atribuição de uma variável a outra copia a referência; podemos ter muitas variáveis referindo-se ao mesmo valor.
- Normalmente usados com tipos de grande tamanho (mais que 16 bytes), onde o custo da alocação dinâmica é relativamente pequeno frente a sua flexibilidade.
- Podem conter o valor null, embora se usarmos uma variável com o valor null a exception NullReferenceException será gerada.
Object
Tipo intrínseco. É a classe base de todas as demais, como o TObject é base no Delphi. Uma variável do tipo object pode conter valores de qualquer tipo. Os tipos por referência são armazenados diretamente; os tipos por valor são alvo de “boxing”.
Arrays
Um array é sempre criado dinamicamente em tempo de execução. Podemos ter arrays de várias dimensões e arrays de arrays. Veja um exemplo de criação e inicialização de um array de inteiros de uma dimensão:
Listagem 2: Demosntração de um array
int[] myIntArray = new int[5] { 1, 2, 3, 4, 5 };
Class
Tipo definido pelo usuário e correspondem a uma class no Delphi As classes são sempre derivadas de object e podem conter campos, métodos e propriedades. Uma classe pode derivar de uma única outra classe, e também de várias interfaces. Veja um exemplo:
Listagem 3: Classe Tempo
public class Tempo { protected int H; protected int M; protected int S; public Tempo() { Ajusta(0, 0, 0); } public Tempo(int _H, int _M, int _S) { Ajusta(_H, _M, _S); } public void Ajusta(int _H, int _M, int _S) { H = _H; M = _M; S = _S; Normaliza(); } public string ParaString() { return string.Format("{0}:{1}:{2}", new object[] {H, M, S}); } void Normaliza() { M = M + S / 60; S = S % 60; H = H + M / 60; M = M % 60; } public double Hora { get { return H + (M / 60.0) + (S / 3600.0); } set { M = 0; S = 0; H = (int) value; double Sobra = value - H; S = (int)(Sobra * 3600); Normaliza(); } } override public string ToString() { return ParaString(); } }
Interface
Tipo definido pelo usuário. Uma interface é uma espécie de classe, mas contém apenas os “protótipos” dos métodos, sem a sua implementação. Corresponde no Delphi a uma interface. Uma classe, além de ser derivada de outra, pode implementar várias interfaces. Veja um exemplo:
Listagem 4: Interface derivada
// Declara a interface interface IControl { void Paint(); } // Cria um interface derivada interface ITextBox: IControl { void SetText(string text); } // A classe implementa a interface class TextBox: ITextBox { void IControl.Paint() {...} void ITextBox.SetText(string text) {...}
Delegate
Tipo definido pelo usuário. É um “ponteiro de função orientado a objeto. Podemos atribuir uma lista de métodos a um delegate e chamá-los ao invocar o delegate. O delegate corresponde mais ou menos a um “procedure of object” do Delphi, mas pode também apontar para métodos static (métodos class no Delphi) e para uma lista de métodos.Veja um exemplo:
Listagem 5: Exemplo de delegate
// Declara um delegate. É um método que não aceita argumentos e retorna inteiros delegate int MyDelegate(); // Declara uma classe. Note que os métodos tem a mesma “assinatura” do delegate acima: // retornam um inteiro e não aceitam argumentos public class MyClass { public int InstanceMethod () { Console.WriteLine("A message from the instance method."); return 0; } static public int StaticMethod () { Console.WriteLine("A message from the static method."); return 0; } } public class MainClass { static public void Main () { MyClass p = new MyClass(); // Mapeia o delegate ao método da classe criada acima MyDelegate d = new MyDelegate(p.InstanceMethod); // Chama o método via delegate d(); // Mapeia outro método (agora é static) d = new MyDelegate(MyClass.StaticMethod); // Chama o método via delegate d(); } }
Tipo string
As strings são tecnicamente um tipo por referência, mas possuem algumas características especiais:
- Não precisam ser inicializadas com o operador new.
- A atribuição de uma variável a outra funciona como se copiasse o conteúdo, criando efetivamente outra cópia da variável.
- Uma string contendo o valor null é uma string vazia; não é um erro usá-la.
- Você não pode criar uma classe derivada de string.
As strings contêm caracteres Unicode e podem ter até 1G de comprimento. Veja um exemplo:
Listagem 6: Utilizando Strings
// Declara e inicializa uma string string Name = "Mary"; // Copia para outra string. Se alterarmos uma delas, a outra manterá o seu valor string NewName = Name; // Atribui à string antiga Name = "John"; // Exibe "John - Mary" System.Console.WriteLine(Name + " - " + NewName);
Conclusão
O C# contém uma estrutura de tipos baseada no Delphi, mas com várias novidades que o tornam mais simples e ao mesmo tempo mais poderoso.