Examine o sistema de tipo .NET
C# é uma linguagem fortemente tipada. Cada variável e constante tem um tipo, assim como toda expressão que avalia um valor. A biblioteca de classes .NET define tipos numéricos internos e tipos complexos que representam uma ampla variedade de construções. Essas construções incluem o sistema de arquivos, conexões de rede, coleções e matrizes de objetos e datas. Um programa C# típico usa tipos da biblioteca de classes e tipos definidos pelo usuário que modelam os conceitos que são específicos para o domínio do problema do programa.
Tipos incorporados
O C# fornece um conjunto padrão de tipos internos. Esses tipos padrão representam inteiros, valores de ponto flutuante, expressões booleanas, caracteres de texto, valores decimais e outros tipos de dados. Há também tipos de cadeia de caracteres e objetos internos. Esses tipos estão disponíveis para você usar em qualquer programa C#.
Tipos personalizados
Use as construções struct, class, interface, enume record para criar seus próprios tipos personalizados. A biblioteca de classes .NET em si é uma coleção de tipos personalizados que você pode usar em seus próprios aplicativos. Por padrão, os tipos usados com mais freqüência na biblioteca de classes estão disponíveis em qualquer programa C#. Outros ficam disponíveis somente quando você adiciona explicitamente uma referência de projeto ao assembly que os define. Depois que o compilador tiver uma referência ao assembly, você poderá declarar variáveis (e constantes) dos tipos declarados nesse assembly no código-fonte.
Sistema de tipo comum
É importante entender dois pontos fundamentais sobre o sistema de tipos no .NET:
Apoia o princípio da herança. Os tipos podem derivar de outros tipos, chamados tipos base. O tipo derivado herda (com algumas restrições) os métodos, propriedades e outros membros do tipo base. O tipo base, por sua vez, pode derivar de algum outro tipo, caso em que o tipo derivado herda os membros de ambos os tipos base em sua hierarquia de herança. Todos os tipos, incluindo tipos numéricos internos, como System.Int32 (palavra-chave C#: int), derivam, em última análise, de um único tipo base, que é
System.Object(palavra-chave C#:object). Essa hierarquia de tipo unificada é chamada de Common Type System (CTS).Cada tipo no CTS é definido como um tipo de valor ou um tipo de referência. Esses tipos incluem todos os tipos personalizados na biblioteca de classes .NET e também seus próprios tipos definidos pelo usuário. Os tipos que você define usando a palavra-chave struct são tipos de valor; Todos os tipos numéricos embutidos são structs. Os tipos que você define usando a palavra-chave class ou record são tipos de referência. Tipos de referência e tipos de valor têm regras de tempo de compilação diferentes e comportamento de tempo de execução diferente.
A ilustração a seguir mostra a relação entre tipos de valor e tipos de referência no CTS.
Classes e estruturas são duas das construções básicas do sistema de tipo comum no .NET. Cada um é essencialmente uma estrutura de dados que encapsula um conjunto de dados e comportamentos que pertencem juntos como uma unidade lógica. Os dados e comportamentos são os membros do class, structou record. Os membros de uma classe incluem propriedades (dados), métodos (comportamentos), campos (variáveis declaradas dentro da classe) e muitos outros.
Uma declaração class, structou record é como um esquema usado para criar instâncias ou objetos em tempo de execução. Se você definir uma classe chamada Person, Person é o nome do tipo. Se você declarar e inicializar uma variável p do tipo Person, p é dito ser um objeto ou instância de Person. Várias instâncias do mesmo tipo de Person podem ser criadas, e cada instância pode ter valores diferentes em suas propriedades e campos.
Uma classe é um tipo de referência. Quando um objeto do tipo é criado, a variável à qual o objeto é atribuído mantém apenas uma referência a essa memória. Quando a referência de objeto é atribuída a uma nova variável, a nova variável refere-se ao objeto original. As alterações feitas através de uma variável são refletidas na outra variável porque ambas se referem aos mesmos dados.
Um struct é um tipo de valor. Quando um struct é criado, a variável à qual o struct é atribuído contém os dados reais do struct. Quando o struct é atribuído a uma nova variável, o valor de dados é copiado. A nova variável e a variável original contêm, portanto, duas cópias separadas dos mesmos dados. As alterações feitas em uma cópia não afetam a outra.
Os tipos de registo podem ser tipos de referência (record class) ou tipos de valor (record struct). Os tipos de registro contêm métodos que oferecem suporte à igualdade de valor.
Em geral, as classes são usadas para modelar comportamentos mais complexos. As classes normalmente armazenam dados que se destinam a ser modificados depois que um objeto de classe é criado. As estruturas são mais adequadas para pequenas estruturas de dados. As estruturas normalmente armazenam dados que não se destinam a ser modificados após a criação do struct. Os tipos de registro são estruturas de dados com outros membros sintetizados do compilador. Os registros geralmente armazenam dados que não se destinam a ser modificados após a criação do objeto.
Tipos de valor
Os tipos de valor derivam de System.ValueType, que deriva de System.Object. Os tipos que derivam de System.ValueType têm um comportamento especial no common language runtime. As variáveis de tipo de valor contêm diretamente seus valores. A memória de um struct é alocada em linha em qualquer contexto em que a variável seja declarada. Não há alocação de heap separada ou sobrecarga de coleta de lixo para variáveis de tipo de valor. Você pode declarar tipos de estrutura de registro que são tipos de valor e incluir os membros sintetizados para registros.
Existem duas categorias de tipos de valor: struct e enum.
Os tipos numéricos internos são structs, e eles têm campos e métodos que você pode acessar:
// constant field on type byte.
byte b = byte.MaxValue;
Mas você declara e atribui valores a eles como se fossem tipos simples não agregados:
byte num = 0xA;
int i = 5;
char c = 'Z';
Os tipos de valor são selados. Não é possível derivar um tipo de qualquer tipo de valor, por exemplo, System.Int32. Não é possível definir um struct herdar de qualquer class ou struct definido pelo usuário porque um struct só pode herdar de System.ValueType.
Você usa a palavra-chave struct para criar seus próprios tipos de valor personalizados. Normalmente, um struct é usado como um contêiner para um pequeno conjunto de variáveis relacionadas, conforme mostrado no exemplo a seguir:
public struct Coords
{
public int x, y;
public Coords(int p1, int p2)
{
x = p1;
y = p2;
}
}
A outra categoria de tipos de valor é enum. Um enum define um conjunto de constantes integrais nomeadas. Por exemplo, a enumeração System.IO.FileMode na biblioteca de classes .NET contém um conjunto de inteiros constantes nomeados que especificam como um arquivo deve ser aberto. A sintaxe de um enum é mostrada no exemplo a seguir:
public enum FileMode
{
CreateNew = 1,
Create = 2,
Open = 3,
OpenOrCreate = 4,
Truncate = 5,
Append = 6,
}
A constante System.IO.FileMode.Create tem um valor de 2. No entanto, o nome é muito mais significativo para os seres humanos que leem o código-fonte e, por essa razão, é melhor usar enumerações em vez de números literais constantes.
Todos os enums herdam de System.Enum, que herda de System.ValueType. Todas as regras que se aplicam a structs também se aplicam a enums.
Tipos de referência
Os class, record, delegate, array e interface são tipos de referência.
Quando você declara uma variável de um tipo de referência, ela contém o valor null até que você a atribua com uma instância desse tipo ou crie uma usando o operador new.
O exemplo a seguir demonstra como declarar variáveis de tipo de referência usando matrizes:
// Declaring an array variable
int[] numbers;
// Initializing the array with a size of 5
numbers = new int[5];
// Alternatively, declaring and initializing an array in one line
int[] numbers2 = new int[] { 1, 2, 3, 4, 5 };
// Assigning a reference to another variable
int[] numbers3 = numbers2;
O operador new cria uma instância do tipo e retorna uma referência a essa instância. A referência é o endereço de memória do objeto, e essa referência é armazenada na variável. Ao atribuir uma variável de tipo de referência a outra variável, você está copiando a referência, não o objeto em si. Ambas as variáveis se referem ao mesmo objeto na memória.
Observação
Além de serem tipos de referência, as matrizes são coleções. As coleções podem ser inicializadas usando expressões de coleção, que eliminam a necessidade de incluir a palavra-chave new ao declarar e inicializar uma matriz em uma linha. Por exemplo: int[] numbers = [ 1, 2, 3, 4, 5 ];.
Você pode criar uma instância de uma classe usando a mesma sintaxe usada para instanciar os tipos internos. O exemplo a seguir demonstra como criar uma instância de uma classe:
MyClass myClass = new MyClass();
MyClass myClass2 = myClass;
O operador new cria uma instância da classe e retorna uma referência a essa instância. A referência é o endereço de memória do objeto, e essa referência é armazenada na variável. Ao atribuir uma variável de tipo de referência a outra variável, você está copiando a referência, não o objeto em si. Ambas as variáveis se referem ao mesmo objeto na memória.