Partilhar via


O sistema de tipo C#

C# é uma linguagem fortemente tipada. Cada variável e constante tem um tipo, assim como toda expressão que avalia um valor. Cada declaração de método especifica um nome, o tipo e a natureza (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 tipos definidos pelo usuário que modelam os conceitos que são 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áximos e mínimos que pode representar.
  • Os membros (métodos, campos, eventos e assim por diante) que ele contém.
  • O tipo base do qual herda.
  • As interfaces que implementa.
  • As operações permitidas.

O compilador usa informações de tipo para garantir que todas as operações executadas em seu código sejam seguras para tipos. 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

Desenvolvedores de C e C++, observe que em C#, bool não é conversível em int.

O compilador incorpora as informações de tipo no arquivo executável como metadados. O Common Language Runtime (CLR) usa esses metadados em tempo de execução para garantir ainda mais a segurança do tipo quando aloca e recupera memória.

Especificando tipos em declarações de variáveis

Quando você declara uma variável ou constante em um programa, você deve especificar seu tipo ou usar a var palavra-chave para permitir que o compilador infera 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 de método. A assinatura a seguir mostra um método que requer um int argumento como 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-lhe um valor booleano 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 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 Transmissão e conversões de tipo.

Tipos incorporados

O C# fornece um conjunto padrão de tipos internos. Eles representam inteiros, valores de ponto flutuante, expressões booleanas, caracteres de texto, valores decimais e outros tipos de dados. Existem também tipos integrados string e object. Esses tipos estão disponíveis para você usar em qualquer programa C#. Para obter a lista completa dos tipos internos, consulte Tipos internos.

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. Para obter mais informações, consulte Biblioteca de classes .NET.

Uma das primeiras decisões que você toma ao definir um tipo é decidir qual construção usar para seu tipo. A lista a seguir ajuda a tomar essa decisão inicial. Há sobreposição nas escolhas. Na maioria dos cenários, mais de uma opção é uma escolha razoável.

  • Se o tamanho do armazenamento de dados for pequeno, não mais de 64 bytes, escolha um struct ou record struct.
  • Se o tipo for imutável, ou se você quiser uma mutação não destrutiva, escolha um struct ou record struct.
  • Se o seu tipo deve ter semântica de valor para igualdade, escolha um record class ou record struct.
  • Se o tipo for usado principalmente para armazenar dados, não comportamento, escolha um record class ou record struct.
  • Se o tipo fizer parte de uma hierarquia de herança, escolha entre record class ou class.
  • Se o tipo usa polimorfismo, escolha um class.
  • Se o principal objetivo for o comportamento, escolha um class.

O 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). 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 .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 palavra-chave 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.

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

Observação

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

Classes e structs 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 da classe, struct ou registro. Os membros incluem seus métodos, propriedades, eventos e assim por diante, conforme listado mais adiante neste artigo.

Uma declaração de classe, struct ou record é como um blueprint usado para criar instâncias ou objetos em tempo de execução. Se você definir uma classe, struct ou registro chamado 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 uma struct é criada, a variável à qual a struct é atribuída contém os dados reais da struct. Quando a struct é atribuída a uma nova variável, ela é copiada. 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 da estrutura. Os tipos de registro são estruturas de dados com membros adicionais sintetizados pelo 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. Tipos que derivam de System.ValueType têm um comportamento especial no CLR. As variáveis de tipo de valor contêm diretamente seus valores. A memória de uma 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 do tipo valor. Você pode declarar record struct tipos 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 integrados são structs, e têm campos e métodos que se podem 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 uma struct para herdar de qualquer classe ou struct definida pelo usuário 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 para qualquer tipo de interface que ele implemente. Essa conversão faz com que uma operação de boxe envolva a estrutura dentro de um objeto de tipo de referência no heap gerenciado. As operações de encaixotamento ocorrem quando um tipo de valor é passado para um método que aceita um System.Object ou qualquer tipo de interface como parâmetro de entrada. Para obter mais informações, consulte "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. 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. É definido como mostrado 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. Para obter mais informações, consulte System.IO.FileMode.

Todos os enums herdam de System.Enum, que herda de System.ValueType. Todas as regras que se aplicam às estruturas também se aplicam aos 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.

Quando você declara uma variável de um reference type, ele contém o valor null até que você o atribua com 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 seguinte exemplo:

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 contém apenas uma referência à localização 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 gestão automática de memória do CLR, que executa a reclamação. No entanto, a coleta de lixo também é altamente otimizada e, na maioria dos cenários, 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 que seus elementos sejam tipos de valor. As matrizes derivam implicitamente da classe System.Array. Você os declara e usa 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 suportam totalmente a herança. Ao criar uma classe, você pode herdar de qualquer outra interface ou classe que não esteja definida como selada. 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, structs e records. Para obter mais informações sobre herança e métodos virtuais, consulte Herança.

Tipos de valores literais

Em C#, os 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 um "f" ou "F" após o número: 4.56f. Se nenhuma letra for acrescentada, o compilador infere um tipo para o literal. Para obter mais informações sobre quais tipos podem ser especificados com sufixos de letra, consulte Tipos numéricos integrais e Tipos numéricos de vírgula flutuante.

Como os literais são digitados e todos os tipos derivam, em última análise, do System.Object, você pode escrever e compilar 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 System.Collections.Generic.List<T> .NET 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 contém, 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 type torna possível reutilizar a mesma classe para manter qualquer tipo de elemento, sem ter que converter cada elemento em objeto. As classes de coleção genéricas são chamadas de coleções fortemente tipadas porque o compilador conhece o tipo específico dos elementos da coleção e pode gerar um erro em tempo de 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 digitadas implicitamente.

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

Os tipos de valor ordinário não podem ter um valor de null. No entanto, você pode criar tipos de valor anuláveis anexando um ? após o tipo. Por exemplo, int? é um int tipo que também pode ter o valor null. Os tipos de valor anulável são instâncias do struct genérico System.Nullable<T>. Os tipos de valores anuláveis são especialmente úteis quando você está passando dados de e para bancos de dados nos quais os 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 diferentes tipos 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 referida por essa variável. Muitas vezes, 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, como mostrado nos dois exemplos a seguir:

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

Em ambos os 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> na segunda.

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 tomadas pelo compilador. Essas ações do compilador incluem a resolução de chamadas de método, a resolução de sobrecarga e as 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 em tempo de execução incluem o despacho de chamadas de método virtual, a avaliação das expressões is e switch, 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 do C#.