O sistema de tipos C#

O C# é uma linguagem fortemente tipada. Todas as variáveis e constantes têm um tipo, assim como cada expressão que é avaliada como um valor. Cada declaração de método especifica um nome, o tipo e o tipo (valor, referência ou saída) para cada parâmetro de entrada e para o valor de retorno. A biblioteca de classes .NET define tipos numéricos internos e tipos complexos que representam uma ampla variedade de construções. Isso inclui 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 dos tipos definidos pelo usuário que modelam os conceitos específicos para o domínio do problema do programa.

As informações armazenadas em um tipo podem incluir os seguintes itens:

  • O espaço de armazenamento que uma variável do tipo requer.
  • Os valores mínimo e máximo que ele pode representar.
  • Os membros (métodos, campos, eventos e etc.) que ele contém.
  • O tipo base do qual ele herda.
  • As interfaces que ela implementa.
  • Os tipos de operações que são permitidos.

O compilador usa informações de tipo para garantir que todas as operações executadas em seu código sejam de tipo seguro. Por exemplo, se você declarar uma variável do tipo int , o compilador permitirá que você use a variável em operações de adição e subtração. Se você tentar executar essas mesmas operações em uma variável do tipo bool , o compilador gerará um erro, conforme mostrado no exemplo a seguir:

int a = 5;
int b = a + 2; //OK

bool test = true;

// Error. Operator '+' cannot be applied to operands of type 'int' and 'bool'.
int c = a + test;

Observação

Os desenvolvedores de C e C++, observe que, em C#, bool não são conversíveis para int .

O compilador insere as informações de tipo no arquivo executável como metadados. O CLR (Common Language Runtime) usa esses metadados em tempo de execução para assegurar mais segurança de tipos ao alocar e recuperar a memória.

Especificando tipos em declarações de variável

Quando você declara uma variável ou constante em um programa, deve especificar seu tipo ou usar a var palavra-chave para permitir que o compilador inferir o tipo. O exemplo a seguir mostra algumas declarações de variáveis que usam tipos numéricos internos e tipos complexos definidos pelo usuário:

// Declaration only:
float temperature;
string name;
MyClass myClass;

// Declaration with initializers (four examples):
char firstLetter = 'C';
var limit = 3;
int[] source = { 0, 1, 2, 3, 4, 5 };
var query = from item in source
            where item <= limit
            select item;

Os tipos de parâmetros de método e valores de retorno são especificados na declaração do método. A assinatura a seguir mostra um método que requer um int como um argumento de entrada e retorna uma cadeia de caracteres:

public string GetName(int ID)
{
    if (ID < names.Length)
        return names[ID];
    else
        return String.Empty;
}
private string[] names = { "Spencer", "Sally", "Doug" };

Depois de declarar uma variável, você não pode redeclará-la com um novo tipo e não pode atribuir um valor não compatível com seu tipo declarado. Por exemplo, você não pode declarar um int e, em seguida, atribuir a ele um valor booliano de true . No entanto, os valores podem ser convertidos em outros tipos, por exemplo, quando são atribuídos a novas variáveis ou passados como argumentos de método. Uma conversão de tipo que não causa a perda de dados é executada automaticamente pelo compilador. Uma conversão que pode causar perda de dados requer um cast no código-fonte.

Para obter mais informações, consulte Conversões Cast e Conversões de Tipo.

Tipos internos

O C# fornece um conjunto padrão de tipos internos. Elas representam inteiros, valores de ponto flutuante, expressões booleanas, caracteres de texto, valores decimais e outros tipos de dados. Também há tipos string e object internos. Esses tipos estão disponíveis para uso em qualquer programa em C#. Para obter a lista completa dos tipos internos, consulte tipos internos.

Tipos personalizados

Você usa as struct construções,, class interface , enum e record para criar seus próprios tipos personalizados. A própria biblioteca de classes do .NET é uma coleção de tipos personalizados que você pode usar em seus próprios aplicativos. Por padrão, os tipos usados com mais frequência na biblioteca de classes estão disponíveis em qualquer programa em C#. Outras se tornam disponíveis somente quando você adiciona explicitamente uma referência de projeto ao assembly que as define. Depois que o compilador tiver uma referência ao assembly, você pode declarar variáveis (e constantes) dos tipos declarados nesse assembly no código-fonte. Para saber mais, confira Biblioteca de classes do .NET.

