Struct (por valor)

Permite declarar tipos que contém vários valores, identificados por um nome. Semelhante a struct do C. Exemplo:

1
2
3
4
5
6
7
8
9
public struct Point
{
    public int x, y;
    public Point(int pl, int p2)
    {
          x = pl;
          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 classes são tipos por referência
  • Não podemos declarar um construtor que não aceite argumentos.
  • Podemos atribuir à variável “this”.
  • 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).

Conversões

Você pode usar tipos numéricos diferentes em expressões. Neste caso, as seguintes conversões ocorrem:

  • Quando misturados tipos inteiros e ponto flutuante, os tipos numéricos são convertidos para ponto flutuante.
  • Os tipos inteiros de menor faixa são convertidos para os de maior faixa, por exemplo, de short para int.

As conversões são feitas automaticamente apenas quando o tipo a receber o valor (do lado esquerdo de uma atribuição) puder conter todos os possíveis valores da expressão (do lado direito de uma atribuição). Muitas conversões não são permitidas, como por exemplo:

  • Entre tipos com e sem sinal de variáveis inteiras do mesmo tamanho (por exemplo, entre int e uint).
  • Atribuições de variáveis inteiras com sinal para inteiros sem sinal, qualquer que seja o seu tamanho.
  • Entre o tipo decimal (de grande resolução) e ambos os tipos de ponto flutuante IEEE, double e float (de maior faixa).
  • Atribuir um double a um float.
  • Atribuir números de ponto flutuante a variáveis inteiras.

Caso você precise fazer uma conversão na qual exista perda de informação, você pode forçá-la com um operador de conversão, como no exemplo abaixo:

1
2
3
4
5
6
7
...
long   myLong = 22;
int   myInt = myLong;<span> </span>// Error 
int   myInt = <strong>(int)</strong>myLong; // OK
int   x = 3.0;<span> </span>// Error
int   y = (int)3.0;<span> </span>// OK
...

As conversões automáticas entre os tipos por valor são as seguintes:

De

Para

sbyte

short, int, long, float, double, ou decimal.

byte

short, ushort, int, uint, long, ulong, float, double, ou decimal.

short

int, long, float, double, ou decimal.

ushort

int, uint, long, ulong, float, double, ou decimal.

int

long, float, double, ou decimal.

uint

long, ulong, float, double, ou decimal.

long

float, double, ou decimal.

ulong

float, double, ou decimal.

char

ushort, int, uint, long, ulong, float, double, ou decimal.

float

double.

Decimal (por valor)

Este tipo está bem menos sujeito a erros de arredondamento que ocorrem com os formatos binários. As contas são feitas na base 10

Tipo Implementação
decimal Ponto flutuante decimal de 128 bits. (1.0 x 10-28 a 7.9 x 1028), 28 dígitos decimais de precisão.

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.

Ponto Flutuante (por valor)

Os números de ponto flutuante são bastante convencionais, usando o padrão IEEE de quatro e oito bytes.

Tipo

Implementação

double

Ponto flutuante binário IEEE de 8 bytes (±5.0 x 10-324 a ±1.7 x 10308), 15 dígitos decimais de precisão.
float Ponto flutuante binário LEEE de 4 bytes (±1.5 x 10-45 a ±3.4 x 1038), 7 dígitos decimais de precisão.

As operações de ponto flutuante não geram erros:

  • Uma operação que gere um resultado muito pequeno para ser representado torna-se zero.
  • Uma divisão por zero com números de ponto flutuante não gera erro e sim produz resultado “infinito”. Você pode verificar se o valor é infinito positivo ou negativo com o método Double.Islnfinity().
  • Uma operação inválida gera como resultado NaN (não é um número).
  • Se um dos operadores for NaN, o resultado é NaN.

Caractere (por valor)

Em C#, todos os caracteres (e strings) são armazenados no padrão Unicode e usam 16 bits por caractere. O Unicode permite armazenar os caracteres de todas as línguas vivas (como grego, japonês, chinês e coreano) e algumas mortas (como sânscrito).

Tipo Implementação

char

Um único caractere Unicode de 16 bits. Não é compatível com inteiro.

O que diferencia efetivamente o texto atribuido a um tipo string (que será visto adiante) é que a atribuição de um caracterer é feita envolvendo o texto por aspas simples (‘) enquanto para a string o texto é envolvido por aspas duplas (“). Outras diferenças mais importantes serão vistas adiante.

Lógicos (por valor)

O C#, ao contrário do C/C++, tem um tipo lógico próprio e incompatível com inteiro.

Tipo Implementação
bool Pode ter os valores true e false. Não é compatível com inteiro

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. Os tipos inteiros não podem ser usados nestas situações, como você faria em C++.

Um exemplo de código abaixo usa variáveis de tipo lógico em uma sintaxe bem legível:

1
2
3
4
5
6
7
8
9
10
11
...
bool EValido = true;
if (EValido)
{
	Console.WriteLine("É valido");
}
else
{
	Console.WriteLine("Nada feito");
}
...

Inteiros (por valor)

Os tipos inteiros têm sempre o mesmo significado, independentemente da implementação.

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 551615).
long Inteiro de 64 bits com sinal (-9 223 372 036 854 775 808 a 9 223 372 036 854 775 807).

Tipos no C#

O C# tem duas grandes categorias de tipos: por valor e por referência. Os tipos por valor são gerenciados diretamente. Os tipos por referência são sempre acessados através de ponteiros.

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 são 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.

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 à 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.

Tipos ponteiro

  • São ponteiros que poderão ser utilizados em código “inseguro”(unsafe) para acessar funções de código externo (dll win32).

Este último são herança da flexibilidade que este tipo de dados dá ao C e C++. Será tratado com mais detalhes no decorrer do curso.