Partilhar via


8 Tipos

8.1 Generalidades

Os tipos da linguagem C# são divididos em duas categorias principais: tipos de referência e tipos de valor. Ambos os tipos de valor e tipos de referência podem ser tipos genéricos, que tomam um ou mais parâmetros de tipo. Os parâmetros de tipo podem designar tipos de valor e tipos de referência.

type
    : reference_type
    | value_type
    | type_parameter
    | pointer_type     // unsafe code support
    ;

pointer_type (§23.3) só está disponível no código não seguro (§23).

Os tipos de valor diferem dos tipos de referência na medida em que as variáveis dos tipos de valor contêm diretamente seus dados, enquanto as variáveis dos tipos de referência armazenam referências aos seus dados, sendo estes últimos conhecidos como objetos. Com tipos de referência, é possível que duas variáveis façam referência ao mesmo objeto e, portanto, é possível que operações em uma variável afetem o objeto referenciado pela outra variável. Com os tipos de valor, as variáveis têm cada uma sua própria cópia dos dados, e não é possível que as operações em uma afetem a outra.

Nota: Quando uma variável é um parâmetro de referência ou saída, ela não tem seu próprio armazenamento, mas faz referência ao armazenamento de outra variável. Neste caso, a variável ref ou out é efetivamente um alias para outra variável e não uma variável distinta. Nota final

O sistema de tipos do C# é unificado de tal forma que um valor de qualquer tipo pode ser tratado como um objeto. Cada tipo em C# deriva direta ou indiretamente do object tipo de classe e object é a classe base final de todos os tipos. Os valores dos tipos de referência são tratados como objetos simplesmente visualizando os valores como tipo object. Os valores dos tipos de valores são tratados como objetos através da realização de operações de boxing e unboxing (§8.3.13).

Por conveniência, ao longo desta especificação, alguns nomes de tipo de biblioteca são escritos sem usar sua qualificação de nome completo. Consulte § C.5 para obter mais informações.

8.2 Tipos de referência

8.2.1 Generalidades

Um tipo de referência é um tipo de classe, um tipo de interface, um tipo de matriz, um tipo de delegado ou o dynamic tipo. Para cada tipo de referência não anulável, há um tipo de referência anulável correspondente anotado anexando o ? ao nome do tipo.

reference_type
    : non_nullable_reference_type
    | nullable_reference_type
    ;

non_nullable_reference_type
    : class_type
    | interface_type
    | array_type
    | delegate_type
    | 'dynamic'
    ;

class_type
    : type_name
    | 'object'
    | 'string'
    ;

interface_type
    : type_name
    ;

array_type
    : non_array_type rank_specifier+
    ;

non_array_type
    : value_type
    | class_type
    | interface_type
    | delegate_type
    | 'dynamic'
    | type_parameter
    | pointer_type      // unsafe code support
    ;

rank_specifier
    : '[' ','* ']'
    ;

delegate_type
    : type_name
    ;

nullable_reference_type
    : non_nullable_reference_type nullable_type_annotation
    ;

nullable_type_annotation
    : '?'
    ;

pointer_type está disponível apenas em código não seguro (§23.3). nullable_reference_type é discutido mais detalhadamente no §8.9.

Um valor de tipo de referência é uma referência a uma instância do tipo, esta última conhecida como um objeto. O valor null especial é compatível com todos os tipos de referência e indica a ausência de uma instância.

8.2.2 Tipos de classes

Um tipo de classe define uma estrutura de dados que contém membros de dados (constantes e campos), membros de função (métodos, propriedades, eventos, indexadores, operadores, construtores de instância, finalizadores e construtores estáticos) e tipos aninhados. Os tipos de classe suportam herança, um mecanismo pelo qual as classes derivadas podem estender e especializar classes base. Instâncias de tipos de classe são criadas usando object_creation_expression s (§12.8.17.2).

Os tipos de classe são descritos no §15.

Certos tipos de classe predefinidos têm um significado especial na linguagem C#, conforme descrito na tabela abaixo.

Tipo de classe Descrição
System.Object A classe base final de todos os outros tipos. Ver ponto 8.2.3.
System.String O tipo de cadeia de caracteres da linguagem C#. Ver ponto 8.2.5.
System.ValueType A classe base de todos os tipos de valor. Ver ponto 8.3.2.
System.Enum A classe base de todos os enum tipos. Ver §19.5.
System.Array A classe base de todos os tipos de matriz. Ver §17.2.2.
System.Delegate A classe base de todos os delegate tipos. Ver §20.1.
System.Exception A classe base de todos os tipos de exceção. Ver §21.3.

8.2.3 O tipo de objeto

O object tipo de classe é a classe base final de todos os outros tipos. Cada tipo em C# deriva direta ou indiretamente do object tipo de classe.

A palavra-chave object é simplesmente um alias para a classe System.Objectpredefinida.

8.2.4 O tipo dinâmico

O dynamic tipo, como object, pode fazer referência a qualquer objeto. Quando as operações são aplicadas a expressões do tipo dynamic, sua resolução é adiada até que o programa seja executado. Assim, se a operação não pode ser legitimamente aplicada ao objeto referenciado, nenhum erro é dado durante a compilação. Em vez disso, uma exceção será lançada quando a resolução da operação falhar em tempo de execução.

O dynamic tipo é descrito em mais pormenor no §8.7 e a ligação dinâmica no §12.3.1.

8.2.5 O tipo de string

O string tipo é um tipo de classe selada que herda diretamente do object. As instâncias da classe representam cadeias de string caracteres Unicode.

Os valores do tipo podem ser escritos como literais de cadeia de string caracteres (§6.4.5.6).

A palavra-chave string é simplesmente um alias para a classe System.Stringpredefinida.

8.2.6 Tipos de interface

Uma interface define um contrato. Uma classe ou estrutura que implemente uma interface deve aderir ao seu contrato. Uma interface pode herdar de várias interfaces base, e uma classe ou struct pode implementar várias interfaces.

Os tipos de interface são descritos no §18.

8.2.7 Tipos de matriz

Uma matriz é uma estrutura de dados que contém zero ou mais variáveis, que são acessadas por meio de índices computados. As variáveis contidas em uma matriz, também chamadas de elementos da matriz, são todas do mesmo tipo, e esse tipo é chamado de tipo de elemento da matriz.

Os tipos de matriz são descritos no §17.

8.2.8 Tipos de delegados

Um delegado é uma estrutura de dados que se refere a um ou mais métodos. Por exemplo, também se refere às suas instâncias de objeto correspondentes.

Nota: O equivalente mais próximo de um delegado em C ou C++ é um ponteiro de função, mas enquanto um ponteiro de função só pode fazer referência a funções estáticas, um delegado pode fazer referência a métodos estáticos e de instância. Neste último caso, o delegado armazena não apenas uma referência ao ponto de entrada do método, mas também uma referência à instância do objeto na qual invocar o método. Nota final

Os tipos de delegados são descritos no §20.

8.3 Tipos de valores

8.3.1 Generalidades

Um tipo de valor é um tipo struct ou um tipo de enumeração. O C# fornece um conjunto de tipos struct predefinidos chamados de tipos simples. Os tipos simples são identificados através de palavras-chave.

value_type
    : non_nullable_value_type
    | nullable_value_type
    ;

non_nullable_value_type
    : struct_type
    | enum_type
    ;

struct_type
    : type_name
    | simple_type
    | tuple_type
    ;

simple_type
    : numeric_type
    | 'bool'
    ;

numeric_type
    : integral_type
    | floating_point_type
    | 'decimal'
    ;

integral_type
    : 'sbyte'
    | 'byte'
    | 'short'
    | 'ushort'
    | 'int'
    | 'uint'
    | 'long'
    | 'ulong'
    | 'char'
    ;