O Common Type System

É importante entender dois pontos fundamentais sobre o sistema de tipos no .NET:

  • Ele dá suporte ao conceito de herança. Os tipos podem derivar de outros tipos, chamados tipos base. O tipo derivado herda (com algumas restrições) os métodos, as propriedades e outros membros do tipo base. O tipo base, por sua vez, pode derivar de algum outro tipo, nesse caso, o tipo derivado herda os membros de ambos os tipos base na sua hierarquia de herança. Todos os tipos, incluindo tipos numéricos internos, como System.Int32 (palavra-chave c#: int ), derivam, por fim, de um único tipo base, que é System.Object (palavra-chave c#: object ). Essa hierarquia unificada de tipos é chamada de CTS (Common Type System). Para obter mais informações sobre herança em C#, consulte Herança.
  • 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 do .NET e também seus próprios tipos definidos pelo usuário. Os tipos que você define usando a struct palavra-chave são tipos de valor; todos os tipos numéricos internos são structs . Os tipos que você define usando a class record palavra-chave ou são tipos de referência. Os tipos de referência e os tipos de valor têm diferentes regras de tempo de compilação e comportamento de tempo de execução diferente.

A ilustração a seguir mostra a relação entre tipos de referência e tipos de valor no CTS.

Captura de tela que mostra de tipos de valor CTS e tipos de referência.

Observação

Você pode ver que os tipos mais usados normalmente são todos organizados no namespace System. No entanto, o namespace no qual um tipo está contido não tem relação com a possibilidade de ele ser um tipo de valor ou um tipo de referência.

Classes e structs são duas das construções básicas do Common Type System no .NET. O C# 9 adiciona registros, que são um tipo de classe. Cada um é, essencialmente, uma estrutura de dados que encapsula um conjunto de dados e os comportamentos que são uma unidade lógica. Os dados e comportamentos são os Membros da classe, estrutura ou registro. Os membros incluem seus métodos, propriedades, eventos e assim por diante, conforme listado posteriormente neste artigo.

Uma declaração de classe, estrutura ou registro é como uma planta usada para criar instâncias ou objetos em tempo de execução. Se você definir uma classe, struct ou registro chamado Person , Person será o nome do tipo. Se você declarar e inicializar um p variável do tipo Person, p será considerado um objeto ou uma instância de Person. Várias instâncias do mesmo tipo 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 por meio 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 está atribuído contém os dados reais do struct. Quando a struct é atribuída a uma nova variável, ela é copiada. A nova variável e a variável original, portanto, contêm duas cópias separadas dos mesmos dados. As alterações feitas em uma cópia não afetam a outra cópia.

Tipos de registro podem ser tipos de referência ( record class ) ou tipos de valor ( record struct ).

Em geral, as classes são usadas para modelar um comportamento mais complexo. Normalmente, as classes armazenam dados que devem ser modificados depois que um objeto de classe é criado. As structs são mais adequadas para estruturas de dados pequenas. Normalmente, as estruturas armazenam dados que não devem ser modificados após a criação da estrutura. Os tipos de registro são estruturas de dados com membros sintetizados adicionais do compilador. Normalmente, os registros armazenam dados que não devem ser modificados depois que o objeto é criado.

Tipos de valor

Os tipos de valor derivam de System.ValueType, que deriva de System.Object. Os tipos que derivam de System.ValueType apresentam um comportamento especial no CLR. Variáveis de tipo de valor contêm diretamente seus valores. A memória de um struct é alocada embutida em qualquer contexto que a variável for declarada. Não há nenhuma alocação de heap separada ou sobrecarga de coleta de lixo para variáveis de tipo de valor. Você pode declarar record struct tipos que são tipos de valor e incluir os membros sintetizados para registros.

Há duas categorias de tipos de valor: struct e enum .

Os tipos numéricos internos são structs e 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 eles fossem tipos simples não agregados:

byte num = 0xA;
int i = 5;
char c = 'Z';

Os tipos de valor são lacrados. Você não pode derivar um tipo de qualquer tipo de valor, por exemplo System.Int32 . Você não pode definir uma struct para herdar de qualquer classe definida pelo usuário ou struct porque uma struct só pode herdar de System.ValueType . No entanto, um struct pode implementar uma ou mais interfaces. Você pode converter um tipo struct em qualquer tipo de interface que ele implementa. Essa conversão faz com que uma operação Boxing envolva a struct dentro de um objeto de tipo de referência no heap gerenciado. As operações de conversão boxing ocorrem quando você passa um tipo de valor para um método que usa um System.Object ou qualquer tipo de interface como parâmetro de entrada. Para obter mais informações, consulte Conversões boxing e unboxing.

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;
    }
}

