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.Object
predefinida.
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.String
predefinida.
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
, ,long
eulong
, o valor padrão é0
. - Para
char
, o valor padrão e'\x0000'
. - Para
float
, o valor padrão e0.0f
. - Para
double
, o valor padrão e0.0d
. - Para
decimal
, o valor padrão é0m
(ou seja, valor zero com escala 0). - Para
bool
, o valor padrão efalse
. - Para um enum_type
E
, o valor padrão é0
, convertido para o tipoE
.
- Para
- 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 aValue
propriedade de tal valor faz com que uma exceção do tipoSystem.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
i
ej
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 emSystem.Int32
e os membros herdados deSystem.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 tipoint
e'a'
é um literal do tipochar
. 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
, ushort
int
, uint
, , long
, , ulong
e 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
a127
, inclusive. - O
byte
tipo representa inteiros de 8 bits sem sinal com valores de0
a255
, inclusive. - O
short
tipo representa inteiros de 16 bits com sinal com valores de-32768
a32767
, inclusive. - O
ushort
tipo representa inteiros de 16 bits sem sinal com valores de0
a65535
, inclusive. - O
int
tipo representa inteiros de 32 bits com sinal com valores de-2147483648
a2147483647
, inclusive. - O
uint
tipo representa inteiros de 32 bits sem sinal com valores de0
a4294967295
, inclusive. - O
long
tipo representa inteiros de 64 bits com sinal com valores de-9223372036854775808
a9223372036854775807
, inclusive. - O
ulong
tipo representa inteiros de 64 bits sem sinal com valores de0
a18446744073709551615
, inclusive. - O
char
tipo representa inteiros de 16 bits sem sinal com valores de0
a65535
, inclusive. O conjunto de valores possíveis para o tipochar
corresponde ao conjunto de caracteres Unicode.Observação: embora
char
tenha a mesma representação queushort
o , 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 osbyte
tipos andushort
tenham intervalos de valores que são totalmente representáveis usando ochar
tipo, as conversões implícitas de sbyte, byte ouushort
tochar
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 paradouble
, 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 formax * y / z
, onde a multiplicação produz um resultado que está fora dodouble
intervalo, mas a divisão subsequente traz o resultado temporário de volta aodouble
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 Emin ≤ e ≤ Emax, 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 decimal
s com um valor absoluto menor que 1.0m
, o valor é exato até pelo menos a 28ª casa decimal. Para decimal
s 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
false
booleano , e um valor integral ou de ponto flutuante diferente de zero ou um ponteiro não nulo pode ser convertido no valortrue
booleano . 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 comnull
. 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
, , uint
ou 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 oItem...
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
, epair3
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 nomesItem1
eItem2
correspondem às suas posições, enquanto o tipo de tupla forpair5
não é permitido, porque os nomesItem2
eItem123
não.As declarações para
pair6
epair7
demonstram que os tipos de tupla são intercambiáveis com os tipos construídos do formulárioValueTuple<...>
e que onew
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 tipobool
- Uma
Value
propriedade do tipoT
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, vamosC
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 tipoA
em tipoC
por uma das seguintes características: - Se a restrição for a restrição do tipo de referência (
class
), o tipoA
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
eSystem.Enum
são tipos de referência que satisfazem essa restrição. nota finalA
é 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 tipoA
deverá atender a um dos seguintes itens:A
é umstruct
tipo ouenum
tipo, mas não um tipo de valor anulável.
Nota:
System.ValueType
eSystem.Enum
são tipos de referência que não atendem a essa restrição. nota finalA
é 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 tipoA
não deverá serabstract
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
é umclass
que não é abstrato e contém um construtor público declarado explicitamente sem parâmetros.A
abstract
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âmetroT
de tipo para queT
satisfaça a restrição imposta pela baseclass
B<T>
. Por outro lado,class
E
não é necessário especificar uma restrição, poisList<T>
implementaIEnumerable
para qualquerT
.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 D
delegado , 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 paraExpression<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 retornax + 1
, e a expressão árvore exp faz referência a uma estrutura de dados que descreve a expressãox => 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
edynamic
- entre tipos construídos que são os mesmos ao substituir
dynamic
porobject
- entre os tipos de tupla que são os mesmos ao substituir
dynamic
porobject
- entre
- As conversões implícitas e explícitas de e para
object
também se aplicam adynamic
. - As assinaturas que são iguais ao serem substituídas
dynamic
sãoobject
consideradas a mesma assinatura. - O tipo
dynamic
é indistinguível do tipoobject
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
,uint
float
long
char
double
ulong
,decimal
ou .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
eR?
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 valornull
, 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
T
de referência , a anotação?
emT?
gera uma mensagem e o tipoT?
é o mesmo queT
. - Para qualquer restrição
where T : C?
de parâmetro de tipo , a anotação?
geraC?
uma mensagem e o tipoC?
é o mesmo queC
. - Para qualquer restrição
where T : U?
de parâmetro de tipo , a anotação?
geraU?
uma mensagem e o tipoU?
é o mesmo queU
. - 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
T
de referência, a anotação?
emT?
indica queT?
um tipo anulável, enquanto o unannotatedT
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
T
de referência, a anotação?
emT?
criaT?
um tipo anulável, enquanto o unannotatedT
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ãodefault(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 dep
não é nulo. Se fosse nulo, essa instrução teria lançado umNullReferenceException
. Isso é semelhante ao comportamento se o código tivesse sido precedido porif (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
ECMA C# draft specification