floating_point_type
    : 'float'
    | 'double'
    ;

tuple_type
    : '(' tuple_type_element (',' tuple_type_element)+ ')'
    ;
    
tuple_type_element
    : type identifier?
    ;
    
enum_type
    : type_name
    ;

nullable_value_type
    : non_nullable_value_type nullable_type_annotation
    ;

Ao contrário de uma variável de um tipo de referência, uma variável de um tipo de valor pode conter o valor null somente se o tipo de valor for um tipo de valor anulável (§8.3.12). Para cada tipo de valor não anulável, há um tipo de valor anulável correspondente denotando o mesmo conjunto de valores mais o valor null.

A atribuição a uma variável de um tipo de valor cria uma cópia do valor que está sendo atribuído. Isso difere da atribuição a uma variável de um tipo de referência, que copia a referência, mas não o objeto identificado pela referência.

8.3.2 O tipo System.ValueType

Todos os tipos de valor herdam implicitamente do classSystem.ValueType, que, por sua vez, herda da classe object. Não é possível que qualquer tipo derive de um tipo de valor, e os tipos de valor são, portanto, implicitamente selados (§15.2.2.3).

Note-se que System.ValueType não é em si um value_type. Pelo contrário, é um class_type do qual todos os value_types são automaticamente derivados.

8.3.3 Construtores padrão

Todos os tipos de valor declaram implicitamente um construtor de instância sem parâmetros público chamado construtor padrão. O construtor padrão retorna uma instância inicializada zero conhecida como o valor padrão para o tipo de valor:

  • Para todos os simple_types, o valor padrão é o valor produzido por um padrão de bits de todos os zeros:
    • Para sbyte, byte, short, , ushort, intuint, long, e , o ulongvalor padrão é 0.
    • Para char, o valor padrão é '\x0000'.
    • Para float, o valor padrão é 0.0f.
    • Para double, o valor padrão é 0.0d.
    • Para decimal, o valor padrão é 0m (ou seja, valor zero com escala 0).
    • Para bool, o valor padrão é false.
    • Para um enum_typeE, o valor padrão é 0, convertido para o tipo E.
  • Para um struct_type, o valor padrão é o valor produzido definindo todos os campos de tipo de valor como seu valor padrão e todos os campos de tipo de referência como null.
  • Para um nullable_value_type o valor padrão é uma instância para a qual a HasValue propriedade é false. O valor padrão também é conhecido como o valor nulo do tipo de valor anulável. A tentativa de ler a Value propriedade de tal valor faz com que uma exceção de tipo System.InvalidOperationException seja lançada (§8.3.12).

Como qualquer outro construtor de instância, o construtor padrão de um tipo de valor é invocado usando o new operador .

Nota: Por razões de eficiência, este requisito não se destina a realmente fazer com que a implementação gere uma chamada de construtor. Para tipos de valor, a expressão de valor padrão (§12.8.21) produz o mesmo resultado que usar o construtor padrão. Nota final

Exemplo: No código abaixo, as variáveis ie jk todas são inicializadas como zero.

class A
{
    void F()
    {
        int i = 0;
        int j = new int();
        int k = default(int);
    }
}

Exemplo final

Como cada tipo de valor tem implicitamente um construtor de instância sem parâmetros público, não é possível que um tipo struct contenha uma declaração explícita de um construtor sem parâmetros. Um tipo struct é, no entanto, permitido declarar construtores de instância parametrizados (§16.4.9).

8.3.4 Tipos de estruturas

Um tipo struct é um tipo de valor que pode declarar constantes, campos, métodos, propriedades, eventos, indexadores, operadores, construtores de instância, construtores estáticos e tipos aninhados. A declaração dos tipos struct é descrita no §16.

8.3.5 Tipos simples

O C# fornece um conjunto de tipos predefinidos struct chamados de tipos simples. Os tipos simples são identificados por meio de palavras-chave, mas essas palavras-chave são simplesmente aliases para tipos predefinidos struct no System namespace, conforme descrito na tabela abaixo.

Palavra-chave Tipo com alias
sbyte System.SByte
byte System.Byte
short System.Int16
ushort System.UInt16
int System.Int32
uint System.UInt32
long System.Int64
ulong System.UInt64
char System.Char
float System.Single
double System.Double
bool System.Boolean
decimal System.Decimal

Como um tipo simples aliases um tipo struct, cada tipo simples tem membros.

Exemplo: int tem os membros declarados e System.Int32 os membros herdados de System.Object, e as seguintes declarações são permitidas:

int i = int.MaxValue;      // System.Int32.MaxValue constant
string s = i.ToString();   // System.Int32.ToString() instance method
string t = 123.ToString(); // System.Int32.ToString() instance method

Exemplo final

Nota: Os tipos simples diferem de outros tipos de struct na medida em que permitem certas operações adicionais:

  • A maioria dos tipos simples permite que valores sejam criados escrevendo literais (§6.4.5), embora o C# não faça provisão para literais de tipos struct em geral. Exemplo: 123 é um literal do tipo int e 'a' é um literal do tipo char. Exemplo final
  • Quando os operandos de uma expressão são todas constantes de tipo simples, é possível para um compilador avaliar a expressão em tempo de compilação. Tal expressão é conhecida como constant_expression (§12.23). Expressões envolvendo operadores definidos por outros tipos struct não são consideradas expressões constantes
  • Através const de declarações, é possível declarar constantes dos tipos simples (§15.4). Não é possível ter constantes de outros tipos de struct, mas um efeito semelhante é fornecido por campos estáticos somente leitura.
  • As conversões que envolvem tipos simples podem participar na avaliação de operadores de conversão definidos por outros tipos de struct, mas um operador de conversão definido pelo utilizador nunca pode participar na avaliação de outro operador de conversão definido pelo utilizador (§10.5.3).

Nota final.

8.3.6 Tipos integrais

C# suporta nove tipos integrais: sbyte, byte, short, ushort, int, uint, long, ulonge char. Os tipos integrais têm os seguintes tamanhos e intervalos de valores:

  • O sbyte tipo representa inteiros de 8 bits assinados com valores de -128 até 127, inclusive.
  • O byte tipo representa inteiros de 8 bits não assinados com valores de 0 até 255, inclusive.
  • O short tipo representa inteiros de 16 bits assinados com valores de -32768 até 32767, inclusive.
  • O ushort tipo representa inteiros de 16 bits não assinados com valores de 0 até 65535, inclusive.
  • O int tipo representa inteiros de 32 bits assinados com valores de -2147483648 até 2147483647, inclusive.
  • O uint tipo representa inteiros de 32 bits não assinados com valores de 0 até 4294967295, inclusive.
  • O long tipo representa inteiros de 64 bits assinados com valores de -9223372036854775808 até 9223372036854775807, inclusive.
  • O ulong tipo representa inteiros de 64 bits não assinados com valores de 0 até 18446744073709551615, inclusive.
  • O char tipo representa inteiros de 16 bits não assinados com valores de 0 até 65535, inclusive. O conjunto de valores possíveis para o char tipo corresponde ao conjunto de caracteres Unicode.

    Nota: Embora char tenha a mesma representação que ushort, nem todas as operações permitidas num tipo são permitidas no outro. Nota final

Todos os tipos de integrais assinados são representados usando o formato de complemento de dois.

Os operadores integral_type unário e binário sempre operam com precisão de 32 bits assinada, precisão de 32 bits não assinada, precisão de 64 bits assinada ou precisão de 64 bits não assinada, conforme detalhado no §12.4.7.

