8 Tipos
8.1 Generalidades
Os tipos da linguagem C# são divididos em duas categorias principais: tipos de referência e tipos de valor. Ambos os tipos de valor e tipos de referência podem ser tipos genéricos, que tomam um ou mais parâmetros de tipo. Os parâmetros de tipo podem designar tipos de valor e tipos de referência.
type
: reference_type
| value_type
| type_parameter
| pointer_type // unsafe code support
;
pointer_type (§23.3) só está disponível no código não seguro (§23).
Os tipos de valor diferem dos tipos de referência na medida em que as variáveis dos tipos de valor contêm diretamente seus dados, enquanto as variáveis dos tipos de referência armazenam referências aos seus dados, sendo estes últimos conhecidos como objetos. Com tipos de referência, é possível que duas variáveis façam referência ao mesmo objeto e, portanto, é possível que operações em uma variável afetem o objeto referenciado pela outra variável. Com os tipos de valor, as variáveis têm cada uma sua própria cópia dos dados, e não é possível que as operações em uma afetem a outra.
Nota: Quando uma variável é um parâmetro de referência ou saída, ela não tem seu próprio armazenamento, mas faz referência ao armazenamento de outra variável. Neste caso, a variável ref ou out é efetivamente um alias para outra variável e não uma variável distinta. Nota final
O sistema de tipos do C# é unificado de tal forma que um valor de qualquer tipo pode ser tratado como um objeto. Cada tipo em C# deriva direta ou indiretamente do object
tipo de classe e object
é a classe base final de todos os tipos. Os valores dos tipos de referência são tratados como objetos simplesmente visualizando os valores como tipo object
. Os valores dos tipos de valores são tratados como objetos através da realização de operações de boxing e unboxing (§8.3.13).
Por conveniência, ao longo desta especificação, alguns nomes de tipo de biblioteca são escritos sem usar sua qualificação de nome completo. Consulte § C.5 para obter mais informações.
8.2 Tipos de referência
8.2.1 Generalidades
Um tipo de referência é um tipo de classe, um tipo de interface, um tipo de matriz, um tipo de delegado ou o dynamic
tipo. Para cada tipo de referência não anulável, há um tipo de referência anulável correspondente anotado anexando o ?
ao nome do tipo.
reference_type
: non_nullable_reference_type
| nullable_reference_type
;
non_nullable_reference_type
: class_type
| interface_type
| array_type
| delegate_type
| 'dynamic'
;
class_type
: type_name
| 'object'
| 'string'
;
interface_type
: type_name
;
array_type
: non_array_type rank_specifier+
;
non_array_type
: value_type
| class_type
| interface_type
| delegate_type
| 'dynamic'
| type_parameter
| pointer_type // unsafe code support
;
rank_specifier
: '[' ','* ']'
;
delegate_type
: type_name
;
nullable_reference_type
: non_nullable_reference_type nullable_type_annotation
;
nullable_type_annotation
: '?'
;
pointer_type está disponível apenas em código não seguro (§23.3). nullable_reference_type é discutido mais detalhadamente no §8.9.
Um valor de tipo de referência é uma referência a uma instância do tipo, esta última conhecida como um objeto. O valor null
especial é compatível com todos os tipos de referência e indica a ausência de uma instância.
8.2.2 Tipos de classes
Um tipo de classe define uma estrutura de dados que contém membros de dados (constantes e campos), membros de função (métodos, propriedades, eventos, indexadores, operadores, construtores de instância, finalizadores e construtores estáticos) e tipos aninhados. Os tipos de classe suportam herança, um mecanismo pelo qual as classes derivadas podem estender e especializar classes base. Instâncias de tipos de classe são criadas usando object_creation_expression s (§12.8.17.2).
Os tipos de classe são descritos no §15.
Certos tipos de classe predefinidos têm um significado especial na linguagem C#, conforme descrito na tabela abaixo.
Tipo de classe | Descrição |
---|---|
System.Object |
A classe base final de todos os outros tipos. Ver ponto 8.2.3. |
System.String |
O tipo de cadeia de caracteres da linguagem C#. Ver ponto 8.2.5. |
System.ValueType |
A classe base de todos os tipos de valor. Ver ponto 8.3.2. |
System.Enum |
A classe base de todos os enum tipos. Ver §19.5. |
System.Array |
A classe base de todos os tipos de matriz. Ver §17.2.2. |
System.Delegate |
A classe base de todos os delegate tipos. Ver §20.1. |
System.Exception |
A classe base de todos os tipos de exceção. Ver §21.3. |
8.2.3 O tipo de objeto
O object
tipo de classe é a classe base final de todos os outros tipos. Cada tipo em C# deriva direta ou indiretamente do object
tipo de classe.
A palavra-chave object
é simplesmente um alias para a classe System.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 pode ser legitimamente aplicada ao objeto referenciado, nenhum erro é dado durante a compilação. Em vez disso, uma exceção será lançada quando a resolução da operação falhar em tempo de execução.
O dynamic
tipo é descrito em mais pormenor no §8.7 e a ligação dinâmica no §12.3.1.
8.2.5 O tipo de string
O string
tipo é um tipo de classe selada que herda diretamente do object
. As instâncias da classe representam cadeias de string
caracteres Unicode.
Os valores do tipo podem ser escritos como literais de cadeia de string
caracteres (§6.4.5.6).
A palavra-chave string
é simplesmente um alias para a classe System.String
predefinida.
8.2.6 Tipos de interface
Uma interface define um contrato. Uma classe ou estrutura que implemente uma interface deve aderir ao seu contrato. Uma interface pode herdar de várias interfaces base, e uma classe ou struct pode implementar várias interfaces.
Os tipos de interface são descritos no §18.
8.2.7 Tipos de matriz
Uma matriz é uma estrutura de dados que contém zero ou mais variáveis, que são acessadas por meio de índices computados. As variáveis contidas em uma matriz, também chamadas de elementos da matriz, são todas do mesmo tipo, e esse tipo é chamado de tipo de elemento da matriz.
Os tipos de matriz são descritos no §17.
8.2.8 Tipos de delegados
Um delegado é uma estrutura de dados que se refere a um ou mais métodos. Por exemplo, também se refere às suas instâncias de objeto correspondentes.
Nota: O equivalente mais próximo de um delegado em C ou C++ é um ponteiro de função, mas enquanto um ponteiro de função só pode fazer referência a funções estáticas, um delegado pode fazer referência a métodos estáticos e de instância. Neste último caso, o delegado armazena não apenas uma referência ao ponto de entrada do método, mas também uma referência à instância do objeto na qual invocar o método. Nota final
Os tipos de delegados são descritos no §20.
8.3 Tipos de valores
8.3.1 Generalidades
Um tipo de valor é um tipo struct ou um tipo de enumeração. O C# fornece um conjunto de tipos struct predefinidos chamados de tipos simples. Os tipos simples são identificados através de palavras-chave.
value_type
: non_nullable_value_type
| nullable_value_type
;
non_nullable_value_type
: struct_type
| enum_type
;
struct_type
: type_name
| simple_type
| tuple_type
;
simple_type
: numeric_type
| 'bool'
;
numeric_type
: integral_type
| floating_point_type
| 'decimal'
;
integral_type
: 'sbyte'
| 'byte'
| 'short'
| 'ushort'
| 'int'
| 'uint'
| 'long'
| 'ulong'
| 'char'
;
floating_point_type
: 'float'
| 'double'
;
tuple_type
: '(' tuple_type_element (',' tuple_type_element)+ ')'
;
tuple_type_element
: type identifier?
;
enum_type
: type_name
;
nullable_value_type
: non_nullable_value_type nullable_type_annotation
;
Ao contrário de uma variável de um tipo de referência, uma variável de um tipo de valor pode conter o valor null
somente se o tipo de valor for um tipo de valor anulável (§8.3.12). Para cada tipo de valor não anulável, há um tipo de valor anulável correspondente denotando o mesmo conjunto de valores mais o valor null
.
A atribuição a uma variável de um tipo de valor cria uma cópia do valor que está sendo atribuído. Isso difere da atribuição a uma variável de um tipo de referência, que copia a referência, mas não o objeto identificado pela referência.
8.3.2 O tipo System.ValueType
Todos os tipos de valor herdam implicitamente do class
System.ValueType
, que, por sua vez, herda da classe object
. Não é possível que qualquer tipo derive de um tipo de valor, e os tipos de valor são, portanto, implicitamente selados (§15.2.2.3).
Note-se que System.ValueType
não é em si um value_type. Pelo contrário, é um class_type do qual todos os value_types são automaticamente derivados.
8.3.3 Construtores padrão
Todos os tipos de valor declaram implicitamente um construtor de instância sem parâmetros público chamado construtor padrão. O construtor padrão retorna uma instância inicializada zero conhecida como o valor padrão para o tipo de valor:
- Para todos os simple_types, o valor padrão é o valor produzido por um padrão de bits de todos os zeros:
- Para
sbyte
,byte
,short
, ,ushort
,int
uint
,long
, e , oulong
valor padrão é0
. - Para
char
, o valor padrão é'\x0000'
. - Para
float
, o valor padrão é0.0f
. - Para
double
, o valor padrão é0.0d
. - Para
decimal
, o valor padrão é0m
(ou seja, valor zero com escala 0). - Para
bool
, o valor padrão éfalse
. - Para um enum_type
E
, o valor padrão é0
, convertido para o tipoE
.
- Para
- Para um struct_type, o valor padrão é o valor produzido definindo todos os campos de tipo de valor como seu valor padrão e todos os campos de tipo de referência como
null
. - Para um nullable_value_type o valor padrão é uma instância para a qual a
HasValue
propriedade é false. O valor padrão também é conhecido como o valor nulo do tipo de valor anulável. A tentativa de ler aValue
propriedade de tal valor faz com que uma exceção de 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 .
Nota: Por razões de eficiência, este requisito não se destina a realmente fazer com que a implementação gere uma chamada de construtor. Para tipos de valor, a expressão de valor padrão (§12.8.21) produz o mesmo resultado que usar o construtor padrão. Nota final
Exemplo: No código abaixo, as variáveis
i
ej
k
todas são inicializadas como zero.class A { void F() { int i = 0; int j = new int(); int k = default(int); } }
Exemplo final
Como cada tipo de valor tem implicitamente um construtor de instância sem parâmetros público, não é possível que um tipo struct contenha uma declaração explícita de um construtor sem parâmetros. Um tipo struct é, no entanto, permitido declarar construtores de instância parametrizados (§16.4.9).
8.3.4 Tipos de estruturas
Um tipo struct é um tipo de valor que pode declarar constantes, campos, métodos, propriedades, eventos, indexadores, operadores, construtores de instância, construtores estáticos e tipos aninhados. A declaração dos tipos struct é descrita no §16.
8.3.5 Tipos simples
O C# fornece um conjunto de tipos predefinidos struct
chamados de tipos simples. Os tipos simples são identificados por meio de palavras-chave, mas essas palavras-chave são simplesmente aliases para tipos predefinidos struct
no System
namespace, conforme descrito na tabela abaixo.
Palavra-chave | Tipo com alias |
---|---|
sbyte |
System.SByte |
byte |
System.Byte |
short |
System.Int16 |
ushort |
System.UInt16 |
int |
System.Int32 |
uint |
System.UInt32 |
long |
System.Int64 |
ulong |
System.UInt64 |
char |
System.Char |
float |
System.Single |
double |
System.Double |
bool |
System.Boolean |
decimal |
System.Decimal |
Como um tipo simples aliases um tipo struct, cada tipo simples tem membros.
Exemplo:
int
tem os membros declarados eSystem.Int32
os membros herdados deSystem.Object
, e as seguintes declarações são permitidas:int i = int.MaxValue; // System.Int32.MaxValue constant string s = i.ToString(); // System.Int32.ToString() instance method string t = 123.ToString(); // System.Int32.ToString() instance method
Exemplo final
Nota: Os tipos simples diferem de outros tipos de struct na medida em que permitem certas operações adicionais:
- A maioria dos tipos simples permite que valores sejam criados escrevendo literais (§6.4.5), embora o C# não faça provisão para literais de tipos struct em geral. Exemplo:
123
é um literal do tipoint
e'a'
é um literal do tipochar
. Exemplo final- Quando os operandos de uma expressão são todas constantes de tipo simples, é possível para um compilador avaliar a expressão em tempo de compilação. Tal expressão é conhecida como constant_expression (§12.23). Expressões envolvendo operadores definidos por outros tipos struct não são consideradas expressões constantes
- Através
const
de declarações, é possível declarar constantes dos tipos simples (§15.4). Não é possível ter constantes de outros tipos de struct, mas um efeito semelhante é fornecido por campos estáticos somente leitura.- As conversões que envolvem tipos simples podem participar na avaliação de operadores de conversão definidos por outros tipos de struct, mas um operador de conversão definido pelo utilizador nunca pode participar na avaliação de outro operador de conversão definido pelo utilizador (§10.5.3).
Nota final.
8.3.6 Tipos integrais
C# suporta nove tipos integrais: sbyte
, byte
, short
, ushort
, int
, uint
, long
, ulong
e char
. Os tipos integrais têm os seguintes tamanhos e intervalos de valores:
- O
sbyte
tipo representa inteiros de 8 bits assinados com valores de-128
até127
, inclusive. - O
byte
tipo representa inteiros de 8 bits não assinados com valores de0
até255
, inclusive. - O
short
tipo representa inteiros de 16 bits assinados com valores de-32768
até32767
, inclusive. - O
ushort
tipo representa inteiros de 16 bits não assinados com valores de0
até65535
, inclusive. - O
int
tipo representa inteiros de 32 bits assinados com valores de-2147483648
até2147483647
, inclusive. - O
uint
tipo representa inteiros de 32 bits não assinados com valores de0
até4294967295
, inclusive. - O
long
tipo representa inteiros de 64 bits assinados com valores de-9223372036854775808
até9223372036854775807
, inclusive. - O
ulong
tipo representa inteiros de 64 bits não assinados com valores de0
até18446744073709551615
, inclusive. - O
char
tipo representa inteiros de 16 bits não assinados com valores de0
até65535
, inclusive. O conjunto de valores possíveis para ochar
tipo corresponde ao conjunto de caracteres Unicode.Nota: Embora
char
tenha a mesma representação queushort
, nem todas as operações permitidas num tipo são permitidas no outro. Nota final
Todos os tipos de integrais assinados são representados usando o formato de complemento de dois.
Os operadores integral_type unário e binário sempre operam com precisão de 32 bits assinada, precisão de 32 bits não assinada, precisão de 64 bits assinada ou precisão de 64 bits não assinada, conforme detalhado no §12.4.7.
O char
tipo é classificado como um tipo integral, mas difere dos outros tipos integrais de duas maneiras:
- Não há conversões implícitas predefinidas de outros tipos para o
char
tipo. Em particular, mesmo que osbyte
tipos eushort
tenham intervalos de valores que são totalmente representáveis usando ochar
tipo, conversões implícitas de sbyte, byte ouushort
parachar
não existem. - As constantes do
char
tipo devem ser escritas como character_literals ou como integer_literals em combinação com um cast to type char.
Exemplo:
(char)10
é o mesmo que'\x000A'
. Exemplo final
Os checked
operadores e as instruções são unchecked
usados para controlar a verificação de estouro para operações aritméticas de tipo integral e conversões (§12.8.20). Em um checked
contexto, um estouro produz um erro em tempo de compilação ou faz com que um System.OverflowException
seja lançado. Em um unchecked
contexto, os estouros são ignorados e todos os bits de ordem alta que não se encaixam no tipo de destino são descartados.
8.3.7 Tipos de vírgula flutuante
O C# suporta dois tipos de ponto flutuante: float
e double
. Os float
tipos e double
são representados usando os formatos IEC 60559 de precisão única de 32 bits e precisão dupla de 64 bits, que fornecem os seguintes conjuntos de valores:
- Zero positivo e zero negativo. Na maioria das situações, o zero positivo e o zero negativo comportam-se de forma idêntica ao valor simples zero, mas certas operações distinguem entre os dois (§12.10.3).
- Infinito positivo e infinito negativo. As infinidades são produzidas por operações como a divisão de um número diferente de zero por zero.
Exemplo:
1.0 / 0.0
produz infinito positivo e–1.0 / 0.0
produz infinito negativo. Exemplo final - O valor Not-a-Number , muitas vezes abreviado NaN. NaNs são produzidos por operações de ponto flutuante inválidas, como dividir zero por zero.
- O conjunto finito de valores diferentes de zero da forma s ×m × 2e, em que s é 1 ou −1, e m e e são determinados pelo tipo particular de vírgula flutuante: Para
float
, 0 << 2²⁴ e −149 ≤ e ≤ 104, e paradouble
, 0 << 2⁵³ e −1075 ≤ e ≤ 970. Os números de vírgula flutuante desnormalizados são considerados valores válidos diferentes de zero. O C# não exige nem proíbe que uma implementação em conformidade suporte números de vírgula flutuante desnormalizados.
O float
tipo pode representar valores que variam de aproximadamente 1,5 × 10⁻⁴⁵ a 3,4 × 10³⁸ com uma precisão de 7 dígitos.
O double
tipo pode representar valores que variam de aproximadamente 5,0 × 10⁻³²⁴ a 1,7 × 10³⁰⁸ com uma precisão de 15-16 dígitos.
Se qualquer operando de um operador binário for um tipo de vírgula flutuante, então promoções numéricas padrão são aplicadas, conforme detalhado no §12.4.7, e a operação é executada com float
ou double
precisão.
Os operadores de ponto flutuante, incluindo os operadores de atribuição, nunca produzem exceções. Em vez disso, em situações excecionais, as operações de ponto flutuante produzem zero, infinito ou NaN, conforme descrito abaixo:
- O resultado de uma operação de vírgula flutuante é arredondado para o valor representável mais próximo no formato de destino.
- Se a magnitude do resultado de uma operação de vírgula flutuante for muito pequena para o formato de destino, o resultado da operação torna-se zero positivo ou zero negativo.
- Se a magnitude do resultado de uma operação de ponto flutuante for muito grande para o formato de destino, o resultado da operação se tornará infinito positivo ou infinito negativo.
- Se uma operação de ponto flutuante for inválida, o resultado da operação torna-se NaN.
- Se um ou ambos os operandos de uma operação de ponto flutuante for NaN, o resultado da operação torna-se NaN.
As operações de vírgula flutuante podem ser efetuadas com maior precisão do que o tipo de resultado da operação. Para forçar um valor de um tipo de vírgula flutuante à precisão exata do seu tipo, pode ser utilizado um molde explícito (§12.9.7).
Exemplo: Algumas arquiteturas de hardware suportam um tipo de ponto flutuante "estendido" ou "duplo longo" com maior alcance e precisão do que o
double
tipo, e executam implicitamente todas as operações de ponto flutuante usando esse tipo de precisão mais alta. Somente com um custo excessivo de desempenho essas arquiteturas de hardware podem ser feitas para executar operações de ponto flutuante com menos precisão e, em vez de exigir uma implementação para perder desempenho e precisão, o C# permite que um tipo de precisão maior seja usado para todas as operações de ponto flutuante. Além de fornecer resultados mais precisos, isso raramente tem efeitos mensuráveis. No entanto, em expressões da 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 para odouble
intervalo, o fato de a expressão ser avaliada em um formato de intervalo mais alto pode fazer com que um resultado finito seja produzido em vez de um infinito. Exemplo final
8.3.8 O tipo decimal
O decimal
tipo é um tipo de dados de 128 bits adequado para cálculos financeiros e monetários. O decimal
tipo pode representar valores incluindo aqueles no intervalo de pelo menos -7,9 × 10⁻²⁸ a 7,9 × 10²⁸, com pelo menos 28 dígitos de precisão.
O conjunto finito de valores do tipo decimal
é da forma (–1)v × c × 10⁻e, onde o sinal v é 0 ou 1, o coeficiente c é dado por 0 ≤ c<Cmax, e a escala e é tal que Emin ≤ e ≤ Emax, onde Cmax é pelo menos 1 × 10²⁸, Emin ≤ 0, e Emax ≥ 28. O decimal
tipo não suporta necessariamente zeros assinados, infinidades ou NaN's.
A decimal
é representado como um número inteiro dimensionado por uma potência de dez. Para decimal
s com um valor absoluto inferior 1.0m
a , o valor é exato a, pelo menos, a 28.ª casa decimal. Para decimal
s com um valor absoluto maior ou igual a 1.0m
, o valor é exato para pelo menos 28 dígitos. Ao contrário dos float
tipos de dados e double
, números fracionários decimais como 0.1
podem ser representados exatamente na representação decimal.
float
double
Nas e representações, tais números geralmente têm expansões binárias não terminativas, tornando essas representações mais propensas a erros de arredondamento.
Se qualquer operando de um operador binário for do decimal
tipo, então promoções numéricas padrão são aplicadas, conforme detalhado no §12.4.7, e a operação é executada com double
precisão.
O resultado de uma operação sobre valores do tipo decimal
é o que resultaria do cálculo de um resultado exato (preservando a escala, conforme definido para cada operador) e, em seguida, arredondando para ajustar a representação. Os resultados são arredondados para o valor representável mais próximo e, quando um resultado está igualmente próximo de dois valores representáveis, para o valor que tem um número par na posição de dígitos menos significativa (isto é conhecido como "arredondamento do banqueiro"). Ou seja, os resultados são exatos até pelo menos a 28ª casa decimal. Observe que o arredondamento pode produzir um valor zero a partir de um valor diferente de zero.
Se uma decimal
operação aritmética produz um resultado cuja magnitude é muito grande para o decimal
formato, um System.OverflowException
é lançado.
O decimal
tipo tem maior precisão, mas pode ter um alcance menor do que os tipos de ponto flutuante. Assim, conversões dos tipos de ponto flutuante para decimal
podem produzir exceções de estouro, e conversões de para os tipos de ponto flutuante podem causar perda de precisão ou exceções de decimal
estouro. Por essas razões, não existem conversões implícitas entre os tipos de ponto flutuante e decimal
, e sem versões explícitas, um erro em tempo de compilação ocorre quando o ponto flutuante e decimal
os operandos são diretamente misturados na mesma expressão.
8.3.9 O tipo Bool
O bool
tipo representa grandezas lógicas booleanas. Os valores possíveis do tipo bool
são true
e false
. A representação de false
é descrita no §8.3.3. Embora a representação de true
não seja especificada, deve ser diferente da de false
.
Não existem conversões padrão entre bool
e outros tipos de valor. Em particular, o bool
tipo é distinto e separado dos tipos integrais, um bool
valor não pode ser usado no lugar de um valor integral e vice-versa.
Nota: Nas linguagens C e C++, um valor zero integral ou de ponto flutuante, ou um ponteiro nulo pode ser convertido para o valor
false
booleano, e um valor integral ou de ponto flutuante diferente de zero, ou um ponteiro não nulo pode ser convertido para o valortrue
booleano. Em C#, essas conversões são realizadas comparando explicitamente um valor integral ou de vírgula flutuante com zero, ou comparando explicitamente uma referência de objeto 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
long
, ou ulong
. O conjunto de valores do tipo de enumeração é o mesmo que o conjunto de valores do tipo subjacente. Os valores do tipo de enumeração não se restringem aos valores das constantes nomeadas. Os tipos de enumeração são definidos através de declarações de enumeração (§19.2).
8.3.11 Tipos de tuplas
Um tipo de tupla representa uma sequência ordenada de valores de comprimento fixo com nomes opcionais e tipos individuais. O número de elementos em um tipo de tupla é referido como sua aridade. Um tipo de tupla é escrito (T1 I1, ..., Tn In)
com n ≥ 2, onde os identificadores são nomes de I1...In
elementos de tupla opcionais.
Esta sintaxe é uma abreviatura de um tipo construído com os tipos T1...Tn
de System.ValueTuple<...>
, que deve ser um conjunto de tipos struct genéricos capazes de expressar diretamente tipos de tupla de qualquer aridade entre dois e sete, inclusive.
Não é necessário existir uma System.ValueTuple<...>
declaração que corresponda diretamente à aridade de qualquer tipo de tupla com um número correspondente de parâmetros de tipo. Em vez disso, tuplas com uma aridade maior que sete são representadas com um tipo System.ValueTuple<T1, ..., T7, TRest>
struct genérico que, além de elementos de tupla, tem um Rest
campo contendo um valor aninhado dos elementos restantes, usando outro System.ValueTuple<...>
tipo. Esta nidificação pode ser observável de várias formas, por exemplo, através da presença de um Rest
campo. Quando apenas um único campo adicional é necessário, o tipo System.ValueTuple<T1>
struct genérico é usado, este tipo não é considerado um tipo de tupla em si. Quando são necessários mais de sete campos adicionais, System.ValueTuple<T1, ..., T7, TRest>
é usado recursivamente.
Os nomes dos elementos dentro de um tipo de tupla devem ser distintos. Um nome de elemento de tupla do formulário ItemX
, onde X
é qualquer sequência de dígitos decimais não0
iniciados que poderiam representar a posição de um elemento de tupla, só é permitido na posição indicada por X
.
Os nomes de elementos opcionais não são representados nos ValueTuple<...>
tipos e não são armazenados na representação de tempo de execução de um valor de tupla. As conversões de identidade (§10.2.2) existem entre tuplas com sequências convertíveis de identidade de tipos de elementos.
O new
operador §12.8.17.2 não pode ser aplicado com a sintaxe new (T1, ..., Tn)
do tipo de tupla . Os valores de tupla podem ser criados a partir de expressões de tupla (§12.8.6), ou aplicando o new
operador diretamente a um tipo construído a partir de ValueTuple<...>
.
Os elementos de tupla são campos públicos com os nomes Item1
, Item2
, etc., e podem ser acessados através de um acesso de membro em um valor de tupla (§12.8.7. Além disso, se o tipo de tupla tiver um nome para um determinado elemento, esse nome pode ser usado para acessar o elemento em questão.
Nota: Mesmo quando tuplas grandes são representadas com valores aninhados
System.ValueTuple<...>
, cada elemento de tupla ainda pode ser acessado diretamente com oItem...
nome correspondente à sua posição. Nota final
Exemplo: Vejam-se os seguintes exemplos:
(int, string) pair1 = (1, "One"); (int, string word) pair2 = (2, "Two"); (int number, string word) pair3 = (3, "Three"); (int Item1, string Item2) pair4 = (4, "Four"); // Error: "Item" names do not match their position (int Item2, string Item123) pair5 = (5, "Five"); (int, string) pair6 = new ValueTuple<int, string>(6, "Six"); ValueTuple<int, string> pair7 = (7, "Seven"); Console.WriteLine($"{pair2.Item1}, {pair2.Item2}, {pair2.word}");
Os tipos de tupla para
pair1
,pair2
, epair3
são todos válidos, com nomes para não, alguns ou todos os elementos do tipo de tupla.O tipo de tupla para
pair4
é válido porque os nomesItem1
eItem2
correspondem às suas posições, enquanto o tipo de tupla parapair5
é proibido, 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 da formaValueTuple<...>
, 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 final
8.3.12 Tipos de valor anulável
Um tipo de valor anulável pode representar todos os valores de seu tipo subjacente mais um valor nulo adicional. Um tipo de valor anulável é escrito T?
, onde T
é o tipo subjacente. Esta sintaxe é abreviada para System.Nullable<T>
, e as duas formas podem ser usadas indistintamente.
Por outro lado, um tipo de valor não anulável é qualquer tipo de valor diferente de System.Nullable<T>
e sua abreviatura T?
(para qualquer T
), mais qualquer parâmetro de tipo que é restrito a ser um tipo de valor não anulável (ou seja, qualquer parâmetro de tipo com uma restrição de tipo de valor (§15.2.5)). O System.Nullable<T>
tipo especifica a restrição de tipo de valor para T
, o que significa que o tipo subjacente de um tipo de valor anulável pode ser qualquer tipo de valor não anulável. O tipo subjacente de um tipo de valor anulável não pode ser um tipo de valor anulável ou um tipo de referência. Por exemplo, int??
é um tipo inválido. Os tipos de referência anuláveis são abrangidos pelo ponto 8.9.
Uma instância de um tipo T?
de valor anulável tem duas propriedades públicas somente leitura:
- Uma
HasValue
propriedade do tipobool
- Uma
Value
propriedade do tipoT
Uma instância para a qual HasValue
é true
dito não ser nulo. Uma instância não nula contém um valor conhecido e Value
retorna esse valor.
Uma instância para a qual HasValue
é false
é dito ser nulo. Uma instância nula tem um valor indefinido. Tentar ler o Value
de uma instância nula faz com que um System.InvalidOperationException
seja lançado. O processo de acessar a propriedade Value de uma instância anulável é chamado de desempacotamento.
Além do construtor padrão, cada tipo T?
de valor nulo tem um construtor público com um único parâmetro do tipo T
. Dado um valor x
de tipo T
, uma invocação do construtor do formulário
new T?(x)
cria uma instância não nula da T?
qual a Value
propriedade é x
. O processo de criação de uma instância não nula de um tipo de valor anulável para um determinado valor é conhecido como encapsulamento.
As conversões implícitas estão disponíveis do null
literal para T?
(§10.2.7) e do T
para T?
(§10.2.6).
O tipo T?
de valor anulável não implementa interfaces (§18). Em particular, isso significa que ele não implementa nenhuma interface que o tipo T
subjacente faz.
8.3.13 Boxe e unboxing
O conceito de boxe e unboxing fornecem uma ponte entre value_types e reference_types, permitindo que qualquer valor de um value_type seja convertido para e do tipo object
. Boxing e unboxing permite uma visão unificada do sistema de tipos em que um valor de qualquer tipo pode ser tratado como um object
.
O boxe é descrito em mais detalhes no §10.2.9 e o unboxing é descrito no §10.3.7.
8.4 Tipos de construção
8.4.1 Generalidades
Uma declaração de tipo genérica, por si só, denota um tipo genérico não acoplado que é usado como um "modelo" para formar muitos tipos diferentes, por meio da aplicação de argumentos de tipo. Os argumentos de tipo são escritos entre colchetes angulares (<
e >
) imediatamente após o nome do tipo genérico. Um tipo que inclui pelo menos um argumento de tipo é chamado de tipo construído. Um tipo construído pode ser usado na maioria dos lugares no idioma em que um nome de tipo pode aparecer. Um tipo genérico não ligado só pode ser utilizado dentro de um typeof_expression (§12.8.18).
Os tipos construídos também podem ser usados em expressões como nomes simples (§12.8.4) ou ao acessar um membro (§12.8.7).
Quando um namespace_or_type_name é avaliado, apenas os tipos genéricos com o número correto de parâmetros de tipo são considerados. Assim, é possível usar o mesmo identificador para identificar diferentes tipos, desde que os tipos tenham números diferentes de parâmetros de tipo. Isso é útil ao misturar classes genéricas e não genéricas no mesmo programa.
Exemplo:
namespace Widgets { class Queue {...} class Queue<TElement> {...} } namespace MyApplication { using Widgets; class X { Queue q1; // Non-generic Widgets.Queue Queue<int> q2; // Generic Widgets.Queue } }
Exemplo final
As regras detalhadas para a pesquisa de nomes nas produções namespace_or_type_name são descritas no §7.8. A resolução de ambiguidades nestas produções é descrita no §6.2.5. Um type_name pode identificar um tipo construído mesmo que não especifique parâmetros de tipo diretamente. Isso pode ocorrer quando um tipo é aninhado dentro de uma declaração genérica class
e o tipo de instância da declaração que contém é implicitamente usado para pesquisa de nome (§15.3.9.7).
Exemplo:
class Outer<T> { public class Inner {...} public Inner i; // Type of i is Outer<T>.Inner }
Exemplo final
Um tipo construído sem enum não deve ser utilizado como unmanaged_type (§8.8).
8.4.2 Argumentos de tipo
Cada argumento em uma lista de argumentos de tipo é simplesmente um tipo.
type_argument_list
: '<' type_arguments '>'
;
type_arguments
: type_argument (',' type_argument)*
;
type_argument
: type
| type_parameter nullable_type_annotation?
;
Cada argumento de tipo deve satisfazer quaisquer restrições relativas ao parâmetro de tipo correspondente (§15.2.5). Um argumento de tipo de referência cuja anulabilidade não corresponde à anulabilidade do parâmetro type satisfaz a restrição; no entanto, pode ser emitido um aviso.
8.4.3 Tipos abertos e fechados
Todos os tipos podem ser classificados como tipos abertos ou fechados. Um tipo aberto é um tipo que envolve parâmetros de tipo. Mais especificamente:
- Um parâmetro type define um tipo aberto.
- Um tipo de matriz é um tipo aberto se e somente se seu tipo de elemento for um tipo aberto.
- Um tipo construído é um tipo aberto se e somente se um ou mais de seus argumentos de tipo for um tipo aberto. Um tipo aninhado construído é um tipo aberto se e somente se um ou mais de seus argumentos de tipo ou os argumentos de tipo de seus tipos que contêm for um tipo aberto.
Um tipo fechado é um tipo que não é um tipo aberto.
Em tempo de execução, todo o código dentro de uma declaração de tipo genérica é executado no contexto de um tipo construído fechado que foi criado aplicando argumentos de tipo à declaração genérica. Cada parâmetro de tipo dentro do tipo genérico é vinculado a um tipo de tempo de execução específico. O processamento em tempo de execução de todas as instruções e expressões sempre ocorre com tipos fechados, e os tipos abertos ocorrem apenas durante o processamento em tempo de compilação.
Dois tipos construídos fechados são conversíveis de identidade (§10.2.2) se forem construídos a partir do mesmo tipo genérico não vinculado, e existe uma conversão de identidade entre cada um dos seus argumentos de tipo correspondentes. Os argumentos de tipo correspondentes podem ser tipos construídos fechados ou tuplas que são conversíveis de identidade. Tipos construídos fechados que são conversíveis de identidade compartilham um único conjunto de variáveis estáticas. Caso contrário, cada tipo construído fechado tem seu próprio conjunto de variáveis estáticas. Como um tipo aberto não existe em tempo de execução, não há variáveis estáticas associadas a um tipo aberto.
8.4.4 Tipos ligados e não acoplados
O termo tipo não acoplado refere-se a um tipo não genérico ou a um tipo genérico não vinculado. O termo tipo vinculado refere-se a um tipo não genérico ou a um tipo construído.
Um tipo não acoplado refere-se à entidade declarada por uma declaração de tipo. Um tipo genérico não acoplado não é em si um tipo e não pode ser usado como o tipo de uma variável, argumento ou valor de retorno, ou como um tipo base. A única construção em que um tipo genérico não vinculado pode ser referenciado é a typeof
expressão (§12.8.18).
8.4.5 Satisfazer as restrições
Sempre que um tipo construído ou um método genérico é referenciado, os argumentos de tipo fornecidos são verificados em relação às restrições de parâmetros de tipo declaradas no tipo ou método genérico (§15.2.5). Para cada where
cláusula, o argumento A
type que corresponde ao parâmetro named type é verificado em relação a cada restrição da seguinte maneira:
- Se a restrição for um
class
tipo, um tipo de interface ou um parâmetro de tipo, 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 esta condicionação, deve ser o caso de o tipoA
ser convertível em tipoC
por uma das seguintes entidades: - Se a restrição for a restrição do tipo de referência (
class
), o tipoA
deve satisfazer uma das seguintes condições:-
A
é um tipo de interface, tipo de classe, tipo delegado, tipo de matriz ou o tipo dinâmico.
Nota:
System.ValueType
eSystem.Enum
são tipos de referência que satisfazem esta restrição. Nota final-
A
é um parâmetro de tipo que se sabe ser um tipo de referência (§8.2).
-
- Se a restrição for a restrição do tipo de valor (
struct
), o tipoA
deve satisfazer uma das seguintes condições:-
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 satisfazem esta restrição. Nota final-
A
é um parâmetro de tipo com a restrição de tipo de valor (§15.2.5).
-
- Se a restrição for a restrição
new()
do construtor , o tipoA
não deve serabstract
e deve ter um construtor público sem parâmetros. Isto é satisfeito se uma das seguintes situações for verdadeira:-
A
é um tipo de valor, uma vez que todos os tipos de valor têm um construtor padrão público (§8.3.3). -
A
é um parâmetro de tipo com a restrição do construtor (§15.2.5). -
A
é um parâmetro de tipo com a restrição de tipo de valor (§15.2.5). -
A
é umclass
que não é abstrato e contém um construtor público explicitamente declarado sem parâmetros. -
A
nãoabstract
é e tem um construtor padrão (§15.11.5).
-
Um erro em tempo de compilação ocorre se uma ou mais restrições de um parâmetro de tipo não forem satisfeitas pelos argumentos de tipo fornecidos.
Como os parâmetros de tipo não são herdados, as restrições também nunca são herdadas.
Exemplo: A seguir,
D
precisa especificar a restrição em seu parâmetroT
type para queT
satisfaça a restrição imposta pela baseclass
B<T>
. Em contraste,class
E
não precisa especificar uma restrição, porqueList<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 final
8.5 Parâmetros de tipo
Um parâmetro type é um identificador que designa um tipo de valor ou tipo de referência ao qual o parâmetro está vinculado em tempo de execução.
type_parameter
: identifier
;
Como um parâmetro type pode ser instanciado com muitos argumentos de tipo diferentes, os parâmetros type têm operações e restrições ligeiramente diferentes de outros tipos.
Nota: Estes incluem:
- Um parâmetro de tipo não pode ser usado diretamente para declarar uma classe base (§15.2.4.2) ou interface (§18.2.4).
- As regras para pesquisa de membros em parâmetros de tipo dependem das restrições, se houver, aplicadas ao parâmetro type. São descritas em pormenor no ponto 12.5.
- As conversões disponíveis para um parâmetro type dependem das restrições, se houver, aplicadas ao parâmetro type. Eles são detalhados em §10.2.12 e §10.3.8.
- O literal
null
não pode ser convertido para um tipo dado por um parâmetro type, exceto se o parâmetro type for conhecido por ser um tipo de referência (§10.2.12). No entanto, uma expressão padrão (§12.8.21) pode ser usada em vez disso. Além disso, um valor com um tipo dado por um parâmetro type pode ser comparado com null usando==
e!=
(§12.12.7), a menos que o parâmetro type tenha a restrição de tipo de valor.- Uma
new
expressão (§12.8.17.2) só pode ser usada com um parâmetro type se o parâmetro type for limitado por um constructor_constraint ou pela restrição de tipo de valor (§15.2.5).- Um parâmetro type não pode ser usado em qualquer lugar dentro de um atributo.
- Um parâmetro type não pode ser usado em um acesso de membro (§12.8.7) ou nome de tipo (§7.8) para identificar um membro estático ou um tipo aninhado.
- Um parâmetro de tipo não pode ser utilizado como unmanaged_type (§8.8).
Nota final
Como um tipo, os parâmetros de tipo são puramente uma construção em tempo de compilação. Em tempo de execução, cada parâmetro de tipo é vinculado a um tipo de tempo de execução que foi especificado fornecendo um argumento type para a declaração de tipo genérico. Assim, o tipo de variável declarada com um parâmetro de tipo será, em tempo de execução, um tipo construído fechado §8.4.3. A execução em tempo de execução de todas as instruções e expressões envolvendo parâmetros de tipo usa o tipo que foi fornecido como o argumento de tipo para esse parâmetro.
8.6 Tipos de árvore de expressão
As árvores de expressão permitem que expressões lambda sejam representadas como estruturas de dados em vez de código executável. Árvores de expressão são valores de tipos de árvore de expressão do formulário System.Linq.Expressions.Expression<TDelegate>
, onde TDelegate
é qualquer tipo delegado. Para o resto desta especificação, estes tipos serão referidos utilizando a abreviatura Expression<TDelegate>
.
Se existir uma conversão de uma expressão lambda para um tipo D
delegado, também existe uma conversão para o tipo Expression<TDelegate>
de árvore de expressão. Enquanto a conversão de uma expressão lambda em um tipo de delegado gera um delegado que faz referência ao código executável para a expressão lambda, a conversão para um tipo de árvore de expressão cria uma representação de árvore de expressão da expressão lambda. Mais detalhes sobre esta conversão são fornecidos em §10.7.3.
Exemplo: O programa a seguir representa uma expressão lambda como código executável e como uma árvore de expressão. Como existe uma conversão para
Func<int,int>
, também existe uma conversão 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 árvore de expressão exp faz referência a uma estrutura de dados que descreve a expressãox => x + 1
.Exemplo final
Expression<TDelegate>
Fornece um método de instância que produz um delegado Compile
do tipo TDelegate
:
Func<int,int> del2 = exp.Compile();
Invocar esse delegado faz com que o código representado pela árvore de expressões seja executado. Assim, dadas as definições acima, del
e del2
são equivalentes, e as duas afirmações seguintes terão o mesmo efeito:
int i1 = del(1);
int i2 = del2(1);
Depois de executar este código, i1
e i2
ambos terão o valor 2
.
A superfície da API fornecida pela é definida pela Expression<TDelegate>
implementação além do requisito para um Compile
método descrito acima.
Nota: Embora os detalhes da API fornecidos para árvores de expressão sejam definidos pela implementação, espera-se que uma implementação:
- Habilitar o código para inspecionar e responder à estrutura de uma árvore de expressão criada como resultado de uma conversão de uma expressão lambda
- Permitir que árvores de expressão sejam criadas programaticamente dentro do código do usuário
Nota final
8.7 O tipo dinâmico
O tipo dynamic
usa a ligação dinâmica, conforme descrito em detalhes no §12.3.2, em oposição à ligação estática que é usada por todos os outros tipos.
O tipo dynamic
é considerado idêntico, object
exceto nos seguintes aspetos:
- As operações sobre expressões de tipo
dynamic
podem ser vinculadas dinamicamente (§12.3.3). - A inferência de tipo (§12.6.3) será preferível
dynamic
seobject
ambos forem candidatos. -
dynamic
não pode ser utilizado como- o tipo numa object_creation_expression (§12.8.17.2)
- uma class_base (§15.2.4)
- um predefined_type num member_access (§12.8.7.1)
- o operando do
typeof
operador - um argumento de atributo
- uma restrição
- Um tipo de método de extensão
- qualquer parte de um argumento de tipo dentro struct_interfaces (§16.2.5) ou interface_type_list (§15.2.4.1).
Devido a esta equivalência, verifica-se o seguinte:
- Há uma conversão de identidade implícita
- entre
object
edynamic
- entre tipos construídos que são os mesmos ao substituir
dynamic
porobject
- entre tipos de tuplas que são os mesmos ao substituir
dynamic
porobject
- entre
- As conversões implícitas e explícitas de e
object
para também se aplicam a e dedynamic
. - As assinaturas que são as mesmas quando substituídas
dynamic
porobject
são consideradas a mesma assinatura. - O tipo
dynamic
é indistinguível do tipoobject
em tempo de execução. - Uma expressão do tipo
dynamic
é referida como uma expressão dinâmica.
8.8 Tipos não gerenciados
unmanaged_type
: value_type
| pointer_type // unsafe code support
;
Um unmanaged_type é qualquer tipo que não seja nem um reference_type nem um type_parameter que não esteja restrito a ser não gerenciado e não contenha campos de instância cujo tipo não seja um unmanaged_type. Por outras palavras, um unmanaged_type é um dos seguintes:
-
sbyte
,byte
,short
,ushort
, ,int
uint
,long
,ulong
,char
,float
double
decimal
ou .bool
- Qualquer enum_type.
- Uma estrutura do tipo struct_type definida pelo usuário, que contenha apenas campos de instância de unmanaged_types.
- Qualquer parâmetro de tipo que esteja restrito a não ser gerenciado.
- Qualquer pointer_type (§23.3).
8.9 Tipos de referência e anulabilidade
8.9.1 Generalidades
Um tipo de referência anulável é indicado anexando um nullable_type_annotation (?
) a um tipo de referência não anulável. Não há diferença semântica entre um tipo de referência não anulável e seu tipo anulável correspondente, ambos podem ser uma referência a um objeto ou null
. A presença ou ausência do nullable_type_annotation declara se uma expressão se destina a permitir valores nulos ou não. Um compilador pode fornecer diagnósticos quando uma expressão não é usada de acordo com essa intenção. O estado nulo de uma expressão é definido no §8.9.5. Existe uma conversão de identidade entre um tipo de referência anulável e o seu correspondente tipo de referência não anulável (§10.2.2).
Existem duas formas de anulabilidade para tipos de referência:
-
nullable: Um nullable-reference-type pode ser atribuído
null
. Seu estado nulo padrão é maybe-null. -
não anulável: uma referência não anulável não deve receber um
null
valor. Seu estado nulo padrão é não-nulo.
Nota: Os tipos
R
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 no §8.2.1. Os diagnósticos devem limitar-se a avisos. Nem a presença ou ausência de anotações anuláveis, nem o estado do contexto anulável podem alterar o tempo de compilação ou o comportamento de tempo de execução de um programa, exceto para alterações em quaisquer mensagens de diagnóstico geradas em tempo de compilação.
8.9.2 Tipos de referência não anuláveis
Um tipo de referência não anulável é um tipo de referência do formulário T
, onde T
é o nome do tipo. O estado nulo padrão de uma variável não anulável é não-nulo. Os avisos podem ser gerados quando uma expressão que é talvez-nula é usada onde um valor não-nulo é necessário.
8.9.3 Tipos de referência anuláveis
Um tipo de referência do formulário T?
(como string?
) é um tipo de referência anulável. O estado nulo padrão de uma variável anulável é talvez nulo. A anotação ?
indica a intenção de que variáveis desse tipo sejam anuláveis. Um compilador pode reconhecer essas intenções de emitir avisos. Quando o contexto de anotação anulável está desativado, o uso dessa anotação pode gerar um aviso.
8.9.4 Contexto anulável
8.9.4.1 Generalidades
Cada linha de código-fonte tem um contexto anulável. As anotações e avisos sinalizam para o controle de contexto anulável anotações anuláveis (§8.9.4.3) e avisos anuláveis (§8.9.4.4), respectivamente. Cada sinalizador pode ser ativado ou desativado. Um compilador pode usar a análise de fluxo estático para determinar o estado nulo de qualquer variável de referência. O estado nulo de uma variável de referência (§8.9.5) não é nulo, talvez nulo ou talvez padrão.
O contexto anulável pode ser especificado no código-fonte através de diretivas anuláveis (§6.5.9) e/ou através de algum mecanismo específico de implementação externo ao código-fonte. Se ambas as abordagens forem usadas, as diretivas anuláveis substituem as configurações feitas por meio de um mecanismo externo.
O estado padrão do contexto anulável é a implementação definida.
Ao longo desta especificação, todo o código C# que não contém diretivas anuláveis, ou sobre o qual nenhuma instrução é feita em relação ao estado de contexto anulável atual, deve ser considerado como tendo sido compilado usando um contexto anulável onde as anotações e avisos estão habilitados.
Nota: Um contexto anulável em que ambos os sinalizadores estão desativados corresponde ao comportamento padrão anterior para tipos de referência. Nota final
8.9.4.2 Desativação anulável
Quando os sinalizadores de aviso e anotações são desabilitados, o contexto anulável é desabilitado.
Quando o contexto anulável está desativado:
- Não deve ser gerado qualquer aviso quando uma variável de um tipo de referência não anotado é inicializada com, ou é-lhe atribuído um valor de,
null
. - Não deve ser gerado qualquer aviso quando uma variável de um tipo de referência que possivelmente tenha o valor nulo.
- Para qualquer tipo
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?
emC?
gera uma mensagem e o tipoC?
é o mesmo queC
. - Para qualquer restrição
where T : U?
de parâmetro de tipo, a anotação?
emU?
gera 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.Nota: Esta mensagem é caracterizada como "informativa" em vez de "aviso", para não confundi-la com o estado da configuração de aviso anulável, que não está relacionada. Nota final
- O operador
!
de perdão nulo (§12.8.9) não tem efeito.
Exemplo:
#nullable disable annotations string? s1 = null; // Informational message; ? is ignored string s2 = null; // OK; null initialization of a reference s2 = null; // OK; null assignment to a reference char c1 = s2[1]; // OK; no warning on dereference of a possible null; // throws NullReferenceException c1 = s2![1]; // OK; ! is ignored
Exemplo final
8.9.4.3 Anotações anuláveis
Quando o sinalizador de aviso está desativado e o sinalizador de anotações está habilitado, o contexto anulável é anotações.
Quando o contexto anulável é anotações:
- Para qualquer tipo
T
de referência , a anotação?
emT?
indica queT?
um tipo anulável, enquanto o não anotadoT
não é anulável. - Nenhum aviso de diagnóstico relacionado à anulabilidade é gerado.
- O operador
!
de perdão nula (§12.8.9) pode alterar o estado nulo analisado de seu operando e quais avisos de diagnóstico de tempo de compilação são produzidos.
Exemplo:
#nullable disable warnings #nullable enable annotations string? s1 = null; // OK; ? makes s2 nullable string s2 = null; // OK; warnings are disabled s2 = null; // OK; warnings are disabled char c1 = s2[1]; // OK; warnings are disabled; throws NullReferenceException c1 = s2![1]; // No warnings
Exemplo final
8.9.4.4 Advertências anuláveis
Quando o sinalizador de aviso está habilitado e o sinalizador de anotações está desativado, o contexto anulável é avisos.
Quando o contexto anulável é avisos, um compilador pode gerar diagnósticos nos seguintes casos:
- Uma variável de referência que foi determinada como sendo talvez nula, é desreferenciada.
- Uma variável de referência de um tipo não anulável é atribuída a uma expressão que talvez seja nula.
- O
?
é usado para observar um tipo de referência anulável. - O operador
!
de perdão nula (§12.8.9) é usado para definir o estado nulo de seu operando como não nulo.
Exemplo:
#nullable disable annotations #nullable enable warnings string? s1 = null; // OK; ? makes s2 nullable string s2 = null; // OK; null-state of s2 is "maybe null" s2 = null; // OK; null-state of s2 is "maybe null" char c1 = s2[1]; // Warning; dereference of a possible null; // throws NullReferenceException c1 = s2![1]; // The warning is suppressed
Exemplo final
8.9.4.5 Ativação anulável
Quando o sinalizador de aviso e o sinalizador de anotações estão habilitados, o contexto anulável é habilitado.
Quando o contexto anulável está habilitado:
- Para qualquer tipo
T
de referência , a anotação?
emT?
tornaT?
um tipo anulável, enquanto o não anotadoT
não é anulável. - Um compilador pode usar a análise de fluxo estático para determinar o estado nulo de qualquer variável de referência. Quando os avisos anuláveis são habilitados, o estado nulo de uma variável de referência (§8.9.5) não é nulo, talvez nulo ou talvez padrão e
- O operador
!
de perdão nula (§12.8.9) define o estado nulo de seu operando como não nulo. - Um compilador pode emitir um aviso se a anulabilidade de um parâmetro type não corresponder à anulabilidade de seu argumento type correspondente.
8.9.5 Nulidades e estados nulos
Um compilador não é necessário para executar qualquer análise estática nem é necessário para gerar quaisquer avisos de diagnóstico relacionados à anulabilidade.
O restante desta subcláusula é condicionalmente normativo.
Um compilador que gera avisos de diagnóstico está em conformidade com essas regras.
Cada expressão tem um dos três estadosnulos:
- talvez nulo: O valor da expressão pode ser avaliado como nulo.
- talvez padrão: O valor da expressão pode ser avaliado como o valor padrão para esse tipo.
- not null: O valor da expressão não é null.
O estado nulo padrão de uma expressão é determinado por seu tipo e pelo estado do sinalizador de anotações quando é declarado:
- O estado nulo padrão de um tipo de referência anulável é:
- Talvez nulo quando sua declaração estiver no texto onde o sinalizador de anotações está habilitado.
- Não é nulo quando sua declaração está no texto onde o sinalizador de anotações está desativado.
- O estado nulo padrão de um tipo de referência não anulável não é nulo.
Nota: O estado talvez padrão é usado com parâmetros de tipo não restritos quando o tipo é um tipo não anulável, como
string
e a expressã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 em texto onde o sinalizador de anotação está habilitado.
Exemplo: Considere o seguinte método onde um parâmetro é anulável e esse valor é atribuído a um tipo não anulável:
#nullable enable public class C { public void M(string? p) { // Warning: Assignment of maybe null value to non-nullable variable string s = p; } }
Um compilador pode emitir um aviso onde o parâmetro que pode ser nulo é atribuído a uma variável que não deve ser nula. Se o parâmetro for verificado nulo antes da atribuição, um compilador pode usá-lo em sua análise de estado nulo e não emitir um aviso:
#nullable enable public class C { public void M(string? p) { if (p != null) { string s = p; // No warning // Use s } } }
Exemplo final
Um compilador pode atualizar o estado nulo de uma variável como parte de sua análise.
Exemplo: Um compilador pode optar por atualizar o estado com base em quaisquer instruções no seu programa:
#nullable enable public void M(string? p) { int length = p.Length; // Warning: p is maybe null string s = p; // No warning. p is not null if (s != null) { int l2 = s.Length; // No warning. s is not null } int l3 = s.Length; // Warning. s is maybe null }
No exemplo anterior, um compilador pode decidir que, após a instrução
int length = p.Length;
, o estado nulo dep
não é nulo. Se fosse nula, essa afirmaçã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 como escrito pode produzir um aviso, cujo objetivo é avisar que uma exceção pode ser lançada implicitamente. Exemplo final
Mais adiante no método, o código verifica se s
não é uma referência nula. O estado nulo de s
pode mudar para talvez nulo depois que o bloco de verificação nula for fechado. Um compilador pode inferir que s
é talvez nulo porque o código foi escrito para assumir que ele pode ter sido nulo. Geralmente, quando o código contém uma verificação nula, um compilador pode inferir que o valor pode ter sido nulo:
Exemplo: Cada uma das expressões a seguir inclui alguma forma de verificação nula. O estado nulo de
o
pode mudar de não nulo para talvez nulo após cada uma destas instruções:#nullable enable public void M(string s) { int length = s.Length; // No warning. s is not null _ = s == null; // Null check by testing equality. The null state of s is maybe null length = s.Length; // Warning, and changes the null state of s to not null _ = s?.Length; // The ?. is a null check and changes the null state of s to maybe null if (s.Length > 4) // Warning. Changes null state of s to not null { _ = s?[4]; // ?[] is a null check and changes the null state of s to maybe null _ = s.Length; // Warning. s is maybe null } }
Declarações de eventos semelhantes a campos e de propriedades automáticas fazem uso de um campo de suporte gerado pelo compilador. A análise de estado nulo pode inferir que a atribuição ao evento ou propriedade é uma atribuição a um campo de suporte gerado pelo compilador.
Exemplo: Um compilador pode determinar que escrever uma propriedade automática ou um evento semelhante a um campo grava o campo de suporte gerado pelo compilador correspondente. O estado nulo da propriedade corresponde ao do campo subjacente.
class Test { public string P { get; set; } public Test() {} // Warning. "P" not set to a non-null value. static void Main() { var t = new Test(); int len = t.P.Length; // No warning. Null state is not null. } }
No exemplo anterior, o construtor não define
P
para um valor não nulo, e um compilador pode emitir um aviso. Não há aviso quando a propriedadeP
é acessada, porque o tipo da propriedade é um tipo de referência não anulável. Exemplo final
Um compilador pode tratar uma propriedade (§15.7) como uma variável com estado, ou como acessadores independentes get e set (§15.7.3).
Exemplo: Um compilador pode escolher se a gravação em uma propriedade altera o estado nulo da leitura da propriedade ou se a leitura de uma propriedade altera o estado nulo dessa propriedade.
class Test { private string? _field; public string? DisappearingProperty { get { string tmp = _field; _field = null; return tmp; } set { _field = value; } } static void Main() { var t = new Test(); if (t.DisappearingProperty != null) { int len = t.DisappearingProperty.Length; // No warning. A compiler can assume property is stateful } } }
No exemplo anterior, o campo de suporte para o
DisappearingProperty
é definido como null quando é lido. No entanto, um compilador pode assumir que a leitura de uma propriedade não altera o estado nulo dessa expressão. Exemplo final
Um compilador pode usar qualquer expressão que desreferencia uma variável, propriedade ou evento para definir o estado nulo como não nulo. Se fosse nula, a expressão de desreferência teria lançado um NullReferenceException
:
Exemplo:
public class C { private C? child; public void M() { _ = child.child.child; // Warning. Dereference possible null value var greatGrandChild = child.child.child; // No warning. } }
Exemplo final
Fim do texto condicionalmente normativo
ECMA C# draft specification