Compartilhar via


12 expressões

12.1 Geral

Uma expressão é uma sequência de operadores e operandos. Essa cláusula define a sintaxe, a ordem de avaliação dos operandos e operadores, além do significado das expressões.

12.2 Classificações de expressão

12.2.1 Geral

O resultado de uma expressão é classificado como um dos seguintes:

  • Um valor . Cada valor tem um tipo associado.
  • Uma variável. A menos que especificado de outra forma, uma variável é tipada explicitamente e tem um tipo associado, ou seja, o tipo declarado da variável. Uma variável tipada implicitamente não tem nenhum tipo associado.
  • Um literal nulo. Uma expressão com essa classificação pode ser convertida implicitamente em um tipo de referência ou tipo de valor anulável.
  • Uma função anônima. Uma expressão com essa classificação pode ser convertida implicitamente em um tipo de árvore de expressão ou tipo de delegado compatível.
  • Uma tupla. Cada tupla tem um número fixo de elementos; cada um com uma expressão e um nome opcional para o elemento da tupla.
  • Um acesso de propriedade. Cada acesso à propriedade tem um tipo associado, ou seja, o tipo da propriedade. Além disso, um acesso de propriedade pode ter uma expressão de instância associada. Quando um acessador de acesso de propriedade de instância é invocado, o resultado da avaliação da expressão de instância se torna a instância representada por this (§12.8.14).
  • Um acesso de indexador. Cada acesso de indexador tem um tipo associado, ou seja, o tipo de elemento do indexador. Além disso, um acesso de indexador tem uma expressão de instância associada e uma lista de argumentos associada. Quando um acessador de acesso de indexador é invocado, o resultado da avaliação da expressão de instância se torna a instância representada por this (§12.8.14) e o resultado da avaliação da lista de argumentos se torna a lista de parâmetros da invocação.
  • Nada. Isso ocorre quando a expressão é uma invocação de um método com um tipo de retorno void. Uma expressão classificada como nada só é válida no contexto de statement_expression (§13.7) ou como o corpo de lambda_expression (§12.19).

Para expressões que ocorrem como subexpressões de expressões maiores, com as restrições anotadas, o resultado também pode ser classificado como um dos seguintes:

  • Um namespace. Uma expressão com essa classificação só pode aparecer como o lado esquerdo de uma member_access (§12.8.7). Em qualquer outro contexto, uma expressão classificada como um namespace causa um erro de tempo de compilação.
  • Um tipo. Uma expressão com essa classificação só pode aparecer como o lado esquerdo de uma member_access (§12.8.7). Em qualquer outro contexto, uma expressão classificada como um tipo causa um erro de tempo de compilação.
  • Um grupo de métodos, que é um conjunto de métodos sobrecarregados resultante de uma pesquisa de membro (§12,5). Um grupo de métodos pode ter uma expressão de instância associada e uma lista de argumentos de tipo associado. Quando um método de instância é invocado, o resultado da avaliação da expressão de instância torna-se a instância representada por this (§12.8.14). Um grupo de métodos é permitido em um invocation_expression (§12.8.10) ou um delegate_creation_expression (§12.8.17.5) e pode ser convertido implicitamente em um tipo de delegado compatível (§10.8). Em qualquer outro contexto, uma expressão classificada como um grupo de métodos causa um erro de tempo de compilação.
  • Um acesso de evento. Cada acesso de evento tem um tipo associado, ou seja, o tipo do evento. Além disso, o acesso a um evento pode ter uma expressão de instância associada. Um acesso de evento pode aparecer como o operando esquerdo dos operadores += e -= (§12.21.5). Em qualquer outro contexto, uma expressão classificada como um acesso de evento causa um erro de tempo de compilação. Quando um acessador de acesso de evento de instância é invocado, o resultado da avaliação da expressão de instância se torna a instância representada por this (§12.8.14).
  • Uma expressão de lançamento, que pode ser utilizada em diversos contextos para lançar uma exceção dentro de uma expressão. Uma expressão throw pode ser convertida por uma conversão implícita para qualquer tipo.

Um acesso de propriedade ou acesso de indexador é sempre reclassificado como um valor pela execução de uma invocação do acessador get ou do acessador set. O acessador específico é determinado pelo contexto do acesso de propriedade ou indexador: se o acesso for o destino de uma atribuição, o acessador set será invocado para atribuir um novo valor (§12.21.2). Caso contrário, o acessador get é invocado para obter o valor atual (§12.2.2).

Um acessador de instância é um acesso de propriedade em uma instância, um acesso de evento em uma instância ou um acesso de indexador.

12.2.2 Valores de expressões

Em última análise, a maioria dos constructos que envolvem uma expressão exige a expressão para indicar um valor . Nesses casos, se a expressão real denotar um namespace, um tipo, um grupo de métodos ou nada, ocorrerá um erro de tempo de compilação. No entanto, se a expressão denotar um acesso de propriedade, um acesso de indexador ou uma variável, o valor da propriedade, do indexador ou da variável será substituído implicitamente:

  • O valor de uma variável é simplesmente o valor armazenado atualmente no local de armazenamento identificado pela variável. Uma variável deve ser considerada definitivamente atribuída (§9.4) antes que seu valor possa ser obtido ou ocorrerá um erro de tempo de compilação.
  • O valor de uma expressão de acesso de propriedade é obtido invocando-se o acessador get da propriedade. Se a propriedade não tiver acessador get, ocorrerá um erro de tempo de compilação. Caso contrário, uma invocação de membro de função (§12.6.6) será executada e o resultado da invocação se tornará o valor da expressão de acesso de propriedade.
  • O valor de uma expressão de acesso do indexador é obtido invocando-se o acessador get do indexador. Se o indexador não tiver o acessor 'get', ocorrerá um erro em tempo de compilação. Caso contrário, uma invocação de membro de função (§12.6.6) é executada com a lista de argumentos associados à expressão de acesso do indexador e o resultado da invocação torna-se o valor da expressão de acesso do indexador.
  • O valor de uma expressão de tupla é obtido aplicando-se uma conversão de tupla implícita (§10.2.13) ao tipo da expressão de tupla. É um erro obter o valor de uma expressão de tupla que não tem um tipo.

12.3 Associação estática e dinâmica

12.3.1 Geral

Associação é o processo de determinar a que uma operação se refere, com base no tipo ou valor de expressões (argumentos, operandos, receptores). Por exemplo, a associação de uma chamada de método é determinada com base no tipo do receptor e dos argumentos. A associação de um operador é determinada com base no tipo dos respectivos operandos.

Em C#, a associação de uma operação geralmente é determinada em tempo de compilação, com base no tipo de tempo de compilação das respectivas subexpressões. Da mesma forma, se uma expressão contiver um erro, o erro será detectado e relatado em tempo de compilação. Essa abordagem é conhecida como associação estática.

No entanto, se uma expressão for uma expressão dinâmica (ou seja, tiver o tipo dynamic), isso indicará que qualquer associação da qual ela que participe deve ser baseada em seu tipo de run-time em vez do tipo que tiver em tempo de compilação. A vinculação de tal operação é, portanto, adiada até o momento em que a operação é executada durante a execução do programa. Isso é conhecido como associação dinâmica.

Quando uma operação é associada dinamicamente, pouca ou nenhuma verificação é executada no momento da compilação. Em vez disso, se a associação run-time falhar, os erros serão relatados como exceções run-time.

As seguintes operações em C# estão sujeitas à associação:

  • Acesso de membro: e.M
  • Invocação de método: e.M(e₁,...,eᵥ)
  • Invocação de delegado: e(e₁,...,eᵥ)
  • Acesso de elemento: e[e₁,...,eᵥ]
  • Criação de objeto: novo C(e₁,...,eᵥ)
  • Operadores unários sobrecarregados: +, -, ! (somente negação lógica), ~, ++, --, true, false
  • Operadores binários sobrecarregados: +, -, *, /, %, &, &&, |, ||, ??, ^, <<, >>, ==, !=, >, <, >=, <=
  • Operadores de atribuição: =, = ref, +=, -=, *=, /=, %=, &=, |=, ^=, <<=, >>=, ??=
  • Conversões implícitas e explícitas

Quando não há expressões dinâmicas envolvidas, o C# usa como padrão a associação estática, o que significa que os tipos de subexpressões de tempo de compilação são usados no processo de seleção. No entanto, quando uma das subexpressões nas operações listadas acima é uma expressão dinâmica, a operação é vinculada dinamicamente.

Ocorrerá um erro em tempo de compilação se uma invocação de método for associada dinamicamente e qualquer um dos parâmetros, incluindo o receptor, for um parâmetro de entrada.

12.3.2 Tempo de associação

A associação estática ocorre em tempo de compilação, enquanto a associação dinâmica ocorre em run-time. Nas subcláusulas a seguir, o termo binding-time refere-se a tempo de compilação ou run-time, dependendo de quando a associação ocorrer.

Exemplo: segue abaixo as noções de associação estática e dinâmica e de tempo de associação:

object o = 5;
dynamic d = 5;
Console.WriteLine(5); // static binding to Console.WriteLine(int)
Console.WriteLine(o); // static binding to Console.WriteLine(object)
Console.WriteLine(d); // dynamic binding to Console.WriteLine(int)

As duas primeiras chamadas são associadas estaticamente: a sobrecarga de Console.WriteLine é escolhida com base no tipo de tempo de compilação de seu argumento. Portanto, o tempo de associação é o tempo de compilação.

A terceira chamada é dinamicamente associada: a sobrecarga de Console.WriteLine é escolhida com base no tipo de run-time de seu argumento. Isso acontece porque o argumento é uma expressão dinâmica – seu tipo de tempo de compilação é dinâmico. Portanto, o tempo de associação para a terceira chamada em tempo de associação é run-time.

fim do exemplo

12.3.3 Associação dinâmica

Essa subcláusula é informativa.

A associação dinâmica permite que programas em C# interajam com objetos dinâmicos, ou seja, objetos que não seguem as regras normais do sistema de tipos da C#. Objetos dinâmicos podem ser objetos de outras linguagens de programação com sistemas de tipos diferentes ou podem ser objetos que são configurados programaticamente para implementar sua própria semântica de associação para operações diferentes.

O mecanismo pelo qual um objeto dinâmico implementa sua própria semântica é definido pela implementação. Uma dada interface – novamente definida pela implementação – é implementada por objetos dinâmicos para sinalizar para o run-time do C# que eles têm semântica especial. Assim, sempre que as operações em um objeto dinâmico são associadas dinamicamente, sua própria semântica de associação, em vez daquelas da C# conforme especificado aqui, assumem o controle.

Embora a finalidade da associação dinâmica seja permitir a interoperação com objetos dinâmicos, a C# permite a associação dinâmica em todos os objetos, sejam eles dinâmicos ou não. Isso permite uma integração mais suave de objetos dinâmicos, pois os resultados das operações neles nem sempre são objetos dinâmicos, mas ainda são de um tipo desconhecido para o programador em tempo de compilação. Além disso, a associação dinâmica pode ajudar a eliminar o código baseado em reflexão propenso a erros, mesmo quando nenhum objeto envolvido é objeto dinâmico.

12.3.4 Tipos de subexpressões

Quando uma operação é associada estaticamente, o tipo de uma subexpressão (por exemplo, um receptor, um argumento, um índice ou um operando) é sempre considerado o tipo de tempo de compilação dessa expressão.

Quando uma operação é associada dinamicamente, o tipo de uma subexpressão é determinado de maneiras diferentes, dependendo do tipo de tempo de compilação da subexpressão:

  • Uma subexpressão de tipo dinâmico de tempo de compilação é considerada com o tipo do valor real que a expressão assume em run-time.
  • Uma subexpressão cujo tipo de tempo de compilação é um parâmetro de tipo é considerada como tendo o tipo ao qual o parâmetro de tipo está associado em run-time
  • Caso contrário, a subexpressão é considerada como tendo o tipo de tempo de compilação.

12.4 Operadores

12.4.1 Geral

Expressões são construídas a partir de operandos e operadores. Os operadores de uma expressão indicam quais operações devem ser aplicadas aos operandos.

Exemple: exemplos de operadores incluem +, -, *, / e new. Exemplos de operandos incluem literais, campos, variáveis locais e expressões. fim do exemplo

Há três tipos de operadores:

  • Operadores unários. Os operadores unários usam um operando e a notação pós-fixada (como –x) ou a notação de sufixo (como x++).
  • Operadores binários. Os operadores binários usam dois operandos e todos usam notação de infixo (como x + y).
  • Operador ternário. Existe apenas um operador ternário que é ?:; ele usa três operandos e a notação de infixo (c ? x : y).

A ordem de avaliação de operadores em uma expressão é determinada pela precedência e pela associatividade dos operadores (§12.4.2).

Operandos em uma expressão são avaliados da esquerda para a direita.

exemplo: em F(i) + G(i++) * H(i), o método F é chamado usando-se o valor antigo de i, então o método G é chamado com o valor antigo de i e, por fim, o método H é chamado com o novo valor de i. Isso é separado e não está relacionado à precedência do operador. fim do exemplo

Alguns operadores pode ser sobrecarregados. A sobrecarga de operador (§12.4.3) permite que implementações de operador definidas pelo usuário sejam especificadas para operações em que um ou os dois operandos são de um tipo struct ou de classe definida pelo usuário.

12.4.2 Precedência e associatividade dos operadores

Quando uma expressão contém vários operadores, a precedência dos operadores controla a ordem na qual os operadores individuais são avaliados.

Observação: por exemplo, a expressão x + y * z é avaliada como x + (y * z) porque o operador * tem precedência maior do que o operador binário +. fim da observação

A precedência de um operador é estabelecida pela definição da sua produção de gramática associada.

Observação: por exemplo, uma additive_expression consiste em uma sequência de multiplicative_expressions separadas por operadores + ou -, dando aos operadores + e - menor precedência do que os operadores *, / e %. fim da observação

Observação: a seguinte tabela resume todos os operadores em ordem de precedência da mais alta para a mais baixa:

Subcláusula Categoria Operadores
§12.8 Primária x.y x?.y f(x) a[x] a?[x] x++ x-- x! new typeof default checked unchecked delegate stackalloc
§12.9 Unário + - !x ~ ++x --x (T)x await x
§12.10 Multiplicativo * / %
§12.10 Aditiva + -
§12.11 Deslocamento << >>
§12.12 Teste de tipo e relacional < > <= >= is as
§12.12 Igualitário == !=
§12.13 AND lógico &
§12.13 XOR lógico ^
§12.13 OR lógico \|
§12.14 AND condicional &&
§12.14 OR condicional \|\|
§12.15 e §12.16 Coalescência nula e expressão throw ?? throw x
§12.18 Condicional ?:
§12.21 e §12.19 Atribuição e expressão lambda = = ref *= /= %= += -= <<= >>= &= ^= \|= => ??=

fim da observação

Quando ocorre um operando entre dois operadores com a mesma precedência, a associatividade dos operadores controla a ordem na qual as operações são executadas:

  • Exceto para os operadores de atribuição e os operadores de coalescência de nulo, todos os operadores binários são associativos à esquerda, o que significa que as operações são executadas da esquerda para a direita.

    Exemplo: x + y + z recebe uma avaliação como (x + y) + z. fim do exemplo

  • Os operadores de atribuição, o operador de avaliação de nulo e o operador condicional (?:) são associativos à direita, o que significa que as operações são executadas da direita para a esquerda.

    Exemplo: x = y = z recebe uma avaliação como x = (y = z). fim do exemplo

Precedência e associatividade podem ser controladas usando parênteses.

Exemplo: x + y * z primeiro multiplica y por z e, em seguida, adiciona o resultado a x, mas (x + y) * z primeiro adiciona x e y e, em seguida, multiplica o resultado por z. fim do exemplo

12.4.3 Sobrecarga de operador

Os operadores unários e binários predefinidos têm implementações predefinidas. Além disso, as implementações definidas pelo usuário podem ser introduzidas incluindo-se as declarações de operador (§15.10) em classes e structs. As implementações de operador definidas pelo usuário sempre têm precedência sobre as implementações de operador predefinidas: somente quando não houver implementações de operador definidas pelo usuário aplicáveis, as implementações predefinidas do operador serão consideradas, conforme descrito em §12.4.4 e §12.4.5.

Os operadores unários sobrecarregáveis são:

+ - ! (somente negação lógica) ~ ++ -- true false

Observação: embora true e false não sejam usados explicitamente em expressões (e, portanto, não sejam incluídos na tabela de precedência em §12.4.2), eles são considerados operadores porque são invocados em vários contextos de expressão: expressões boolianas (§12.24) e expressões que envolvem o condicional (§12.18) e operadores lógicos condicionais (§12.14). fim da observação

Observação: o operador null-forgiving (!, §12.8.9 pós-fixado) não é um operador sobrecarregável. fim da observação

Os operadores binários sobrecarregados são:

+  -  *  /  %  &  |  ^  <<  >>  ==  !=  >  <  <=  >=

Somente os operadores listados acima podem ser sobrecarregados. Em particular, não é possível sobrecarregar o acesso de membro, a invocação do método ou os operadores =, &&, ||, ??, ?:, =>, checked, unchecked, new, typeof, default, as e is.

Quando um operador binário está sobrecarregado, o operador de atribuição composta correspondente, se houver, também é implicitamente sobrecarregado.

Exemplo: uma sobrecarga do operador * também é uma sobrecarga do operador *=. Isso é descrito mais adiante em §12.21. fim do exemplo

O respectivo operador de atribuição (=) não pode ser sobrecarregado. Uma atribuição sempre realiza um armazenamento simples de um valor em uma variável (§12.21.2).

Operações de conversão, como (T)x, são sobrecarregadas fornecendo conversões definidas pelo usuário (§10.5).

Observação: as conversões definidas pelo usuário não afetam o comportamento dos operadores is ou as. fim da observação

O acesso de elemento, como a[x], não é considerado um operador que pode ser sobrecarregado. Em vez disso, a indexação definida pelo usuário tem suporte por meio de indexadores (§15.9).

Em expressões, os operadores são referenciados usando-se a notação de operador e, em declarações, os operadores são referenciados usando-se a notação funcional. A tabela a seguir mostra a relação entre notações de operadores e notações funcionais para operadores unários e binários. Na primeira entrada, «op» indica qualquer operador de prefixo unário sobrecarregável. Na segunda entrada, «op» indica os operadores pós-fixados unário ++ e --. Na terceira entrada, «op» indica qualquer operador binário sobrecarregável.

Observação: por um exemplo de sobrecarregamento de ++ e -- operadores, consulte §15.10.2. fim da observação

Notação do operador Notação funcional
«op» x operator «op»(x)
x «op» operator «op»(x)
x «op» y operator «op»(x, y)

As declarações de operador definidas pelo usuário sempre exigem que pelo menos um dos parâmetros que contém a declaração do operador seja do tipo de classe ou struct.

Observação: portanto, não é possível que um operador definido pelo usuário tenha a mesma assinatura que um operador predefinido. fim da observação

As declarações de operador definidas pelo usuário não podem modificar a sintaxe, a precedência nem a associatividade de um operador.

Exemplo: o operador / é sempre um operador binário, sempre tem o nível de precedência especificado em §12.4.2 e é sempre associativo à esquerda. fim do exemplo

Observação: embora seja possível que um operador definido pelo usuário execute qualquer atividade de computação, as implementações que produzem resultados diferentes daqueles que são intuitivamente esperados são fortemente desencorajadas. Por exemplo, uma implementação do operador == deve comparar os dois operandos quanto à igualdade e retornar um resultado bool apropriado. fim da observação

As descrições de operadores individuais de §12.9 até §12.21 especificam as implementações predefinidas dos operadores e as regras adicionais que se aplicam a cada operador. As descrições usam os termos resolução de sobrecarga de operador unário, resolução de sobrecarga do operador binário, promoção numérica e definições de operador elevado, que são encontradas nas subcláusulas a seguir.

12.4.4 Resolução de sobrecarga do operador unário

Uma operação do formato «op» x ou x «op», em que «op» é um operador unário sobrecarregável e x é uma expressão do tipo X, é processada da seguinte maneira:

  • O conjunto de operadores definidos pelo usuário candidatos fornecidos por X para a operação operator «op»(x) é determinado usando-se as regras de §12.4.6.
  • Se o conjunto de operadores definidos pelo usuário candidato não estiver vazio, ele se tornará o conjunto de operadores candidatos para a operação. Caso contrário, as implementações binárias predefinidas de operator «op», incluindo os formatos elevados, tornam-se o conjunto de operadores candidatos para a operação. As implementações predefinidas de um determinado operador são especificadas na descrição do operador. Os operadores predefinidos fornecidos por um tipo enum ou delegado são incluídos neste conjunto somente quando o tipo de tempo de vinculação de qualquer dos operandos—ou o tipo subjacente, caso seja um tipo anulável—é o tipo enum ou delegado.
  • As regras de resolução de sobrecarga de §12.6.4 são aplicadas ao conjunto de operadores de candidato para selecionar o melhor operador em relação à lista de argumentos (x) e esse operador se torna o resultado do processo de resolução de sobrecarga. Se a resolução de sobrecarga não selecionar um único operador melhor, ocorrerá um erro de tempo de associação.

12.4.5 Resolução de sobrecarga do operador binário

Uma operação do formato x «op» y, em que «op» é um operador binário sobrecarregável, x é uma expressão do tipo X e y é uma expressão do tipo Y, é processada da seguinte maneira:

  • O conjunto de operadores definidos pelo usuário de candidato fornecidos por X e Y para a operação operator «op»(x, y) é determinado. O conjunto consiste na união dos operadores de candidato fornecidos por X e os operadores de candidato fornecidos por Y, cada um determinado usando-se as regras de §12.4.6. Para o conjunto combinado, os candidatos são mesclados da seguinte maneira:
    • Se X e Y forem conversíveis de identidade ou se X e Y forem derivados de um tipo base comum, os operadores de candidato compartilhados só ocorrerão no conjunto combinado uma vez.
    • Se houver uma conversão de identidade entre X e Y, um operador «op»Y fornecido por Y tiver o mesmo tipo de retorno que um «op»X fornecido por X e os tipos de operando «op»Y tiverem uma conversão de identidade para os tipos de operando correspondentes de «op»X, então apenas «op»X ocorrerá no conjunto.
  • Se o conjunto de operadores definidos pelo usuário candidato não estiver vazio, ele se tornará o conjunto de operadores candidatos para a operação. Caso contrário, as implementações binárias predefinidas de operator «op», incluindo os formatos elevados, tornam-se o conjunto de operadores candidatos para a operação. As implementações predefinidas de um determinado operador são especificadas na descrição do operador. Para operadores enumerados e delegados predefinidos, os únicos operadores considerados são aqueles fornecidos por um tipo de enumeração ou delegado que é o tipo de tempo de associação de um dos operandos.
  • As regras de resolução de sobrecarga de §12.6.4 são aplicadas ao conjunto de operadores de candidato para selecionar o melhor operador em relação à lista de argumentos (x, y) e esse operador se torna o resultado do processo de resolução de sobrecarga. Se a resolução de sobrecarga não selecionar um único operador melhor, ocorrerá um erro de tempo de associação.

12.4.6 Operadores de candidato definidos pelo usuário

Dado um tipo T e uma operação operator «op»(A), em que «op» é um operador sobrecarregável e A é uma lista de argumentos, o conjunto de operadores definidos pelo usuário candidato fornecido por T para o operador «op»(A) é determinado da seguinte maneira:

  • Determine o tipo T₀. Se T for um tipo de valor anulável, T₀ será seu tipo subjacente; caso contrário, T₀ será igual a T.
  • Para todas as operator «op» declarações em T₀ e todos os formatos de precisão desses operadores, se pelo menos um operador for aplicável (§12.6.4.2) em relação à lista de argumentos A, o conjunto de operadores candidatos consistirá em todos esses operadores aplicáveis em T₀.
  • Caso contrário, se T₀ é object, o conjunto de operadores candidatos estará vazio.
  • Caso contrário, o conjunto de operadores de candidato fornecido pelo T₀ for o conjunto de operadores de candidato fornecido pela classe base direta de T₀ ou a classe base efetiva de T₀ se T₀ será um parâmetro de tipo.

12.4.7 Promoções numéricas

12.4.7.1 Geral

Essa subcláusula é informativa.

§12.4.7 e suas subcláusulas são um resumo do efeito combinado de:

  • as regras para conversões numéricas implícitas (§10.2.3);
  • as regras para melhor conversão (§12.6.4.7); e
  • os operadores aritméticos disponíveis (§12.10), relacionais (§12.12) e lógicos integrais (§12.13.2).

A promoção numérica consiste em executar automaticamente algumas conversões implícitas dos operandos dos operadores numéricos unários e binários predefinidos. A promoção numérica não é um mecanismo distinto, mas sim um efeito de aplicar a resolução de sobrecarga aos operadores predefinidos. A promoção numérica especificamente não afeta a avaliação de operadores definidos pelo usuário, embora os operadores definidos pelo usuário possam ser implementados para exibir efeitos semelhantes.

Como exemplo de promoção numérica, considere as implementações predefinidas do operador * binário:

int operator *(int x, int y);
uint operator *(uint x, uint y);
long operator *(long x, long y);
ulong operator *(ulong x, ulong y);
float operator *(float x, float y);
double operator *(double x, double y);
decimal operator *(decimal x, decimal y);

Quando as regras de resolução de sobrecarga (§12.6.4) são aplicadas a esse conjunto de operadores, o efeito é selecionar o primeiro dos operadores para os quais existem conversões implícitas dos tipos de operando.

Exemplo: para a operação b * s, em que b é um byte e s é um short, a resolução de sobrecarga seleciona operator *(int, int) como o melhor operador. Portanto, o efeito é que b e s serão convertidos em int e o tipo do resultado será int. Da mesma forma, para a operação i * d, onde i é um int e d é um double, a resolução de overload seleciona operator *(double, double) como o melhor operador. fim do exemplo

Fim do texto informativo.

12.4.7.2 Promoções numéricas unárias

Essa subcláusula é informativa.

A promoção numérica unária ocorre para os operandos dos operadores unários predefinidos +, - e ~. A promoção numérica unária consiste na conversão de operandos do tipo sbyte, byte, short, ushort ou char para o tipo int. Além disso, para o operador unário –, a promoção numérica unária converte operandos do tipo uint no tipo long.

Fim do texto informativo.

12.4.7.3 Promoções numéricas binárias

Essa subcláusula é informativa.

A promoção numérica binária ocorre para os operandos dos operadores unários predefinidos +, -, *, /, %, &, |, ^, ==, !=, >, <, >= e <=. A promoção numérica binária converte implicitamente os dois operandos em um tipo comum que, no caso dos operadores não relacionais, também se torna o tipo de resultado da operação. A promoção numérica binária consiste na aplicação das seguintes regras, na ordem em que elas aparecem aqui:

  • Se um dos operando for decimal, o outro operando será convertido para o tipo decimal ou ocorrerá um erro de tempo de associação se o outro operando for do tipo float ou double.
  • Caso contrário, se qualquer operando for do tipo double, o outro operando será convertido para o tipo double.
  • Caso contrário, se qualquer operando for do tipo float, o outro operando será convertido para o tipo float.
  • Caso contrário, se um dos operandos for do tipo ulong, o outro operando será convertido para o tipo ulong ou ocorrerá um erro de tempo de associação se o outro operando for do tipo type sbyte, short, int ou long.
  • Caso contrário, se qualquer operando for do tipo long, o outro operando será convertido para o tipo long.
  • Caso contrário, se o operando for do tipo uint e o outro operando for do tipo sbyte, short ou int, os dois operandos serão convertidos para o tipo long.
  • Caso contrário, se qualquer operando for do tipo uint, o outro operando será convertido para o tipo uint.
  • Caso contrário, os dois operandos serão convertidos no tipo int.

Observação: a primeira regra não permite operações que misturem o tipo decimal com os tipos double e float. A regra segue o fato de que não há conversões implícitas entre o tipo decimal e os tipos double e float. fim da observação

Observação: note também que não é possível que um operando seja do tipo ulong quando o outro operando for de um tipo integral assinado. O motivo é que não existe nenhum tipo integral que possa representar toda a gama de ulong, bem como os tipos integrais assinados. fim da observação

Nos dois casos acima, uma expressão de conversão pode ser usada para converter explicitamente um operando em um tipo compatível com o outro operando.

Exemplo: no código a seguir

decimal AddPercent(decimal x, double percent) =>
    x * (1.0 + percent / 100.0);

ocorre um erro de tempo de associação porque um decimal não pode ser multiplicado por um double. O erro é resolvido convertendo-se explicitamente o segundo operando em decimal da seguinte maneira:

decimal AddPercent(decimal x, double percent) =>
    x * (decimal)(1.0 + percent / 100.0);

fim do exemplo

Fim do texto informativo.

12.4.8 Operadores de precisão

Operadores elevados permitem que operadores predefinidos e definidos pelo usuário que operam em tipos de valor não anuláveis também sejam usados com formatos anuláveis desses tipos. Os operadores elevados são construídos a partir de operadores predefinidos e definidos pelo usuário que satisfazem determinados requisitos, conforme descrito a seguir:

  • Para os operadores unários +, ++, -, --, !(negação lógica) e ~, um formato de precisão de um operador existirá se os tipos de operando e de resultado forem dos tipos de valor não anuláveis. O formato elevado é construído adicionando-se um único modificador ? ao operando e aos tipos de resultado. O operador elevado produzirá um valor null se o operando for null. Caso contrário, o operador elevado desencapsula o operando, aplica o operador subjacente e encapsula o resultado.
  • Para os operadores binários +, -, *, /, %, &, |, ^, << e >>, um formato de precisão de um operador existirá se os tipos de operando e de resultado forem todos dos tipos de valor não anuláveis. O formato elevado é construído adicionando-se um único modificador ? a cada operando e ao tipo de resultado. O operador elevado produzirá um valor null se um ou ambos os operandos forem null (uma exceção sendo os operadores & e | do tipo bool?, conforme descrito em §12.13.5). Caso contrário, o operador elevado desencapsula os operandos, aplica o operador subjacente e encapsula o resultado.
  • Para os operadores de igualdade == e !=, o formato elevado de um operador existirá se os tipos de operando forem tipos de valor não anuláveis e se o tipo de resultado for bool. O formato elevado é construído adicionando-se um único modificador ? a cada tipo de operando. O operador elevado considera dois valores null iguais e um valor null diferente de qualquer valor não null. Se os dois operandos não foremnull, o operador elevado vai desencapsular os operandos e aplicar o operador subjacente para produzir o resultado de bool.
  • Para os operadores relacionais <, >, <=e >=, existe uma forma elevada de um operador se os tipos dos operandos forem tipos de valor não anuláveis, e se o tipo de resultado for bool. O formato elevado é construído adicionando-se um único modificador ? a cada tipo de operando. O operador elevado produzirá o valor false se um ou os dois operandos forem null. Caso contrário, o operador elevado vai desencapsular os operandos e aplicar o operador subjacente para produzir o resultado de bool.

12.5 Pesquisa de membros

12.5.1 Geral

Uma pesquisa de membro é o processo pelo qual é determinado o significado de um nome no contexto de um tipo. Uma pesquisa de membro pode ocorrer como parte da avaliação de um simple_name (§12.8.4) ou um member_access (§12.8.7) em uma expressão. Se simple_name ou member_access ocorrer como a primary_expression de uma invocation_expression (§12.8.10.2), o membro será invocado.

Se um membro for um método ou evento, ou se for uma constante, um campo ou uma propriedade de um tipo delegado (§20) ou do tipo dynamic (§8.2.4), o membro será considerado invocável.

A pesquisa de membro considera não apenas o nome de um membro, mas também o número de parâmetros de tipo que o membro tem e se o membro está acessível. Para fins de pesquisa de membro, os métodos genéricos e os tipos genéricos aninhados têm o número de parâmetros de tipo indicados em suas respectivas declarações e todos os outros membros têm parâmetros de tipo zero.

Uma pesquisa de um nome N com argumentos de tipo K em um tipo T é processada da seguinte maneira:

  • Primeiro, um conjunto de membros acessíveis denominado N é determinado:
    • Se T for um parâmetro de tipo, o conjunto será a união dos conjuntos de membros acessíveis denominados N em cada um dos tipos especificados como restrição primária ou restrição secundária (§15.2.5) para T, juntamente com o conjunto de membros acessíveis denominados N em object.
    • Caso contrário, o conjunto consiste em todos os membros acessíveis (§7.5) denominados N em T, incluindo os membros herdados e os membros acessíveis denominados N em object. Se T for um tipo construído, o conjunto de membros será obtido substituindo-se os argumentos de tipo, conforme descrito em §15.3.3. Os membros que incluem um modificador de override são excluídos do conjunto.
  • Em seguida, se K for zero, todos os tipos aninhados cujas declarações incluírem parâmetros de tipo serão removidos. Se K não for zero, todos os membros com um número diferente de parâmetros de tipo serão removidos. Quando K é zero, os métodos que têm parâmetros de tipo não são removidos, uma vez que o processo de inferência de tipo (§12.6.3) pode ser capaz de inferir os argumentos de tipo.
  • Em seguida, se o membro for invocado, todos os membros não invocados serão removidos do conjunto.
  • Em seguida, os membros ocultos por outros membros são removidos do conjunto. Para cada membro S.M no conjunto, em que S é o tipo no qual o membro M é declarado, são aplicadas as seguintes regras:
    • Se M for uma constante, campo, propriedade, evento ou membro de enumeração, todos os membros declarados em um tipo base S serão removidos do conjunto.
    • Se M for uma declaração de tipo, todos os tipos não declarados em um tipo base S serão removidos do conjunto e todas as declarações de tipo com o mesmo número de parâmetros de tipo que M declaradas em um tipo base de S serão removidas do conjunto.
    • Se M for um método, todos os membros não método declarados em um tipo base S serão removidos do conjunto.
  • Em seguida, removem-se do conjunto os membros da interface que são ocultados pelos membros da classe. Essa etapa só terá efeito se T for um parâmetro de tipo e T tiver uma classe base eficaz diferente de object e um conjunto de interfaces eficazes não vazios (§15.2.5). Para cada membro S.M no conjunto, em que S for o tipo no qual o membro M é declarado, as seguintes regras serão aplicadas se S for uma declaração de classe diferente de object:
    • Se M for uma constante, campo, propriedade, evento, membro de enumeração ou declaração de tipo, todos os membros declarados em uma declaração de interface serão removidos do conjunto.
    • Se M for um método, todos os membros que não sejam método declarados em uma declaração de interface serão removidos do conjunto e todos os métodos com a mesma assinatura que M declarados em uma declaração de interface serão removidos do conjunto.
  • Finalmente, tendo removido os membros ocultos, o resultado da pesquisa é determinado:
    • Se o conjunto consistir em um único membro que não seja um método, esse membro será o resultado da pesquisa.
    • Caso contrário, se o conjunto contiver apenas métodos, esse grupo de métodos será o resultado da pesquisa.
    • Caso contrário, a pesquisa será ambígua e ocorrerá um erro no momento da associação.

Para pesquisas de membro em tipos que não sejam parâmetros de tipo e interfaces, e pesquisas de membro em interfaces que são estritamente de herança única (cada interface na cadeia de herança tem exatamente zero ou uma interface base direta), o efeito das regras de pesquisa é simplesmente que membros derivados ocultam os membros base que possuem o mesmo nome ou a mesma assinatura. Essas pesquisas de herança única nunca são ambíguas. As ambiguidades que podem surgir de consultas de membros em interfaces de herança múltipla são descritas em §18.4.6.

Observação: essa fase representa apenas um tipo de ambiguidade. Se a pesquisa de membro resultar em um grupo de métodos, os usos adicionais do grupo de métodos poderão falhar devido à ambiguidade, por exemplo, conforme descrito em §12.6.4.1 e §12.6.6.2. fim da observação

12.5.2 Tipos de base

Para fins de consulta de membro, um tipo T é considerado como tendo os seguintes tipos base:

  • Se T for object ou dynamic, então T não terá nenhum tipo base.
  • Se T for enum_type, os tipos de base de T serão os tipos de classe System.Enum, System.ValueType e object.
  • Se T for struct_type, os tipos de base T serão os tipos de classe System.ValueType e object.

    Observação: nullable_value_type é struct_type (§8.3.1). fim da observação

  • Se T for class_type, os tipos de base T serão as classes de base de T, incluindo o tipo de classe object.
  • Se T for interface_type, os tipos de base T serão as interfaces de base T e o tipo de classe object.
  • Se T for array_type, os tipos de base T são tipos de classe System.Array e object.
  • Se T for delegate_type, os tipos de base T serão os tipos de classe System.Delegate e object.

12.6 Membros de função

12.6.1 Geral

Membros de função são membros que contêm instruções executáveis. Os membros de função são sempre membros de tipos e não podem ser membros de namespaces. O C# define as seguintes categorias de membros de função:

  • Métodos
  • Propriedades
  • Eventos
  • Indexadores
  • Operadores definidos pelo usuário
  • Construtores de instância
  • Construtores estáticos
  • Finalizadores

Exceto para finalizadores e construtores estáticos (que não podem ser invocados explicitamente), as instruções contidas nos membros da função são executadas por invocações de membro de função. A sintaxe real para escrever uma invocação de membro de função depende da categoria de membro de função específica.

A lista de argumentos (§12.6.2) de uma invocação de membro de função fornece valores reais ou referências variáveis para os parâmetros do membro de função.

Invocações de métodos genéricos podem empregar inferência de tipo para determinar o conjunto de argumentos de tipo a serem passados para o método. Esse processo é descrito em §12.6.3.

Invocações de métodos, indexadores, operadores e construtores de instância empregam o mecanismo de resolução de sobrecarga para determinar qual membro de função de um conjunto de candidatos deve ser invocado. Esse processo é descrito em §12.6.4.

Depois que um membro de função específico tiver sido identificado no momento da associação, possivelmente por meio da resolução de sobrecarga, o processo real de run-time para invocar o membro de função é descrito em §12.6.6.

Observação: a tabela a seguir resume o processamento que ocorre em constructos que envolvem as seis categorias de membros de função que podem ser invocadas explicitamente. Na tabela ex, y e value indicam expressões classificadas como variáveis ou valores; T indica uma expressão classificada como um tipo; F é o nome simples de um método e P é o nome simples de uma propriedade.

Constructo Exemplo Descrição
Invocação de método F(x, y) A resolução de sobrecarga é aplicada para selecionar o melhor método F na classe ou struct que o contém. O método é invocado com a lista de argumentos (x, y). Se o método não for static, a expressão de instância será this.
T.F(x, y) A resolução de sobrecarga é aplicada para selecionar o melhor método F na classe ou struct T. Ocorrerá um erro de tempo de associação se o método não for static. O método é invocado com a lista de argumentos (x, y).
e.F(x, y) A resolução de sobrecarga é aplicada para selecionar o melhor método F na classe, estrutura ou interface fornecida pelo tipo de e. Ocorrerá um erro de tempo de associação se o método for static. O método é invocado com a expressão de instância e e a lista de argumentos (x, y).
Acesso à propriedade P O acessador get da propriedade P na classe ou estrutura contida é invocado. Ocorrerá um erro de tempo de compilação se P for somente gravação. Se P não for static, a expressão de instância será this.
P = value O acessador set da propriedade P na classe ou estrutura contida é invocado com a lista de argumentos (value). Ocorrerá um erro de tempo de compilação se P for somente leitura. Se P não for static, a expressão de instância será this.
T.P O acessador get da propriedade P na classe ou estrutura T é invocado. Ocorrerá um erro de tempo de compilação se P não for static ou se P for somente leitura.
T.P = value O acessador set da propriedade P na classe ou estrutura T é invocado com a lista de argumentos (value). Ocorrerá um erro de compilação se P não for static ou se P for somente leitura.
e.P O acessador get da propriedade P na classe, estrutura ou interface fornecida pelo tipo de E é invocado com a expressão de instância e. Ocorrerá um erro de tempo de associação se P for static ou se P for somente gravação.
e.P = value O acessador set da propriedade P na classe, estrutura ou interface fornecida pelo tipo de E é invocado com a expressão de instância e e a lista de argumentos (value). Ocorrerá um erro de tempo de associação se P for static ou se P for somente leitura.
Acesso de evento E += value O acessador add do evento E na classe ou estrutura contida é invocado. Se E não for static, a expressão de instância será this.
E -= value O acessador remove do evento E na classe ou estrutura contida é invocado. Se E não for static, a expressão de instância será this.
T.E += value O acessador add do evento E na classe ou estrutura T é invocado. Ocorrerá um erro de tempo de associação se E não for static.
T.E -= value O acessador remove do evento E na classe ou estrutura T é invocado. Ocorrerá um erro de tempo de associação se E não for static.
e.E += value O acessador de adição do evento E na classe, struct ou interface fornecida pelo tipo de E é invocado com a expressão de instância e. Ocorrerá um erro de tempo de associação se E for static.
e.E -= value O acessador remove do evento E na classe, estrutura ou interface fornecida pelo tipo de E é invocado com a expressão de instância e. Ocorrerá um erro de tempo de associação se E for static.
Acesso de indexador e[x, y] A resolução de sobrecarga é aplicada para selecionar o melhor indexador na classe, estrutura ou interface fornecida pelo tipo de e. O acessador 'get' do indexador é invocado com a expressão de instância e e a lista de argumentos (x, y). Ocorrerá um erro de tempo de associação se o indexador for somente gravação.
e[x, y] = value A resolução de sobrecarga é aplicada para selecionar o melhor indexador na classe, estrutura ou interface fornecida pelo tipo de e. O acessador set do indexador é invocado com a expressão de instância e e a lista de argumentos (x, y, value). Ocorrerá um erro de tempo de associação se o indexador for somente leitura.
Invocação do operador -x A resolução de sobrecarga é aplicada para selecionar o melhor operador unário na classe ou estrutura fornecida pelo tipo de x. O operador selecionado é invocado com a lista de argumentos (x).
x + y A resolução de sobrecarga é aplicada para selecionar o melhor operador binário nas classes ou structs fornecidos pelos tipos de x e y. O operador selecionado é invocado com a lista de argumentos (x, y).
Invocação do construtor de instância new T(x, y) A resolução de sobrecarga é aplicada para selecionar o melhor construtor de instância na classe ou estrutura T. O construtor de instância é invocado com a lista de argumentos (x, y).

fim da observação

12.6.2 Listas de argumentos

12.6.2.1 Geral

Cada invocação de membro de função e delegado inclui uma lista de argumentos, que fornece valores reais ou referências de variável para os parâmetros do membro da função. A sintaxe para especificar a lista de argumentos de uma invocação de membro de função depende da categoria de membro da função:

  • Por exemplo para construtores, métodos, indexadores e delegados, os argumentos são especificados como argument_list conforme descrito abaixo. Para indexadores, ao invocar o acessador set, a lista de argumentos inclui também a expressão especificada como o operando direito do operador de atribuição.

    Observação: esse argumento adicional não é utilizado para resolução de sobrecarga, apenas durante a invocação do acessador set. fim da observação

  • Para propriedades, a lista de argumentos está vazia ao invocar o acessador get, e consiste na expressão especificada como o operando direito do operador de atribuição ao invocar o acessador set.
  • Para eventos, a lista de argumentos consiste na expressão especificada como o operando direito do operador += ou -=.
  • Para operadores definidos pelo usuário, a lista de argumentos consiste no operando único do operador unário ou nos dois operandos do operador binário.

Os argumentos de propriedades (§15.7) e eventos (§15.8) são sempre passados como parâmetros de valor (§15.6.2.2). Os argumentos de operadores definidos pelo usuário (§15.10) são sempre passados como parâmetros de valor (§15.6.2.2) ou parâmetros de entrada (§9.2.8). Os argumentos dos indexadores (§15.9) são sempre passados como parâmetros de valor (§15.6.2.2), parâmetros de entrada (§9.2.8) ou matrizes de parâmetro (§15.6.2.4). Não há suporte para parâmetros de saída e referência para essas categorias de membros de função.

Os argumentos de um construtor de instância, método, indexador ou invocação de delegado são especificados como argument_list:

argument_list
    : argument (',' argument)*
    ;

argument
    : argument_name? argument_value
    ;

argument_name
    : identifier ':'
    ;

argument_value
    : expression
    | 'in' variable_reference
    | 'ref' variable_reference
    | 'out' variable_reference
    ;

argument_list consiste em um ou mais argumentos separados por vírgulas. Cada argumento consiste em um argument_name opcional seguido por um argument_value. Um argumento com argument_name é chamado de argumento denominado, enquanto um argumento sem argument_name é um argumento posicional.

O argument_value pode assumir uma das seguintes formas:

  • Uma expressão, indicando que o argumento é passado como um parâmetro de valor ou é transformado em um parâmetro de entrada e, em seguida, passado como esse, conforme determinado por (§12.6.4.2 e descrito em §12.6.2.3.
  • A palavra-chave in seguida por variable_reference (§9.5), indicando que o argumento é passado como um parâmetro de entrada (§15.6.2.3.2). Uma variável deve ser definitivamente atribuída (§9.4) antes que possa ser passada como um parâmetro de entrada.
  • A palavra-chave ref seguida por variable_reference (§9.5), indicando que o argumento é passado como um parâmetro de referência (§15.6.2.3.3). Uma variável deve ser definitivamente atribuída (§9.4) antes que possa ser passada como um parâmetro de referência.
  • A palavra-chave out seguida por variable_reference (§9.5), indicando que o argumento é passado como um parâmetro de saída (§15.6.2.3.4). Uma variável é considerada definitivamente atribuída (§9.4) após uma invocação de membro de função na qual a variável é passada como um parâmetro de saída.

O formato determina o modo parameter-passing do argumento: valor, entrada, referência ou saída, respectivamente. No entanto, conforme mencionado acima, um argumento com o modo de passagem por valor pode ser transformado em um com o modo de passagem por entrada.

Passar um campo volátil (§15.5.4) como um parâmetro de entrada, saída ou referência causa um aviso, pois o campo não pode ser tratado como volátil pelo método invocado.

12.6.2.2 Parâmetros correspondentes

Para cada argumento em uma lista de argumentos, deve haver um parâmetro correspondente no membro de função ou delegado que está sendo invocado.

A lista de parâmetros usada abaixo é determinada da seguinte maneira:

  • Para métodos virtuais e indexadores definidos em classes, a lista de parâmetros é escolhida na primeira declaração ou substituição do membro da função encontrado ao começar com o tipo estático do receptor e pesquisar nas classes base.
  • Para métodos parciais, a lista de parâmetros da declaração de método parcial definidora é usada.
  • Para todos os outros delegados e membros de função, há apenas uma lista de parâmetros, que é aquela usada.

A posição de um argumento ou parâmetro é definida como o número de argumentos ou parâmetros que o precedem na lista de argumentos ou na lista de parâmetros.

Os parâmetros correspondentes para argumentos de membro de função são estabelecidos da seguinte maneira:

  • Argumentos na argument_list de construtores de instância, métodos, indexadores e delegados:
    • Um argumento posicional em que um parâmetro ocorre na mesma posição na lista de parâmetros corresponde a esse parâmetro, a menos que o parâmetro seja uma matriz de parâmetros e o membro de função seja invocado em sua forma expandida.
    • Um argumento posicional de um membro de função com uma matriz de parâmetros invocada em sua forma expandida, que ocorre na ou após a posição da matriz de parâmetros na lista de parâmetros, corresponde a um elemento na matriz de parâmetros.
    • Um argumento denominado corresponde ao parâmetro do mesmo nome na lista de parâmetros.
    • Para indexadores, ao invocar o acessador set, a expressão especificada como o operando direito do operador de atribuição corresponde ao parâmetro implícito value da declaração do acessador set.
  • Para propriedades, ao invocar o acessador get, não há argumentos. Ao invocar o acessador set, a expressão especificada como o operando direito do operador de atribuição corresponde ao parâmetro implícito de valor da declaração do acessador set.
  • Para operadores unários definidos pelo usuário (incluindo conversões), o operando único corresponde ao único parâmetro da declaração do operador.
  • Para operadores binários definidos pelo usuário, o operando esquerdo corresponde ao primeiro parâmetro e o operando à direita corresponde ao segundo parâmetro da declaração do operador.
  • Um argumento sem nome corresponde a nenhum parâmetro quando ele está após um argumento nomeado fora de posição ou um argumento nomeado que corresponde a uma matriz de parâmetros.

    Observação: isso impede que void M(bool a = true, bool b = true, bool c = true); sejam invocados por M(c: false, valueB);. O primeiro argumento é usado fora de posição (o argumento é usado na primeira posição, mas o parâmetro denominado c está na terceira posição); portanto, os argumentos a seguir devem ser denominados. Em outras palavras, os argumentos nomeados que não são os últimos só são permitidos quando o nome e a posição resultam na identificação do mesmo parâmetro correspondente. fim da observação

12.6.2.3 Avaliação em run-time de listas de argumentos

Durante o processamento em run-time de uma invocação de membro de função (§12.6.6), as expressões ou referências de variáveis de uma lista de argumentos são avaliadas em ordem, da esquerda para a direita, da seguinte maneira:

  • Para um argumento de valor, se o modo de passagem do parâmetro for valor

    • a expressão de argumento é avaliada e uma conversão implícita (§10.2) para o tipo de parâmetro correspondente é executada. O valor resultante torna-se o valor inicial do parâmetro de valor na invocação do membro da função.

    • Caso contrário, o modo de passagem do parâmetro é de entrada. Se o argumento for uma referência variável e existir uma conversão de identidade (§10.2.2) entre o tipo do argumento e o tipo do parâmetro, o local de armazenamento resultante se tornará o local de armazenamento representado pelo parâmetro na invocação do membro da função. Caso contrário, um local de armazenamento será criado com o mesmo tipo do parâmetro correspondente. A expressão de argumento é avaliada e uma conversão implícita (§10.2) para o tipo de parâmetro correspondente é executada. O valor resultante é armazenado dentro desse local de armazenamento. Esse local de armazenamento é representado pelo parâmetro de entrada na invocação do membro da função.

      Exemplo: dadas as seguintes declarações e chamadas de método:

      static void M1(in int p1) { ... }
      int i = 10;
      M1(i);         // i is passed as an input argument
      M1(i + 5);     // transformed to a temporary input argument
      

      Na M1(i) chamada de método, i é passado como um argumento de entrada, pois é classificado como uma variável e tem o mesmo tipo int que o parâmetro de entrada. Na chamada do método M1(i + 5), uma variável sem nome int é criada, inicializada com o valor passado como argumento e então passada como um argumento de entrada. Consulte §12.6.4.2 e §12.6.4.4.

      fim do exemplo

  • Para um argumento de entrada, saída ou referência, a referência de variável é avaliada e o local de armazenamento resultante torna-se o local de armazenamento representado pelo parâmetro na invocação do membro da função. Para um argumento de entrada ou referência, a variável deve ser atribuída definitivamente no ponto da chamada de método. Se a referência de variável for fornecida como um argumento de saída ou for um elemento de matriz de um reference_type, uma verificação em run-time será executada para garantir que o tipo de elemento da matriz seja idêntico ao tipo do parâmetro. Se essa verificação falhar, System.ArrayTypeMismatchException será gerado.

Observação: essa verificação em run-time é necessária devido à covariância da matriz (§17.6). fim da observação

Exemplo: no código a seguir

class Test
{
    static void F(ref object x) {...}

    static void Main()
    {
        object[] a = new object[10];
        object[] b = new string[10];
        F(ref a[0]); // Ok
        F(ref b[1]); // ArrayTypeMismatchException
    }
}

A segunda invocação de F faz com que um System.ArrayTypeMismatchException seja lançado porque o tipo de elemento real de b é string e não object.

fim do exemplo

Métodos, indexadores e construtores de instância podem declarar seu parâmetro mais correto como uma matriz de parâmetros (§15.6.2.4). Esses membros de função são invocados em sua forma normal ou em sua forma expandida, dependendo do que é aplicável (§12.6.4.2):

  • Quando um membro de função com uma matriz de parâmetros é invocado em sua forma normal, o argumento fornecido para a matriz de parâmetros deve ser uma única expressão que é conversível implicitamente (§10.2) para o tipo de matriz de parâmetros. Nesse caso, a matriz de parâmetros age precisamente como um parâmetro de valor.
  • Quando membro de função com uma matriz de parâmetros é invocado em sua forma expandida, a invocação deve especificar zero ou mais argumentos posicionais para a matriz de parâmetros, em que cada argumento é uma expressão conversível implicitamente (§10.2) para o tipo de elemento da matriz de parâmetros. Nesse caso, a invocação cria uma instância do tipo de matriz de parâmetros com um tamanho correspondente ao número de argumentos, inicializa os elementos da instância da matriz com os valores de argumento fornecidos e usa a instância de matriz recém-criada como o argumento real.

As expressões de uma lista de argumentos são sempre avaliadas em ordem textual.

Exemplo: Assim, o exemplo

class Test
{
    static void F(int x, int y = -1, int z = -2) =>
        Console.WriteLine($"x = {x}, y = {y}, z = {z}");

    static void Main()
    {
        int i = 0;
        F(i++, i++, i++);
        F(z: i++, x: i++);
    }
}

produz a saída

x = 0, y = 1, z = 2
x = 4, y = -1, z = 3

fim do exemplo

Quando um membro de função com uma matriz de parâmetros é invocado em sua forma expandida com pelo menos um argumento expandido, a invocação é processada como se uma expressão de criação de matriz com um inicializador de matriz (§12.8.17.4) fosse inserida em torno dos argumentos expandidos. Uma matriz vazia é passada quando não há argumentos para a matriz de parâmetros; não é especificado se a referência passada é para uma matriz vazia recém-alocada ou existente.

Exemplo: dada a declaração

void F(int x, int y, params object[] args);

as invocações a seguir do formato expandido do método

F(10, 20, 30, 40);
F(10, 20, 1, "hello", 3.0);

correspondem exatamente a

F(10, 20, new object[] { 30, 40 });
F(10, 20, new object[] { 1, "hello", 3.0 });

fim do exemplo

Quando os argumentos são omitidos de um membro de função com parâmetros opcionais correspondentes, os argumentos padrão da declaração do membro da função são passados implicitamente. (Isso pode envolver a criação de um local de armazenamento, conforme descrito acima.)

Observação: como eles são sempre constantes, sua avaliação não afetará a avaliação dos argumentos restantes. fim da observação

12.6.3 Inferência de tipos

12.6.3.1 Geral

Quando um método genérico é chamado sem especificar argumentos de tipo, um processo de inferência de tipo tenta inferir os argumentos de tipo para a chamada. A presença de inferência de tipo permite que uma sintaxe mais conveniente seja usada para chamar um método genérico e permite que o programador evite especificar informações de tipo redundantes.

Exemplo:

class Chooser
{
    static Random rand = new Random();

    public static T Choose<T>(T first, T second) =>
        rand.Next(2) == 0 ? first : second;
}

class A
{
    static void M()
    {
        int i = Chooser.Choose(5, 213); // Calls Choose<int>
        string s = Chooser.Choose("apple", "banana"); // Calls Choose<string>
    }
}

Por meio da inferência de tipo, os argumentos de tipo int e string são determinados direto dos argumentos para o método.

fim do exemplo

A inferência de tipos ocorre como parte do processamento em tempo de associação de uma invocação de método (§12.8.10.2) e ocorre antes da etapa de resolução de sobrecarga da invocação. Quando um grupo de métodos específico é definido em uma invocação de método e nenhum argumento de tipo é definido como parte da invocação do método, a inferência de tipo é aplicada a cada método genérico no grupo de métodos. Se a inferência de tipo for bem-sucedida, os argumentos de tipo inferido serão usados para determinar os tipos de argumentos para resolução de sobrecarga subsequente. Se a resolução de sobrecarga escolher um método genérico como aquele a ser invocado, os argumentos de tipo inferidos serão usados como argumentos de tipo para a invocação. Se a inferência de tipo para um método específico falhar, esse método não participará da resolução de sobrecarga. A falha de inferência de tipos, por si só, não causa um erro de tempo de associação. No entanto, isso geralmente leva a um erro de tempo de vinculação quando a resolução de sobrecarga falha em encontrar qualquer método aplicável.

Se cada argumento fornecido não corresponder a exatamente um parâmetro no método (§12.6.2.2), ou houver um parâmetro não opcional sem nenhum argumento correspondente, a inferência falhará imediatamente. Caso contrário, suponha que o método genérico tenha a seguinte assinatura:

Tₑ M<X₁...Xᵥ>(T₁ p₁ ... Tₓ pₓ)

Com uma chamada de método do formato M(E₁ ...Eₓ) a tarefa de inferência de tipo é localizar argumentos de tipo exclusivos S₁...Sᵥ para cada um dos parâmetros de tipo X₁...Xᵥ para que a chamada M<S₁...Sᵥ>(E₁...Eₓ) se torne válida.

O processo de inferência de tipo é descrito abaixo como um algoritmo. Um compilador compatível pode ser implementado usando-se uma abordagem alternativa, desde que atinja o mesmo resultado em todos os casos.

Durante o processo de inferência, cada parâmetro de tipo Xᵢ é fixo em um determinado tipo Sᵢ ou não fixo com um conjunto associado de limites. Cada um dos limites é algum tipo T. Inicialmente, cada variável de tipo Xᵢ não está fixada com um conjunto vazio de limites.

A inferência de tipo ocorre em fases. Cada fase tentará inferir argumentos de tipo para mais variáveis de tipo com base nos resultados da fase anterior. A primeira fase faz algumas inferências iniciais de limites, enquanto a segunda fase corrige variáveis de tipo para tipos específicos e infere limites adicionais. A segunda fase pode ter que ser repetida várias vezes.

Observação: a inferência de tipo também é usada em outros contextos, incluindo a conversão de grupos de métodos (§12.6.3.14) e para encontrar o melhor tipo comum de um conjunto de expressões (§12.6.3.15). fim da observação

12.6.3.2 A primeira fase

Para cada um dos argumentos do método Eᵢ:

  • Se Eᵢ for uma função anônima, uma inferência de tipo de parâmetro explícito (§12.6.3.8) será feita deEᵢparaTᵢ
  • Caso contrário, se Eᵢ tiver um tipo U e o parâmetro correspondente for um parâmetro de valor (§15.6.2.2), uma inferência de limite inferior (§12.6.3.10) será feita deUparaTᵢ.
  • Caso contrário, se Eᵢ tiver um tipo U e o parâmetro correspondente for um parâmetro de referência (§15.6.2.3.3.3) ou parâmetro de saída (§15.6.2 .3.4), uma inferência exata (§12.6.3.9) será feita deUparaTᵢ.
  • Caso contrário, se Eᵢ tiver um tipo U e o parâmetro correspondente for um parâmetro de entrada (§15.6.2.3.2) e Eᵢ for um argumento de entrada, então uma inferência exata (§12.6.3.9) será feita deUparaTᵢ.
  • Caso contrário, se Eᵢ tiver um U de tipo e o parâmetro correspondente for um parâmetro de entrada (§15.6.2.3.2), então uma inferência de limite inferior (§12.6.3.10) será feita deUparaTᵢ.
  • Caso contrário, nenhuma inferência será feita para esse argumento.

12.6.3.3 A segunda fase

A segunda fase continua da seguinte maneira:

  • Todas as variáveis de tipo não fixasXᵢ que não dependem de (§12.6.3.6) nenhum Xₑ são fixas (§12.6.3.12).
  • Se tais variáveis de tipo não existirem, todas as variáveis de tipo não fixasXᵢ serão fixadas para as quais todas as seguintes condições são válidas:
    • Há pelo menos uma variável de tipo Xₑ que depende deXᵢ
    • Xᵢ tem um conjunto não vazio de limites
  • Se não existirem tais variáveis de tipo e ainda houver variáveis de tipo não fixadas, a inferência de tipo falhará.
  • Caso contrário, se não houver mais variáveis de tipo não fixas, a inferência do tipos será bem-sucedida.
  • Caso contrário, para todos os argumentos Eᵢ com o tipo de parâmetro correspondente Tᵢ em que os tipos de saída (§12.6.3.5) contêm variáveis de tipo não fixasXₑ, mas os tipos de entrada (§12.6.3.4) não, uma inferência de tipo de saída (§12.6.3.7) é feita deEᵢparaTᵢ. Em seguida, a segunda fase é repetida.

12.6.3.4 Tipos de entrada

Se E for um grupo de métodos ou uma função anônima com tipagem implícita e T for um tipo delegado ou tipo de árvore de expressão, todos os tipos de parâmetro de T serão tipos de entrada deEcom tipoT.

12.6.3.5 Tipos de saída

Se E for um grupo de métodos ou uma função anônima e T for um tipo delegado ou tipo de árvore de expressão, o tipo de retorno de T será um tipo de saída deEcom o tipoT.

12.6.3.6 Dependência

Uma variável de tipo não fixaXᵢdependerá diretamente de uma variável de tipo de não fixaXₑ se, para algum argumento Eᵥ com o tipo TᵥXₑ, ocorrer em um tipo de entrada de Eᵥ com o tipo Tᵥ e Xᵢ ocorrer em um tipo de saída de Eᵥ com o tipo Tᵥ.

Xₑ depende deXᵢ se Xₑdepender diretamente deXᵢ ou se Xᵢdepender diretamente deXᵥ e Xᵥdepender deXₑ. Assim, "depende de" é o fechamento transitivo, mas não reflexivo de "depende diretamente de".

12.6.3.7 Inferências de tipo de saída

Uma inferência de tipo de saída é feita de uma expressão Epara um tipo T da seguinte maneira:

  • Se E for uma função anônima com tipo de retorno inferido U (§12.6.3.13) e T seja um tipo delegado ou tipo de árvore de expressão com tipo de retorno Tₓ, então uma inferência de limite inferior (§12.6.3.10) é feita deUparaTₓ.
  • Caso contrário, se E for um grupo de métodos e T for um tipo de árvore de expressão ou tipo delegado com tipos de parâmetro T₁...Tᵥ e tipo de retorno Tₓ, e a resolução de sobrecarga de E com os tipos T₁...Tᵥ produzir um único método com o tipo de retorno U, uma inferência de limite inferior será feita deUparaTₓ.
  • Caso contrário, se E for uma expressão do tipo U, então é feita uma inferência de limite inferiordeUparaT.
  • Caso contrário, nenhuma inferência será feita.

12.6.3.8 Inferências explícitas de tipo de parâmetro

Uma inferência de tipo de parâmetro explícito é feita de uma expressão Epara um tipo T da seguinte maneira:

  • Se E for uma função anônima explicitamente tipada com tipos de parâmetro U₁...Uᵥ e T for um tipo delegado ou tipo de árvore de expressão com tipos de parâmetro V₁...Vᵥ, para cada Uᵢ, uma inferência exata (§12.6.3.9) será feita deUᵢpara o Vᵢ correspondente.

12.6.3.9 Inferências exatas

Uma inferência exatade um tipo Upara um tipo V é feita da seguinte maneira:

  • Se V for não fixadoXᵢ, U será adicionado ao conjunto de limites exatos para Xᵢ.
  • Caso contrário, os conjuntos V₁...Vₑ e U₁...Uₑ serão determinados verificando-se se algum dos seguintes casos se aplica:
    • V é um tipo de matriz V₁[...] e U é um tipo de matriz U₁[...] da mesma classificação
    • V é o tipo V₁? e U é o tipo U₁
    • V é um tipo de constructo C<V₁...Vₑ> e U é um tipo de constructo C<U₁...Uₑ>
      Se qualquer um desses casos se aplicar, uma inferência exata será feita de cada Uᵢ ao Vᵢ correspondente.
  • Caso contrário, nenhuma inferência será feita.

12.6.3.10 Inferências de limite inferior

Uma inferência de limite inferior de um tipo Upara um tipo V é feita da seguinte maneira:

  • Se V for não fixadoXᵢ, U será adicionado ao conjunto de limites inferiores para Xᵢ.
  • Caso contrário, se V for o tipo V₁? e U for o tipo U₁? uma inferência de limite inferior será feita de U₁ para V₁.
  • Caso contrário, os conjuntos U₁...Uₑ e V₁...Vₑ serão determinados verificando-se se algum dos seguintes casos se aplica:
    • V é um tipo de matriz V₁[...] e U é um tipo de matriz U₁[...] da mesma classificação
    • V é um de IEnumerable<V₁>, ICollection<V₁>, IReadOnlyList<V₁>>, IReadOnlyCollection<V₁> ou IList<V₁> e U é um tipo de matriz unidimensional U₁[]
    • V é um tipo class, struct, interface ou delegate construído, tipo C<V₁...Vₑ> e há um tipo exclusivo C<U₁...Uₑ> de modo que U (ou, se U for um tipo parameter, sua classe base efetiva ou qualquer membro do conjunto de interface efetivo) seja idêntico a inherits de (direta ou indiretamente) ou implementa (direta ou indiretamente) C<U₁...Uₑ>.
    • (A restrição de "exclusividade" significa que, no caso de interface C<T>{} class U: C<X>, C<Y>{}, nenhuma inferência é feita ao inferir-se de U para C<T> porque U₁ pode ser X ou Y.)
      Se qualquer um desses casos se aplicar, uma inferência será feita de cada Uᵢ a Vᵢ correspondente da seguinte maneira:
    • Se Uᵢ não for conhecido como um tipo de referência, será feita uma inferência exata .
    • Caso contrário, se U for um tipo de matriz, uma inferência de limite inferior será feita.
    • Caso contrário, se V for C<V₁...Vₑ> a inferência dependerá do parâmetro de tipo i-th de C:
      • Se for covariante, uma inferência de limite inferior é realizada.
      • Se for contravariante, será feita uma inferência de limite superior.
      • Se for invariável, uma inferência exata será feita.
  • Caso contrário, nenhuma inferência será feita.

12.6.3.11 Inferências de limite superior

Uma inferência de limite superior de um tipo Upara um tipo V é feita da seguinte maneira:

  • Se V for não fixadoXᵢ, então U será adicionado ao conjunto de limites superiores para Xᵢ.
  • Caso contrário, os conjuntos V₁...Vₑ e U₁...Uₑ serão determinados verificando-se se algum dos seguintes casos se aplica:
    • U é um tipo de matriz U₁[...] e V é um tipo de matriz V₁[...] da mesma classificação
    • U é um de IEnumerable<Uₑ>, ICollection<Uₑ>, IReadOnlyList<Uₑ>, IReadOnlyCollection<Uₑ> ou IList<Uₑ> e V é um tipo de matriz unidimensional Vₑ[]
    • U é o tipo U1? e V é o tipo V1?
    • U é um tipo de classe, estrutura, interface ou delegado construído C<U₁...Uₑ> e V é um tipo class, struct, interface ou delegate que é identical para, inherits de (direta ou indiretamente), ou implementa (direta ou indiretamente) um tipo exclusivo C<V₁...Vₑ>
    • (A restrição de "exclusividade" significa que, dada uma interface C<T>{} class V<Z>: C<X<Z>>, C<Y<Z>>{}, nenhuma inferência é feita quando se infere de C<U₁> para V<Q>. Não se fazem inferências de U₁ para X<Q> ou Y<Q>.)
      Se qualquer um desses casos se aplicar, uma inferência será feita de cada Uᵢ a Vᵢ correspondente da seguinte maneira:
    • Se Uᵢ não for conhecido como um tipo de referência, será feita uma inferência exata .
    • Caso contrário, se V for um tipo de matriz, será feita uma inferência de limite superior
    • Caso contrário, se U for C<U₁...Uₑ> a inferência dependerá do parâmetro de tipo i-th de C:
      • Se for covariante, será feita uma inferência de limite superior .
      • Se for contravariante, então será feita uma inferência de limite inferior.
      • Se for invariável, uma inferência exata será feita.
  • Caso contrário, nenhuma inferência será feita.

12.6.3.12 Correção

Uma variável de tipo não fixaXᵢ com um conjunto de limites é fixada da seguinte maneira:

  • O conjunto de tipos candidatosUₑ começa sendo idêntico ao conjunto de todos os tipos presentes no conjunto de limites para Xᵢ.
  • Cada limite de Xᵢ é examinado por sua vez: para cada limite exato U de Xᵢ, todos os tipos Uₑ que não são idênticos a U são removidos do conjunto de candidatos. Para cada limite inferior U de Xᵢ, todos os tipos Uₑ para os quais não existe uma conversão implícita de U são removidos do conjunto de candidatos. Para cada limite superior U de Xᵢ, todos os tipos Uₑ dos quais não existe uma conversão implícita para U são removidos do conjunto de candidatos.
  • Se entre os tipos de candidatos restantes Uₑ houver um tipo exclusivo V para o qual há uma conversão implícita de todos os outros tipos de candidatos, Xᵢ será corrigido para V.
  • Caso contrário, a inferência de tipo falhará.

12.6.3.13 Tipo de retorno inferido

O tipo de retorno inferido de uma função anônima F é usado durante a inferência de tipos e a resolução de sobrecarga. O tipo de retorno inferido só pode ser determinado para uma função anônima em que todos os tipos de parâmetro são conhecidos, seja porque são dados explicitamente, prestados por meio de uma conversão de função anônima ou inferidos durante a inferência de tipo em uma invocação de método genérico envolvente.

O tipo de retorno efetivo inferido é determinado da seguinte maneira:

  • Se o corpo de F for uma expressão que tenha um tipo, o tipo de retorno efetivo inferido de F será o tipo dessa expressão.
  • Se o corpo de F for um bloco e o conjunto de expressões nas instruções do bloco return possuir um tipo comum ideal T (§12.6.3.15), o tipo de retorno efetivo inferido de F será T.
  • Caso contrário, um tipo de retorno efetivo não poderá ser inferido para F.

O tipo de retorno inferido é determinado da seguinte maneira:

  • Se F for assíncrono e caso o corpo de F seja uma expressão classificada como nada (§12.2) ou um bloco em que nenhuma instrução return contenha expressões, o tipo de retorno inferido é «TaskType» (§15.14.1).
  • Se F for assíncrono e tiver um tipo Tde retorno efetivo inferido, o tipo de retorno inferido será «TaskType»<T>»(§15.14.1).
  • Se F for não assíncrono e tiver um tipo de retorno efetivo inferido T, o tipo de retorno inferido será T.
  • Caso contrário, um tipo de retorno não poderá ser inferido para F.

Exemplo: como exemplo de inferência de tipo envolvendo funções anônimas, considere o método de extensão Select declarado na classe System.Linq.Enumerable:

namespace System.Linq
{
    public static class Enumerable
    {
        public static IEnumerable<TResult> Select<TSource,TResult>(
            this IEnumerable<TSource> source,
            Func<TSource,TResult> selector)
        {
            foreach (TSource element in source)
            {
                yield return selector(element);
            }
        }
   }
}

Supondo que o namespace System.Linq tenha sido importado com uma diretiva using namespace, e considerando uma classe Customer com uma propriedade Name do tipo string, o método Select pode ser usado para selecionar os nomes de uma lista de clientes.

List<Customer> customers = GetCustomerList();
IEnumerable<string> names = customers.Select(c => c.Name);

A invocação do método de extensão (§12.8.10.3) de Select é processada reescrevendo-se a invocação para uma invocação de método estático:

IEnumerable<string> names = Enumerable.Select(customers, c => c.Name);

Como os argumentos de tipo não foram especificados explicitamente, a inferência de tipo é usada para inferir os argumentos de tipo. Primeiro, o argumento dos clientes está relacionado ao parâmetro de origem, inferindo-se TSource ser Customer. Em seguida, usando-se o processo de inferência de tipo de função anônimo descrito acima, c recebe o tipo Customer e a expressão c.Name está relacionada ao tipo de retorno do parâmetro seletor, inferindo-se TResult ser string. Assim, a invocação é equivalente a

Sequence.Select<Customer,string>(customers, (Customer c) => c.Name)

e o resultado é do tipo IEnumerable<string>.

O exemplo a seguir demonstra como a inferência de tipo de função anônima permite que as informações de tipo "fluam" entre argumentos em uma invocação de método genérico. Considerando-se o seguinte método e invocação:

class A
{
    static Z F<X,Y,Z>(X value, Func<X,Y> f1, Func<Y,Z> f2)
    {
        return f2(f1(value));
    }

    static void M()
    {
        double hours = F("1:15:30", s => TimeSpan.Parse(s), t => t.TotalHours);
    }
}

A inferência de tipo para a invocação prossegue da seguinte maneira: primeiro, o argumento "1:15:30" está relacionado ao parâmetro de valor, inferindo X ser string. Em seguida, o parâmetro da primeira função anônima, s, recebe o tipo inferido string e a expressão TimeSpan.Parse(s) está relacionada ao tipo de retorno de f1, inferindo-se Y a ser System.TimeSpan. Finalmente, o parâmetro da segunda função anônima, t, recebe o tipo inferido System.TimeSpan e a expressão t.TotalHours está relacionada ao tipo de retorno de f2, inferindo-se Z a ser double. Portanto, o resultado da invocação é do tipo double.

fim do exemplo

12.6.3.14 Inferência de tipo para conversão de grupos de métodos

Semelhante a chamadas de métodos genéricos, a inferência de tipo também deve ser aplicada quando um grupo de métodos M que contém um método genérico é convertido em um determinado tipo delegado D (§10.8). Dado um método

Tₑ M<X₁...Xᵥ>(T₁ x₁ ... Tₑ xₑ)

e o grupo de métodos M sendo atribuído ao tipo delegado D a tarefa de inferência de tipo é localizar argumentos de tipo S₁...Sᵥ para que a expressão:

M<S₁...Sᵥ>

torna-se compatível (§20.2) com D.

Ao contrário do algoritmo de inferência de tipo para chamadas de métodos genéricos, nesse caso, há apenas tipos de argumentos , nenhuma expressão de argumento . Em particular, não há funções anônimas e, portanto, não há necessidade de várias fases de inferência.

Em vez disso, todos Xᵢ são considerados não fixos e uma inferência de limite inferior é feita de cada tipo de argumento Uₑ de Dpara o tipo de parâmetro correspondente Tₑ de M. Se, para qualquer um dos Xᵢ, não forem encontrados limites, a inferência de tipos falhará. Caso contrário, todos os Xᵢ são fixos para Sᵢcorrespondentes, que são o resultado da inferência de tipo.

12.6.3.15 Localizando o melhor tipo comum de um conjunto de expressões

Em alguns casos, um tipo comum precisa ser inferido para um conjunto de expressões. Em particular, os tipos de elementos de matrizes tipadas implicitamente e os tipos de retorno de funções anônimas com corpos de bloco são encontrados dessa forma.

O melhor tipo comum para um conjunto de expressões E₁...Eᵥ é determinado da seguinte maneira:

  • Uma nova variável de tipo não fixa X é introduzida.
  • Para cada expressão Ei, é executada uma inferência de tipo de saída (§12.6.3.7) para X.
  • X é fixo (§12.6.3.12), se possível, e o tipo resultante é o melhor tipo comum.
  • Caso contrário, a inferência falhará.

Observação: intuitivamente, essa inferência é equivalente a chamar um método void M<X>(X x₁ ... X xᵥ) com Eᵢ como argumentos e inferir-se X. fim da observação

12.6.4 Resolução de sobrecarga

12.6.4.1 Geral

A resolução de sobrecarga é um mecanismo de tempo de associação para selecionar o melhor membro da função a ser invocado, dado um conjunto de membros de função candidatos e uma lista de argumentos. A resolução de sobrecarga seleciona o membro da função a ser invocado nos seguintes contextos distintos em C#:

  • Invocação de um método nomeado em uma invocation_expression (§12.8.10).
  • Invocação de um construtor de instância nomeado em uma object_creation_expression (§12.8.17.2).
  • Invocação de um acessador de índice por meio de um acesso_de_elemento (§12.8.12).
  • Invocação de um operador predefinido ou definido pelo usuário referenciado em uma expressão (§12.4.4 e §12.4.5).

Cada um desses contextos define o conjunto de membros da função candidata e a lista de argumentos à sua maneira exclusiva. Por exemplo, o conjunto de candidatos para uma invocação de método não inclui métodos marcados como substituição (§12.5), e métodos em uma classe base não são candidatos se qualquer método em uma classe derivada for aplicável (§12.8.10.2).

Depois que os membros da função candidata e a lista de argumentos tiverem sido identificados, a seleção do membro da melhor função será a mesma em todos os casos:

  • Primeiro, o conjunto de membros da função candidata é reduzido aos membros da função aplicáveis em relação à lista de argumentos determinada (§12.6.4.2). Se esse conjunto reduzido estiver vazio, ocorrerá um erro de tempo de compilação.
  • Em seguida, o melhor membro da função do conjunto de membros da função candidata aplicável é posicionado. Se o conjunto contiver apenas um membro de função, esse membro da função será o melhor membro da função. Caso contrário, o melhor membro da função é o membro de uma função que é melhor do que todos os outros membros da função em relação à lista de argumentos fornecida, desde que cada membro da função seja comparado a todos os outros membros da função usando-se as regras em §12.6.4.3. Se não houver exatamente um membro de uma função que seja melhor do que todos os outros membros da função, então a invocação do membro da função será ambígua e ocorrerá um erro de tempo de associação.

As subcláusulas a seguir definem os significados exatos dos termos membro da função aplicável e melhor membro de função.

12.6.4.2 Membro da função aplicável

Um membro da função é considerado um membro da função aplicável em relação a uma lista de argumentos A quando todos os seguintes itens são verdadeiros:

  • Cada argumento em A corresponde a um parâmetro na declaração do membro da função, conforme descrito em §12.6.2.2.2, no máximo um argumento corresponde a cada parâmetro e qualquer parâmetro ao qual nenhum argumento corresponde é um parâmetro opcional.
  • Para cada argumento em A, o modo de passagem de parâmetro do argumento é idêntico ao modo de passagem de parâmetro do parâmetro correspondente e
    • para um parâmetro de valor ou uma matriz de parâmetros, existe uma conversão implícita (§10.2) da expressão de argumento para o tipo do parâmetro correspondente ou
    • para um parâmetro de referência ou saída, há uma conversão de identidade entre o tipo da expressão de argumento (se houver) e o tipo do parâmetro correspondente, ou
    • para um parâmetro de entrada quando o argumento correspondente tem o modificador in, há uma conversão de identidade entre o tipo da expressão de argumento (se houver) e o tipo do parâmetro correspondente, ou
    • para um parâmetro de entrada quando o argumento correspondente omite o modificador in, existe uma conversão implícita (§10.2) da expressão de argumento para o tipo do parâmetro correspondente.

Para uma função membro que inclui uma matriz de parâmetros, se a função membro for aplicável pelas regras acima, ela será aplicável na sua forma normal . Se um membro de função que inclui uma matriz de parâmetros não for aplicável em sua forma normal, o membro da função poderá, em vez disso, ser aplicável em seu formato expandido:

  • O formato expandido é construído substituindo-se a matriz de parâmetros na declaração do membro da função por parâmetros de valor zero ou mais do tipo de elemento da matriz de parâmetros, de modo que o número de argumentos na lista de argumentos A corresponda ao número total de parâmetros. Se A tiver menos argumentos do que o número de parâmetros fixos na declaração do membro da função, a forma expandida do membro da função não poderá ser construída e, portanto, não será aplicável.
  • Caso contrário, o formulário expandido será aplicável se, para cada argumento em A, um dos seguintes é verdadeiro:
    • o modo de passagem de parâmetro do argumento é idêntico ao modo de passagem de parâmetro do parâmetro correspondente e:
      • para um parâmetro de valor fixo ou um parâmetro de valor criado pela expansão, existe uma conversão implícita (§10.2) da expressão de argumento para o tipo do parâmetro correspondente; ou
      • para um parâmetro por referência, o tipo da expressão de argumento é idêntico ao tipo do parâmetro correspondente.
    • o modo de passagem de parâmetro do argumento é valor, e o modo de passagem de parâmetro do parâmetro correspondente é entrada, e uma conversão implícita (§10.2) existe da expressão de argumento para o tipo do parâmetro correspondente.

Quando a conversão implícita do tipo de argumento para o tipo de parâmetro de um parâmetro de entrada é uma conversão implícita dinâmica (§10.2.10), os resultados são indefinidos.

Exemplo: dadas as seguintes declarações e chamadas de método:

public static void M1(int p1) { ... }
public static void M1(in int p1) { ... }
public static void M2(in int p1) { ... }
public static void Test()
{
    int i = 10; uint ui = 34U;

    M1(in i);   // M1(in int) is applicable
    M1(in ui);  // no exact type match, so M1(in int) is not applicable
    M1(i);      // M1(int) and M1(in int) are applicable
    M1(i + 5);  // M1(int) and M1(in int) are applicable
    M1(100u);   // no implicit conversion exists, so M1(int) is not applicable

    M2(in i);   // M2(in int) is applicable
    M2(i);      // M2(in int) is applicable
    M2(i + 5);  // M2(in int) is applicable
}

fim do exemplo

  • Um método estático só será aplicável se o grupo de métodos resultar de um simple_name ou de um member_access por um tipo.
  • Um método de instância só será aplicável se o grupo de métodos resultar de um simple_name, um member_access por meio de uma variável ou valor ou um base_access.
    • Se o grupo de métodos vier de um simple_name, um método de instância só será aplicável se o acesso this for permitido §12.8.14.
  • Quando o grupo de métodos resulta de member_access que pode ser por meio de uma instância ou de um tipo, conforme descrito em §12.8.7.2, os métodos estáticos e de instância são aplicáveis.
  • Um método genérico cujos argumentos de tipo (explicitamente especificados ou inferidos) não atendem a suas restrições não é aplicável.
  • No contexto de uma conversão de grupo de métodos, haverá uma conversão de identidade (§10.2.2) ou uma conversão de referência implícita (§10.2.8) do tipo de retorno do método para o tipo de retorno de delegado. Caso contrário, o método candidato não será aplicável.

12.6.4.3 Membro de função melhor

Para determinar o membro de função melhor, uma lista de argumentos simplificada A é construída contendo apenas as próprias expressões de argumento na ordem em que aparecem na lista de argumentos originais e deixando de fora quaisquer argumentos out ou ref.

As listas de parâmetros para cada membro da função candidata são construídas da seguinte maneira:

  • O formato expandido será usado se o membro da função for aplicável somente no formato expandido.
  • Parâmetros opcionais sem argumentos correspondentes são removidos da lista de parâmetros
  • Parâmetros de referência e saída são removidos da lista de parâmetros
  • Os parâmetros são reordenados para que ocorram na mesma posição que o argumento correspondente na lista de argumentos.

Dada uma lista de argumentos A com um conjunto de expressões de argumento {E₁, E₂, ..., Eᵥ} e dois membros de função aplicáveis Mᵥ e Mₓ com tipos de parâmetro {P₁, P₂, ..., Pᵥ} e {Q₁, Q₂, ..., Qᵥ}, Mᵥ é definido como um membro de função melhor do que Mₓ se

  • para cada argumento, a conversão implícita de Eᵥ para Qᵥ não é melhor do que a conversão implícita de Eᵥ para Pᵥ e
  • para pelo menos um argumento, a conversão de Eᵥ para Pᵥ é melhor do que a conversão de Eᵥ para Qᵥ.

Caso as sequências de tipo de parâmetro {P₁, P₂, ..., Pᵥ} e {Q₁, Q₂, ..., Qᵥ} sejam equivalentes (ou seja, cada Pᵢ tem uma conversão de identidade para o Qᵢcorrespondente), as seguintes regras de desempate são aplicadas, em ordem, para determinar o membro de função melhor.

  • Se Mᵢ for um método não genérico e Mₑ for um método genérico, Mᵢ será melhor que Mₑ.
  • Caso contrário, se Mᵢ for aplicável em sua forma normal e Mₑ tiver uma matriz de parâmetros e for aplicável somente em sua forma expandida, Mᵢ será melhor que Mₑ.
  • Caso contrário, se ambos os métodos tiverem matrizes de parâmetros e forem aplicáveis somente em seus formatos expandidos e se a matriz de parâmetros de Mᵢ tiver menos elementos do que a matriz de params de Mₑ, Mᵢ será melhor do que Mₑ.
  • Caso contrário, se Mᵥ tiver tipos de parâmetro mais específicos do que Mₓ, Mᵥ será melhor que Mₓ. {R1, R2, ..., Rn} e {S1, S2, ..., Sn} representam os tipos de parâmetros não instanciados e não expandidos de Mᵥ e Mₓ. Os tipos de parâmetro de Mᵥsão mais específicos do que os de Mₓse, para cada parâmetro, Rx não for menos específico do que Sx, e, para pelo menos um parâmetro, Rx for mais específico do que Sx:
    • Um parâmetro de tipo é menos específico do que um parâmetro não tipo.
    • Recursivamente, um tipo construído é mais específico do que outro tipo construído (com o mesmo número de argumentos de tipo) se pelo menos um argumento de tipo for mais específico e nenhum argumento de tipo for menos específico do que o argumento de tipo correspondente no outro.
    • Um tipo de matriz é mais específico do que outro tipo de matriz (com o mesmo número de dimensões) se o tipo de elemento do primeiro for mais específico do que o tipo de elemento do segundo.
  • Caso contrário, se um membro for um operador não elevado e o outro for um operador elevado, o não elevado será melhor.
  • Se nenhum membro da função tiver sido considerado melhor e todos os parâmetros de Mᵥ tiverem um argumento correspondente, enquanto os argumentos padrão precisarão ser substituídos por pelo menos um parâmetro opcional em Mₓ, então Mᵥ será melhor do que Mₓ.
  • Se para pelo menos um parâmetro Mᵥ usa a escolha de passagem de parâmetro melhor (§12.6.4.4) do que o parâmetro correspondente em Mₓ e nenhum dos parâmetros em Mₓ usa uma escolha de passagem de parâmetro melhor do que Mᵥ, Mᵥ é melhor do que Mₓ.
  • Caso contrário, nenhum membro de função é melhor.

12.6.4.4 Melhor modo de passagem de parâmetro

É permitido que os parâmetros correspondentes em dois métodos sobrecarregados diferem apenas pelo modo de passagem de parâmetro, desde que um dos dois parâmetros tenha o modo de passagem de valor, da seguinte maneira:

public static void M1(int p1) { ... }
public static void M1(in int p1) { ... }

Considerando-se int i = 10;, de acordo com §12.6.4.2, as chamadas M1(i) e M1(i + 5) resultam nas duas sobrecargas sendo aplicáveis. Nesses casos, o método com o modo de passagem de parâmetro por valor é a melhor escolha de modo de passagem de parâmetro.

Observação: não existe essa necessidade de opções para argumentos de entrada, saída ou modos de passagem de referência, pois esses argumentos correspondem apenas aos mesmos modos de passagem de parâmetro. fim da observação

12.6.4.5 Melhor conversão da expressão

Considerando uma conversão implícita C₁ que converte de uma expressão E para um tipo T₁, e uma conversão implícita C₂ que converte de uma expressão E para um tipo T₂, C₁ é uma conversão melhor do que C₂ se uma das seguintes condições for verdadeira:

  • E corresponde exatamente T₁ e E não corresponde exatamente T₂ (§12.6.4.6)
  • E corresponde exatamente a ambos ou nenhum de T₁ e T₂, e T₁ é um destino de conversão melhor do que T₂ (§12.6.4.7)
  • E é um grupo de métodos (§12.2), T₁ é compatível (§20.4) com o melhor método do grupo de métodos para conversão C₁ e T₂ não é compatível com o método mais simples do grupo de métodos para conversão C₂

12.6.4.6 Expressão exatamente correspondente

Considerando-se uma expressão E e um tipo T, Ecorresponde exatamente aT se uma das seguintes condições for:

  • E tem um tipo S e existe uma conversão de identidade de S para T
  • E é uma função anônima, T é um tipo delegate D ou um tipo de árvore de expressão Expression<D>, conforme uma das seguintes condições:
    • Existe um tipo de retorno inferido, X, para E no contexto da lista de parâmetros de D (§12.6.3.12), e existe uma conversão de identidade de X para o tipo de retorno de D
    • E é um lambda async sem valor retornado e D tem um tipo de retorno que é um «TaskType» não genérico
    • Ou E é não síncrono e D tem um tipo de retorno Y ou E é assíncrono e D tem um tipo de retorno «TaskType»<Y>(§15.14.1), e uma das seguintes condições se aplica:
      • O corpo de E é uma expressão que corresponde exatamente a Y
      • O corpo de E é um bloco em que cada instrução de retorno retorna uma expressão que corresponde exatamente a Y

12.6.4.7 Melhor alvo de conversão

Considerando dois tipos T₁ e T₂, T₁ é um alvo de conversão melhor do que T₂ se uma das seguintes condições for atendida:

  • Existe uma conversão implícita de T₁ para T₂ e não existe nenhuma conversão implícita de T₂ para T₁
  • T₁ é «TaskType»<S₁>(§15.14.1), T₂ é «TaskType»<S₂>e S₁ é um destino de conversão melhor do que S₂
  • T₁ é «TaskType»<S₁>(§15.14.1), T₂ é «TaskType»<S₂>, e T₁ é mais especializado do que T₂
  • T₁ é S₁ ou S₁? em que S₁ é um tipo integral assinado e T₂ é S₂ ou S₂? em que S₂ é um tipo integral sem sinal. Especificamente:
    • S₁ é sbyte e S₂ é byte, ushort, uint ou ulong
    • S₁ é short e S₂ é ushort, uint ou ulong
    • S₁ é int e S₂ é uint ou ulong.
    • S₁ é long e S₂ é ulong.

12.6.4.8 Sobrecarga em classes genéricas

Observação: embora as assinaturas declaradas sejam exclusivas (§8.6), é possível que a substituição de argumentos de tipo resulte em assinaturas idênticas. Em tal situação, a resolução de sobrecarga escolherá a assinatura original mais específica (§12.6.4.3) antes da substituição dos argumentos de tipo, caso exista; caso contrário, relatará um erro. fim da observação

Exemplo: os exemplos a seguir mostram sobrecargas válidas e inválidas de acordo com esta regra:

public interface I1<T> { ... }
public interface I2<T> { ... }

public abstract class G1<U>
{
    public abstract int F1(U u);           // Overload resolution for G<int>.F1
    public abstract int F1(int i);         // will pick non-generic

    public abstract void F2(I1<U> a);      // Valid overload
    public abstract void F2(I2<U> a);
}

abstract class G2<U,V>
{
    public abstract void F3(U u, V v);     // Valid, but overload resolution for
    public abstract void F3(V v, U u);     // G2<int,int>.F3 will fail

    public abstract void F4(U u, I1<V> v); // Valid, but overload resolution for
    public abstract void F4(I1<V> v, U u); // G2<I1<int>,int>.F4 will fail

    public abstract void F5(U u1, I1<V> v2);   // Valid overload
    public abstract void F5(V v1, U u2);

    public abstract void F6(ref U u);      // Valid overload
    public abstract void F6(out V v);
}

fim do exemplo

12.6.5 Verificação de tempo de compilação da invocação de membro dinâmico

Embora a resolução de sobrecarga de uma operação dinamicamente associada ocorra em run-time, às vezes é possível, em tempo de compilação, conhecer a lista de membros da função da qual uma sobrecarga será escolhida:

  • Para uma invocação de delegado (§12.8.10.4), a lista é um membro de função única com a mesma lista de parâmetros que o delegate_type da invocação
  • Para uma invocação de método (§12.8.10.2) em um tipo ou em um valor cujo tipo estático não é dinâmico, o conjunto de métodos acessíveis no grupo de métodos é conhecido em tempo de compilação.
  • Para uma expressão de criação de objeto (§12.8.17.2) o conjunto de construtores acessíveis no tipo é conhecido em tempo de compilação.
  • Para um acesso de indexador (§12.8.12.3), o conjunto de indexadores acessíveis no receptor é conhecido em tempo de compilação.

Nesses casos, é realizada uma verificação limitada em tempo de compilação em cada membro do conjunto conhecido de membros de função, para determinar se é possível afirmar com certeza que nunca serão invocados em tempo de execução. Para cada membro da função F um parâmetro modificado e uma lista de argumentos são construídos:

  • Primeiro, se F for um método genérico e argumentos de tipo forem fornecidos, eles serão substituídos pelos parâmetros de tipo na lista de parâmetros. No entanto, se argumentos de tipo não foram fornecidos, nenhuma substituição desse tipo ocorrerá.
  • Em seguida, qualquer parâmetro cujo tipo está aberto (ou seja, contém um parâmetro de tipo; consulte §8.4.3) é ignorado, juntamente com seus argumentos correspondentes.

Para F passar na verificação, todas as seguintes condições devem ser atendidas:

  • A lista de parâmetros modificada para F é aplicável à lista de argumentos modificados em termos de §12.6.4.2.
  • Todos os tipos construídos na lista de parâmetros modificados atendem às suas restrições (§8.4.5).
  • Se os parâmetros de tipo de F foram substituídos na etapa acima, suas restrições serão atendidas.
  • Se F for um método estático, o grupo de métodos não deverá ter resultado de um member_access cujo receptor é conhecido em tempo de compilação como uma variável ou valor.
  • Se F for um método de instância, o grupo de métodos não deverá ter resultado de um member_access cujo receptor é conhecido em tempo de compilação como um tipo.

Se nenhum candidato passar neste teste, ocorrerá um erro de tempo de compilação.

12.6.6 Invocação de membro da função

12.6.6.1 Geral

Essa subcláusula descreve o processo que ocorre em run-time para invocar um membro de função específico. Supõe-se que um processo de tempo de associação já determinou o membro específico a ser invocado, possivelmente aplicando a resolução de sobrecarga a um conjunto de membros de funções candidatos.

Para fins de descrever o processo de invocação, os membros da função são divididos em duas categorias:

  • Membros de função estática. Esses são métodos estáticos, acessadores de propriedade estáticos e operadores definidos pelo usuário. Membros de função estática são sempre não virtuais.
  • Membros de função de instância. Estes são métodos de instância, construtores de instância, acessadores de propriedade de instância e acessadores de indexador. Os membros da função de instância são não virtuais ou virtuais e são sempre invocados em uma instância específica. A instância é computada por uma expressão de instância e torna-se acessível dentro do membro da função como this (§12.8.14). Para um construtor de instância, a expressão de instância é considerada o objeto recém-alocado.

O processamento em run-time de uma invocação de membro de função consiste nas seguintes etapas, em que M é o membro da função e, se M for um membro da instância, E é a expressão de instância:

  • Se M for um membro de função estático:

    • A lista de argumentos é avaliada conforme descrito em §12.6.2.
    • M é invocado.
  • Caso contrário, se E for um tipo de valor Ve M for declarado ou substituído em V:

    • E é avaliado. Se essa avaliação causar uma exceção, nenhuma etapa adicional será executada. Para um construtor de instância, essa avaliação consiste em alocar armazenamento (normalmente de uma pilha de execução) para o novo objeto. Nesse caso, E é classificado como uma variável.
    • Se E não for classificado como uma variável ou se V não for um tipo de struct readonly (§16.2.2), e E será um dos seguintes:
      • um parâmetro de entrada (§15.6.2.3.2), ou
      • um campo readonly (§15.5.3), ou
      • uma variável de referência readonly ou valor de retorno (§9,7)

    em seguida, uma variável local temporária do tipo de E é criada e o valor de E é atribuído a essa variável. E é reclassificada como uma referência a essa variável local temporária. A variável temporária é acessível como this dentro de M, mas não de outra forma. Portanto, somente quando E pode ser gravado é possível que o chamador observe as alterações que M faz para this.

    • A lista de argumentos é avaliada conforme descrito em §12.6.2.
    • M é invocado. A variável referenciada por E torna-se a variável referenciada por this.
  • Ou:

    • E é avaliado. Se essa avaliação causar uma exceção, nenhuma etapa adicional será executada.
    • A lista de argumentos é avaliada conforme descrito em §12.6.2.
    • Se o tipo de E for um value_type, uma conversão boxing (§10.2.9) será executada para converter E em um class_type e E será considerado como sendo desse class_type nas etapas a seguir. Se o value_type for um enum_type, o class_type será System.Enum;, caso contrário, será System.ValueType.
    • O valor de E é verificado como sendo válido. Se o valor de E for nulo, System.NullReferenceException será lançado e nenhuma etapa adicional será executada.
    • A implementação do membro da função a ser invocada é determinada:
      • Se o tipo de tempo de associação de E for uma interface, o membro da função a ser invocado será a implementação de M fornecida pelo tipo de run-time da instância referenciada por E. Esse membro da função é determinado aplicando as regras de mapeamento de interface (§18.6.5) para determinar a implementação de M fornecida pelo tipo de tempo de execução da instância referenciada por E.
      • Caso contrário, se M for um membro de função virtual, o membro da função a ser invocado será a implementação de M fornecida pelo tipo de run-time da instância referenciada por E. Esse membro da função é determinado aplicando as regras para determinar a implementação mais derivada (§15.6.4) de M em relação ao tipo de run-time da instância referenciada por E.
      • Caso contrário, M é um membro de função não virtual e o membro da função a ser invocado é M si mesmo.
    • A implementação do membro da função determinada na etapa acima é invocada. O objeto referenciado por E torna-se o objeto referenciado por isso.

O resultado da invocação de um construtor de instância (§12.8.17.2) é o valor criado. O resultado da invocação de qualquer outro membro de função é o valor retornado, se houver, a partir do corpo (§13.10.5).

12.6.6.2 Invocações em instâncias boxed

Um membro da função implementado em um value_type pode ser invocado por meio de uma instância boxed desse value_type nas seguintes situações:

  • Quando o membro da função é uma substituição de um método herdado do tipo class_type e é invocado por meio de uma expressão de instância desse class_type.

    Observação: class_type sempre será um dos System.Object, System.ValueType ou System.Enumfim da observação

  • Quando o membro da função é uma implementação de um membro da função de interface e é invocado por meio de uma expressão de instância de um interface_type.
  • Quando o membro da função é invocado por meio de um delegado.

Nessas situações, a instância boxed é considerada como contendo uma variável do value_type, e essa variável se torna a variável referenciada por esta durante a invocação do membro da função.

Observação: em particular, isso significa que, quando um membro da função é invocado em uma instância em caixa, é possível que o membro da função modifique o valor contido na instância em caixa. fim da observação

12.7 Desconstrução

A desconstrução é um processo pelo qual uma expressão é transformada em uma tupla de expressões individuais. A desconstrução é usada quando o destino de uma atribuição simples é uma expressão de tupla, a fim de obter valores a serem atribuídos a cada um dos elementos dessa tupla.

Uma expressão E é desconstruída em uma expressão de tupla com elementos n da seguinte maneira:

  • Se E for uma expressão de tupla com n elementos, o resultado da desconstrução é a própria expressão E.
  • Caso contrário, se E tiver um tipo de tupla (T1, ..., Tn) com elementos n, E será avaliado em uma variável temporária __v e o resultado da desconstrução será a expressão (__v.Item1, ..., __v.Itemn).
  • Caso contrário, se a expressão E.Deconstruct(out var __v1, ..., out var __vn) for resolvida em tempo de compilação para uma instância exclusiva ou método de extensão, essa expressão será avaliada e o resultado da desconstrução será a expressão (__v1, ..., __vn). Esse método é chamado de desconstrutor.
  • Caso contrário, E não poderá ser desconstruído.

Aqui, __v e __v1, ..., __vn referem-se a variáveis temporárias invisíveis e inacessíveis.

Observação: uma expressão do tipo dynamic não pode ser desconstruída. fim da observação

12.8 Expressões primárias

12.8.1 Geral

As expressões primárias incluem as formas mais simples de expressões.

primary_expression
    : literal
    | interpolated_string_expression
    | simple_name
    | parenthesized_expression
    | tuple_expression
    | member_access
    | null_conditional_member_access
    | invocation_expression
    | element_access
    | null_conditional_element_access
    | this_access
    | base_access
    | post_increment_expression
    | post_decrement_expression
    | null_forgiving_expression
    | array_creation_expression
    | object_creation_expression
    | delegate_creation_expression
    | anonymous_object_creation_expression
    | typeof_expression
    | sizeof_expression
    | checked_expression
    | unchecked_expression
    | default_value_expression
    | nameof_expression    
    | anonymous_method_expression
    | pointer_member_access     // unsafe code support
    | pointer_element_access    // unsafe code support
    | stackalloc_expression
    ;

Observação: essa regra gramatical não está pronta para ANTLR, pois faz parte de um conjunto de regras mutuamente recursivas à esquerda (primary_expression, , member_access, invocation_expression, element_accesspost_increment_expression, , post_decrement_expressione null_forgiving_expressionpointer_member_accesspointer_element_access) que a ANTLR não manipula. Técnicas padrão podem ser usadas para transformar a gramática para remover a recursão mútua à esquerda. Esse Padrão não faz isso, pois nem todas as estratégias de análise exigem isso (por exemplo, um analisador LALR não faria) e isso ofuscaria a estrutura e a descrição. fim da observação

pointer_member_access (§23.6.3) e pointer_element_access (§23.6.4) só estão disponíveis em código não seguro (§23).

12.8.2 Literais

Uma primary_expression que consiste em um literal (§6.4.5) é classificada como um valor.

12.8.3 Expressões de cadeia de caracteres interpoladas

Uma interpolated_string_expression consiste em $, $@ou @$, imediatamente seguido pelo texto em caracteres ". No texto entre aspas, há zero ou mais interpolações delimitadas por caracteres { e }, cada uma delas inclui uma expressão e especificações opcionais de formatação.

Expressões de cadeia de caracteres interpoladas têm dois formatos; regular (interpolated_regular_string_expression) e verbatim (interpolated_verbatim_string_expression); que são lexicamente semelhantes, mas diferem semanticamente das duas formas de literais de cadeia de caracteres (§6.4.5.6).

interpolated_string_expression
    : interpolated_regular_string_expression
    | interpolated_verbatim_string_expression
    ;

// interpolated regular string expressions

interpolated_regular_string_expression
    : Interpolated_Regular_String_Start Interpolated_Regular_String_Mid?
      ('{' regular_interpolation '}' Interpolated_Regular_String_Mid?)*
      Interpolated_Regular_String_End
    ;

regular_interpolation
    : expression (',' interpolation_minimum_width)?
      Regular_Interpolation_Format?
    ;

interpolation_minimum_width
    : constant_expression
    ;

Interpolated_Regular_String_Start
    : '$"'
    ;

// the following three lexical rules are context sensitive, see details below

Interpolated_Regular_String_Mid
    : Interpolated_Regular_String_Element+
    ;

Regular_Interpolation_Format
    : ':' Interpolated_Regular_String_Element+
    ;

Interpolated_Regular_String_End
    : '"'
    ;

fragment Interpolated_Regular_String_Element
    : Interpolated_Regular_String_Character
    | Simple_Escape_Sequence
    | Hexadecimal_Escape_Sequence
    | Unicode_Escape_Sequence
    | Open_Brace_Escape_Sequence
    | Close_Brace_Escape_Sequence
    ;

fragment Interpolated_Regular_String_Character
    // Any character except " (U+0022), \\ (U+005C),
    // { (U+007B), } (U+007D), and New_Line_Character.
    : ~["\\{}\u000D\u000A\u0085\u2028\u2029]
    ;

// interpolated verbatim string expressions

interpolated_verbatim_string_expression
    : Interpolated_Verbatim_String_Start Interpolated_Verbatim_String_Mid?
      ('{' verbatim_interpolation '}' Interpolated_Verbatim_String_Mid?)*
      Interpolated_Verbatim_String_End
    ;

verbatim_interpolation
    : expression (',' interpolation_minimum_width)?
      Verbatim_Interpolation_Format?
    ;

Interpolated_Verbatim_String_Start
    : '$@"'
    | '@$"'
    ;

// the following three lexical rules are context sensitive, see details below

Interpolated_Verbatim_String_Mid
    : Interpolated_Verbatim_String_Element+
    ;

Verbatim_Interpolation_Format
    : ':' Interpolated_Verbatim_String_Element+
    ;

Interpolated_Verbatim_String_End
    : '"'
    ;

fragment Interpolated_Verbatim_String_Element
    : Interpolated_Verbatim_String_Character
    | Quote_Escape_Sequence
    | Open_Brace_Escape_Sequence
    | Close_Brace_Escape_Sequence
    ;

fragment Interpolated_Verbatim_String_Character
    : ~["{}]    // Any character except " (U+0022), { (U+007B) and } (U+007D)
    ;

// lexical fragments used by both regular and verbatim interpolated strings

fragment Open_Brace_Escape_Sequence
    : '{{'
    ;

fragment Close_Brace_Escape_Sequence
    : '}}'
    ;

Seis das regras léxicas definidas acima são contextuais da seguinte maneira:

Regra Requisitos contextuais
Interpolated_Regular_String_Mid Reconhecido somente após Interpolated_Regular_String_Start, entre quaisquer interpolações subsequentes e antes do Interpolated_Regular_String_Endcorrespondente.
Regular_Interpolation_Format Reconhecido somente em regular_interpolation e quando os dois-pontos iniciais (:) não estão aninhados em nenhum tipo de parênteses, chaves ou colchetes.
Interpolated_Regular_String_End Reconhecido somente após um Interpolated_Regular_String_Start e apenas se algum token interveniente for Interpolated_Regular_String_Mid ou tokens que possam fazer parte de regular_interpolation, incluindo tokens para qualquer interpolated_regular_string_expression contida nessas interpolações.
Interpolated_Verbatim_String_MidVerbatim_Interpolation_FormatInterpolated_Verbatim_String_End O reconhecimento dessas três regras segue o das regras correspondentes acima com cada mencionado regra gramatical regular substituída pelo texto correspondente.

Observação: as regras acima são sensíveis ao contexto, pois suas definições se sobrepõem às de outros tokens no idioma. fim da observação

Observação: a gramática acima não está pronta para ANTLR devido às regras de léxicos sensíveis ao contexto. Assim como acontece com outros geradores lexer, o ANTLR dá suporte a regras léxicos sensíveis ao contexto, por exemplo, usando seus modos léxicos , mas esse é um detalhe de implementação e, portanto, não faz parte dessa especificação. fim da observação

interpolated_string_expression é classificado como um valor. Se for convertido imediatamente em System.IFormattable ou System.FormattableString com uma conversão de cadeia de caracteres interpolada implícita (§10.2.5), a expressão de cadeia de caracteres interpolada terá esse tipo. Caso contrário, tem o tipo string.

Observação: as diferenças entre os tipos possíveis de uma interpolated_string_expression podem ser determinadas na documentação para System.String (§C.2) e System.FormattableString (§C.3). fim da observação

O significado de uma interpolação, regular_interpolation e verbatim_interpolation, é formatar o valor da expressão como um string de acordo com o formato especificado pelo Regular_Interpolation_Format ou Verbatim_Interpolation_Formatou de acordo com um formato padrão para o tipo de expressão. A string formatada é modificada pelo interpolation_minimum_width, se houver, para produzir o string final a ser interpolado na interpolated_string_expression.

Observação: como o formato padrão de um tipo é determinado é detalhado na documentação de System.String (§C.2) e System.FormattableString (§C.3). Descrições de formatos padrão, que são idênticas para Regular_Interpolation_Format e Verbatim_Interpolation_Format, podem ser encontradas na documentação do System.IFormattable (§C.4) e em outros tipos na biblioteca padrão (§C). fim da observação

Em um interpolation_minimum_width, a constant_expression deve ter uma conversão implícita para int. Seja a largura do campo o valor absoluto desta expressão_constante, e o alinhamento seja o sinal (positivo ou negativo) do valor desta expressão_constante:

  • Se o valor da largura do campo for menor ou igual ao tamanho da cadeia de caracteres formatada, a cadeia de caracteres formatada não será modificada.
  • Caso contrário, a cadeia de caracteres formatada é preenchida com caracteres de espaço em branco para que seu comprimento seja igual à largura do campo:
    • Se o alinhamento for positivo, a cadeia de caracteres formatada estará alinhada à direita adicionando o preenchimento.
    • Caso contrário, ele é alinhado à esquerda adicionando preenchimento.

O significado geral de uma interpolated_string_expression, incluindo a formatação e o preenchimento acima de interpolações, é definido por uma conversão da expressão em uma invocação de método: se o tipo da expressão é System.IFormattable ou System.FormattableString esse método é System.Runtime.CompilerServices.FormattableStringFactory.Create (§C.3) que retorna um valor do tipo System.FormattableString; caso contrário, o tipo deve ser string e o método é string.Format (§C.2) que retorna um valor do tipo string.

Em ambos os casos, a lista de argumentos da chamada consiste em um literal de caracteres de formato com especificações de formato para cada interpolação, e um argumento a cada expressão correspondente para as especificações de formato.

O literal da cadeia de caracteres de formato é construído da seguinte maneira, em que N é o número de interpolações na interpolated_string_expression. O literal de cadeia de caracteres de formato consiste em, em ordem:

  • Os caracteres de Interpolated_Regular_String_Start ou Interpolated_Verbatim_String_Start
  • Os caracteres de Interpolated_Regular_String_Mid ou Interpolated_Verbatim_String_Mid se houver
  • Então se N ≥ 1 para cada número I de 0 a N-1:
    • Uma especificação de espaço reservado:
      • Um caractere de chave esquerda ({)
      • A representação decimal de I
      • Então, se regular_interpolation ou verbatim_interpolation correspondente tiver interpolation_minimum_width, será usada uma vírgula (,) seguida pela representação decimal do valor de constant_expression
      • Os caracteres de Regular_Interpolation_Format ou Verbatim_Interpolation_Format, se houver, correspondentes a regular_interpolation ou verbatim_interpolation
      • Um caractere de chave direita (})
    • Os caracteres de Interpolated_Regular_String_Mid ou Interpolated_Verbatim_String_Mid imediatamente após a interpolação correspondente, se houver
  • Os caracteres de Interpolated_Regular_String_End ou Interpolated_Verbatim_String_End.

Os argumentos subsequentes são expressões expressões das interpolações, se houver, em ordem.

Quando interpolated_string_expression contém várias interpolações, as expressões nessas interpolações são avaliadas em ordem textual da esquerda para a direita.

Exemplo:

Este exemplo usa os seguintes recursos de especificação de formato:

  • a especificação de formato X que formata inteiros como hexadecimal maiúsculo,
  • o formato padrão de um valor string é o próprio valor,
  • valores de alinhamento positivos que são alinhados à direita dentro da largura de campo mínima especificada,
  • valores de alinhamento negativos que são alinhados à esquerda dentro da largura de campo mínima especificada,
  • constantes definidas para interpolation_minimum_width e
  • que {{ e }} são formatados como { e }, respectivamente.

Considerando:

string text = "red";
int number = 14;
const int width = -4;

Em seguida:

Expressão de cadeia de caracteres interpolada Significado equivalente a string Valor
$"{text}" string.Format("{0}", text) "red"
$"{{text}}" string.Format("{{text}}) "{text}"
$"{ text , 4 }" string.Format("{0,4}", text) " red"
$"{ text , width }" string.Format("{0,-4}", text) "red "
$"{number:X}" string.Format("{0:X}", number) "E"
$"{text + '?'} {number % 3}" string.Format("{0} {1}", text + '?', number % 3) "red? 2"
$"{text + $"[{number}]"}" string.Format("{0}", text + string.Format("[{0}]", number)) "red[14]"
$"{(number==0?"Zero":"Non-zero")}" string.Format("{0}", (number==0?"Zero":"Non-zero")) "Non-zero"

fim do exemplo

12.8.4 Nomes simples

simple_name consiste em um identificador, opcionalmente seguido por uma lista de argumentos de tipo:

simple_name
    : identifier type_argument_list?
    ;

Um simple_name é um das formas I ou I<A₁, ..., Aₑ>, em que I é um único identificador e I<A₁, ..., Aₑ> é um type_argument_list opcional. Quando type_argument_list não é especificado, considere que e é zero. simple_name é avaliado e classificado da seguinte maneira:

  • Se e for zero e simple_name aparecer dentro de um espaço de declaração de variável local (§7.3) que contenha diretamente uma variável local, parâmetro ou constante com o nome I, simple_name se referirá a essa variável local, parâmetro ou constante e será classificado como uma variável ou valor.
  • Se e for zero e simple_name aparecer dentro de uma declaração de método genérico, mas fora dos atributos de seu method_declaration, e se essa declaração incluir um parâmetro de tipo com o nome I, simple_name se referirá a esse parâmetro de tipo.
  • Caso contrário, para cada tipo de instância T (§15.3.2), começando com o tipo de instância da declaração de tipo imediatamente adjacente e continuando com o tipo de instância de cada declaração de classe ou struct envolvente (se houver):
    • Se e for zero e a declaração de T incluir um parâmetro de tipo com o nome I, simple_name se referirá a esse parâmetro de tipo.
    • Caso contrário, se uma pesquisa de membro (§12.5) de I em T com argumentos de tipo e produz uma correspondência:
      • Se T for o tipo de instância da classe ou estrutura que o envolve imediatamente e a pesquisa identificar um ou mais métodos, o resultado será um grupo de métodos com uma expressão de instância associada a this. Se uma lista de argumentos de tipo tiver sido especificada, ela será usada na chamada de um método genérico (§12.8.10.2).
      • Caso contrário, se T for o tipo de instância da classe ou estrutura imediatamente contida, se a pesquisa identificar um membro de instância e a referência ocorrer dentro do bloco de um construtor de instância, um método de instância ou um acessador de instância (§12.2.1), o resultado será o mesmo que um acesso de membro (§12.8.7) do formato this.I. Isso só pode acontecer quando e for zero.
      • Caso contrário, o resultado será o mesmo que um acesso a membro (§12.8.7) da forma T.I ou T.I<A₁, ..., Aₑ>.
  • Caso contrário, para cada namespace N, começando com o namespace no qual o simple_name ocorre, continuando com cada namespace delimitado (se houver) e terminando com o namespace global, as seguintes etapas são avaliadas até que uma entidade esteja localizada:
    • Se e for zero e I for o nome de um namespace no N, então:
      • Se o local em que simple_name ocorre estiver delimitado pela declaração de namespace para N e declaração de namespace contiver um extern_alias_directive ou using_alias_directive que associe o nome I com um namespace ou tipo, então simple_name será ambíguo e ocorrerá um erro de tempo de compilação.
      • Caso contrário, simple_name refere-se ao namespace chamado I em N.
    • Caso contrário, se N contiver um tipo acessível com o nome I e parâmetros de tipo e, então:
      • Se e for zero e o local onde o simple_name ocorrer estiver delimitado por uma declaração de namespace para N e a declaração de namespace contiver um extern_alias_directive ou using_alias_directive que associe o nome I a um namespace ou tipo, o simple_name será ambíguo e ocorrerá um erro de tempo de compilação.
      • Caso contrário, namespace_or_type_name refere-se ao tipo construído com os argumentos de tipo fornecidos.
    • Caso contrário, se o local em que o simple_name ocorre estiver dentro de uma declaração de namespace para N:
      • Se e for zero e a declaração de namespace contiver extern_alias_directive ou using_alias_directive que associe o nome I a um namespace ou tipo importado, simple_name se referirá a esse namespace ou tipo.
      • Caso contrário, se os namespaces importados pelos using_namespace_directives da declaração de namespace contiverem exatamente um tipo de nome com parâmetros de tipo I e e, o simple_name se referirá a esse tipo construído com os argumentos de tipo fornecidos.
      • Caso contrário, se os namespaces importados pelos using_namespace_directives da declaração de namespace contiverem mais de um tipo com nome I e parâmetros de tipo e, o simple_name será ambíguo e um erro de tempo de compilação ocorrerá.

    Observação: esta etapa inteira é exatamente paralela à etapa correspondente no processamento de namespace_or_type_name (§7.8). fim da observação

  • Caso contrário, se e for zero e I for o identificador _, o simple_name será um descarte simples, que é um formato de expressão de declaração (§12.17).
  • Caso contrário, simple_name será indefinido e ocorrerá um erro de tempo de compilação.

12.8.5 Expressões-entre parênteses

Uma expressão com parênteses consiste em uma expressão entre parênteses.

parenthesized_expression
    : '(' expression ')'
    ;

Uma parenthesized_expression é avaliada avaliando a expressão dentro dos parênteses. Se a expressão entre parênteses denotar um namespace ou tipo, ocorrerá um erro de tempo de compilação. Caso contrário, o resultado da parenthesized_expression é o resultado da avaliação da expressão contida.

12.8.6 Expressões de tupla

Uma tuple_expression representa uma tupla e consiste em duas ou mais expressões separadas por vírgulas e, opcionalmente, nomeadas entre parênteses. Uma deconstruction_expression é uma sintaxe abreviada para uma tupla que contém expressões de declaração implicitamente tipadas.

tuple_expression
    : '(' tuple_element (',' tuple_element)+ ')'
    | deconstruction_expression
    ;
    
tuple_element
    : (identifier ':')? expression
    ;
    
deconstruction_expression
    : 'var' deconstruction_tuple
    ;
    
deconstruction_tuple
    : '(' deconstruction_element (',' deconstruction_element)+ ')'
    ;

deconstruction_element
    : deconstruction_tuple
    | identifier
    ;

Uma tuple_expression é classificada como uma tupla.

Uma deconstruction_expressionvar (e1, ..., en) é um formato abreviado de tuple_expression(var e1, ..., var en) e segue o mesmo comportamento. Isso se aplica recursivamente a qualquer deconstruction_tuple aninhada na deconstruction_expression. Cada identificador aninhado em uma deconstruction_expression, portanto, introduz uma expressão de declaração (§12.17). Como resultado, uma deconstruction_expression só pode ocorrer no lado esquerdo de uma atribuição simples.

Uma expressão de tupla terá um tipo se e somente se cada expressão de elemento Ei tiver um tipo Ti. O tipo deve ser um tipo de tupla da mesma aridade que a expressão de tupla, em que cada elemento é fornecido pelo seguinte:

  • Se o elemento de tupla na posição correspondente tiver um nome Ni, o elemento de tipo de tupla deverá ser Ti Ni.
  • Caso contrário, se Ei for do formato Ni, E.Ni ou E?.Ni, o elemento de tipo de tupla deverá ser Ti Ni, a menos que ocorra qualquer uma das seguintes condições:
    • Outro elemento da expressão de tupla tem o nome Ni, ou
    • Outro elemento de tupla sem um nome tem uma expressão de elemento de tupla do formato Ni, E.Ni ou E?.Ni ou
    • Ni é da forma ItemX, em que X é uma sequência de dígitos decimais que não começam com0e que podem representar a posição de um elemento de tupla, e X não representa a posição do elemento.
  • Caso contrário, o elemento de tipo de tupla deverá ser Ti.

Uma expressão de tupla é avaliada avaliando cada uma das expressões de elemento da esquerda para a direita.

Um valor de tupla pode ser obtido de uma expressão de tupla convertendo-a em um tipo de tupla (§10.2.13), reclassificando-a como um valor (§12.2.2) ou fazendo dela o destino de uma atribuição de desconstrução (§12.21.2).

Exemplo:

(int i, string) t1 = (i: 1, "One");
(long l, string) t2 = (l: 2, null);
var t3 = (i: 3, "Three");          // (int i, string)
var t4 = (i: 4, null);             // Error: no type

Neste exemplo, as quatro expressões de tupla são válidas. As duas primeiras, t1 e t2, não usam o tipo da expressão de tupla, mas aplicam uma conversão de tupla implícita. No caso de t2, a conversão de tupla implícita depende das conversões implícitas de 2 para long e de null para string. A terceira expressão de tupla tem um tipo (int i, string) e, portanto, pode ser reclassificada como um valor desse tipo. A declaração de t4, por outro lado, é um erro: a expressão de tupla não tem tipo porque o segundo elemento não tem tipo.

if ((x, y).Equals((1, 2))) { ... };

Este exemplo mostra que, às vezes, as tuplas podem levar a várias camadas de parênteses, especialmente quando a expressão de tupla é o único argumento para uma invocação de método.

fim do exemplo

12.8.7 Acesso de membro

12.8.7.1 Geral

Um member_access consiste em uma primary_expression, um predefined_type ou um qualified_alias_member, seguido por um token ".", por um identificador e, opcionalmente, por uma type_argument_list.

member_access
    : primary_expression '.' identifier type_argument_list?
    | predefined_type '.' identifier type_argument_list?
    | qualified_alias_member '.' identifier type_argument_list?
    ;

predefined_type
    : 'bool' | 'byte' | 'char' | 'decimal' | 'double' | 'float' | 'int'
    | 'long' | 'object' | 'sbyte' | 'short' | 'string' | 'uint' | 'ulong'
    | 'ushort'
    ;

A produção de qualified_alias_member é definida em §14.8.

member_access é do formato E.I ou do formato E.I<A₁, ..., Aₑ>, em que E é primary_expression, predefined_type ou qualified_alias_member, I é um único identificador e <A₁, ..., Aₑ> é type_argument_listopcional. Quando type_argument_list não é especificado, considere que e é zero.

Um member_access com uma primary_expression do tipo dynamic é associado dinamicamente (§12.3.3). Nesse caso, o compilador classifica o acesso do membro como um acesso de propriedade do tipo dynamic. As regras abaixo para determinar o significado do member_access são aplicadas em run-time, usando o tipo de run-time em vez do tipo de tempo de compilação da primary_expression. Se essa classificação em run-time levar a um grupo de métodos, o acesso ao membro será a primary_expression de uma invocation_expression.

member_access é avaliado e classificado da seguinte maneira:

  • Se e for igual a zero e E for namespace e E contiver um namespace aninhado com o nome I, o resultado será esse namespace.
  • Caso contrário, se E for um namespace e E contiver um tipo acessível com parâmetros de tipo de nome I e K, o resultado será esse tipo construído com os argumentos de tipo fornecidos.
  • Se E for classificado como um tipo, se E não for um parâmetro de tipo e se uma pesquisa de membro (§12.5) de I em E com parâmetros de tipo K produzir uma correspondência, então E.I será avaliada e classificada da seguinte maneira:

    Observação: quando o resultado dessa pesquisa de membro é um grupo de métodos e K é zero, o grupo de métodos pode conter métodos com parâmetros de tipo. Isso permite que esses métodos sejam considerados para inferência de argumento de tipo. fim da observação

    • Se I identificar um tipo, o resultado será esse tipo construído com determinados argumentos de tipo.
    • Se I identificar um ou mais métodos, o resultado será um grupo de métodos sem nenhuma expressão de instância associada.
    • Se I identificar uma propriedade estática, o resultado será um acesso de propriedade sem nenhuma expressão de instância associada.
    • Se I identifica um campo estático:
      • Se o campo for somente leitura e a referência ocorrer fora do construtor estático da classe ou estrutura na qual o campo é declarado, o resultado será um valor, ou seja, o valor do campo estático I em E.
      • Caso contrário, o resultado é uma variável, ou seja, o campo estático I em E.
    • Se I identifica um evento estático:
      • Se a referência ocorrer dentro da classe ou struct em que o evento é declarado e o evento foi declarado sem event_accessor_declarations (§15.8.1), E.I será processado exatamente como se I fosse um campo estático.
      • Caso contrário, o resultado é um acesso de evento com uma expressão de instância não associada.
    • Se I identificar uma constante, o resultado será um valor, ou seja, o valor dessa constante.
    • Se I identificar um membro de enumeração, o resultado será um valor, ou seja, o valor desse membro de enumeração.
    • Caso contrário, E.I é uma referência de membro inválida e ocorre um erro de tempo de compilação.
  • Se E for um acesso de propriedade, acesso do indexador, variável ou valor, de tipo T, e uma pesquisa de membro (§12.5) de I em T com argumentos de tipo K produza uma correspondência, E.I será avaliado e classificado da seguinte maneira:
    • Primeiro, se E for um acesso de propriedade ou indexador, o valor da propriedade ou do acesso do indexador será obtido (§12.2.2) e E será reclassificado como um valor.
    • Se I identificar um ou mais métodos, o resultado será um grupo de métodos sem nenhuma expressão de instância associada de E.
    • Se I identificar uma propriedade de instância, o resultado será um acesso de propriedade com uma expressão de instância associada de E e um tipo associado que é o tipo da propriedade. Se T for um tipo de classe, o tipo associado será escolhido na primeira declaração ou substituição da propriedade encontrada ao começar com Te pesquisar por meio de suas classes base.
    • Se T for um class_type e se I identificar um campo de instância desse class_type:
      • Se o valor de E for null, um System.NullReferenceException será lançado.
      • Caso contrário, se o campo for somente leitura e a referência ocorrer fora de um construtor de instância da classe na qual o campo é declarado, o resultado será um valor, ou seja, o valor do campo I no objeto referenciado por E.
      • Caso contrário, o resultado é uma variável, ou seja, o campo I no objeto referenciado por E.
    • Se T for um struct_type e se I identificar um campo de instância desse struct_type:
      • Se E for um valor ou se o campo for somente leitura e a referência ocorrer fora de um construtor de instância do struct no qual o campo é declarado, o resultado será um valor, ou seja, o valor do campo I na instância de struct fornecida pelo E.
      • Caso contrário, o resultado é uma variável, ou seja, o campo I na instância de struct fornecida por E.
    • Caso I identifique um evento de instância:
      • Se a referência ocorrer dentro da classe ou estrutura em que o evento é declarado, e o evento foi declarado sem event_accessor_declarations (§15.8.1), e a referência não ocorrer como o lado esquerdo de um operador a += ou -=, E.I será processado exatamente como se I fosse um campo de instância.
      • Caso contrário, o resultado é um acesso de evento com uma expressão de instância associada de E.
  • Caso contrário, será feita uma tentativa de processar E.I como uma invocação de método de extensão (§12.8.10.3). Se isso falhar, E.I será uma referência de membro inválida e ocorrerá um erro de tempo de associação.

12.8.7.2 Nomes simples e nomes de tipo idênticos

Em um acesso de membro na forma E.I, se E for um identificador único e se o significado de E como um simple_name (§12.8.4) for uma constante, campo, propriedade, variável local ou parâmetro com o mesmo tipo que o significado de E como um type_name (§7.8.1), nessa situação, são permitidos os dois possíveis significados de E. A pesquisa de membro de E.I nunca é ambígua, pois I deve necessariamente ser membro do tipo E nos dois casos. Em outras palavras, a regra simplesmente permite o acesso aos membros estáticos e aos tipos aninhados de E, onde, de outra forma, ocorreria um erro de tempo de compilação.

Exemplo:

struct Color
{
    public static readonly Color White = new Color(...);
    public static readonly Color Black = new Color(...);
    public Color Complement() => new Color(...);
}

class A
{
    public «Color» Color;              // Field Color of type Color

    void F()
    {
        Color = «Color».Black;         // Refers to Color.Black static member
        Color = Color.Complement();  // Invokes Complement() on Color field
    }

    static void G()
    {
        «Color» c = «Color».White;       // Refers to Color.White static member
    }
}

Somente para fins expositórios, dentro da classe A, as ocorrências do identificador Color que fazem referência ao tipo Color são delimitadas por «...» e aquelas que fazem referência ao campo Color não são.

fim do exemplo

12.8.8 Acesso de membro condicional nulo

Um null_conditional_member_access é uma versão condicional do member_access (§12.8.7) e será um erro de tempo de associação se o tipo de resultado for void. Para uma expressão condicional nula em que o tipo de resultado pode ser void consulte (§12.8.11).

Um null_conditional_member_access consiste em uma primary_expression seguida pelos dois tokens "?" e ".", depois por um identificador coma um type_argument_list opcional, seguido por zero ou mais dependent_accesses, qualquer um dos quais pode ser precedido por um null_forgiving_operator.

null_conditional_member_access
    : primary_expression '?' '.' identifier type_argument_list?
      (null_forgiving_operator? dependent_access)*
    ;
    
dependent_access
    : '.' identifier type_argument_list?    // member access
    | '[' argument_list ']'                 // element access
    | '(' argument_list? ')'                // invocation
    ;

null_conditional_projection_initializer
    : primary_expression '?' '.' identifier type_argument_list?
    ;

Uma expressão null_conditional_member_accessE é do formato P?.A. O significado de E é determinado da seguinte maneira:

  • Se o tipo de P for um tipo de valor anulável:

    Que T seja o tipo de P.Value.A.

    • Se T for um parâmetro de tipo que não é conhecido por ser um tipo de referência ou um tipo de valor não anulável, ocorrerá um erro de tempo de compilação.

    • Se T for um tipo de valor não anulável, o tipo de E será T? e o significado de E será o mesmo que o significado de:

      ((object)P == null) ? (T?)null : P.Value.A
      

      Exceto que P é avaliado apenas uma vez.

    • Caso contrário, o tipo de E é T e o significado de E é o mesmo que o significado de:

      ((object)P == null) ? (T)null : P.Value.A
      

      Exceto que P é avaliado apenas uma vez.

  • Ou:

    Seja T o tipo da expressão P.A.

    • Se T for um parâmetro de tipo que não é conhecido por ser um tipo de referência ou um tipo de valor não anulável, ocorrerá um erro de tempo de compilação.

    • Se T for um tipo de valor não anulável, o tipo de E será T? e o significado de E será o mesmo que o significado de:

      ((object)P == null) ? (T?)null : P.A
      

      Exceto que P é avaliado apenas uma vez.

    • Caso contrário, o tipo de E é T e o significado de E é o mesmo que o significado de:

      ((object)P == null) ? (T)null : P.A
      

      Exceto que P é avaliado apenas uma vez.

Observação: em uma expressão do formato:

P?.A₀?.A₁

Se P for avaliado como null, então nem A₀ nem A₁ serão avaliados. O mesmo será verdadeiro se uma expressão for uma sequência de operações null_conditional_member_access ou null_conditional_element_access§12.8.13.

fim da observação

Um null_conditional_projection_initializer é uma restrição de null_conditional_member_access e tem as mesmas semânticas. Ela ocorre apenas como um inicializador de projeção em uma expressão de criação de objeto anônimo (§12.8.17.3).

12.8.9 Expressões tolerantes a valores nulos

12.8.9.1 Geral

O valor, o tipo, a classificação (§12.2) e o contexto seguro (§16.4.15) de uma expressão que permite valor nulo é o valor, o tipo, a classificação e o contexto seguro de sua primary_expression.

null_forgiving_expression
    : primary_expression null_forgiving_operator
    ;

null_forgiving_operator
    : '!'
    ;

Observação: os operadores de perdão nulo e de negação lógica de prefixo (§12.9.4), embora representados pelo mesmo token léxico (!), são distintos. Somente este último pode ser substituído (§15.10), a definição do operador que permite valor nulo é fixa. fim da observação

É um erro em tempo de compilação aplicar o operador null-forgiving a valores nulos mais de uma vez à mesma expressão, independentemente de parênteses intervenientes.

Exemplo: todos os seguintes são inválidos:

var p = q!!;            // error: applying null_forgiving_operator more than once
var s = ( ( m(t) ! ) )! // error: null_forgiving_operator applied twice to m(t)

fim do exemplo

O restante desta subcláusula e as subcláusulas irmãs a seguir são condicionalmente normativas.

Um compilador que executa a análise de estado nulo estático (§8.9.5) deve estar em conformidade com a especificação a seguir.

O operador tolerante a valores nulos é uma pseudo-operação em tempo de compilação usada para informar a análise de estado nulo estático do compilador. Ele tem dois usos: para substituir a determinação de um compilador de que uma expressão talvez seja nula; e para impedir que um compilador emita um aviso relacionado à nulidade.

Aplicar o operador tolerante a nulos a uma expressão para a qual a análise de estado nulo estático de um compilador não gera avisos não é um erro.

12.8.9.2 Substituindo uma determinação "talvez nula"

Em algumas circunstâncias, a análise de estado nulo estático de um compilador pode determinar que uma expressão tem o estado nulo talvez nulo e emitir um aviso de diagnóstico quando outras informações indicarem que a expressão não pode ser nula. Aplicar o operador forgiving nulo a tal expressão informa à análise de estado nulo estático do compilador de que o estado nulo está em não é nulo; o que impede o aviso de diagnóstico e pode informar qualquer análise em andamento.

Exemplo: Considere o seguinte:

#nullable enable
public static void M()
{
    Person? p = Find("John");                  // returns Person?
    if (IsValid(p))
    {
       Console.WriteLine($"Found {p!.Name}");  // p can't be null
    }
}

public static bool IsValid(Person? person) =>
    person != null && person.Name != null;

Se IsValid retornar true, p poderá ser desreferenciado com segurança para acessar sua propriedade Name e o aviso de "desreferenciamento de um valor possivelmente nulo" poderá ser suprimido usando !.

fim do exemplo

Exemplo: o operador tolerante a valores nulos deve ser usado com cuidado. Considere:

#nullable enable
int B(int? x)
{
    int y = (int)x!; // quash warning, throw at runtime if x is null
    return y;
}

Aqui, o operador tolerante a valores nulos é aplicado a um tipo de valor e anula qualquer aviso sobre x. No entanto, se x for null em tempo de execução, uma exceção será lançada, pois null não poderá ser convertido em int.

fim do exemplo

12.8.9.3 Substituindo outros avisos de análise nula

Além de substituir as determinações de talvez nulo, conforme mencionado acima, pode haver outras circunstâncias em que se deseja anular a determinação de análise de estado nulo estático de um compilador quando uma expressão requer um ou mais avisos. Aplicar o operador que ignora nulos a uma expressão solicita que o compilador não emita avisos para a expressão. Em resposta, um compilador pode optar por não emitir avisos e também pode modificar sua análise adicional.

Exemplo: Considere o seguinte:

#nullable enable
public static void Assign(out string? lv, string? rv) { lv = rv; }

public string M(string? t)
{
    string s;
    Assign(out s!, t ?? "«argument was null»");
    return s;
}

Os tipos dos parâmetros do método Assign, lv & rv, são string?, com lv sendo um parâmetro de saída, e ele executa uma atribuição simples.

O método M passa a variável s, do tipo string, como parâmetro de saída de Assign, e o compilador usado emite um aviso, pois s não é uma variável anulável. Considerando-se que o segundo argumento de Assign não pode ser nulo, o operador null the null-forgiving é usado para anular o aviso.

fim do exemplo

Fim do texto normativo condicional.

12.8.10 Expressões de invocação

12.8.10.1 Geral

invocation_expression é usado para invocar um método.

invocation_expression
    : primary_expression '(' argument_list? ')'
    ;

A primary_expression pode ser uma null_forgiving_expression se e somente se possuir um delegate_type.

Uma invocation_expression será associada dinamicamente (§12.3.3) se pelo menos uma das seguintes condições for aplicável:

  • A primary_expression tem tipo de tempo de compilação dynamic.
  • Pelo menos um argumento opcional da lista de argumentos tem tipo de tempo de compilação dynamic.

Nesse caso, o compilador classifica invocation_expression como um valor do tipo dynamic. As regras abaixo para determinar o significado da invocation_expression são aplicadas em run-time, usando o tipo de run-time em vez do tipo de tempo de compilação da primary_expression e dos argumentos que têm o tipo de tempo de compilação dynamic. Se a expressão primária não tiver tipo de tempo de compilação dynamic, a chamada do método passará por uma verificação de tempo de compilação limitada, conforme descrito em §12.6.5.

A primary_expression de uma invocation_expression deve ser um grupo de métodos ou um valor de um delegate_type. Se primary_expression for um grupo de métodos, o invocation_expression será uma invocação de método (§12.8.10.2). Se primary_expression for um valor de delegate_type, o invocation_expression será uma invocação delegada (§12.8.10.4). Se a primary_expression não for um grupo de métodos nem um valor de um delegate_type, ocorrerá um erro de tempo de associação.

O argumento opcional argument_list (§12.6.2) fornece valores ou referências de variáveis para os parâmetros do método.

O resultado da avaliação de invocation_expression é classificado da seguinte maneira:

  • Se o invocation_expression invocar um método sem valor de retorno (§15.6.1) ou um delegado sem valor de retorno, o resultado é inexistente. Uma expressão classificada como nada é permitida somente no contexto de statement_expression (§13.7) ou como o corpo de lambda_expression (§12.19). Caso contrário, ocorrerá um erro de tempo de associação.
  • Caso contrário, se invocation_expression invoca um método returns-by-ref (§15.6.1) ou um delegado returns-by-ref, o resultado será uma variável com um tipo associado do tipo return do método ou delegado. Se a invocação for de um método de instância e o receptor for de um tipo de classe T, o tipo associado será escolhido na primeira declaração ou substituição do método encontrado ao começar com T e pesquisar por meio das classes base.
  • Caso contrário, a expressão de invocação invoca um método que retorna por valor (§15.6.1) ou um delegado que retorna por valor, e o resultado é um valor, com um tipo associado ao tipo de retorno do método ou delegado. Se a invocação for de um método de instância e o receptor for de um tipo de classe T, o tipo associado será escolhido na primeira declaração ou substituição do método encontrado ao começar com T e pesquisar por meio das classes base.

12.8.10.2 Invocações de método

Para uma invocação de método, primary_expression de invocation_expression deve ser um grupo de métodos. O grupo de métodos identifica o único método a ser invocado ou o conjunto de métodos sobrecarregados do qual escolher um método específico a ser invocado. Neste último caso, a determinação do método específico a ser invocado baseia-se no contexto fornecido pelos tipos dos argumentos em argument_list.

O processamento de tempo de associação de uma invocação de método do formato M(A), em que M é um grupo de métodos (possivelmente incluindo uma type_argument_list), e A é uma argument_listopcional, consiste nas seguintes etapas:

  • O conjunto de métodos candidatos para a invocação do método é construído. Para cada método F associado ao grupo de métodos M:
    • Se F não for genérico, F será um candidato quando:
      • M não tem nenhuma lista de argumentos de tipo e
      • F é aplicável ao A (§12.6.4.2).
    • Se F for genérico e M não tiver nenhuma lista de argumentos de tipo, F será um candidato quando:
      • A inferência de tipo (§12.6.3) é bem-sucedida, inferindo uma lista de argumentos de tipo para a chamada e
      • Depois que os argumentos de tipo inferidos são substituídos pelos parâmetros de tipo de método correspondentes, todos os tipos construídos na lista de parâmetros de F atendem às suas restrições (§8.4.5) e a lista de parâmetros de F é aplicável em relação a A (§12.6.4.2)
    • Se F for genérico e M incluir uma lista de argumentos de tipo, F será um candidato quando:
      • F tem o mesmo número de parâmetros de tipo de método que foram fornecidos na lista de argumentos de tipo e
      • Depois que os argumentos de tipo são substituídos pelos parâmetros de tipo de método correspondentes, todos os tipos construídos na lista de parâmetros de F atendem às suas restrições (§8.4.5) e a lista de parâmetros de F é aplicável em relação a A (§12.6.4.2).
  • O conjunto de métodos candidatos é reduzido para conter apenas métodos dos tipos mais derivados: para cada método C.F no conjunto, em que C é o tipo no qual o método F é declarado, todos os métodos declarados em um tipo base de C são removidos do conjunto. Além disso, se C for um tipo de classe diferente de object, todos os métodos declarados em um tipo de interface serão removidos do conjunto.

    Observação: esta última regra só tem efeito quando o grupo de métodos for o resultado de uma pesquisa de membro em um parâmetro de tipo com uma classe base efetiva diferente de object e um conjunto de interfaces efetivas não vazio. fim da observação

  • Se o conjunto resultante de métodos candidatos estiver vazio, o processamento adicional ao longo das etapas a seguir será abandonado e, em vez disso, será feita uma tentativa de processar a invocação como uma invocação do método de extensão (§12.8.10.3). Se isso falhar, nenhum método aplicável existirá e ocorrerá um erro de tempo de associação.
  • O melhor método do conjunto de métodos candidatos é identificado usando as regras de resolução de sobrecarga de §12.6.4. Se um único método melhor não puder ser identificado, a invocação do método será ambígua e ocorrerá um erro de tempo de associação. Ao executar a resolução de sobrecarga, os parâmetros de um método genérico são considerados depois de substituir os argumentos de tipo (fornecidos ou inferidos) pelos parâmetros de tipo de método correspondentes.

Depois que um método tiver sido selecionado e validado em tempo de vinculação pelas etapas acima, a invocação em tempo de execução real será processada de acordo com as regras de invocação do membro de função descritas em §12.6.6.

Observação: O efeito intuitivo das regras de resolução descritas acima é o seguinte: para localizar o método específico invocado por uma chamada de método, comece com o tipo indicado pela chamada de método e prossiga subindo a cadeia de herança até encontrar pelo menos uma declaração de método aplicável, acessível e que não substitua. Em seguida, execute a inferência de tipo e a resolução de sobrecarga no conjunto de métodos aplicáveis, acessíveis e não substituídos declarados nesse tipo, e invoque o método assim selecionado. Se nenhum método foi encontrado, tente processar a invocação como uma invocação de método de extensão. fim da observação

12.8.10.3 Invocações de método de extensão

Em uma invocação de método (§12.6.6.2) de uma das formas

«expr» . «identifier» ( )  
«expr» . «identifier» ( «args» )  
«expr» . «identifier» < «typeargs» > ( )  
«expr» . «identifier» < «typeargs» > ( «args» )

se o processamento normal da invocação não encontrar métodos aplicáveis, será feita uma tentativa de processar o constructo como uma invocação de método de extensão. Se «expr» ou qualquer um dos «args» tiver dynamic de tipo de tempo de compilação, os métodos de extensão não serão aplicados.

O objetivo é encontrar o melhor type_nameC, para que a invocação do método estático correspondente possa ocorrer:

C . «identifier» ( «expr» )  
C . «identifier» ( «expr» , «args» )  
C . «identifier» < «typeargs» > ( «expr» )  
C . «identifier» < «typeargs» > ( «expr» , «args» )

Um método de extensão Cᵢ.Mₑ será qualificado se:

  • Cᵢ é uma classe não genérica, não aninhada
  • O nome de Mₑ é identificador
  • Mₑ é acessível e aplicável quando aplicado aos argumentos como um método estático, conforme mostrado acima
  • Existe uma conversão implícita de identidade, referência ou boxing de expr para o tipo do primeiro parâmetro de Mₑ.

A pesquisa para C é a seguinte:

  • Começando com a declaração de namespace mais próxima, continuando com cada declaração de namespace envolvente e terminando com a unidade de compilação que a contém, são feitas tentativas sucessivas para encontrar um conjunto candidato de métodos de extensão:
    • Se o namespace ou a unidade de compilação fornecido contiver diretamente declarações de tipo não genérico Cᵢ com métodos de extensão qualificados Mₑ, o conjunto desses métodos de extensão será o conjunto de candidatos.
    • Se os namespaces importados usando diretivas de namespace no namespace fornecido ou na unidade de compilação contiverem diretamente declarações de tipo não genérico Cᵢ com métodos de extensão qualificados Mₑ, o conjunto desses métodos de extensão será o conjunto de candidatos.
  • Se nenhum conjunto de candidatos for encontrado em nenhuma declaração de namespace ou unidade de compilação, ocorrerá um erro de tempo de compilação.
  • Caso contrário, a resolução de sobrecarga será aplicada ao conjunto de candidatos, conforme descrito em §12.6.4. Se nenhum método melhor for encontrado, ocorrerá um erro de tempo de compilação.
  • C é o tipo no qual o melhor método é declarado como um método de extensão.

Usando-se C como destino, a chamada de método é processada como uma invocação de método estático (§12.6.6).

Observação: ao contrário de uma invocação de método de instância, nenhuma exceção é gerada quando expr é avaliada como uma referência nula. Em vez disso, esse valor null é passado para o método de extensão da mesma forma que seria em uma invocação regular de método estático. Cabe à implementação do método de extensão decidir como responder a essa chamada. fim da observação

As regras anteriores significam que os métodos de instância têm precedência sobre métodos de extensão, que os métodos de extensão disponíveis em declarações de namespace interno têm precedência sobre os métodos de extensão disponíveis em declarações de namespace externos e que os métodos de extensão declarados diretamente em um namespace têm precedência sobre os métodos de extensão importados para esse mesmo namespace com uma diretiva de namespace usando.

Exemplo:

public static class E
{
    public static void F(this object obj, int i) { }
    public static void F(this object obj, string s) { }
}

class A { }

class B
{
    public void F(int i) { }
}

class C
{
    public void F(object obj) { }
}

class X
{
    static void Test(A a, B b, C c)
    {
        a.F(1);            // E.F(object, int)
        a.F("hello");      // E.F(object, string)
        b.F(1);            // B.F(int)
        b.F("hello");      // E.F(object, string)
        c.F(1);            // C.F(object)
        c.F("hello");      // C.F(object)
    }
}

No exemplo, o método de Btem precedência sobre o primeiro método de extensão e o método de Ctem precedência sobre ambos os métodos de extensão.

public static class C
{
    public static void F(this int i) => Console.WriteLine($"C.F({i})");
    public static void G(this int i) => Console.WriteLine($"C.G({i})");
    public static void H(this int i) => Console.WriteLine($"C.H({i})");
}

namespace N1
{
    public static class D
    {
        public static void F(this int i) => Console.WriteLine($"D.F({i})");
        public static void G(this int i) => Console.WriteLine($"D.G({i})");
    }
}

namespace N2
{
    using N1;

    public static class E
    {
        public static void F(this int i) => Console.WriteLine($"E.F({i})");
    }

    class Test
    {
        static void Main(string[] args)
        {
            1.F();
            2.G();
            3.H();
        }
    }
}

A saída deste exemplo é:

E.F(1)
D.G(2)
C.H(3)

D.G tem precedência sobre C.Ge E.F tem precedência sobre D.F e C.F.

fim do exemplo

12.8.10.4 Invocações de delegado

Para uma invocação de delegado, a primary_expression da invocation_expression deve ser um valor de um delegate_type. Além disso, considerando delegate_type ser um membro de função com a mesma lista de parâmetros que o delegate_type, delegate_type deve ser aplicável (§12.6.4.2) em relação a argument_list de invocation_expression.

O processamento em run-time de uma invocação de delegado do formato D(A), em que D é uma primary_expression de um delegate_type e A é uma argument_listopcional, consiste nas seguintes etapas:

  • D é avaliado. Se essa avaliação causar uma exceção, nenhuma etapa adicional será executada.
  • A lista de argumentos A é avaliada. Se essa avaliação causar uma exceção, nenhuma etapa adicional será executada.
  • O valor de D é verificado como sendo válido. Se o valor de D for null, System.NullReferenceException será lançado e nenhuma etapa adicional será executada.
  • Caso contrário, D é uma referência a uma instância delegada. Invocações de membro de função (§12.6.6) são executadas em cada uma das entidades que podem ser chamadas na lista de invocação de delegado. Para entidades chamáveis que consistem de uma instância e um método de instância, a instância para a invocação é a instância contida na entidade chamável.

Consulte §20.6 para obter detalhes de várias listas de invocação sem parâmetros.

12.8.11 Expressão de invocação condicional nula

Uma null_conditional_invocation_expression é sintaticamente um null_conditional_member_access (§12.8.8) ou um null_conditional_element_access (§12.8.13) onde o dependent_access final é uma expressão de invocação (§12.8.10).

Uma null_conditional_invocation_expression ocorre no contexto de uma statement_expression (§13.7), um anonymous_function_body (§12.19.1) ou um method_body (§15.6.1).

Ao contrário do null_conditional_member_access ou null_conditional_element_accesssintaticamente equivalente, uma null_conditional_invocation_expression pode ser classificada como nada.

null_conditional_invocation_expression
    : null_conditional_member_access null_forgiving_operator? '(' argument_list? ')'
    | null_conditional_element_access null_forgiving_operator? '(' argument_list? ')'
    ;

O null_forgiving_operator opcional poderá ser incluído se e somente se o null_conditional_member_access ou null_conditional_element_access tiver um delegate_type.

Uma expressão null_conditional_invocation_expressionE é do formato P?A; onde A é o resto do null_conditional_member_access ou null_conditional_element_access sintaticamente equivalente, portanto, A começará com . ou [. Deixe PA indicar a concatenação de P e A.

Quando E ocorre como statement_expression o significado de E é o mesmo que o significado da instrução :

if ((object)P != null) PA

exceto que P é avaliado apenas uma vez.

Quando E ocorre como anonymous_function_body ou method_body o significado de E depende de sua classificação:

  • Se E for classificado como nada, seu significado será o mesmo que o significado do bloco :

    { if ((object)P != null) PA; }
    

    exceto que P é avaliado apenas uma vez.

  • Caso contrário, o significado de E é o mesmo que o significado do bloco :

    { return E; }
    

    e, por sua vez, o significado desse bloco depende se E é sintaticamente equivalente a null_conditional_member_access (§12.8.8) or null_conditional_element_access (§12.8.13).

12.8.12 Acesso ao elemento

12.8.12.1 Geral

Um element_access consiste em um primary_expression, seguido por um token "[", seguido por um argument_list, seguido por um token "]". argument_list consiste em um ou mais argumentos separados por vírgulas.

element_access
    : primary_expression '[' argument_list ']'
    ;

Ao reconhecer um primary_expression se as alternativas element_access e pointer_element_access (§23.6.4) forem aplicáveis, a última será escolhida se o primary_expression inserido for do tipo de ponteiro (§23.3).

O argument_list de um element_access não deve conter out ou ref argumentos.

O primary_expression de um element_access não deve ser um array_creation_expression a menos que inclua um array_initializer ou um stackalloc_expression , a menos que inclua um stackalloc_initializer.

Observação: essa restrição existe para não permitir código potencialmente confuso, como:

    var a = new int[3][1];

que, de outra forma, seria interpretado como:

    var a = (new int[3])[1];

Uma restrição semelhante se aplica a null_conditional_element_access (§12.8.13). fim da observação

Um element_access será associado dinamicamente (§12.3.3) se pelo menos uma das seguintes condições for aplicável:

  • A primary_expression tem tipo de tempo de compilação dynamic.
  • Pelo menos uma expressão da argument_list tem tipo de tempo de compilação dynamic e a primary_expression não tem tipo de array.

Nesse caso, o compilador classifica element_access como um valor do tipo dynamic. As regras abaixo para determinar o significado do element_access são então aplicadas em tempo de execução, usando o tipo de tempo de execução em vez do tipo de tempo de compilação das expressões primary_expression e argument_list que têm o tipo dynamicde tempo de compilação. Se o primary_expression não tiver o tipo dynamicde tempo de compilação, o acesso ao elemento passará por uma verificação de tempo de compilação limitada, conforme descrito em §12.6.5.

Se a primary_expression de um element_access for um valor de tipo_de_array, o element_access será um acesso de array (§12.8.12.2). Caso contrário, o primary_expression será uma variável ou valor de uma classe, struct ou tipo de interface que tenha um ou mais membros indexador, nesse caso, o element_access é um acesso de indexador (§12.8.12.3).

12.8.12.2 Acesso de matriz

Para um acesso de matriz, a primary_expression do element_access deve ser um valor de um tipo de array. Além disso, o argument_list de um acesso de matriz não deve conter argumentos nomeados. O número de expressões em argument_list deve ser igual à classificação de array_type, e cada expressão deve ser do tipo int, uint, long ou ulong, ou deve ser implicitamente conversível para um ou mais desses tipos.

O resultado da avaliação de um acesso de matriz é uma variável do tipo de elemento da matriz, ou seja, o elemento de matriz selecionado pelos valores das expressões no argument_list.

O processamento em tempo de execução de um acesso a matriz do formulário P[A], onde P é uma primary_expression de um array_type e A é uma argument_list, consiste nas seguintes etapas:

  • P é avaliado. Se essa avaliação causar uma exceção, nenhuma etapa adicional será executada.
  • As expressões de índice de argument_list são avaliadas em ordem, da esquerda para a direita. Após a avaliação de cada expressão de índice, uma conversão implícita (§10.2) para um dos seguintes tipos é executada: int, uint, long, ulong. O primeiro tipo nessa lista para o qual existe uma conversão implícita é escolhido. Por exemplo, se a expressão de índice for do tipo short uma conversão implícita em int será executada, uma vez que são possíveis conversões implícitas de short para int e de short para long. Se a avaliação de uma expressão de índice ou a conversão implícita subsequente causar uma exceção, nenhuma expressão de índice adicional será avaliada e nenhuma outra etapa será executada.
  • O valor de P é verificado como sendo válido. Se o valor de P for null, System.NullReferenceException será lançado e nenhuma etapa adicional será executada.
  • O valor de cada expressão em argument_list é verificado em relação aos limites reais de cada dimensão da instância da matriz referenciada por P. Se um ou mais valores estiverem fora do intervalo, System.IndexOutOfRangeException será lançado e nenhuma etapa adicional será executada.
  • O local do elemento de matriz fornecido pelas expressões de índice é computado e esse local se torna o resultado do acesso à matriz.

12.8.12.3 Acesso de indexador

Para um acesso de indexador, o primary_expression do element_access deve ser uma variável ou valor de uma classe, struct ou tipo de interface, e esse tipo deve implementar um ou mais indexadores aplicáveis em relação ao argument_list do element_access.

O processamento em tempo de vinculação de um acesso de indexador do formulário P[A], onde P é uma primary_expression de uma classe, struct ou tipo de interface T, e A é uma argument_list, consiste nas seguintes etapas:

  • O conjunto de indexadores fornecido por T foi construído. O conjunto consiste em todos os indexadores declarados em T ou um tipo base de T que não são declarações de substituição e podem ser acessados no contexto atual (§7.5).
  • O conjunto é reduzido aos indexadores aplicáveis e não ocultos por outros indexadores. As regras a seguir são aplicadas a cada indexador S.I no conjunto, em que S é o tipo no qual o indexador I é declarado:
    • Se I não for aplicável em relação ao A (§12.6.4.2), I será removido do conjunto.
    • Se I for aplicável em relação a A (§12.6.4.2), todos os indexadores declarados em um tipo base de S serão removidos do conjunto.
    • Se I for aplicável em relação a A (§12.6.4.2) e S for um tipo de classe diferente de object, todos os indexadores declarados em uma interface serão removidos do conjunto.
  • Se o conjunto resultante de indexadores candidatos estiver vazio, nenhum indexador aplicável existirá e ocorrerá um erro de tempo de associação.
  • O melhor indexador do conjunto de indexadores candidatos é identificado usando as regras de resolução de sobrecarga de §12.6.4. Se um único melhor indexador não puder ser identificado, o acesso ao indexador será ambíguo e ocorrerá um erro de tempo de associação.
  • As expressões de índice de argument_list são avaliadas em ordem, da esquerda para a direita. O resultado do processamento do acesso do indexador é uma expressão classificada como um acesso de indexador. A expressão de acesso do indexador faz referência ao indexador determinado na etapa acima e tem uma expressão de instância associada de P e uma lista de argumentos associada de A e um tipo associado que é o tipo do indexador. Se T for um tipo de classe, o tipo associado será escolhido na primeira declaração ou substituição de indexador encontrado ao começar com T e pesquisar por meio de suas classes base.

Dependendo do contexto no qual ele é usado, um acesso de indexador causa a invocação do acessador get ou do acessador set do indexador. Se o acesso do indexador for o destino de uma atribuição, o acessador set será invocado para atribuir um novo valor (§12.21.2). Em todos os outros casos, o acessador get é invocado para obter o valor atual (§12.2.2).

12.8.13 Acesso de elemento condicional nulo

Um null_conditional_element_access consiste em um primary_expression, seguido pelos dois tokens "?" e "[", seguido por um argument_list, seguido por um token "]", seguido por zero ou mais dependent_access, dos quais qualquer um pode ser precedido por um null_forgiving_operator.

null_conditional_element_access
    : primary_expression '?' '[' argument_list ']'
      (null_forgiving_operator? dependent_access)*
    ;

O argument_list de um null_conditional_element_access não deve conter out ou ref argumentos.

O primary_expression de um null_conditional_element_access não deve ser um array_creation_expression a menos que inclua um array_initializer, ou um stackalloc_expression a menos que inclua um stackalloc_initializer.

Observação: essa restrição existe para não permitir código potencialmente confuso. Uma restrição semelhante se aplica a element_access (§12.8.12) em que um exemplo do que é excluído pode ser encontrado. fim da observação

Um null_conditional_element_access é uma versão condicional do element_access (§12.8.12) e será um erro de tempo de associação se o tipo de resultado for void. Para uma expressão condicional nula em que o tipo de resultado pode ser void consulte (§12.8.11).

Uma expressão null_conditional_element_accessE é do formato P?[A]B; onde B são os dependent_accesses, se houver. O significado de E é determinado da seguinte maneira:

  • Se o tipo de P for um tipo de valor anulável:

    Seja T o tipo da expressão P.Value[A]B.

    • Se T for um parâmetro de tipo que não é conhecido por ser um tipo de referência ou um tipo de valor não anulável, ocorrerá um erro de tempo de compilação.

    • Se T for um tipo de valor não anulável, o tipo de E será T? e o significado de E será o mesmo que o significado de:

      ((object)P == null) ? (T?)null : P.Value[A]B
      

      Exceto que P é avaliado apenas uma vez.

    • Caso contrário, o tipo de E é T e o significado de E é o mesmo que o significado de:

      ((object)P == null) ? null : P.Value[A]B
      

      Exceto que P é avaliado apenas uma vez.

  • Ou:

    Seja T o tipo da expressão P[A]B.

    • Se T for um parâmetro de tipo que não é conhecido por ser um tipo de referência ou um tipo de valor não anulável, ocorrerá um erro de tempo de compilação.

    • Se T for um tipo de valor não anulável, o tipo de E será T? e o significado de E será o mesmo que o significado de:

      ((object)P == null) ? (T?)null : P[A]B
      

      Exceto que P é avaliado apenas uma vez.

    • Caso contrário, o tipo de E é T e o significado de E é o mesmo que o significado de:

      ((object)P == null) ? null : P[A]B
      

      Exceto que P é avaliado apenas uma vez.

Observação: em uma expressão do formato:

P?[A₀]?[A₁]

se P for avaliado como null, nem A₀ nem A₁ são avaliados. O mesmo será verdadeiro se uma expressão for uma sequência de operações null_conditional_element_access ou null_conditional_member_access§12.8.8.

fim da observação

12.8.14 Este acesso

Um this_access consiste na palavra chave this.

this_access
    : 'this'
    ;

Um this_access é permitido somente no bloco de um construtor de instância, um método de instância, um acessador de instância (§12.2.1) ou um finalizador. Ele tem um dos seguintes significados:

  • Quando this é usado em uma expressão_primária dentro de um construtor de instância de uma classe, ele é classificado como um valor. O tipo do valor é o tipo de instância (§15.3.2) da classe na qual o uso ocorre e o valor é uma referência ao objeto que está sendo construído.
  • Quando this é usado em primary_expression em um método de instância ou acessador de instância de uma classe, ele é classificado como um valor. O tipo do valor é o tipo de instância (§15.3.2) da classe na qual o uso ocorre e o valor é uma referência ao objeto para o qual o método ou acessador foi invocado.
  • Quando this é usado em uma primary_expression em um construtor de instância de uma estrutura, ele é classificado como uma variável. O tipo da variável é o tipo de instância (§15.3.2) do struct no qual o uso ocorre e a variável representa o struct que está sendo construído.
    • Se a declaração do construtor não tiver nenhum inicializador de construtor, a variável this se comportará exatamente como um parâmetro de saída do tipo struct. Em particular, isso significa que a variável deve ser definitivamente atribuída em cada caminho de execução do construtor de instância.
    • Caso contrário, a variável this se comportará exatamente como um parâmetro ref do tipo struct. Em particular, isso significa que a variável é considerada inicialmente atribuída.
  • Quando this é usado em uma primary_expression em um método de instância ou acessador de instância de uma estrutura, ele é classificado como uma variável. O tipo da variável é o tipo de instância (§15.3.2) do struct no qual o uso ocorre.
    • Se o método ou acessador não for um iterador (§15.15) ou uma função assíncrona (§15.14), a this variável representará o struct para o qual o método ou acessador foi invocado.
      • Se o struct for um readonly struct, a variável this se comportará exatamente como um parâmetro de entrada do tipo struct
      • Caso contrário, a variável this se comportará exatamente como um parâmetro ref do tipo struct
    • Se o método ou acessador for uma função iterador ou assíncrona, a variável this representará uma cópia do struct para o qual o método ou acessador foi invocado e se comportará exatamente da mesma forma que um parâmetro do valor do tipo struct.

O uso de this em primary_expression em um contexto diferente daqueles listados acima é um erro de tempo de compilação. Em particular, não é possível fazer referência a this em um método estático, acessador de propriedade estático ou em variable_initializer de uma declaração de campo.

12.8.15 Acesso base

base_access consiste na palavra-chave base seguida por um token “.”, um identificado e type_argument_list opcional ou argument_list entre colchetes:

base_access
    : 'base' '.' identifier type_argument_list?
    | 'base' '[' argument_list ']'
    ;

Um base_access é usado para acessar membros da classe base que estão ocultos por membros com nomes semelhantes na classe ou estrutura atual. Um base_access é permitido somente no corpo de um construtor de instância, um método de instância, um acessador de instância (§12.2.1) ou um finalizador. Quando base.I ocorre em uma classe ou struct, I deve indicar um membro da classe base dessa classe ou struct. Da mesma forma, quando base[E] ocorre em uma classe, um indexador aplicável deve existir na classe base.

Em tempo de associação, as expressões base_access do formato base.I e base[E] são avaliadas exatamente como se fossem escritas ((B)this).I e ((B)this)[E], em que B é a classe base da classe ou estrutura na qual a construção ocorre. Assim, base.I e base[E] correspondem a this.I e this[E], exceto que this é exibido como uma instância da classe base.

Quando base_access faz referência a um membro de função virtual (um método, propriedade ou indexador), a determinação de qual membro de função invocar em run-time (§12.6.6) é alterada. O membro da função invocado é determinado pela localização da implementação mais derivada (§15.6.4) do membro da função em relação a B (em vez de em relação ao tipo de run-time de this, como seria de costume em um acesso não base). Portanto, em uma substituição de um membro de função virtual, um base_access pode ser usado para invocar a implementação herdada do membro da função. Se o membro da função referenciado por um base_access for abstrato, ocorrerá um erro de tempo de associação.

Observação: ao contrário de this, base não é uma expressão em si. É uma palavra-chave usada apenas no contexto de base_access ou constructor_initializer (§15.11.2). fim da observação

12.8.16 Operadores de incremento e de decremento pós-fixados

post_increment_expression
    : primary_expression '++'
    ;

post_decrement_expression
    : primary_expression '--'
    ;

O operando de uma operação de adição ou remoção pós-fixada deve ser uma expressão classificada como uma variável, um acesso de propriedade ou um acesso de indexador. O resultado da operação é um valor do mesmo tipo do operando.

Se primary_expression tiver o tipo de tempo de compilação dynamic, o operador será associado dinamicamente (§12.3.3), post_increment_expression ou post_decrement_expression terá o tipo de tempo de compilação dynamic e as regras a seguir serão aplicadas em run-time usando-se o tipo de run-time de primary_expression.

Se o operando de uma operação de adição ou remoção pós-fixada for um acesso de propriedade ou indexador, a propriedade ou o indexador deverá ter acessores get e set. Se esse não for o caso, ocorrerá um erro de tempo de associação.

A resolução de sobrecarga de operador unário (§12.4.4) é aplicada para selecionar uma implementação de operador específica. Há operadores predefinidos ++ e -- para os seguintes tipos: sbyte, byte, short, ushort, int, uint, long, ulong, char, float, double, decimal e qualquer tipo de enumeração. Os operadores predefinidos ++ retornam o valor produzido adicionando-se 1 ao operando e os operadores predefinidos -- retornam o valor produzido pela subtração de 1 do operando. Em um contexto verificado, se o resultado dessa adição ou subtração estiver fora do intervalo do tipo de resultado e o tipo de resultado for um tipo integral ou tipo de enumeração, System.OverflowException será lançado.

Deve haver uma conversão implícita do tipo de retorno do operador unário selecionado para o tipo primary_expression; caso contrário, ocorrerá um erro de tempo de compilação.

O processamento run-time de uma operação de incremento ou decremento pós-fixada no formato x++ ou x-- consiste nas seguintes etapas:

  • Se x for classificado como uma variável:
    • x é avaliado para produzir a variável.
    • O valor de x é armazenado.
    • O valor salvo x é convertido no tipo de operando do operador selecionado e o operador é invocado com esse valor como seu argumento.
    • O valor retornado pelo operador é convertido no tipo x e armazenado no local fornecido pela avaliação anterior de x.
    • O valor salvo x torna-se o resultado da operação.
  • Se x for classificado como uma propriedade ou acesso de indexador:
    • A expressão de instância (se x não for static) e a lista de argumentos (se x for um acesso de indexador) associadas a x serão avaliadas e os resultados serão usados nas invocações subsequentes do acessador get e set.
    • O acessador get de x é invocado e o valor retornado é salvo.
    • O valor salvo x é convertido no tipo de operando do operador selecionado e o operador é invocado com esse valor como seu argumento.
    • O valor retornado pelo operador é convertido no tipo x e o acessador set de x é invocado com esse valor como seu argumento de valor.
    • O valor salvo x torna-se o resultado da operação.

Os operadores ++ e -- também dão suporte à notação de pré-fixada (§12.9.6). O resultado de x++ ou x-- é o valor de xantes da operação, enquanto o resultado de ++x ou --x for o valor de xapós a operação. Nos dois casos, x em si tem o mesmo valor após a operação.

Uma implementação do operador ++ ou do operador -- pode ser invocada usando a notação pós-fixada ou pré-fixada. Não é possível ter implementações de operador separadas para as duas notações.

12.8.17 O operador new

12.8.17.1 Geral

O operador new é usado para criar novas instâncias de tipos.

Há três formas de novas expressões:

  • Expressões de criação de objeto são usadas para criar novas instâncias de tipos de classe e tipos de valor.
  • As expressões de criação de matriz são usadas para criar novas instâncias de tipos de matriz.
  • Expressões de criação de delegado são usadas para obter instâncias de tipos de delegado.

O operador new implica na criação de uma instância de um tipo, mas não implica necessariamente a alocação de memória. Em particular, instâncias de tipos de valor não exigem memória adicional além das variáveis em que residem e nenhuma alocação ocorre quando usa-se new para criar as instâncias de tipos de valor.

Observação: expressões de criação de delegado nem sempre criam novas instâncias. Quando a expressão é processada da mesma forma que uma conversão de grupo de métodos (§10.8) ou uma conversão de função anônima (§10.7), isso pode resultar em uma instância de delegado existente sendo reutilizada. fim da observação

12.8.17.2 Expressões de criação de objeto

12.8.17.2.1 Geral

object_creation_expression é usado para criar uma nova instância de class_type ou value_type.

object_creation_expression
    : 'new' type '(' argument_list? ')' object_or_collection_initializer?
    | 'new' type object_or_collection_initializer
    ;

object_or_collection_initializer
    : object_initializer
    | collection_initializer
    ;

O tipo de uma object_creation_expression deve ser um class_type, um value_typeou um type_parameter. O tipo não pode ser um tuple_type ou um class_type abstrato ou estático.

A argument_list opcional (§12.6.2) só será permitida se o tipo for um class_type ou um struct_type.

Uma expressão de criação de objeto pode omitir a lista de argumentos do construtor e incluir parênteses, desde que inclua um inicializador de objeto ou inicializador de coleção. Omitir a lista de argumentos do construtor e os parênteses é equivalente a especificar uma lista de argumentos vazia.

O processamento de uma expressão de criação de objeto que inclui um inicializador de objeto ou inicializador de coleção consiste primeiro em processar o construtor de instância e, em seguida, processar as inicializações de membro ou elemento especificadas pelo inicializador de objeto (§12.8.17.2.2) ou inicializador de coleção (§12.8.17.2.3).

Se qualquer um dos argumentos em argument_list opcional tiver o tipo de tempo de compilação dynamic, object_creation_expression será associado dinamicamente (§12.3.3) e as regras a seguir serão aplicadas em run-time usando o tipo de run-time desses argumentos de argument_list que têm o tipo de tempo de compilação dynamic. No entanto, a criação do objeto passa por uma verificação de tempo de compilação limitada, conforme descrito em §12.6.5.

O processamento de object_creation_expression do formato new T(A), em que T é um class_type ou value_type e A é argument_list opcional, consiste nas seguintes etapas:

  • Se T for um value_type e A não estiver presente:
    • object_creation_expression é uma invocação de construtor padrão. O resultado do object_creation_expression é um valor do tipo T, ou seja, o valor padrão para T conforme definido em §8.3.3.
  • Caso contrário, se T for um type_parameter e A não estiver presente:
    • Se nenhuma restrição de tipo de valor ou restrição de construtor (§15.2.5) tiver sido especificada para T, ocorrerá um erro de tempo de associação.
    • O resultado de object_creation_expression é um valor do tipo de run-time ao qual o parâmetro de tipo foi associado, ou seja, o resultado de invocar o construtor padrão desse tipo. O tipo de run-time pode ser um tipo de referência ou valor.
  • Caso contrário, se T for um class_type ou um struct_type:
    • Se T for class_type abstrato ou estático, ocorrerá um erro de tempo de compilação.
    • O construtor de instância a ser invocado é determinado usando-se as regras de resolução de sobrecarga de §12.6.4. O conjunto de construtores de instâncias candidatos consiste em todos os construtores de instância acessível declarados em T, que são aplicáveis em relação a A (§12.6.4.2). Se o conjunto de construtores de instâncias candidatos estiver vazio ou se um único melhor construtor de instância não puder ser identificado, ocorrerá um erro em tempo de ligação.
    • O resultado de object_creation_expression é um valor do tipo T, ou seja, o valor produzido invocando o construtor de instância determinado na etapa acima.
    • Caso contrário, object_creation_expression é inválido e ocorre um erro de tempo de associação.

Mesmo que object_creation_expression esteja associado dinamicamente, o tipo de tempo de compilação ainda será T.

O processamento de run-time de uma object_creation_expression do novo formato T(A), em que T é um class_type ou um struct_type e A é uma argument_listopcional, consiste nas seguintes etapas:

  • Se T for um class_type:
    • Uma nova instância da classe T é alocada. Se não houver memória suficiente disponível para alocar a nova instância, System.OutOfMemoryException será gerado e nenhuma outra etapa será executada.
    • Todos os campos da nova instância são inicializados para seus valores padrão (§9.3).
    • O construtor de instância é invocado de acordo com as regras de invocação de membro de função (§12.6.6). Uma referência à instância recém-alocada é passada automaticamente para o construtor de instância e a instância pode ser acessada dentro desse construtor.
  • Se T for um struct_type:
    • Uma instância do tipo T é criada alocando-se uma variável local temporária. Como um construtor de instância de struct_type é necessário para atribuir definitivamente um valor a cada campo da instância que está sendo criada, nenhuma inicialização da variável temporária é necessária.
    • O construtor de instância é invocado de acordo com as regras de invocação de membro de função (§12.6.6). Uma referência à instância recém-alocada é passada automaticamente para o construtor de instância e a instância pode ser acessada dentro desse construtor.
12.8.17.2.2 Inicializadores de objeto

Um inicializador de objeto especifica os valores para zero ou mais campos, propriedades ou elementos indexados de um objeto.

object_initializer
    : '{' member_initializer_list? '}'
    | '{' member_initializer_list ',' '}'
    ;

member_initializer_list
    : member_initializer (',' member_initializer)*
    ;

member_initializer
    : initializer_target '=' initializer_value
    ;

initializer_target
    : identifier
    | '[' argument_list ']'
    ;

initializer_value
    : expression
    | object_or_collection_initializer
    ;

Um inicializador de objeto consiste em uma sequência de inicializadores de membro, delimitados pelos tokens { e } e separados por vírgulas. Cada member_initializer deve designar um destino para a inicialização. Um identificador deve denominar um campo acessível ou propriedade do objeto que esteja sendo inicializado, enquanto argument_list entre colchetes deve especificar argumentos para um indexador acessível no objeto que esteja sendo inicializado. É um erro que um inicializador de objeto inclua mais de um inicializador de membro para o mesmo campo ou propriedade.

Observação: embora um inicializador de objeto não tenha permissão para definir o mesmo campo ou propriedade mais de uma vez, não há essas restrições para os indexadores. Um inicializador de objeto pode conter vários destinos de inicializador que se referem a indexadores e pode até usar os mesmos argumentos do indexador várias vezes. fim da observação

Cada initializer_target é seguido por um sinal de igual e uma expressão, um inicializador de objeto ou um inicializador de coleção. Não é possível que expressões dentro do inicializador de objeto se refiram ao objeto recém-criado que ele está inicializando.

Um inicializador de membro que especifica uma expressão após o sinal de igual é processado da mesma forma que uma atribuição (§12.21.2) para o destino.

Um inicializador de membro que especifica um inicializador de objeto após o sinal de igual é um inicializador de objeto aninhado, ou seja, uma inicialização de um objeto integrado. Em vez de atribuir um novo valor ao campo ou à propriedade, as atribuições no inicializador de objeto aninhado são tratadas como atribuições para os membros do campo ou da propriedade. Inicializadores de objeto aninhados não podem ser aplicados a propriedades com um tipo de valor ou a campos somente leitura com um tipo de valor.

Um inicializador membro que especifica um inicializador de coleção após o sinal de igual é uma inicialização de uma coleção integrada. Em vez de atribuir uma nova coleção para o campo de destino, propriedade ou indexador, os elementos fornecidos no inicializador são adicionados à coleção referenciada pelo destino. O destino deve ser de um tipo de coleção que atenda aos requisitos especificados em §12.8.17.2.3.

Quando um destino de inicializador se refere a um indexador, os argumentos para o indexador sempre serão avaliados exatamente uma vez. Assim, mesmo que os argumentos acabem nunca sendo usados (por exemplo, devido a um inicializador aninhado vazio), eles são avaliados quanto aos efeitos colaterais.

Exemplo: a seguinte classe representa um ponto com duas coordenadas:

public class Point
{
    public int X { get; set; }
    public int Y { get; set; }
}

Uma instância de Point pode ser criada e inicializada da seguinte maneira:

Point a = new Point { X = 0, Y = 1 };

Isso tem o mesmo efeito que

Point __a = new Point();
__a.X = 0;
__a.Y = 1;
Point a = __a;

em que __a é uma variável temporária invisível e inacessível.

A classe a seguir mostra um retângulo criado de dois pontos e a criação e a inicialização de uma instância de Rectangle:

public class Rectangle
{
    public Point P1 { get; set; }
    public Point P2 { get; set; }
}

Uma instância de Rectangle pode ser criada e inicializada da seguinte maneira:

Rectangle r = new Rectangle
{
    P1 = new Point { X = 0, Y = 1 },
    P2 = new Point { X = 2, Y = 3 }
};

Isso tem o mesmo efeito que

Rectangle __r = new Rectangle();
Point __p1 = new Point();
__p1.X = 0;
__p1.Y = 1;
__r.P1 = __p1;
Point __p2 = new Point();
__p2.X = 2;
__p2.Y = 3;
__r.P2 = __p2;
Rectangle r = __r;

em que __r, __p1 e __p2 são variáveis temporárias que são invisíveis e inacessíveis.

Se o construtor de Rectanglealocar as duas instâncias incorporadas de Point, elas poderão ser usadas para inicializar as instâncias incorporadas de Point em vez de atribuir novas instâncias.

public class Rectangle
{
    public Point P1 { get; } = new Point();
    public Point P2 { get; } = new Point();
}

o constructo a seguir pode ser usado para inicializar as instâncias de Point inseridas em vez de atribuir novas instâncias:

Rectangle r = new Rectangle
{
    P1 = { X = 0, Y = 1 },
    P2 = { X = 2, Y = 3 }
};

Isso tem o mesmo efeito que

Rectangle __r = new Rectangle();
__r.P1.X = 0;
__r.P1.Y = 1;
__r.P2.X = 2;
__r.P2.Y = 3;
Rectangle r = __r;

fim do exemplo

12.8.17.2.3 Inicializadores de coleção

Um inicializador de coleção especifica os elementos de uma coleção.

collection_initializer
    : '{' element_initializer_list '}'
    | '{' element_initializer_list ',' '}'
    ;

element_initializer_list
    : element_initializer (',' element_initializer)*
    ;

element_initializer
    : non_assignment_expression
    | '{' expression_list '}'
    ;

expression_list
    : expression (',' expression)*
    ;

Um inicializador de coleção consiste em uma sequência de inicializadores de elemento, delimitados pelos tokens { e } e separados por vírgulas. Cada inicializador especifica um elemento a ser adicionado ao objeto de coleção que está sendo inicializado e consiste em uma lista de expressões entre os tokens { e }, separadas por vírgulas. Um inicializador de elemento de expressão única pode ser escrito sem chaves, mas não pode ser uma expressão de atribuição, para evitar ambiguidade com inicializadores de membros. A produção de non_assignment_expression é definida em §12.22.

Exemplo: a seguir há um exemplo de uma expressão de criação de objeto que inclui um inicializador de coleção:

List<int> digits = new List<int> { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };

fim do exemplo

O objeto de coleção ao qual um inicializador de coleção é aplicado deve ser de um tipo que implementa System.Collections.IEnumerable ou ocorre um erro de tempo de compilação. Para cada elemento especificado em ordem da esquerda para a direita, a pesquisa de membro normal é aplicada para localizar um membro chamado Add. Se o resultado da pesquisa de membro não for um grupo de métodos, ocorrerá um erro de tempo de compilação. Caso contrário, a resolução de sobrecarga será aplicada com a lista de expressões do inicializador de elementos como a lista de argumentos e o inicializador de coleção invocará o método resultante. Portanto, o objeto de coleção deve conter uma instância ou método de extensão aplicável com o nome Add para cada inicializador de elemento.

Exemplo: o seguinte mostra uma classe que representa um contato com um nome e uma lista de números de telefone e a criação e inicialização de um List<Contact>:

public class Contact
{
    public string Name { get; set; }
    public List<string> PhoneNumbers { get; } = new List<string>();
}

class A
{
    static void M()
    {
        var contacts = new List<Contact>
        {
            new Contact
            {
                Name = "Chris Smith",
                PhoneNumbers = { "206-555-0101", "425-882-8080" }
            },
            new Contact
            {
                Name = "Bob Harris",
                PhoneNumbers = { "650-555-0199" }
            }
        };
    }
}

que tem o mesmo efeito que

var __clist = new List<Contact>();
Contact __c1 = new Contact();
__c1.Name = "Chris Smith";
__c1.PhoneNumbers.Add("206-555-0101");
__c1.PhoneNumbers.Add("425-882-8080");
__clist.Add(__c1);
Contact __c2 = new Contact();
__c2.Name = "Bob Harris";
__c2.PhoneNumbers.Add("650-555-0199");
__clist.Add(__c2);
var contacts = __clist;

em que __clist, __c1 e __c2 são variáveis temporárias que são invisíveis e inacessíveis.

fim do exemplo

12.8.17.3 Expressões de criação de objeto anônimo

Um anonymous_object_creation_expression é usado para criar um objeto de tipo anônimo.

anonymous_object_creation_expression
    : 'new' anonymous_object_initializer
    ;

anonymous_object_initializer
    : '{' member_declarator_list? '}'
    | '{' member_declarator_list ',' '}'
    ;

member_declarator_list
    : member_declarator (',' member_declarator)*
    ;

member_declarator
    : simple_name
    | member_access
    | null_conditional_projection_initializer
    | base_access
    | identifier '=' expression
    ;

Um inicializador de objeto anônimo declara um tipo anônimo e retorna uma instância desse tipo. Um tipo anônimo é um tipo de classe sem nome que herda diretamente de object. Os membros de um tipo anônimo constituem uma sequência de propriedades somente leitura inferidas do inicializador de objeto anônimo usado para criar uma instância do tipo. Especificamente, um inicializador de objeto anônimo do formato

new { p₁=e₁,p₂=e₂,Pv=Ev}

declara um tipo anônimo do formato

class __Anonymous1
{
    private readonly «T1» «f1»;
    private readonly «T2» «f2»;
    ...
    private readonly «Tn» «fn»;

    public __Anonymous1(«T1» «a1», «T2» «a2»,..., «Tn» «an»)
    {
        «f1» = «a1»;
        «f2» = «a2»;
        ...
        «fn» = «an»;
    }

    public «T1» «p1» { get { return «f1»; } }
    public «T2» «p2» { get { return «f2»; } }
    ...
    public «Tn» «pn» { get { return «fn»; } }
    public override bool Equals(object __o) { ... }
    public override int GetHashCode() { ... }
}

em que cada «Tx» é o tipo da expressão correspondente «ex». A expressão usada em um member_declarator deve ter um tipo. Portanto, é um erro de tempo de compilação para uma expressão em um member_declarator ser null ou uma função anônima.

Os nomes de um tipo anônimo e do parâmetro para seu próprio método Equals são gerados automaticamente pelo compilador e não podem ser referenciados no texto do programa.

No mesmo programa, dois inicializadores de objetos anônimos que especificam uma sequência de propriedades dos mesmos nomes e tipos de tempo de compilação na mesma ordem produzirão instâncias do mesmo tipo anônimo.

Exemplo: no exemplo

var p1 = new { Name = "Lawnmower", Price = 495.00 };
var p2 = new { Name = "Shovel", Price = 26.95 };
p1 = p2;

a atribuição na última linha é permitida porque p1 e p2 são do mesmo tipo anônimo.

fim do exemplo

Os métodos Equals e GetHashcode em tipos anônimos substituem os métodos herdados de object e são definidos em termos de Equals e GetHashcode das propriedades, de modo que duas instâncias do mesmo tipo anônimo sejam iguais se e somente se todas as suas propriedades forem iguais.

Um declarador de membro pode ser abreviado para um nome simples (§12.8.4), um acesso de membro (§12.8.7), um inicializador de projeção condicional nulo §12.8.8 ou um acesso base (§12.8.15). Isso é chamado de inicializador de projeção e é uma abreviação para a declaração e a atribuição a uma propriedade com o mesmo nome. Especificamente, declaradores de membros das formas

«identifier», «expr» . «identifier» e «expr» ? . «identifier»

são exatamente equivalentes ao seguinte, respectivamente:

«identifer» = «identifier», «identifier» = «expr» . «identifier» e «identifier» = «expr» ? . «identifier»

Assim, em um inicializador de projeção, o identificador seleciona o valor e o campo ou a propriedade ao quais o valor é atribuído. Intuitivamente, um inicializador de projeção projeta não apenas um valor, mas também o nome do valor.

12.8.17.4 Expressões de criação de matriz

array_creation_expression é usado para criar uma nova instância de array_type.

array_creation_expression
    : 'new' non_array_type '[' expression_list ']' rank_specifier*
      array_initializer?
    | 'new' array_type array_initializer
    | 'new' rank_specifier array_initializer
    ;

Uma expressão de criação de matriz do primeiro formato aloca uma instância de matriz do tipo que resulta da exclusão de cada uma das expressões individuais da lista de expressões.

Exemplo: a expressão de criação da matriz new int[10,20] produz uma instância de matriz do tipo int[,] e a nova expressão de criação da matriz int[10][,] produz uma instância de matriz do tipo int[][,]. fim do exemplo

Cada expressão na lista de expressões deve ser do tipo int, uint, long ou ulong ou implicitamente conversível para um ou mais desses tipos. O valor de cada expressão determina o tamanho da dimensão correspondente na instância de matriz recém-alocada. Como o comprimento de uma dimensão de matriz deve ser não negativo, é um erro em tempo de compilação ter uma expressão constante com um valor negativo na lista de expressões.

Exceto em um contexto não seguro (§23.2), o layout das matrizes não é especificado.

Se uma expressão de criação de matriz do primeiro formato incluir um inicializador de matriz, cada expressão na lista de expressões será uma constante e os tamanhos de dimensão e a classificação especificados pela lista de expressões corresponderão aos do inicializador da matriz.

Em uma expressão de criação de matriz do segundo ou terceiro formato, a classificação do tipo de matriz especificada ou do especificador de classificação deve corresponder à do inicializador de matriz. Os tamanhos de dimensão individuais são inferidos do número de elementos em cada um dos níveis de aninhamento correspondentes do inicializador de matriz. Assim, a expressão de inicialização na declaração a seguir

var a = new int[,] {{0, 1}, {2, 3}, {4, 5}};

corresponde exatamente a

var a = new int[3, 2] {{0, 1}, {2, 3}, {4, 5}};

Uma expressão de criação de matriz do terceiro formato é conhecida como uma expressão de criação de matriz tipada implicitamente. É semelhante ao segundo formato, exceto que o tipo de elemento da matriz não é fornecido explicitamente, mas determinado como o melhor tipo comum (§12.6.3.15) do conjunto de expressões no inicializador de matriz. Para uma matriz multidimensional, ou seja, uma em que o rank_specifier contém pelo menos uma vírgula, esse conjunto inclui todas as expressões encontradas em array_initializers aninhados.

Inicializadores de matriz são descritos mais adiante em §17.7.

O resultado da avaliação de uma expressão de criação de matriz é classificado como um valor, ou seja, uma referência à instância de matriz recém-alocada. O processamento em run-time de uma expressão de criação de matriz consiste nas seguintes etapas:

  • As expressões de tamanho da dimensão da expression_list são avaliadas em ordem, da esquerda para a direita. Após a avaliação de cada expressão, uma conversão implícita (§10.2) para um dos seguintes tipos é executada: int, uint, long, ulong. O primeiro tipo nessa lista para o qual existe uma conversão implícita é escolhido. Se a avaliação de uma expressão ou a conversão implícita subsequente causar uma exceção, nenhuma expressão adicional será avaliada e nenhuma outra etapa será executada.
  • Os valores computados para os tamanhos de dimensão são validados da seguinte maneira: se um ou mais dos valores forem menores que zero, System.OverflowException será gerado e nenhuma etapa adicional será executada.
  • Uma instância de matriz com os tamanhos de dimensão especificados é alocada. Se não houver memória suficiente disponível para alocar a nova instância, System.OutOfMemoryException será gerado e nenhuma outra etapa será executada.
  • Todos os elementos da nova instância de matriz são inicializados com seus valores padrão (§9.3).
  • Se a expressão de criação da matriz contiver um inicializador de matriz, cada expressão no inicializador de matriz será avaliada e atribuída ao elemento de matriz correspondente. As avaliações e as atribuições são executadas na ordem em que as expressões são escritas no inicializador da matriz – em outras palavras, os elementos são inicializados em ordem de índice crescente, com a dimensão mais à direita aumentando primeiro. Se a avaliação de uma determinada expressão ou a atribuição subsequente ao elemento de matriz correspondente causar uma exceção, nenhum elemento adicional será inicializado (e os elementos restantes terão, portanto, seus valores padrão).

Uma expressão de criação de matriz permite a instanciação de uma matriz com elementos de um tipo de matriz, mas os elementos de tal matriz devem ser inicializados manualmente.

Exemplo: a instrução

int[][] a = new int[100][];

cria uma matriz unidimensional com 100 elementos do tipo int[]. O valor inicial de cada elemento é null. Não é possível que a mesma expressão de criação de matriz também instancie as submatrizes e a instrução

int[][] a = new int[100][5]; // Error

resulta em um erro de tempo de compilação. A instanciação das submatrizes pode, em vez disso, ser executada manualmente, como em

int[][] a = new int[100][];
for (int i = 0; i < 100; i++)
{
    a[i] = new int[5];
}

fim do exemplo

Observação: quando uma matriz de matrizes tem uma forma "retangular", ou seja, quando as submatrizes têm o mesmo tamanho, é mais eficiente usar uma matriz multidimensional. No exemplo acima, a instanciação da matriz de matrizes cria 101 objetos – uma matriz externa e 100 submatrizes. Em contraste,

int[,] a = new int[100, 5];

cria apenas um único objeto, uma matriz bidimensional e realiza a alocação em uma única instrução.

fim da observação

Exemplo: veja a seguir exemplos de expressões de criação de matriz tipadas implicitamente:

var a = new[] { 1, 10, 100, 1000 };                     // int[]
var b = new[] { 1, 1.5, 2, 2.5 };                       // double[]
var c = new[,] { { "hello", null }, { "world", "!" } }; // string[,]
var d = new[] { 1, "one", 2, "two" };                   // Error

A última expressão causa um erro de tempo de compilação porque nem int nem string é implicitamente conversível para o outro e, portanto, não há o melhor tipo comum. Uma expressão de criação de matriz tipada explicitamente deve ser usada nesse caso, por exemplo, especificando-se o tipo a ser object[]. Como alternativa, um dos elementos pode ser convertido em um tipo base comum, que se tornaria o tipo de elemento inferido.

fim do exemplo

Expressões de criação de matriz digitadas implicitamente podem ser combinadas com inicializadores de objetos anônimos (§12.8.17.3) para criar estruturas de dados tipadas anonimamente.

Exemplo:

var contacts = new[]
{
    new
    {
        Name = "Chris Smith",
        PhoneNumbers = new[] { "206-555-0101", "425-882-8080" }
    },
    new 
    {
        Name = "Bob Harris",
       PhoneNumbers = new[] { "650-555-0199" }
    }
};

fim do exemplo

12.8.17.5 Expressões de criação de delegado

Uma delegate_creation_expression é usada para obter uma instância de um delegate_type.

delegate_creation_expression
    : 'new' delegate_type '(' expression ')'
    ;

O argumento de uma expressão de criação de delegado deve ser um grupo de métodos, uma função anônima ou um valor do tipo de tempo de compilação dynamic ou um delegate_type. Se o argumento for um grupo de métodos, ele identificará o método e, para um método de instância, o objeto para o qual criar um delegado. Se o argumento for uma função anônima, ele definirá diretamente os parâmetros e o corpo do método do destino delegado. Se o argumento for um valor, ele identificará uma instância delegada da qual criar uma cópia.

Se a expressão tiver o tipo de tempo de compilação dynamic, a delegate_creation_expression será associada dinamicamente (§12.8.17.5) e as regras abaixo serão aplicadas durante a execução usando o tipo de tempo de execução da expressão. Caso contrário, as regras serão aplicadas em tempo de compilação.

O processamento de tempo de associação de uma delegate_creation_expression do novo formato D(E), onde D é um delegate_type e E é uma expressão, consiste nas seguintes etapas:

  • Se E for um grupo de métodos, a expressão de criação de delegado será processada da mesma forma que uma conversão de grupo de métodos (§10.8) de E para D.

  • Se E for uma função anônima, a expressão de criação de delegado será processada da mesma forma que uma conversão de função anônima (§10.7) de E para D.

  • Se E for um valor, E será compatível (§20.2) com D, e o resultado será uma referência a um delegado recém-criado com uma lista de invocação de entrada única que invoca E.

O processamento em run-time de uma delegate_creation_expression do novo formato D(E), onde D é um delegate_type e E é uma expressão, consiste nas seguintes etapas:

  • Se E for um grupo de métodos, a expressão de criação de delegado será avaliada como uma conversão de grupo de métodos (§10.8) de E para D.
  • Se E for uma função anônima, a criação de delegado será avaliada como uma conversão de função anônima de E para D (§10.7).
  • Se E for um valor de delegate_type:
    • E é avaliado. Se essa avaliação causar uma exceção, nenhuma etapa adicional será executada.
    • Se o valor de E for null, System.NullReferenceException será lançado e nenhuma etapa adicional será executada.
    • Uma nova instância do tipo de delegado D é alocada. Se não houver memória suficiente disponível para alocar a nova instância, System.OutOfMemoryException será gerado e nenhuma outra etapa será executada.
    • A nova instância de delegado é inicializada com uma lista de invocação de única entrada que invoca E.

A lista de invocação de um delegado é determinada quando o delegado é instanciado e, em seguida, permanece constante durante todo o tempo de vida de delegado. Em outras palavras, não é possível alterar as entidades chamáveis de destino de um delegado depois que ele for criado.

Observação: lembre-se de que, quando dois delegados são combinados ou um é removido de outro, um novo delegado resulta; nenhum delegado existente tem seu conteúdo alterado. fim da observação

Não é possível criar um delegado que se refira a uma propriedade, indexador, operador definido pelo usuário, construtor de instância, finalizador ou construtor estático.

Exemplo: conforme descrito acima, quando um delegado é criado direto de um grupo de métodos, a lista de parâmetros e o tipo de retorno de delegado determinam quais dos métodos sobrecarregados selecionar. No exemplo

delegate double DoubleFunc(double x);

class A
{
    DoubleFunc f = new DoubleFunc(Square);

    static float Square(float x) => x * x;
    static double Square(double x) => x * x;
}

o campo A.f é inicializado com um delegado que se refere ao segundo método Square porque esse método corresponde exatamente à lista de parâmetros e ao tipo de retorno de DoubleFunc. Se o segundo método Square não estivesse presente, um erro de tempo de compilação teria ocorrido.

fim do exemplo

12.8.18 Operador typeof

O operador typeof é usado para obter o objeto System.Type para um tipo.

typeof_expression
    : 'typeof' '(' type ')'
    | 'typeof' '(' unbound_type_name ')'
    | 'typeof' '(' 'void' ')'
    ;

unbound_type_name
    : identifier generic_dimension_specifier? ('.' identifier generic_dimension_specifier?)*
    | unbound_qualified_alias_member ('.' identifier generic_dimension_specifier?)*
    ;

unbound_qualified_alias_member
    : identifier '::' identifier generic_dimension_specifier?
    ;

generic_dimension_specifier
    : '<' comma* '>'
    ;

comma
    : ','
    ;

O primeiro formato de typeof_expression consiste em uma palavra-chave typeof seguida por um tipo entre parênteses. O resultado de uma expressão desse formato é o objeto System.Type para o tipo indicado. Há apenas um objeto System.Type para qualquer tipo específico. Isso significa que, para um tipo T, typeof(T) == typeof(T) é sempre verdadeiro. O tipo não pode ser dynamic.

O segundo formato de typeof_expression consiste em uma palavra-chave typeof seguida por um unbound_type_nameentre parênteses.

Observação: as gramáticas de unbound_type_name e unbound_qualified_alias_member seguem as de type_name (§7.8) e qualified_alias_member (§14.8.1), exceto que os generic_dimension_specifiersão substituídos por type_argument_lists. fim da observação

Ao reconhecer o operando de uma typeof_expression se unbound_type_name e type_name forem aplicáveis, ou seja, quando não contiver um generic_dimension_specifier nem um type_argument_list, type_name será escolhido.

Observação: o ANTLR faz a escolha especificada automaticamente devido à ordenação das alternativas de typeof_expression. fim da observação

O significado de um unbound_type_name é determinado como se:

  • A sequência de tokens é convertida em um type_name substituindo cada generic_dimension_specifier por um type_argument_list com o mesmo número de vírgulas e a palavra-chave object que cada type_argument.
  • O type_name resultante é resolvido para um tipo construído (§7,8).
  • O unbound_type_name é então o tipo genérico não associado associado ao tipo construído resolvido (§8.4).

Observação: não há nenhum requisito para que uma implementação transforme a sequência de tokens ou produza o tipo construído intermediário, apenas que o tipo genérico não associado que é determinado é "como se" esse processo fosse seguido. fim da observação

É um erro que o nome do tipo seja um tipo de referência anulável.

O resultado de typeof_expression é o objeto System.Type para o tipo genérico não associado resultante.

A terceira forma de typeof_expression consiste em uma palavra-chave typeof seguida por uma palavra-chave void entre parênteses. O resultado de uma expressão desse formato é o objeto System.Type que representa a ausência de um tipo. O objeto type retornado por typeof(void) é distinto do objeto type retornado para qualquer tipo.

Observação: este objeto especial System.Type é útil em bibliotecas de classes que permitem reflexão de métodos da linguagem, em que esses métodos desejam ter uma maneira de representar o tipo de retorno de qualquer método, incluindo os métodos void, com uma instância de System.Type. fim da observação

O operador typeof pode ser usado em um parâmetro de tipo. É um erro de tempo de compilação se o nome do tipo for conhecido por ser um tipo de referência anulável. O resultado é o objeto System.Type para o tipo de run-time associado ao parâmetro de tipo. Se o tipo de run-time for um tipo de referência anulável, o resultado será o tipo de referência não anulável correspondente. O operador typeof também pode ser usado em um tipo construído ou em um tipo genérico não associado (§8.4.4). O objeto System.Type para um tipo genérico não associado não é o mesmo que o objeto System.Type do tipo de instância (§15.3.2). O tipo de instância é sempre um tipo construído fechado em run-time, de modo que seu objeto System.Type depende dos argumentos de tipo de run-time em uso. O tipo genérico não associado, por outro lado, não tem argumentos de tipo e produz o mesmo objeto System.Type, independentemente dos argumentos de tipo de run-time.

Exemplo: o exemplo

class X<T>
{
    public static void PrintTypes()
    {
        Type[] t =
        {
            typeof(int),
            typeof(System.Int32),
            typeof(string),
            typeof(double[]),
            typeof(void),
            typeof(T),
            typeof(X<T>),
            typeof(X<X<T>>),
            typeof(X<>)
        };
        for (int i = 0; i < t.Length; i++)
        {
            Console.WriteLine(t[i]);
        }
    }
}

class Test
{
    static void Main()
    {
        X<int>.PrintTypes();
    }
}

produz a saída a seguir:

System.Int32
System.Int32
System.String
System.Double[]
System.Void
System.Int32
X`1[System.Int32]
X`1[X`1[System.Int32]]
X`1[T]

Observe que int e System.Int32 são do mesmo tipo. O resultado de typeof(X<>) não depende do argumento de tipo, mas o resultado de typeof(X<T>) depende.

fim do exemplo

12.8.19 Operador sizeof

O operador sizeof retorna o número de bytes de 8 bits ocupados por uma variável de um determinado tipo. O tipo especificado como um operando ao sizeof deve ser um unmanaged_type (§8.8).

sizeof_expression
    : 'sizeof' '(' unmanaged_type ')'
    ;

Para determinados tipos predefinidos, o operador sizeof produz um valor de int constante, conforme mostrado na tabela abaixo:

Expressão Resultado
sizeof(sbyte) 1
sizeof(byte) 1
sizeof(short) 2
sizeof(ushort) 2
sizeof(int) 4
sizeof(uint) 4
sizeof(long) oito
sizeof(ulong) oito
sizeof(char) 2
sizeof(float) 4
sizeof(double) oito
sizeof(bool) 1
sizeof(decimal) 16

Para um tipo de enumeração T, o resultado da expressão sizeof(T) é um valor constante igual ao tamanho de seu tipo subjacente, conforme fornecido acima. Para todos os outros tipos de operando, o operador sizeof é especificado em §23.6.9.

12.8.20 Os operadores verificados e não verificados

Os operadores checked e unchecked são usados para controlar o contexto de verificação de estouro para operações e conversões aritméticas do tipo integral.

checked_expression
    : 'checked' '(' expression ')'
    ;

unchecked_expression
    : 'unchecked' '(' expression ')'
    ;

O operador checked avalia a expressão contida em um contexto verificado e o operador unchecked avalia a expressão contida em um contexto não verificado. Uma checked_expression ou unchecked_expression corresponde exatamente a uma parenthesized_expression (§12.8.5), com exceção de que a expressão contida é avaliada no contexto de verificação de estouro fornecido.

O contexto de verificação de estouro também pode ser controlado com as instruções checked e unchecked (§13.12).

As seguintes operações são afetadas pelo contexto de verificação de estouro estabelecido pelos operadores e instruções verificados e não verificados:

  • Os operadores predefinidos ++ e -- (§12.8.16 e §12.9.6), quando o operando é de um tipo integral ou de enumeração.
  • O operador predefinido - unário (§12.9.3), quando o operando é de um tipo integral.
  • Os operadores binários predefinidos +, -, * e / (§12.10), quando os dois operandos são de tipos integrais ou enumerados.
  • Conversões numéricas explícitas (§10.3.2) de um tipo integral ou enum para outro tipo integral ou enum, ou de float ou double para um tipo integral ou enum.

Quando uma das operações acima produz um resultado muito grande para se representar no tipo de destino, o contexto no qual a operação é executada controla o comportamento resultante:

  • Em um contexto checked, se a operação for uma expressão constante (§12.23), ocorrerá um erro em tempo de compilação. Caso contrário, quando a operação for executada em run-time, System.OverflowException será lançado.
  • Em um contexto unchecked, o resultado é truncado pelo descarte dos bits de ordem superior que não se ajustam ao tipo de destino.

Para expressões não constantes (§12.23) (expressões que são avaliadas em tempo de execução) que não estão delimitadas por nenhum operador ou instrução checked ou unchecked, o contexto padrão de verificação de estouro é não verificado, a menos que fatores externos (como opções do compilador e configuração do ambiente de execução) requeiram avaliação verificada.

Para expressões de constante (§12.23) (expressões que podem ser totalmente avaliadas no tempo de compilação), o contexto de verificação de estouro padrão sempre é verificado. A menos que uma expressão de constante seja explicitamente colocada em um contexto unchecked, estouros que ocorrerem durante a avaliação do tempo de compilação da expressão sempre causarão erros de tempo de compilação.

O corpo de uma função anônima não é afetado por contextos checked ou unchecked em que a função anônima ocorre.

Exemplo: no código a seguir

class Test
{
    static readonly int x = 1000000;
    static readonly int y = 1000000;

    static int F() => checked(x * y);    // Throws OverflowException
    static int G() => unchecked(x * y);  // Returns -727379968
    static int H() => x * y;             // Depends on default
}

nenhum erro de tempo de compilação é relatado, pois nenhuma das expressões pode ser avaliada em tempo de compilação. Em run-time, o método F lança um System.OverflowExceptione o método G retorna –727379968 (os 32 bits inferiores do resultado fora do intervalo). O comportamento do método H depende do contexto de verificação de estouro padrão da compilação, mas é igual a F ou G.

fim do exemplo

Exemplo: no código a seguir

class Test
{
    const int x = 1000000;
    const int y = 1000000;

    static int F() => checked(x * y);    // Compile-time error, overflow
    static int G() => unchecked(x * y);  // Returns -727379968
    static int H() => x * y;             // Compile-time error, overflow
}

os estouros que ocorrem ao avaliar as expressões constantes em F e H fazem com que erros de tempo de compilação sejam relatados porque as expressões são avaliadas em um contexto checked. Um estouro também ocorre ao avaliar a expressão constante em G, mas como a avaliação ocorre em um contexto de unchecked, o estouro não é relatado.

fim do exemplo

Os operadores checked e unchecked afetam apenas o contexto de verificação de estouro das operações contidas textualmente nos tokens "(" e ")". Os operadores não têm efeito sobre os membros da função que são invocados como resultado da avaliação da expressão contida.

Exemplo: no código a seguir

class Test
{
    static int Multiply(int x, int y) => x * y;

    static int F() => checked(Multiply(1000000, 1000000));
}

o uso de checked em F não afeta a avaliação de x * y em Multiply, assim, x * y é avaliado no contexto padrão de verificação de estouro.

fim do exemplo

O operador unchecked é conveniente ao escrever constantes dos tipos integrais assinados na notação hexadecimal.

Exemplo:

class Test
{
    public const int AllBits = unchecked((int)0xFFFFFFFF);
    public const int HighBit = unchecked((int)0x80000000);
}

As duas constantes hexadecimal acima são do tipo uint. Como as constantes estão fora do intervalo de int, sem o operador unchecked, as conversões para int produziriam erros de tempo de compilação.

fim do exemplo

Observação: os operadores e as instruções checked e unchecked permitem que os programadores controlem alguns aspectos de alguns cálculos numéricos. No entanto, o comportamento de alguns operadores numéricos depende dos tipos de dados de seus operandos. Por exemplo, multiplicar dois números decimais sempre resulta em uma exceção de estouro mesmo dentro de um bloco explicitamente não verificado. Da mesma forma, multiplicar dois floats nunca resulta em uma exceção por estouro, mesmo dentro de uma construção explicitamente verificada. Além disso, outros operadores nunca são afetados pelo modo de verificação, seja ele padrão ou explícito. fim da observação

12.8.21 Expressões de valor padrão

Uma expressão de valor padrão é usada para obter o valor padro (§9.3) de um tipo.

default_value_expression
    : explictly_typed_default
    | default_literal
    ;

explictly_typed_default
    : 'default' '(' type ')'
    ;

default_literal
    : 'default'
    ;

default_literal representa um valor padrão (§9.3). Ele não tem um tipo, mas pode ser convertido em qualquer tipo por meio de uma conversão literal padrão (§10.2.16).

O resultado de uma default_value_expression é o padrão (§9.3) do tipo explícito em um explictly_typed_defaultou o tipo de destino da conversão para uma default_value_expression.

default_value_expression é uma expressão constante (§12.23) se o tipo for um dos seguintes:

  • um tipo de referência
  • um parâmetro de tipo conhecido por ser um tipo de referência (§8.2);
  • um dos seguintes tipos de valor: sbyte, byte, short, ushort, int, uint, long, ulong, char, float, double, decimal, bool,; ou
  • qualquer tipo de enumeração.

12.8.22 Alocação da pilha

Uma expressão de alocação de pilha aloca um bloco de memória na pilha de execução. A pilha de execução é uma área de memória em que as variáveis locais são armazenadas. A pilha de execução não faz parte do heap gerenciado. A memória usada para o armazenamento de variável local é recuperada automaticamente quando a função atual retorna.

As regras de contexto seguro para uma expressão de alocação de pilha são descritas em §16.4.15.7.

stackalloc_expression
    : 'stackalloc' unmanaged_type '[' expression ']'
    | 'stackalloc' unmanaged_type? '[' constant_expression? ']' stackalloc_initializer
    ;

stackalloc_initializer
     : '{' stackalloc_initializer_element_list '}'
     ;

stackalloc_initializer_element_list
     : stackalloc_element_initializer (',' stackalloc_element_initializer)* ','?
     ;
    
stackalloc_element_initializer
    : expression
    ;

unmanaged_type (§8.8) indica o tipo dos itens que serão armazenados no local recém-alocado e a expressão indica o número desses itens. Juntos, eles especificam o tamanho de alocação necessário. O tipo de expressão deve ser conversível implicitamente para o tipo int.

Como o tamanho de uma alocação de pilha não pode ser negativo, é um erro de tempo de compilação especificar o número de itens como uma constant_expression que resulta em um valor negativo.

Em run-time, se o número de itens a serem alocados for um valor negativo, o comportamento será indefinido. Se for zero, nenhuma alocação será feita e o valor retornado será definido pela implementação. Se não houver memória suficiente disponível para alocar os itens, System.StackOverflowException será gerado.

Quando stackalloc_initializer está presente:

  • Se unmanaged_type for omitido, será inferido seguindo as regras de melhor tipo comum (§12.6.3.15) para o conjunto de stackalloc_element_initializers.
  • Caso constant_expression seja omitido, o número de stackalloc_element_initializers será inferido.
  • Se constant_expression estiver presente, deverá ser igual ao número de stackalloc_element_initializers.

Cada stackalloc_element_initializer deve ter uma conversão implícita para unmanaged_type (§10.2). O stackalloc_element_initializerinicializa elementos na memória alocada em ordem crescente, começando com o elemento no índice zero. Na ausência de stackalloc_initializer, o conteúdo da memória recém-alocada é indefinido.

Se uma stackalloc_expression ocorrer diretamente como a expressão de inicialização de uma local_variable_declaration (§13.6.2), em que o local_variable_type é um tipo de ponteiro (§23.3) ou inferido (var), o resultado da stackalloc_expression será um ponteiro do tipo T* (§23.9). Nesse caso, stackalloc_expression deve aparecer em código não seguro. Caso contrário, o resultado de stackalloc_expression é uma instância do tipo Span<T>, em que T é unmanaged_type:

  • Span<T> (§C.3) é um tipo de estrutura ref (§16.2.3), que apresenta um bloco de memória, aqui o bloco alocado por stackalloc_expression, como uma coleção indexável de itens tipados (T).
  • A propriedade Length do resultado retorna o número de itens alocados.
  • O indexador do resultado (§15.9) retorna uma variable_reference (§9.5) para um item do bloco alocado e o intervalo é verificado.

Inicializadores de alocação de pilha não são permitidos em blocos catch ou finally (§13.11).

Observação: não há como liberar explicitamente a memória alocada usando-se stackalloc. fim da observação

Todos os blocos de memória alocados em pilha criados durante a execução de um membro da função são descartados automaticamente quando esse membro da função é retornado.

Com exceção do operador stackalloc, C# não fornece construções predefinidas para gerenciar memória coletada não de lixo. Esses serviços normalmente são fornecidos por meio do suporte a bibliotecas de classes ou importados diretamente do sistema operacional subjacente.

Exemplo:

// Memory uninitialized
Span<int> span1 = stackalloc int[3];
// Memory initialized
Span<int> span2 = stackalloc int[3] { -10, -15, -30 };
// Type int is inferred
Span<int> span3 = stackalloc[] { 11, 12, 13 };
// Error; result is int*, not allowed in a safe context
var span4 = stackalloc[] { 11, 12, 13 };
// Error; no conversion from Span<int> to Span<long>
Span<long> span5 = stackalloc[] { 11, 12, 13 };
// Converts 11 and 13, and returns Span<long> 
Span<long> span6 = stackalloc[] { 11, 12L, 13 };
// Converts all and returns Span<long>
Span<long> span7 = stackalloc long[] { 11, 12, 13 };
// Implicit conversion of Span<T>
ReadOnlySpan<int> span8 = stackalloc int[] { 10, 22, 30 };
// Implicit conversion of Span<T>
Widget<double> span9 = stackalloc double[] { 1.2, 5.6 };

public class Widget<T>
{
    public static implicit operator Widget<T>(Span<double> sp) { return null; }
}

No caso de span8, stackalloc resulta em um Span<int>, que é convertido por um operador implícito em ReadOnlySpan<int>. Da mesma forma, para span9, Span<double> resultante é convertido no tipo definido pelo usuário Widget<double> usando-se a conversão, conforme mostrado. fim do exemplo

12.8.23 Operador nameof

nameof_expression é usado para obter o nome de uma entidade de programa como uma cadeia de caracteres constante.

nameof_expression
    : 'nameof' '(' named_entity ')'
    ;
    
named_entity
    : named_entity_target ('.' identifier type_argument_list?)*
    ;
    
named_entity_target
    : simple_name
    | 'this'
    | 'base'
    | predefined_type 
    | qualified_alias_member
    ;

Como nameof não é uma palavra-chave, nameof_expression é sempre sintaticamente ambíguo com uma invocação do nome simples nameof. Por motivos de compatibilidade, se uma pesquisa (§12.8.4) do nome nameof for bem-sucedida, a expressão será tratada como uma invocation_expression, independentemente de a invocação ser válida. Caso contrário, é uma nameof_expression.

Pesquisas de acesso a membro e nome simples são realizadas em named_entity durante o tempo de compilação, seguindo-se as regras descritas em §12.8.4 e §12.8.7. No entanto, quando a pesquisa descrita em §12.8.4 e §12.8.7 resulta em um erro porque um membro da instância foi encontrado em um contexto estático, nameof_expression não produz esse erro.

É um erro em tempo de compilação de uma named_entity que designa um grupo de métodos para ter uma type_argument_list. É um erro de tempo de compilação para um named_entity_target ter o tipo dynamic.

nameof_expression é uma expressão constante do tipo string e não tem efeito em run-time. Especificamente, a named_entity não é avaliada e é ignorada para fins de análise de atribuição definitiva (§9.4.4.22). Seu valor é o último identificador da named_entity antes da type_argument_list final opcional, transformado do formato seguinte:

  • O prefixo "@", se usado, é removido.
  • Cada unicode_escape_sequence é transformada no caractere Unicode correspondente.
  • Todos os formatting_characters são removidos.

Essas são as mesmas transformações aplicadas em §6.4.3 ao testar-se a igualdade entre identificadores.

Exemplo: veja a seguir os resultados de várias expressões nameof, supondo um tipo genérico List<T> declarado no namespace System.Collections.Generic:

using TestAlias = System.String;

class Program
{
    static void Main()
    {
        var point = (x: 3, y: 4);

        string n1 = nameof(System);                      // "System"
        string n2 = nameof(System.Collections.Generic);  // "Generic"
        string n3 = nameof(point);                       // "point"
        string n4 = nameof(point.x);                     // "x"
        string n5 = nameof(Program);                     // "Program"
        string n6 = nameof(System.Int32);                // "Int32"
        string n7 = nameof(TestAlias);                   // "TestAlias"
        string n8 = nameof(List<int>);                   // "List"
        string n9 = nameof(Program.InstanceMethod);      // "InstanceMethod"
        string n10 = nameof(Program.GenericMethod);      // "GenericMethod"
        string n11 = nameof(Program.NestedClass);        // "NestedClass"

        // Invalid
        // string x1 = nameof(List<>);            // Empty type argument list
        // string x2 = nameof(List<T>);           // T is not in scope
        // string x3 = nameof(GenericMethod<>);   // Empty type argument list
        // string x4 = nameof(GenericMethod<T>);  // T is not in scope
        // string x5 = nameof(int);               // Keywords not permitted
        // Type arguments not permitted for method group
        // string x6 = nameof(GenericMethod<Program>);
    }

    void InstanceMethod() { }

    void GenericMethod<T>()
    {
        string n1 = nameof(List<T>); // "List"
        string n2 = nameof(T);       // "T"
    }

    class NestedClass { }
}

Partes potencialmente surpreendentes deste exemplo são a resolução de nameof(System.Collections.Generic) como apenas "Generic" em vez do namespace completo e de nameof(TestAlias) como "TestAlias" em vez de "String". fim do exemplo

12.8.24 Expressões de método anônimo

Uma anonymous_method_expression é uma das duas maneiras de definir uma função anônima. Isso será descrito mais abaixo em §12.19.

12.9 Operadores unários

12.9.1 Geral

Os operadores +, -, ! (apenas negação lógica §12.9.4), ~, ++, --, conversão e await são chamados de operadores unários.

Observação: o operador tolerante a valores nulos pós-fixado (§12.8.9), !, devido à sua natureza somente de tempo de compilação e não sobrecarregável, é excluído da lista acima. fim da observação

unary_expression
    : primary_expression
    | '+' unary_expression
    | '-' unary_expression
    | logical_negation_operator unary_expression
    | '~' unary_expression
    | pre_increment_expression
    | pre_decrement_expression
    | cast_expression
    | await_expression
    | pointer_indirection_expression    // unsafe code support
    | addressof_expression              // unsafe code support
    ;

pointer_indirection_expression (§23.6.2) e addressof_expression (§23.6.5) estão disponíveis somente em código não seguro (§23).

Se o operando de uma unary_expression tiver o tipo de tempo de compilação dynamic, ele será associado dinamicamente (§12.3.3). Nesse caso, o tipo de tempo de compilação da unary_expression é dynamic, e a resolução descrita abaixo ocorrerá em run-time usando o tipo de run-time do operando.

12.9.2 Operador de adição de unário

Para uma operação do formato +x, a resolução de sobrecarga do operador unário (§12.4.4) é aplicada para selecionar uma implementação de operador específica. O operando é convertido no tipo de parâmetro do operador selecionado e o tipo do resultado é o tipo de retorno do operador. Os operadores de adição de unários predefinidos são:

int operator +(int x);
uint operator +(uint x);
long operator +(long x);
ulong operator +(ulong x);
float operator +(float x);
double operator +(double x);
decimal operator +(decimal x);

Para cada um desses operadores, o resultado é o valor do operando.

As formas elevadas (§12.4.8) dos operadores unários de adição predefinidos anteriormente mencionados também são predefinidas.

12.9.3 Operador de subtração de unário

Para uma operação do formato –x, a resolução de sobrecarga do operador unário (§12.4.4) é aplicada para selecionar uma implementação de operador específica. O operando é convertido no tipo de parâmetro do operador selecionado e o tipo do resultado é o tipo de retorno do operador. Os operadores de subtração de unários predefinidos são:

  • Negação de inteiro:

    int operator –(int x);
    long operator –(long x);
    

    O resultado é computado subtraindo-se X de zero. Se o valor de X for o menor valor representável do tipo do operando (−2³¹ para int ou −2⁶³ para long), então a negação matemática de X não será representável dentro do tipo do operando. Se isso ocorrer em um contexto checked, uma System.OverflowException será lançada; se ocorrer em um contexto unchecked, o resultado será o valor do operando e o estouro não será relatado.

    Se o operando do operador de negação for do tipo uint, ele será convertido para o tipo long, e o tipo do resultado será long. Uma exceção é a regra que permite o valor int−2147483648 (-2³¹) ser escrito como um literal inteiro decimal (§6.4.5.3).

    Se o operando do operador de negação for do tipo ulong, ocorrerá um erro de tempo de compilação. Uma exceção é a regra que permite que o valor long−9223372036854775808 (-2⁶³) seja escrito como um literal decimal inteiro (§6.4.5.3)

  • Negação de ponto flutuante:

    float operator –(float x);
    double operator –(double x);
    

    O resultado é o valor de X com seu sinal invertido. Se x for NaN, o resultado também será NaN.

  • Negação decimal:

    decimal operator –(decimal x);
    

    O resultado é computado subtraindo-se X de zero. A negação decimal é equivalente ao uso do operador de subtração unário do tipo System.Decimal.

As formas elevadas (§12.4.8) dos operadores unários de subtração predefinidos anteriormente mencionados também são predefinidas.

12.9.4 Operador de negação lógico

Para uma operação do formato !x, a resolução de sobrecarga do operador unário (§12.4.4) é aplicada para selecionar uma implementação de operador específica. O operando é convertido no tipo de parâmetro do operador selecionado e o tipo do resultado é o tipo de retorno do operador. Existe apenas um operador de negação lógico predefinido:

bool operator !(bool x);

Esse operador computa a negação lógica do operando: se o operando for true, o resultado será false. Se o operando for false, o resultado será true.

As formas elevadas (§12.4.8) do operador de negação lógica predefinido e não elevado definido acima também são predefinidas.

Observação: os operadores de negação lógica de prefixo e de perdão nulo de sufixo (§12.8.9), embora representados pelo mesmo símbolo léxico (!), são distintos. fim da observação

12.9.5 Operador de complemento bit a bit

Para uma operação do formato ~x, a resolução de sobrecarga do operador unário (§12.4.4) é aplicada para selecionar uma implementação de operador específica. O operando é convertido no tipo de parâmetro do operador selecionado e o tipo do resultado é o tipo de retorno do operador. Os operadores de complemento bit a bit predefinidos são:

int operator ~(int x);
uint operator ~(uint x);
long operator ~(long x);
ulong operator ~(ulong x);

Para cada um desses operadores, o resultado da operação é o complemento bit a bit de x.

Cada tipo de enumeração E fornece implicitamente o seguinte operador de complemento bit a bit:

E operator ~(E x);

O resultado da avaliação de ~x, em que X é uma expressão de um tipo de enumeração E com um tipo subjacente U, é exatamente o mesmo que avaliar (E)(~(U)x), exceto que a conversão em E sempre é executada como se estivesse em um contexto de unchecked (§12.8.20).

As formas elevadas (§12.4.8) dos operadores de complemento bit a bit predefinidos não elevados anteriormente mencionados também são predefinidas.

12.9.6 Operadores de incremento e de decremento pré-fixados

pre_increment_expression
    : '++' unary_expression
    ;

pre_decrement_expression
    : '--' unary_expression
    ;

O operando de uma operação de adição ou remoção pré-fixada deve ser uma expressão classificada como uma variável, um acesso de propriedade ou um acesso de indexador. O resultado da operação é um valor do mesmo tipo do operando.

Se o operando de uma operação de adição ou remoção prefixada for um acesso de propriedade ou indexador, a propriedade ou o indexador deverá ter acessores get e set. Se esse não for o caso, ocorrerá um erro de tempo de associação.

A resolução de sobrecarga de operador unário (§12.4.4) é aplicada para selecionar uma implementação de operador específica. Há operadores predefinidos ++ e -- para os seguintes tipos: sbyte, byte, short, ushort, int, uint, long, ulong, char, float, double, decimal e qualquer tipo de enumeração. Os operadores predefinidos ++ retornam o valor produzido adicionando-se 1 ao operando e os operadores predefinidos -- retornam o valor produzido pela subtração de 1 do operando. Em um contexto checked, se o resultado dessa adição ou subtração estiver fora do intervalo do tipo de resultado e o tipo de resultado for um tipo integral ou tipo de enumeração, System.OverflowException será lançado.

Deve haver uma conversão implícita do tipo de retorno do operador unário selecionado para o tipo unary_expression; caso contrário, ocorrerá um erro de tempo de compilação.

O processamento run-time de uma operação de incremento ou decremento pré-fixada no formato ++x ou --x consiste nas seguintes etapas:

  • Se x for classificado como uma variável:
    • x é avaliado para produzir a variável.
    • O valor de x é convertido no tipo de operando do operador selecionado e o operador é invocado com esse valor como seu argumento.
    • O valor retornado pelo operador é convertido no tipo de x. O valor resultante é armazenado no local fornecido pela avaliação de x e torna-se o resultado da operação.
  • Se x for classificado como uma propriedade ou acesso de indexador:
    • A expressão de instância (se x não for static) e a lista de argumentos (se x for um acesso de indexador) associadas a x serão avaliadas e os resultados serão usados nas invocações subsequentes do acessador get e set.
    • O acessador get de x é invocado.
    • O valor retornado pelo acessador get é convertido no tipo de operando do operador selecionado e o operador é invocado com esse valor como seu argumento.
    • O valor retornado pelo operador é convertido no tipo de x. O acessador set de x é invocado com esse valor como seu argumento de valor.
    • Esse valor também se torna o resultado da operação.

Os operadores ++ e -- também dão suporte à notação de pós-fixada (§12.8.16). O resultado de x++ ou x-- é o valor de x antes da operação, enquanto o resultado de ++x ou --x for o valor de x depois da operação. Nos dois casos, x em si tem o mesmo valor após a operação.

Uma implementação do operador ++ ou do operador -- pode ser invocada usando a notação pós-fixada ou pré-fixada. Não é possível ter implementações de operador separadas para as duas notações.

As formas elevadas (§12.4.8) dos operadores de adição e remoção pós-fixados predefinidos não elevados definidos acima também são predefinidos.

12.9.7 Expressões de conversão

cast_expression é usado para converter explicitamente uma expressão em um determinado tipo.

cast_expression
    : '(' type ')' unary_expression
    ;

Uma cast_expression do formato (T)E, em que T é um tipo e E é uma unary_expression, executa uma conversão explícita (§10.3) do valor de E no tipo T. Se não houver nenhuma conversão explícita de E para T, ocorrerá um erro de tempo de associação. Caso contrário, o resultado será o valor produzido pela conversão explícita. O resultado é sempre classificado como um valor, mesmo que E denote uma variável.

A gramática de cast_expression leva a algumas ambiguidades sintáticas.

Exemplo: a expressão (x)–y pode ser interpretada como um cast_expression (uma conversão de –y para o tipo x) ou como uma additive_expression combinada com uma parenthesized_expression (que calcula o valor x – y). fim do exemplo

Para resolver as ambiguidades de cast_expression, a seguinte regra existe: uma sequência de um ou mais tokens (§6.4) entre parênteses é considerada o início de cast_expression somente se pelo menos um dos seguintes itens for verdadeiro:

  • A sequência de tokens é a gramática correta para um tipo, mas não para uma expressão.
  • A sequência de tokens está correta para a gramática de um tipo, e o token imediatamente após o fechamento dos parênteses é o token "~", o token "!", o token "(", um identificador (§6.4.3), um literal (§6.4.5) ou qualquer palavra-chave (§6.4.4), exceto as e is.

O termo "gramática correta" acima significa apenas que a sequência de tokens deve estar em conformidade com a produção gramatical específica. Ele especificamente não considera o significado real de nenhum identificador constituinte.

Exemplo: se x e y forem identificadores, x.y será a gramática correta para um tipo, mesmo que x.y não denote realmente um tipo. fim do exemplo

Observação: na regra de desambiguação, segue-se que, se x e y forem identificadores, (x)y, (x)(y) e (x)(-y) serão cast_expressions, mas (x)-y não será, mesmo se x identificar um tipo. No entanto, se x for uma palavra-chave que identifique um tipo predefinido (como int), todas as quatro formas serão cast_expressions (porque essa palavra-chave não poderia ser uma expressão por si só). fim da observação

12.9.8 Expressões await

12.9.8.1 Geral

O operador await é usado para suspender a avaliação da função assíncrona circundante até que a operação assíncrona representada pelo operando seja concluída.

await_expression
    : 'await' unary_expression
    ;

Um await_expression só é permitido no corpo de uma função assíncrona (§15.14). Na função assíncrona circundante mais próxima, uma await_expression não deve ocorrer nestes locais:

  • Em uma função anônima aninhada (não assíncrona)
  • No bloco de uma lock_statement
  • Em uma conversão de função anônima em um tipo de árvore de expressão (§10.7.3)
  • Em um contexto não seguro

Observação: uma await_expression não pode ocorrer na maioria dos lugares em uma query_expression, pois esses lugares são transformados sintaticamente para usar expressões lambda não assíncronas. fim da observação

Em uma função assíncrona, await não deve ser usado como um available_identifier, embora o identificador de texto @await possa ser usado. Portanto, não há ambiguidade sintática entre await_expressione várias expressões envolvendo identificadores. Fora das funções assíncronas, await atua como um identificador normal.

O operando de uma await_expression é chamado de tarefa. Representa uma operação assíncrona que pode ou não ser concluída no momento em que o await_expression é avaliado. A finalidade do operador await é suspender a execução da função assíncrona delimitadora até que a tarefa aguardada seja concluída e, em seguida, obter o resultado.

12.9.8.2 Expressões aguardáveis

A tarefa de uma await_expression deve ser aguardável. Uma expressão t será aguardável se ocorrer uma das seguintes condições:

  • t é do tipo de tempo de compilação dynamic
  • t tem uma instância acessível ou um método de extensão chamado GetAwaiter sem parâmetros e sem parâmetros de tipo, e um tipo de retorno A para o qual todas as seguintes características são verdadeiras:
    • A implementa a interface System.Runtime.CompilerServices.INotifyCompletion (daqui em diante conhecido como INotifyCompletion para simplificação)
    • A tem uma propriedade de instância IsCompleted do tipo bool que é acessível e legível
    • A possui um método de instância acessível GetResult que não possui parâmetros nem parâmetros de tipo

O objetivo do método GetAwaiter é obter um awaiter para a tarefa. O tipo A é chamado de tipo awaiter para a expressão await.

A finalidade da propriedade IsCompleted é determinar se a tarefa já foi concluída. Nesse caso, não é necessário suspender a avaliação.

A finalidade do método INotifyCompletion.OnCompleted é associar uma "continuação" à tarefa; ou seja, um delegado (do tipo System.Action) que será invocado assim que a tarefa for concluída.

A finalidade do método GetResult é obter o resultado da tarefa depois que ela for concluída. Esse resultado pode ser uma conclusão bem-sucedida, possivelmente com um valor de resultado ou pode ser uma exceção gerada pelo método GetResult.

12.9.8.3 Classificação de expressões await

A expressão await t é classificada da mesma forma que a expressão (t).GetAwaiter().GetResult(). Portanto, se o tipo de retorno de GetResult for void, a await_expression será classificada como nenhum. Se ela tiver um não void tipo de retorno T, await_expression será classificado como um valor do tipo T.

12.9.8.4 Avaliação em run-time de expressões await

Em run-time, a expressão await t é avaliada da seguinte maneira:

  • Um awaiter a é obtido ao avaliar a expressão (t).GetAwaiter().
  • bool b é obtido avaliando-se a expressão (a).IsCompleted.
  • Se b for false, a avaliação dependerá de se a implementa a interface System.Runtime.CompilerServices.ICriticalNotifyCompletion (posteriormente conhecida como ICriticalNotifyCompletion para fins de brevidade). Essa verificação é feita no momento da associação; ou seja, em tempo de execução se a tiver o tipo de tempo de compilação dynamic, e caso contrário, em tempo de compilação. Deixe r indicar o delegado de retomada (§15.14):
    • Se a não implementar ICriticalNotifyCompletion, então a expressão ((a) as INotifyCompletion).OnCompleted(r) será avaliada.
    • Se a implementar ICriticalNotifyCompletion, então a expressão ((a) as ICriticalNotifyCompletion).UnsafeOnCompleted(r) será avaliada.
    • A avaliação é suspensa e o controle é retornado ao chamador atual da função assíncrona.
  • Imediatamente após (se b for true) ou após a invocação posterior de delegado de retomada (se b foi false), a expressão (a).GetResult() será avaliada. Se ela retornar um valor, esse valor será o resultado de await_expression. Caso contrário, o resultado será nada.

A implementação de um awaiter dos métodos de interface INotifyCompletion.OnCompleted e ICriticalNotifyCompletion.UnsafeOnCompleted deve fazer com que o delegado r seja invocado no máximo uma vez. Caso contrário, o comportamento da função assíncrona delimitante é indefinido.

12.10 Operadores aritméticos

12.10.1 Geral

Os operadores *, /, %, + e - são chamados de operadores aritméticos.

multiplicative_expression
    : unary_expression
    | multiplicative_expression '*' unary_expression
    | multiplicative_expression '/' unary_expression
    | multiplicative_expression '%' unary_expression
    ;

additive_expression
    : multiplicative_expression
    | additive_expression '+' multiplicative_expression
    | additive_expression '-' multiplicative_expression
    ;

Se um operando de um operador aritmético tiver o tipo de tempo de compilação dynamic, a expressão será associada dinamicamente (§12.3.3). Nesse caso, o tipo de tempo de compilação da expressão é dynamic e a resolução descrita abaixo ocorrerá em run-time usando-se o tipo de run-time desses operandos que têm o tipo de tempo de compilação dynamic.

12.10.2 Operador de multiplicação

Para uma operação do formato x * y, a resolução de sobrecarga do operador binário (§12.4.5) é aplicada para selecionar uma implementação de operador específica. Os operandos são convertidos nos tipos de parâmetro do operador selecionado e o tipo do resultado é o tipo de retorno do operador.

Os operadores de multiplicação predefinidos estão listados abaixo. Todos os operadores computam o produto de x e y.

  • Multiplicação de inteiros

    int operator *(int x, int y);
    uint operator *(uint x, uint y);
    long operator *(long x, long y);
    ulong operator *(ulong x, ulong y);
    

    Em um contexto checked, se o produto estiver fora do intervalo do tipo de resultado, um System.OverflowException será lançado. Em um contexto unchecked, estouros não são relatados e os bits significativos de alta ordem fora do intervalo do tipo de resultado são descartados.

  • Multiplicação de ponto flutuante:

    float operator *(float x, float y);
    double operator *(double x, double y);
    

    O produto é computado de acordo com as regras da aritmética IEC 60559. A tabela a seguir lista os resultados de todas as combinações possíveis de valores finitos não zeros, zeros, infinitos e NaNs. Na tabela, x e y são valores positivos finitos. z é o resultado de x * y, arredondado para o valor representável mais próximo. Se a magnitude do resultado for muito grande para o tipo de destino, z será infinito. Por causa do arredondamento, z pode ser zero, embora nem x nem y seja zero.

    +y -y +0 -0 +∞ -∞ NaN
    +x +z -z +0 -0 +∞ -∞ NaN
    -x -z +z -0 +0 -∞ +∞ NaN
    +0 +0 -0 +0 -0 NaN NaN NaN
    -0 -0 +0 -0 +0 NaN NaN NaN
    +∞ +∞ -∞ NaN NaN +∞ -∞ NaN
    -∞ -∞ +∞ NaN NaN -∞ +∞ NaN
    NaN NaN NaN NaN NaN NaN NaN NaN

    (Exceto quando observado de outra forma, nas tabelas de ponto flutuante em §12.10.2§12.10.6 o uso de "+" significa que o valor é positivo; o uso de "-" significa que o valor é negativo; e a falta de um sinal significa que o valor pode ser positivo ou negativo ou não tem nenhum sinal (NaN).)

  • Multiplicação decimal:

    decimal operator *(decimal x, decimal y);
    

    Se a magnitude do valor resultante for muito grande para se representar no formato decimal, System.OverflowException será gerado. Por causa do arredondamento, o resultado pode ser zero, mesmo quando nenhum dos operandos é zero. A escala do resultado, antes de qualquer arredondamento, é a soma das escalas dos dois operandos. A multiplicação decimal é equivalente ao uso do operador de multiplicação do tipo System.Decimal.

As formas elevadas (§12.4.8) dos operadores de multiplicação predefinidos não elevados anteriormente mencionados também são predefinidas.

12.10.3 Operador de divisão

Para uma operação do formato x / y, a resolução de sobrecarga do operador binário (§12.4.5) é aplicada para selecionar uma implementação de operador específica. Os operandos são convertidos nos tipos de parâmetro do operador selecionado e o tipo do resultado é o tipo de retorno do operador.

Os operadores de divisão predefinidos estão listados abaixo. Todos os operadores computam o quociente de x e y.

  • Divisão de inteiros:

    int operator /(int x, int y);
    uint operator /(uint x, uint y);
    long operator /(long x, long y);
    ulong operator /(ulong x, ulong y);
    

    Se o valor do operando direito for zero, um System.DivideByZeroException será lançado.

    A divisão arredonda o resultado para zero. Portanto, o valor absoluto do resultado é o maior inteiro possível menor que seja menor ou igual ao valor absoluto do quociente dos dois operandos. O resultado é zero ou positivo quando os dois operandos têm o mesmo sinal e zero ou negativo quando os dois operandos têm sinais opostos.

    Se o operando à esquerda for o menor valor representável de int ou long e o operando à direita for –1, ocorrerá uma situação de estouro. Em um contexto checked, isso faz com que uma System.ArithmeticException (ou uma subclasse dela) seja lançada. Em um contexto unchecked, é definido pela implementação se uma System.ArithmeticException (ou uma subclasse) for lançada ou o estouro não for informado, e o valor resultante é o do operando esquerdo.

  • Divisão de ponto flutuante:

    float operator /(float x, float y);
    double operator /(double x, double y);
    

    O quociente é computado de acordo com as regras da aritmética IEC 60559. A tabela a seguir lista os resultados de todas as combinações possíveis de valores finitos não zeros, zeros, infinitos e NaNs. Na tabela, x e y são valores positivos finitos. z é o resultado de x / y, arredondado para o valor representável mais próximo.

    +y -y +0 -0 +∞ -∞ NaN
    +x +z -z +∞ -∞ +0 -0 NaN
    -x -z +z -∞ +∞ -0 +0 NaN
    +0 +0 -0 NaN NaN +0 -0 NaN
    -0 -0 +0 NaN NaN -0 +0 NaN
    +∞ +∞ -∞ +∞ -∞ NaN NaN NaN
    -∞ -∞ +∞ -∞ +∞ NaN NaN NaN
    NaN NaN NaN NaN NaN NaN NaN NaN
  • Divisão decimal:

    decimal operator /(decimal x, decimal y);
    

    Se o valor do operando direito for zero, um System.DivideByZeroException será lançado. Se a magnitude do valor resultante for muito grande para se representar no formato decimal, System.OverflowException será gerado. Por causa do arredondamento, o resultado pode ser zero, mesmo que o primeiro operando não seja zero. A escala do resultado, antes de qualquer arredondamento, é a escala mais próxima da escala preferencial que preservará um resultado igual ao resultado exato. A escala preferencial é a escala de x menos a escala de y.

    A divisão decimal é equivalente ao uso do operador de divisão do tipo System.Decimal.

As formas elevadas (§12.4.8) dos operadores de divisão predefinidos não elevados anteriormente mencionados também são predefinidas.

12.10.4 Operador de resto

Para uma operação do formato x % y, a resolução de sobrecarga do operador binário (§12.4.5) é aplicada para selecionar uma implementação de operador específica. Os operandos são convertidos nos tipos de parâmetro do operador selecionado e o tipo do resultado é o tipo de retorno do operador.

Os operadores restantes predefinidos estão listados abaixo. Todos os operadores computam o restante da divisão entre x e y.

  • Resto inteiro:

    int operator %(int x, int y);
    uint operator %(uint x, uint y);
    long operator %(long x, long y);
    ulong operator %(ulong x, ulong y);
    

    O resultado de x % y é o valor produzido por x – (x / y) * y. Se y for zero, uma System.DivideByZeroException será lançada.

    Se o operando esquerdo for o menor valor de int ou long e o operando direito for –1, uma System.OverflowException será lançada se e somente se x / y lançar uma exceção.

  • Resto de ponto flutuante:

    float operator %(float x, float y);
    double operator %(double x, double y);
    

    A tabela a seguir lista os resultados de todas as combinações possíveis de valores finitos não zeros, zeros, infinitos e NaNs. Na tabela, x e y são valores positivos finitos. z é o resultado de x % y e é computado como x – n * y, em que n é o maior inteiro possível menor ou igual a x / y. Esse método de cálculo do restante é análogo ao usado para operandos inteiros, mas difere da definição IEC 60559 (na qual n é o inteiro mais próximo de x / y).

    +y -y +0 -0 +∞ -∞ NaN
    +x +z +z NaN NaN +x +x NaN
    -x -z -z NaN NaN -x -x NaN
    +0 +0 +0 NaN NaN +0 +0 NaN
    -0 -0 -0 NaN NaN -0 -0 NaN
    +∞ NaN NaN NaN NaN NaN NaN NaN
    -∞ NaN NaN NaN NaN NaN NaN NaN
    NaN NaN NaN NaN NaN NaN NaN NaN
  • Restante decimal:

    decimal operator %(decimal x, decimal y);
    

    Se o valor do operando direito for zero, um System.DivideByZeroException será lançado. É definido pela implementação quando uma System.ArithmeticException (ou uma subclasse dela) é lançada. Uma implementação em conformidade não deve gerar uma exceção para x % y em qualquer caso em que x / y não gere uma exceção. A escala do resultado, antes de qualquer arredondamento, é a maior das escalas dos dois operandos, e o sinal do resultado, se não zero, é o mesmo que o de x.

    O resto decimal é equivalente ao uso do operador de resto do tipo System.Decimal.

    Observação: essas regras garantem que, para todos os tipos, o resultado nunca tenha o sinal oposto do operando esquerdo. fim da observação

Os formatos elevados (§12.4.8) dos operadores de resto predefinidos não elevados anteriormente mencionados também são predefinidos.

12.10.5 Operador de adição

Para uma operação do formato x + y, a resolução de sobrecarga do operador binário (§12.4.5) é aplicada para selecionar uma implementação de operador específica. Os operandos são convertidos nos tipos de parâmetro do operador selecionado e o tipo do resultado é o tipo de retorno do operador.

Os operadores de adição predefinidos estão listados abaixo. Para tipos numéricos e de enumeração, os operadores de adição predefinidos computam a soma dos dois operandos. Quando um ou os dois operandos são do tipo string, os operadores de adição predefinidos concatenam a representação de cadeia de caracteres dos operandos.

  • Adição de números inteiros

    int operator +(int x, int y);
    uint operator +(uint x, uint y);
    long operator +(long x, long y);
    ulong operator +(ulong x, ulong y
    

    Em um contexto checked, se a soma estiver fora do intervalo do tipo de resultado, System.OverflowException será lançado. Em um contexto unchecked, estouros não são relatados e os bits significativos de alta ordem fora do intervalo do tipo de resultado são descartados.

  • Adição de ponto flutuante:

    float operator +(float x, float y);
    double operator +(double x, double y);
    

    A soma é computada de acordo com as regras da aritmética IEC 60559. A tabela a seguir lista os resultados de todas as combinações possíveis de valores finitos não zeros, zeros, infinitos e NaNs. Na tabela, x e y são valores finitos diferentes de zero e z é o resultado de x + y. Se x e y tiverem a mesma magnitude, mas sinais opostos, z será zero positivo. Se x + y for muito grande para se representar no tipo de destino, z será um infinito com o mesmo sinal que x + y.

    y +0 -0 +∞ -∞ NaN
    x z x x +∞ -∞ NaN
    +0 y +0 +0 +∞ –∞ NaN
    -0 y +0 -0 +∞ -∞ NaN
    +∞ +∞ +∞ +∞ +∞ NaN NaN
    -∞ -∞ -∞ -∞ NaN -∞ NaN
    NaN NaN NaN NaN NaN NaN NaN
  • Adição decimal:

    decimal operator +(decimal x, decimal y);
    

    Se a magnitude do valor resultante for muito grande para se representar no formato decimal, System.OverflowException será gerado. A escala do resultado, antes de qualquer arredondamento, é a maior das escalas dos dois operandos.

    A adição decimal é equivalente ao uso do operador de adição do tipo System.Decimal.

  • Adição de enumeração. Cada tipo de enumeração fornece implicitamente os seguintes operadores predefinidos, em que E é o tipo de enumeração e U é o tipo subjacente de E:

    E operator +(E x, U y);
    E operator +(U x, E y);
    

    Em run-time, esses operadores são avaliados exatamente como (E)((U)x + (U)y).

  • Concatenação de cadeia de caracteres:

    string operator +(string x, string y);
    string operator +(string x, object y);
    string operator +(object x, string y);
    

    Essas sobrecargas do operador binário + realizam a concatenação de strings. Se um operando de concatenação de cadeia de caracteres for null, uma cadeia de caracteres vazia será substituída. Caso contrário, qualquer operando nãostring será convertido em sua representação de cadeia de caracteres através da invocação do método virtual ToString herdado do tipo object. Se ToString retornar null, uma cadeia de caracteres vazia será substituída.

    Exemplo:

    class Test
    {
        static void Main()
        {
            string s = null;
            Console.WriteLine("s = >" + s + "<");  // Displays s = ><
    
            int i = 1;
            Console.WriteLine("i = " + i);         // Displays i = 1
    
            float f = 1.2300E+15F;
            Console.WriteLine("f = " + f);         // Displays f = 1.23E+15
    
            decimal d = 2.900m;
            Console.WriteLine("d = " + d);         // Displays d = 2.900
       }
    }
    

    A saída mostrada nos comentários é o resultado típico em um sistema em inglês dos EUA. A saída precisa pode depender das configurações regionais do ambiente de execução. O próprio operador de concatenação de cadeia de caracteres se comporta da mesma maneira em cada caso, mas os métodos ToString implicitamente chamados durante a execução podem ser afetados pelas configurações regionais.

    fim do exemplo

    O resultado do operador de concatenação de cadeia de caracteres é um string que consiste nos caracteres do operando esquerdo seguidos pelos caracteres do operando direito. O operador de concatenação de cadeia de caracteres nunca retorna um valor null. Uma System.OutOfMemoryException poderá ser lançada se não houver memória suficiente disponível para alocar a cadeia de caracteres resultante.

  • Combinação de delegados. Cada tipo de delegado fornece implicitamente o seguinte operador predefinido, em que D é o tipo delegado:

    D operator +(D x, D y);
    

    Se o primeiro operando for null, o resultado da operação será o valor do segundo operando (mesmo que também seja null). Caso contrário, se o segundo operando for null, o resultado da operação será o valor do primeiro operando. Caso contrário, o resultado da operação é uma nova instância de delegado cuja lista de invocação consiste nos elementos na lista de invocação do primeiro operando, seguido pelos elementos na lista de invocação do segundo operando. Ou seja, a lista de invocação de delegado resultante é a concatenação das listas de invocação dos dois operandos.

    Observação: Para exemplos de combinação de delegados, consulte §12.10.6 e §20.6. Como System.Delegate não é um tipo delegado, o operador + não é definido para ele. fim da observação

As formas elevadas (§12.4.8) dos operadores de adição predefinidos não elevados anteriormente mencionados também são predefinidas.

12.10.6 Operador de subtração

Para uma operação do formato x – y, a resolução de sobrecarga do operador binário (§12.4.5) é aplicada para selecionar uma implementação de operador específica. Os operandos são convertidos nos tipos de parâmetro do operador selecionado e o tipo do resultado é o tipo de retorno do operador.

Os operadores de subtração predefinidos estão listados abaixo. Todos os operadores subtraem y de x.

  • Subtração de inteiros

    int operator –(int x, int y);
    uint operator –(uint x, uint y);
    long operator –(long x, long y);
    ulong operator –(ulong x, ulong y
    

    Em um contexto checked, se a diferença estiver fora do intervalo do tipo de resultado, System.OverflowException será lançado. Em um contexto unchecked, estouros não são relatados e os bits significativos de alta ordem fora do intervalo do tipo de resultado são descartados.

  • Subtração de ponto flutuante:

    float operator –(float x, float y);
    double operator –(double x, double y);
    

    A diferença é computada de acordo com as regras da aritmética IEC 60559. A tabela a seguir lista os resultados de todas as combinações possíveis de valores finitos não zeros, zeros, infinitos e NaNs. Na tabela, x e y são valores finitos diferentes de zero e z é o resultado de x – y. Se x e y forem iguais, z será zero positivo. Se x – y for muito grande para se representar no tipo de destino, z será um infinito com o mesmo sinal que x – y.

    y +0 -0 +∞ -∞ NaN
    x z x x -∞ +∞ NaN
    +0 -y +0 +0 -∞ +∞ NaN
    -0 -y -0 +0 -∞ +∞ NaN
    +∞ +∞ +∞ +∞ NaN +∞ NaN
    -∞ -∞ -∞ -∞ -∞ NaN NaN
    NaN NaN NaN NaN NaN NaN NaN

    (Na tabela acima, as entradas de denotam a negação de de , não que o valor seja negativo.)

  • Subtração decimal:

    decimal operator –(decimal x, decimal y);
    

    Se a magnitude do valor resultante for muito grande para se representar no formato decimal, System.OverflowException será gerado. A escala do resultado, antes de qualquer arredondamento, é a maior das escalas dos dois operandos.

    A subtração decimal é equivalente ao uso do operador de subtração do tipo System.Decimal.

  • Subtração de enumeração. Cada tipo de enumeração fornece implicitamente o seguinte operador predefinido, em que E é o tipo de enumeração e U é o tipo subjacente de E:

    U operator –(E x, E y);
    

    Esse operador é avaliado exatamente como (U)((U)x – (U)y). Em outras palavras, o operador computa a diferença entre os valores ordinais de x e y, e o tipo do resultado é o tipo subjacente da enumeração.

    E operator –(E x, U y);
    

    Esse operador é avaliado exatamente como (E)((U)x – y). Em outras palavras, o operador subtrai um valor do tipo subjacente da enumeração, gerando um valor da enumeração.

  • Remoção de delegado. Cada tipo de delegado fornece implicitamente o seguinte operador predefinido, em que D é o tipo delegado:

    D operator –(D x, D y);
    

    A semântica é a seguinte:

    • Se o primeiro operando for null, o resultado da operação será null.
    • Caso contrário, se o segundo operando for null, o resultado da operação será o valor do primeiro operando.
    • Caso contrário, os dois operandos representam listas de invocação não vazias (§20.2).
      • Se as listas forem iguais, conforme determinado pelo operador de igualdade de delegação (§12.12.9), o resultado da operação será null.
      • Caso contrário, o resultado da operação é uma nova lista de invocação que consiste na lista do primeiro operando com as entradas do segundo operando removidas dela, desde que a lista do segundo operando seja uma sublista do primeiro. (Para determinar a igualdade da sublista, as entradas correspondentes são comparadas quanto ao operador de igualdade delegado.) Se a lista do segundo operando corresponder a várias sublistas de entradas contíguas na lista do primeiro operando, a última sublista correspondente de entradas contíguas será removida.
      • Caso contrário, o resultado da operação é o valor do operando esquerdo.

    Nenhuma das listas dos operandos (se houver) é alterada no processo.

    Exemplo:

    delegate void D(int x);
    
    class C
    {
        public static void M1(int i) { ... }
        public static void M2(int i) { ... }
    }
    
    class Test
    {
        static void Main()
        {
            D cd1 = new D(C.M1);
            D cd2 = new D(C.M2);
            D list = null;
    
            list = null - cd1;                             // null
            list = (cd1 + cd2 + cd2 + cd1) - null;         // M1 + M2 + M2 + M1
            list = (cd1 + cd2 + cd2 + cd1) - cd1;          // M1 + M2 + M2
            list = (cd1 + cd2 + cd2 + cd1) - (cd1 + cd2);  // M2 + M1
            list = (cd1 + cd2 + cd2 + cd1) - (cd2 + cd2);  // M1 + M1
            list = (cd1 + cd2 + cd2 + cd1) - (cd2 + cd1);  // M1 + M2
            list = (cd1 + cd2 + cd2 + cd1) - (cd1 + cd1);  // M1 + M2 + M2 + M1
            list = (cd1 + cd2 + cd2 + cd1) - (cd1 + cd2 + cd2 + cd1);  // null
        }
    }
    

    fim do exemplo

As formas elevadas (§12.4.8) dos operadores de subtração predefinidos não elevados anteriormente mencionados também são predefinidas.

12.11 Operadores de deslocamento

Os operadores << e >> são usados para executar operações de deslocamento de bits.

shift_expression
    : additive_expression
    | shift_expression '<<' additive_expression
    | shift_expression right_shift additive_expression
    ;

Se um operando de uma shift_expression tiver tipo de tempo de compilação dynamic, a expressão será associada dinamicamente (§12.3.3). Nesse caso, o tipo de tempo de compilação da expressão é dynamic e a resolução descrita abaixo ocorrerá em run-time usando-se o tipo de run-time desses operandos que têm o tipo de tempo de compilação dynamic.

Para uma operação do formato x << count or x >> count, a resolução de sobrecarga do operador binário (§12.4.5) é aplicada para selecionar uma implementação de operador específica. Os operandos são convertidos nos tipos de parâmetro do operador selecionado e o tipo do resultado é o tipo de retorno do operador.

Ao declarar um operador de deslocamento sobrecarregado, o tipo do primeiro operando sempre será a classe ou estrutura que declara o operador, e o tipo do segundo operando sempre será int.

Os operadores de deslocamento predefinidos estão listados abaixo.

  • Deslocar à esquerda:

    int operator <<(int x, int count);
    uint operator <<(uint x, int count);
    long operator <<(long x, int count);
    ulong operator <<(ulong x, int count);
    

    O operador << desloca x para a esquerda por um número de bits calculado conforme descrito abaixo.

    Os bits de alta ordem fora do intervalo do tipo de resultado de x são descartados, os bits restantes são deslocados para a esquerda e as posições vazias dos bits de baixa ordem são preenchidas com zero.

  • Deslocar à direita:

    int operator >>(int x, int count);
    uint operator >>(uint x, int count);
    long operator >>(long x, int count);
    ulong operator >>(ulong x, int count);
    

    O operador >> desloca x à direita por um número de bits calculado, conforme descrito abaixo.

    Quando x é do tipo int ou long, os bits de ordem baixa de x são descartados, os bits restantes são deslocados para a direita e as posições de bits vazias de ordem alta são definidas como zero se x não for negativo e definido como um se x for negativo.

    Quando x é do tipo uint ou ulong, os bits de ordem baixa de x são descartados, os bits restantes são deslocados para a direita e as posições de bits vazias de ordem alta são definidas como zero.

Para os operadores predefinidos, o número de bits a serem deslocados é computado da seguinte maneira:

  • Quando o tipo de x é int ou uint, a contagem de deslocamentos é fornecida pelos cinco bits de ordem inferior de count. Em outras palavras, a contagem de deslocamentos é computada de count & 0x1F.
  • Quando o tipo de x é long ou ulong, a contagem de deslocamentos é fornecida pelos seis bits de ordem inferior de count. Em outras palavras, a contagem de deslocamentos é computada de count & 0x3F.

Se a contagem de deslocamentos resultante for zero, os operadores de deslocamento simplesmente retornarão o valor de x.

As operações de deslocamento nunca causam estouros e produzem os mesmos resultados nos contextos marcados e desmarcados.

Quando o operando esquerdo do operador >> é de um tipo integral assinado, o operador executa um deslocamento aritmético para a direita em que o valor do bit mais significativo (o bit de sinal) do operando é propagado para as posições de bit vazias de ordem superior. Quando o operando esquerdo do operador >> é de um tipo integral não assinado, o operador executa um deslocamento lógico para a direita, em que as posições de bits vazias de ordem superior são sempre definidas como zero. Para executar a operação oposta daquela inferida do tipo de operando, conversões explícitas podem ser usadas.

Exemplo: se x for uma variável do tipo int, a operação unchecked ((int)((uint)x >> y)) executará um deslocamento lógico à direita de x. fim do exemplo

As formas elevadas (§12.4.8) dos operadores de deslocamento predefinidos não elevados anteriormente mencionados também são predefinidas.

12.12 Operadores de teste de tipo e relacional

12.12.1 Geral

Os operadores ==, !=, <, >, <=, >=, is e as são chamados de operadores relacionais e de teste de tipo.

relational_expression
    : shift_expression
    | relational_expression '<' shift_expression
    | relational_expression '>' shift_expression
    | relational_expression '<=' shift_expression
    | relational_expression '>=' shift_expression
    | relational_expression 'is' type
    | relational_expression 'is' pattern
    | relational_expression 'as' type
    ;

equality_expression
    : relational_expression
    | equality_expression '==' relational_expression
    | equality_expression '!=' relational_expression
    ;

Observação: a pesquisa pelo operando direito do operador is deve primeiro ser testada como um tipo, e depois como uma expressão que pode abranger vários tokens. No caso em que o operando é uma expressão, a expressão padrão deve ter precedência pelo menos tão alta quanto shift_expression. fim da observação

O operador is é descrito em §12.12.12 e o operador as é descrito em §12.12.13.

Os operadores ==, !=, <, >, <= e >= são operadores de comparação.

Se default_literal (§12.8.21) for usado como operando de um operador <, >, <=, or >=, ocorrerá um erro de tempo de compilação. Se um default_literal for usado como os dois operandos de um operador == ou !=, um erro de tempo de compilação ocorrerá. Se default_literal for usado como o operando esquerdo do operador is ou as, ocorrerá um erro de tempo de compilação.

Se um operando de um operador de comparação tiver o tipo de tempo de compilação dynamic, a expressão será associada dinamicamente (§12.3.3). Nesse caso, o tipo de tempo de compilação da expressão é dynamic e a resolução descrita abaixo ocorrerá em run-time usando-se o tipo de run-time desses operandos que têm o tipo de tempo de compilação dynamic.

Para uma operação do formato x «op» y, em que «op» é um operador de comparação, a resolução de sobrecarga (§12.4.5) é aplicada para selecionar uma implementação de operador específico. Os operandos são convertidos nos tipos de parâmetro do operador selecionado e o tipo do resultado é o tipo de retorno do operador. Se os dois operandos de uma equality_expression forem o literal null, a resolução de sobrecarga não será executada e a expressão será avaliada como um valor constante de true ou false de acordo com se o operador é == ou !=.

Os operadores de comparação predefinidos são descritos nas subcláusula a seguir. Todos os operadores de comparação predefinidos retornam um resultado do tipo bool, conforme descrito na tabela a seguir.

Operação Resultado
x == y true se x for igual a y; caso contrário, false
x != y true se x não for igual a y; caso contrário, false.
x < y true se x for menor que y, caso contrário, false
x > y true se x for maior que y, caso contrário, false
x <= y true se x for menor ou igual a y, caso contrário, false
x >= y true se x for maior ou igual a y, caso contrário, false

12.12.2 Operadores de comparação de inteiros

Os operadores de comparação de inteiro predefinidos são:

bool operator ==(int x, int y);
bool operator ==(uint x, uint y);
bool operator ==(long x, long y);
bool operator ==(ulong x, ulong y);

bool operator !=(int x, int y);
bool operator !=(uint x, uint y);
bool operator !=(long x, long y);
bool operator !=(ulong x, ulong y);

bool operator <(int x, int y);
bool operator <(uint x, uint y);
bool operator <(long x, long y);
bool operator <(ulong x, ulong y);

bool operator >(int x, int y);
bool operator >(uint x, uint y);
bool operator >(long x, long y);
bool operator >(ulong x, ulong y);

bool operator <=(int x, int y);
bool operator <=(uint x, uint y);
bool operator <=(long x, long y);
bool operator <=(ulong x, ulong y);

bool operator >=(int x, int y);
bool operator >=(uint x, uint y);
bool operator >=(long x, long y);
bool operator >=(ulong x, ulong y);

Cada um desses operadores compara os valores numéricos dos dois operandos inteiros e retorna um valor bool que indica se a relação específica é true ou false.

As formas elevadas (§12.4.8) dos operadores de comparação de inteiros predefinidos não elevados anteriormente mencionados também são predefinidas.

12.12.3 Operadores de comparação de ponto flutuante

Os operadores de comparação de ponto flutuante predefinidos são:

bool operator ==(float x, float y);
bool operator ==(double x, double y);

bool operator !=(float x, float y);
bool operator !=(double x, double y);

bool operator <(float x, float y);
bool operator <(double x, double y);

bool operator >(float x, float y);
bool operator >(double x, double y);

bool operator <=(float x, float y);
bool operator <=(double x, double y);

bool operator >=(float x, float y);
bool operator >=(double x, double y);

Os operadores comparam os operandos de acordo com as regras do padrão IEC 60559:

Se um dos operandos for NaN, o resultado será false para todos os operadores, exceto !=, para o qual o resultado será true. Para dois operandos, x != y sempre produz o mesmo resultado que !(x == y). No entanto, quando um ou os dois operandos são NaN, os operadores <, >, <= e >=não produzem os mesmos resultados que a negação lógica do operador oposto.

Exemplo: se x ou y for NaN, x < y será false, mas !(x >= y) será true. fim do exemplo

Quando nenhum operando é NaN, os operadores comparam os valores dos dois operandos de ponto flutuante em relação à ordenação

–∞ < –max < ... < –min < –0.0 == +0.0 < +min < ... < +max < +∞

em que min e max são os menores e os maiores valores finitos positivos que podem ser representados no formato de ponto flutuante fornecido. Os efeitos notáveis dessa ordenação são:

  • Zeros negativos e positivos são considerados iguais.
  • Um infinito negativo é considerado menor que todos os outros valores, mas igual a outro infinito negativo.
  • Um infinito positivo é considerado maior do que todos os outros valores, mas igual a outro infinito positivo.

As formas elevadas (§12.4.8) dos operadores de comparação de pontos flutuantes predefinidos não elevados anteriormente mencionados também são predefinidas.

12.12.4 Operadores de comparação decimal

Os operadores de comparação decimal predefinidos são:

bool operator ==(decimal x, decimal y);
bool operator !=(decimal x, decimal y);
bool operator <(decimal x, decimal y);
bool operator >(decimal x, decimal y);
bool operator <=(decimal x, decimal y);
bool operator >=(decimal x, decimal y);

Cada um desses operadores compara os valores numéricos dos dois operandos decimais e retorna um valor bool que indica se a relação específica é true ou false. Cada comparação decimal é equivalente ao uso do operador relacional ou de igualdade correspondente do tipo System.Decimal.

As formas elevadas (§12.4.8) dos operadores de comparação de decimais predefinidos não elevados anteriormente mencionados também são predefinidas.

12.12.5 Operadores de igualdade booliana

Os operadores de igualdade booliana predefinidos são:

bool operator ==(bool x, bool y);
bool operator !=(bool x, bool y);

O resultado de == será true se x e y estiverem true ou se x e y estiverem false. Caso contrário, o resultado será false.

O resultado de != será false se x e y estiverem true ou se x e y estiverem false. Caso contrário, o resultado será true. Quando os operandos são do tipo bool, o operador != produz o mesmo resultado que o operador ^.

As formas elevadas (§12.4.8) dos operadores de igualdade booleana predefinidos não elevados anteriormente mencionados também são predefinidas.

12.12.6 Operadores de comparação de enumeração

Cada tipo de enumeração fornece implicitamente os seguintes operadores de comparação predefinidos

bool operator ==(E x, E y);
bool operator !=(E x, E y);

bool operator <(E x, E y);
bool operator >(E x, E y);
bool operator <=(E x, E y);
bool operator >=(E x, E y);

O resultado da avaliação de x «op» y, em que x e y são expressões de um tipo de enumeração E com um tipo subjacente U e «op» é um dos operadores de comparação, é exatamente o mesmo da avaliação de ((U)x) «op» ((U)y). Em outras palavras, os operadores de comparação de tipo de enumeração simplesmente comparam os valores integrais subjacentes dos dois operandos.

As formas elevadas (§12.4.8) dos operadores de comparação de enumeração predefinidos não elevados anteriormente mencionados também são predefinidas.

12.12.7 Operadores de igualdade em tipos de referência

Cada tipo de classe C implicitamente fornece os seguintes operadores de igualdade de tipo de referência predefinidos:

bool operator ==(C x, C y);
bool operator !=(C x, C y);

a menos que existam operadores de igualdade predefinidos para C (por exemplo, quando C é string ou System.Delegate).

Os operadores retornam o resultado da comparação das duas referências para igualdade ou não igualdade. operator == retorna true se e somente se x e y se referirem à mesma instância ou forem os dois null, enquanto operator != retornará true se e somente se operator == com os mesmos operandos retornarem false.

Além das regras normais de aplicabilidade (§12.6.4.2), os operadores de igualdade de tipo de referência predefinidos exigem uma das duas seguintes situações para serem aplicáveis:

  • Os dois operandos são um valor de um tipo conhecido por ser um reference_type ou o literal null. Além disso, uma conversão de identidade ou referência explícita (§10.3.5) existe de um operando para o tipo do outro operando.
  • Um operando é o literal null e o outro operando é um valor do tipo T em que T é um type_parameter que não é conhecido por ser um tipo de valor e não tem a restrição de tipo de valor.
    • Se em run-time T for um tipo de valor não anulável, o resultado de == será false e o resultado de != será true.
    • Se em run-time T for um tipo de valor anulável, o resultado será computado da propriedade HasValue do operando, conforme descrito em (§12.12.10).
    • Se, em tempo de execução, T for um tipo de referência, o resultado será true se o operando for null; caso contrário, será false.

A menos que uma dessas condições seja verdadeira, ocorrerá um erro de tempo de associação.

Observação: as implicações notáveis dessas regras são:

  • É um erro de tempo de associação usar os operadores de igualdade de tipo de referência predefinidos para comparar duas referências que são conhecidas por serem diferentes em tempo de associação. Por exemplo, se os tipos de tempo de associação dos operandos forem dois tipos de classe e se nenhum deles derivar do outro, então seria impossível para os dois operandos fazerem referência ao mesmo objeto. Portanto, a operação será considerada um erro de tempo de associação.
  • Os operadores de igualdade de tipo de referência predefinidos não permitem que operandos de tipo de valor sejam comparados (exceto quando os parâmetros de tipo são comparados a null, que é tratado de forma especial).
  • Operandos de operadores de igualdade de tipo de referência predefinidos nunca são delimitados. Não teria sentido executar tais operações boxing, pois as referências às instâncias boxed recém-alocadas seriam necessariamente diferentes de todas as outras referências.

Para uma operação de formato x == y ou x != y, se houver algumoperator == ou operator != definido pelo usuário aplicável, as regras de resolução de sobrecarga de (§12.4.5) selecionarão aquele operador em vez do operador de igualdade de tipo de referência predefinido. Sempre é possível selecionar o operador de igualdade de tipo de referência predefinido convertendo explicitamente um ou ambos os operandos para o tipo object.

fim da observação

Exemplo: O exemplo a seguir verifica se um argumento de um tipo de parâmetro de tipo não restringido é null.

class C<T>
{
   void F(T x)
   {
      if (x == null)
      {
          throw new ArgumentNullException();
      }
      ...
   }
}

O constructo x == null é permitido mesmo que T possa representar um tipo de valor não anulável e o resultado seja simplesmente definido como false quando T for um tipo de valor não anulável.

fim do exemplo

Para uma operação de formato x == y ou x != y, se houver algumaoperator == ou operator != aplicável, as regras de resolução de sobrecarga do operador (§12.4.5) selecionarão esse operador em vez do operador de igualdade de tipo de referência predefinido.

Observação: É sempre possível selecionar o operador de igualdade de tipo de referência predefinido convertendo explicitamente ambos os operandos para o tipo object. fim da observação

Exemplo: o exemplo

class Test
{
    static void Main()
    {
        string s = "Test";
        string t = string.Copy(s);
        Console.WriteLine(s == t);
        Console.WriteLine((object)s == t);
        Console.WriteLine(s == (object)t);
        Console.WriteLine((object)s == (object)t);
    }
}

produz a saída

True
False
False
False

As variáveis s e t referem-se a duas instâncias de cadeia de caracteres distintas que contêm os mesmos caracteres. A primeira comparação gera True porque o operador de igualdade de cadeia de caracteres predefinido (§12.12.8) é selecionado quando os dois operandos são do tipo string. Todas as comparações restantes produzem False porque a sobrecarga de operator == no tipo string não é aplicável quando qualquer dos operandos tem um tipo de tempo de associação de object.

Observe que a técnica acima não é significativa para tipos de valor. O exemplo

class Test
{
    static void Main()
    {
        int i = 123;
        int j = 123;
        Console.WriteLine((object)i == (object)j);
    }
}

resulta em False porque as conversões criam referências a duas instâncias separadas de valores int boxed.

fim do exemplo

12.12.8 Operadores de igualdade de cadeia de caracteres

Os operadores de igualdade de cadeia de caracteres predefinidos são:

bool operator ==(string x, string y);
bool operator !=(string x, string y);

Dois valores string são considerados iguais quando um dos seguintes valores é verdadeiro:

  • Os dois valores são null.
  • Os dois valores são referências não null a instâncias de cadeia de caracteres que têm comprimentos idênticos e caracteres idênticos em cada posição de caractere.

Os operadores de igualdade de cadeia de caracteres comparam valores de cadeia de caracteres em vez de referências de cadeia de caracteres. Quando duas instâncias de cadeia de caracteres separadas contêm exatamente a mesma sequência de caracteres, os valores das cadeias de caracteres são iguais, mas as referências são diferentes.

Observação: conforme descrito em §12.12.7, os operadores de igualdade de tipo de referência podem ser usados para comparar referências de cadeia de caracteres em vez de valores de cadeia de caracteres. fim da observação

12.12.9 Operadores de igualdade de delegado

Os operadores de igualdade de delegado predefinidos são:

bool operator ==(System.Delegate x, System.Delegate y);
bool operator !=(System.Delegate x, System.Delegate y);

Duas instâncias de delegado são consideradas iguais da seguinte maneira:

  • Se uma ou as duas instâncias delegadas forem null, as instâncias serão consideradas iguais se e somente se as duas forem null.
  • Se os delegados tiverem tipos de tempo de execução diferentes, eles nunca serão iguais.
  • Se ambas as instâncias de delegado tiverem uma lista de invocação (§20.2), essas instâncias serão iguais se e somente se suas listas de invocação tiverem o mesmo tamanho e cada entrada na lista de invocação for igual (conforme definido abaixo) à entrada correspondente, em ordem, na lista de invocação da outra.

As regras a seguir regem a igualdade de entradas da lista de invocação:

  • Se duas entradas da lista de invocação se referirem ao mesmo método estático, as entradas serão iguais.
  • Se duas entradas da lista de invocação se referirem ao mesmo método não estático no mesmo objeto de destino (conforme definido pelos operadores de igualdade de referência), as entradas serão iguais.
  • As entradas da lista de invocação produzidas a partir da avaliação de funções anônimas semanticamente idênticas (§12.19) com o mesmo conjunto (possivelmente vazio) de instâncias de variável externa capturadas são permitidas (mas não necessárias) iguais.

Se a resolução de sobrecarga do operador for resolvida para um operador de igualdade de delegado e os tipos de tempo de associação dos dois operandos forem tipos de delegados, conforme descrito em §20 em vez de System.Delegate, e não houver nenhuma conversão de identidade entre os tipos de operando do tipo de associação, ocorrerá um erro de tempo de associação.

Observação: essa regra impede comparações que não podem considerar valores diferentes de null como iguais, pois são referências a instâncias de tipos diferentes de delegados. fim da observação

12.12.10 Operadores de igualdade entre tipos de valor anuláveis e o literal nulo

Os operadores == e != permitem que um operando seja um valor de um tipo de valor anulável e o outro seja o literal null, mesmo que não exista nenhum operador predefinido ou definido pelo usuário (em formato não elevado ou elevado) para a operação.

Para uma operação de uma das formas

x == null    null == x    x != null    null != x

em que x é uma expressão de um tipo de valor anulável, se a resolução de sobrecarga do operador (§12.4.5) não encontrar um operador aplicável, o resultado será computado da propriedade HasValue de x. Especificamente, os dois primeiros formatos são convertidos em !x.HasValue e os dois últimos formatos são convertidos em x.HasValue.

12.12.11 Operadores de igualdade de tupla

Os operadores de igualdade de tupla são aplicados em par aos elementos dos operandos de tupla em ordem lexical.

Se cada operando x e y de um operador de == ou != for classificado como uma tupla ou como um valor com um tipo de tupla (§8.3.11), o operador será um operador de igualdade de tupla.

Se um operando e for classificado como uma tupla, os elementos e1...en serão os resultados da avaliação das expressões de elemento da expressão de tupla. Caso contrário, se e for um valor de tipo tupla, os elementos deverão ser t.Item1...t.Itemn, onde t é o resultado da avaliação de e.

Os operandos x e y de um operador de igualdade de tupla devem ter a mesma aridade, ou ocorrerá um erro de tempo de compilação. Para cada par de elementos xi e yi, o mesmo operador de igualdade deve ser aplicado e produzirá um resultado do tipo bool, dynamic, um tipo que tem uma conversão implícita em bool ou um tipo que define os operadores true e false.

O operador de igualdade de tupla x == y é avaliado da seguinte maneira:

  • O operando do lado esquerdo x é avaliado.
  • O operando do lado direito y é avaliado.
  • Para cada par de elementos xi e yi em ordem lexical:
    • O operador xi == yi é avaliado e um resultado do tipo bool é obtido da seguinte maneira:
      • Se a comparação produziu bool, esse será o resultado.
      • Caso contrário, se a comparação produziu dynamic, o operador false será invocado dinamicamente nele e o valor bool resultante será negado com o operador de negação lógica (!).
      • Caso contrário, se o tipo da comparação tiver uma conversão implícita para bool, essa conversão será aplicada.
      • Caso contrário, se o tipo da comparação tiver um operador false, esse operador será invocado e o valor bool resultante será negado com o operador de negação lógica (!).
    • Se o bool resultante for false, nenhuma avaliação adicional ocorrerá, e o resultado do operador de igualdade entre tuplas será false.
  • Se todas as comparações de elementos produzirem true, o resultado do operador de igualdade de tupla será true.

O operador de igualdade de tupla x != y é avaliado da seguinte maneira:

  • O operando do lado esquerdo x é avaliado.
  • O operando do lado direito y é avaliado.
  • Para cada par de elementos xi e yi em ordem lexical:
    • O operador xi != yi é avaliado e um resultado do tipo bool é obtido da seguinte maneira:
      • Se a comparação produziu bool, esse será o resultado.
      • Caso contrário, se a comparação produziu dynamic, o operador true será invocado dinamicamente nele e o valor bool resultante será o resultado.
      • Caso contrário, se o tipo da comparação tiver uma conversão implícita para bool, essa conversão será aplicada.
      • Caso contrário, se o tipo da comparação tiver um operador true, esse operador será invocado e o valor bool resultante será o resultado.
    • Se o bool resultante for true, nenhuma avaliação adicional ocorrerá, e o resultado do operador de igualdade entre tuplas será true.
  • Se todas as comparações de elementos produzirem false, o resultado do operador de igualdade de tupla será false.

12.12.12 Operador is

Há dois formatos do operador is. Um deles é o operador is-type, que tem um tipo no lado direito. O outro é o operador is-pattern, que tem um padrão no lado direito.

12.12.12.1 Operador is-type

O operador is-type é usado para verificar se o tipo de run-time de um objeto é compatível com um tipo determinado. A verificação é executada em run-time. O resultado da operação E is T, em que E é uma expressão e T é um tipo diferente de dynamic, é um valor booleano que indica se E não é nulo e pode ser convertido com sucesso para o tipo T por uma conversão de referência, uma conversão de encaixotamento, uma conversão de desencaixotamento, uma conversão de encapsulamento ou uma conversão de desencapsulamento.

A operação é avaliada como segue:

  1. Se E for uma função anônima ou um grupo de métodos, ocorrerá um erro de tempo de compilação.
  2. Se E for o literal null ou se o valor de E for null, o resultado será false.
  3. Ou:
  4. Deixe R ser o tipo de run-time de E.
  5. Deixe D ser derivado de R da seguinte maneira:
  6. Se R for um tipo de valor anulável, D será o tipo subjacente de R.
  7. Caso contrário, D é R.
  8. O resultado depende de D e T da seguinte maneira:
  9. Se T for um tipo de referência, o resultado será true se:
    • existe uma conversão de identidade entre D e T,
    • D é um tipo de referência e uma conversão de referência implícita de D para T existe ou
    • Ou: D é um tipo de valor e há uma conversão de boxing de D em T.
      Ou: D é um tipo de valor e T é um tipo de interface implementado por D.
  10. Se T for um tipo de valor anulável, o resultado será true se D for o tipo subjacente de T.
  11. Se T for um tipo de valor não anulável, o resultado será true se D e T forem do mesmo tipo.
  12. Caso contrário, o resultado será false.

Conversões definidas pelo usuário não são consideradas pelo operador is.

Observação: como o operador is é avaliado em run-time, todos os argumentos de tipo foram substituídos e não há tipos abertos (§8.4.3) a serem considerados. fim da observação

Observação: o operador is pode ser compreendido em termos de conversões e tipos de tempo de compilação da seguinte maneira, em que C é o tipo de tempo de compilação de E:

  • Se o tipo de tempo de compilação de e for o mesmo que T ou se uma conversão de referência implícita (§10.2.8), conversão de boxing (§10.2.9), conversão de encapsulamento (§10.6) ou uma conversão de desencapsulamento explícita (§10.6) existir do tipo de tempo de compilação de E para T:
    • Se C for de um tipo de valor não anulável, o resultado da operação será true.
    • Caso contrário, o resultado da operação será equivalente à avaliação de E != null.
  • Caso contrário, se uma conversão de referência explícita (§10.3.5) ou conversão de unboxing (§10.3.7) existir de C para T, ou se C ou T for um tipo aberto (§8.4.3), as verificações em run-time como acima deverão ser realizadas.
  • Caso contrário, nenhuma conversão de referência, boxing, encapsulamento ou desencapsulamento de E para o tipo T será possível, e o resultado da operação será false. Um compilador pode implementar otimizações com base no tipo de tempo de compilação.

fim da observação

12.12.12.2 Operador is-pattern

O operador de padrão é usado para verificar se o valor calculado por uma expressão corresponde a um determinado padrão (§11). A verificação é executada em run-time. O resultado do operador is-pattern será verdadeiro se o valor corresponder ao padrão; caso contrário, é falso.

Para uma expressão da forma E is P, em que E é uma expressão relacional do tipo T e P é um padrão, é um erro de tempo de compilação se qualquer uma das seguintes condições ocorrer:

  • E não designa um valor ou não tem um tipo.
  • O padrão P não é aplicável (§11.2) ao tipo T.

12.12.13 Operador as

O operador as é usado para converter explicitamente um valor em um determinado tipo de referência ou tipo de valor anulável. Ao contrário de uma expressão de conversão (§12.9.7), o operador as nunca gera uma exceção. Em vez disso, se não for possível realizar a conversão indicada, o valor resultante será null.

Em uma operação do formato E as T, E deve ser uma expressão e T deve ser um tipo de referência, um parâmetro de tipo conhecido como um tipo de referência ou um tipo de valor anulável. Além disso, pelo menos um das seguintes situações deve ser verdadeira ou, caso contrário, ocorrerá um erro de tempo de compilação:

  • Uma conversão de identidade (§10.2.2), anulável implícita (§10.2.6), de referência implícita (§10.2.8), boxing (§10.2.9), anulável explícita (§10.3.4), de referência explícita (§10.3.5) ou de encapsulamento (§8.3.12) existe de E para T.
  • O tipo de E ou T é um tipo aberto.
  • E é o literal null.

Se o tipo de tempo de compilação de E não for dynamic, a operação E as T produzirá o mesmo resultado que

E is T ? (T)(E) : (T)null

exceto que E é avaliado apenas uma vez. Espera-se que um compilador otimize E as T para executar no máximo uma verificação de tipo em tempo de execução em vez das duas verificações de tipo em tempo de execução implícitas pela expansão acima.

Se o tipo de tempo de compilação de E for dynamic, ao contrário do operador de conversão, o operador as não será associado dinamicamente (§12.3.3). Portanto, a expansão nesse caso é:

E is T ? (T)(object)(E) : (T)null

Algumas conversões, como as definidas pelo usuário, não são possíveis com o operador as e devem ser realizadas usando expressões de conversão.

Exemplo: no exemplo

class X
{
    public string F(object o)
    {
        return o as string;  // OK, string is a reference type
    }

    public T G<T>(object o)
        where T : Attribute
    {
        return o as T;       // Ok, T has a class constraint
    }

    public U H<U>(object o)
    {
        return o as U;       // Error, U is unconstrained
    }
}

o parâmetro de tipo T de G é conhecido por ser um tipo de referência, pois tem a restrição de classe. No entanto, o parâmetro de tipo U de H não é; portanto, o uso do operador as em H não é permitido.

fim do exemplo

12.13 Operadores lógicos

12.13.1 Geral

Os operadores &, ^e | são chamados de operadores lógicos.

and_expression
    : equality_expression
    | and_expression '&' equality_expression
    ;

exclusive_or_expression
    : and_expression
    | exclusive_or_expression '^' and_expression
    ;

inclusive_or_expression
    : exclusive_or_expression
    | inclusive_or_expression '|' exclusive_or_expression
    ;

Se um operando de um operador lógico tiver o tipo de tempo de compilação dynamic, a expressão será associada dinamicamente (§12.3.3). Nesse caso, o tipo de tempo de compilação da expressão é dynamic e a resolução descrita abaixo ocorrerá em run-time usando-se o tipo de run-time desses operandos que têm o tipo de tempo de compilação dynamic.

Para uma operação do formato x «op» y, em que «op» é um dos operadores lógicos, a resolução de sobrecarga (§12.4.5) é aplicada para selecionar uma implementação de operador específico. Os operandos são convertidos nos tipos de parâmetro do operador selecionado e o tipo do resultado é o tipo de retorno do operador.

Os operadores lógicos predefinidos são descritos nas subcláusula a seguir.

12.13.2 Operadores lógicos inteiros

Os operadores lógicos inteiros predefinidos são:

int operator &(int x, int y);
uint operator &(uint x, uint y);
long operator &(long x, long y);
ulong operator &(ulong x, ulong y);

int operator |(int x, int y);
uint operator |(uint x, uint y);
long operator |(long x, long y);
ulong operator |(ulong x, ulong y);

int operator ^(int x, int y);
uint operator ^(uint x, uint y);
long operator ^(long x, long y);
ulong operator ^(ulong x, ulong y);

O operador & computa o AND lógico bit a bit dos dois operandos, o operador | computa o OR lógico bit a bit dos dois operandos e o operador ^ computa o OR exclusivo lógico bit a bit dos dois operandos. Nenhum transbordamento é possível nessas operações.

As formas elevadas (§12.4.8) dos operadores lógicos de inteiros predefinidos não elevados anteriormente mencionados também são predefinidas.

12.13.3 Operadores lógicos de enumeração

Cada tipo de enumeração E fornece implicitamente os seguintes operadores lógicos predefinidos:

E operator &(E x, E y);
E operator |(E x, E y);
E operator ^(E x, E y);

O resultado da avaliação dex «op» y, em que x e y são expressões de um tipo de enumeração E with an underlying type U e «op» é um dos operadores lógicos, é exatamente o mesmo da avaliação de (E)((U)x «op» (U)y). Em outras palavras, os operadores lógicos de tipo de enumeração executam a operação lógica no tipo subjacente dos dois operandos.

As formas elevadas (§12.4.8) dos operadores lógicos de enumeração predefinidos não elevados anteriormente mencionados também são predefinidas.

12.13.4 Operadores lógicos boolianos

Os operadores lógicos boolianos predefinidos são:

bool operator &(bool x, bool y);
bool operator |(bool x, bool y);
bool operator ^(bool x, bool y);

O resultado de x & y será true se ambos x e y forem true. Caso contrário, o resultado será false.

O resultado de x | y será true se x ou y for true. Caso contrário, o resultado será false.

O resultado de x ^ y é true se x é true e y é false ou x é false e y é true. Caso contrário, o resultado será false. Quando os operandos são do tipo bool, o operador ^ computa o mesmo resultado que o operador !=.

12.13.5 Booleano anulável & e | operadores

O tipo booliano anulável bool? pode representar três valores, true, false e null.

Assim como acontece com os outros operadores binários, os formatos de precisão dos operadores lógicos & e | (§12.13.4) também são predefinidos:

bool? operator &(bool? x, bool? y);
bool? operator |(bool? x, bool? y);

A semântica dos operadores & e | levantados é definida pela tabela a seguir.

x y x & y x \| y
true true true true
true false false true
true null null true
false true false true
false false false false
false null false null
null true null true
null false false null
null null null null

Observação: o tipo bool? é conceitualmente semelhante ao tipo de três valores usado para expressões boolianas no SQL. A tabela acima segue a mesma semântica que o SQL, enquanto a aplicação das regras de §12.4.8 aos operadores & e | não. As regras de §12.4.8 já fornecem uma semântica semelhante à do SQL para o operador elevado de ^. fim da observação

12.14 Operadores lógicos condicionais

12.14.1 Geral

Os operadores && e || são chamados de operadores lógicos condicionais. Eles também são chamados de operadores lógicos de "curto-circuito".

conditional_and_expression
    : inclusive_or_expression
    | conditional_and_expression '&&' inclusive_or_expression
    ;

conditional_or_expression
    : conditional_and_expression
    | conditional_or_expression '||' conditional_and_expression
    ;

Os operadores && e || são versões condicionais dos operadores & e |:

  • A operação x && y corresponde à operação x & y, exceto que y é avaliada somente se x não for false.
  • A operação x || y corresponde à operação x | y, exceto que y é avaliada somente se x não for true.

Observação: o motivo pelo qual o curto-circuito usa as condições 'not true' e 'not false' é para permitir que operadores condicionais definidos pelo usuário definam quando o curto-circuito se aplica. Os tipos definidos pelo usuário podem estar em um estado em que operator true retorna false e operator false retorna false. Nesses casos, nem && nem || darão curto-circuito. fim da observação

Se um operando de um operador lógico condicional tiver o tipo de tempo de compilação dynamic, a expressão será associada dinamicamente (§12.3.3). Nesse caso, o tipo de tempo de compilação da expressão é dynamic e a resolução descrita abaixo ocorrerá em run-time usando-se o tipo de run-time desses operandos que têm o tipo de tempo de compilação dynamic.

Uma operação do formato x && y ou x || y é processada aplicando-se a resolução de sobrecarga (§12.4.5) como se a operação tivesse sido escrita como x & y ou x | y. E, em seguida,

  • Se a resolução de sobrecarga não encontrar um único operador melhor ou se a resolução de sobrecarga selecionar um dos operadores lógicos inteiros predefinidos ou operadores lógicos boolianos anuláveis (§12.13.5), ocorrerá um erro de tempo de associação.
  • Caso contrário, se o operador selecionado for um dos operadores lógicos boolianos predefinidos (§12.13.4), a operação será processada conforme descrito em §12.14.2.
  • Caso contrário, o operador selecionado será um operador definido pelo usuário e a operação será processada conforme descrito em §12.14.3.

Não é possível sobrecarregar diretamente os operadores lógicos condicionais. No entanto, como os operadores lógicos condicionais são avaliados em termos de operadores lógicos regulares, as sobrecargas dos operadores lógicos regulares também são, sob certas restrições, consideradas sobrecargas dos operadores lógicos condicionais. Isso é descrito mais adiante em §12.14.3.

12.14.2 Operadores lógicos condicionais boolianos

Quando os operandos de && ou || são do tipo bool ou quando os operandos são de tipos que não definem um operator & ou operator | aplicável, mas definem conversões implícitas para bool, a operação é processada da seguinte maneira:

  • A operação x && y é avaliada como x ? y : false. Em outras palavras, x é primeiro avaliado e convertido para o tipo bool. Em seguida, se x for true, y será avaliado e convertido para o tipo bool, e isso se torna o resultado da operação. Caso contrário, o resultado da operação será false.
  • A operação x || y é avaliada como x ? true : y. Em outras palavras, x é primeiro avaliado e convertido para o tipo bool. Então, se x for true, o resultado da operação será true. Caso contrário, y é avaliado e convertido no tipo bool e isso se torna o resultado da operação.

12.14.3 Operadores lógicos condicionais definidos pelo usuário

Quando os operandos de && ou || são de tipos que declaram um operator & ou operator | definidos pelo usuário aplicável, as duas seguintes situações devem ser verdadeiras, em que T é o tipo no qual o operador selecionado é declarado:

  • O tipo de retorno e o tipo de cada parâmetro do operador selecionado devem ser T. Em outras palavras, o operador deve computar o AND lógico ou o OR lógico de dois operandos do tipo T e retornará um resultado do tipo T.
  • T deve conter declarações de operator true e operator false.

Um erro de tempo de vinculação ocorrerá se um desses requisitos não for atendido. Caso contrário, a operação && ou || é avaliada combinando-se o operator true ou operator false definido pelo usuário com o operador definido pelo usuário selecionado:

  • A operação x && y é avaliada como T.false(x) ? x : T.&(x, y), em que T.false(x) é uma invocação do operator false declarado em T e T.&(x, y) é uma invocação do operator & selecionado. Em outras palavras, x é avaliado primeiro e operator false é invocado no resultado para determinar se x é definitivamente falso. Em seguida, se x for definitivamente falso, o resultado da operação será o valor computado anteriormente para x. Caso contrário, y é avaliado, e o operator & selecionado é invocado com o valor previamente calculado para x e o valor computado para y, produzindo o resultado da operação.
  • A operação x || y é avaliada como T.true(x) ? x : T.|(x, y), em que T.true(x) é uma invocação do operator true declarado em T e T.|(x, y) é uma invocação do operator | selecionado. Em outras palavras, x é avaliado primeiro e operator true é invocado no resultado para determinar se x é definitivamente verdadeiro. Em seguida, se x for definitivamente verdadeiro, o resultado da operação será o valor computado anteriormente para x. Caso contrário, y é avaliado, e o operator | selecionado é invocado com o valor previamente calculado para x e o valor computado para y, produzindo o resultado da operação.

Em qualquer uma dessas operações, a expressão fornecida por x é avaliada apenas uma vez e a expressão fornecida por y não é avaliada ou avaliada apenas uma vez.

12.15 O operador de coalescência nula

O operador ?? é chamado operador de coalescência nula.

null_coalescing_expression
    : conditional_or_expression
    | conditional_or_expression '??' null_coalescing_expression
    | throw_expression
    ;

Em uma expressão de associação nula do formato a ?? b, se a não fornull, o resultado será a; caso contrário, o resultado será b. A operação avaliará b somente se a for null.

O operador de coalescência nula é associativo à direita, o que significa que as operações são agrupadas da direita para a esquerda.

Exemplo: uma expressão do formato a ?? b ?? c é avaliada como a ?? (b ?? c). Em termos gerais, uma expressão do formato E1 ?? E2 ?? ... ?? EN retorna o primeiro dos operandos que é nãonull ou null se todos os operandos forem null. fim do exemplo

O tipo da expressão a ?? b depende de quais conversões implícitas estão disponíveis nos operandos. Em ordem de preferência, o tipo de a ?? b é A₀, A ou B, em que A é o tipo de a (desde que a tenha um tipo), B é o tipo de b(desde que b tenha um tipo) e A₀ seja o tipo subjacente de A se A for um tipo de valor anulável ou A. Especificamente, a ?? b é processado da seguinte maneira:

  • Se A existir e for um tipo não gerenciado (§8.8) ou conhecido por ser um tipo de valor não anulável, ocorrerá um erro de tempo de compilação.
  • Caso contrário, se A existir e b for uma expressão dinâmica, o tipo de resultado será dynamic. Em run-time, a é avaliado primeiro. Se a não for null, a será convertido em dynamic e isso se tornará o resultado. Caso contrário, b será avaliado e esse será o resultado.
  • Caso contrário, se A existir e for um tipo de valor anulável e existir uma conversão implícita de b para A₀, o tipo de resultado será A₀. Em run-time, a é avaliado primeiro. Se a não for null, a será desembalhado para o tipo A₀, e isso será o resultado. Caso contrário, b é avaliado e convertido no tipo A₀, o que se torna o resultado.
  • Caso contrário, se A existir e houver uma conversão implícita de b para A, o tipo de resultado será A. Em run-time, a é avaliado primeiro. Se a não for null, a será o resultado. Caso contrário, b é avaliado e convertido no tipo A, o que se torna o resultado.
  • Caso contrário, se A existir e for um tipo de valor anulável, b tiver um tipo B e existir uma conversão implícita de A₀ para B, o tipo de resultado será B. Em run-time, a é avaliado primeiro. Se a não for null, a será desembrulhado para o tipo A₀ e convertido no tipo B, e isso se torna o resultado. Caso contrário, b será avaliado e se tornará o resultado.
  • Caso contrário, se b tiver um tipo B e houver uma conversão implícita de a para B, o tipo de resultado será B. Em run-time, a é avaliado primeiro. Se a não for null, a é convertido para o tipo Be isso se torna o resultado. Caso contrário, b será avaliado e se tornará o resultado.
  • Caso contrário, a e b são incompatíveis e ocorre um erro de tempo de compilação.

Exemplo:

T M<T>(T a, T b) => a ?? b;

string s = M(null, "text");
int i = M(19, 23);

O parâmetro T de tipo para o método M não é restrito. Portanto, o argumento de tipo pode ser um tipo de referência ou um tipo de valor anulável, conforme mostrado na primeira chamada para M. O argumento de tipo também pode ser um tipo de valor não anulável, conforme mostrado na segunda chamada para M. Quando o argumento de tipo é um tipo de valor não anulável, o valor da expressão a ?? b é a.

fim do exemplo

12.16 Operador de expressão throw

throw_expression
    : 'throw' null_coalescing_expression
    ;

Uma throw_expression lança o valor produzido avaliando a null_coalescing_expression. A expressão deve ser implicitamente conversível para System.Exception e o resultado da avaliação da expressão será convertido em System.Exception antes de ser lançada. O comportamento em run-time da avaliação de uma expressão de lançamento é o mesmo especificado para uma instrução de lançamento (§13.10.6).

Uma throw_expression não tem tipo. Uma throw_expression é conversível para todos os tipos por uma conversão implícita de lançamento.

Uma expressão de lançamento só deve ocorrer nos seguintes contextos sintáticos:

  • Como o segundo ou terceiro operando de um operador condicional ternário (?:).
  • Como o segundo operando de um operador de coalescência de nulos (??).
  • Como o corpo de um lambda ou membro com corpo de expressão.

12.17 Expressões de declaração

Uma expressão de declaração declara uma variável local.

declaration_expression
    : local_variable_type identifier
    ;

local_variable_type
    : type
    | 'var'
    ;

simple_name_ também é considerado uma expressão de declaração se a pesquisa de nome simples não encontrar uma declaração associada (§12.8.4). Quando usado como uma expressão de declaração, _ é chamado de descartável simples. É semanticamente equivalente a var _, mas pode-se usar em mais lugares.

Uma declaração de lançamento só deve ocorrer nos seguintes contextos sintáticos:

  • Como um outargument_value em uma argument_list.
  • Como um simples descarte _ compreendendo o lado esquerdo de uma atribuição simples (§12.21.2).
  • Como um tuple_element em uma ou mais tuple_expressions aninhadas recursivamente, sendo a mais externa aquela que compreende o lado esquerdo de uma atribuição de desconstrução. Uma deconstruction_expression dá origem a expressões de declaração nessa posição, embora as expressões de declaração não estejam sintaticamente presentes.

Observação: isso significa que uma expressão de declaração não pode ser colocada entre parênteses. fim da observação

É um erro que uma variável tipada implicitamente declarada com uma declaration_expression seja referenciada na argument_list em que é declarada.

É um erro para uma variável declarada com uma declaration_expression ser referenciada na atribuição de desconstrução em que ela ocorre.

Uma expressão de declaração que é um descarte simples ou em que o local_variable_type é o identificador var é classificado como uma variável tipada implicitamente. A expressão não tem tipo e o tipo da variável local é inferido com base no contexto sintático da seguinte maneira:

  • Em argument_list o tipo inferido da variável é o tipo declarado do parâmetro correspondente.
  • Como o lado esquerdo de uma atribuição simples, o tipo inferido da variável é o tipo do lado direito da atribuição.
  • Em uma tuple_expression no lado esquerdo de uma atribuição simples, o tipo inferido da variável é o tipo do elemento de tupla correspondente no lado direito (após a desconstrução) da atribuição.

Caso contrário, a expressão de declaração será classificada como uma variável tipada explicitamente e o tipo da expressão, bem como a variável declarada, será aquele fornecido pelo local_variable_type.

Uma expressão de declaração com o identificador _ é um descarte (§9.2.9.2) e não introduz um nome para a variável. Uma expressão de declaração com um identificador diferente de _ insere esse nome no espaço mais próximo de declaração de variáveis locais (§7.3).

Exemplo:

string M(out int i, string s, out bool b) { ... }

var s1 = M(out int i1, "One", out var b1);
Console.WriteLine($"{i1}, {b1}, {s1}");
// Error: i2 referenced within declaring argument list
var s2 = M(out var i2, M(out i2, "Two", out bool b2), out b2);
var s3 = M(out int _, "Three", out var _);

A declaração de s1 mostra expressões de declaração tipadas explícita e implicitamente. O tipo inferido de b1 é bool porque esse é o tipo do parâmetro de saída correspondente em M1. O WriteLine subsequente é capaz de acessar i1 e b1, que foram introduzidos no escopo delimitador.

A declaração de s2 mostra uma tentativa de usar i2 na chamada aninhada para M, o que não é permitido, porque a referência ocorre na lista de argumentos em que i2 foi declarado. Por outro lado, a referência a b2 no argumento final é permitida, pois ocorre após o final da lista de argumentos aninhados em que b2 foi declarado.

A declaração de s3 mostra o uso de expressões de declaração implícita e explicitamente tipadas que são descartadas. Como os descartes não declaram uma variável nomeada, são permitidas as múltiplas ocorrências do identificador _.

(int i1, int _, (var i2, var _), _) = (1, 2, (3, 4), 5);

Este exemplo demonstra o uso de declarações implícita e explicitamente tipadas para variáveis e descartes em uma atribuição de desestruturação. simple_name_ é equivalente a var _ quando não é encontrada nenhuma declaração de _.

void M1(out int i) { ... }

void M2(string _)
{
    M1(out _);      // Error: `_` is a string
    M1(out var _);
}

Este exemplo mostra o uso de var _ para fornecer um descarte tipado implicitamente quando _ não estiver disponível, pois designa uma variável no escopo delimitador.

fim do exemplo

12.18 Operador condicional

O operador ?: é chamado operador condicional. Também é chamado, às vezes, de operador ternário.

conditional_expression
    : null_coalescing_expression
    | null_coalescing_expression '?' expression ':' expression
    | null_coalescing_expression '?' 'ref' variable_reference ':'
      'ref' variable_reference
    ;

Uma expressão de lançamento (§12.16) não será permitida em um operador condicional se ref estiver presente.

Uma expressão condicional do formato b ? x : y avalia primeiro a condição b. Em seguida, se b for true, x será avaliado e se tornará o resultado da operação. Caso contrário, y será avaliado e se tornará o resultado da operação. Uma expressão condicional nunca avalia x e y.

O operador condicional é associativo à direita, o que significa que as operações são agrupadas da direita para a esquerda.

Exemplo: uma expressão do formato a ? b : c ? d : e é avaliada como a ? b : (c ? d : e). fim do exemplo

O primeiro operando do operador ?: deve ser uma expressão que possa ser implicitamente convertida em bool ou uma expressão de um tipo que implementa operator true. Se nenhum desses requisitos for atendido, ocorrerá um erro de tempo de compilação.

Se ref estiver presente:

  • Uma conversão de identidade deve existir entre os tipos de dois variable_references, e o tipo do resultado pode ser qualquer tipo. Se um dos tipos for dynamic, a inferência de tipo preferirá dynamic (§8.7). Se um dos tipos for um tipo de tupla (§8.3.11), a inferência de tipo incluirá os nomes de elementos quando os nomes de elementos na mesma posição ordinal forem correspondentes nas duas tuplas.
  • O resultado é uma referência de variável, que é gravável se as duas variable_references forem graváveis.

Observação: quando ref está presente, conditional_expression retorna uma referência variável, que pode ser atribuída a uma variável de referência usando-se o operador = ref ou passada como um parâmetro de referência/entrada/saída. fim da observação

Se ref não estiver presente, o segundo e o terceiro operandos, respectivamente x e y, do operador ?: controlarão o tipo da expressão condicional:

  • Se x tiver tipo de X e y tiver tipo de Y,
    • Se houver uma conversão de identidade entre X e Y, o resultado será o melhor tipo comum de um conjunto de expressões (§12.6.3.15). Se um dos tipos for dynamic, a inferência de tipo preferirá dynamic (§8.7). Se um dos tipos for um tipo de tupla (§8.3.11), a inferência de tipo incluirá os nomes de elementos quando os nomes de elementos na mesma posição ordinal forem correspondentes nas duas tuplas.
    • Caso contrário, se uma conversão implícita (§10.2) existir de X para Y, mas não de Y para X, Y será o tipo da expressão condicional.
    • Caso contrário, se uma conversão de enumeração implícita (§10.2.4) existir de X para Y, Y será o tipo da expressão condicional.
    • Caso contrário, se uma conversão de enumeração implícita (§10.2.4) existir de Y para X, X será o tipo da expressão condicional.
    • Caso contrário, se uma conversão implícita (§10.2) existir de Y para X, mas não de X para Y, X será o tipo da expressão condicional.
    • Caso contrário, nenhum tipo de expressão poderá ser determinado e ocorrerá um erro de tempo de compilação.
  • Se apenas um de x e y tiver um tipo e x e y forem implicitamente conversíveis para esse tipo, esse será o tipo da expressão condicional.
  • Caso contrário, nenhum tipo de expressão poderá ser determinado e ocorrerá um erro de tempo de compilação.

O processamento run-time de uma expressão condicional ref do formato b ? ref x : ref y consiste nas seguintes etapas:

  • Primeiro, b é avaliado e o valor bool de b é determinado:
    • Se houver uma conversão implícita do tipo de b para bool, essa conversão implícita será executada para produzir um valor bool.
    • Caso contrário, operator true definido pelo tipo de b é invocado para produzir um valor bool.
  • Se o valor bool produzido pela etapa acima for true, x será avaliado e a referência de variável resultante se tornará o resultado da expressão condicional.
  • Caso contrário, y será avaliado e a referência de variável resultante se tornará o resultado da expressão condicional.

O processamento run-time de uma expressão condicional do formato b ? x : y consiste nas seguintes etapas:

  • Primeiro, b é avaliado e o valor bool de b é determinado:
    • Se houver uma conversão implícita do tipo de b para bool, essa conversão implícita será executada para produzir um valor bool.
    • Caso contrário, operator true definido pelo tipo de b é invocado para produzir um valor bool.
  • Se o valor bool produzido pela etapa acima for true, x será avaliado e convertido no tipo da expressão condicional e isso se tornará o resultado da expressão condicional.
  • Caso contrário, y é avaliado e convertido no tipo da expressão condicional e isso se torna o resultado da expressão condicional.

12.19 Expressões de função anônima

12.19.1 Geral

Uma função anônima é uma expressão que representa uma definição de método "em linha". Uma função anônima não tem um valor ou tipo em si mesma, mas é conversível para um delegado ou tipo de árvore de expressão compatível. A avaliação de uma conversão de função anônima depende do tipo de destino da conversão: se for um tipo de delegado, a conversão será avaliada como um valor de delegado que referencia o método definido pela função anônima. Se for um tipo de árvore de expressão, a conversão resulta em uma árvore de expressão que representa a estrutura do método na forma de uma estrutura de objeto.

Observação: por motivos históricos, há dois tipos sintáticos de funções anônimas, ou seja, lambda_expressions e anonymous_method_expressions. Para quase todos os fins, lambda_expressions são mais concisas e expressivas do que anonymous_method_expressions, que permanecem no idioma para retrocompatibilidade. fim da observação

lambda_expression
    : 'async'? anonymous_function_signature '=>' anonymous_function_body
    ;

anonymous_method_expression
    : 'async'? 'delegate' explicit_anonymous_function_signature? block
    ;

anonymous_function_signature
    : explicit_anonymous_function_signature
    | implicit_anonymous_function_signature
    ;

explicit_anonymous_function_signature
    : '(' explicit_anonymous_function_parameter_list? ')'
    ;

explicit_anonymous_function_parameter_list
    : explicit_anonymous_function_parameter
      (',' explicit_anonymous_function_parameter)*
    ;

explicit_anonymous_function_parameter
    : anonymous_function_parameter_modifier? type identifier
    ;

anonymous_function_parameter_modifier
    : 'ref'
    | 'out'
    | 'in'
    ;

implicit_anonymous_function_signature
    : '(' implicit_anonymous_function_parameter_list? ')'
    | implicit_anonymous_function_parameter
    ;

implicit_anonymous_function_parameter_list
    : implicit_anonymous_function_parameter
      (',' implicit_anonymous_function_parameter)*
    ;

implicit_anonymous_function_parameter
    : identifier
    ;

anonymous_function_body
    : null_conditional_invocation_expression
    | expression
    | 'ref' variable_reference
    | block
    ;

Ao identificar um anonymous_function_body, se as alternativas null_conditional_invocation_expression e expression forem aplicáveis, deve-se escolher a primeira.

Observação: a sobreposição e a prioridade entre alternativas aqui é exclusivamente para conveniência descritiva; podem ser elaboradas regras gramaticais para remover a sobreposição. ANTLR e outros sistemas gramaticais adotam a mesma conveniência e, portanto, anonymous_function_body tem a semântica especificada automaticamente. fim da observação

Observação: quando tratada como uma expressão, um formato sintático como x?.M() seria um erro se o tipo de resultado de M for void (§12.8.13). Mas quando tratado como uma null_conditional_invocation_expression, o tipo de resultado pode ser void. fim da observação

Exemplo: o tipo de resultado de List<T>.Reverse é void. No código a seguir, o corpo da expressão anônima é uma null_conditional_invocation_expression, portanto, não é um erro.

Action<List<int>> a = x => x?.Reverse();

fim do exemplo

O operador => tem a mesma precedência da atribuição (=) e é associativo à direita.

Uma função anônima com o async modificador é uma função assíncrona e segue as regras descritas em §15.14.

Os parâmetros de uma função anônima na forma de uma expressão lambda podem ser explicitamente ou implicitamente tipados. Em uma lista de parâmetros explicitamente tipada, o tipo de cada parâmetro é explicitamente indicado. Em uma lista de parâmetros tipada implicitamente, os tipos dos parâmetros são inferidos do contexto em que a função anônima ocorre — especificamente, quando a função anônima é convertida para um tipo de delegate compatível ou tipo de árvore de expressões, esse tipo fornece os tipos de parâmetro (§10.7).

Em lambda_expression com um único parâmetro tipado implicitamente, pode-se omitir os parênteses da lista de parâmetros. Em outras palavras, uma função anônima do formato

( «param» ) => «expr»

pode ser abreviado para

«param» => «expr»

A lista de parâmetros de uma função anônima no formato de anonymous_method_expression é opcional. Se fornecidos, os parâmetros devem ser explicitamente tipados. Caso contrário, a função anônima será conversível para um delegado com qualquer lista de parâmetros que não contenha parâmetros de saída.

O corpo do bloco de uma função anônima é sempre acessível (§13.2).

Exemplo: alguns exemplos de funções anônimas seguem abaixo:

x => x + 1                             // Implicitly typed, expression body
x => { return x + 1; }                 // Implicitly typed, block body
(int x) => x + 1                       // Explicitly typed, expression body
(int x) => { return x + 1; }           // Explicitly typed, block body
(x, y) => x * y                        // Multiple parameters
() => Console.WriteLine()              // No parameters
async (t1,t2) => await t1 + await t2   // Async
delegate (int x) { return x + 1; }     // Anonymous method expression
delegate { return 1 + 1; }             // Parameter list omitted

fim do exemplo

O comportamento das expressões lambda e das expressões de método anônimo é o mesmo, exceto nos seguintes pontos:

  • anonymous_method_expressions permitem que a lista de parâmetros seja totalmente omitida, facilitando a conversibilidade para tipos de delegados de qualquer lista de parâmetros de valor.
  • lambda_expressions permite que os tipos de parâmetro sejam omitidos e inferidos, enquanto anonymous_method_expressions exige que os tipos de parâmetro sejam explicitamente declarados.
  • O corpo de uma lambda_expression pode ser uma expressão ou um bloco, enquanto o corpo de uma anonymous_method_expression precisa ser um bloco.
  • Somente lambda_expressions têm conversões para tipos de árvore de expressão compatíveis (§8.6).

12.19.2 Assinaturas de função anônimas

anonymous_function_signature de uma função anônima define os nomes e, opcionalmente, os tipos dos parâmetros para a função anônima. O escopo dos parâmetros da função anônima é anonymous_function_body (§7.7). Junto com a lista de parâmetros (se fornecida) o corpo do método anônimo constitui um espaço de declaração (§7.3). Portanto, é um erro de tempo de compilação se o nome de um parâmetro da função anônima for igual ao nome de uma variável local, constante local ou parâmetro cujo escopo inclui o anonymous_method_expression ou lambda_expression.

Se uma função anônima tiver um explicit_anonymous_function_signature, então o conjunto dos tipos de delegados compatíveis e dos tipos de árvores de expressão será restrito àqueles que possuam os mesmos tipos de parâmetros e modificadores na mesma ordem (§10.7). Ao contrário das conversões de grupo de métodos (§10.8), não há suporte para contravariância de tipos de parâmetro de função anônima. Se uma função anônima não tiver anonymous_function_signature, o conjunto de tipos de delegado e tipos de árvore de expressão compatíveis será restrito àqueles que não têm parâmetros de saída.

Observe que anonymous_function_signature não pode incluir atributos ou uma matriz de parâmetros. No entanto, anonymous_function_signature pode ser compatível com um tipo de delegado cuja lista de parâmetros contém uma matriz de parâmetros.

Observe também que a conversão em um tipo de árvore de expressão, mesmo se compatível, ainda pode falhar em tempo de compilação (§8.6).

12.19.3 Corpos de função anônima

O corpo (expressão ou bloco) de uma função anônima está sujeito às seguintes regras:

  • Se a função anônima incluir uma assinatura, os parâmetros especificados na assinatura estarão disponíveis no corpo. Se a função anônima não tiver assinatura, ela poderá ser convertida em um tipo de delegado ou tipo de expressão com parâmetros (§10.7), mas os parâmetros não poderão ser acessados no corpo.
  • Com exceção dos parâmetros por referência especificados na assinatura (se houver) da função anônima delimitante mais próxima, é um erro de tempo de compilação para o corpo acessar um parâmetro por referência.
  • Com exceção dos parâmetros especificados na assinatura (se houver) da função anônima delimitante mais próxima, é um erro de compilação se o corpo acessar um parâmetro de tipo ref struct.
  • Quando o tipo de this é um tipo de estrutura, é um erro de tempo de compilação que o corpo acesse this. Isso é verdade se o acesso for explícito (como em this.x) ou implícito (como em x em que x é um membro de instância do struct). Essa regra simplesmente proíbe esse acesso e não afeta se a pesquisa de membro resultar em um membro do struct.
  • O corpo tem acesso às variáveis externas (§12.19.6) da função anônima. O acesso a uma variável externa referenciará a instância da variável que está ativa no momento da avaliação da lambda_expression ou anonymous_method_expression (§12.19.7).
  • É um erro de tempo de compilação se o corpo contiver uma instrução goto, uma instrução break ou uma instrução continue cujo destino está fora do corpo ou dentro do corpo de uma função anônima contida.
  • Uma instrução return no corpo retorna o controle de uma invocação da função anônima delimitante mais próxima, não do membro da função que a envolve.

Não está claro explicitamente se existe outro meio de executar o bloco de uma função anônima além da avaliação e da invocação de lambda_expression ou anonymous_method_expression. Em particular, um compilador pode optar por implementar uma função anônima sintetizando um ou mais tipos ou métodos nomeados. Os nomes de tais elementos sintetizados devem ser de um formato reservado para uso do compilador (§6.4.3).

12.19.4 Resolução de sobrecarga

Funções anônimas em uma lista de argumentos participam da inferência do tipo e da resolução de sobrecarga. Consulte §12.6.3 e §12.6.4 para obter as regras exatas.

Exemplo: o exemplo a seguir ilustra o efeito das funções anônimas na resolução de sobrecarga.

class ItemList<T> : List<T>
{
    public int Sum(Func<T, int> selector)
    {
        int sum = 0;
        foreach (T item in this)
        {
            sum += selector(item);
        }
        return sum;
    }

    public double Sum(Func<T, double> selector)
    {
        double sum = 0;
        foreach (T item in this)
        {
            sum += selector(item);
        }
        return sum;
    }
}

A classe ItemList<T> tem dois métodos Sum. Cada um usa um argumento selector, que extrai o valor a ser somado de um elemento da lista. O valor extraído pode ser um int ou um double, e a soma resultante também é um int ou um double.

Os métodos Sum poderiam, por exemplo, ser usados para calcular somas de uma lista de linhas de detalhe em um pedido.

class Detail
{
    public int UnitCount;
    public double UnitPrice;
    ...
}

class A
{
    void ComputeSums()
    {
        ItemList<Detail> orderDetails = GetOrderDetails( ... );
        int totalUnits = orderDetails.Sum(d => d.UnitCount);
        double orderTotal = orderDetails.Sum(d => d.UnitPrice * d.UnitCount);
        ...
    }

    ItemList<Detail> GetOrderDetails( ... )
    {
        ...
    }
}

Na primeira invocação de orderDetails.Sum, os dois métodos Sum são aplicáveis porque a função anônima d => d.UnitCount é compatível com Func<Detail,int> e Func<Detail,double>. No entanto, a resolução de sobrecarga escolhe o primeiro método Sum porque a conversão para Func<Detail,int> é melhor do que a conversão para Func<Detail,double>.

Na segunda invocação de orderDetails.Sum, somente o segundo método Sum é aplicável porque a função anônima d => d.UnitPrice * d.UnitCount produz um valor do tipo double. Assim, a resolução de sobrecarga escolhe o segundo método Sum para essa invocação.

fim do exemplo

12.19.5 Funções anônimas e associação dinâmica

Uma função anônima não pode ser um receptor, argumento ou operando de uma operação associada dinamicamente.

12.19.6 Variáveis externas

12.19.6.1 Geral

Qualquer matriz de parâmetro, parâmetro de valor ou variável local cujo escopo inclui lambda_expression ou anonymous_method_expression é chamada de variável externa da função anônima. Em um membro de função de instância de uma classe, esse valor é considerado um parâmetro de valor e é uma variável externa de qualquer função anônima contida no membro da função.

12.19.6.2 Variáveis externas capturadas

Quando uma variável externa é referenciada por uma função anônima, diz-se que a variável externa foi capturada pela função anônima. Normalmente, o tempo de vida de uma variável local é limitado à execução do bloco ou instrução ao qual está associado (§9.2.9.1). No entanto, o tempo de vida de uma variável externa capturada é estendido pelo menos até que a árvore de delegado ou expressão criada a partir da função anônima se torne elegível para coleta de lixo.

Exemplo: no exemplo

delegate int D();

class Test
{
    static D F()
    {
        int x = 0;
        D result = () => ++x;
        return result;
    }

    static void Main()
    {
        D d = F();
        Console.WriteLine(d());
        Console.WriteLine(d());
        Console.WriteLine(d());
    }
}

a variável local x é capturada pela função anônima e o tempo de vida de x é estendido pelo menos até que o delegado retornado de F se torne qualificado para coleta de lixo. Como cada invocação da função anônima opera na mesma instância de x, a saída do exemplo é:

1
2
3

fim do exemplo

Quando uma variável local ou um parâmetro de valor é capturado por uma função anônima, a variável local ou parâmetro não é mais considerado uma variável fixa (§23.4), mas é considerada uma variável movível. No entanto, variáveis externas capturadas não podem ser usadas em uma instrução fixed (§23.7), portanto, o endereço de uma variável externa capturada não pode ser tomado.

Observação: Ao contrário de uma variável não capturada, uma variável local capturada pode ser exposta simultaneamente a vários threads de execução. fim da observação

12.19.6.3 Instanciação de variáveis locais

Uma variável local é considerada instanciada quando a execução entra no escopo da variável.

Exemplo: por exemplo, quando o método a seguir é invocado, a variável local x é instanciada e inicializada três vezes – uma vez para cada iteração do loop.

static void F()
{
    for (int i = 0; i < 3; i++)
    {
        int x = i * 2 + 1;
        ...
    }
}

No entanto, mover a declaração de x fora do loop resulta em uma única instanciação de x:

static void F()
{
    int x;
    for (int i = 0; i < 3; i++)
    {
        x = i * 2 + 1;
        ...
    }
}

fim do exemplo

Quando não é capturada, não há como observar exatamente a frequência com que uma variável local é instanciada, pois os tempos de vida das instanciações são desarticulados, é possível que cada instanciação simplesmente use o mesmo local de armazenamento. No entanto, quando uma função anônima captura uma variável local, os efeitos da instanciação se tornam aparentes.

Exemplo: o exemplo

delegate void D();
class Test
{
    static D[] F()
    {
        D[] result = new D[3];
        for (int i = 0; i < 3; i++)
        {
            int x = i * 2 + 1;
            result[i] = () => Console.WriteLine(x);
        }
        return result;
    }

    static void Main()
    {
        foreach (D d in F())
        {
            d();
        }
    }
}

produz a saída:

1
3
5

No entanto, quando a declaração de x é movida para fora do loop:

delegate void D();

class Test
{
    static D[] F()
    {
        D[] result = new D[3];
        int x;
        for (int i = 0; i < 3; i++)
        {
            x = i * 2 + 1;
            result[i] = () => Console.WriteLine(x);
        }
        return result;
   }

   static void Main()
   {
       foreach (D d in F())
       {
           d();
       }
   }
}

a saída é:

5
5
5

Observe que um compilador é permitido (mas não necessário) para otimizar as três instanciações em uma única instância de delegado (§10.7.2).

fim do exemplo

Se for-loop declarar uma variável de iteração, essa variável em si será considerada declarada fora do loop.

Exemplo: portanto, se o exemplo for alterado para capturar a variável de iteração em si:

delegate void D();

class Test
{
    static D[] F()
    {
        D[] result = new D[3];
        for (int i = 0; i < 3; i++)
        {
            result[i] = () => Console.WriteLine(i);
        }
        return result;
   }

   static void Main()
   {
       foreach (D d in F())
       {
           d();
       }
   }
}

apenas uma instância da variável de iteração será capturada, o que produzirá a saída:

3
3
3

fim do exemplo

É possível que delegados de funções anônimas compartilhem algumas variáveis capturadas, mas tenham instâncias separadas de outras.

Exemplo: por exemplo, se F for alterado para

static D[] F()
{
    D[] result = new D[3];
    int x = 0;
    for (int i = 0; i < 3; i++)
    {
        int y = 0;
        result[i] = () => Console.WriteLine($"{++x} {++y}");
    }
    return result;
}

os três delegados capturam a mesma instância de x, mas capturam instâncias separadas de y, e a saída é:

1 1
2 1
3 1

fim do exemplo

Funções anônimas separadas podem capturar a mesma instância de uma variável externa.

Exemplo: no exemplo:

delegate void Setter(int value);
delegate int Getter();

class Test
{
    static void Main()
    {
        int x = 0;
        Setter s = (int value) => x = value;
        Getter g = () => x;
        s(5);
        Console.WriteLine(g());
        s(10);
        Console.WriteLine(g());
    }
}

as duas funções anônimas capturam a mesma instância da variável local x e, portanto, podem "se comunicar" por meio dessa variável. A saída de exemplo é:

5
10

fim do exemplo

12.19.7 Avaliação de expressões de função anônima

Uma função anônima F sempre deve ser convertida em um tipo de delegado D ou em um tipo de árvore de expressão E, diretamente ou por meio da execução de uma expressão de criação de delegado new D(F). Essa conversão determina o resultado da função anônima, conforme descrito em §10.7.

12.19.8 Exemplo de implementação

Essa subcláusula é informativa.

Esse subcláusula descreve uma possível implementação de conversões de função anônima em termos de outros constructos de C#. A implementação descrita aqui baseia-se nos mesmos princípios usados por um compilador de C# comercial, mas não é de forma alguma uma implementação obrigatória, nem é a única possível. Ela menciona apenas brevemente as conversões em árvores de expressão, pois sua semântica exata está fora do escopo dessa especificação.

O restante dessa subcláusula fornece vários exemplos de código que contém funções anônimas com características diferentes. Para cada exemplo, fornece-se uma tradução correspondente para o código que usa apenas outros constructos de C#. Nos exemplos, assume-se o identificador D por representar o seguinte tipo de delegado:

public delegate void D();

A forma mais simples de uma função anônima é aquela que não captura nenhuma variável externa:

delegate void D();

class Test
{
    static void F()
    {
        D d = () => Console.WriteLine("test");
    }
}

Isso pode ser convertido em uma instanciação de delegado que faz referência a um método estático gerado pelo compilador no qual o código da função anônima é colocado:

delegate void D();

class Test
{
    static void F()
    {
        D d = new D(__Method1);
    }

    static void __Method1()
    {
        Console.WriteLine("test");
    }
}

No exemplo abaixo, uma função anônima faz referência aos membros de instância de this:

delegate void D();

class Test
{
    int x;

    void F()
    {
        D d = () => Console.WriteLine(x);
    }
}

Isso pode ser traduzido para um método de instância gerado pelo compilador que contém o código da função anônima:

delegate void D();

class Test
{
   int x;

   void F()
   {
       D d = new D(__Method1);
   }

   void __Method1()
   {
       Console.WriteLine(x);
   }
}

Neste exemplo, a função anônima captura uma variável local:

delegate void D();

class Test
{
    void F()
    {
        int y = 123;
        D d = () => Console.WriteLine(y);
    }
}

O tempo de vida da variável local agora deve ser estendido para ser igual a pelo menos o tempo de vida de delegado da função anônima. Isso pode ser obtido elevando a variável local a um campo de uma classe gerada pelo compilador. A instanciação da variável local (§12.19.6.3) corresponde à criação de uma instância da classe gerada pelo compilador e o acesso à variável local corresponde ao acesso a um campo na instância da classe gerada pelo compilador. Além disso, a função anônima torna-se um método de instância da classe gerada pelo compilador:

delegate void D();

class Test
{
    void F()
    {
        __Locals1 __locals1 = new __Locals1();
        __locals1.y = 123;
        D d = new D(__locals1.__Method1);
    }

    class __Locals1
    {
        public int y;

        public void __Method1()
        {
            Console.WriteLine(y);
        }
    }
}

Finalmente, a função anônima a seguir captura this bem como duas variáveis locais com tempos de vida diferentes:

delegate void D();

class Test
{
   int x;

   void F()
   {
       int y = 123;
       for (int i = 0; i < 10; i++)
       {
           int z = i * 2;
           D d = () => Console.WriteLine(x + y + z);
       }
   }
}

Aqui, uma classe gerada pelo compilador é criada para cada bloco em que as variáveis locais são capturadas, permitindo que as variáveis em diferentes blocos tenham tempos de vida independentes. Uma instância de __Locals2, a classe gerada pelo compilador para o bloco interno, contém a variável local z e um campo que faz referência a uma instância de __Locals1. Uma instância de __Locals1, a classe gerada pelo compilador para o bloco externo, contém a variável local y e um campo que faz referência this do membro da função delimitador. Com essas estruturas de dados, é possível alcançar todas as variáveis externas capturadas por meio de uma instância de __Local2 e, portanto, o código da função anônima pode ser implementado como um método de instância dessa classe.

delegate void D();

class Test
{
    int x;

    void F()
    {
        __Locals1 __locals1 = new __Locals1();
        __locals1.__this = this;
        __locals1.y = 123;
        for (int i = 0; i < 10; i++)
        {
            __Locals2 __locals2 = new __Locals2();
            __locals2.__locals1 = __locals1;
            __locals2.z = i * 2;
            D d = new D(__locals2.__Method1);
        }
    }

    class __Locals1
    {
        public Test __this;
        public int y;
    }

    class __Locals2
    {
        public __Locals1 __locals1;
        public int z;

        public void __Method1()
        {
            Console.WriteLine(__locals1.__this.x + __locals1.y + z);
        }
    }
}

A mesma técnica aplicada aqui para capturar as variáveis locais também pode ser usada ao converter-se funções anônimas em árvores de expressão: as referências aos objetos gerados pelo compilador podem ser armazenadas na árvore de expressão, e o acesso às variáveis locais pode ser representado como acessos de campo nesses objetos. A vantagem dessa abordagem é que ela permite que as variáveis locais "de precisão" sejam compartilhadas entre os delegados e as árvores de expressão.

Fim do texto informativo.

12.20 Expressões de consulta

12.20.1 Geral

As expressões de consulta fornecem uma sintaxe integrada à linguagem para consultas semelhantes a linguagens de consulta relacionais e hierárquicas, como SQL e XQuery.

query_expression
    : from_clause query_body
    ;

from_clause
    : 'from' type? identifier 'in' expression
    ;

query_body
    : query_body_clause* select_or_group_clause query_continuation?
    ;

query_body_clause
    : from_clause
    | let_clause
    | where_clause
    | join_clause
    | join_into_clause
    | orderby_clause
    ;

let_clause
    : 'let' identifier '=' expression
    ;

where_clause
    : 'where' boolean_expression
    ;

join_clause
    : 'join' type? identifier 'in' expression 'on' expression
      'equals' expression
    ;

join_into_clause
    : 'join' type? identifier 'in' expression 'on' expression
      'equals' expression 'into' identifier
    ;

orderby_clause
    : 'orderby' orderings
    ;

orderings
    : ordering (',' ordering)*
    ;

ordering
    : expression ordering_direction?
    ;

ordering_direction
    : 'ascending'
    | 'descending'
    ;

select_or_group_clause
    : select_clause
    | group_clause
    ;

select_clause
    : 'select' expression
    ;

group_clause
    : 'group' expression 'by' expression
    ;

query_continuation
    : 'into' identifier query_body
    ;

Uma expressão de consulta começa com uma cláusula from e termina com uma cláusula select ou group. A cláusula from inicial pode ser seguida por zero ou mais cláusulas from, let, where, join ou orderby. Cada cláusula from é um gerador que introduz uma variável de intervalo que varia entre os elementos de uma sequência de. Cada cláusula let introduz uma variável de intervalo que representa um valor computado por meio de variáveis de intervalo anteriores. Cada cláusula where é um filtro que exclui os itens do resultado. Cada cláusula join compara as chaves especificadas da sequência de origem com as chaves de outra sequência, gerando os pares correspondentes. Cada cláusula orderby reordena os itens de acordo com os critérios especificados. A cláusula select ou group final especifica a forma do resultado em termos das variáveis de intervalo. Finalmente, uma cláusula into pode ser usada para "unir" consultas tratando os resultados de uma consulta como um gerador em uma consulta subsequente.

12.20.2 Ambiguidades em expressões de consulta

As expressões de consulta usam várias palavras-chave contextuais (§6.4.4): ascending, by, descending, equals, from, group, into, join, let, on, orderby, select e where.

Para evitar ambiguidades que possam surgir do uso desses identificadores como palavras-chave e nomes simples, os identificadores são considerados palavras-chave em qualquer lugar dentro de uma expressão de consulta, a menos que sejam prefixados com "@" (§6.4.4) nesse caso, eles são considerados identificadores. Para essa finalidade, uma expressão de consulta é qualquer expressão que comece com "fromidentificador" seguido por qualquer token, exceto ";", "=" ou ",".

12.20.3 Conversão de expressão de consulta

12.20.3.1 Geral

A linguagem C# não especifica a semântica de execução das expressões de consulta. Em vez disso, as expressões de consulta são convertidas em invocações de métodos que aderem ao padrão de expressão de consulta (§12.20.4). Especificamente, as expressões de consulta são convertidas em invocações de métodos chamados Where, Select, SelectMany, Join, GroupJoin, OrderBy, OrderByDescending, ThenBy, ThenByDescending, GroupBy e Cast. Espera-se que esses métodos tenham tipos de retorno e assinaturas específicos, conforme descrito em §12.20.4. Esses métodos podem ser métodos de instância do objeto que está sendo consultado ou métodos de extensão que são externos ao objeto. Esses métodos implementam a execução real da consulta.

A tradução de expressões de consulta para invocações de método é um mapeamento sintático que ocorre antes da execução de qualquer associação de tipo ou resolução de sobrecarga. Após a conversão das expressões de consulta, as invocações de método resultantes são processadas como invocações de método regular; isso pode, por sua vez, descobrir erros de tempo de compilação. Essas condições de erro incluem, mas não se limitam a métodos que não existem, argumentos dos tipos errados e métodos genéricos nos quais ocorre falha da inferência de tipo.

Uma expressão de consulta é processada aplicando-se repetidamente as conversões a seguir até que nenhuma redução adicional seja possível. As conversões são listadas em ordem de aplicação: cada seção pressupõe que as conversões nas seções anteriores foram executadas exaustivamente e, depois de esgotadas, uma seção não será revisitada posteriormente no processamento da mesma expressão de consulta.

É um erro de compilação quando uma expressão de consulta inclui uma atribuição a uma variável de intervalo ou o uso de uma variável de intervalo como argumento para um parâmetro de referência ou de saída.

Algumas conversões injetam variáveis de intervalo com identificadores transparentes indicados por *. Eles são descritos mais adiante em §12.20.3.8.

12.20.3.2 Expressões de consulta com continuações

Uma expressão de consulta com uma continuação seguindo o corpo de consulta

from «x1» in «e1» «b1» into «x2» «b2»

é convertida em

from «x2» in ( from «x1» in «e1» «b1» ) «b2»

As conversões nas seções a seguir pressupõem que as consultas não têm continuações.

Exemplo: O Exemplo:

from c in customers
group c by c.Country into g
select new { Country = g.Key, CustCount = g.Count() }

é convertida em:

from g in
   (from c in customers
   group c by c.Country)
select new { Country = g.Key, CustCount = g.Count() }

a tradução final disso é:

customers.
GroupBy(c => c.Country).
Select(g => new { Country = g.Key, CustCount = g.Count() })

fim do exemplo

12.20.3.3 Tipos de variável de intervalo explícito

Uma cláusula from que especifica explicitamente um tipo de variável de intervalo

from «T» «x» in «e»

é convertida em

from «x» in ( «e» ) . Cast < «T» > ( )

Uma cláusula join que especifica explicitamente um tipo de variável de intervalo

join «T» «x» in «e» on «k1» equals «k2»

é convertida em

join «x» in ( «e» ) . Cast < «T» > ( ) on «k1» equals «k2»

As conversões nas seções a seguir pressupõem que as consultas não têm tipos de variáveis de intervalo explícitos.

Exemplo: o exemplo

from Customer c in customers
where c.City == "London"
select c

é convertida em

from c in (customers).Cast<Customer>()
where c.City == "London"
select c

cuja tradução final é

customers.
Cast<Customer>().
Where(c => c.City == "London")

fim do exemplo

Observação: tipos de variável de intervalo explícito são úteis para consultar coleções que implementam a interface de IEnumerable não genérica, mas não a interface de IEnumerable<T> genérica. No exemplo acima, esse seria o caso se os clientes fossem do tipo ArrayList. fim da observação

12.20.3.4 Expressões de consulta degeneradas

Uma expressão de consulta do formato

from «x» in «e» select «x»

é convertida em

( «e» ) . Select ( «x» => «x» )

Exemplo: o exemplo

from c in customers
select c

é convertida em

(customers).Select(c => c)

fim do exemplo

Uma expressão de consulta degenerada é aquela que seleciona trivialmente os elementos da origem.

Observação: fases posteriores da conversão (§12.20.3.6 e §12.20.3.7) removem consultas degeneradas introduzidas por outras etapas de tradução substituindo-as por sua origem. No entanto, é importante garantir que o resultado de uma expressão de consulta nunca seja o próprio objeto de origem. Caso contrário, retornar o resultado de tal consulta pode expor inadvertidamente dados privados (por exemplo, uma matriz de elementos) a um chamador. Portanto, essa etapa protege as consultas degeneradas escritas diretamente no código-fonte chamando explicitamente Select na origem. Em seguida, cabe aos implementadores de Select e outros operadores de consulta garantir que esses métodos nunca retornem o próprio objeto de origem. fim da observação

12.20.3.5 Cláusulas from, let, where, join e orderby

Uma expressão de consulta com uma segunda cláusula from seguida por uma cláusula select

from «x1» in «e1»  
from «x2» in «e2»  
select «v»

é convertida em

( «e1» ) . SelectMany( «x1» => «e2» , ( «x1» , «x2» ) => «v» )

Exemplo: o exemplo

from c in customers
from o in c.Orders
select new { c.Name, o.OrderID, o.Total }

é convertida em

(customers).
SelectMany(c => c.Orders,
(c,o) => new { c.Name, o.OrderID, o.Total }
)

fim do exemplo

Uma expressão de consulta com uma segunda cláusula from seguida por um corpo de consulta Q contendo um conjunto não vazio de cláusulas do corpo da consulta:

from «x1» in «e1»
from «x2» in «e2»
Q

é convertida em

from * in («e1») . SelectMany( «x1» => «e2» ,
                              ( «x1» , «x2» ) => new { «x1» , «x2» } )
Q

Exemplo: o exemplo

from c in customers
from o in c.Orders
orderby o.Total descending
select new { c.Name, o.OrderID, o.Total }

é convertida em

from * in (customers).
   SelectMany(c => c.Orders, (c,o) => new { c, o })
orderby o.Total descending
select new { c.Name, o.OrderID, o.Total }

cuja tradução final é

customers.
SelectMany(c => c.Orders, (c,o) => new { c, o }).
OrderByDescending(x => x.o.Total).
Select(x => new { x.c.Name, x.o.OrderID, x.o.Total })

onde x é um identificador gerado pelo compilador que, caso contrário, é invisível e inacessível.

fim do exemplo

Uma expressão let com a cláusula from que a precede:

from «x» in «e»  
let «y» = «f»  
...

é convertida em

from * in ( «e» ) . Select ( «x» => new { «x» , «y» = «f» } )  
...

Exemplo: o exemplo

from o in orders
let t = o.Details.Sum(d => d.UnitPrice * d.Quantity)
where t >= 1000
select new { o.OrderID, Total = t }

é convertida em

from * in (orders).Select(
    o => new { o, t = o.Details.Sum(d => d.UnitPrice * d.Quantity) })
where t >= 1000
select new { o.OrderID, Total = t }

cuja tradução final é

orders
    .Select(o => new { o, t = o.Details.Sum(d => d.UnitPrice * d.Quantity) })
    .Where(x => x.t >= 1000)
    .Select(x => new { x.o.OrderID, Total = x.t })

onde x é um identificador gerado pelo compilador que, caso contrário, é invisível e inacessível.

fim do exemplo

Uma expressão where com a cláusula from que a precede:

from «x» in «e»  
where «f»  
...

é convertida em

from «x» in ( «e» ) . Where ( «x» => «f» )  
...

Uma cláusula join imediatamente seguida por uma cláusula select

from «x1» in «e1»  
join «x2» in «e2» on «k1» equals «k2»  
select «v»

é convertida em

( «e1» ) . Join( «e2» , «x1» => «k1» , «x2» => «k2» , ( «x1» , «x2» ) => «v» )

Exemplo: o exemplo

from c in customers
join o in orders on c.CustomerID equals o.CustomerID
select new { c.Name, o.OrderDate, o.Total }

é convertida em

(customers).Join(
   orders,
   c => c.CustomerID, o => o.CustomerID,
   (c, o) => new { c.Name, o.OrderDate, o.Total })

fim do exemplo

Uma cláusula join seguida por uma cláusula do corpo da consulta:

from «x1» in «e1»  
join «x2» in «e2» on «k1» equals «k2»  
...

é convertida em

from * in ( «e1» ) . Join(  
«e2» , «x1» => «k1» , «x2» => «k2» ,
( «x1» , «x2» ) => new { «x1» , «x2» })  
...

Uma cláusula join-into imediatamente seguida por uma cláusula select

from «x1» in «e1»  
join «x2» in «e2» on «k1» equals «k2» into «g»  
select «v»

é convertida em

( «e1» ) . GroupJoin( «e2» , «x1» => «k1» , «x2» => «k2» ,
                     ( «x1» , «g» ) => «v» )

Uma cláusula join into seguida por uma cláusula do corpo da consulta

from «x1» in «e1»  
join «x2» in «e2» on «k1» equals «k2» into *g»  
...

é convertida em

from * in ( «e1» ) . GroupJoin(  
   «e2» , «x1» => «k1» , «x2» => «k2» , ( «x1» , «g» ) => new { «x1» , «g» })
...

Exemplo: o exemplo

from c in customers
join o in orders on c.CustomerID equals o.CustomerID into co
let n = co.Count()
where n >= 10
select new { c.Name, OrderCount = n }

é convertida em

from * in (customers).GroupJoin(
    orders,
    c => c.CustomerID,
    o => o.CustomerID,
    (c, co) => new { c, co })
let n = co.Count()
where n >= 10
select new { c.Name, OrderCount = n }

cuja tradução final é

customers
    .GroupJoin(
        orders,
        c => c.CustomerID,
        o => o.CustomerID,
        (c, co) => new { c, co })
    .Select(x => new { x, n = x.co.Count() })
    .Where(y => y.n >= 10)
    .Select(y => new { y.x.c.Name, OrderCount = y.n })

em que x e y são identificadores gerados pelo compilador que são invisíveis e inacessíveis.

fim do exemplo

Uma cláusula orderby e a cláusula from que a precede:

from «x» in «e»  
orderby «k1» , «k2» , ... , «kn»  
...

é convertida em

from «x» in ( «e» ) .
OrderBy ( «x» => «k1» ) .
ThenBy ( «x» => «k2» ) .
... .
ThenBy ( «x» => «kn» )
...

Se uma cláusula ordering especificar um indicador de direção decrescente, uma invocação de OrderByDescending ou ThenByDescending será produzida.

Exemplo: o exemplo

from o in orders
orderby o.Customer.Name, o.Total descending
select o

tem a tradução final

(orders)
    .OrderBy(o => o.Customer.Name)
    .ThenByDescending(o => o.Total)

fim do exemplo

As conversões a seguir pressupõem que não há cláusulas let, where, join ou orderby e não mais do que uma cláusula from inicial em cada expressão de consulta.

12.20.3.6 Cláusulas Select

Uma expressão de consulta do formato

from «x» in «e» select «v»

é convertida em

( «e» ) . Select ( «x» => «v» )

exceto quando «v» é o identificador «x», a conversão é apenas

( «e» )

Exemplo: o exemplo

from c in customers.Where(c => c.City == "London")
select c

é simplesmente convertida em

(customers).Where(c => c.City == "London")

fim do exemplo

12.20.3.7 Agrupar cláusulas

Uma cláusula group

from «x» in «e» group «v» by «k»

é convertida em

( «e» ) . GroupBy ( «x» => «k» , «x» => «v» )

exceto quando «v» é o identificador «x», a conversão é

( «e» ) . GroupBy ( «x» => «k» )

Exemplo: o exemplo

from c in customers
group c.Name by c.Country

é convertida em

(customers).GroupBy(c => c.Country, c => c.Name)

fim do exemplo

12.20.3.8 Identificadores transparentes

Determinadas traduções injetam variáveis de alcance com identificadores transparentes indicados por *. Identificadores transparentes existem apenas como uma etapa intermediária no processo de conversão da expressão de consulta.

Quando uma conversão de consulta injeta um identificador transparente, outras etapas de conversão propagam o identificador transparente em funções anônimas e inicializadores de objetos anônimos. Nesses contextos, identificadores transparentes têm o seguinte comportamento:

  • Quando um identificador transparente ocorre como um parâmetro em uma função anônima, os membros do tipo anônimo associado estão automaticamente dentro do escopo do corpo da função anônima.
  • Quando um membro com um identificador transparente está no escopo, os membros desse membro também estão no escopo.
  • Quando um identificador transparente ocorre como um declarador de membro em um inicializador de objeto anônimo, ele introduz um membro com um identificador transparente.

Nas etapas de conversão descritas acima, os identificadores transparentes são sempre introduzidos junto com os tipos anônimos, com a intenção de capturar várias variáveis de intervalo como membros de um único objeto. Uma implementação de C# tem permissão para usar um mecanismo diferente dos tipos anônimos a fim de agrupar diversas variáveis de intervalo. Os exemplos de conversão a seguir pressupõem que os tipos anônimos são usados e mostram uma possível conversão de identificadores transparentes.

Exemplo: o exemplo

from c in customers
from o in c.Orders
orderby o.Total descending
select new { c.Name, o.Total }

é convertida em

from * in (customers).SelectMany(c => c.Orders, (c,o) => new { c, o })
orderby o.Total descending
select new { c.Name, o.Total }

que é convertida ainda mais em

customers
    .SelectMany(c => c.Orders, (c,o) => new { c, o })
    .OrderByDescending(* => o.Total)
    .Select(\* => new { c.Name, o.Total })

que, quando identificadores transparentes são apagados, é equivalente a

customers
    .SelectMany(c => c.Orders, (c,o) => new { c, o })
    .OrderByDescending(x => x.o.Total)
    .Select(x => new { x.c.Name, x.o.Total })

onde x é um identificador gerado pelo compilador que, caso contrário, é invisível e inacessível.

O exemplo

from c in customers
join o in orders on c.CustomerID equals o.CustomerID
join d in details on o.OrderID equals d.OrderID
join p in products on d.ProductID equals p.ProductID
select new { c.Name, o.OrderDate, p.ProductName }

é convertida em

from * in (customers).Join(
    orders,
    c => c.CustomerID,
    o => o.CustomerID,
    (c, o) => new { c, o })
join d in details on o.OrderID equals d.OrderID
join p in products on d.ProductID equals p.ProductID
select new { c.Name, o.OrderDate, p.ProductName }

que é ainda mais reduzida a

customers
    .Join(orders, c => c.CustomerID,
        o => o.CustomerID, (c, o) => new { c, o })
    .Join(details, * => o.OrderID, d => d.OrderID, (*, d) => new { *, d })
    .Join(products, * => d.ProductID, p => p.ProductID,
        (*, p) => new { c.Name, o.OrderDate, p.ProductName })

cuja tradução final é

customers
    .Join(orders, c => c.CustomerID,
        o => o.CustomerID, (c, o) => new { c, o })
    .Join(details, x => x.o.OrderID, d => d.OrderID, (x, d) => new { x, d })
    .Join(products, y => y.d.ProductID, p => p.ProductID,
        (y, p) => new { y.x.c.Name, y.x.o.OrderDate, p.ProductName })

em que x e y são identificadores gerados pelo compilador que são invisíveis e inacessíveis. fim do exemplo

12.20.4 O padrão da expressão de consulta

O padrão da expressão de consulta estabelece um padrão de métodos que os tipos podem implementar para dar suporte às expressões de consulta.

Um tipo genérico C<T> dá suporte ao padrão da expressão de consulta se seus métodos de membro público e os métodos de extensão publicamente acessíveis puderem ser substituídos pela definição de classe a seguir. Os membros e os métodos de extensão acessíveis são referidos como o "formato" de um tipo genérico C<T>. Um tipo genérico é usado para ilustrar as relações adequadas entre os tipos de parâmetro e de retorno, mas também é possível implementar o padrão para os tipos não genéricos.

delegate R Func<T1,R>(T1 arg1);
delegate R Func<T1,T2,R>(T1 arg1, T2 arg2);

class C
{
    public C<T> Cast<T>() { ... }
}

class C<T> : C
{
    public C<T> Where(Func<T,bool> predicate) { ... }
    public C<U> Select<U>(Func<T,U> selector) { ... }
    public C<V> SelectMany<U,V>(Func<T,C<U>> selector,
        Func<T,U,V> resultSelector) { ... }
    public C<V> Join<U,K,V>(C<U> inner, Func<T,K> outerKeySelector,
        Func<U,K> innerKeySelector, Func<T,U,V> resultSelector) { ... }
    public C<V> GroupJoin<U,K,V>(C<U> inner, Func<T,K> outerKeySelector,
        Func<U,K> innerKeySelector, Func<T,C<U>,V> resultSelector) { ... }
    public O<T> OrderBy<K>(Func<T,K> keySelector) { ... }
    public O<T> OrderByDescending<K>(Func<T,K> keySelector) { ... }
    public C<G<K,T>> GroupBy<K>(Func<T,K> keySelector) { ... }
    public C<G<K,E>> GroupBy<K,E>(Func<T,K> keySelector,
        Func<T,E> elementSelector) { ... }
}

class O<T> : C<T>
{
    public O<T> ThenBy<K>(Func<T,K> keySelector) { ... }
    public O<T> ThenByDescending<K>(Func<T,K> keySelector) { ... }
}

class G<K,T> : C<T>
{
    public K Key { get; }
}

Os métodos acima usam os tipos delegados genéricos Func<T1, R> e Func<T1, T2, R>, mas também podem ter usado outros tipos de delegados ou árvore de expressão com as mesmas relações nos tipos de parâmetro e retorno.

Observação: a relação recomendada entre C<T> e O<T> que garante que os métodos ThenBy e ThenByDescending estejam disponíveis apenas no resultado de um OrderBy ou OrderByDescending. fim da observação

Observação: a forma recomendada do resultado de GroupBy – uma sequência de sequências, em que cada sequência interna tem uma propriedade Key adicional. fim da observação

Observação: como as expressões de consulta são convertidas para invocações de método por meio de um mapeamento sintático, os tipos têm flexibilidade considerável na forma como implementam um ou todos os padrões da expressão de consulta. Por exemplo, os métodos do padrão podem ser implementados como métodos de instância ou como métodos de extensão porque os dois têm a mesma sintaxe de invocação e os métodos podem solicitar delegado ou árvores de expressão porque as funções anônimas são conversíveis para os dois. Tipos que implementam apenas parte do padrão de expressão de consulta suportam apenas conversões de expressões de consulta que são mapeadas para os métodos compatíveis com esse tipo. fim da observação

Observação: o namespace System.Linq fornece uma implementação do padrão da expressão de consulta para qualquer tipo que implemente a interface System.Collections.Generic.IEnumerable<T>. fim da observação

12.21 Operadores de atribuição

12.21.1 Geral

Todos, exceto um dos operadores de atribuição, atribuem um novo valor a uma variável, propriedade, evento ou elemento indexador. A exceção = ref atribui uma referência variável (§9.5) a uma variável de referência (§9.7).

assignment
    : unary_expression assignment_operator expression
    ;

assignment_operator
    : '=' 'ref'? | '+=' | '-=' | '*=' | '/=' | '%=' | '&=' | '|=' | '^=' | '<<=' | '??='
    | right_shift_assignment
    ;

O operando esquerdo de uma atribuição deve ser uma expressão classificada como uma variável ou, com exceção de = ref, um acesso de propriedade, um acesso de indexador, um acesso de evento ou uma tupla. Uma expressão de declaração não é permitida diretamente como um operando esquerdo, mas pode ocorrer como uma etapa na avaliação de uma atribuição de desconstrução.

O operador = é chamado de operador de atribuição simples. Ele atribui o valor ou os valores do operando direito à variável, propriedade, elemento indexador ou aos elementos de tupla fornecidos pelo operando esquerdo. O operando esquerdo do operador de atribuição simples não deve ser um acesso de evento (exceto conforme descrito em §15.8.2). O operador de atribuição simples é descrito em §12.21.2.

O operador = ref é chamado de operador de atribuição ref. Constitui o operando à direita, que deve ser variable_reference (§9.5), o referenciante da variável de referência designada pelo operando esquerdo. O operador de atribuição ref é descrito em §12.21.3.

Os operadores de atribuição, exceto os operadores = e = ref, são chamados de operadores de atribuição composta. Esses operadores são processados da seguinte maneira:

  • Para o operador ??=, somente se o valor do operando esquerdo for null, o operando à direita será avaliado e o resultado atribuído à variável, propriedade ou elemento de indexador especificado pelo operando esquerdo.
  • Caso contrário, a operação indicada é executada nos dois operandos e, em seguida, o valor resultante é atribuído ao elemento variável, propriedade ou indexador fornecido pelo operando esquerdo. Os operadores de atribuição composta são descritos em §12.21.4.

Os operadores += e -= com uma expressão de acesso de evento como operando esquerdo são chamados de operadores de atribuição de eventos. Nenhum outro operador de atribuição é válido com um acesso de evento como o operando esquerdo. Os operadores de atribuição de evento são descritos em §12.21.5.

Os operadores de atribuição são associativos à direita, o que significa que as operações são agrupadas da direita para a esquerda.

Exemplo: uma expressão do formato a = b = c é avaliada como a = (b = c). fim do exemplo

12.21.2 Atribuição simples

O operador = é chamado de operador de atribuição simples.

Se o operando esquerdo de uma atribuição simples for do formato E.P ou E[Ei] em que E tem o tipo de tempo de compilação dynamic, a atribuição será associada dinamicamente (§12.3.3). Nesse caso, o tipo de tempo de compilação da expressão de atribuição é dynamice a resolução descrita abaixo ocorrerá em run-time com base no tipo de run-time de E. Se o operando esquerdo for da forma E[Ei] em que pelo menos um elemento de Ei tiver o tipo de tempo de compilação dynamic, e o tipo de tempo de compilação de E não for uma matriz, o acesso ao indexador resultante é dinamicamente vinculado, mas com verificação de tempo de compilação limitada (§12.6.5).

Uma atribuição simples em que o operando esquerdo é classificado como uma tupla também é chamada de atribuição de desconstrução. Se algum dos elementos da tupla do operando esquerdo tiver um nome, ocorrerá um erro em tempo de compilação. Se qualquer um dos elementos da tupla do operando esquerdo for uma declaration_expression, e qualquer outro elemento não for uma declaration_expression ou um simples descarte, ocorrerá um erro em tempo de compilação.

O tipo de uma atribuição simples x = y é o tipo de uma atribuição para x de y, que é determinada recursivamente da seguinte maneira:

  • Se x for uma expressão de tupla (x1, ..., xn) e y puder ser desconstruída em uma expressão de tupla (y1, ..., yn) com elementos de n (§12.7), e cada atribuição para xi de yi tiver o tipo Ti, a atribuição terá o tipo (T1, ..., Tn).
  • Caso contrário, se x for classificado como uma variável, a variável não será readonly, x tiver um tipo T e y tiver uma conversão implícita para T, a atribuição terá o tipo T.
  • Caso contrário, se x for classificado como uma variável tipada implicitamente (ou seja, uma expressão de declaração tipada implicitamente) e y tiver um tipo T, o tipo inferido da variável será T e a atribuição terá o tipo T.
  • Caso contrário, se x for classificado como um acesso de indexador ou propriedade, a propriedade ou o indexador tiver um acessador set acessível, x tiver um tipo T e y tiver uma conversão implícita para T, a atribuição terá o tipo T.
  • Caso contrário, a atribuição não será válida e ocorrerá um erro de tempo de associação.

O processamento run-time de uma atribuição simples do formato x = y com o tipo T é executado como uma atribuição para x de y com o tipo T, que consiste nas seguintes etapas recursivas:

  • x é avaliado caso ainda não tenha sido.
  • Se x for classificado como uma variável, y será avaliado e, se necessário, convertido em T por meio de uma conversão implícita (§10.2).
    • Se a variável fornecida por x for um elemento de matriz de reference_type, uma verificação run-time será executada para garantir que o valor computado para y seja compatível com a instância de matriz da qual x é um elemento. A verificação terá êxito se y for null ou se uma conversão de referência implícita (§10.2.8) existir no tipo da instância referenciada por y para o tipo de elemento real da instância de matriz que contém x. Caso contrário, uma System.ArrayTypeMismatchException será gerada.
    • O valor resultante da avaliação e conversão de y é armazenado no local fornecido pela avaliação de x e é gerado como resultado da atribuição.
  • Se x for classificado como uma propriedade ou acesso de indexador:
    • y é avaliado e, se necessário, convertido em T por meio de uma conversão implícita (§10.2).
    • O acessador set de x é invocado com o valor resultante da avaliação e conversão de y como seu argumento de valor.
    • O valor resultante da avaliação e conversão de y é produzido como resultado da atribuição.
  • Se x for classificado como uma tupla (x1, ..., xn) com aridade n:
    • y é desconstruído com elementos n em uma expressão de tupla e.
    • uma tupla de resultado t é criada ao converter e em T usando uma conversão de tupla implícita.
    • para cada xi da esquerda para a direita, uma atribuição a xi de t.Itemi é executada, exceto que os xi não são avaliados novamente.
    • t é gerado como resultado da atribuição.

Observação: se o tipo de tempo de compilação de x for dynamic e houver uma conversão implícita do tipo de tempo de compilação de y para dynamic, nenhuma resolução run-time será necessária. fim da observação

Observação: as regras de covariância de matriz (§17.6) permitem que um valor de um tipo de matriz A[] seja uma referência a uma instância de um tipo de matriz B[], desde que exista uma conversão de referência implícita de B para A. Devido a essas regras, a atribuição a um elemento de matriz de um reference_type requer uma verificação run-time para garantir que o valor atribuído seja compatível com a instância da matriz. No exemplo

string[] sa = new string[10];
object[] oa = sa;
oa[0] = null;              // OK
oa[1] = "Hello";           // OK
oa[2] = new ArrayList();   // ArrayTypeMismatchException

a última atribuição faz com que uma System.ArrayTypeMismatchException seja lançada porque uma referência a um ArrayList não pode ser armazenada em um elemento de um string[].

fim da observação

Quando uma propriedade ou indexador declarado em um struct_type é o destino de uma atribuição, a expressão de instância associada à propriedade ou ao acesso do indexador deve ser classificada como uma variável. Se a expressão de instância for classificada como um valor, ocorrerá um erro de tempo de associação.

Observação: devido a §12.8.7, a mesma regra também se aplica aos campos. fim da observação

Exemplo: dadas as declarações:

struct Point
{
   int x, y;

   public Point(int x, int y)
   {
      this.x = x;
      this.y = y;
   }

   public int X
   {
      get { return x; }
      set { x = value; }
   }

   public int Y {
      get { return y; }
      set { y = value; }
   }
}

struct Rectangle
{
    Point a, b;

    public Rectangle(Point a, Point b)
    {
        this.a = a;
        this.b = b;
    }

    public Point A
    {
        get { return a; }
        set { a = value; }
    }

    public Point B
    {
        get { return b; }
        set { b = value; }
    }
}

no exemplo

Point p = new Point();
p.X = 100;
p.Y = 100;
Rectangle r = new Rectangle();
r.A = new Point(10, 10);
r.B = p;

as atribuições para p.X, p.Y, r.A e r.B são permitidas porque p e r são variáveis. No entanto, no exemplo

Rectangle r = new Rectangle();
r.A.X = 10;
r.A.Y = 10;
r.B.X = 100;
r.B.Y = 100;

as atribuições são todas inválidas, já que r.A e r.B não são variáveis.

fim do exemplo

12.21.3 Atribuição de referência

O operador = ref é conhecido como operador de atribuição de referência.

O operando esquerdo deve ser uma expressão que se associe a uma variável de referência (§9.7), um parâmetro de referência (diferente de this), um parâmetro de saída ou um parâmetro de entrada. O operando direito deve ser uma expressão que produz variable_reference designando um valor do mesmo tipo que o operando esquerdo.

É um erro de tempo de compilação se ref-safe-context (§9.7.2) do operando esquerdo for mais amplo do que ref-safe-context do operando à direita.

O operando direito deve ser definitivamente atribuído no ponto da atribuição de referência.

Quando o operando esquerdo se associa a um parâmetro de saída, ocorrerá um erro se esse parâmetro de saída não tiver sido definitivamente atribuído no início do operador de atribuição ref.

Se o operando esquerdo for um ref gravável (ou seja, ele designa qualquer coisa diferente de um parâmetro local ou de entrada ref readonly), o operando à direita deverá ser um variable_reference gravável. Se a variável de operando direito for gravável, o operando esquerdo poderá ser uma referência gravável ou somente leitura.

A operação torna o operando esquerdo um alias da variável do operando direito. O alias pode ser definido como somente leitura, mesmo que a variável do operando direito seja gravável.

O operador de atribuição de referência resulta em uma variable_reference do tipo atribuído. Será gravável se o operando esquerdo for gravável.

O operador de atribuição de referência não deve ler o local de armazenamento referenciado pelo operando direito.

Exemplo: veja alguns exemplos de como usar = ref:

public static int M1() { ... }
public static ref int M2() { ... }
public static ref uint M2u() { ... }
public static ref readonly int M3() { ... }
public static void Test()
{
int v = 42;
ref int r1 = ref v; // OK, r1 refers to v, which has value 42
r1 = ref M1();      // Error; M1 returns a value, not a reference
r1 = ref M2();      // OK; makes an alias
r1 = ref M2u();     // Error; lhs and rhs have different types
r1 = ref M3();    // error; M3 returns a ref readonly, which r1 cannot honor
ref readonly int r2 = ref v; // OK; make readonly alias to ref
r2 = ref M2();      // OK; makes an alias, adding read-only protection
r2 = ref M3();      // OK; makes an alias and honors the read-only
r2 = ref (r1 = ref M2());  // OK; r1 is an alias to a writable variable,
              // r2 is an alias (with read-only access) to the same variable
}

fim do exemplo

Observação: ao ler-se o código usando um operador = ref, pode ser tentador ler a parte ref como sendo parte do operando. Isso pode causar alguma confusão quando o operando é uma expressão de ?: condicional. **Por exemplo, ao ler ref int a = ref b ? ref x : ref y;, é importante entender isso como = ref sendo o operador e b ? ref x : ref y sendo o operando à direita: ref int a = ref (b ? ref x : ref y);. É importante ressaltar que a expressão ref bnão é parte dessa instrução, embora possa parecer assim à primeira vista. fim da observação

12.21.4 Atribuição composta

Se o operando esquerdo de uma atribuição composta for do formato E.P ou E[Ei] em que E tem o tipo em tempo de compilação dynamic, a atribuição será associada dinamicamente (§12.3.3). Nesse caso, o tipo de tempo de compilação da expressão de atribuição é dynamice a resolução descrita abaixo ocorrerá em run-time com base no tipo de run-time de E. Se o operando esquerdo for da forma E[Ei] em que pelo menos um elemento de Ei tiver o tipo de tempo de compilação dynamic, e o tipo de tempo de compilação de E não for uma matriz, o acesso ao indexador resultante é dinamicamente vinculado, mas com verificação de tempo de compilação limitada (§12.6.5).

a ??= b é equivalente a (T) (a ?? (a = b)), exceto que a é avaliado apenas uma vez, onde T é o tipo de a quando o tipo b de é dinâmico e, caso contrário T , é o tipo de a ?? b.

Caso contrário, uma operação do formulário x «op»= y é processada aplicando a resolução de sobrecarga do operador binário (§12.4.5) como se a operação tivesse sido gravada x «op» y. Então

  • Se o tipo de retorno do operador selecionado for implicitamente conversível para o tipo x, a operação será avaliada como x = x «op» y, exceto que x receberá a avaliação uma vez.
  • Caso contrário, se o operador selecionado for um operador predefinido, se o tipo de retorno do operador selecionado for explicitamente conversível para o tipo x e se y for implicitamente conversível para o tipo x ou o operador for um operador de deslocamento, a operação será avaliada como x = (T)(x «op» y), em que T é o tipo x, exceto que x receberá a avaliação apenas uma vez.
  • Caso contrário, a atribuição composta é inválida e ocorre um erro de tempo de associação.

O termo "receberá a avaliação apenas uma vez" significa que, na avaliação de x «op» y, os resultados das expressões constituintes de x são temporariamente salvos e reutilizados ao executar-se a atribuição para x.

Exemplo: no A()[B()] += C() de atribuição, em que A é um método que retorna int[] e B e C são métodos que retornam int, os métodos são invocados apenas uma vez, na ordem A, B, C. fim do exemplo

Quando o operando esquerdo de uma atribuição composta for um acesso de propriedade ou de indexador, a propriedade ou o indexador deverá ter tanto um acessador do tipo get quanto um acessador do tipo set. Se esse não for o caso, ocorrerá um erro de tempo de associação.

A segunda regra acima permite que x «op»= y seja avaliado como x = (T)(x «op» y) em contextos específicos. A regra existe para que os operadores pré-definidos possam ser usados como operadores compostos quando o operando esquerdo for do tipo sbyte, byte, short, ushortou char. Mesmo quando os dois argumentos são de um desses tipos, os operadores predefinidos produzem um resultado do tipo int, conforme descrito em §12.4.7.3. Portanto, sem uma conversão, não seria possível atribuir o resultado ao operando esquerdo.

O efeito intuitivo da regra para operadores predefinidos é que x «op»= y é permitido se x «op» y e x = y forem permitidos.

Exemplo: no código a seguir

byte b = 0;
char ch = '\0';
int i = 0;
b += 1;           // OK
b += 1000;        // Error, b = 1000 not permitted
b += i;           // Error, b = i not permitted
b += (byte)i;     // OK
ch += 1;          // Error, ch = 1 not permitted
ch += (char)1;    // OK

O motivo intuitivo para cada erro é que uma atribuição simples correspondente também teria sido um erro.

fim do exemplo

Observação: isso também significa que as operações de atribuição composta suportam operadores elevados. Como uma atribuição composta x «op»= y é avaliada como x = x «op» y ou x = (T)(x «op» y), as regras de avaliação abrangem implicitamente os operadores elevados. fim da observação

12.21.5 Atribuição de evento

Se o operando esquerdo do operador a += or -= for classificado como um acesso de evento, a expressão será avaliada da seguinte maneira:

  • A expressão de instância, se houver, do acesso ao evento é avaliada.
  • O operando à direita do operador += ou -= é avaliado e, se necessário, convertido no tipo do operando esquerdo por meio de uma conversão implícita (§10.2).
  • Um acessador do evento é invocado, com uma lista de argumentos que consiste no valor calculado na etapa anterior. Se o operador for +=, o acessador de adição será invocado; se o operador for -=, o acessador de remoção será invocado.

Uma expressão de atribuição de evento não suspende um valor. Portanto, uma expressão de atribuição de evento é válida apenas no contexto de um statement_expression (§13.7).

12.22 Expressão

Uma expressão é uma non_assignment_expression ou uma atribuição.

expression
    : non_assignment_expression
    | assignment
    ;

non_assignment_expression
    : declaration_expression
    | conditional_expression
    | lambda_expression
    | query_expression
    ;

12.23 Expressões constantes

Uma expressão constante é uma expressão que deve ser totalmente avaliada em tempo de compilação.

constant_expression
    : expression
    ;

Uma expressão constante deve ter o valor null ou um dos seguintes tipos:

  • sbyte, byte, short, ushort, int, , uint, long, , ulong, char, float, , double, decimal, bool, string;
  • um tipo de enumeração; ou
  • uma expressão de valor padrão (§12.8.21) para um tipo de referência.

Somente os seguintes constructos são permitidos em expressões constantes:

  • Literais (incluindo o literal null).
  • Referências a membros const de tipos de classe e estrutura.
  • Referências a membros de tipos de enumeração.
  • Referências a constantes locais.
  • Subexpressões entre parênteses, que são elas mesmas expressões constantes.
  • Expressões de conversão.
  • expressões checked e unchecked.
  • Expressões nameof.
  • Os operadores unários predefinidos +, -, ! (negação lógica) e ~.
  • Os operadores binários predefinidos +, -, *, /, %, <<, >>, &, |, ^, &&, ||, ==, !=, <, >, <= e >=.
  • O operador condicional ?:.
  • Operador tolerante a valores nulos ! (§12.8.9).
  • expressões sizeof, desde que o tipo não gerenciado seja um dos tipos especificados em §23.6.9 para os quais sizeof retorna um valor constante.
  • Expressões de valor padrão, desde que o tipo seja um dos tipos listados anteriormente.

As seguintes conversões são permitidas nas expressões constantes:

  • Conversões de identidade
  • Conversões numéricas
  • Conversões de enumeração
  • Conversões de expressão constante
  • Conversões de referência implícitas e explícitas, desde que a origem das conversões seja uma expressão constante avaliada como o valor null.

Observação: outras conversões, incluindo boxing, unboxing e conversões de referência implícita de valores que não sejam null, não são permitidas em expressões constantes. fim da observação

Exemplo: no código a seguir

class C
{
    const object i = 5;         // error: boxing conversion not permitted
    const object str = "hello"; // error: implicit reference conversion
}

a inicialização de i é um erro porque uma conversão de boxing é necessária. A inicialização de str é um erro porque uma conversão de referência implícita de um valor não null é necessária.

fim do exemplo

Sempre que uma expressão atender aos requisitos listados acima, a expressão será avaliada em tempo de compilação. Isso é verdadeiro mesmo se a expressão for uma subexpressão de uma expressão maior que contenha as construções não constantes.

A avaliação em tempo de compilação de expressões constantes usa as mesmas regras que a avaliação run-time de expressões não constantes, exceto quando a avaliação run-time gera uma exceção, pois nesse caso, a avaliação em tempo de compilação produzirá um erro de tempo de compilação.

A menos que uma expressão constante seja explicitamente colocada em um contexto unchecked, estouros que ocorrem em operações e conversões aritméticas de tipo integral durante a avaliação em tempo de compilação da expressão sempre causam erros em tempo de compilação (§12.8.20).

Expressões constantes são necessárias nos contextos listados abaixo e isso é indicado na gramática usando-se constant_expression. Nesses contextos, ocorrerá um erro em tempo de compilação caso uma expressão não possa ser totalmente avaliada em tempo de compilação.

  • Declarações de constante (§15.4)
  • Declarações de membro de enumeração (§19.4)
  • Argumentos padrão de listas de parâmetros (§15.6.2)
  • Rótulos case de uma instrução switch (§13.8.3).
  • Instruções goto case (§13.10.4)
  • Tamanhos de dimensão em uma expressão de criação de matriz (§12.8.17.4) que inclui um inicializador.
  • Atributos (§22)
  • Em um constant_pattern (§11.2.3)

Uma conversão de expressão constante implícita (§10.2.11) permite que uma expressão constante do tipo int seja convertida em sbyte, byte, short, ushort, uint ou ulong, desde que o valor da expressão constante esteja dentro do intervalo do tipo de destino.

12.24 Expressões boolianas

Um boolean_expression é uma expressão que produz um resultado do tipo bool; diretamente ou por meio da aplicação de operator true em alguns contextos, conforme especificado abaixo:

boolean_expression
    : expression
    ;

A expressão condicional de controle de uma if_statement (§13.8.2), while_statement (§13.9.2), do_statement (§13.9.3) ou for_statement (§13.9.4) é uma boolean_expression. A expressão condicional de controle do operador ?: (§12.18) segue as mesmas regras de uma expressão booleana , mas, por motivos de precedência do operador, é classificada como uma expressão de coalescência nula .

Um boolean_expressionE é necessário para se poder produzir um valor do tipo bool da seguinte maneira:

  • Se E for implicitamente conversível para bool, essa conversão implícita será aplicada em run-time.
  • Caso contrário, a resolução de sobrecarga de operador unário (§12.4.4) é utilizada para encontrar a melhor implementação única de operator true em E, e essa implementação é aplicada em tempo de execução.
  • Se nenhum operador desse tipo for encontrado, ocorrerá um erro de tempo de associação.