O char tipo é classificado como um tipo integral, mas difere dos outros tipos integrais de duas maneiras:

  • Não há conversões implícitas predefinidas de outros tipos para o char tipo. Em particular, mesmo que os byte tipos e ushort tenham intervalos de valores que são totalmente representáveis usando o char tipo, conversões implícitas de sbyte, byte ou ushort para char não existem.
  • As constantes do char tipo devem ser escritas como character_literals ou como integer_literals em combinação com um cast to type char.

Exemplo: (char)10 é o mesmo que '\x000A'. Exemplo final

Os checked operadores e as instruções são unchecked usados para controlar a verificação de estouro para operações aritméticas de tipo integral e conversões (§12.8.20). Em um checked contexto, um estouro produz um erro em tempo de compilação ou faz com que um System.OverflowException seja lançado. Em um unchecked contexto, os estouros são ignorados e todos os bits de ordem alta que não se encaixam no tipo de destino são descartados.

8.3.7 Tipos de vírgula flutuante

O C# suporta dois tipos de ponto flutuante: float e double. Os float tipos e double são representados usando os formatos IEC 60559 de precisão única de 32 bits e precisão dupla de 64 bits, que fornecem os seguintes conjuntos de valores:

  • Zero positivo e zero negativo. Na maioria das situações, o zero positivo e o zero negativo comportam-se de forma idêntica ao valor simples zero, mas certas operações distinguem entre os dois (§12.10.3).
  • Infinito positivo e infinito negativo. As infinidades são produzidas por operações como a divisão de um número diferente de zero por zero.

    Exemplo: 1.0 / 0.0 produz infinito positivo e –1.0 / 0.0 produz infinito negativo. Exemplo final

  • O valor Not-a-Number , muitas vezes abreviado NaN. NaNs são produzidos por operações de ponto flutuante inválidas, como dividir zero por zero.
  • O conjunto finito de valores diferentes de zero da forma s ×m × 2e, em que s é 1 ou −1, e m e e são determinados pelo tipo particular de vírgula flutuante: Para float, 0 << 2²⁴ e −149 ≤ e ≤ 104, e para double, 0 << 2⁵³ e −1075 ≤ e ≤ 970. Os números de vírgula flutuante desnormalizados são considerados valores válidos diferentes de zero. O C# não exige nem proíbe que uma implementação em conformidade suporte números de vírgula flutuante desnormalizados.

O float tipo pode representar valores que variam de aproximadamente 1,5 × 10⁻⁴⁵ a 3,4 × 10³⁸ com uma precisão de 7 dígitos.

O double tipo pode representar valores que variam de aproximadamente 5,0 × 10⁻³²⁴ a 1,7 × 10³⁰⁸ com uma precisão de 15-16 dígitos.

Se qualquer operando de um operador binário for um tipo de vírgula flutuante, então promoções numéricas padrão são aplicadas, conforme detalhado no §12.4.7, e a operação é executada com float ou double precisão.

Os operadores de ponto flutuante, incluindo os operadores de atribuição, nunca produzem exceções. Em vez disso, em situações excecionais, as operações de ponto flutuante produzem zero, infinito ou NaN, conforme descrito abaixo:

  • O resultado de uma operação de vírgula flutuante é arredondado para o valor representável mais próximo no formato de destino.
  • Se a magnitude do resultado de uma operação de vírgula flutuante for muito pequena para o formato de destino, o resultado da operação torna-se zero positivo ou zero negativo.
  • Se a magnitude do resultado de uma operação de ponto flutuante for muito grande para o formato de destino, o resultado da operação se tornará infinito positivo ou infinito negativo.
  • Se uma operação de ponto flutuante for inválida, o resultado da operação torna-se NaN.
  • Se um ou ambos os operandos de uma operação de ponto flutuante for NaN, o resultado da operação torna-se NaN.

As operações de vírgula flutuante podem ser efetuadas com maior precisão do que o tipo de resultado da operação. Para forçar um valor de um tipo de vírgula flutuante à precisão exata do seu tipo, pode ser utilizado um molde explícito (§12.9.7).

Exemplo: Algumas arquiteturas de hardware suportam um tipo de ponto flutuante "estendido" ou "duplo longo" com maior alcance e precisão do que o double tipo, e executam implicitamente todas as operações de ponto flutuante usando esse tipo de precisão mais alta. Somente com um custo excessivo de desempenho essas arquiteturas de hardware podem ser feitas para executar operações de ponto flutuante com menos precisão e, em vez de exigir uma implementação para perder desempenho e precisão, o C# permite que um tipo de precisão maior seja usado para todas as operações de ponto flutuante. Além de fornecer resultados mais precisos, isso raramente tem efeitos mensuráveis. No entanto, em expressões da forma x * y / z, onde a multiplicação produz um resultado que está fora do double intervalo, mas a divisão subsequente traz o resultado temporário de volta para o double intervalo, o fato de a expressão ser avaliada em um formato de intervalo mais alto pode fazer com que um resultado finito seja produzido em vez de um infinito. Exemplo final

8.3.8 O tipo decimal

O decimal tipo é um tipo de dados de 128 bits adequado para cálculos financeiros e monetários. O decimal tipo pode representar valores incluindo aqueles no intervalo de pelo menos -7,9 × 10⁻²⁸ a 7,9 × 10²⁸, com pelo menos 28 dígitos de precisão.

O conjunto finito de valores do tipo decimal é da forma (–1)v × c × 10⁻e, onde o sinal v é 0 ou 1, o coeficiente c é dado por 0 ≤ c<Cmax, e a escala e é tal que Emine ≤ Emax, onde Cmax é pelo menos 1 × 10²⁸, Emin ≤ 0, e Emax ≥ 28. O decimal tipo não suporta necessariamente zeros assinados, infinidades ou NaN's.

A decimal é representado como um número inteiro dimensionado por uma potência de dez. Para decimals com um valor absoluto inferior 1.0ma , o valor é exato a, pelo menos, a 28.ª casa decimal. Para decimals com um valor absoluto maior ou igual a 1.0m, o valor é exato para pelo menos 28 dígitos. Ao contrário dos float tipos de dados e double , números fracionários decimais como 0.1 podem ser representados exatamente na representação decimal. float double Nas e representações, tais números geralmente têm expansões binárias não terminativas, tornando essas representações mais propensas a erros de arredondamento.

Se qualquer operando de um operador binário for do decimal tipo, então promoções numéricas padrão são aplicadas, conforme detalhado no §12.4.7, e a operação é executada com double precisão.

O resultado de uma operação sobre valores do tipo decimal é o que resultaria do cálculo de um resultado exato (preservando a escala, conforme definido para cada operador) e, em seguida, arredondando para ajustar a representação. Os resultados são arredondados para o valor representável mais próximo e, quando um resultado está igualmente próximo de dois valores representáveis, para o valor que tem um número par na posição de dígitos menos significativa (isto é conhecido como "arredondamento do banqueiro"). Ou seja, os resultados são exatos até pelo menos a 28ª casa decimal. Observe que o arredondamento pode produzir um valor zero a partir de um valor diferente de zero.

Se uma decimal operação aritmética produz um resultado cuja magnitude é muito grande para o decimal formato, um System.OverflowException é lançado.

O decimal tipo tem maior precisão, mas pode ter um alcance menor do que os tipos de ponto flutuante. Assim, conversões dos tipos de ponto flutuante para decimal podem produzir exceções de estouro, e conversões de para os tipos de ponto flutuante podem causar perda de precisão ou exceções de decimal estouro. Por essas razões, não existem conversões implícitas entre os tipos de ponto flutuante e decimal, e sem versões explícitas, um erro em tempo de compilação ocorre quando o ponto flutuante e decimal os operandos são diretamente misturados na mesma expressão.

8.3.9 O tipo Bool

