Compartilhar via


8 Tipos

8.1 Geral

Os tipos da linguagem C# são divididos em duas categorias principais: tipos de referência e tipos de valor. Os tipos de valor e os tipos de referência podem ser tipos genéricos, que usam 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) está disponível apenas em código não seguro (§23).

Os tipos de valor diferem dos tipos de referência porque as variáveis dos tipos de valor contêm diretamente seus dados, enquanto as variáveis dos tipos de referência armazenam referências a 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 as operações em uma variável afetem o objeto referenciado pela outra variável. Com os tipos de valor, cada uma das variáveis tem 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. Nesse 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 modo que um valor de qualquer tipo pode ser tratado como um objeto. Cada tipo no C#, direta ou indiretamente, deriva do tipo de classe object, e object é a classe base definitiva de todos os tipos. Os valores de tipos de referência são tratados como objetos simplesmente exibindo os valores como tipo object. Os valores dos tipos de valor são tratados como objetos executando operações de conversão 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 Geral

Um tipo de referência é um tipo de classe, um tipo de interface, um tipo de matriz, um tipo 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 acrescentando 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 adiante no §8.9.

Um valor de tipo de referência é uma referência a uma instância do tipo, esta última conhecida como 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 classe

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 dão suporte à 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.

Determinados 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. Consulte §8.2.3.
System.String O tipo de cadeia de caracteres da linguagem C#. Ver §8.2.5.
System.ValueType A classe base de todos os tipos de valor. Consulte §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. Consulte §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 puder ser aplicada legitimamente ao objeto referenciado, nenhum erro será fornecido 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 detalhes em §8.7 e a associação dinâmica em §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 string classe representam cadeias de caracteres Unicode.

Os valores do string tipo podem ser gravados como literais de cadeia de 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 struct que implementa 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 em §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 calculados. 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. Para métodos de instância, também se refere às instâncias de objeto correspondentes.

Observação: 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. No ú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 delegado são descritos na seção 20.

8.3 Tipos de valor

8.3.1 Geral

Um tipo de valor é um tipo struct ou um tipo de enumeração. O C# fornece um conjunto de tipos de struct predefinidos chamados de tipos simples. Os tipos simples são identificados por meio 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 que denota 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 class System.ValueType, que, por sua vez, herda da classe object. Não é possível que nenhum tipo derive de um tipo de valor, e os tipos de valor são, portanto, implicitamente selados (§15.2.2.3).

Observe que System.ValueType isso não é em si um value_type. Em vez disso, é 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 público sem parâmetros chamado construtor padrão. O construtor padrão retorna uma instância inicializada por 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, int, uint, , longe ulong, o valor padrão é 0.
    • Para char, o valor padrão e '\x0000'.
    • Para float, o valor padrão e 0.0f.
    • Para double, o valor padrão e 0.0d.
    • Para decimal, o valor padrão é 0m (ou seja, valor zero com escala 0).
    • Para bool, o valor padrão e false.
    • Para um enum_type E, o valor padrão é 0, convertido para o tipo E.
  • Por um struct_type, o valor padrão é o valor produzido pela definição de todos os campos de tipo de valor como seu valor padrão e todos os campos de tipo de referência como null.
  • Por 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 do 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.

Observação: por motivos de eficiência, esse requisito não se destina a 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 o uso do construtor padrão. nota final

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

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

exemplo de fim

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

8.3.4 Tipos de struct

Um tipo de 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 de tipos struct é descrita em §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 alias de um tipo struct, todo tipo simples tem membros.

Exemplo: int tem os membros declarados em System.Int32 e os membros herdados de System.Object, e as seguintes instruçõ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 de fim

Observação: os tipos simples diferem de outros tipos de struct porque permitem determinadas 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 de fim
  • Quando os operandos de uma expressão são todos constantes de tipo simples, é possível que o compilador avalie a expressão em tempo de compilação. Tal expressão é conhecida como constant_expression (§12.23). Expressões que envolvem operadores definidos por outros tipos de struct não são consideradas expressões constantes
  • Por meio 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 da avaliação de operadores de conversão definidos por outros tipos de struct, mas um operador de conversão definido pelo usuário nunca pode participar da avaliação de outro operador de conversão definido pelo usuário (§10.5.3).

nota final.

8.3.6 Tipos integrais

O C# dá suporte a nove tipos integrais: sbyte, byte, short, ushortint, uint, , long, , ulonge char. Os tipos integrais têm os seguintes tamanhos e intervalos de valores:

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

    Observação: embora char tenha a mesma representação que ushorto , nem todas as operações permitidas em um tipo são permitidas no outro. nota final

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

