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. C# utiliza principalmente um sistema de tipos normativo. Um sistema de tipos normativo usa nomes para identificar cada tipo. Em C#, struct, class e interface tipos, incluindo record tipos, são todos identificados pelo seu nome. Cada declaração de método especifica um nome, tipo e tipo (valor, referência ou saída) para cada parâmetro 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. Estas construções incluem o sistema de ficheiros, ligações de rede, coleções e arrays 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.

C# também suporta tipos estruturais, como tuplas e tipos anónimos. Os tipos estruturais são definidos pelos nomes e tipos de cada membro, e pela ordem dos membros numa expressão. Os tipos estruturais não têm nomes únicos.

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;

Especificas os tipos de parâmetros do método e valores de retorno na declaração do 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 declarares uma variável, não podes redeclará-la com um novo tipo, nem podes atribuir um valor incompatível com o tipo declarado. Por exemplo, você não pode declarar um int e, em seguida, atribuir-lhe um valor booleano de true. No entanto, pode converter valores para outros tipos, como quando os atribui a novas variáveis ou os passa como argumentos de método. O compilador realiza automaticamente uma conversão de tipos que não causa perda de dados. 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. Estes tipos representam inteiros, valores de ponto flutuante, expressões booleanas, caracteres de texto, valores decimais e outros tipos de dados. A linguagem também inclui tipos incorporados string e object. Podes usar estes tipos em qualquer programa de C#. Para uma lista completa dos tipos incorporados, veja Tipos incorporados.

Tipos personalizados

Crie tipos estruturais usando tuplas para armazenar membros de dados relacionados. Estes tipos fornecem uma estrutura que contém múltiplos membros. As tuplas têm um comportamento limitado. São um recipiente para valores. Estes são os tipos mais simples que pode criar. Mais tarde podes decidir que precisas de comportamento. Nesse caso, pode converter uma tupla num struct ou num class.

Use os struct, class, interface, enum e record para criar os 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#. Disponibilizas outros tipos adicionando explicitamente uma referência de pacote ao pacote que os fornece. Depois de o compilador ter uma referência ao pacote, podes declarar variáveis e constantes dos tipos declarados nos assemblies desse pacote no código-fonte.

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. Algumas escolhas sobrepõem-se. Na maioria dos cenários, mais de uma opção é uma escolha razoável.

  • Se o tipo de dados não faz parte do domínio da tua aplicação e não inclui comportamento, usa um tipo estrutural.
  • 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 principalmente para armazenar dados, com comportamento mínimo, 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.

Também pode escolher um interface para modelar um contrato: comportamento descrito por membros que pode ser implementado por tipos não relacionados. As interfaces são abstratas e declaram membros que devem ser implementados por todos os tipos class ou struct que herdam dessa interface.

O sistema de tipo comum

O sistema de tipos comum 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 incorporados como System.Int32 (palavra-chave C#: int), derivam em última instância 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. Estes tipos incluem todos os tipos personalizados da biblioteca de classes .NET e também os seus próprios tipos definidos pelo utilizador:

  • Os tipos que defines usando as struct palavras-chave ou record struct são tipos de valor. Todos os tipos numéricos incorporados são structs.
  • Os tipos que defines usando , classrecord class, ou record palavras-chave são tipos de referência.

Os tipos de referência e tipos de valor têm regras de compilação diferentes e comportamentos diferentes em tempo de execução.

Observação

Os tipos mais usados estã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 construto é 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 registo é como um modelo que utilizas para criar instâncias ou objetos durante o 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. Pode criar múltiplas instâncias do mesmo Person tipo, e cada instância pode ter valores diferentes nas suas propriedades e campos.

Uma classe é um tipo de referência. Quando crias um objeto desse tipo, a variável a que atribuis o objeto contém apenas uma referência a essa memória. Quando atribui a referência do objeto a uma nova variável, a nova variável refere-se ao objeto original. As alterações que fazes através de uma variável refletem-se na outra variável porque ambas se referem aos mesmos dados.

Um struct é um tipo de valor. Quando crias um struct, a variável à qual atribuis o struct contém os dados reais do struct. Quando atribuis a struct 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 que fazes a 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, use classes para modelar comportamentos mais complexos. As classes normalmente armazenam dados que se modificam depois de criar um objeto de classe. As estruturas são mais adequadas para pequenas estruturas de dados. As structs normalmente armazenam dados que não se modificam depois de a struct ser criada. Os tipos de registro são estruturas de dados com membros sintetizados extras do compilador. Os registos normalmente armazenam dados que não se modificam depois de o objeto ser criado.

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. 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 se pode derivar um tipo a partir de qualquer tipo de valor, como 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".

Use a palavra-chave struct para criar os 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(int x, int y)
{
    public int X { get; init; } = x;
    public int Y { get; init; } = y;
}

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 defines como class, record class, record, delegate, array ou interface é um reference type.

Quando declaras uma variável de um reference type, ela contém o valor null até que lhe atribuas uma instância desse tipo ou cries uma usando o new operador. O exemplo seguinte demonstra a criação e atribuição de uma classe:

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

Não podes instanciar diretamente um interface 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();

// 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 crias o objeto, o sistema aloca memória no heap gerido. 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. Declara-os e utiliza-os usando a sintaxe simplificada que o C# fornece, como mostrado no seguinte exemplo:

// 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#, o compilador atribui um tipo a valores literais. 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 não acrescentares uma letra, o compilador infere um tipo para o literal. Para mais informações sobre que tipos pode especificar com sufixos de letras, veja Tipos numéricos integrais e Tipos numéricos de ponto flutuante.

Como os literais são tipados, e todos os tipos derivam em última análise de System.Object, pode-se escrever e compilar código como o seguinte:

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

Declare um tipo com um ou mais parâmetros de tipo que atuam como marcadores para o tipo real (o tipo concreto). O código do cliente fornece o tipo concreto quando cria uma instância do tipo. Estes tipos são chamados tipos genéricos. Por exemplo, o tipo System.Collections.Generic.List<T> .NET tem um parâmetro de tipo que, por convenção, é chamado T. Quando cria uma instância do tipo, especifica o tipo dos objetos que a lista contém, tais como:string

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

Usar o parâmetro de tipo torna possível reutilizar a mesma classe para conter qualquer tipo de elemento, sem ter de converter cada elemento em objeto. As classes genéricas de coleção são 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, tentar adicionar um inteiro ao stringList objeto no exemplo anterior. Para obter mais informações, consulte Genéricos.

Tuplas e tipos anónimos

Criar um tipo para conjuntos simples de valores relacionados pode ser inconveniente se não pretender armazenar ou passar esses valores usando APIs públicas. Pode criar tuplas ou tipos anónimos para este fim. Para mais informações, veja Tuplas e Tipos Anónimos.

Tipos de valor anulável

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.

Declarações implícitas de tipos

Tipa implicitamente uma variável local (mas não os membros da classe) usando a var palavra-chave. A variável ainda recebe um tipo em tempo de compilação, mas o compilador fornece esse tipo. Para obter mais informações, consulte Variáveis locais digitadas implicitamente.

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 que o compilador realiza. 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#.