O bool tipo representa grandezas lógicas booleanas. Os valores possíveis do tipo bool são true e false. A representação de false é descrita no §8.3.3. Embora a representação de true não seja especificada, deve ser diferente da de false.

Não existem conversões padrão entre bool e outros tipos de valor. Em particular, o bool tipo é distinto e separado dos tipos integrais, um bool valor não pode ser usado no lugar de um valor integral e vice-versa.

Nota: Nas linguagens C e C++, um valor zero integral ou de ponto flutuante, ou um ponteiro nulo pode ser convertido para o valor falsebooleano, e um valor integral ou de ponto flutuante diferente de zero, ou um ponteiro não nulo pode ser convertido para o valor truebooleano. Em C#, essas conversões são realizadas comparando explicitamente um valor integral ou de vírgula flutuante com zero, ou comparando explicitamente uma referência de objeto com null. Nota final

8.3.10 Tipos de enumeração

Um tipo de enumeração é um tipo distinto com constantes nomeadas. Cada tipo de enumeração tem um tipo subjacente, que deve ser byte, sbyte, short, ushort, int, uintlong, ou ulong. O conjunto de valores do tipo de enumeração é o mesmo que o conjunto de valores do tipo subjacente. Os valores do tipo de enumeração não se restringem aos valores das constantes nomeadas. Os tipos de enumeração são definidos através de declarações de enumeração (§19.2).

8.3.11 Tipos de tuplas

Um tipo de tupla representa uma sequência ordenada de valores de comprimento fixo com nomes opcionais e tipos individuais. O número de elementos em um tipo de tupla é referido como sua aridade. Um tipo de tupla é escrito (T1 I1, ..., Tn In) com n ≥ 2, onde os identificadores são nomes de I1...In elementos de tupla opcionais.

Esta sintaxe é uma abreviatura de um tipo construído com os tipos T1...Tn de System.ValueTuple<...>, que deve ser um conjunto de tipos struct genéricos capazes de expressar diretamente tipos de tupla de qualquer aridade entre dois e sete, inclusive. Não é necessário existir uma System.ValueTuple<...> declaração que corresponda diretamente à aridade de qualquer tipo de tupla com um número correspondente de parâmetros de tipo. Em vez disso, tuplas com uma aridade maior que sete são representadas com um tipo System.ValueTuple<T1, ..., T7, TRest> struct genérico que, além de elementos de tupla, tem um Rest campo contendo um valor aninhado dos elementos restantes, usando outro System.ValueTuple<...> tipo. Esta nidificação pode ser observável de várias formas, por exemplo, através da presença de um Rest campo. Quando apenas um único campo adicional é necessário, o tipo System.ValueTuple<T1> struct genérico é usado, este tipo não é considerado um tipo de tupla em si. Quando são necessários mais de sete campos adicionais, System.ValueTuple<T1, ..., T7, TRest> é usado recursivamente.

Os nomes dos elementos dentro de um tipo de tupla devem ser distintos. Um nome de elemento de tupla do formulário ItemX, onde X é qualquer sequência de dígitos decimais não0 iniciados que poderiam representar a posição de um elemento de tupla, só é permitido na posição indicada por X.

Os nomes de elementos opcionais não são representados nos ValueTuple<...> tipos e não são armazenados na representação de tempo de execução de um valor de tupla. As conversões de identidade (§10.2.2) existem entre tuplas com sequências convertíveis de identidade de tipos de elementos.

O new operador §12.8.17.2 não pode ser aplicado com a sintaxe new (T1, ..., Tn)do tipo de tupla . Os valores de tupla podem ser criados a partir de expressões de tupla (§12.8.6), ou aplicando o new operador diretamente a um tipo construído a partir de ValueTuple<...>.