Para obter mais informações sobre structs, consulte tipos de estrutura. Para obter mais informações sobre tipos de valor, consulte tipos de valor.

A outra categoria de tipos de valor é enum . Uma enum define um conjunto de constantes integrais nomeadas. Por exemplo, a enumeração System.IO.FileMode na biblioteca de classes do .NET contém um conjunto de números inteiros constantes nomeados que especificam como um arquivo deve ser aberto. Ele é definido como mostrado no exemplo a seguir:

public enum FileMode
{
    CreateNew = 1,
    Create = 2,
    Open = 3,
    OpenOrCreate = 4,
    Truncate = 5,
    Append = 6,
}

A System.IO.FileMode.Create constante tem um valor de 2. No entanto, o nome é muito mais significativo para as pessoas que lêem o código-fonte e, por esse motivo, é melhor usar enumerações em vez de números literais constantes. Para obter mais informações, consulte System.IO.FileMode.

Todas as enumerações herdam de System.Enum, que herda de System.ValueType. Todas as regras que se aplicam a structs também se aplicam a enums. Para obter mais informações sobre enums, consulte tipos de enumeração.

Tipos de referência

Um tipo que é definido como um class , record , delegate , matriz ou interface é um reference type .

Ao declarar uma variável de a reference type , ela contém o valor null até que você a atribua a uma instância desse tipo ou crie uma usando o new operador. A criação e a atribuição de uma classe são demonstradas no exemplo a seguir:

MyClass myClass = new MyClass();
MyClass myClass2 = myClass;

Um interface não pode ser instanciado diretamente usando o new operador. Em vez disso, crie e atribua uma instância de uma classe que implementa a interface. Considere o exemplo a seguir:

MyClass myClass = new MyClass();

// Declare and assign using an existing value.
IMyInterface myInterface = myClass;

// Or create and assign a value in a single statement.
IMyInterface myInterface2 = new MyClass();

Quando o objeto é criado, a memória é alocada no heap gerenciado. A variável mantém apenas uma referência ao local do objeto. Os tipos no heap gerenciado exigem sobrecarga quando são alocados e quando são recuperados. A coleta de lixo é a funcionalidade de gerenciamento automático de memória do CLR, que executa a recuperação. No entanto, a coleta de lixo também é altamente otimizada e, na maioria dos cenários, ela não cria um problema de desempenho. Para obter mais informações sobre a coleta de lixo, consulte Gerenciamento automático de memória.

Todas as matrizes são tipos de referência, mesmo se seus elementos forem tipos de valor. As matrizes derivam implicitamente da System.Array classe. Você declara e usa-os com a sintaxe simplificada fornecida pelo C#, conforme mostrado no exemplo a seguir:

// Declare and initialize an array of integers.
int[] nums = { 1, 2, 3, 4, 5 };

// Access an instance property of System.Array.
int len = nums.Length;

Os tipos de referência dão suporte completo à herança. Quando você cria uma classe, pode herdar de qualquer outra interface ou classe que não esteja definida como sealed. Outras classes podem herdar de sua classe e substituir seus métodos virtuais. Para obter mais informações sobre como criar suas próprias classes, consulte classes, estruturas e registros. Para obter mais informações sobre herança e métodos virtuais, consulte Herança.

Tipos de valores literais

No C#, valores literais recebem um tipo do compilador. Você pode especificar como um literal numérico deve ser digitado anexando uma letra ao final do número. Por exemplo, para especificar que o valor 4.56 deve ser tratado como um float , acrescente "f" ou "f" após o número: 4.56f . Se nenhuma letra for anexada, o compilador inferirá um tipo para o literal. Para obter mais informações sobre quais tipos podem ser especificados com sufixos de letra, consulte tipos numéricos inteiros e tipos numéricos de ponto flutuante.