Os integral_type operadores unários e binários sempre operam com precisão de 32 bits com sinal, precisão de 32 bits sem sinal, precisão de 64 bits com sinal ou precisão de 64 bits sem sinal, conforme detalhado em §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, embora os byte tipos and ushort tenham intervalos de valores que são totalmente representáveis usando o char tipo, as conversões implícitas de sbyte, byte ou ushort to char não existem.
  • As constantes do char tipo devem ser escritas como character_literals ou como integer_literals em combinação com uma conversão para o tipo char.

Exemplo: (char)10 é o mesmo que '\x000A'. exemplo de fim

Os checked operadores e instruções and unchecked são 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 de 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 alta ordem que não se encaixam no tipo de destino são descartados.

8.3.7 Tipos de ponto flutuante

O C# dá suporte a dois tipos de ponto flutuante: float e double. Os float tipos and double são representados usando os formatos IEC 60559 de precisão simples 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 se comportam de forma idêntica ao valor zero simples, mas certas operações distinguem entre os dois (§12.10.3).
  • Infinito positivo e infinito negativo. Os infinitos são produzidos 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 de fim

  • O valor Not-a-Number , geralmente abreviado como NaN. NaNs são produzidos por operações de ponto flutuante inválidas, como a divisão de zero por zero.
  • O conjunto finito de valores diferentes de zero da forma s × m × 2e, onde s é 1 ou −1, e m e e e são determinados pelo tipo de ponto flutuante particular: Para float, 0< m< 2²⁴ e −149 ≤ e ≤ 104, e para double, 0< m< 2⁵³ e −1075 ≤ e ≤ 970. Números de ponto 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 dê suporte a números de ponto 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 a 16 dígitos.

Se um dos operandos de um operador binário for um tipo de ponto flutuante, as promoções numéricas padrão serão aplicadas, conforme detalhado em §12.4.7, e a operação será executada com float precisão double .

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

  • O resultado de uma operação de ponto flutuante é arredondado para o valor representável mais próximo no formato de destino.
  • Se a magnitude do resultado de uma operação de ponto flutuante for muito pequena para o formato de destino, o resultado da operação se tornará 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 se tornará NaN.
  • Se um ou ambos os operandos de uma operação de ponto flutuante forem NaN, o resultado da operação se tornará NaN.

As operações de ponto flutuante podem ser executadas com maior precisão do que o tipo de resultado da operação. Para forçar um valor de um tipo de ponto flutuante para a precisão exata de seu tipo, uma conversão explícita (§12.9.7) pode ser usada.

Exemplo: algumas arquiteturas de hardware dão suporte a 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 em desempenho essas arquiteturas de hardware podem ser feitas para executar operações de ponto flutuante com menos precisão e, em vez de exigir que uma implementação perca o desempenho e a precisão, o C# permite que um tipo de precisão mais alta 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 ao 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 de fim

8.3.8 O tipo decimal

O tipo decimal é 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 precisão de pelo menos 28 dígitos.

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 EmineEmax, onde Cmax é pelo menos 1 × 10²⁸, Emin ≤ 0, e Emax ≥ 28. O decimal tipo não dá necessariamente suporte a zeros com sinal, infinitos ou NaN's.

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

Se um dos operandos de um operador binário for do tipo, as promoções numéricas padrão serão aplicadas, conforme detalhado em §12.4.7, e a operação será executada decimal com double precisão.