Os elementos de tupla são campos públicos com os nomes Item1, Item2, etc., e podem ser acessados através de um acesso de membro em um valor de tupla (§12.8.7. Além disso, se o tipo de tupla tiver um nome para um determinado elemento, esse nome pode ser usado para acessar o elemento em questão.

Nota: Mesmo quando tuplas grandes são representadas com valores aninhados System.ValueTuple<...> , cada elemento de tupla ainda pode ser acessado diretamente com o Item... nome correspondente à sua posição. Nota final

Exemplo: Vejam-se os seguintes exemplos:

(int, string) pair1 = (1, "One");
(int, string word) pair2 = (2, "Two");
(int number, string word) pair3 = (3, "Three");
(int Item1, string Item2) pair4 = (4, "Four");
// Error: "Item" names do not match their position
(int Item2, string Item123) pair5 = (5, "Five");
(int, string) pair6 = new ValueTuple<int, string>(6, "Six");
ValueTuple<int, string> pair7 = (7, "Seven");
Console.WriteLine($"{pair2.Item1}, {pair2.Item2}, {pair2.word}");

Os tipos de tupla para pair1, pair2, e pair3 são todos válidos, com nomes para não, alguns ou todos os elementos do tipo de tupla.

O tipo de tupla para pair4 é válido porque os nomes Item1 e Item2 correspondem às suas posições, enquanto o tipo de tupla para pair5 é proibido, porque os nomes Item2 e Item123 não.

As declarações para pair6 e pair7 demonstram que os tipos de tupla são intercambiáveis com os tipos construídos da forma ValueTuple<...>, e que o new operador é permitido com a última sintaxe.

A última linha mostra que os elementos de tupla Item podem ser acessados pelo nome correspondente à sua posição, bem como pelo nome do elemento de tupla correspondente, se presente no tipo. Exemplo final

8.3.12 Tipos de valor anulável

Um tipo de valor anulável pode representar todos os valores de seu tipo subjacente mais um valor nulo adicional. Um tipo de valor anulável é escrito T?, onde T é o tipo subjacente. Esta sintaxe é abreviada para System.Nullable<T>, e as duas formas podem ser usadas indistintamente.

Por outro lado, um tipo de valor não anulável é qualquer tipo de valor diferente de System.Nullable<T> e sua abreviatura T? (para qualquer T), mais qualquer parâmetro de tipo que é restrito a ser um tipo de valor não anulável (ou seja, qualquer parâmetro de tipo com uma restrição de tipo de valor (§15.2.5)). O System.Nullable<T> tipo especifica a restrição de tipo de valor para T, o que significa que o tipo subjacente de um tipo de valor anulável pode ser qualquer tipo de valor não anulável. O tipo subjacente de um tipo de valor anulável não pode ser um tipo de valor anulável ou um tipo de referência. Por exemplo, int?? é um tipo inválido. Os tipos de referência anuláveis são abrangidos pelo ponto 8.9.

Uma instância de um tipo T? de valor anulável tem duas propriedades públicas somente leitura:

  • Uma HasValue propriedade do tipo bool
  • Uma Value propriedade do tipo T

Uma instância para a qual HasValue é true dito não ser nulo. Uma instância não nula contém um valor conhecido e Value retorna esse valor.

Uma instância para a qual HasValue é false é dito ser nulo. Uma instância nula tem um valor indefinido. Tentar ler o Value de uma instância nula faz com que um System.InvalidOperationException seja lançado. O processo de acessar a propriedade Value de uma instância anulável é chamado de desempacotamento.

Além do construtor padrão, cada tipo T? de valor nulo tem um construtor público com um único parâmetro do tipo T. Dado um valor x de tipo T, uma invocação do construtor do formulário

new T?(x)

cria uma instância não nula da T? qual a Value propriedade é x. O processo de criação de uma instância não nula de um tipo de valor anulável para um determinado valor é conhecido como encapsulamento.

As conversões implícitas estão disponíveis do null literal para T? (§10.2.7) e do T para T? (§10.2.6).

O tipo T? de valor anulável não implementa interfaces (§18). Em particular, isso significa que ele não implementa nenhuma interface que o tipo T subjacente faz.

8.3.13 Boxe e unboxing

O conceito de boxe e unboxing fornecem uma ponte entre value_types e reference_types, permitindo que qualquer valor de um value_type seja convertido para e do tipo object. Boxing e unboxing permite uma visão unificada do sistema de tipos em que um valor de qualquer tipo pode ser tratado como um object.

O boxe é descrito em mais detalhes no §10.2.9 e o unboxing é descrito no §10.3.7.

8.4 Tipos de construção

8.4.1 Generalidades

Uma declaração de tipo genérica, por si só, denota um tipo genérico não acoplado que é usado como um "modelo" para formar muitos tipos diferentes, por meio da aplicação de argumentos de tipo. Os argumentos de tipo são escritos entre colchetes angulares (< e >) imediatamente após o nome do tipo genérico. Um tipo que inclui pelo menos um argumento de tipo é chamado de tipo construído. Um tipo construído pode ser usado na maioria dos lugares no idioma em que um nome de tipo pode aparecer. Um tipo genérico não ligado só pode ser utilizado dentro de um typeof_expression (§12.8.18).

Os tipos construídos também podem ser usados em expressões como nomes simples (§12.8.4) ou ao acessar um membro (§12.8.7).

Quando um namespace_or_type_name é avaliado, apenas os tipos genéricos com o número correto de parâmetros de tipo são considerados. Assim, é possível usar o mesmo identificador para identificar diferentes tipos, desde que os tipos tenham números diferentes de parâmetros de tipo. Isso é útil ao misturar classes genéricas e não genéricas no mesmo programa.

Exemplo:

namespace Widgets
{
    class Queue {...}
    class Queue<TElement> {...}
}

namespace MyApplication
{
    using Widgets;

    class X
    {
        Queue q1;      // Non-generic Widgets.Queue
        Queue<int> q2; // Generic Widgets.Queue
    }
}

Exemplo final

As regras detalhadas para a pesquisa de nomes nas produções namespace_or_type_name são descritas no §7.8. A resolução de ambiguidades nestas produções é descrita no §6.2.5. Um type_name pode identificar um tipo construído mesmo que não especifique parâmetros de tipo diretamente. Isso pode ocorrer quando um tipo é aninhado dentro de uma declaração genérica class e o tipo de instância da declaração que contém é implicitamente usado para pesquisa de nome (§15.3.9.7).

Exemplo:

class Outer<T>
{
    public class Inner {...}

    public Inner i; // Type of i is Outer<T>.Inner
}

Exemplo final

Um tipo construído sem enum não deve ser utilizado como unmanaged_type (§8.8).

8.4.2 Argumentos de tipo

Cada argumento em uma lista de argumentos de tipo é simplesmente um tipo.

type_argument_list
    : '<' type_arguments '>'
    ;

type_arguments
    : type_argument (',' type_argument)*
    ;   

type_argument
    : type
    | type_parameter nullable_type_annotation?
    ;

Cada argumento de tipo deve satisfazer quaisquer restrições relativas ao parâmetro de tipo correspondente (§15.2.5). Um argumento de tipo de referência cuja anulabilidade não corresponde à anulabilidade do parâmetro type satisfaz a restrição; no entanto, pode ser emitido um aviso.

8.4.3 Tipos abertos e fechados

Todos os tipos podem ser classificados como tipos abertos ou fechados. Um tipo aberto é um tipo que envolve parâmetros de tipo. Mais especificamente:

  • Um parâmetro type define um tipo aberto.
  • Um tipo de matriz é um tipo aberto se e somente se seu tipo de elemento for um tipo aberto.
  • Um tipo construído é um tipo aberto se e somente se um ou mais de seus argumentos de tipo for um tipo aberto. Um tipo aninhado construído é um tipo aberto se e somente se um ou mais de seus argumentos de tipo ou os argumentos de tipo de seus tipos que contêm for um tipo aberto.

Um tipo fechado é um tipo que não é um tipo aberto.

Em tempo de execução, todo o código dentro de uma declaração de tipo genérica é executado no contexto de um tipo construído fechado que foi criado aplicando argumentos de tipo à declaração genérica. Cada parâmetro de tipo dentro do tipo genérico é vinculado a um tipo de tempo de execução específico. O processamento em tempo de execução de todas as instruções e expressões sempre ocorre com tipos fechados, e os tipos abertos ocorrem apenas durante o processamento em tempo de compilação.

Dois tipos construídos fechados são conversíveis de identidade (§10.2.2) se forem construídos a partir do mesmo tipo genérico não vinculado, e existe uma conversão de identidade entre cada um dos seus argumentos de tipo correspondentes. Os argumentos de tipo correspondentes podem ser tipos construídos fechados ou tuplas que são conversíveis de identidade. Tipos construídos fechados que são conversíveis de identidade compartilham um único conjunto de variáveis estáticas. Caso contrário, cada tipo construído fechado tem seu próprio conjunto de variáveis estáticas. Como um tipo aberto não existe em tempo de execução, não há variáveis estáticas associadas a um tipo aberto.

8.4.4 Tipos ligados e não acoplados

O termo tipo não acoplado refere-se a um tipo não genérico ou a um tipo genérico não vinculado. O termo tipo vinculado refere-se a um tipo não genérico ou a um tipo construído.

Um tipo não acoplado refere-se à entidade declarada por uma declaração de tipo. Um tipo genérico não acoplado não é em si um tipo e não pode ser usado como o tipo de uma variável, argumento ou valor de retorno, ou como um tipo base. A única construção em que um tipo genérico não vinculado pode ser referenciado é a typeof expressão (§12.8.18).

8.4.5 Satisfazer as restrições

Sempre que um tipo construído ou um método genérico é referenciado, os argumentos de tipo fornecidos são verificados em relação às restrições de parâmetros de tipo declaradas no tipo ou método genérico (§15.2.5). Para cada where cláusula, o argumento A type que corresponde ao parâmetro named type é verificado em relação a cada restrição da seguinte maneira:

  • Se a restrição for um class tipo, um tipo de interface ou um parâmetro de tipo, vamos C representar essa restrição com os argumentos de tipo fornecidos substituídos por quaisquer parâmetros de tipo que apareçam na restrição. Para satisfazer esta condicionação, deve ser o caso de o tipo A ser convertível em tipo C por uma das seguintes entidades:
    • Uma conversão de identidade (§10.2.2)
    • Uma conversão de referência implícita (§10.2.8)
    • Uma conversão de boxe (§10.2.9), desde que o tipo A seja um tipo de valor não anulável.
    • Uma referência implícita, boxing ou conversão de parâmetro de tipo de um parâmetro A de tipo para C.
  • Se a restrição for a restrição do tipo de referência (class), o tipo A deve satisfazer uma das seguintes condições:
    • A é um tipo de interface, tipo de classe, tipo delegado, tipo de matriz ou o tipo dinâmico.

    Nota: System.ValueType e System.Enum são tipos de referência que satisfazem esta restrição. Nota final

    • A é um parâmetro de tipo que se sabe ser um tipo de referência (§8.2).
  • Se a restrição for a restrição do tipo de valor (struct), o tipo A deve satisfazer uma das seguintes condições:
    • A é um struct tipo ou enum tipo, mas não um tipo de valor anulável.

    Nota: System.ValueType e System.Enum são tipos de referência que não satisfazem esta restrição. Nota final

    • A é um parâmetro de tipo com a restrição de tipo de valor (§15.2.5).
  • Se a restrição for a restrição new()do construtor , o tipo A não deve ser abstract e deve ter um construtor público sem parâmetros. Isto é satisfeito se uma das seguintes situações for verdadeira:
    • A é um tipo de valor, uma vez que todos os tipos de valor têm um construtor padrão público (§8.3.3).
    • A é um parâmetro de tipo com a restrição do construtor (§15.2.5).
    • A é um parâmetro de tipo com a restrição de tipo de valor (§15.2.5).
    • A é um class que não é abstrato e contém um construtor público explicitamente declarado sem parâmetros.
    • A não abstract é e tem um construtor padrão (§15.11.5).

Um erro em tempo de compilação ocorre se uma ou mais restrições de um parâmetro de tipo não forem satisfeitas pelos argumentos de tipo fornecidos.

Como os parâmetros de tipo não são herdados, as restrições também nunca são herdadas.

Exemplo: A seguir, D precisa especificar a restrição em seu parâmetro T type para que T satisfaça a restrição imposta pela base classB<T>. Em contraste, classE não precisa especificar uma restrição, porque List<T> implementa IEnumerable para qualquer T.

class B<T> where T: IEnumerable {...}
class D<T> : B<T> where T: IEnumerable {...}
class E<T> : B<List<T>> {...}

Exemplo final

8.5 Parâmetros de tipo

Um parâmetro type é um identificador que designa um tipo de valor ou tipo de referência ao qual o parâmetro está vinculado em tempo de execução.

type_parameter
    : identifier
    ;

Como um parâmetro type pode ser instanciado com muitos argumentos de tipo diferentes, os parâmetros type têm operações e restrições ligeiramente diferentes de outros tipos.

Nota: Estes incluem:

  • Um parâmetro de tipo não pode ser usado diretamente para declarar uma classe base (§15.2.4.2) ou interface (§18.2.4).
  • As regras para pesquisa de membros em parâmetros de tipo dependem das restrições, se houver, aplicadas ao parâmetro type. São descritas em pormenor no ponto 12.5.
  • As conversões disponíveis para um parâmetro type dependem das restrições, se houver, aplicadas ao parâmetro type. Eles são detalhados em §10.2.12 e §10.3.8.
  • O literal null não pode ser convertido para um tipo dado por um parâmetro type, exceto se o parâmetro type for conhecido por ser um tipo de referência (§10.2.12). No entanto, uma expressão padrão (§12.8.21) pode ser usada em vez disso. Além disso, um valor com um tipo dado por um parâmetro type pode ser comparado com null usando == e != (§12.12.7), a menos que o parâmetro type tenha a restrição de tipo de valor.
  • Uma new expressão (§12.8.17.2) só pode ser usada com um parâmetro type se o parâmetro type for limitado por um constructor_constraint ou pela restrição de tipo de valor (§15.2.5).
  • Um parâmetro type não pode ser usado em qualquer lugar dentro de um atributo.
  • Um parâmetro type não pode ser usado em um acesso de membro (§12.8.7) ou nome de tipo (§7.8) para identificar um membro estático ou um tipo aninhado.
  • Um parâmetro de tipo não pode ser utilizado como unmanaged_type (§8.8).

Nota final

Como um tipo, os parâmetros de tipo são puramente uma construção em tempo de compilação. Em tempo de execução, cada parâmetro de tipo é vinculado a um tipo de tempo de execução que foi especificado fornecendo um argumento type para a declaração de tipo genérico. Assim, o tipo de variável declarada com um parâmetro de tipo será, em tempo de execução, um tipo construído fechado §8.4.3. A execução em tempo de execução de todas as instruções e expressões envolvendo parâmetros de tipo usa o tipo que foi fornecido como o argumento de tipo para esse parâmetro.

8.6 Tipos de árvore de expressão

As árvores de expressão permitem que expressões lambda sejam representadas como estruturas de dados em vez de código executável. Árvores de expressão são valores de tipos de árvore de expressão do formulário System.Linq.Expressions.Expression<TDelegate>, onde TDelegate é qualquer tipo delegado. Para o resto desta especificação, estes tipos serão referidos utilizando a abreviatura Expression<TDelegate>.

Se existir uma conversão de uma expressão lambda para um tipo Ddelegado, também existe uma conversão para o tipo Expression<TDelegate>de árvore de expressão. Enquanto a conversão de uma expressão lambda em um tipo de delegado gera um delegado que faz referência ao código executável para a expressão lambda, a conversão para um tipo de árvore de expressão cria uma representação de árvore de expressão da expressão lambda. Mais detalhes sobre esta conversão são fornecidos em §10.7.3.

Exemplo: O programa a seguir representa uma expressão lambda como código executável e como uma árvore de expressão. Como existe uma conversão para Func<int,int>, também existe uma conversão para Expression<Func<int,int>>:

Func<int,int> del = x => x + 1;             // Code
Expression<Func<int,int>> exp = x => x + 1; // Data

Após essas atribuições, o delegado del faz referência a um método que retorna x + 1, e a árvore de expressão exp faz referência a uma estrutura de dados que descreve a expressão x => x + 1.

Exemplo final

Expression<TDelegate> Fornece um método de instância que produz um delegado Compile do tipo TDelegate:

Func<int,int> del2 = exp.Compile();

Invocar esse delegado faz com que o código representado pela árvore de expressões seja executado. Assim, dadas as definições acima, del e del2 são equivalentes, e as duas afirmações seguintes terão o mesmo efeito:

int i1 = del(1);
int i2 = del2(1);

Depois de executar este código, i1 e i2 ambos terão o valor 2.

A superfície da API fornecida pela é definida pela Expression<TDelegate> implementação além do requisito para um Compile método descrito acima.

Nota: Embora os detalhes da API fornecidos para árvores de expressão sejam definidos pela implementação, espera-se que uma implementação:

  • Habilitar o código para inspecionar e responder à estrutura de uma árvore de expressão criada como resultado de uma conversão de uma expressão lambda
  • Permitir que árvores de expressão sejam criadas programaticamente dentro do código do usuário

Nota final

8.7 O tipo dinâmico

O tipo dynamic usa a ligação dinâmica, conforme descrito em detalhes no §12.3.2, em oposição à ligação estática que é usada por todos os outros tipos.

O tipo dynamic é considerado idêntico, object exceto nos seguintes aspetos:

  • As operações sobre expressões de tipo dynamic podem ser vinculadas dinamicamente (§12.3.3).
  • A inferência de tipo (§12.6.3) será preferível dynamic se object ambos forem candidatos.
  • dynamic não pode ser utilizado como
    • o tipo numa object_creation_expression (§12.8.17.2)
    • uma class_base (§15.2.4)
    • um predefined_type num member_access (§12.8.7.1)
    • o operando do typeof operador
    • um argumento de atributo
    • uma restrição
    • Um tipo de método de extensão
    • qualquer parte de um argumento de tipo dentro struct_interfaces (§16.2.5) ou interface_type_list (§15.2.4.1).

Devido a esta equivalência, verifica-se o seguinte:

  • Há uma conversão de identidade implícita
    • entre object e dynamic
    • entre tipos construídos que são os mesmos ao substituir dynamic por object
    • entre tipos de tuplas que são os mesmos ao substituir dynamic por object
  • As conversões implícitas e explícitas de e object para também se aplicam a e de dynamic.
  • As assinaturas que são as mesmas quando substituídas dynamic por object são consideradas a mesma assinatura.
  • O tipo dynamic é indistinguível do tipo object em tempo de execução.
  • Uma expressão do tipo dynamic é referida como uma expressão dinâmica.

8.8 Tipos não gerenciados

unmanaged_type
    : value_type
    | pointer_type     // unsafe code support
    ;

Um unmanaged_type é qualquer tipo que não seja nem um reference_type nem um type_parameter que não esteja restrito a ser não gerenciado e não contenha campos de instância cujo tipo não seja um unmanaged_type. Por outras palavras, um unmanaged_type é um dos seguintes:

  • sbyte, byte, short, ushort, , intuint, long, ulong, char, floatdoubledecimalou .bool
  • Qualquer enum_type.
  • Uma estrutura do tipo struct_type definida pelo usuário, que contenha apenas campos de instância de unmanaged_types.
  • Qualquer parâmetro de tipo que esteja restrito a não ser gerenciado.
  • Qualquer pointer_type (§23.3).

8.9 Tipos de referência e anulabilidade

8.9.1 Generalidades

Um tipo de referência anulável é indicado anexando um nullable_type_annotation (?) a um tipo de referência não anulável. Não há diferença semântica entre um tipo de referência não anulável e seu tipo anulável correspondente, ambos podem ser uma referência a um objeto ou null. A presença ou ausência do nullable_type_annotation declara se uma expressão se destina a permitir valores nulos ou não. Um compilador pode fornecer diagnósticos quando uma expressão não é usada de acordo com essa intenção. O estado nulo de uma expressão é definido no §8.9.5. Existe uma conversão de identidade entre um tipo de referência anulável e o seu correspondente tipo de referência não anulável (§10.2.2).

Existem duas formas de anulabilidade para tipos de referência:

  • nullable: Um nullable-reference-type pode ser atribuído null. Seu estado nulo padrão é maybe-null.
  • não anulável: uma referência não anulável não deve receber um null valor. Seu estado nulo padrão é não-nulo.

Nota: Os tipos R e R? são representados pelo mesmo tipo subjacente, R. Uma variável desse tipo subjacente pode conter uma referência a um objeto ou ser o valor null, que indica "sem referência". Nota final

A distinção sintática entre um tipo de referência anulável e seu tipo de referência não anulável correspondente permite que um compilador gere diagnósticos. Um compilador deve permitir o nullable_type_annotation conforme definido no §8.2.1. Os diagnósticos devem limitar-se a avisos. Nem a presença ou ausência de anotações anuláveis, nem o estado do contexto anulável podem alterar o tempo de compilação ou o comportamento de tempo de execução de um programa, exceto para alterações em quaisquer mensagens de diagnóstico geradas em tempo de compilação.

8.9.2 Tipos de referência não anuláveis

Um tipo de referência não anulável é um tipo de referência do formulário T, onde T é o nome do tipo. O estado nulo padrão de uma variável não anulável é não-nulo. Os avisos podem ser gerados quando uma expressão que é talvez-nula é usada onde um valor não-nulo é necessário.

8.9.3 Tipos de referência anuláveis

Um tipo de referência do formulário T? (como string?) é um tipo de referência anulável. O estado nulo padrão de uma variável anulável é talvez nulo. A anotação ? indica a intenção de que variáveis desse tipo sejam anuláveis. Um compilador pode reconhecer essas intenções de emitir avisos. Quando o contexto de anotação anulável está desativado, o uso dessa anotação pode gerar um aviso.

8.9.4 Contexto anulável

8.9.4.1 Generalidades

Cada linha de código-fonte tem um contexto anulável. As anotações e avisos sinalizam para o controle de contexto anulável anotações anuláveis (§8.9.4.3) e avisos anuláveis (§8.9.4.4), respectivamente. Cada sinalizador pode ser ativado ou desativado. Um compilador pode usar a análise de fluxo estático para determinar o estado nulo de qualquer variável de referência. O estado nulo de uma variável de referência (§8.9.5) não é nulo, talvez nulo ou talvez padrão.

O contexto anulável pode ser especificado no código-fonte através de diretivas anuláveis (§6.5.9) e/ou através de algum mecanismo específico de implementação externo ao código-fonte. Se ambas as abordagens forem usadas, as diretivas anuláveis substituem as configurações feitas por meio de um mecanismo externo.

O estado padrão do contexto anulável é a implementação definida.

Ao longo desta especificação, todo o código C# que não contém diretivas anuláveis, ou sobre o qual nenhuma instrução é feita em relação ao estado de contexto anulável atual, deve ser considerado como tendo sido compilado usando um contexto anulável onde as anotações e avisos estão habilitados.

Nota: Um contexto anulável em que ambos os sinalizadores estão desativados corresponde ao comportamento padrão anterior para tipos de referência. Nota final

8.9.4.2 Desativação anulável

Quando os sinalizadores de aviso e anotações são desabilitados, o contexto anulável é desabilitado.

Quando o contexto anulável está desativado:

  • Não deve ser gerado qualquer aviso quando uma variável de um tipo de referência não anotado é inicializada com, ou é-lhe atribuído um valor de, null.
  • Não deve ser gerado qualquer aviso quando uma variável de um tipo de referência que possivelmente tenha o valor nulo.
  • Para qualquer tipo Tde referência , a anotação ? em T? gera uma mensagem e o tipo T? é o mesmo que T.
  • Para qualquer restrição where T : C?de parâmetro de tipo, a anotação ? em C? gera uma mensagem e o tipo C? é o mesmo que C.
  • Para qualquer restrição where T : U?de parâmetro de tipo, a anotação ? em U? gera uma mensagem e o tipo U? é o mesmo que U.
  • A restrição class? genérica gera uma mensagem de aviso. O parâmetro type deve ser um tipo de referência.

    Nota: Esta mensagem é caracterizada como "informativa" em vez de "aviso", para não confundi-la com o estado da configuração de aviso anulável, que não está relacionada. Nota final

  • O operador ! de perdão nulo (§12.8.9) não tem efeito.

Exemplo:

#nullable disable annotations
string? s1 = null;    // Informational message; ? is ignored
string s2 = null;     // OK; null initialization of a reference
s2 = null;            // OK; null assignment to a reference
char c1 = s2[1];      // OK; no warning on dereference of a possible null;
                      //     throws NullReferenceException
c1 = s2![1];          // OK; ! is ignored

Exemplo final

8.9.4.3 Anotações anuláveis

Quando o sinalizador de aviso está desativado e o sinalizador de anotações está habilitado, o contexto anulável é anotações.

Quando o contexto anulável é anotações:

  • Para qualquer tipo Tde referência , a anotação ? em T? indica que T? um tipo anulável, enquanto o não anotado T não é anulável.
  • Nenhum aviso de diagnóstico relacionado à anulabilidade é gerado.
  • O operador ! de perdão nula (§12.8.9) pode alterar o estado nulo analisado de seu operando e quais avisos de diagnóstico de tempo de compilação são produzidos.

Exemplo:

#nullable disable warnings
#nullable enable annotations
string? s1 = null;    // OK; ? makes s2 nullable
string s2 = null;     // OK; warnings are disabled
s2 = null;            // OK; warnings are disabled
char c1 = s2[1];      // OK; warnings are disabled; throws NullReferenceException
c1 = s2![1];          // No warnings

Exemplo final

8.9.4.4 Advertências anuláveis

Quando o sinalizador de aviso está habilitado e o sinalizador de anotações está desativado, o contexto anulável é avisos.

Quando o contexto anulável é avisos, um compilador pode gerar diagnósticos nos seguintes casos:

  • Uma variável de referência que foi determinada como sendo talvez nula, é desreferenciada.
  • Uma variável de referência de um tipo não anulável é atribuída a uma expressão que talvez seja nula.
  • O ? é usado para observar um tipo de referência anulável.
  • O operador ! de perdão nula (§12.8.9) é usado para definir o estado nulo de seu operando como não nulo.

Exemplo:

#nullable disable annotations
#nullable enable warnings
string? s1 = null;    // OK; ? makes s2 nullable
string s2 = null;     // OK; null-state of s2 is "maybe null"
s2 = null;            // OK; null-state of s2 is "maybe null"
char c1 = s2[1];      // Warning; dereference of a possible null;
                      //          throws NullReferenceException
c1 = s2![1];          // The warning is suppressed

Exemplo final

8.9.4.5 Ativação anulável

Quando o sinalizador de aviso e o sinalizador de anotações estão habilitados, o contexto anulável é habilitado.

Quando o contexto anulável está habilitado:

  • Para qualquer tipo Tde referência , a anotação ? em T? torna T? um tipo anulável, enquanto o não anotado T não é anulável.
  • Um compilador pode usar a análise de fluxo estático para determinar o estado nulo de qualquer variável de referência. Quando os avisos anuláveis são habilitados, o estado nulo de uma variável de referência (§8.9.5) não é nulo, talvez nulo ou talvez padrão e
  • O operador ! de perdão nula (§12.8.9) define o estado nulo de seu operando como não nulo.
  • Um compilador pode emitir um aviso se a anulabilidade de um parâmetro type não corresponder à anulabilidade de seu argumento type correspondente.

8.9.5 Nulidades e estados nulos

Um compilador não é necessário para executar qualquer análise estática nem é necessário para gerar quaisquer avisos de diagnóstico relacionados à anulabilidade.

O restante desta subcláusula é condicionalmente normativo.

Um compilador que gera avisos de diagnóstico está em conformidade com essas regras.

Cada expressão tem um dos três estadosnulos:

  • talvez nulo: O valor da expressão pode ser avaliado como nulo.
  • talvez padrão: O valor da expressão pode ser avaliado como o valor padrão para esse tipo.
  • not null: O valor da expressão não é null.

O estado nulo padrão de uma expressão é determinado por seu tipo e pelo estado do sinalizador de anotações quando é declarado:

  • O estado nulo padrão de um tipo de referência anulável é:
    • Talvez nulo quando sua declaração estiver no texto onde o sinalizador de anotações está habilitado.
    • Não é nulo quando sua declaração está no texto onde o sinalizador de anotações está desativado.
  • O estado nulo padrão de um tipo de referência não anulável não é nulo.

Nota: O estado talvez padrão é usado com parâmetros de tipo não restritos quando o tipo é um tipo não anulável, como string e a expressão default(T) é o valor nulo. Como null não está no domínio para o tipo não anulável, o estado talvez seja padrão. Nota final

Um diagnóstico pode ser produzido quando uma variável (§9.2.1) de um tipo de referência não anulável é inicializada ou atribuída a uma expressão que talvez seja nula quando essa variável é declarada em texto onde o sinalizador de anotação está habilitado.

Exemplo: Considere o seguinte método onde um parâmetro é anulável e esse valor é atribuído a um tipo não anulável:

#nullable enable
public class C
{
    public void M(string? p)
    {
        // Warning: Assignment of maybe null value to non-nullable variable
        string s = p;
    }
}

Um compilador pode emitir um aviso onde o parâmetro que pode ser nulo é atribuído a uma variável que não deve ser nula. Se o parâmetro for verificado nulo antes da atribuição, um compilador pode usá-lo em sua análise de estado nulo e não emitir um aviso:

#nullable enable
public class C
{
    public void M(string? p)
    {
        if (p != null)
        {
            string s = p; // No warning
            // Use s
        }
    }
}

Exemplo final

Um compilador pode atualizar o estado nulo de uma variável como parte de sua análise.

Exemplo: Um compilador pode optar por atualizar o estado com base em quaisquer instruções no seu programa:

#nullable enable
public void M(string? p)
{
    int length = p.Length; // Warning: p is maybe null

    string s = p; // No warning. p is not null

    if (s != null)
    {
        int l2 = s.Length; // No warning. s is not null 
    }
    int l3 = s.Length; // Warning. s is maybe null
}

No exemplo anterior, um compilador pode decidir que, após a instrução int length = p.Length;, o estado nulo de p não é nulo. Se fosse nula, essa afirmação teria lançado um NullReferenceException. Isso é semelhante ao comportamento se o código tivesse sido precedido por if (p == null) throw NullReferenceException(); exceto que o código como escrito pode produzir um aviso, cujo objetivo é avisar que uma exceção pode ser lançada implicitamente. Exemplo final

Mais adiante no método, o código verifica se s não é uma referência nula. O estado nulo de s pode mudar para talvez nulo depois que o bloco de verificação nula for fechado. Um compilador pode inferir que s é talvez nulo porque o código foi escrito para assumir que ele pode ter sido nulo. Geralmente, quando o código contém uma verificação nula, um compilador pode inferir que o valor pode ter sido nulo:

Exemplo: Cada uma das expressões a seguir inclui alguma forma de verificação nula. O estado nulo de o pode mudar de não nulo para talvez nulo após cada uma destas instruções:

#nullable enable
public void M(string s)
{
    int length = s.Length; // No warning. s is not null

    _ = s == null; // Null check by testing equality. The null state of s is maybe null
    length = s.Length; // Warning, and changes the null state of s to not null

    _ = s?.Length; // The ?. is a null check and changes the null state of s to maybe null
    if (s.Length > 4) // Warning. Changes null state of s to not null
    {
        _ = s?[4]; // ?[] is a null check and changes the null state of s to maybe null
        _ = s.Length; // Warning. s is maybe null
    }
}

Declarações de eventos semelhantes a campos e de propriedades automáticas fazem uso de um campo de suporte gerado pelo compilador. A análise de estado nulo pode inferir que a atribuição ao evento ou propriedade é uma atribuição a um campo de suporte gerado pelo compilador.

Exemplo: Um compilador pode determinar que escrever uma propriedade automática ou um evento semelhante a um campo grava o campo de suporte gerado pelo compilador correspondente. O estado nulo da propriedade corresponde ao do campo subjacente.

class Test
{
    public string P
    {
        get;
        set;
    }

    public Test() {} // Warning. "P" not set to a non-null value.

    static void Main()
    {
        var t = new Test();
        int len = t.P.Length; // No warning. Null state is not null.
    }
}

No exemplo anterior, o construtor não define P para um valor não nulo, e um compilador pode emitir um aviso. Não há aviso quando a propriedade P é acessada, porque o tipo da propriedade é um tipo de referência não anulável. Exemplo final

Um compilador pode tratar uma propriedade (§15.7) como uma variável com estado, ou como acessadores independentes get e set (§15.7.3).

Exemplo: Um compilador pode escolher se a gravação em uma propriedade altera o estado nulo da leitura da propriedade ou se a leitura de uma propriedade altera o estado nulo dessa propriedade.

class Test
{
    private string? _field;
    public string? DisappearingProperty
    {
        get
        {
               string tmp = _field;
               _field = null;
               return tmp;
        }
        set
        {
             _field = value;
        }
    }

    static void Main()
    {
        var t = new Test();
        if (t.DisappearingProperty != null)
        {
            int len = t.DisappearingProperty.Length; // No warning. A compiler can assume property is stateful
        }
    }
}

No exemplo anterior, o campo de suporte para o DisappearingProperty é definido como null quando é lido. No entanto, um compilador pode assumir que a leitura de uma propriedade não altera o estado nulo dessa expressão. Exemplo final

Um compilador pode usar qualquer expressão que desreferencia uma variável, propriedade ou evento para definir o estado nulo como não nulo. Se fosse nula, a expressão de desreferência teria lançado um NullReferenceException:

Exemplo:


public class C
{
    private C? child;
   
    public void M()
    {
        _ = child.child.child; // Warning. Dereference possible null value
        var greatGrandChild = child.child.child; // No warning. 
    }
}

Exemplo final

Fim do texto condicionalmente normativo