Como os literais são digitados e todos os tipos derivam System.Object , você pode escrever e compilar um código como o código a seguir:

string s = "The answer is " + 5.ToString();
// Outputs: "The answer is 5"
Console.WriteLine(s);

Type type = 12345.GetType();
// Outputs: "System.Int32"
Console.WriteLine(type);

Tipos genéricos

Um tipo pode ser declarado com um ou mais parâmetros de tipo que servem como um espaço reservado para o tipo real (o tipo concreto). O código do cliente fornece o tipo concreto quando cria uma instância do tipo. Esses tipos são chamados de tipos genéricos. Por exemplo, o tipo .NET System.Collections.Generic.List<T> tem um parâmetro de tipo que, por convenção, recebe o nome T . Ao criar uma instância do tipo, você especifica o tipo dos objetos que a lista conterá, por exemplo string :

List<string> stringList = new List<string>();
stringList.Add("String example");
// compile time error adding a type other than a string:
stringList.Add(4);

O uso do parâmetro de tipo possibilita a reutilização da mesma classe para conter qualquer tipo de elemento sem precisar converter cada elemento em objeto. As classes de coleção genéricas são chamadas de coleções com rigidez de tipos , pois o compilador sabe o tipo específico dos elementos da coleção e pode gerar um erro no momento da compilação se, por exemplo, você tentar adicionar um inteiro ao stringList objeto no exemplo anterior. Para obter mais informações, consulte Genéricos.

Tipos implícitos, tipos anônimos e tipos de valor anulável

Você pode digitar implicitamente uma variável local (mas não membros da classe) usando a var palavra-chave. A variável ainda recebe um tipo em tempo de compilação, mas o tipo é fornecido pelo compilador. Para obter mais informações, consulte Variáveis locais de tipo implícito.

Pode ser inconveniente criar um tipo nomeado para conjuntos simples de valores relacionados que você não pretende armazenar ou passar os limites de método externos. Você pode criar tipos anônimos para essa finalidade. Para obter mais informações, consulte Tipos Anônimos.

Tipos de valor comum não podem ter um valor de null . No entanto, você pode criar tipos de valor anuláveis acrescentando um ? após o tipo. Por exemplo, int? é um int tipo que também pode ter o valor null . Os tipos de valor anuláveis são instâncias do tipo struct genérico System.Nullable<T> . Os tipos de valor anulável são especialmente úteis quando você está passando dados de e para bancos de dado nos quais valores numéricos podem ser null . Para obter mais informações, consulte tipos de valor anulável.

Tipo de tempo de compilação e tipo de tempo de execução

Uma variável pode ter tipos diferentes de tempo de compilação e tempo de execução. O tipo de tempo de compilação é o tipo declarado ou inferido da variável no código-fonte. O tipo de tempo de execução é o tipo da instância referenciada por essa variável. Geralmente, esses dois tipos são os mesmos, como no exemplo a seguir:

string message = "This is a string of characters";

Em outros casos, o tipo de tempo de compilação é diferente, conforme mostrado nos dois exemplos a seguir:

object anotherMessage = "This is another string of characters";
IEnumerable<char> someCharacters = "abcdefghijklmnopqrstuvwxyz";

Nos dois exemplos anteriores, o tipo de tempo de execução é um string . O tipo de tempo de compilação está object na primeira linha e IEnumerable<char> no segundo.

Se os dois tipos forem diferentes para uma variável, é importante entender quando o tipo de tempo de compilação e o tipo de tempo de execução se aplicam. O tipo de tempo de compilação determina todas as ações executadas pelo compilador. Essas ações de compilador incluem resolução de chamada de método, resolução de sobrecarga e conversões implícitas e explícitas disponíveis. O tipo de tempo de execução determina todas as ações que são resolvidas em tempo de execução. Essas ações de tempo de execução incluem a expedição de chamadas de método virtual, avaliação is e switch expressões e outras APIs de teste de tipo. Para entender melhor como seu código interage com tipos, reconheça qual ação se aplica a qual tipo.

Para obter mais informações, consulte os seguintes artigos:

Especificação da linguagem C#

Para obter mais informações, consulte a especificação da linguagem C#. A especificação da linguagem é a fonte definitiva para a sintaxe e o uso de C#.