O resultado de uma operação em valores do tipo decimal é aquele 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 é igualmente próximo de dois valores representáveis, para o valor que tem um número par na posição de dígito menos significativo (isso é conhecido como "arredondamento de 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 produzir um resultado cuja magnitude é muito grande para o decimal formato, a System.OverflowException será lançado.

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

8.3.9 O tipo Bool

O bool tipo representa quantidades lógicas booleanas. Os valores possíveis do tipo bool são true e false. A representação de false é descrita em §8.3.3. Embora a representação de true não seja especificada, ela 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.

Observação: nas linguagens C e C++, um valor integral zero ou de ponto flutuante ou um ponteiro nulo pode ser convertido no valor falsebooleano , e um valor integral ou de ponto flutuante diferente de zero ou um ponteiro não nulo pode ser convertido no valor truebooleano . Em C#, essas conversões são realizadas comparando explicitamente um valor integral ou de ponto 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, , uintou long 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 são restritos aos valores das constantes nomeadas. Os tipos de enumeração são definidos por meio de declarações de enumeração (§19.2).

8.3.11 Tipos de tupla

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 é chamado de aridade. Um tipo de tupla é escrito (T1 I1, ..., Tn In) com n ≥ 2, em que os identificadores são nomes de elementos de tupla I1...In opcionais.

Essa sintaxe é uma abreviação para um tipo construído com os tipos T1...Tn de System.ValueTuple<...>, que deve ser um conjunto de tipos de 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, as tuplas com uma aridade maior que sete são representadas com um tipo System.ValueTuple<T1, ..., T7, TRest> de struct genérico que, além dos elementos de tupla, tem um Rest campo que contém um valor aninhado dos elementos restantes, usando outro System.ValueTuple<...> tipo. Essa nidificação pode ser observada de várias maneiras, por exemplo, através da presença de um Rest campo. Quando apenas um único campo adicional é necessário, o tipo System.ValueTuple<T1> de struct genérico é usado; esse tipo não é considerado um tipo de tupla em si. Quando mais de sete campos adicionais são obrigatórios, System.ValueTuple<T1, ..., T7, TRest> é usado recursivamente.

Os nomes de elementos dentro de um tipo de tupla devem ser distintos. Um nome de elemento de tupla da forma ItemX, onde X é qualquer sequência de dígitos decimais não0 iniciados que podem representar a posição de um elemento de tupla, só é permitido na posição denotada 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 conversí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 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 por meio 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 poderá 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: Dado 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 no, alguns ou todos os elementos de tipo de tupla.

O tipo de tupla for pair4 é válido porque os nomes Item1 e Item2 correspondem às suas posições, enquanto o tipo de tupla for pair5 não é permitido, 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 do formulário 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 de fim

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 é gravado T?, onde T é o tipo subjacente. Essa sintaxe é uma abreviação de System.Nullable<T>, e as duas formas podem ser usadas de forma intercambiável.

Por outro lado, um tipo de valor não anulável é qualquer tipo de valor diferente de System.Nullable<T> e sua abreviação T? (para qualquer T), além de qualquer parâmetro de tipo que seja 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 abordados na seção 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 é considerada não nula. Uma instância não nula contém um valor conhecido e Value retorna esse valor.

Uma instância para a qual HasValue é false é considerada nula. Uma instância nula tem um valor indefinido. A tentativa de ler o Value de uma instância nula faz com que a System.InvalidOperationException seja lançada. O processo de acesso à propriedade Value de uma instância anulável é conhecido como desencapsulamento.

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

new T?(x)

cria uma instância não nula para T? a 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 de 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 encaixotamento e unboxing fornece uma ponte entre value_types e reference_types, permitindo que qualquer valor de um value_type seja convertido de e para o tipo object. A conversão boxing e a unboxing permitem uma exibição unificada do sistema de tipos em que um valor de qualquer tipo pode ser tratado como um object.

A conversão boxing é descrita com mais detalhes em §10.2.9 e a unboxing é descrita em §10.3.7.

8.4 Tipos construídos

8.4.1 Geral

Uma declaração de tipo genérico, por si só, denota um tipo genérico não associado que é usado como um "blueprint" 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 associado só pode ser usado 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, somente 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 tipos diferentes, 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 de fim

As regras detalhadas para pesquisa de nome nas produções namespace_or_type_name são descritas em §7.8. A resolução de ambiguidades nessas produções é descrita em §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 em uma declaração genérica class e o tipo de instância da declaração que a contém é usado implicitamente 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 de fim

Um tipo construído não enum não deve ser usado 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 todas as restrições no parâmetro de tipo correspondente (§15.2.5). Um argumento de tipo de referência cuja nulidade não corresponde à nulidade do parâmetro de tipo satisfaz a restrição; no entanto, um aviso pode ser emitido.

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 de tipo 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 forem 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 o contêm forem 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érico é 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 está associado 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 somente 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 associado e existir uma conversão de identidade entre cada um de 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. Os 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 acoplados e não acoplados

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

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

8.4.5 Satisfazendo restrições

Sempre que um tipo construído ou método genérico é referenciado, os argumentos de tipo fornecidos são verificados em relação às restrições de parâmetro de tipo declaradas no tipo ou método genérico (§15.2.5). Para cada where cláusula, o argumento A de tipo que corresponde ao parâmetro de tipo nomeado é 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 a restrição, é possível converter o tipo A em tipo C por uma das seguintes características:
    • Uma conversão de identidade (§10.2.2)
    • Uma conversão de referência implícita (§10.2.8)
    • Uma conversão boxing (§10.2.9), desde que esse tipo A seja um tipo de valor não anulável.
    • Uma conversão implícita de referência, conversão boxing ou 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 um dos seguintes elementos:
    • 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 essa restrição. nota final

    • A é um parâmetro de tipo que é conhecido por ser um tipo de referência (§8.2).
  • Se a restrição for a restrição do tipo de valor (struct), o tipo A deverá atender a um dos seguintes itens:
    • 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 atendem a essa 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 deverá ser abstract e deverá ter um construtor público sem parâmetros. Isso será satisfeito se uma das seguintes opções for verdadeira:
    • A é um tipo de valor, pois 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 declarado explicitamente sem parâmetros.
    • Aabstract não é e tem um construtor padrão (§15.11.5).

Um erro em tempo de compilação ocorrerá se uma ou mais restrições de um parâmetro de tipo não forem atendidas 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 de tipo para que T satisfaça a restrição imposta pela base class B<T>. Por outro lado, class E não é necessário especificar uma restrição, pois 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 de fim

8.5 Parâmetros de tipo

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

type_parameter
    : identifier
    ;

Como um parâmetro de tipo pode ser instanciado com muitos argumentos de tipo diferentes, os parâmetros de tipo 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 membro em parâmetros de tipo dependem das restrições, se houver, aplicadas ao parâmetro de tipo. Eles são detalhados no §12.5.
  • As conversões disponíveis para um parâmetro de tipo dependem das restrições, se houver, aplicadas ao parâmetro de tipo. Eles são detalhados em §10.2.12 e §10.3.8.
  • O literal null não pode ser convertido em um tipo fornecido por um parâmetro de tipo, exceto se o parâmetro de tipo for conhecido como 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 fornecido por um parâmetro de tipo pode ser comparado com null usando == e != (§12.12.7), a menos que o parâmetro de tipo tenha a restrição de tipo de valor.
  • Uma new expressão (§12.8.17.2) só pode ser usada com um parâmetro de tipo se o parâmetro de tipo for restrito por um constructor_constraint ou pela restrição de tipo de valor (§15.2.5).
  • Um parâmetro de tipo não pode ser usado em nenhum lugar dentro de um atributo.
  • Um parâmetro de tipo 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 usado como um 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 é associado a um tipo de tempo de execução que foi especificado fornecendo um argumento de tipo para a declaração de tipo genérico. Assim, o tipo de uma 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 que envolvem 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 as expressões lambda sejam representadas como estruturas de dados em vez de código executável. As á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 restante desta especificação, esses tipos serão referidos usando a abreviação Expression<TDelegate>.

Se existir uma conversão de uma expressão lambda para um tipo Ddelegado , também existirá 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 delegado gera um delegado que faz referência ao código executável para a expressão lambda, a conversão em um tipo de árvore de expressão cria uma representação de árvore de expressão da expressão lambda. Mais detalhes dessa 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>, uma conversão também existe 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 expressão árvore exp faz referência a uma estrutura de dados que descreve a expressão x => x + 1.

exemplo de fim

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

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

Invocar esse delegado faz com que o código representado pela árvore de expressão seja executado. Assim, dadas as definições acima, del e del2 são equivalentes, e as duas afirmações a seguir 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 por Expression<TDelegate> é definida pela implementação além do requisito de um Compile método descrito acima.

Observação: 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 no código do usuário

nota final

8.7 O tipo dinâmico

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

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

  • As operações em expressões do tipo dynamic podem ser associadas dinamicamente (§12.3.3).
  • A inferência de tipo (§12.6.3) será preferível dynamic object se ambos forem candidatos.
  • dynamic não pode ser usado como
    • o tipo em um object_creation_expression (§12.8.17.2)
    • uma class_base (§15.2.4)
    • uma predefined_type em um 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 de struct_interfaces (§16.2.5) ou interface_type_list (§15.2.4.1).

Devido a essa equivalência, o seguinte é válido:

  • 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 os tipos de tupla que são os mesmos ao substituir dynamic por object
  • As conversões implícitas e explícitas de e para object também se aplicam a dynamic.
  • As assinaturas que são iguais ao serem substituídas dynamic são object consideradas a mesma assinatura.
  • O tipo dynamic é indistinguível do tipo object em tempo de execução.
  • Uma expressão do tipo dynamic é chamada de 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 um reference_type, um type_parameter ou um tipo construído e não contém campos de instância cujo tipo não seja um unmanaged_type. Em outras palavras, um unmanaged_type é um dos seguintes:

  • sbyte, byte, short, ushort, int, uintfloatlongchardoubleulong, decimalou .bool
  • Qualquer enum_type.
  • Qualquer struct_type definido pelo usuário que não seja um tipo construído e contenha campos de instância somente de unmanaged_types.
  • Em código não seguro (§23.2), qualquer pointer_type (§23.3).

8.9 Tipos de referência e nulidade

8.9.1 Geral

Um tipo de referência anulável é indicado pelo acréscimo de 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 em §8.9.5. Existe uma conversão de identidade entre um tipo de referência anulável e seu tipo de referência não anulável correspondente (§10.2.2).

Há duas formas de nulidade para tipos de referência:

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

Observação: 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 em §8.2.1. O diagnóstico deve ser limitado 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 as 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 seja nulo. A anotação ? indica a intenção de que as variáveis desse tipo sejam anuláveis. O compilador pode reconhecer essas intenções para emitir avisos. Quando o contexto de anotação anulável é desabilitado, o uso dessa anotação pode gerar um aviso.

8.9.4 Contexto anulável

8.9.4.1 Geral

Cada linha de código-fonte tem um contexto anulável. Os sinalizadores de anotações e avisos para o contexto anulável controlam anotações anuláveis (§8.9.4.3) e avisos anuláveis (§8.9.4.4), respectivamente. Cada sinalizador pode ser ativado ou desativado. O 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 por meio de diretivas anuláveis (§6.5.9) e/ou por meio de algum mecanismo específico de implementação externo ao código-fonte. Se ambas as abordagens forem usadas, as diretivas anuláveis substituirão as configurações feitas por meio de um mecanismo externo.

O estado padrão do contexto anulável é definido pela implementação.

Ao longo dessa 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 atual do contexto anulável, deve ser considerado como tendo sido compilado usando um contexto anulável em que as anotações e os avisos estão habilitados.

Observação: 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 Desabilitar anulável

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

Quando o contexto anulável é desabilitado:

  • Nenhum aviso deve ser gerado quando uma variável de um tipo de referência não anotado é inicializada ou atribuída a um valor de, null.
  • Nenhum aviso deve ser gerado quando uma variável de um tipo de referência que possivelmente tem 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 ? gera C? 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 ? gera U? 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.

    Observação: essa 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 ! tolerante a 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 de fim

8.9.4.3 Anotações anuláveis

Quando o sinalizador de aviso está desabilitado 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 unannotated T não permite valor nulo.
  • Nenhum aviso de diagnóstico relacionado à nulidade é gerado.
  • O operador ! tolerante a nulos (§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 de fim

8.9.4.4 Avisos anuláveis

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

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 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 ! tolerante a nulos (§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 de fim

8.9.4.5 Habilitaçã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? cria T? um tipo anulável, enquanto o unannotated T não permite valor nulo.
  • O 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 ! tolerante a nulo (§12.8.9) define o estado nulo de seu operando como não nulo.
  • O compilador poderá emitir um aviso se a nulidade de um parâmetro de tipo não corresponder à nulidade de seu argumento de tipo correspondente.

8.9.5 Nulidade e estados nulos

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

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:

  • maybe null: o valor da expressão pode ser avaliado como nulo.
  • maybe default: 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 é nulo.

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

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

Observação: o estado padrão maybe é usado com parâmetros de tipo irrestritos 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 no texto em que o sinalizador de anotação está habilitado.

Exemplo: considere o seguinte método em que 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)
    {
        // Assignment of maybe null value to non-nullable variable
        string s = p;
    }
}

O compilador pode emitir um aviso em que 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, o compilador poderá usá-lo em sua análise de estado anulável e não emitir um aviso:

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

exemplo de fim

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

Exemplo: o compilador pode optar por atualizar o estado com base em qualquer instrução em seu programa:

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

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

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

No exemplo anterior, o compilador pode decidir que, após a instrução int length = p.Length;, o estado nulo de p não é nulo. Se fosse nulo, essa instruçã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 conforme escrito pode produzir um aviso, cujo objetivo é avisar que uma exceção pode ser lançada implicitamente.

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 verificado nulo for fechado. O compilador pode inferir que s talvez seja nulo porque o código foi escrito para assumir que pode ter sido nulo. Geralmente, quando o código contém uma verificação nula, o compilador pode inferir que o valor pode ter sido nulo.exemplo de fim

Exemplo: o compilador pode tratar uma propriedade (§15.7) como uma variável com estado ou como acessadores get e set independentes (§15.7.3). Em outras palavras, 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 exemplo anterior, o campo de suporte do é definido como nulo quando é lido DisappearingProperty . No entanto, um compilador pode supor que a leitura de uma propriedade não altera o estado nulo dessa expressão. exemplo de fim

Fim do texto condicionalmente normativo