Partilhar via


12 Expressões

12.1 Generalidades

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

12.2 Classificações das expressões

12.2.1 Generalidades

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 é explicitamente digitada e tem um tipo associado, ou seja, o tipo declarado da variável. Uma variável digitada implicitamente não tem nenhum tipo associado.
  • Um literal nulo. Uma expressão com essa classificação pode ser implicitamente convertida 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 implicitamente convertida em um tipo de delegado compatível ou tipo de árvore de expressão.
  • Uma tupla. Cada tupla tem um número fixo de elementos, cada um com uma expressão e um nome de elemento de tupla opcional.
  • Um acesso à propriedade. Cada acesso à propriedade tem um tipo associado, ou seja, o tipo da propriedade. Além disso, um acesso à propriedade pode ter uma expressão de instância associada. Quando um acessor de uma propriedade de instância é invocado, o resultado da avaliação da expressão da instância torna-se a instância representada por this (§12.8.14).
  • Acesso a um indexador. Cada acesso ao 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 um acesso de indexador é invocado, o resultado da avaliação da expressão da instância torna-se a instância representada por this (§12.8.14), e o resultado da avaliação da lista de argumentos torna-se 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 de void. Uma expressão classificada como nada só é válida no contexto de uma statement_expression (§13.7) ou como o corpo de uma lambda_expression (§12.19).

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

  • Um namespace. Uma expressão com esta classificação só pode aparecer no lado esquerdo de um member_access (§12.8.7). Em qualquer outro contexto, uma expressão classificada como um namespace causa um erro em tempo de compilação.
  • Um tipo. Uma expressão com esta classificação só pode aparecer no lado esquerdo de um member_access (§12.8.7). Em qualquer outro contexto, uma expressão classificada como um tipo causa um erro em tempo de compilação.
  • Um grupo de métodos, que é um conjunto de métodos sobrecarregados resultantes de uma consulta de membros (§12.5). Um grupo de métodos pode ter uma expressão de instância associada e uma lista de argumentos de tipo associada. 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 numa expressão de invocação (§12.8.10) ou numa expressão de criação de delegado (§12.8.17.6), podendo ser implicitamente convertido para 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 em tempo de compilação.
  • Entrada para um evento Cada acesso ao evento tem um tipo associado, ou seja, o tipo do evento. Além disso, um acesso a eventos pode ter uma expressão de instância associada. Um acesso a eventos pode aparecer como o operando esquerdo dos operadores += e -= (§12.21.5). Em qualquer outro contexto, uma expressão classificada como um acesso a eventos causa um erro em tempo de compilação. Quando um acessador de acesso a evento de instância é invocado, o resultado da avaliação da expressão da instância transforma-se na instância representada por this (§12.8.14).
  • Uma expressão de lançamento, que pode ser usada em vários contextos para lançar uma exceção numa expressão. Uma expressão throw pode ser convertida por conversão implícita para qualquer tipo.

Um acesso de propriedade ou acesso de indexador é sempre reclassificado como um valor executando uma invocação do acessador get ou do acessador definido. O acessador específico é determinado pelo contexto da propriedade ou acesso do indexador: Se o acesso for o destino de uma atribuição, o acessador definido é 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 a eventos em uma instância ou um acesso de indexador.

12.2.2 Valores das expressões

A maioria das construções que envolvem uma expressão, em última análise, requer que a expressão denote um valor . Nesses casos, se a expressão real denotar um namespace, um tipo, um grupo de métodos ou nada, ocorrerá um erro em tempo de compilação. No entanto, se a expressão denotar um acesso à propriedade, um acesso de indexador ou uma variável, o valor da propriedade, indexador ou variável será implicitamente substituído:

  • O valor de uma variável é simplesmente o valor atualmente armazenado 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 então ocorre um erro em tempo de compilação.
  • O valor de uma expressão de acesso a uma propriedade é obtido ao invocar o método get da propriedade. Se a propriedade não tiver um accessor get, ocorrerá um erro em tempo de compilação. Caso contrário, uma invocação de membro da função (§12.6.6) é executada e o resultado da invocação torna-se o valor da expressão de acesso à propriedade.
  • O valor de uma expressão de acesso do indexador é obtido invocando o acessor get do indexador. Se o indexador não tiver um 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 associada à 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 uma conversão implícita de tupla (§10.2.13) ao tipo de expressão de tupla. É um erro obter o valor de uma expressão de tupla que não tem um tipo.

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

12.3.1 Generalidades

Binding é o processo de determinar a que uma operação se refere, com base no tipo ou valor de expressões (argumentos, operandos, recetores). Por exemplo, a ligação de uma chamada de método é determinada com base no tipo de recetor e argumentos. A ligação de um operador é determinada com base no tipo de seus operandos.

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

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

Quando uma operação é vinculada dinamicamente, pouca ou nenhuma verificação é executada em tempo de compilação. Em vez disso, se a associação em tempo de execução falhar, os erros serão relatados como exceções em tempo de execução.

As seguintes operações em C# estão sujeitas a vinculação:

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

Quando nenhuma expressão dinâmica está envolvida, o C# assume como padrão a vinculação estática, o que significa que os tipos de subexpressões em 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.

É um erro de tempo de compilação se uma invocação de método for dinamicamente vinculada e qualquer dos parâmetros, incluindo o recetor, for um parâmetro de entrada.

12.3.2 Tempo de vinculação

A vinculação estática ocorre em tempo de compilação, enquanto a vinculação dinâmica ocorre em tempo de execução. Nas subcláusulas a seguir, o termo de tempo de vinculação refere-se a tempo de compilação ou tempo de execução, dependendo de quando a ligação ocorre.

Exemplo: O seguinte ilustra as noções de ligação estática e dinâmica e de tempo de ligaçã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 limitadas estaticamente: a sobrecarga de Console.WriteLine é escolhida com base no tipo de tempo de compilação de seu argumento. Assim, o tempo de ligação é tempo de compilação.

A terceira chamada é limitada dinamicamente: a sobrecarga de Console.WriteLine é escolhida com base no tipo de tempo de execução de seu argumento. Isso acontece porque o argumento é uma expressão dinâmica – seu tipo de tempo de compilação é dinâmico. Assim, o tempo de ligação para a terceira chamada é tempo de execução.

exemplo final

12.3.3 Ligação dinâmica

Esta subcláusula é informativa.

A vinculação dinâmica permite que programas C# interajam com objetos dinâmicos, ou seja, objetos que não seguem as regras normais do sistema de tipo 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 programaticamente configurados para implementar sua própria semântica de ligação para diferentes operações.

O mecanismo pelo qual um objeto dinâmico implementa sua própria semântica é definido pela implementação. Uma determinada interface – novamente definida pela implementação – é implementada por objetos dinâmicos para sinalizar ao tempo de execução do C# que eles têm semântica especial. Assim, sempre que as operações em um objeto dinâmico são vinculadas dinamicamente, suas próprias semânticas de ligação, em vez das do C# conforme especificado nesta especificação, assumem o controle.

Enquanto o objetivo da vinculação dinâmica é permitir a interoperação com objetos dinâmicos, o C# permite a vinculaçã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, já que os resultados das operações neles podem não ser objetos dinâmicos, mas ainda são de um tipo desconhecido para o programador em tempo de compilação. Além disso, a vinculaçã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 é ligada estaticamente, o tipo de uma subexpressão (por exemplo, um recetor e argumento, um índice ou um operando) é sempre considerado como sendo o tipo em tempo de compilação dessa expressão.

Quando uma operação é vinculada 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 dinâmica de tipo de tempo de compilação é considerada como tendo o tipo do valor real que a expressão avalia em tempo de execução
  • Uma subexpressão cujo tipo durante a compilação é um parâmetro de tipo é considerada como tendo o tipo ao qual o parâmetro de tipo está associado durante a execução.
  • Caso contrário, a subexpressão é considerada como tendo seu tipo de tempo de compilação.

12.4 Operadores

12.4.1 Generalidades

As 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.

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

Existem três tipos de operadores:

  • Operadores unários. Os operadores unários usam um operando e podem seguir notação de prefixo (como –x) ou notação de sufixo (como x++).
  • Operadores binários. Os operadores binários usam dois operandos e todos usam notação infix (como x + y).
  • Operador ternário. Existe apenas um operador ternário, ?:, ; ele usa três operandos e usa notação infix (c ? x : y).

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

Os 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 o valor antigo de i, então o método G é chamado com o valor antigo de ie, finalmente, o método H é chamado com o novo valor de i. Isso é separado e não relacionado à precedência do operador. exemplo final

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

12.4.2 Precedência e associatividade do operador

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.

Nota: Por exemplo, a expressão x + y * z é avaliada como x + (y * z) porque o operador * tem maior precedência do que o operador + binário. nota final

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

Nota: Por exemplo, um additive_expression consiste numa sequência de multiplicative_expressions separados por operadores + ou -, dando assim aos operadores + e - menor precedência do que os operadores *, /e %. nota final

Nota: O quadro seguinte resume todos os operadores por ordem de precedência do mais elevado para o mais baixo:

Subcláusula Categoria Operadores
§12.8 Primário 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 Aditivo + -
§12.11 Turno << >>
§12.12 Testes relacionais e de tipo < > <= >= is as
§12.12 Igualdade == !=
§12.13 Lógica E &
§12.13 XOR lógico ^
§12.13 OU Lógico \|
§12.14 Condicional E &&
§12.14 OU condicional \|\|
§12.15 e §12.16 Expressão de coalescência e lançamento nula ?? throw x
§12.18 Condicional ?:
§12.21 e §12.19 Atribuição e expressão lambda = = ref *= /= %= += -= <<= >>= &= ^= \|= =>

nota final

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

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

    Exemplo: x + y + z é avaliada como (x + y) + z. exemplo final

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

    Exemplo: x = y = z é avaliada como x = (y = z). exemplo final

A precedência e a associatividade podem ser controladas entre parênteses.

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

12.4.3 Sobrecarga do operador

Todos os operadores unários e binários têm implementações predefinidas. Além disso, implementações definidas pelo usuário podem ser introduzidas incluindo 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 existirem implementações de operador definidas pelo usuário aplicáveis as implementações de operador predefinidas serão consideradas, conforme descrito no §12.4.4 e §12.4.5.

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

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

Nota: Embora true e false não sejam usados explicitamente em expressões (e, portanto, não estejam incluídos na tabela de precedência no §12.4.2), eles são considerados operadores porque são invocados em vários contextos de expressão: expressões booleanas (§12.24) e expressões envolvendo o condicional (§12.18) e operadores lógicos condicionais (§12.14). nota final

Nota: O operador de perdão nulo (postfix !, §12.8.9) não é um operador sobrecarregável. nota final

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

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

Apenas os operadores listados acima podem ser sobrecarregados. Em particular, não é possível sobrecarregar o acesso de membros, a invocação de métodos ou os operadores =, &&, ||, ??, ?:, =>, checked, unchecked, new, typeof, default, ase is.

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

Exemplo: Uma sobrecarga do operador * é também uma sobrecarga do operador *=. Isto é descrito mais pormenorizadamente no §12.21. exemplo final

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

As operações de transmissão, como (T)x, são sobrecarregadas pelo fornecimento de conversões definidas pelo usuário (§10.5).

Nota: As conversões definidas pelo usuário não afetam o comportamento dos operadores is ou as. nota final

O acesso a elementos, como a[x], não é considerado um operador passível de sobrecarga. Em vez disso, a indexação definida pelo usuário é suportada por meio de indexadores (§15.9).

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

Nota: Para um exemplo de sobrecarga dos operadores de ++ e --, ver §15.10.2. nota final

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 seja do tipo de classe ou struct que contém a declaração do operador.

Nota: Assim, não é possível que um operador definido pelo utilizador tenha a mesma assinatura que um operador predefinido. nota final

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

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

Nota: Embora seja possível para um operador definido pelo usuário executar qualquer cálculo que desejar, implementações que produzem resultados diferentes daqueles que são intuitivamente esperados são fortemente desencorajadas. Por exemplo, uma implementação de operador == deve comparar os dois operandos para igualdade e retornar um resultado bool apropriado. nota final

As descrições de operadores individuais em §12.9 a §12.21 especificar as implementações predefinidas dos operadores e quaisquer regras adicionais que se apliquem a cada operador. As descrições fazem uso dos termos resolução de sobrecarga do operador unário, resolução de sobrecarga do operador binário, promoção numérica, e definições de operador levantadas das quais são encontradas nas subcláusulas a seguir.

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

Uma operação da forma «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 forma:

  • O conjunto de operadores candidatos definidos pelo usuário fornecidos pela X para a operação operator «op»(x) é determinado usando as regras do §12.4.6.
  • Se o conjunto de operadores candidatos definidos pelo usuário não estiver vazio, isso se tornará o conjunto de operadores candidatos para a operação. Caso contrário, as implementações de operator «op» binárias predefinidas, incluindo as suas formas elevadas, tornam-se o conjunto de operadores possíveis 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ó são incluídos neste conjunto quando o tipo de tempo de ligação — ou o tipo subjacente, se for um tipo anulável — de qualquer operando é o tipo enum ou delegado.
  • As regras de resolução de sobrecarga de §12.6.4 são aplicadas ao conjunto de operadores candidatos para selecionar o melhor operador em relação à lista de argumentos (x), e este operador torna-se o resultado do processo de resolução de sobrecarga. Se a resolução de sobrecarga não conseguir selecionar um único melhor operador, ocorrerá um erro de tempo de ligação.

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

Uma operação da forma x «op» y, onde «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 forma:

  • O conjunto de operadores candidatos definidos pelo usuário fornecidos por X e Y para a operação operator «op»(x, y) é determinado. O conjunto consiste na união dos operadores candidatos fornecidos pelo X e dos operadores candidatos fornecidos pelo Y, cada um determinado usando as regras do §12.4.6. Para o conjunto combinado, os candidatos são fundidos da seguinte forma:
    • Se X e Y forem conversíveis de identidade, ou se X e Y forem derivadas de um tipo de base comum, os operadores candidatos 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 pelo Y tem o mesmo tipo de retorno que um «op»X fornecido por X e os tipos de operando de «op»Y têm uma conversão de identidade para os tipos de operando correspondentes de «op»X, então apenas «op»X ocorre no conjunto.
  • Se o conjunto de operadores candidatos definidos pelo usuário não estiver vazio, isso se tornará o conjunto de operadores candidatos para a operação. Caso contrário, as implementações de operator «op» binárias predefinidas, incluindo suas formas levantadas, 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 enum e delegate predefinidos, os únicos operadores considerados são aqueles fornecidos por um enum ou tipo de delegado que é o tipo de tempo de ligação de um dos operandos.
  • As regras de resolução de sobrecarga de §12.6.4 são aplicadas ao conjunto de operadores candidatos para selecionar o melhor operador em relação à lista de argumentos (x, y), e este operador torna-se o resultado do processo de resolução de sobrecarga. Se a resolução de sobrecarga não conseguir selecionar um único melhor operador, ocorrerá um erro de tempo de ligação.

12.4.6 Operadores candidatos definidos pelo utilizador

Dado um tipo T e uma operação operator «op»(A), em que «op» é um operador sobrecarregado e A é uma lista de argumentos, o conjunto de operadores candidatos definidos pelo utilizador fornecido pela T para o operador «op»(A) é determinado da seguinte forma:

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

12.4.7 Promoções numéricas

12.4.7.1 Generalidades

Esta 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 uma melhor conversão (§12.6.4.7); e ainda
  • 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 certas 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 da aplicação da 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 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 este 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, onde b é um byte e s é um short, a resolução de sobrecarga seleciona operator *(int, int) como o melhor operador. Assim, o efeito é que b e s são convertidos em int, e o tipo do resultado é int. Da mesma forma, para a operação i * d, onde i é um int e d é um double, overload resolução seleciona operator *(double, double) como o melhor operador. exemplo final

Fim do texto informativo.

12.4.7.2 Promoções numéricas unárias

Esta subcláusula é informativa.

A promoção numérica unária ocorre para os operandos dos operadores unários predefinidos +, -e ~. A promoção unária de valores numéricos consiste simplesmente em converter operandos do tipo sbyte, byte, short, ushortou char para tipo int. Além disso, para o operador unário, a promoção numérica unária converte operandos do tipo uint para o tipo long.

Fim do texto informativo.

12.4.7.3 Promoções numéricas binárias

Esta subcláusula é informativa.

A promoção numérica binária ocorre para os operandos dos operadores binários pré-definidos +, -, *, /, %, &, |, ^, ==, !=, >, <, >=e <=. A promoção numérica binária converte implicitamente ambos os 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 aparecem aqui:

  • Se um operando for do tipo decimal, o outro operando será convertido em tipo decimal, ou ocorrerá um erro de tempo de ligação se o outro operando for do tipo float ou double.
  • Caso contrário, se um operando for do tipo double, o outro operando será convertido em tipo double.
  • Caso contrário, se um operando for do tipo float, o outro operando será convertido em tipo float.
  • Caso contrário, se um operando for do tipo ulong, o outro operando será convertido em tipo ulong, ou ocorrerá um erro de tempo de ligação se o outro operando for de type sbyte, short, intou long.
  • Caso contrário, se um operando for do tipo long, o outro operando será convertido em tipo long.
  • Caso contrário, se um operando for do tipo uint e o outro operando for do tipo sbyte, shortou int, ambos os operandos serão convertidos para o tipo long.
  • Caso contrário, se um operando for do tipo uint, o outro operando será convertido em tipo uint.
  • Caso contrário, ambos os operandos serão convertidos ao tipo int.

Nota: A primeira regra não permite quaisquer operações que misturem o tipo decimal com os tipos double e float. A regra decorre do fato de que não há conversões implícitas entre o tipo decimal e os tipos double e float. nota final

Nota: Observe também que não é possível que um operando seja do tipo ulong quando o outro operando é de um tipo integral assinado. A razão é que não existe nenhum tipo integral que possa representar a gama completa de ulong, bem como os tipos integrais assinados. nota final

Em ambos os casos acima, uma expressão de transmissã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);

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

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

exemplo final

Fim do texto informativo.

12.4.8 Operadores de elevação

Operadores elevados permitem que os operadores predefinidos e definidos pelo utilizador que operam em tipos de valores não anuláveis também sejam usados com formas anuláveis desses tipos. Os operadores ascensorizados são construídos a partir de operadores predefinidos e definidos pelo utilizador que cumprem determinados requisitos, conforme descrito a seguir:

  • Para os operadores unários +, ++, -, --, !(negação lógica) e ~, existe uma forma levantada de um operador se os tipos de operando e resultado forem ambos tipos de valores não anuláveis. O formulário levantado é construído adicionando um único modificador de ? ao operando e aos tipos de resultado. O operador levantado produz um valor null se o operando estiver null. Caso contrário, o operador elevado desencapsula o operando, aplica o operador subjacente e encapsula o resultado.
  • Para os operadores binários +, -, *, /, %, &, |, ^, <<e >>, existe uma forma levantada de um operador se os tipos de operando e resultado forem todos tipos de valor não anuláveis. A forma elevada é construída ao adicionar um único modificador ? a cada operando e tipo de resultado. O operador elevado produz um valor null se um ou ambos os operandos forem null (uma exceção é para os operadores & e | do tipo bool?, conforme descrito no §12.13.5). Caso contrário, o operador levantado desembrulha os operandos, aplica o operador subjacente e envolve o resultado.
  • Para os operadores de igualdade == e !=, existe uma forma elevada de um operador se os tipos de operando forem ambos tipos de valor não anuláveis e se o tipo de resultado for bool. A forma levantada é construída ao adicionar um modificador único ? a cada tipo de operando. O operador levantado considera dois valores de null como iguais e um valor de null como desigual a qualquer valor que não sejanull. Se ambos os operandos não foremnull, o operador levantado desembrulha os operandos e aplica o operador subjacente para produzir o resultado bool.
  • Para os operadores relacionais <, >, <=e >=, existe uma forma levantada de um operador se os tipos de operando forem ambos tipos de valor não anuláveis e se o tipo de resultado for bool. A forma elevada é construída adicionando um único modificador ? a cada tipo de operando. O operador levantado produz o valor false se um ou ambos os operandos são null. Caso contrário, o operador elevado desembrulha os operandos e aplica o operador subjacente para obter o resultado bool.

12.5 Pesquisa de membros

12.5.1 Generalidades

Uma consulta de membro é o processo pelo qual se determina 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 ocorrerem como primary_expression de um invocation_expression (§12.8.10.2), diz-se que o membro é então invocado.

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

A pesquisa de membros 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 membros, métodos genéricos e tipos genéricos aninhados têm o número de parâmetros de tipo indicados em suas respetivas declarações e todos os outros membros têm parâmetros de tipo zero.

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

  • Primeiro, um conjunto de membros acessíveis chamado N é determinado:
    • Se T for um parâmetro de tipo, então o conjunto é a união dos conjuntos de membros acessíveis nomeados 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 nomeados N em object.
    • Caso contrário, o conjunto consiste em todos os membros acessíveis (§7.5) nomeados N em T, incluindo os membros herdados e os membros acessíveis nomeados N em object. Se T for um tipo construído, o conjunto de membros é obtido substituindo argumentos de tipo, conforme descrito no §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 incluem 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 com 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 invocáveis serão removidos do conjunto.
  • Em seguida, os membros que estão ocultos por outros membros são removidos do conjunto. Para cada membro S.M no conjunto, onde S é o tipo no qual o membro M é declarado, as seguintes regras são aplicadas:
    • Se M for uma constante, campo, propriedade, evento ou membro de enumeração, todos os membros declarados em um tipo base de S serão removidos do conjunto.
    • Se M for uma declaração de tipo, todos os não-tipos declarados em um tipo base de 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 de S serão removidos do conjunto.
  • Em seguida, os membros da interface que estão ocultos pelos membros da classe são removidos do conjunto. Esta etapa só tem efeito se T for um parâmetro de tipo e T tiver uma classe base efetiva diferente de object e um conjunto de interfaces efetivo não vazio (§15.2.5). Para cada membro S.M no conjunto, em que S é o tipo no qual o membro M é declarado, as seguintes regras sã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 não-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 declarada em uma declaração de interface serão removidos do conjunto.
  • Finalmente, tendo removido membros ocultos, o resultado da pesquisa é determinado:
    • Se o conjunto consiste em um único membro que não é um método, então esse membro é 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 é ambígua e ocorre um erro de tempo de ligação.

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

Nota: Esta fase só explica um tipo de ambiguidade. Se a pesquisa de membros resultar em um grupo de métodos, outros usos do grupo de métodos podem falhar devido a ambiguidade, por exemplo, conforme descrito no §12.6.4.1 e §12.6.6.2. nota final

12.5.2 Tipos de base

Para fins de pesquisa de membros, considera-se que um tipo T tem os seguintes tipos de base:

  • Se T é object ou dynamic, então T não tem nenhum tipo de base.
  • Se T for um enum_type, os tipos base de T são os tipos de classe System.Enum, System.ValueTypee object.
  • Se T for um struct_type, os tipos base de T são os tipos de classe System.ValueType e object.

    Nota: Um nullable_value_type é um struct_type (§8.3.1). nota final

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

12.6 Membros da função

12.6.1 Generalidades

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

  • Metodologia
  • 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 meio de invocações de membros da função. A sintaxe real para escrever uma invocação de membro de função depende da categoria de membro da 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 de variáveis para os parâmetros do membro da 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. Este processo é descrito no §12.6.3.

Invocações de métodos, indexadores, operadores e construtores de instância utilizam a resolução de sobrecarga para determinar qual membro de função de um conjunto candidato deve ser invocado. Este processo é descrito no §12.6.4.

Uma vez que um determinado membro de função tenha sido identificado no momento da ligação, possivelmente através da resolução de sobrecarga de funções, o processo real em tempo de execução de invocar o membro de função é descrito no §12.6.6.

Nota: A tabela a seguir resume o processamento que ocorre em construções envolvendo as seis categorias de membros da função que podem ser explicitamente invocados. Na tabela, e, x, ye 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.

Construir Exemplo Descrição
Invocação do método F(x, y) A resolução de sobrecarga é aplicada para selecionar o melhor método F na classe ou struct que a 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. Um erro de tempo de ligação ocorre se o método não estiver 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, struct ou interface dada pelo tipo de e. Um erro de tempo de ligação ocorre 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 acessor get da propriedade P na classe ou struct contenedora é invocado. Um erro em tempo de compilação ocorre se P for somente gravação. Se P não for static, a expressão de instância é this.
P = value O definidor da propriedade P na classe ou struct que a contém é chamado com a lista de argumentos (value). Ocorre um erro em tempo de compilação se o P for de apenas leitura. Se P não estiver static, a expressão de instância será this.
T.P O método de acesso get da propriedade P na classe ou estrutura T é invocado. Um erro em tempo de compilação ocorre se P não for static ou se P for somente escrita.
T.P = value O acessador de conjunto da propriedade P na classe ou struct T é invocado com a lista de argumentos (value). Um erro em tempo de compilação ocorre se P não for static ou se P for somente leitura.
e.P O acessor get da propriedade P na classe, struct ou interface determinada pelo tipo de E é invocado com a expressão de instância e. Um erro de tempo de vinculação ocorre se P for static ou se P for somente escrita.
e.P = value O acessor de definição da propriedade P na classe, estrutura ou interface especificada pelo tipo de E é invocado com a expressão de instância e e a lista de argumentos (value). Um erro de tempo de ligação ocorre se P for static ou se P for somente leitura.
Acesso ao evento E += value O acessador add do evento E na classe ou struct que contém é invocado. Se E não for static, a expressão de instância é this.
E -= value O método de remoção do evento E na classe ou estrutura que o contém é invocado. Se E não for static, a expressão de instância é this.
T.E += value O acessador add do evento E na classe ou struct T é invocado. Um erro de tempo de ligação ocorre se E não estiver static.
T.E -= value O método de remoção do evento E na classe ou estrutura T é invocado. Um erro de tempo de vinculação ocorre se E não for static.
e.E += value O acessador add do evento E na classe, struct ou interface dada pelo tipo de E é invocado com a expressão de instância e. Um erro de tempo de vinculação ocorre se E estiver static.
e.E -= value O acessor de remoção do evento E, na classe, struct ou interface do tipo dado por E, é invocado com a expressão de instância e. Um erro de tempo de vinculação ocorre se E estiver static.
Acesso ao indexador e[x, y] A resolução de sobrecarga é aplicada para selecionar o melhor indexador na classe, struct ou interface dada 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). Um erro de tempo de ligação ocorre se o indexador for apenas de escrita.
e[x, y] = value A resolução de sobrecarga é aplicada para selecionar o melhor indexador na classe, struct ou interface, conforme definido pelo tipo e. O acessador definido do indexador é invocado com a expressão de instância e e a lista de argumentos (x, y, value). Ocorre um erro de tempo de ligaçã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 dada 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 estruturas dadas 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 struct T. O construtor de instância é invocado com a lista de argumentos (x, y).

nota final

12.6.2 Listas de argumentos

12.6.2.1 Generalidades

Cada membro de função e invocação de delegado inclui uma lista de argumentos, que fornece valores reais ou referências de variáveis 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, construtores, métodos, indexadores e delegados, os argumentos são especificados como um argument_list, conforme descrito abaixo. Para indexadores, ao invocar o acessador definido, a lista de argumentos inclui adicionalmente a expressão especificada como o operando direito do operador de atribuição.

    Nota: Este argumento adicional não é usado para resolução de sobrecarga, apenas durante a invocação do acessador definido. nota final

  • 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 definido.
  • 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 dos operadores definidos pelo utilizador (§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âmetros (§15.6.2.4). Os parâmetros de saída e de referência não são suportados para essas categorias de membros da função.

Os argumentos de um construtor de instância, método, indexador ou invocação delegada são especificados como um 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
    ;

Um argument_list consiste em um ou mais argumentos s, separados por vírgulas. Cada argumento consiste em um argument_name opcional seguido por um argument_value. Um argumento com um argument_name é referido como um argumento nomeado, enquanto um argumento sem um 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 tal, conforme determinado por (§12.6.4.2 e descrito no §12.6.2.3.
  • A palavra-chave in seguida de um 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 de poder ser passada como parâmetro de entrada.
  • A palavra-chave ref seguida de um variable_reference (§9.5), indicando que o argumento é passado como parâmetro de referência (§15.6.2.3.3). Uma variável deve ser definitivamente atribuída (§9.4) antes de poder ser passada como parâmetro de referência.
  • A palavra-chave out seguida por um 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 formulário determina o modo de passagem de parâmetros do argumento: valor, de entrada, de referência ou de saída, respectivamente. No entanto, como mencionado acima, um argumento com modo de passagem de valor, pode ser transformado em um com modo de passagem de entrada.

Passar um campo volátil (§15.5.4) como um parâmetro de entrada, saída ou referência causa um aviso, uma vez que 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 da função ou delegado que está sendo invocado.

A lista de parâmetros utilizada a seguir é determinada da seguinte forma:

  • Para métodos virtuais e indexadores definidos em classes, a lista de parâmetros é selecionada a partir da primeira declaração ou substituição do membro de função encontrado ao começar com o tipo estático do recetor e pesquisar nas suas classes base.
  • Para métodos parciais, a lista de parâmetros da declaração de método parcial definidora é usada.
  • Para todos os outros membros da função e delegados, há apenas uma única lista de parâmetros, que é a 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 os argumentos dos membros da função são estabelecidos da seguinte forma:

  • Argumentos na lista de argumentos 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 da função seja invocado em sua forma expandida.
    • Um argumento posicional de um membro da 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 nomeado 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 value implícito da declaração de acessador set.
  • Para propriedades, ao invocar o método 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 de valor implícito da declaração de acessador set.
  • Para operadores unários definidos pelo usuário (incluindo conversões), o operando único corresponde ao parâmetro único da declaração do operador.
  • Para operadores binários definidos pelo usuário, o operando esquerdo corresponde ao primeiro parâmetro e o operando direito corresponde ao segundo parâmetro da declaração do operador.
  • Um argumento sem nome corresponde a nenhum parâmetro quando está após um argumento nomeado fora de posição ou um argumento nomeado que corresponde a uma matriz de parâmetros.

    Nota: Isso evita 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 chamado c está na terceira posição), portanto, os argumentos a seguir devem ser nomeados. Em outras palavras, argumentos nomeados que não são finais só são permitidos quando o nome e a posição resultam no encontro do mesmo parâmetro correspondente. nota final

12.6.2.3 Avaliação em tempo de execução de listas de argumentos

Durante o processamento em tempo de execução 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 forma:

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

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

    • caso contrário, o modo de passagem do parâmetro é de entrada. Se o argumento for uma referência de 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 torna-se o local de armazenamento representado pelo parâmetro na chamada do membro da função. Caso contrário, um local de armazenamento é criado com o mesmo tipo do parâmetro correspondente. A expressão do argumento é avaliada e uma conversão implícita (§10.2) para o tipo de parâmetro correspondente é realizada. O valor resultante é armazenado nesse local de armazenamento. Esse local de armazenamento é representado pelo parâmetro de entrada na chamada 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 chamada do método M1(i), i em si é passada como um argumento de entrada, porque é classificada como uma variável e tem o mesmo tipo int que o parâmetro de entrada. Na chamada de método M1(i + 5), uma variável int sem nome é criada, inicializada com o valor do argumento e, em seguida, passada como um argumento de entrada. Ver §12.6.4.2 e §12.6.4.4.

      exemplo final

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

Nota: esta verificação de tempo de execução é necessária devido à covariância da matriz (§17.6). nota final

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.

exemplo final

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

  • Quando um membro da função com uma matriz de parâmetros é invocado na sua forma normal, o argumento dado para a matriz de parâmetros deve ser uma única expressão que seja implicitamente convertível (§10.2) para o tipo de matriz de parâmetros. Neste caso, a matriz de parâmetros age precisamente como um parâmetro de valor.
  • Quando um membro da 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, onde cada argumento é uma expressão que é implicitamente conversível (§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 comprimento correspondente ao número de argumentos, inicializa os elementos da instância de 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

exemplo final

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.5) 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 seguintes invocações da forma expandida 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 });

exemplo final

Quando os argumentos são omitidos de um membro da 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.)

Nota: Como estes são sempre constantes, a sua avaliação não terá impacto na avaliação dos restantes argumentos. nota final

12.6.3 Inferência de tipo

12.6.3.1 Generalidades

Quando um método genérico é chamado sem especificar argumentos de tipo, um processo de inferência de tipo tenta inferir 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>
    }
}

Através da inferência de tipo, os argumentos de tipo int e string são determinados a partir dos argumentos para o método.

fim do exemplo

A inferência de tipo ocorre como parte do processamento em tempo de ligaçã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étodo específico é especificado em uma invocação de método e nenhum argumento de tipo é especificado como parte da chamada de 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 a 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 inferido serão usados como os argumentos de tipo para a invocação. Se a inferência de tipo para um determinado método falhar, esse método não participará da resolução de sobrecarga. A falha de inferência de tipo, por si só, não causa um erro de tempo de ligação. No entanto, muitas vezes leva a um erro de tempo de vinculação quando a resolução de sobrecarga não consegue encontrar quaisquer métodos aplicáveis.

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

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

Com uma chamada de método da forma M(E₁ ...Eₓ), a tarefa de inferência de tipo é encontrar argumentos de tipo exclusivos S₁...Sᵥ para cada um dos parâmetros de tipo X₁...Xᵥ de modo 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 conforme pode ser implementado usando 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ᵢ é fixado para um tipo específico Sᵢ ou não fixado com um conjunto de limites associado. Cada um dos limites é algum tipo T. Inicialmente, cada variável de tipo Xᵢ não é 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 fixa variáveis de tipo para tipos específicos e infere outros limites. A segunda fase pode ter de ser repetida várias vezes.

Nota: A inferência de tipo também é usada em outros contextos, incluindo para conversão de grupos de métodos (§12.6.3.14) e encontrar o melhor tipo comum de um conjunto de expressões (§12.6.3.15). nota final

12.6.3.2 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ícita (§12.6.3.8) é 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), então uma inferência de limite inferior (§12.6.3.10) é 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), ou parâmetro de saída (§15.6.2.3.4), então uma inferência exata (§12.6.3.9) é feita deUparaTᵢ.
  • Caso contrário, se Eᵢ tem um tipo U e o parâmetro correspondente é um parâmetro de entrada (§15.6.2.3.2) e Eᵢ é um argumento de entrada, então uma inferência exata (§12.6.3.9) é 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), então uma inferência de limite inferior (§12.6.3.10) é feita deUparaTᵢ.
  • Caso contrário, não é feita qualquer inferência para este argumento.

12.6.3.3 Segunda fase

A segunda fase prossegue do seguinte modo:

  • Todas as variáveis de tipo não fixas Xᵢ que não dependem de (§12.6.3.6) nada Xₑ são fixas (§12.6.3.12).
  • Se não existirem tais variáveis de tipo, todas variáveis de tipo não fixas Xᵢserão fixas para as quais todas as seguintes opções se mantêm:
    • Há pelo menos uma variável de tipo Xₑ da qual dependeXᵢ
    • 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 existirem mais variáveis de tipo não fixadas, a inferência de tipos é 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 de não fixas Xₑ mas os tipos de entrada (§12.6.3.4) não contêm, 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 digitada implicitamente e T for um tipo de delegado ou tipo de árvore de expressão, todos os tipos de parâmetros de T são tipos de entrada deEcom o 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 de delegado ou tipo de árvore de expressão, o tipo de retorno de T é um tipo de saída deEcom o tipoT.

12.6.3.6 Dependência

Uma variável de tipo não fixa depende diretamente de uma variável de tipo não fixa se, para algum argumento, com o tipo ocorre em um tipo de entrada de com o tipo e ocorre em um tipo de saída de com o tipo .

Xₑ depende deXᵢ se Xₑdepende diretamente deXᵢ ou se Xᵢdepende diretamente deXᵥ e Xᵥdepende 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 de inferência de tipo de saída é feita de uma expressão para um tipo T da seguinte maneira:

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

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

Um de inferência de tipo de parâmetro explícito é feito de uma Ede expressão para um tipo T da seguinte maneira:

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

12.6.3.9 Inferências exatas

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

  • Se V for um dos Xᵢ não fixos, U será adicionado ao conjunto de limites exatos para Xᵢ.
  • Caso contrário, os conjuntos V₁...Vₑ e U₁...Uₑ são determinados verificando 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 construído C<V₁...Vₑ> e U é um tipo construído C<U₁...Uₑ>
      Se algum desses casos se aplicar, então é feita uma inferência exata de cada Uᵢ para o Vᵢcorrespondente.
  • Caso contrário, não são feitas inferências.

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 um dos Xᵢ não fixos, U será adicionado ao conjunto de limites inferiores para Xᵢ.
  • Caso contrário, se V é o tipo V₁? e U é o tipo U₁? então uma inferência de limite inferior é feita de U₁ para V₁.
  • Caso contrário, os conjuntos U₁...Uₑ e V₁...Vₑ são determinados verificando 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 dos IEnumerable<V₁>, ICollection<V₁>, IReadOnlyList<V₁>>, IReadOnlyCollection<V₁> ou IList<V₁> e U é um tipo de matriz unidimensional U₁[]
    • V é um classconstruído, struct, interface ou delegate tipo C<V₁...Vₑ> e existe um tipo único C<U₁...Uₑ> tal que U (ou, se U for um tipo parameter, sua classe base efetiva ou qualquer membro de seu conjunto de interface efetivo) é idêntico a, inherits de (direta ou indiretamente), ou implementa (direta ou indiretamente) C<U₁...Uₑ>.
    • (A restrição de "unicidade" significa que, no caso da interface C<T>{} class U: C<X>, C<Y>{}, então nenhuma inferência é feita ao inferir de U para C<T> porque U₁ pode ser X ou Y.)
      Se qualquer um destes casos se aplicar, então é feita uma inferência de cada Uᵢ para o Vᵢ correspondente da seguinte forma:
    • Se Uᵢ não é conhecido por ser um tipo de referência, então uma inferência exata é feita
    • 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ₑ> então a inferência depende do parâmetro de tipo i-th de C:
      • Se for covariante, então é feita uma inferência de limite inferior .
      • Se for contravariante, então é feita uma inferência de limite superior.
      • Se for invariante, então é feita uma inferência exata.
  • Caso contrário, não são feitas inferências.

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 um dos Xᵢ não fixos, então U é adicionado ao conjunto de limites superiores para Xᵢ.
  • Caso contrário, os conjuntos V₁...Vₑ e U₁...Uₑ são determinados verificando 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 dos 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 é uma classe, struct, interface ou tipo delegado construído C<U₁...Uₑ> e V é um tipo class, struct, interface ou delegate que é identical a, inherits de (diretamente ou indiretamente), ou implementa (diretamente ou indiretamente) um tipo único C<V₁...Vₑ>
    • (A restrição de "unicidade" significa que, dada uma interface C<T>{} class V<Z>: C<X<Z>>, C<Y<Z>>{}, então nenhuma inferência é feita ao inferir de C<U₁> para V<Q>. As inferências não são feitas de U₁ para X<Q> ou Y<Q>.)
      Se qualquer um destes casos se aplicar, então é feita uma inferência de cada Uᵢ para o Vᵢ correspondente da seguinte forma:
    • Se Uᵢ não é conhecido por ser um tipo de referência, então uma inferência exata é feita
    • Caso contrário, se V for um tipo de matriz, será feita uma de inferência de limite superior
    • Caso contrário, se U for C<U₁...Uₑ> então a inferência depende do parâmetro de tipo i-th de C:
      • Se for covariante, então é feita uma inferência de limite superior .
      • Se for contravariante, então é feita uma inferência de limite inferior .
      • Se for invariante, então é feita uma inferência exata.
  • Caso contrário, não são feitas inferências.

12.6.3.12 Fixação

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

  • O conjunto de tipos candidatosUₑ começa como o conjunto de todos os tipos no conjunto de limites para Xᵢ.
  • Cada limite para Xᵢ é examinado sucessivamente: 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 candidato. Para cada limite inferior U de Xᵢ, todos os tipos Uₑ para os quais não há uma conversão implícita de U são removidos do conjunto candidato. Para cada limite superior U de Xᵢ, todos os tipos Uₑ para os quais não existe uma conversão implícita para U devido a e são removidos do conjunto candidato.
  • Se entre os tipos candidatos restantes Uₑ houver um tipo único V para o qual há uma conversão implícita de todos os outros tipos candidatos, então Xᵢ é fixado 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 tipo e resolução de sobrecarga. O tipo de retorno inferido só pode ser determinado para uma função anônima onde todos os tipos de parâmetros são conhecidos, seja porque eles são explicitamente fornecidos, fornecidos através 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 anexa.

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

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

O tipo de retorno inferido é determinado da seguinte forma:

  • Se F é assíncrono e o corpo de F é uma expressão classificada como nula (§12.2), ou um bloco onde nenhuma instrução return contém expressões, o tipo de retorno inferido é «TaskType» (§15.15.1).
  • Se F for assíncrono e tiver um tipo de retorno efetivo inferido T, o tipo de retorno inferido será «TaskType»<T>»(§15.15.1).
  • Se F não for 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 pode ser inferido para F.

Exemplo: Como um 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 foi importado com uma diretiva using namespace e dado uma classe Customer com uma propriedade Name do tipo string, o método Select pode ser usado para selecionar os nomes em 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) do Select é processada reescrevendo 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 customers está relacionado ao parâmetro source, inferindo TSource ser Customer. Em seguida, usando o processo de inferência de tipo de função anônima descrito acima, c é dado tipo Customer, e a expressão c.Name está relacionada ao tipo de retorno do parâmetro seletor, inferindo TResult ser string. Assim, a invocação equivale 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. Dado 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 forma: Primeiro, o argumento "1:15:30" está relacionado ao parâmetro value, inferindo X ser string. Em seguida, o parâmetro da primeira função anônima, s, é dado o tipo inferido string, e a expressão TimeSpan.Parse(s) está relacionada ao tipo de retorno de f1, inferindo Y ser System.TimeSpan. Finalmente, o parâmetro da segunda função anônima, t, é dado o tipo inferido System.TimeSpan, e a expressão t.TotalHours está relacionada com o tipo de retorno de f2, inferindo Z ser double. Assim, o resultado da invocação é do tipo double.

exemplo final

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

À semelhança das 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 num 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étodo genérico, neste caso, existem apenas tipos de argumentos , não existem expressões de argumentos . Em particular, não há funções anónimas e, portanto, não há necessidade de múltiplas fases de inferência.

Em vez disso, todos os Xᵢ são considerados não fixo, e uma de 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 algum dos Xᵢ nenhum limite for encontrado, a inferência de tipo falhará. Caso contrário, todos os Xᵢ são fixos a Sᵢcorrespondentes, que são o resultado da inferência de tipo.

12.6.3.15 Encontrar 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. Particularmente, os tipos de elementos de matrizes implicitamente tipadas e os tipos de retorno de funções anónimas com blocos de corpo são determinados desta forma.

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

  • É introduzida uma nova variável de tipo não fixaX.
  • Para cada expressão Ei, uma inferência do tipo de saída (§12.6.3.7) é realizada a partir dela até X.
  • X é fixo (§12.6.3.12), se possível, e o tipo resultante é o melhor tipo comum.
  • Caso contrário, a inferência falha.

Nota: Intuitivamente esta inferência é equivalente a chamar um método void M<X>(X x₁ ... X xᵥ) com o Eᵢ como argumentos e inferir X. nota final

12.6.4 Resolução de sobrecarga

12.6.4.1 Generalidades

A resolução de sobrecarga é um mecanismo de tempo vinculativo para selecionar o melhor membro da função a ser invocado, dada uma lista de argumentos e um conjunto de membros da função candidata. 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 numa invocation_expression (§12.8.10).
  • Invocação de um construtor de instância nomeado em object_creation_expression (§12.8.17.2).
  • Invocação de um acessor do indexador através de um element_access (§12.8.12).
  • Invocação de um operador predefinido ou definido pelo utilizador referenciado numa 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 de sua própria maneira. 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 os 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).

Uma vez identificados os membros da função candidata e a lista de argumentos, a seleção do melhor membro da função é a mesma em todos os casos:

  • Em primeiro lugar, o conjunto de membros da função candidata é reduzido aos membros da função que são aplicáveis em relação à lista de argumentos dada (§12.6.4.2). Se esse conjunto reduzido estiver vazio, ocorrerá um erro em tempo de compilação.
  • Em seguida, o melhor membro da função do conjunto de candidatos aplicáveis é localizado. Se o conjunto contiver apenas um membro da 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 da função que é melhor do que todos os outros membros da função em relação à lista de argumentos dada, desde que cada membro da função seja comparado com todos os outros membros da função usando as regras §12.6.4.3. Se não houver exatamente um membro da função que seja superior a todos os outros membros da função, então a invocação do membro da função é ambígua e ocorre um erro em tempo de ligação.

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

12.6.4.2 Membro da função aplicável

Diz-se que um membro de função é considerado um membro de função aplicável em relação a uma lista de argumentos A quando todas as condições a seguir são verdadeiras:

  • Cada argumento em A corresponde a um parâmetro na declaração do membro da função conforme descrito no §12.6.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âmetros do argumento é idêntico ao modo de passagem de parâmetros 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 do 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 do argumento para o tipo do parâmetro correspondente.

Para um membro da função que inclui uma matriz de parâmetros, se o membro da função é aplicável pelas regras acima, diz-se que ele é aplicável em sua forma normal. Se um membro da 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 sua forma expandida:

  • A forma expandida é construída substituindo a matriz de parâmetros na declaração de membro da função por zero ou mais parâmetros de valor 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 pode ser construída e, portanto, não é aplicável.
  • Caso contrário, a forma expandida será aplicável se, para cada argumento em A, uma das seguintes opções for verdadeira:
    • o modo de passagem de parâmetros do argumento é idêntico ao modo de passagem de parâmetros 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 do argumento para o tipo do parâmetro correspondente, ou
      • Para um parâmetro by-reference, o tipo da expressão de argumento é idêntico ao tipo do parâmetro correspondente.
    • O modo de passagem de parâmetros do argumento é valor, e o modo de passagem de parâmetros do parâmetro correspondente é entrada, e uma conversão implícita (§10.2) existe da expressão do 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ó é aplicável se o grupo de métodos resultar de um simple_name ou de um member_access através de um tipo.
  • Um método de instância só é aplicável se o grupo de métodos resultar de um simple_name, um member_access através de uma variável ou valor, ou um base_access.
    • Se o grupo de métodos resultar de um simple_name, um método de instância só será aplicável se this acesso for permitido §12.8.14.
  • Quando o conjunto de métodos resulta de um member_access que pode ocorrer através de uma instância ou de um tipo, como descrito no §12.8.7.2, tanto os métodos de instância como os estáticos são aplicáveis.
  • Um método genérico cujos argumentos de tipo (explicitamente especificados ou inferidos) nem todos satisfaçam suas restrições não é aplicável.
  • No contexto de uma conversão de grupo de métodos, deve existir 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 do delegado. Caso contrário, o método candidato não é aplicável.

12.6.4.3 Membro de função melhorado

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

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

  • A forma expandida é usada se o membro da função for aplicável apenas na forma expandida.
  • Os parâmetros opcionais sem argumentos correspondentes são removidos da lista de parâmetros
  • Os 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âmetros {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, a fim de determinar o melhor membro da função.

  • Se Mᵢ é um método não genérico e Mₑ é um método genérico, então Mᵢ é melhor do que Mₑ.
  • Caso contrário, se Mᵢ é aplicável em sua forma normal e Mₑ tem uma matriz de parâmetros e é aplicável apenas em sua forma expandida, então Mᵢ é melhor do que Mₑ.
  • Caso contrário, se ambos os métodos tiverem matrizes de parâmetros e forem aplicáveis apenas em suas formas expandidas, e se a matriz de params de Mᵢ tiver menos elementos do que a matriz de params de Mₑ, então Mᵢ é melhor do que Mₑ.
  • Caso contrário, se Mᵥ tiver mais tipos de parâmetros específicos do que Mₓ, então Mᵥ é melhor do que Mₓ. Deixe que {R1, R2, ..., Rn} e {S1, S2, ..., Sn} representem os tipos de parâmetros não instanciados e não expandidos de Mᵥ e Mₓ. Mᵥtipos de parâmetros são mais específicos do que Mₓs se, para cada parâmetro, Rx não for menos específico do que Sxe, pelo menos para um parâmetro, Rx for mais específico do que Sx:
    • Um parâmetro type é menos específico do que um parâmetro non-type.
    • 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 é mais específico e nenhum argumento de tipo é 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 levantado e o outro for um operador levantado, o não levantado é melhor.
  • Se nenhum membro da função foi encontrado para ser melhor, e todos os parâmetros de Mᵥ têm um argumento correspondente, enquanto os argumentos padrão precisam ser substituídos por pelo menos um parâmetro opcional em Mₓ, então Mᵥ é melhor do que Mₓ.
  • Se para pelo menos um parâmetro Mᵥ usar a melhor opção de passagem de parâmetros (§12.6.4.4) do que o parâmetro correspondente em Mₓ e nenhum dos parâmetros em Mₓ usar a melhor opção de passagem de parâmetros do que Mᵥ, Mᵥ é melhor do que Mₓ.
  • Caso contrário, nenhum membro da função é melhor.

12.6.4.4 Melhor modo de passagem de parâmetros

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

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

Dado int i = 10;, de acordo com §12.6.4.2, as chamadas M1(i) e M1(i + 5) resultam na aplicação de ambas as sobrecargas. Nesses casos, o método com o modo de passagem de parâmetros por valor é a melhor opção de modo de passagem de parâmetros.

Nota: Não existe essa escolha para argumentos dos modos de entrada, saída ou passagem de referência, pois esses argumentos correspondem exatamente aos mesmos modos de passagem de parâmetros. nota final

12.6.4.5 Melhor conversão da expressão

Dada uma conversão implícita C₁ que converte uma expressão E para um tipo T₁, e uma conversão implícita C₂ que converte uma expressão E para um tipo T₂, C₁ é uma melhor conversão do que C₂ se uma das seguintes opções se mantiver:

  • E corresponde exatamente a T₁ e E não corresponde exatamente a T₂ (§12.6.4.6)
  • E corresponde exatamente a ambos ou a nenhum dos T₁ e T₂, e T₁ é um alvo 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 único do grupo de métodos para conversão C₁, e T₂ não é compatível com o melhor método único do grupo de métodos para conversão C₂

12.6.4.6 Expressão exatamente correspondente

Dada uma expressão E e um tipo T, Ecorresponde exatamenteT se uma das seguintes opções se mantiver:

  • E tem um tipo Se existe uma conversão de identidade de S para T
  • E é uma função anónima, T é um tipo de delegado D ou um tipo de árvore de expressões Expression<D> e 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 de retorno, e D tem um tipo de retorno que é um «TaskType» não genérico
    • Ou E não é assíncrona e D tem um tipo de retorno Y ou E é assíncrona e D tem um tipo de retorno «TaskType»<Y>(§15.15.1), e uma das seguintes condições:
      • O corpo de E é uma expressão que corresponde exatamente a Y
      • O corpo de E é um bloco onde cada instrução return retorna uma expressão que corresponde exatamente a Y

12.6.4.7 Melhor objetivo de conversão

Dado dois tipos e , é um de conversão melhor do que se uma das seguintes opções se mantiver:

  • Existe uma conversão implícita de T₁ para T₂ e não existe conversão implícita de T₂ para T₁
  • T₁ é «TaskType»<S₁>(§15.15.1), T₂ é «TaskType»<S₂>e S₁ é um alvo de conversão melhor do que S₂
  • T₁ é «TaskType»<S₁>(§15.15.1), T₂ é «TaskType»<S₂>, e T₁ é mais especializado do que T₂
  • T₁ é S₁ ou S₁? onde S₁ é um tipo integral assinado, e T₂ é S₂ ou S₂? onde S₂ é um tipo integral não assinado. Mais especificamente:
    • S₁ é sbyte e S₂ é byte, ushort, uintou ulong
    • S₁ é short e S₂ é ushort, uintou ulong
    • S₁ é int e S₂ é uint, ou ulong
    • S₁ é long e S₂ é ulong

12.6.4.8 Sobrecarga nas classes genéricas

Nota: Embora as assinaturas declaradas devam ser únicas (§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 mais específica (§12.6.4.3) das assinaturas originais (antes da substituição de argumentos de tipo), caso exista; caso contrário, reportará um erro. nota final

Exemplo: Os exemplos a seguir mostram sobrecargas que são 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);
}

exemplo final

12.6.5 Verificação em tempo de compilação da invocação dinâmica de membros

Embora a resolução de sobrecarga de uma operação dinamicamente vinculada ocorra em tempo de execução, às vezes é possível em tempo de compilação saber a lista de membros da função a partir da qual uma sobrecarga será escolhida:

  • Para uma invocação delegada (§12.8.10.4), a lista é um único membro de função 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 indexador (§12.8.12.3), o conjunto de indexadores acessíveis no recetor é conhecido no momento da compilação.

Nesses casos, é realizada uma verificação limitada em tempo de compilação para cada membro no conjunto conhecido de membros da função, para determinar de forma certeira que nunca será invocado durante a execução. Para cada membro da função F uma lista de parâmetros e argumentos modificados são construídos:

  • Primeiro, se F é um método genérico e argumentos de tipo foram fornecidos, então esses são substituídos para os parâmetros de tipo na lista de parâmetros. No entanto, se os argumentos de tipo não foram fornecidos, essa substituição não acontece.
  • Em seguida, qualquer parâmetro cujo tipo esteja aberto (ou seja, contenha um parâmetro de tipo; ver §8.4.3) é elidido, juntamente com o(s) seu(s) parâmetro(s) correspondente(s).

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

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

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

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

12.6.6.1 Generalidades

Esta subcláusula descreve o processo que ocorre em tempo de execução para invocar um membro de função específico. Presume-se que um processo de tempo vinculativo já determinou o membro específico a invocar, possivelmente aplicando a resolução de sobrecarga a um conjunto de membros de funções candidatos.

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

  • Membros da função estática. Estes são métodos estáticos, acessadores de propriedade estática e operadores definidos pelo usuário. Os membros da função estática são sempre não virtuais.
  • Membros da 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 de função de instância são não virtuais ou virtuais e são sempre invocados numa instância específica. A instância é calculada 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 da instância é considerada como o objeto recém-alocado.

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

  • Se M for um membro da função estática:

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

    • E é avaliada. 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. Neste caso, E é classificada como uma variável.
    • Se E não for classificada como uma variável, ou se V não for um tipo struct somente leitura (§16.2.2), e E for um dos seguintes:
      • um parâmetro de entrada (§15.6.2.3.2), ou
      • um campo readonly (§15.5.3), ou
      • uma readonly variável de referência ou retorno (§9.7),

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

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

    • E é avaliada. Se essa avaliação causar uma exceção, nenhuma etapa adicional será executada.
    • A lista de argumentos é avaliada conforme descrito no §12.6.2.
    • Se o tipo de E for um value_type, uma conversão boxing (§10.2.9) é realizada para converter E em uma class_type, e E é considerado desse class_type nos passos seguintes. Se o value_type é um enum_type, o class_type é System.Enum; caso contrário, é System.ValueType.
    • O valor de E é verificado como válido. Se o valor de E for nulo, um System.NullReferenceException será lançado e nenhuma outra etapa será executada.
    • A implementação do membro da função a ser invocada é determinada:
      • Se o tipo de tempo de ligação de E for uma interface, o membro de função a invocar será a implementação de M fornecida pelo tipo em tempo de execução da instância referenciada por E. Este membro funcional é definido pela aplicação das 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 fornecido pelo tipo de tempo de execução da instância referenciada por E. Este membro da função é determinado pela aplicação das regras para determinar a implementação mais derivada (§15.6.4) de M em relação ao tipo de tempo de execução da instância referenciada por E.
      • Caso contrário, M é um membro de função não virtual e o membro da função que deve ser invocado é M ele mesmo.
    • A implementação da função membro determinada na etapa acima é invocada. O objeto referenciado por E torna-se o objeto referenciado por este.

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, se houver, devolvido a partir do corpo da função (§13.10.5).

12.6.6.2 Invocações em instâncias encapsuladas

Um membro de função implementado em um value_type pode ser invocado por meio de uma instância em caixa 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 através de uma expressão de instância desse class_type.

    Nota: O class_type será sempre um dos System.Object, System.ValueType ou System.Enumnota final

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

Nessas situações, considera-se que a instância em caixa contém uma variável do value_type, e essa variável se torna a variável referenciada por isso dentro da chamada de membro da função.

Nota: Em particular, isto significa que, quando um membro de função é invocado em uma instância em caixa, é possível que o membro de função modifique o valor contido na instância em caixa. nota final

12.7 Desconstrução

A desconstrução é um processo pelo qual uma expressão se transforma numa tupla de expressões individuais. A desconstrução é utilizada quando o objetivo de uma atribuição simples é expressado como uma tupla, para obter valores a serem atribuídos a cada elemento dessa tupla.

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

  • Se E é uma expressão de tupla com n elementos, o resultado da decomposição é a própria expressão E.
  • Caso contrário, se E tem um tipo de tupla (T1, ..., Tn) com elementos n, então E é avaliada em uma variável temporária __v, e o resultado da desconstrução é 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). Tal método é referido como um desconstrutor de .
  • Caso contrário, E não pode ser desconstruída.

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

Nota: Uma expressão do tipo dynamic não pode ser desconstruída. nota final

12.8 Expressões primárias

12.8.1 Generalidades

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

primary_expression
    : primary_no_array_creation_expression
    | array_creation_expression
    ;

primary_no_array_creation_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
    | 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
    ;

Nota: Estas regras gramaticais não estão preparadas para ANTLR, pois fazem parte de um conjunto de regras recursivas mutuamente à esquerda (primary_expression, primary_no_array_creation_expression, member_access, invocation_expression, element_access, post_increment_expression, post_decrement_expression, null_forgiving_expression, pointer_member_access e pointer_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. Isso não foi feito, pois nem todas as estratégias de análise o exigem (por exemplo, um analisador LALR não o faria) e isso ofuscaria a estrutura e a descrição. nota final

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).

As expressões primárias são divididas entre array_creation_expressions e primary_no_array_creation_expressions. Tratar array_creation_expression dessa maneira, em vez de listá-lo junto com as outras formas de expressão simples, permite que a gramática não permita códigos potencialmente confusos, como

object o = new int[3][1];

que, de outro modo, seria interpretado como

object o = (new int[3])[1];

12.8.2 Literais

Um primary_expression que consiste em um literal (§6.4.5) é considerado um valor.

12.8.3 Expressões de cadeia interpoladas

Um interpolated_string_expression consiste em $, $@ou @$, imediatamente seguido por texto entre os caracteres ". Dentro do texto citado há zero ou mais interpolações delimitadas por { e } caracteres, cada uma das quais inclui uma expressão e especificações de formatação opcionais.

As expressões de cadeia de caracteres interpoladas têm duas formas; regular (interpolated_regular_string_expression) e literal (interpolated_verbatim_string_expression); que são lexicamente semelhantes, mas diferem semanticamente das duas formas de literais de cadeia (§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 lexicais acima definidas são sensíveis ao contexto como se segue:

Regra Requisitos contextuais
Interpolated_Regular_String_Mid Reconhecido apenas após um Interpolated_Regular_String_Start, entre quaisquer interpolações seguintes, e antes do Interpolated_Regular_String_Endcorrespondente.
Formato_de_Interpolação_Regular Reconhecido apenas dentro de um regular_interpolation e quando os dois pontos iniciais (:) não estão aninhados dentro de qualquer tipo de parêntesis (parênteses/chaves/quadrado).
Interpolated_Regular_String_End Apenas reconhecidos após uma Interpolated_Regular_String_Start e apenas se quaisquer tokens intervenientes forem Interpolated_Regular_String_Mids ou tokens que possam fazer parte de regular_interpolations, incluindo tokens para quaisquer interpolated_regular_string_expressions contidos nessas interpolações.
Interpolated_Verbatim_String_MidVerbatim_Interpolation_FormatInterpolated_Verbatim_String_End O reconhecimento destas três regras segue o das regras correspondentes acima, com cada regra gramatical regular mencionada sendo substituída pela correspondente regra literal .

Nota: As regras acima são sensíveis ao contexto, pois suas definições se sobrepõem às de outros tokens no idioma. nota final

Nota: A gramática acima não está preparada para ANTLR devido às regras lexicais sensíveis ao contexto. Tal como acontece com outros geradores lexer, o ANTLR suporta regras lexicais sensíveis ao contexto, por exemplo, usando seus modos lexicais , mas este é um detalhe de implementação e, portanto, não faz parte desta especificação. nota final

Um interpolated_string_expression é classificado como um valor. Se for imediatamente convertida 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 interpolada tem esse tipo. Caso contrário, tem o tipo string.

Nota: As diferenças entre os tipos possíveis de um interpolated_string_expression podem ser determinadas a partir da documentação para System.String (§C.2) e System.FormattableString (§C.3). nota final

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

Nota: A forma como o formato predefinido de um tipo é determinado é detalhada na documentação relativa a System.String (§C.2) e System.FormattableString (§C.3). Descrições de formatos padrão, que são idênticos para Regular_Interpolation_Format e Verbatim_Interpolation_Format, podem ser encontradas na documentação para System.IFormattable (§C.4) e em outros tipos na biblioteca padrão (§C). nota final

Em interpolation_minimum_width, o constant_expression terá uma conversão implícita para int. Deixe a largura do campo ser 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 comprimento da cadeia de caracteres formatada, a cadeia 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 será alinhada à direita precedendo o preenchimento,
    • Caso contrário, é alinhado à esquerda com a adição do preenchimento.

O significado geral de um interpolated_string_expression, incluindo a formatação e preenchimento de interpolações acima, é 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 devolve um valor do tipo string.

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

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

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

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

Quando um interpolated_string_expression contém múltiplas 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 hexadecimais maiúsculos,
  • o formato padrão para um valor string é o próprio valor,
  • valores de alinhamento positivo que justifiquem à direita dentro da largura mínima de campo especificada,
  • valores de alinhamento negativos que justificam à esquerda dentro da largura mínima de campo especificada,
  • constantes definidas de interpolation_minimum_width, e
  • que {{ e }} são formatados como { e } respectivamente.

Dado:

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

Em seguida:

Expressão de cadeia de caracteres interpolada significado equivalente como 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"

exemplo final

12.8.4 Nomes simples

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

simple_name
    : identifier type_argument_list?
    ;

Um simple_name é da forma I ou da forma I<A₁, ..., Aₑ>, onde I é um identificador único e I<A₁, ..., Aₑ> é uma lista_de_argumentos_de_tipoopcional. Quando nenhuma type_argument_list for especificada, considere e como zero. O simple_name é avaliado e classificado da seguinte forma:

  • Se e é zero e o simple_name aparece dentro de um espaço de declaração de variável local (§7.3) que contém diretamente uma variável, parâmetro ou constante local com nome I, então o simple_name refere-se a essa variável, parâmetro ou constante local e é classificado como uma variável ou valor.
  • Se e for zero e o simple_name aparecer dentro de uma declaração de método genérico, mas fora dos atributos da sua declaração_de_método, e se essa declaração incluir um parâmetro de tipo com nome I, então o simple_name se refere 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 envolvente 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 type com nome I, então o simple_name refere-se a esse parâmetro type.
    • Caso contrário, se uma pesquisa de membro (§12.5) de I em T com e argumentos de tipo produz uma correspondência:
      • Se T for o tipo de instância da classe ou tipo struct imediatamente circundante 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 de this. Se uma lista de argumentos de tipo foi especificada, ela é usada para chamar um método genérico (§12.8.10.2).
      • Caso contrário, se T for o tipo de instância da classe ou tipo struct imediatamente abrangente, se a pesquisa identificar um membro de instância e se 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 é o mesmo que um acesso a um membro (§12.8.7) do formulário this.I. Isso só pode acontecer quando e é zero.
      • Caso contrário, o resultado é 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 de inclusão (se houver) e terminando com o namespace global, as etapas a seguir são avaliadas até que uma entidade seja localizada:
    • Se e for zero e I for o nome de um namespace no N, então:
      • Se o local onde o simple_name ocorre estiver dentro de uma declaração de espaço de nomes para N e a declaração de espaço de nomes contiver uma diretiva extern_alias_directive ou uma diretiva using_alias_directive que associe o nome I a um espaço de nomes ou tipo, então o simple_name torna-se ambíguo e ocorre um erro em tempo de compilação.
      • Caso contrário, o 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 ocorre estiver dentro de 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, então o simple_name será ambíguo e ocorrerá um erro durante a compilação.
      • Caso contrário, o namespace_or_type_name refere-se ao tipo construído com os argumentos de tipo fornecidos.
    • Caso contrário, se o local onde o simple_name ocorre estiver delimitado por uma declaração de namespace para N:
      • Se e for zero 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 importado, o simple_name se refere 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 com parâmetros name I e e type, o simple_name se refere 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 fica ambíguo, resultando num erro de compilação.

    Nota: Toda esta etapa é exatamente paralela à etapa correspondente no processamento de um namespace_or_type_name (§7.8). nota final

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

12.8.5 Expressões entre parênteses

Um parenthesized_expression consiste em uma expressão entre parênteses.

parenthesized_expression
    : '(' expression ')'
    ;

Uma expressão entre parênteses é avaliada ao se avaliar a expressão dentro dos parênteses. Se a expressão entre parênteses indicar um namespace ou tipo, ocorrerá um erro em 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 tuplas

Um tuple_expression representa uma tupla e consiste em duas ou mais expressões separadas por vírgulas e com nome opcionals entre parênteses. Um deconstruction_expression é uma sintaxe abreviada para uma tupla contendo expressões de declaração digitadas implicitamente.

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
    ;

Um tuple_expression é classificado como uma tupla.

Um deconstruction_expressionvar (e1, ..., en) é uma forma abreviada do tuple_expression(var e1, ..., var en) e segue o mesmo comportamento. Isso aplica-se recursivamente a qualquer deconstruction_tupleaninhado no deconstruction_expression. Cada identificador aninhado dentro de um deconstruction_expression introduz assim 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 tem um tipo se e só se cada um dos seus elementos Ei tem um tipo Ti. O tipo deve ser um tipo de tupla da mesma aridade que a expressão da tupla, em que cada elemento é dado pelo seguinte:

  • Se o elemento da tupla na posição correspondente tiver um nome Ni, então o elemento do tipo de tupla deve ser Ti Ni.
  • Caso contrário, se Ei for da forma Ni ou E.Ni ou E?.Ni, o elemento do tipo de tupla deve ser Ti Ni, a menos que qualquer uma das seguintes condições se aplique:
    • Outro elemento da expressão de tupla tem o nome Ni, ou
    • Outro elemento da tupla sem nome possui uma expressão da forma Ni ou E.Ni ou E?.Ni, ou
    • Ni é da forma ItemX, onde X é uma sequência de dígitos decimais não iniciados por0que poderiam representar a posição de um elemento da tupla, e X não representa a posição de um elemento.
  • Caso contrário, o elemento do tipo de tupla deve ser Ti.

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

Um valor de tupla pode ser obtido a partir 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 tornando-a alvo 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, todas as quatro expressões de tupla são válidas. Os dois primeiros, t1 e t2, não usam o tipo de expressão de tupla, mas aplicam uma conversão de tupla implícita. No caso de t2, a conversão implícita da tupla baseia-se nas 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 seu segundo elemento não tem tipo.

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

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

exemplo final

12.8.7 Acesso dos membros

12.8.7.1 Generalidades

Um member_access consiste de um primary_expression, um predefined_typeou um qualified_alias_member, seguido por um token “.”, seguido por um identificador , opcionalmente seguido por um 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 qualified_alias_member está definida no §14.8.

Um member_access é da forma E.I ou da forma E.I<A₁, ..., Aₑ>, onde E é um primary_expression, predefined_type ou qualified_alias_member,I é um identificador único, e <A₁, ..., Aₑ> é um type_argument_listopcional. Quando nenhuma type_argument_list for especificada, considere e como zero.

Uma member_access com uma primary_expression do tipo dynamic está dinamicamente ligada (§12.3.3). Nesse caso, o compilador classifica o acesso de membro como um acesso de propriedade do tipo dynamic. As regras abaixo para determinar o significado do member_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 do primary_expression. Se essa classificação em tempo de execução levar a um grupo de métodos, o acesso do membro será o primary_expression de um invocation_expression.

O member_access é avaliado e classificado da seguinte forma:

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

    Nota: 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 tais métodos sejam considerados para inferência de argumentos de tipo. nota final

    • Se I identifica um tipo, o resultado é esse tipo construído com qualquer argumento de tipo dado.
    • Se I identificar um ou mais métodos, o resultado será um grupo de métodos sem expressão de instância associada.
    • Se I identificar uma propriedade estática, o resultado será um acesso à propriedade sem 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 struct em que o campo é declarado, então o resultado é um valor, isto é, 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), então E.I é processado exatamente como se I fosse um campo estático.
      • Caso contrário, o resultado será um acesso a eventos sem expressão de instância associada.
    • Se I identifica uma constante, então o resultado é 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 em tempo de compilação.
  • Se E for um acesso de propriedade, acesso de indexador, variável ou valor, cujo tipo é T, e uma pesquisa de membro (§12.5) de I em T com argumentos de tipo K produz uma correspondência, então E.I é avaliada e classificada da seguinte forma:
    • Primeiro, se E é uma propriedade ou acesso indexador, então o valor da propriedade ou acesso indexador é obtido (§12.2.2) e E é reclassificado como um valor.
    • Se I identificar um ou mais métodos, o resultado será um grupo de métodos com uma expressão de instância associada de E.
    • Se I identificar uma propriedade de instância, o resultado será um acesso à 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 a partir da primeira declaração ou substituição da propriedade encontrada ao iniciar com Te pesquisar em suas classes base.
    • Se T for um class_type e I identificar um campo de instância desse class_type:
      • Se o valor de E é null, então um System.NullReferenceException é 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 seja um struct_type e I identifica um campo de instância daquele struct_type:
      • Se E é um valor, ou se o campo é somente leitura e a referência ocorre fora de um construtor de instância do struct no qual o campo é declarado, então o resultado é um valor, ou seja, o valor do campo I na instância struct dado por E.
      • Caso contrário, o resultado é uma variável, ou seja, o campo I na instância struct dada por E.
    • Caso I identifique um evento de exemplo:
      • Se a referência se encontrar dentro da classe ou estrutura onde o evento é declarado, e o evento tiver sido declarado sem event_accessor_declarations (§15.8.1), e a referência não aparecer como o operando à esquerda do operador a += ou -=, então E.I é tratado exatamente como se I fosse um campo de instância.
      • Caso contrário, o resultado será um acesso a um evento com uma expressão de instância associada de E.
  • Caso contrário, é 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 é uma referência de membro inválida e ocorre um erro de tempo de ligação.

12.8.7.2 Nomes simples e nomes de tipo idênticos

Em um acesso de membro do formulário E.I, se E for um identificador único, e se o significado de E como um simple_name (§12.8.4) seja uma constante, campo, propriedade, variável local ou parâmetro do mesmo tipo que o significado de E como um type_name (§7.8.1), então ambos os significados possíveis de E são permitidos. A pesquisa de membros E.I nunca é ambígua, uma vez que I será necessariamente um membro do tipo E em ambos os casos. Em outras palavras, a regra apenas permite o acesso aos membros estáticos e aos tipos aninhados de E, caso contrário, ocorreria um erro em 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
    }
}

Apenas para fins expositivos, dentro da classe A, as ocorrências do identificador de 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.

exemplo final

12.8.8 Acesso de Membro Condicional Nulo

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

Um null_conditional_member_access consiste em um primary_expression seguido pelos dois tokens "?" e ".", seguido por um identificador com um type_argument_listopcional, e posteriormente zero ou mais dependent_accesses, e qualquer um deles 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 tem a forma P?.A. O significado de E é determinado da seguinte forma:

  • 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 em tempo de compilação.

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

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

      Só que P é avaliada 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
      

      Só que P é avaliada apenas uma vez.

  • Caso contrário:

    Que T seja 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 em tempo de compilação.

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

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

      Só que P é avaliada 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
      

      Só que P é avaliada apenas uma vez.

Nota: Numa expressão do formulário:

P?.A₀?.A₁

então, se P se avalia para null, nem A₀ nem A₁ são avaliados. O mesmo se aplica se uma expressão for uma sequência de null_conditional_member_access ou null_conditional_element_access§12.8.13 operações.

nota final

Um null_conditional_projection_initializer é uma restrição de null_conditional_member_access e tem a mesma semântica. Ocorre apenas como um inicializador de projeção em uma expressão anônima de criação de objeto (§12.8.17.7).

12.8.9 Expressões de tolerância nula

12.8.9.1 Generalidades

O valor, tipo, classificação (§12.2) e contexto seguro de uma expressão com perdão de nulo (§16.4.12) são o valor, tipo, classificação e contexto seguro da sua expressão_primária.

null_forgiving_expression
    : primary_expression null_forgiving_operator
    ;

null_forgiving_operator
    : '!'
    ;

Nota: Os operadores de perdão de nulidade do postfix e de negação lógica de prefixo (§12.9.4), embora representados pelo mesmo token lexical (!), são distintos. Apenas este último pode ser substituído (§15.10), a definição do operador de perdão nulo é fixa. nota final

É um erro em tempo de compilação aplicar o operador "null-forgiving" mais de uma vez à mesma expressão, mesmo com parênteses intervenientes.

Exemplo: todos os itens a seguir 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)

exemplo final

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

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

O operador null-forgiving é uma pseudo-operação durante a compilação que é usada para informar a análise estática do estado de nulidade de um compilador. Tem duas utilizações: para anular a determinação de um compilador de que uma expressão pode ser nula; e para anular um aviso emitido por um compilador relacionado com a possibilidade de nulidade.

Aplicar o operador de perdão nula a uma expressão para a qual a análise estática de estado nulo de um compilador não produz nenhum aviso não é um erro.

12.8.9.2 Anular uma determinação "talvez nula"

Em algumas circunstâncias, a análise estática de estado nulo 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 indicam que a expressão não pode ser nula. A aplicação do operador null-forgiving a tal expressão informa a análise estática do estado nulo do compilador de que o estado nulo está em e não em, o que evita o aviso de diagnóstico e pode informar a análise em curso.

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 pode ser desreferenciado com segurança para acessar sua propriedade Name, e o aviso "desreferenciação de um valor possivelmente nulo" pode ser suprimido usando !.

exemplo final

Exemplo: O operador de tolerância nula deve ser usado com cautela, 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 de tolerância nula é aplicado a um tipo de valor e anula qualquer aviso em x. No entanto, se x estiver null em tempo de execução, uma exceção será lançada, pois null não pode ser convertida para int.

exemplo final

12.8.9.3 Anulando outros avisos de análise nula

Além de substituir as determinações de talvez nulos de como mencionado acima, pode haver outras circunstâncias em que se deseja substituir a determinação na análise estática do estado nulo de um compilador, indicando que uma expressão requer um ou mais avisos. A aplicação do operador null-forgiving a tal expressão solicita que um compilador não emita nenhum aviso 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 de parâmetros do método Assign, lv & rv, são string?, com lv sendo um parâmetro de saída, e executa uma atribuição simples.

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 porque s não é uma variável anulável. Dado que o segundo argumento do Assignnão pode ser nulo, o operador de perdão nulo é usado para anular o aviso.

exemplo final

Fim do texto condicionalmente normativo.

12.8.10 Expressões de invocação

12.8.10.1 Generalidades

Um invocation_expression é usado para invocar um método.

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

O primary_expression pode ser um null_forgiving_expression se e somente se tiver um delegate_type.

Um invocation_expression está dinamicamente vinculado (§12.3.3) se pelo menos uma das seguintes situações se mantiver:

  • O primary_expression tem o tipo de tempo de compilação dynamic.
  • Pelo menos um argumento do argument_list opcional tem o tipo de tempo de compilação dynamic.

Nesse caso, o compilador classifica o invocation_expression como um valor do tipo dynamic. As regras indicadas abaixo para determinar o significado de invocation_expression são então aplicadas durante a execução, usando o tipo em tempo de execução em vez do tipo em tempo de compilação do primary_expression e dos argumentos que têm o tipo em tempo de compilação dynamic. Se o primary_expression não tiver o tipo de tempo de compilação dynamic, então a invocação do método passa por uma verificação limitada em tempo de compilação, conforme descrito no §12.6.5.

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

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

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

  • Se a expressão de invocação invocar um método returns-no-value (§15.6.1) ou um delegado returns-no-value, o resultado é nulo. Uma expressão que é classificada como nada só é permitida no contexto de um statement_expression (§13.7) ou como o corpo de um lambda_expression (§12.19). Caso contrário, ocorrerá um erro de tempo de vinculação.
  • Caso contrário, se o invocation_expression invocar um método de retorno por referência (§15.6.1) ou um delegado de retorno por referência, o resultado será uma variável com o 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 recetor for de um tipo de classe T, o tipo associado será escolhido a partir da primeira declaração ou sobrescrição do método encontrado, começando por T e pesquisando nas suas classes base.
  • Caso contrário, o invocation_expression invoca um método de retorno por valor (§15.6.1) ou um delegado de retorno por valor, e o resultado é um valor, com um tipo associado do tipo de retorno do método ou delegado. Se a invocação for de um método de instância e o recetor for de um tipo de classe T, o tipo associado será escolhido a partir da primeira declaração ou substituição do método encontrado ao iniciar com T e pesquisar em suas classes base.

12.8.10.2 Invocações de método

Para uma invocação de métodos, a primary_expression do 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 dos quais escolher um método específico para invocar. Neste último caso, a determinação do método específico a invocar baseia-se no contexto fornecido pelos tipos de argumentos no argument_list.

O processamento em tempo de ligação de uma invocação de método da forma M(A), onde M é um grupo de métodos (possivelmente com a inclusão de um type_argument_list), e A é um 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 é candidato quando:
      • M não tem nenhuma lista de argumentos de tipo, e
      • F é aplicável no que diz respeito ao A (§12.6.4.2).
    • Se F for genérico e M não tiver uma lista de argumentos de tipo, F é um candidato quando:
      • A inferência de tipo (§12.6.3) é bem-sucedida, inferindo uma lista de argumentos de tipo para a chamada, e
      • Uma vez 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 satisfazem 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
      • Uma vez 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 satisfazem 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, onde C é o tipo no qual o método F é declarado, todos os métodos declarados em um tipo de 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.

    Nota: Esta última regra só tem efeito quando o grupo de métodos foi 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 efetivo não vazio. nota final

  • 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 de método de extensão (§12.8.10.3). Se isso falhar, não existem métodos aplicáveis e ocorre um erro de tempo de vinculação.
  • O melhor método do conjunto de métodos candidatos é identificado usando as regras de resolução de sobrecarga do §12.6.4. Se um único melhor método não puder ser identificado, a invocação do método será ambígua e ocorrerá um erro de tempo de ligação. Ao executar a resolução de sobrecarga, os parâmetros de um método genérico são considerados após a substituição dos argumentos de tipo (fornecidos ou inferidos) pelos parâmetros de tipo de método correspondentes.

Uma vez que um método tenha sido selecionado e validado em tempo de vinculação pelas etapas acima, a invocação em tempo de execução real é processada de acordo com as regras de invocação de membro da função descritas no §12.6.6.

Nota: O efeito intuitivo das regras de resolução descritas acima é o seguinte: Para localizar o método específico invocado por uma invocação de método, comece com o tipo indicado pela invocação do método e prossiga na cadeia de herança até que pelo menos uma declaração de método aplicável, acessível e sem substituição seja encontrada. Em seguida, execute a inferência de tipo e a resolução de sobrecarga no conjunto de métodos aplicáveis, acessíveis e sem substituição declarados nesse tipo e invoque o método selecionado. Se nenhum método foi encontrado, tente processar a invocação como uma invocação de método de extensão. nota final

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

Numa 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 a construção como uma invocação de método de extensão. Se «expr» ou qualquer um dos «args» tiver o tipo de tempo de compilação dynamic, os métodos de extensão não se aplicarão.

O objetivo é encontrar o melhor type_nameC, para permitir a invocação do método estático correspondente.

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

Um método de extensão Cᵢ.Mₑé elegível se:

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

A busca por C prossegue da seguinte forma:

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

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

Nota: Ao contrário de uma invocação de método de instância, nenhuma exceção é lançada quando expr é avaliada como uma referência nula. Em vez disso, este valor null é passado para o método de extensão como seria através de uma invocação regular de método estático. Cabe à implementação do método de extensão decidir como responder a tal chamada. nota final

As regras anteriores significam que os métodos de instância têm precedência sobre os 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 externo 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 using namespace.

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.G, e E.F tem precedência sobre D.F e C.F.

exemplo final

12.8.10.4 Invocações de delegados

Para uma invocação de delegado, a expressão_primária da expressão_de_invocação deve ser um valor de um tipo_delegado. Além disso, considerando que o delegate_type é um membro da função com a mesma lista de parâmetros que o delegate_type, o delegate_type será aplicável (§12.6.4.2) no que diz respeito à argument_list do invocation_expression.

O processamento em tempo de execução de uma invocação delegada do formulário D(A), em que D é uma primary_expression de um delegate_type e A é um argument_listopcional, consiste nas seguintes etapas:

  • D é avaliada. 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 válido. Se o valor de D for null, uma System.NullReferenceException será gerada e nenhuma outra etapa será executada.
  • Caso contrário, D é uma referência a uma instância delegada. As invocações de membros da função (§12.6.6) são realizadas em todas as entidades chamáveis na lista de invocação do delegado. Para entidades chamáveis que consistem em 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

Um null_conditional_invocation_expression é sintaticamente um null_conditional_member_access (§12.8.8) ou 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 um statement_expression (§13.7), anonymous_function_body (§12.19.1), ou method_body (§15.6.1).

Ao contrário do sintaticamente equivalente null_conditional_member_access ou null_conditional_element_access, 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 pode ser incluído se e apenas se o null_conditional_member_access ou o null_conditional_element_access tiver um delegate_type.

Uma expressão null_conditional_invocation_expressionE tem a forma P?A; onde A é o restante do null_conditional_member_access ou null_conditional_element_accesssintaticamente equivalente, A começará, portanto, com . ou [. Que PA represente a concatenação de P e A.

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

if ((object)P != null) PA

só que P é avaliada apenas uma vez.

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

  • Se E é classificado como nada, então o seu significado é o mesmo que o significado do bloco :

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

    só que P é avaliada 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 deste bloco depende de se E é sintácticamente equivalente a um null_conditional_member_access (§12.8.8) ou a um null_conditional_element_access (§12.8.13).

12.8.12 Acesso a elementos

12.8.12.1 Generalidades

Um element_access consiste em um primary_no_array_creation_expression, seguido de um token “[”, um argument_liste, por fim, um token “]”. O argument_list consiste em um ou mais argumentos s, separados por vírgulas.

element_access
    : primary_no_array_creation_expression '[' argument_list ']'
    ;

A argument_list de um element_access não pode conter argumentos out ou ref.

Um element_access está dinamicamente vinculado (§12.3.3) se pelo menos uma das seguintes situações se mantiver:

  • O primary_no_array_creation_expression tem o tipo de tempo de compilação dynamic.
  • Pelo menos uma expressão do argument_list tem o tipo de tempo de compilação dynamic e o primary_no_array_creation_expression não tem um tipo de matriz.

Neste caso, o compilador classifica o 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_no_array_creation_expression e argument_list que têm o tipo de tempo de compilação dynamic. Se o primary_no_array_creation_expression não tiver o tipo de tempo de compilação dynamic, o acesso ao elemento passa por uma verificação limitada em tempo de compilação, conforme descrito no §12.6.5.

Se o primary_no_array_creation_expression de um element_access for um valor de um tipo de matriz, o element_access é um acesso à matriz (§12.8.12.2). Caso contrário, o primary_no_array_creation_expression deve ser um valor ou variável de uma classe, estrutura ou tipo de interface que tenha um ou mais membros indexadores, caso em que o element_access é considerado um acesso a um indexador (§12.8.12.3).

12.8.12.2 Acesso à matriz

Para um acesso à matriz, o primary_no_array_creation_expression do element_access deve ser o valor de um array_type. Além disso, o argument_list de um acesso à matriz não pode conter argumentos nomeados. O número de expressões na argument_list deve ser o mesmo que o grau da array_type, e cada expressão deve ser do tipo int, uint, longou ulong,, ou deve ser implicitamente convertível para um ou mais destes tipos.

O resultado da avaliação de um acesso à matriz é uma variável do tipo de elemento da matriz, ou seja, o elemento da matriz selecionado pelo(s) valor(es) da(s) expressão(ões) no argument_list.

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

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

12.8.12.3 Acesso ao indexador

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

O processamento em tempo de ligação de um acesso de indexador na forma P[A], onde P é uma primary_no_array_creation_expression de uma classe, struct ou tipo de interface T, e A é uma argument_list, consiste nos seguintes passos:

  • A coleção de indexadores fornecida por T foi construída. 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 são acessíveis no contexto atual (§7.5).
  • O conjunto é reduzido aos indexadores que são aplicáveis e não ocultos por outros indexadores. As seguintes regras são aplicadas a cada S.I indexador no conjunto, onde 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), então I é removido do conjunto.
    • Se I for aplicável em relação ao A (§12.6.4.2), então todos os indexadores declarados em um tipo de base de S sã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, não existirão indexadores aplicáveis e ocorrerá um erro de tempo de ligaçã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 do indexador será ambíguo e ocorrerá um erro de tempo de ligação.
  • As expressões índice do 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 acesso do 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 a partir da primeira declaração ou substituição do indexador encontrado ao iniciar com T e pesquisar em suas classes base.

Dependendo do contexto em que é 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 definido 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 a elementos condicionais nulos

Um null_conditional_element_access consiste num primary_no_array_creation_expression seguido pelos dois tokens "?" e "[", seguido de um argument_list, seguido do token "]", seguido de zero ou mais dependent_accesses, qualquer dos quais pode ser precedido por um null_forgiving_operator.

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

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

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

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

    Que T seja 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 em tempo de compilação.

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

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

      Só que P é avaliada 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
      

      Só que P é avaliada apenas uma vez.

  • Caso contrário:

    Que T seja 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 em tempo de compilação.

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

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

      Só que P é avaliada 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
      

      Só que P é avaliada apenas uma vez.

Nota: Numa expressão do formulário:

P?[A₀]?[A₁]

Caso P avalie para null, nem A₀ nem A₁ são avaliados. O mesmo se aplica se uma expressão for uma sequência de operações de null_conditional_element_access ou null_conditional_member_access§12.8.8.

nota final

12.8.14 Este acesso

Um this_access consiste na palavra-chave this.

this_access
    : 'this'
    ;

Uma this_access só é permitida no de bloco de um construtor de instância, um método de instância, um acessador de instância (§12.2.1) ou um finalizador. Tem um dos seguintes significados:

  • Quando this é usado numa 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 um primary_expression dentro de 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 um primary_expression dentro de um construtor de instância de um struct, ele é classificado como uma variável. O tipo da variável é o tipo de instância (§15.3.2) da struct dentro da qual o uso ocorre, e a variável representa a struct que está sendo construída.
    • Se a declaração do construtor não tiver inicializador do construtor, a variável this se comportará exatamente da mesma forma que 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 da instância.
    • Caso contrário, a variável this se comporta exatamente da mesma forma que um parâmetro ref do tipo struct. Em particular, isso significa que a variável é considerada inicialmente atribuída.
  • Quando this é usado num primary_expression dentro de 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) da struct dentro da qual o uso ocorre.
    • Se o método ou acessador não for um iterador (§15.14) ou uma função assíncrona (§15.15), a variável this representa a struct para a qual o método ou acessador foi invocado.
      • Se o struct for um readonly struct, a variável this se comportará exatamente da mesma forma que um parâmetro de entrada do tipo struct
      • Caso contrário, a variável this se comporta exatamente da mesma forma que um parâmetro ref do tipo struct
    • Se o método ou acessador for um iterador ou uma função assíncrona, a variável this representa uma cópia do struct para o qual o método ou acessador foi invocado e se comporta exatamente da mesma forma que um valor parâmetro do tipo struct.

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

12.8.15 Acesso à base

Um base_access consiste na palavra-chave base seguida por um token ".", um identificador e uma type_argument_list opcional ou uma argument_list entre colchetes.

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

Um base_access é usado para aceder aos membros de uma classe base que são ocultados 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 numa classe ou struct, designarei um membro da classe base dessa classe ou struct. Do mesmo modo, quando base[E] ocorre numa classe, deve existir um indexador aplicável na classe base.

Em tempo de ligação, base_access expressões da forma base.I e base[E] são avaliadas exatamente como se fossem escritas ((B)this).I e ((B)this)[E], onde B é a classe base da classe ou struct em que a construção ocorre. Assim, base.I e base[E] correspondem a this.I e this[E], exceto this é visto como uma instância da classe base.

Quando um base_access faz referência a um membro de função virtual (um método, propriedade ou indexador), a determinação de qual membro da função invocar em tempo de execução (§12.6.6) é alterada. O membro da função que é invocado é determinado encontrando a 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 execução de this, como seria habitual num acesso não base). Assim, dentro de uma substituição de um membro da 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 ligação.

Nota: Ao contrário this, base não é uma expressão em si. É uma palavra-chave usada apenas no contexto de um base_access ou de um constructor_initializer (§15.11.2). nota final

12.8.16 Operadores de incremento e decréscimo Postfix

post_increment_expression
    : primary_expression '++'
    ;

post_decrement_expression
    : primary_expression '--'
    ;

O operando de uma operação de incremento ou decréscimo posfixada deve ser uma expressão que seja classificada como uma variável, um acesso à propriedade, ou um acesso ao indexador. O resultado da operação é um valor do mesmo tipo que o operando.

Se o primary_expression tiver o tipo de tempo de compilação dynamic então o operador está dinamicamente vinculado (§12.3.3), o post_increment_expression ou post_decrement_expression tem o tipo de tempo de compilação dynamic e as seguintes regras são aplicadas em tempo de execução usando o tipo de tempo de execução do primary_expression.

Se o operando de uma operação de incremento ou decréscimo de postfix for uma propriedade ou acesso de indexador, a propriedade ou indexador deverá ter um acessador get e um acessador set. Se este não for o caso, ocorre um erro de tempo de ligação.

A resolução de sobrecarga do operador unário (§12.4.4) é aplicada para selecionar uma implementação específica do operador. Existem operadores de ++ e -- predefinidos para os seguintes tipos: sbyte, byte, short, ushort, int, uint, long, ulong, char, float, double, decimale qualquer tipo de enum. Os operadores de ++ predefinidos retornam o valor produzido adicionando 1 ao operando e os operadores de -- predefinidos retornam o valor produzido subtraindo 1 do operando. Num contexto de verificação, 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 de enumeração, um System.OverflowException é lançado.

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

O processamento em tempo de execução de uma operação de incremento ou decréscimo pós-fixo da forma x++ ou x-- consiste nas seguintes etapas:

  • Se x for classificada como uma variável:
    • x é avaliada para produzir a variável.
    • O valor de x é guardado.
    • O valor salvo de x é convertido para o tipo de operando do operador selecionado e o operador é invocado com esse valor como argumento.
    • O valor retornado pelo operador é convertido para o tipo de x e armazenado no local dado pela avaliação anterior de x.
    • O valor economizado de x torna-se o resultado da operação.
  • Se x estiver classificado como uma propriedade ou um acesso a 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 são avaliadas, e os resultados são utilizados nas subsequentes invocações dos acessadores get e set.
    • O acessador get de x é invocado e o valor retornado é salvo.
    • O valor salvo de x é convertido para o tipo de operando do operador selecionado e o operador é invocado com esse valor como argumento.
    • O valor retornado pelo operador é convertido para o tipo de x e o acessador definido de x é invocado com esse valor como seu argumento de valor.
    • O valor economizado de x torna-se o resultado da operação.

Os operadores ++ e -- também suportam notação de prefixo (§12.9.6). O resultado de x++ ou x-- é o valor de xantes da operação, enquanto o resultado de ++x ou --x é o valor de xapós a operação, respectivamente. Em ambos os casos, x em si tem o mesmo valor após a operação.

A implementação do operador ++ ou do operador -- pode ser invocada usando a notação postfix ou de prefixo. Não é possível ter implementações de operadores separadas para as duas notações.

12.8.17 O novo operador

12.8.17.1 Generalidades

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

Existem três formas de novas expressões:

  • Expressões de criação de objeto e expressões de criação de objeto anônimo 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.
  • As expressões de definição de delegados são usadas para obter instâncias de tipos de delegados.

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

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

12.8.17.2 Expressões de criação de objetos

Um object_creation_expression é usado para criar uma nova instância de um class_type ou um 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 um object_creation_expression deverá ser um class_type, um value_typeou um type_parameter. O tipo não pode ser um tuple_type ou um class_typeabstrato ou estático.

A lista de argumentos facultativa (§12.6.2) só é permitida se o tipo for uma class_type ou um struct_type .

Uma expressão de criação de objeto pode omitir a lista de argumentos do construtor e colocar parênteses, desde que inclua um inicializador de objeto ou inicializador de coleção. Omitir a lista de argumentos do construtor e colocar 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 em primeiro 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.3) ou inicializador de coleção (§12.8.17.4).

Se qualquer um dos argumentos no argument_list opcional tiver o tipo de tempo de compilação dynamic então o object_creation_expression está vinculado dinamicamente (§12.3.3) e as regras a seguir são aplicadas em tempo de execução usando o tipo de tempo de execução dos argumentos do 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 limitada em tempo de compilação, conforme descrito no §12.6.5.

O processamento em tempo de associação de uma object_creation_expression da forma new T(A), onde T é um class_type, ou um value_type, e A é uma argument_listopcional, inclui os seguintes passos:

  • Se T for um valor do tipo e A não estiver presente:
    • O 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 no §8.3.3.
  • Caso contrário, se T for um parâmetro_de_tipo 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 ligação.
    • O resultado do object_creation_expression é um valor do tipo de tempo de execução ao qual o parâmetro type foi vinculado, ou seja, o resultado de invocar o construtor padrão desse tipo. O tipo de tempo de execução pode ser um tipo de referência ou um tipo de valor.
  • Caso contrário, se T for um class_type ou um struct_type:
    • Se T for um class_typeabstrato ou estático, ocorrerá um erro em tempo de compilação.
    • O construtor de instância a ser invocado é determinado usando as regras de resolução de sobrecarga de §12.6.4. O conjunto de construtores de instância candidatos consiste em todos os construtores de instância acessíveis declarados em T, que são aplicáveis em relação a A (§12.6.4.2). Se o conjunto de construtores de instância candidatos estiver vazio ou se um único construtor de melhor instância não puder ser identificado, ocorrerá um erro em tempo de ligação.
    • O resultado do 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, o object_creation_expression é inválido e ocorre um erro de tempo de ligação.

Mesmo que o object_creation_expression esteja vinculado dinamicamente, o tipo de tempo de compilação ainda é T.

O processamento em tempo de execução de um object_creation_expression do formulário new T(A), em que T é class_type ou um struct_type e A é um argument_listopcional, consiste nas seguintes etapas:

  • Se T for um tipo_de_classe:
    • Uma nova instância de classe T é alocada. Se não houver memória suficiente disponível para alocar a nova instância, uma System.OutOfMemoryException será lançada e nenhuma outra etapa será executada.
    • Todos os campos da nova instância são inicializados com seus valores padrão (§9.3).
    • O construtor da instância é invocado de acordo com as regras de invocação do membro da função (§12.6.6). Uma referência à instância recém-alocada é passada automaticamente para o construtor da instância e a instância pode ser acessada de dentro desse construtor como esta.
  • Se T for um struct_type:
    • Uma instância do tipo T é criada alocando uma variável local temporária. Como um construtor de instância de um 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 da instância é invocado de acordo com as regras de invocação do membro da função (§12.6.6). Uma referência à instância recém-alocada é passada automaticamente para o construtor da instância e a instância pode ser acessada de dentro desse construtor como esta.

12.8.17.3 Inicializadores de objeto

Um inicializador de objeto especifica 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 membros, delimitados por tokens { e } e separados por vírgulas. Cada member_initializer designará um destino para a inicialização. Um identificador de deve nomear um campo acessível ou propriedade do objeto que está sendo inicializado, enquanto um argument_list entre colchetes deve especificar argumentos para um indexador acessível no objeto que está sendo inicializado. É um erro para um inicializador de objeto incluir mais de um inicializador de membro para o mesmo campo ou propriedade.

Nota: Embora um inicializador de objeto não tenha permissão para definir o mesmo campo ou propriedade mais de uma vez, não há tais restrições para indexadores. Um inicializador de objeto pode conter vários destinos de inicializador referentes a indexadores e pode até usar os mesmos argumentos de indexador várias vezes. nota final

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 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) ao alvo.

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 incorporado. 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 membros do campo ou propriedade. Os 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 de membro que define um inicializador de coleção após o sinal de igual é uma inicialização de uma coleção embutida. Em vez de atribuir uma nova coleção ao campo, propriedade ou indexador de destino, os elementos fornecidos no inicializador são adicionados à coleção referenciada pelo destino. O alvo deve ser de um tipo de recolha que satisfaça os requisitos especificados no §12.8.17.4.

Quando um destino inicializador se refere a um indexador, os argumentos para o indexador devem ser sempre avaliados exatamente uma vez. Assim, mesmo que os argumentos acabem por nunca ser usados (por exemplo, por causa de um inicializador aninhado vazio), eles são avaliados pelos seus efeitos secundários.

Exemplo: A classe a seguir 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 };

Isto tem o mesmo efeito que

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

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

A classe a seguir mostra um retângulo criado a partir de dois pontos e a criação e inicialização de uma instância 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 }
};

Isto 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;

onde __r, __p1 e __p2 são variáveis temporárias que, de outra forma, seriam invisíveis e inacessíveis.

Se o construtor de Rectanglealocar as duas instâncias incorporadas de Point, elas podem 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();
}

A construção a seguir pode ser usada para inicializar as instâncias de Point incorporadas em vez de atribuir novas instâncias:

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

Isto 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;

exemplo final

12.8.17.4 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_list ',' expression
    ;

Um inicializador de coleção consiste em uma sequência de inicializadores de elementos, delimitados por tokens { e } e separados por vírgulas. Cada inicializador de elemento especifica um elemento a ser adicionado ao objeto de coleção que está sendo inicializado e consiste em uma lista de expressões delimitadas por tokens { e } 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 non_assignment_expression é definida no §12.22.

Exemplo: A seguir está 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 };

exemplo de encerramento

O objeto de coleção ao qual um optimizador de coleções é aplicado deve ser de um tipo que implemente System.Collections.IEnumerable ou irá ocorrer um erro durante a compilação. Para cada elemento especificado na ordem da esquerda para a direita, a pesquisa de membro normal é aplicada para encontrar um membro chamado Add. Se o resultado da pesquisa de membros não for um grupo de métodos, ocorrerá um erro em tempo de compilação. Caso contrário, a resolução de sobrecarga é aplicada com a lista de expressões do inicializador do elemento como a lista de argumentos, e o inicializador da coleção invoca o método resultante. Assim, 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:A seguir 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;

onde __clist, __c1 e __c2 são variáveis temporárias que, de outra forma, seriam invisíveis e inacessíveis.

exemplo de encerramento

12.8.17.5 Expressões de criação de matriz

Um array_creation_expression é usado para criar uma nova instância de um 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 formulário aloca uma ocorrê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 de matriz new int[10,20] produz uma instância de matriz do tipo int[,], e a expressão de criação de matriz new int[10][,] produz uma instância de matriz do tipo int[][,]. exemplo de encerramento

Cada expressão da lista de expressões deve ser do tipo int, uint, long, ou ulong, ou implicitamente convertível num ou mais destes tipos. O valor de cada expressão determina o comprimento da dimensão correspondente na instância de matriz recém-alocada. Uma vez que o comprimento de uma dimensão de matriz não deve ser 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 inseguro (§23.2), o layout das matrizes não é especificado.

Se uma expressão de criação de matriz do primeiro formulário incluir um inicializador de matriz, cada expressão na lista de expressões deve ser uma constante e os comprimentos de ordem e dimensão especificados pela lista de expressões devem corresponder aos do inicializador de matriz.

Em uma expressão de criação de matriz da segunda ou terceira forma, a classificação do tipo de matriz especificado ou especificador de classificação deve corresponder à do inicializador de matriz. Os comprimentos de dimensão individuais são inferidos a partir do número de elementos em cada um dos níveis de aninhamento correspondentes do inicializador de matriz. Assim, a expressão do inicializador na seguinte declaração

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 da terceira forma é referida como uma expressão de criação de matriz de tipo implícito . É semelhante à segunda forma, exceto que o tipo de elemento da matriz não é explicitamente dado, mas determinado como o melhor tipo comum (§12.6.3.15) do conjunto de expressões no inicializador da matriz. Para uma matriz multidimensional, ou seja, aquela em que o rank_specifier contém pelo menos uma vírgula, esse conjunto compreende todos osde expressão encontrados em array_initializers aninhados.

Os inicializadores de matriz são descritos mais detalhadamente 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 tempo de execução de uma expressão de criação de matriz consiste nas seguintes etapas:

  • As expressões do comprimento da dimensão do expression_list são avaliadas em ordem, da esquerda para a direita. Após a avaliação de cada expressão, é realizada uma conversão implícita (§10.2) para um dos seguintes tipos: int, uint, long, ulong. Escolhe-se o primeiro tipo desta lista para o qual existe uma conversão implícita. Se a avaliação de uma expressão ou a conversão implícita subsequente causar uma exceção, nenhuma outra expressão será avaliada e nenhuma outra etapa será executada.
  • Os valores calculados para os comprimentos de dimensão são validados da seguinte forma: Se um ou mais dos valores forem menores que zero, uma System.OverflowException será lançada e nenhuma outra etapa será executada.
  • Uma instância de matriz com os comprimentos de dimensão fornecidos é alocada. Se não houver memória suficiente disponível para alocar a nova instância, uma System.OutOfMemoryException será lançada 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 de matriz contiver um inicializador de matriz, cada expressão no inicializador de matriz será avaliada e atribuída ao seu elemento de matriz correspondente. As avaliações e atribuições são realizadas na ordem em que as expressões são escritas no inicializador da matriz, ou seja, os elementos são inicializados em ordem crescente de índice, 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 outro elemento 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 dessa matriz devem ser inicializados manualmente.

Exemplo: A declaraçã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 declaração

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

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

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

exemplo de encerramento

Nota: Quando uma matriz de matrizes tem uma forma "retangular", ou seja, quando as submatrizes são todas do mesmo comprimento, é 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 contrapartida,

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

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

nota final

Exemplo: A seguir estão exemplos de expressões de criação de matriz digitadas 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 em 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 explicitamente tipada deve ser usada nesse caso, por exemplo, especificando o tipo a ser object[]. Alternativamente, um dos elementos pode ser convertido para um tipo de base comum, que se tornaria o tipo de elemento inferido.

exemplo final

Expressões de criação de matriz digitadas implicitamente podem ser combinadas com inicializadores de objeto anônimos (§12.8.17.7) para criar estruturas de dados digitadas 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" }
    }
};

exemplo final

12.8.17.6 Expressões de criação de delegados

Um delegate_creation_expression é usado 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 delegado do tipo 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 define 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, o delegate_creation_expression será vinculado dinamicamente (§12.8.17.6), e as regras abaixo serão aplicadas em tempo de execução usando o tipo de tempo de execução da expressão . Caso contrário, as regras são aplicadas em tempo de compilação.

O processamento em tempo de vinculação de uma delegate_creation_expression do formulário new 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 delegada 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 delegada é 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 deve ser compatível (§20.2) com D, e o resultado é uma referência a um delegado recém-criado com uma lista de invocação de entrada única que invoca E.

O processamento em tempo de execução de uma delegate_creation_expression da forma new 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 é uma função anônima, a criação de delegado é avaliada como uma conversão de função anônima de E para D (§10.7).
  • Se E for um valor de um tipo_delegado:
    • E é avaliada. Se essa avaliação causar uma exceção, nenhuma etapa adicional será executada.
    • Se o valor de E for null, uma System.NullReferenceException será lançada e nenhuma outra etapa será executada.
    • Uma nova instância do tipo delegado D é criada. Se não houver memória suficiente disponível para alocar a nova instância, uma System.OutOfMemoryException será lançada e nenhuma outra etapa será executada.
    • A nova instância delegada é inicializada com uma lista de invocação de entrada única 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 do delegado. Em outras palavras, não é possível alterar as entidades chamáveis de destino de um delegado depois que ele foi criado.

Nota: Lembre-se, quando dois delegados são combinados ou um é removido do outro, um novo delegado resulta; nenhum delegado existente tem seu conteúdo alterado. nota final

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 a partir de um grupo de métodos, a lista de parâmetros e o tipo de retorno do delegado determinam qual 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, teria ocorrido um erro em tempo de compilação.

exemplo final

12.8.17.7 Expressões anônimas de criação de objetos

Um anonymous_object_creation_expression é usado para criar um objeto de um 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 são uma sequência de propriedades de leitura apenas, que são inferidas a partir do inicializador de objeto anónimo utilizado para criar uma instância do tipo. Especificamente, um inicializador de objeto anônimo do formulário

new { p₁=e₁,p₂=e₂, ... pv=ev}

declara um tipo anónimo do formulário

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 utilizada num member_declarator deve ter um tipo. Assim, é um erro em tempo de compilação que uma expressão em um member_declarator seja null ou uma função anónima.

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

Dentro do mesmo programa, dois inicializadores de objeto 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.

exemplo final

Os métodos Equals e GetHashcode em tipos anônimos substituem os métodos herdados de objecte são definidos em termos de Equals e GetHashcode das propriedades, de modo que duas instâncias do mesmo tipo anônimo são 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 atribuição de uma propriedade com o mesmo nome. Especificamente, os declarantes membros dos formulários

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

são precisamente equivalentes às seguintes, respetivamente:

«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 propriedade ao qual 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.18 Tipo de operador

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 '::' identifier generic_dimension_specifier?
    | unbound_type_name '.' identifier generic_dimension_specifier?
    ;

generic_dimension_specifier
    : '<' comma* '>'
    ;

comma
    : ','
    ;

A primeira forma de typeof_expression consiste em uma palavra-chave typeof seguida de um tipo entre parênteses. O resultado de uma expressão deste formulário é o objeto System.Type para o tipo indicado. Há apenas um objeto System.Type para qualquer tipo. Isto significa que, para um tipo T, typeof(T) == typeof(T) é sempre verdade. O tipo não pode ser dynamic.

A segunda forma de typeof_expression consiste em uma palavra-chave typeof seguida por um parêntesis unbound_type_name.

Nota: Um unbound_type_name é muito semelhante a um type_name (§7.8), exceto que um unbound_type_name contém generic_dimension_specifiers onde um type_name contém type_argument_lists. nota final

Quando o operando de um typeof_expression é uma sequência de tokens que satisfaz as gramáticas de ambos unbound_type_name e type_name, ou seja, quando não contém nem um generic_dimension_specifier nem um type_argument_list, a sequência de tokens é considerada uma type_name. O significado de um unbound_type_name é determinado da seguinte forma:

  • Converta a sequência de tokens numa type_name substituindo cada generic_dimension_specifier por uma type_argument_list que tenha o mesmo número de vírgulas e a palavra-chave object como cada type_argument.
  • Avalie o type_nameresultante, ignorando todas as restrições de parâmetros de tipo.
  • O unbound_type_name se resolve em o tipo genérico não vinculado associado ao tipo construído resultante (§8.4).

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

O resultado do typeof_expression é o objeto System.Type para o tipo genérico não vinculado 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 desta forma é o objeto System.Type que representa a ausência de um tipo. O objeto de tipo retornado por typeof(void) é distinto do objeto de tipo retornado para qualquer tipo.

Nota: Este objeto System.Type especial é útil em bibliotecas de classe que permitem a reflexão sobre métodos na linguagem, onde esses métodos desejam ter uma maneira de representar o tipo de retorno de qualquer método, incluindo métodos void, com uma instância de System.Type. nota final

O operador typeof pode ser usado em um parâmetro type. É um erro de tempo de compilação se o nome do tipo é conhecido por ser um tipo de referência anulável. O resultado é o objeto System.Type para o tipo de tempo de execução que foi vinculado ao parâmetro type. Se o tipo de tempo de execução 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 utilizado num tipo construído ou num tipo genérico não ligado (§8.4.4). O objeto System.Type para um tipo genérico não acoplado 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 tempo de execução, portanto, seu objeto System.Type depende dos argumentos de tipo de tempo de execução em uso. O tipo genérico não vinculado, por outro lado, não tem argumentos de tipo e produz o mesmo objeto System.Type, independentemente dos argumentos de tipo em tempo de execução.

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 seguinte saída:

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 type, mas o resultado de typeof(X<T>) depende.

exemplo final

12.8.19 A dimensão do operador

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 operando para dimensionar deve ser um unmanaged_type (§8.8).

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

Para certos 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) 8
sizeof(ulong) 8
sizeof(char) 2
sizeof(float) 4
sizeof(double) 8
sizeof(bool) 1
sizeof(decimal) 16

Para um tipo de enum T, o resultado da expressão sizeof(T) é um valor constante igual ao tamanho do seu tipo subjacente, como dado acima. Para todos os outros tipos de operando, o operador sizeof é especificado no §23.6.9.

12.8.20 Operadores controlados e não controlados

Os operadores checked e unchecked são usados para controlar o contexto de verificação de overflow para operações aritméticas de tipos inteiros e conversões.

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. Um checked_expression ou unchecked_expression corresponde exatamente a um parenthesized_expression (§12.8.5), exceto que a expressão contida é avaliada no contexto de verificação de estouro dado.

O contexto de verificação de estouro também pode ser controlado através das 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 declaraçõ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 enum.
  • O operador predefinido - unário (§12.9.3), quando o operando é de um tipo integral.
  • Os operadores binários predefinidos +, -, *e / (§12.10), quando ambos os operandos são do tipo integral ou enum.
  • 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 que é muito grande para 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 é executada em tempo de execução, um System.OverflowException é gerado.
  • Em um contexto unchecked, o resultado é truncado descartando quaisquer bits de ordem alta que não se encaixam no 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 são incluídas por nenhum operador ou instrução checked ou unchecked, o contexto padrão de verificação de estouro é desmarcado, a menos que fatores externos (como opções de compilador e configuração do ambiente de execução) exijam uma avaliação verificada.

Para expressões constantes (§12.23) (expressões que podem ser totalmente avaliadas em tempo de compilação), o contexto padrão de verificação de estouro está sempre ativado. A menos que uma expressão constante seja explicitamente colocada num contexto unchecked, estouros que ocorrem durante a avaliação de compilação da expressão sempre causam erros de compilação.

O corpo de uma função anónima não é afetado pelos contextos checked ou unchecked em que a função anónima se encontra.

Exemplo: No código seguinte

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 em tempo de compilação é relatado, uma vez que nenhuma das expressões pode ser avaliada em tempo de compilação. Em execução, o método F gera um System.OverflowException, e o método G retorna –727379968 (os 32 bits inferiores do resultado fora do intervalo). O comportamento do método H depende do contexto padrão de verificação de estouro para a compilação, sendo igual a F ou a G.

exemplo final

Exemplo: O seguinte código

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 num contexto checked. Um estouro também ocorre ao avaliar a expressão constante em G, mas como a avaliação ocorre em um contexto unchecked, o estouro não é relatado.

exemplo final

Os operadores checked e unchecked afetam apenas o contexto de verificação de estouro para as operações que estão textualmente contidas 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 seguinte

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, portanto, x * y é avaliada no contexto padrão de verificação de estouro.

exemplo final

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

Exemplo:

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

Ambas as constantes hexadecimais acima são do tipo uint. Como as constantes estão fora do intervalo de int, sem o operador unchecked, as versões para int produziriam erros em tempo de compilação.

exemplo final

Nota: Os operadores e instruções checked e unchecked permitem aos programadores controlar certos aspetos 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 decimais sempre resulta numa exceção de estouro, mesmo em uma construção explicitamente não verificada. Da mesma forma, a multiplicação de dois pontos flutuantes nunca resulta em uma exceção no overflow, mesmo dentro de uma estrutura explicitamente verificada. Além disso, outros operadores nunca são afetados pelo modo de verificação, seja padrão ou explícito. nota final

12.8.21 Expressões de valor padrão

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

default_value_expression
    : explictly_typed_default
    | default_literal
    ;

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

default_literal
    : 'default'
    ;

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

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

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

  • um tipo de referência
  • um parâmetro de tipo que se sabe 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,; quer
  • qualquer tipo de enumeração.

12.8.22 Alocação de pilha

Uma expressão de alocação de pilha aloca um bloco de memória da pilha de execução. A pilha de execução é uma área de memória onde as variáveis locais são armazenadas. A pilha de execução não faz parte do heap gerenciado. A memória usada para armazenamento de variáveis locais é 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.12.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
    ;

O unmanaged_type (§8.8) indica o tipo de itens que serão armazenados no local recém-alocado e a expressão indica o número desses itens. Em conjunto, estes especificam a dimensão de atribuição necessária. O tipo de expressão deve ser implicitamente convertível no tipo int.

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

No tempo de execução, 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, um System.StackOverflowException será lançado.

Quando um stackalloc_initializer está presente:

  • Se unmanaged_type for omitido, infere-se seguindo as regras para o melhor tipo comum (§12.6.3.15) para o conjunto de stackalloc_element_initializers.
  • Se constant_expression for omitida, infere-se que corresponde ao número de stackalloc_element_initializers.
  • Se constant_expression estiver presente, será igual ao número de stackalloc_element_initializers.

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

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

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

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

Nota: Não há nenhuma maneira de liberar explicitamente a memória alocada usando stackalloc. nota final

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

Exceto para o operador stackalloc, o C# não fornece construções predefinidas para gerenciar a memória coletada que não seja lixo. Esses serviços são normalmente fornecidos pelo 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, o Span<double> resultante é convertido para o tipo definido pelo usuário Widget<double> usando a conversão, conforme mostrado. exemplo final

12.8.23 Nome do operador

Um 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
    ;

Porque nameof não é uma palavra-chave, um nameof_expression é sempre sintaticamente ambíguo com uma invocação do nome simples nameof. Por razões de compatibilidade, se uma pesquisa de nome (§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 simples de nome e acesso de membro são realizadas no named_entity em tempo de compilação, seguindo as regras descritas no §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, um nameof_expression não produz esse erro.

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

Um nameof_expression é uma expressão constante do tipo stringe não tem efeito em tempo de execução. Especificamente, sua named_entity não é avaliada e é ignorada para fins de análise de atribuição definida (§9.4.4.22). O seu valor é o último identificador do named_entity antes da lista de argumentos de tipo final opcional, transformado desta forma:

  • O prefixo "@", se usado, é removido.
  • Cada unicode_escape_sequence é transformado no seu caractere Unicode correspondente.
  • Todos os caracteres de formatação são removidos.

Estas são as mesmas transformações aplicadas no §6.4.3 ao testar a igualdade entre identificadores.

Exemplo: O exemplo seguinte ilustra os resultados de várias expressões nameof, assumindo que um tipo genérico List<T> foi declarado dentro do 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) para apenas "Generic" em vez do namespace completo, e de nameof(TestAlias) para "TestAlias" em vez de "String". exemplo final

12.8.24 Expressões de método anónimas

Um anonymous_method_expression é uma das duas formas de definir uma função anónima. Estes são descritos mais pormenorizadamente no §12.19.

12.9 Operadores unários

12.9.1 Generalidades

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

Nota: O operador de perdão nulo do postfix (§12.8.9), !, devido à sua natureza apenas de tempo de compilação e não sobrecarregável, está excluído da lista acima. nota final

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 apenas em código não seguro (§23).

Se o operando de um unary_expression tiver o tipo de tempo de compilação dynamic, ele está dinamicamente vinculado (§12.3.3). Nesse caso, o tipo de tempo de compilação do unary_expression é dynamice a resolução descrita abaixo ocorrerá em tempo de execução usando o tipo de tempo de execução do operando.

12.9.2 Operador Unary plus

Para uma operação do formulário +x, a resolução de sobrecarga do operador unário (§12.4.4) é aplicada para selecionar uma implementação específica do operador. O operando é convertido para o tipo de parâmetro do operador selecionado, e o tipo do resultado é o tipo de retorno do operador. Os operadores unary plus 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 é simplesmente o valor do operando.

As formas levantadas (§12.4.8) das formas unárias predefinidas não levantadas mais os operadores acima definidos também são predefinidas.

12.9.3 Unary menos operador

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

  • Negação inteira:

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

    O resultado é calculado subtraindo X de zero. Se o valor de X é o menor valor representável do tipo de operando (−2³¹ para int ou −2⁶³ para long), então a negação matemática de X não é representável dentro do tipo de operando. Se isso ocorrer dentro de um contexto checked, um System.OverflowException é gerado; se ocorrer dentro de 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 que o valor int−2147483648 (−2³¹) seja 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 em tempo de compilação. Uma exceção é a regra que permite que o valor long−9223372036854775808 (−2⁶³) seja escrito como um literal inteiro decimal (§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 é NaN, o resultado também é NaN.

  • Negação decimal:

    decimal operator –(decimal x);
    

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

As formas levantadas (§12.4.8) dos operadores unários predefinidos não levantados definidos acima também são predefinidas.

12.9.4 Operador de negação lógica

Para uma operação do formulário !x, a resolução de sobrecarga do operador unário (§12.4.4) é aplicada para selecionar uma implementação específica do operador. O operando é convertido para o 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ógica predefinido:

bool operator !(bool x);

Este operador calcula 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 não elevado definido acima também são predefinidas.

Nota: Os operadores de negação lógica de prefixo e de perdão nulo de pós-fixo (§12.8.9), embora representados pelo mesmo token lexical (!), são distintos. nota final

12.9.5 Operador do complemento Bitwise

Para uma operação do formulário ~x, a resolução de sobrecarga do operador unário (§12.4.4) é aplicada para selecionar uma implementação específica do operador. O operando é convertido para o tipo de parâmetro do operador selecionado, e o tipo do resultado é o tipo devolvido pelo operador. Os operadores de complemento de 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 destes operadores, o resultado da operação é o complemento bit a bit de x.

Cada tipo de enumeração E implicitamente fornece o seguinte operador de complemento por bits:

E operator ~(E x);

O resultado da avaliação ~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 para E é sempre realizada como se estivesse em um contexto unchecked (§12.8.20).

As formas levantadas (§12.4.8) dos operadores de complemento bitwise predefinidos não levantados definidos acima também são predefinidas.

12.9.6 Operadores de incremento e decréscimo de prefixo

pre_increment_expression
    : '++' unary_expression
    ;

pre_decrement_expression
    : '--' unary_expression
    ;

O operando de uma operação de incremento ou decréscimo de prefixo deve ser uma expressão classificada como variável, um acesso a uma propriedade ou um acesso indexador. O resultado da operação é um valor do mesmo tipo que o operando.

Se o operando de uma operação de incremento ou decréscimo de prefixo for uma propriedade ou um acesso de indexador, a propriedade ou indexador deve ter um acessor get e um acessor set. Se este não for o caso, ocorre um erro de tempo de ligação.

A resolução de sobrecarga do operador unário (§12.4.4) é aplicada para selecionar uma implementação específica do operador. Existem operadores de ++ e -- predefinidos para os seguintes tipos: sbyte, byte, short, ushort, int, uint, long, ulong, char, float, double, decimale qualquer tipo de enum. Os operadores de ++ predefinidos retornam o valor produzido adicionando 1 ao operando e os operadores de -- predefinidos retornam o valor produzido subtraindo 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 integral ou enumeração, uma System.OverflowException é lançada.

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

O processamento em tempo de execução de um incremento de prefixo ou operação de decréscimo do formulário ++x ou --x consiste nas seguintes etapas:

  • Se x for classificada como uma variável:
    • x é avaliada para produzir a variável.
    • O valor de x é convertido para o tipo de operando do operador selecionado e o operador é invocado com esse valor como argumento.
    • O valor retornado pelo operador é convertido para o tipo de x. O valor resultante é armazenado no local dado pela avaliação de x.
    • e torna-se o resultado da operação.
  • Se x for classificado como uma propriedade ou acesso 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) associada a x são avaliadas, e os resultados são usados nas subsequentes invocações dos acessores get e set.
    • O acessor get de x é invocado.
    • O valor retornado pelo acessador get é convertido para o tipo de operando do operador selecionado e o operador é invocado com esse valor como argumento.
    • O valor retornado pelo operador é convertido para o tipo de x. O acessador de conjunto de x é invocado com esse valor como seu argumento de valor.
    • Este valor também se torna o resultado da operação.

Os operadores ++ e -- também suportam notação postfix (§12.8.16). O resultado de x++ ou x-- é o valor de x antes da operação, enquanto o resultado de ++x ou --x é o valor de x após a operação. Em ambos os casos, x em si tem o mesmo valor após a operação.

A implementação do operador ++ ou do operador -- pode ser invocada usando notação pós-fixa ou prefixa. Não é possível ter implementações de operadores separadas para as duas notações.

As formas elevadas (§12.4.8) dos operadores predefinidos de incremento e decréscimo de prefixo não elevados definidos acima também são predefinidas.

12.9.7 Transmitir expressões

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

cast_expression
    : '(' type ')' unary_expression
    ;

Uma cast_expression da forma (T)E, onde T é um tipo e E é um unary_expression, realiza uma conversão explícita (§10.3) do valor de E para o tipo T. Se não existir nenhuma conversão explícita de E para T, ocorrerá um erro em tempo de ligação. Caso contrário, o resultado é 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 um cast_expression resulta em certas ambiguidades sintáticas.

Exemplo: A expressão (x)–y pode ser interpretada como uma 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). exemplo final

Para resolver cast_expression ambiguidades, existe a seguinte regra: Uma sequência de um ou mais tokens (§6.4) entre parênteses é considerada o início de um cast_expression somente se pelo menos uma das seguintes situações for verdadeira:

  • A sequência de tokens é a gramática correta para um tipo, mas não para uma expressão.
  • A sequência de tokens constitui a gramática correta para um tipo, e o token imediatamente após os parênteses de fecho é 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 estrutura gramatical indicada. Especificamente, não considera o significado real de quaisquer identificadores constituintes.

Exemplo: Se x e y são identificadores, então x.y é a gramática correta para um tipo, mesmo que x.y realmente não denote um tipo. exemplo final

Nota: Da regra de desambiguação, segue-se que, se x e y são identificadores, (x)y, (x)(y)e (x)(-y) são cast_expressions, mas (x)-y não é, mesmo que x identifique um tipo. No entanto, se x é uma palavra-chave que identifica um tipo predefinido (como int), então todas as quatro formas são cast_expressions (porque tal palavra-chave não poderia ser uma expressão por si só). nota final

12.9.8 Aguarde expressões

12.9.8.1 Generalidades

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

await_expression
    : 'await' unary_expression
    ;

Uma await_expression é permitida apenas no corpo de uma função assíncrona (§15.15). Na função assíncrona de encerramento mais próxima, não deve ocorrer await_expression nestes locais:

  • Dentro de uma função anónima aninhada (não assincrónica)
  • Dentro do bloco de um lock_statement
  • Em uma conversão de função anônima para um tipo de árvore de expressão (§10.7.3)
  • Num contexto inseguro

Nota: Um await_expression não pode ocorrer na maioria dos locais dentro de um query_expression, uma vez que este é sintaticamente transformado para usar expressões lambda não assíncronas. nota final

No interior de uma função assíncrona, await não deve ser utilizado como available_identifier embora o identificador literal @await possa ser utilizado. Não há, portanto, ambiguidade sintática entre await_expressions e várias expressões que envolvem identificadores. Fora das funções assíncronas, await atua como um identificador normal.

O operando de um await_expression é denominado tarefa. Ele representa uma operação assíncrona que pode ou não estar concluída no momento em que o await_expression é avaliado. O objetivo do operador await é suspender a execução da função assíncrona de inclusão até que a tarefa esperada seja concluída e, em seguida, obter seu resultado.

12.9.8.2 Expressões aguardadas

A tarefa de um await_expression deve ser aguardada. Uma expressão t é passível de espera se uma das seguintes condições se mantiver:

  • t é do tipo dynamic durante a compilação
  • 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 todos os seguintes itens se mantêm:
    • A implementa o System.Runtime.CompilerServices.INotifyCompletion de interface (doravante conhecido como INotifyCompletion para brevidade)
    • A tem uma propriedade de instância legível e acessível IsCompleted do tipo bool
    • A tem 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 garçom para a tarefa. Tipo A é chamado de tipo de esperador para a expressão await.

O objetivo da propriedade IsCompleted é determinar se a tarefa já está concluída. Em caso afirmativo, não há necessidade de suspender a avaliação.

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

O objetivo do método GetResult é obter o resultado da tarefa uma vez concluída. Este resultado pode ser uma conclusão bem-sucedida, possivelmente com um valor de resultado, ou uma exceção lançada pelo método GetResult.

12.9.8.3 Classificação das expressões de espera

A expressão await t é classificada da mesma forma que a expressão (t).GetAwaiter().GetResult(). Assim, se o tipo de retorno de GetResult for void, o await_expression é classificado como nada. Se o tipo de retorno não forvoidT, o await_expression é classificado como um valor do tipo T.

12.9.8.4 Avaliação em tempo de execução de expressões 'await'

Em tempo de execução, a expressão await t é avaliada da seguinte forma:

  • Um awaiter a é obtido avaliando a expressão (t).GetAwaiter().
  • Uma boolb é obtida através da avaliação da expressão (a).IsCompleted.
  • Se b é false então a avaliação depende se a implementa a interface System.Runtime.CompilerServices.ICriticalNotifyCompletion (doravante conhecida como ICriticalNotifyCompletion para brevidade). Esta verificação é feita no momento da vinculação; ou seja, em tempo de execução se a tiver o tipo de tempo de compilação dynamic, e em tempo de compilação caso contrário. Que o r indique o delegado de retomada (§15.15):
    • Se a não implementar ICriticalNotifyCompletion, a expressão ((a) as INotifyCompletion).OnCompleted(r) será avaliada.
    • Se a implementar ICriticalNotifyCompletion, a expressão ((a) as ICriticalNotifyCompletion).UnsafeOnCompleted(r) será avaliada.
    • A avaliação é então suspensa e o controle é retornado ao chamador atual da função assíncrona.
  • Imediatamente após (se b foi true), ou após posterior invocação do delegado de retomada (se b foi false), a expressão (a).GetResult() é avaliada. Se ele retornar um valor, esse valor é o resultado do await_expression. Caso contrário, o resultado não é nada.

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

12.10 Operadores aritméticos

12.10.1 Generalidades

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 tem o tipo de tempo de compilação dynamic, então a expressão é dinamicamente ligada (§12.3.3). Nesse caso, o tipo de tempo de compilação da expressão é dynamic, e a resolução descrita abaixo ocorrerá em tempo de execução usando o tipo de tempo de execução dos operandos que têm o tipo de tempo de compilação dynamic.

12.10.2 Operador de multiplicação

Para uma operação do formulário x * y, a resolução de sobrecarga do operador binário (§12.4.5) é aplicada para selecionar uma implementação de operador específico. Os operandos são convertidos para os tipos de parâmetros 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 calculam o produto de x e y.

  • Multiplicação inteira:

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

    Num contexto checked, se o produto estiver fora do intervalo do tipo de resultado, um System.OverflowException é lançado. Em um contexto unchecked, estouros não são relatados e quaisquer bits significativos de alta ordem fora do intervalo do tipo de resultado são descartados.

  • Multiplicação de vírgula flutuante:

    float operator *(float x, float y);
    double operator *(double x, double y);
    

    O produto é calculado 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, diferentes de zero, zeros, infinidades e NaNs. Na tabela, x e y são valores finitos positivos. 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 é infinito. Devido ao 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

    (Salvo indicação em contrário, nas tabelas de vírgulas flutuantes §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 sinal (NaN).)

  • Multiplicação decimal:

    decimal operator *(decimal x, decimal y);
    

    Se a dimensão do valor resultante for muito grande para representar no formato decimal, uma System.OverflowException é gerada. Devido ao arredondamento, o resultado pode ser zero mesmo que nenhum operando seja 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 levantadas (§12.4.8) dos operadores de multiplicação predefinidos não levantados acima definidos também são predefinidas.

12.10.3 Operador de divisão

Para uma operação do formulário x / y, a resolução de sobrecarga do operador binário (§12.4.5) é aplicada para selecionar uma implementação de operador específico. Os operandos são convertidos para os tipos de parâmetros 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 calculam o quociente de x e y.

  • Divisão inteira:

    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, uma System.DivideByZeroException será lançada.

    A divisão arredonda o resultado em direção a zero. Assim, o valor absoluto do resultado é o maior inteiro possível que é 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 esquerdo for o menor valor representável int ou long e o operando direito for –1, ocorre um overflow. 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 um System.ArithmeticException (ou uma subclasse dele) é lançado ou se o estouro não é relatado, com o valor resultante sendo o do operando à esquerda.

  • Divisão de vírgula flutuante:

    float operator /(float x, float y);
    double operator /(double x, double y);
    

    O quociente é calculado 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 diferentes de zero, zeros, infinidades e NaNs. Na tabela, x e y são valores finitos positivos. 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, uma System.DivideByZeroException será lançada. Se a magnitude do valor resultante for muito grande para representar no formato decimal, será gerado um erro System.OverflowException. Devido ao 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 preferida que preservará um resultado igual ao resultado exato. A escala preferida é 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 levantadas (§12.4.8) dos operadores de divisão predefinidos não levantados acima definidos também são predefinidas.

12.10.4 Operador remanescente

Para uma operação do formulário x % y, a resolução de sobrecarga do operador binário (§12.4.5) é aplicada para selecionar uma implementação de operador específico. Os operandos são convertidos para os tipos de parâmetros do operador selecionado, e o tipo do resultado é o tipo de retorno do operador.

Os restantes operadores predefinidos estão listados abaixo. Todos os operadores calculam 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 é lançada.

    Se o operando esquerdo for o menor valor int ou long e o operando direito for –1, uma exceção 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 diferentes de zero, zeros, infinidades e NaNs. Na tabela, x e y são valores finitos positivos. z é o resultado de x % y e é calculado como x – n * y, onde n é o maior número inteiro possível que é menor ou igual a x / y. Este 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
  • Remanescente decimal:

    decimal operator %(decimal x, decimal y);
    

    Se o valor do operando direito for zero, um System.DivideByZeroException é gerado. É definido pela implementação quando um System.ArithmeticException (ou uma subclasse dele) é lançado. Uma implementação conforme não deverá lançar uma exceção para x % y em qualquer caso em que x / y não lance uma exceção. A escala do resultado, antes de qualquer arredondamento, é a maior das escalas dos dois operandos, e o sinal do resultado, se diferente de zero, é o mesmo que o de x.

    O resto decimal é equivalente ao uso do operador resto do tipo System.Decimal.

    Nota: Essas regras asseguram que, para todos os tipos, o resultado nunca tenha o sinal oposto do operando esquerdo. nota final

As formas levantadas (§12.4.8) dos operadores remanescentes predefinidos não levantados definidos acima também são predefinidas.

12.10.5 Operador de adição

Para uma operação do formulário x + y, a resolução de sobrecarga do operador binário (§12.4.5) é aplicada para selecionar uma implementação de operador específico. Os operandos são convertidos para os tipos de parâmetros 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 calculam a soma dos dois operandos. Quando um ou ambos os 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 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, uma System.OverflowException será lançada. Em um contexto unchecked, os transbordos não são relatados e os bits significativos de alta ordem que estão fora do intervalo do tipo de resultado são descartados.

  • Adição de vírgula flutuante:

    float operator +(float x, float y);
    double operator +(double x, double y);
    

    A soma é calculada 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 nulos, 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 têm a mesma magnitude, mas sinais opostos, z é zero positivo. Se x + y for muito grande para representar no tipo de destino, z é 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 demasiado grande para ser representada no formato decimal, é gerada uma exceção System.OverflowException. 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, onde E é o tipo enum e U é o tipo subjacente de E:

    E operator +(E x, U y);
    E operator +(U x, E y);
    

    Em tempo de execução, estes operadores são avaliados exatamente como (E)((U)x + (U)y).

  • Concatenação de cordas:

    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 que não sejastring será convertido na sua representação de sequência de caracteres ao invocar o 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 US-English. 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 por configurações regionais.

    exemplo de encerramento

    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. Um System.OutOfMemoryException pode ser lançado 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, onde D é o tipo de 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 isso 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 delegada cuja lista de invocação consiste nos elementos na lista de invocação do primeiro operando, seguidos pelos elementos na lista de invocação do segundo operando. Ou seja, a lista de invocação do delegado resultante é a concatenação das listas de invocação dos dois operandos.

    Nota: Para exemplos de combinação de delegados, ver §12.10.6 e §20.6. Como System.Delegate não é um tipo de delegado, o operador + não está definido para ele. nota final

As formas elevadas (§12.4.8) dos operadores de adição predefinidos não elevados acima definidos também são predefinidas.

12.10.6 Operador de subtração

Para uma operação do formulário x – y, a resolução de sobrecarga do operador binário (§12.4.5) é aplicada para selecionar uma implementação de operador específico. Os operandos são convertidos para os tipos de parâmetros 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 inteira:

    int operator –(int x, int y);
    uint operator –(uint x, uint y);
    long operator –(long x, long y);
    ulong operator –(ulong x, ulong y
    

    Num contexto checked, se a diferença estiver fora do intervalo do tipo de resultado, é lançada uma System.OverflowException. Em um contexto unchecked, estouros não são relatados e quaisquer 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 é calculada 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 não nulos finitos, 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 são iguais, z é zero positivo. Se x – y for muito grande para representar no tipo de destino, z é 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 -y denotam o negação de y, não que o valor seja negativo.)

  • Subtração decimal:

    decimal operator –(decimal x, decimal y);
    

    Caso a magnitude do valor resultante seja demasiado grande para ser representada no formato decimal, uma exceção System.OverflowException será gerada. 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, onde E é o tipo de enum e U é o tipo subjacente de E:

    U operator –(E x, E y);
    

    Este operador é avaliado exatamente como (U)((U)x – (U)y). Em outras palavras, o operador calcula 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);
    

    Este operador é avaliado exatamente como (E)((U)x – y). Em outras palavras, o operador subtrai um valor do tipo subjacente da enumeração, produzindo um valor da enumeração.

  • Remoção de delegado. Cada tipo de delegado fornece implicitamente o seguinte operador predefinido, onde D é o tipo de delegado:

    D operator –(D x, D y);
    

    A semântica é a seguinte:

    • Caso o primeiro operando seja 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, ambos os operandos representam listas de invocação não vazias (§20.2).
      • Se as listas forem iguais, como determinado pelo operador de igualdade do delegado (§12.12.9), o resultado da operação é 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 dele, desde que a lista do segundo operando seja uma sublista do primeiro. (Para determinar a igualdade da sublista, as entradas correspondentes são comparadas como para o operador de igualdade de 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 será o valor do operando da esquerda.

    Nenhuma das listas de 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
        }
    }
    

    exemplo final

As formas elevadas (§12.4.8) dos operadores de subtração predefinidos não elevados definidos acima também são predefinidas.

12.11 Operadores de turnos

Os operadores << e >> são usados para executar operações de transferência de bits.

shift_expression
    : additive_expression
    | shift_expression '<<' additive_expression
    | shift_expression right_shift additive_expression
    ;

Se um operando de um shift_expression tiver o tipo de tempo de compilação dynamic, então a expressão é dinamicamente ligada (§12.3.3). Nesse caso, o tipo de tempo de compilação da expressão é dynamic, e a resolução descrita abaixo ocorrerá em tempo de execução usando o tipo de tempo de execução dos operandos que têm o tipo de tempo de compilação dynamic.

Para uma operação da forma x << count ou 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 para os tipos de parâmetros do operador selecionado, e o tipo do resultado é o tipo de retorno do operador.

Ao declarar um operador de turno sobrecarregado, o tipo do primeiro operando deve ser sempre a classe ou estrutura que contém a declaração do operador, e o tipo do segundo operando deve ser sempre int.

Os operadores de turnos predefinidos estão listados abaixo.

  • Vire para a 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 à esquerda por um número de bits calculados conforme descrito abaixo.

    Os bits de ordem alta 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 de bits vazios de ordem baixa são definidas como zero.

  • Vire para a 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 para a direita por um número de bits calculados 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 vazios de ordem alta são definidas como zero se x não for negativo e definidas como uma 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 vazios de ordem alta são definidas como zero.

Para os operadores predefinidos, o número de bits a deslocar é calculado da seguinte forma:

  • Quando o tipo de x é int ou uint, a contagem de turnos é dada pelos cinco bits de ordem baixa de count. Em outras palavras, a contagem de turnos é calculada a partir de count & 0x1F.
  • Quando o tipo de x é long ou ulong, a contagem de deslocamento é dada pelos seis bits de ordem inferior de count. Em outras palavras, a contagem de turnos é calculada a partir de count & 0x3F.

Se a contagem de turnos resultante for zero, os operadores de turno simplesmente retornarão o valor de x.

As operações de deslocamento nunca causam excessos e produzem os mesmos resultados em contextos verificados e não verificados.

Quando o operando esquerdo do operador de >> é de um tipo integral assinado, o operador executa um aritmético deslocamento para a direita em que o valor do bit mais significativo (o bit de sinal) do operando é propagado para as posições de bit vazio de ordem alta. Quando o operando esquerdo do operador >> é de um tipo integral não assinado, o operador executa um deslocamento de lógico para a direita, em que as posições de bit vazio de alta ordem são sempre definidas como zero. Para executar a operação oposta à inferida do tipo de operando, moldes explícitos podem ser usados.

Exemplo: Se x é uma variável do tipo int, a operação unchecked ((int)((uint)x >> y)) executa um deslocamento lógico para a direita de x. exemplo final

As formas levantadas (§12.4.8) dos operadores de turnos predefinidos não levantados acima definidos também são predefinidas.

12.12 Operadores de ensaios relacionais e de tipo

12.12.1 Generalidades

Os operadores ==, !=, <, >, <=, >=, ise 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
    ;

Nota: A pesquisa para o operando direito do operador is deve primeiro ser testada como um tipo e, depois, como uma expressão que pode abranger múltiplos tokens. No caso em que o operando é um expreesion, a expressão padrão deve ter precedência pelo menos tão alta quanto shift_expression. nota final

O operador is é descrito em §12.12.12 e o operador as é descrito em §12.12.13.

Os operadores de ==, !=, <, >, <= e >= são operadores de comparação .

Se um default_literal (§12.8.21) for usado como um operando de um operador <, >, <=ou >=, ocorrerá um erro em tempo de compilação. Se um default_literal for usado como ambos os operandos de um operador == ou !=, ocorrerá um erro em tempo de compilação. Se um default_literal for usado como o operando esquerdo do operador is ou as, ocorrerá um erro em tempo de compilação.

Se um operando de um operador de comparação tem o tipo de tempo de compilação dynamic, então a expressão é dinamicamente ligada (§12.3.3). Nesse caso, o tipo de tempo de compilação da expressão é dynamic, e a resolução descrita abaixo ocorrerá em tempo de execução usando o tipo de tempo de execução dos operandos que têm o tipo de tempo de compilação dynamic.

Para uma operação do formulário x «op» y, onde «op» é um operador de comparação, a resolução de sobrecarga (§12.4.5) é aplicada para selecionar uma implementação de operador específica. Os operandos são convertidos para os tipos de parâmetros do operador selecionado, e o tipo do resultado é o tipo de retorno do operador. Se ambos os operandos de uma equality_expression forem o literal null, então a resolução de sobrecarga não é realizada e a expressão é avaliada como um valor constante de true ou false, consoante o operador seja == ou !=.

Os operadores de comparação predefinidos são descritos nas subcláusulas 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, false de outra forma
x != y true se x não for igual a y, false de outra forma
x < y true se x for menor que y, false caso contrário
x > y true se x for maior que y, false de outra forma
x <= y true se x for menor ou igual a y, false de outra forma
x >= y true se x for maior ou igual a y, false de outra forma

12.12.2 Operadores de comparação de números inteiros

Os operadores de comparação de inteiros 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 versões elevadas (§12.4.8) dos operadores de comparação de inteiros predefinidos não elevados definidos acima 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 da norma IEC 60559:

Se qualquer operando for NaN, o resultado será false para todos os operadores, exceto !=, para o qual o resultado é true. Para quaisquer dois operandos, x != y sempre produz o mesmo resultado que !(x == y). No entanto, quando um ou ambos os operandos são NaN, os operadores <, >, <=e >= não produzem os mesmos resultados que a negação lógica do operador oposto.

Exemplo: Se qualquer um dos x e y é NaN, então x < y é false, mas !(x >= y) é true. exemplo final

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 < +∞

onde min e max são os menores e maiores valores finitos positivos que podem ser representados no formato de ponto flutuante dado. Efeitos notáveis desta ordenação são:

  • Os zeros negativos e positivos são considerados iguais.
  • Um infinito negativo é considerado menor do 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 levantadas (§12.4.8) dos operadores de comparação de ponto flutuante predefinidos não levantados acima definidos 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 levantadas (§12.4.8) dos operadores de comparação decimal predefinidos não levantados definidos acima também são predefinidas.

12.12.5 Operadores de igualdade booleana

Os operadores de igualdade booleanos predefinidos são:

bool operator ==(bool x, bool y);
bool operator !=(bool x, bool y);

O resultado da == é true se tanto x quanto y são true ou se x e y são false. Caso contrário, o resultado é false.

O resultado da != é false se tanto x quanto y são true ou se x e y são false. Caso contrário, o resultado é true. Quando os operandos são do tipo bool, o operador != produz o mesmo resultado que o operador ^.

As formas levantadas (§12.4.8) dos operadores de igualdade booleanos predefinidos não levantados definidos acima 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, onde 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 que avaliar ((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 levantadas (§12.4.8) dos operadores de comparação de enumeração predefinidos não levantados definidos acima também são predefinidas.

12.12.7 Operadores de igualdade de tipo de referência

Cada tipo de classe C fornece implicitamente 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 de outra forma para C (por exemplo, quando C é string ou System.Delegate).

Os operadores devolvem 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 referem à mesma instância ou são null, enquanto operator != retorna true se e somente se operator == com os mesmos operandos retornarem false.

Para além das regras normais de aplicabilidade (§12.6.4.2), os operadores de igualdade de tipo de referência predefinidos requerem um dos seguintes elementos para serem aplicáveis:

  • Ambos os operandos são um valor de um tipo conhecido por ser um reference_type ou o literal null. Além disso, há uma conversão de identidade ou referência explícita (§10.3.5) de um operando para o tipo do outro operando.
  • Um operando é o literal nulle o outro operando é um valor do tipo T, onde T é um parâmetro de tipo que não é reconhecido como um tipo de valor nem possui a restrição de tipo de valor.
    • Se no tempo de execução T for um tipo de valor não anulável, o resultado de == será false e o resultado de != será true.
    • Se no tempo de execução T for um tipo de valor anulável, o resultado será calculado a partir da propriedade HasValue do operando, conforme descrito em (§12.12.10).
    • Se, no tempo de execução, T for um tipo de referência, o resultado será true se o operando for null, e false caso contrário.

A menos que uma dessas condições seja verdadeira, ocorre um erro de tempo de vinculação.

Nota: As implicações notáveis destas regras são:

  • É um erro de tempo de ligação usar os operadores de igualdade de tipo de referência predefinidos para comparar duas referências que são conhecidas por serem diferentes no momento da ligação. Por exemplo, se os tipos de tempo de ligação dos operandos são dois tipos de classe, e se nenhum deriva do outro, então seria impossível para os dois operandos fazer referência ao mesmo objeto. Assim, a operação é considerada um erro de tempo de ligação.
  • Os operadores de igualdade de tipo de referência predefinidos não permitem que operandos dos tipos de valor sejam comparados (exceto quando os parâmetros de tipo são comparados com null, que é tratado de forma especial).
  • Os operandos de operadores de igualdade de tipo de referência predefinidos nunca são encaixotados. Não faria sentido realizar tais operações de boxe, uma vez que as referências às instâncias em caixa recém-alocadas seriam necessariamente diferentes de todas as outras referências.

Para uma operação da forma x == y ou x != y, se existir algum operator == ou operator != definido pelo utilizador, as regras de resolução de sobrecarga do operador (§12.4.5) selecionarão esse operador em vez do operador de igualdade de tipo 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.

nota final

Exemplo: O exemplo a seguir verifica se um argumento de um tipo de parâmetro sem restrições está null.

class C<T>
{
   void F(T x)
   {
      if (x == null)
      {
          throw new ArgumentNullException();
      }
      ...
   }
}

A construção x == null é permitida mesmo que T possa representar um tipo de valor não anulável, e o resultado é simplesmente definido para ser false quando T é um tipo de valor não anulável.

exemplo final

Para uma operação da forma x == y ou x != y, se existir algum operator == 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.

Nota: É sempre possível selecionar o operador de igualdade predefinido do tipo de referência fazendo a conversão explícita de ambos os operandos para o tipo object. nota final

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 contendo os mesmos caracteres. A primeira comparação gera True porque o operador de igualdade de string predefinido (§12.12.8) é selecionado quando ambos os 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 um dos operandos tem um tipo de tempo de vinculaçã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);
    }
}

produz False porque as conversões criam referências a duas instâncias separadas de valores de int encaixotados.

exemplo final

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 uma das seguintes opções é verdadeira:

  • Ambos os valores são null.
  • Ambos os valores são referências nãonull a cadeias de caracteres que têm comprimentos idênticos e caracteres idênticos em cada posição.

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.

Nota: Conforme descrito no §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. nota final

12.12.9 Delegar operadores de igualdade

Os operadores de igualdade de delegados predefinidos são:

bool operator ==(System.Delegate x, System.Delegate y);
bool operator !=(System.Delegate x, System.Delegate y);

Duas instâncias delegadas são consideradas iguais da seguinte forma:

  • Se uma das instâncias delegadas for null, então elas são iguais se e somente se ambas forem null.
  • Se os delegados tiverem tipos de tempo de execução diferentes, eles nunca serão iguais.
  • Se ambas as instâncias delegadas 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 comprimento, e cada entrada na lista de invocação de uma for igual (conforme definido abaixo) à entrada correspondente, na ordem, na lista de invocação da outra.

As seguintes regras regem a igualdade das entradas na 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áveis externas capturadas são permitidas (mas não obrigatórias) para serem iguais.

Se a resolução de sobrecarga do operador for resolvida para qualquer um dos operadores de igualdade delegada, e os tipos de tempo de ligação de ambos os operandos forem tipos delegados conforme descrito em §20 em vez de System.Delegate, e não houver conversão de identidade entre os tipos de operando do tipo vinculação, ocorrerá um erro de tempo de ligação.

Nota: Esta regra impede comparações que nunca podem considerar valores nãonull como iguais devido a serem referências a instâncias de diferentes tipos de delegados. nota final

12.12.10 Operadores de igualdade entre tipos de valor anulável 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 nenhum operador predefinido ou definido pelo usuário (na forma sem levantamento ou com levantamento) exista para a operação.

Para uma operação de um dos formulários

x == null    null == x    x != null    null != x

onde x é uma expressão de um tipo de valor anulável, se a resolução de sobrecarga do operador (§12.4.5) não conseguir encontrar um operador aplicável, o resultado é calculado a partir da propriedade HasValue de x. Especificamente, as duas primeiras formas são traduzidas em !x.HasValue, e as duas últimas formas são traduzidas em x.HasValue.

12.12.11 Operadores de igualdade de Tupla

Os operadores de igualdade de tupla são aplicados em pares aos elementos dos operandos de tupla em ordem lexical.

Se cada operando x e y de um operador == ou != for classificado como tupla ou como valor de tipo tupla (§8.3.11), o operador é um operador de igualdade de tupla .

Se um operando e for classificado como uma tupla, os elementos e1...en deverão ser os resultados da avaliação das expressões dos elementos da expressão de tupla. Caso contrário, se e for um valor de um tipo de tupla, os elementos devem 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 aridade igual, ou ocorre um erro de tempo de compilação. Para cada par de elementos xi e yi, aplica-se o mesmo operador de igualdade, que produz um resultado de tipo bool, dynamic, um tipo que tenha uma conversão implícita para bool, ou um tipo que defina os operadores true e false.

O operador de igualdade de tuplas x == y é avaliado da seguinte forma:

  • O operando do lado esquerdo x é o 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 forma:
      • Se a comparação produziu um bool então esse é o resultado.
      • Caso contrário, se a comparação produziu um dynamic então o operador false é invocado dinamicamente nele, e o valor bool resultante é negado com o operador de negação lógica (!).
      • Caso contrário, se o tipo de comparação tiver uma conversão implícita para bool, essa conversão é aplicada.
      • Caso contrário, se o tipo de 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, então não ocorre qualquer avaliação adicional, e o resultado do operador de igualdade de tupla é false.
  • Se todas as comparações de elementos resultaram em true, o resultado do operador de igualdade de tupla é true.

O operador de igualdade das tuplas x != y é avaliado da seguinte forma:

  • 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 forma:
      • Se a comparação produziu um bool então esse é o resultado.
      • Caso contrário, se a comparação produziu um dynamic então o operador true é invocado dinamicamente nele, e o valor bool resultante é o resultado.
      • Caso contrário, se o tipo de comparação tiver uma conversão implícita para bool, essa conversão é aplicada.
      • Caso contrário, se o tipo de comparação tiver um operador true, esse operador será invocado e o valor bool resultante será o resultado.
    • Se o bool resultante for true, então nenhuma avaliação adicional ocorre, e o resultado do operador de igualdade de tupla é true.
  • Se todas as comparações de elementos resultarem em false, o resultado do operador de igualdade de tupla é false.

12.12.12 O operador é

Existem duas formas 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.

O operador do tipo is

O operador is-type é utilizado para verificar a compatibilidade do tipo de execução de um objeto com um tipo especificado. A verificação é realizada em tempo de execução. 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 êxito para o tipo T por uma conversão de referência, uma conversão de boxe, uma conversão de unboxing, uma conversão de encapsulamento ou uma conversão de desempacotamento.

A operação é avaliada da seguinte forma:

  1. Se E for uma função anônima ou um grupo de métodos, ocorrerá um erro em tempo de compilação
  2. Se E é o null literal, ou se o valor de E é null, o resultado é false.
  3. Caso contrário:
  4. Seja R o tipo de tempo de execução do E.
  5. O D será derivado do R da seguinte forma:
  6. Se R for um tipo de valor anulável, D é o tipo subjacente de R.
  7. Caso contrário, D é R.
  8. O resultado depende de D e de T da seguinte forma:
  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 existe uma conversão de referência implícita de D para T, ou
    • Ou: D é um tipo de valor e existe uma conversão de boxe de D para 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 é false.

As conversões definidas pelo usuário não são consideradas pelo operador is.

Nota: Como o operador is é avaliado em tempo de execução, todos os argumentos de tipo foram substituídos e não há tipos abertos (§8.4.3) a considerar. nota final

Nota: O operador is pode ser entendido em termos de tipos de tempo de compilação e conversões da seguinte forma, em que C é o tipo de Eem tempo de compilação :

  • Se o tipo de e em tempo de compilação for o mesmo que T, ou se existir uma conversão de referência implícita (§10.2.8), conversão de encaixotamento (§10.2.9), conversão de embrulho (§10.6), ou uma conversão explícita de desempacotamento (§10.6) do tipo de E em tempo de compilação 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 equivale a avaliar E != null.
  • Caso contrário, se existir uma conversão de referência explícita (§10.3.5) ou uma conversão de unboxing (§10.3.7) de C para T, ou se C ou T for um tipo aberto (§8.4.3), então as verificações em tempo de execução, como descrito acima, devem ser feitas.
  • Caso contrário, nenhuma conversão de referência, caixa, encapsulamento ou desembrulhamento de E para o tipo T é possível, e o resultado da operação é false. Um compilador pode implementar otimizações com base no tipo de tempo de compilação.

nota final

12.12.12.2 O operador is-pattern

O operador is-pattern é usado para verificar se o valor calculado por uma expressão corresponde a um padrão dado (§11). A verificação é realizada em tempo de execução. O resultado do operador is-pattern é true se o valor corresponder ao padrão; caso contrário, é falso.

Para uma expressão da forma E is P, onde E é uma expressão relacional do tipo T e P é um padrão, é um erro em tempo de compilação se qualquer um dos seguintes se mantiver:

  • 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 Como operador

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 cast (§12.9.7), o operador as nunca lança uma exceção. Em vez disso, se a conversão indicada não for possível, o valor resultante será null.

Numa operação da forma 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 uma das seguintes situações deve ser verdadeira, caso contrário ocorre um erro em tempo de compilação:

  • Uma identidade (§10.2.2), anulável implícita (§10.2.6), referência implícita (§10.2.8), encaixotamento (§10.2.9), anulável explícita (§10.3.4), referência explícita (§10.3.5), ou wrapping (§8.3.12), existe uma conversão de E para T.
  • O tipo de E ou T é um tipo aberto.
  • E é o null literal.

Se o tipo de E em tempo de compilação não for dynamic, a operação E as T produz o mesmo resultado que

E is T ? (T)(E) : (T)null

Só que E só é avaliada uma vez. Pode-se esperar que um compilador otimize E as T para executar no máximo uma verificação de tipo de tempo de execução, em oposição às duas verificações de tipo de tempo de execução implícitas pela expansão acima.

Se o tipo de E em tempo de compilação for dynamic, ao contrário do operador de conversão, o operador as não é vinculado dinamicamente (§12.3.3). Portanto, a expansão neste caso é:

E is T ? (T)(object)(E) : (T)null

Observe que algumas conversões, como conversões definidas pelo usuário, não são possíveis com o operador as e, em vez disso, devem ser executadas usando expressões de transmissã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, porque possui a restrição de classe. O parâmetro de tipo U de H não é, no entanto; por conseguinte, a utilização do operador de as em H não é permitida.

exemplo final

12.13 Operadores lógicos

12.13.1 Generalidades

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 tem o tipo de tempo de compilação dynamic, então a expressão é dinamicamente ligada (§12.3.3). Nesse caso, o tipo de tempo de compilação da expressão é dynamic, e a resolução descrita abaixo ocorrerá em tempo de execução usando o tipo de tempo de execução dos operandos que têm o tipo de tempo de compilação dynamic.

Para uma operação da forma x «op» y, onde «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 para os tipos de parâmetros do operador selecionado, e o tipo do resultado é o tipo de retorno do operador.

Os operadores lógicos predefinidos são descritos nas subcláusulas 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 & calcula o AND lógico bit a bit dos dois operandos, o operador | calcula o OR lógico bit a bit dos dois operandos e o operador ^ calcula o OR lógico exclusivo bit a bit dos dois operandos. Não é possível haver qualquer transbordamento a partir destas operações.

As formas elevadas (§12.4.8) dos operadores lógicos inteiros predefinidos não elevados definidos acima também são predefinidas.

12.13.3 Operadores lógicos de enumeração

Cada tipo de enumeração E implicitamente fornece 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 x «op» y, onde x e y são expressões de um tipo de enumeração E com um tipo subjacente U, e «op» é um dos operadores lógicos, é exatamente o mesmo que avaliar (E)((U)x «op» (U)y). Em outras palavras, os operadores lógicos do tipo de enumeração simplesmente executam a operação lógica no tipo subjacente dos dois operandos.

As formas levantadas (§12.4.8) dos operadores lógicos de enumeração predefinidos não levantados definidos acima também são predefinidas.

12.13.4 Operadores lógicos booleanos

Os operadores lógicos booleanos predefinidos são:

bool operator &(bool x, bool y);
bool operator |(bool x, bool y);
bool operator ^(bool x, bool y);

O resultado da x & y é true se tanto x como y forem true. Caso contrário, o resultado é false.

O resultado de x | y é true se ou x ou y for true. Caso contrário, o resultado é false.

O resultado da x ^ y é true se x é true e y é false, ou x é false e y é true. Caso contrário, o resultado é false. Quando os operandos são do tipo bool, o operador ^ calcula o mesmo resultado que o operador !=.

12.13.5 & booleanos anuláveis e | operadores

O tipo booleano anulável bool? pode representar três valores, true, falsee null.

Tal como acontece com os outros operadores binários, as formas levantadas dos operadores lógicos & e | (§12.13.4) também são pré-definidas:

bool? operator &(bool? x, bool? y);
bool? operator |(bool? x, bool? y);

A semântica dos operadores elevados & e | é 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

Nota: O tipo bool? é conceitualmente semelhante ao tipo de três valores usado para expressões booleanas em SQL. A tabela acima segue a mesma semântica do SQL, enquanto a aplicação das regras do §12.4.8 aos operadores & e | não. As regras de §12.4.8 já fornecem semântica semelhante à do SQL para o operador levantado de ^. nota final

12.14 Operadores lógicos condicionais

12.14.1 Generalidades

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 apenas se x não estiver false.
  • A operação x || y corresponde à operação x | y, exceto que y é avaliada apenas se x não estiver true.

Nota: A razão pela qual o curto-circuito utiliza as condições «não verdadeiro» e «não falso» é permitir que os operadores condicionais definidos pelo utilizador definam quando se aplica o curto-circuito. 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 || entrariam em curto-circuito. nota final

Se um operando de um operador lógico condicional tem o tipo de tempo de compilação dynamic, então a expressão é dinamicamente ligada (§12.3.3). Nesse caso, o tipo de tempo de compilação da expressão é dynamic, e a resolução descrita abaixo ocorrerá em tempo de execução usando o tipo de tempo de execução dos operandos que têm o tipo de tempo de compilação dynamic.

Uma operação do formulário x && y ou x || y é processada aplicando resolução de sobrecarga (§12.4.5) como se a operação tivesse sido escrita x & y ou x | y. Em seguida,

  • Se a resolução de sobrecarga não conseguir encontrar um único melhor operador, ou se a resolução de sobrecarga selecionar um dos operadores lógicos inteiros predefinidos ou operadores lógicos booleanos anuláveis (§12.13.5), ocorrerá um erro de tempo de ligação.
  • Caso contrário, se o operador selecionado for um dos operadores lógicos booleanos predefinidos (§12.13.4), a operação é processada conforme descrito no §12.14.2.
  • Caso contrário, o operador selecionado é um operador definido pelo usuário e a operação é processada conforme descrito no §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 dos operadores lógicos regulares, as sobrecargas dos operadores lógicos regulares são, com certas restrições, também consideradas sobrecargas dos operadores lógicos condicionais. Isto é descrito mais pormenorizadamente no §12.14.3.

12.14.2 Operadores lógicos condicionais booleanos

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 forma:

  • A operação x && y é avaliada como x ? y : false. Em outras palavras, x é primeiro avaliada e convertida para o tipo bool. Então, se x é true, y é avaliado e convertido para o tipo bool, e isso se torna o resultado da operação. Caso contrário, o resultado da operação é false.
  • A operação x || y é avaliada como x ? true : y. Em outras palavras, x é primeiro avaliada e convertida para o tipo bool. Então, se x é true, o resultado da operação é true. Caso contrário, y é avaliado e convertido para o 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 || forem de tipos que declarem um operator & ou operator |definido pelo usuário aplicável, ambos os itens a seguir devem ser verdadeiros, onde 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. Por outras palavras, o operador deve calcular o AND lógico ou o OR lógico de dois operandos do tipo Te devolver um resultado do tipo T.
  • T deve conter declarações de operator true e operator false.

Um erro de tempo de vinculação ocorre se qualquer um desses requisitos não for satisfeito. Caso contrário, a operação && ou || é avaliada combinando 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), onde T.false(x) é uma invocação do operator false declarado em Te T.&(x, y) é uma invocação do operator &selecionado. Em outras palavras, x é primeiro avaliado e operator false é invocado no resultado para determinar se x é definitivamente falso. Então, se x é definitivamente falso, o resultado da operação é o valor previamente calculado para x. Caso contrário, y é avaliado, e a operator & selecionada é invocada com base no valor previamente calculado para x e o valor calculado para y, para produzir o resultado da operação.
  • A operação x || y é avaliada como T.true(x) ? x : T.|(x, y), onde T.true(x) é uma invocação do operator true declarado em Te T.|(x, y) é uma invocação do operator |selecionado. Em outras palavras, x é primeiro avaliado e operator true é invocado no resultado para determinar se x é definitivamente verdadeiro. Então, se x for definitivamente verdadeira, o resultado da operação é o valor previamente calculado para x. Caso contrário, y é avaliado e a operator | selecionada é invocada sobre o valor previamente calculado para x e o valor calculado para y para produzir o resultado da operação.

Em qualquer uma dessas operações, a expressão dada por x é avaliada apenas uma vez, e a expressão dada por y não é avaliada ou avaliada exatamente uma vez.

12.15 O operador coalescente nulo

O operador ?? é chamado de operador de coalescência nula.

null_coalescing_expression
    : conditional_or_expression
    | conditional_or_expression '??' null_coalescing_expression
    | throw_expression
    ;

Em uma expressão coalescente nula da forma a ?? b, se a é não-null, o resultado é a; caso contrário, o resultado é b. A operação avalia b apenas se a estiver 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 da forma a ?? b ?? c é avaliada como uma ?? (b ?? c). Em termos gerais, uma expressão do formulário E1 ?? E2 ?? ... ?? EN retorna o primeiro dos operandos que não énull, ou null se todos os operandos forem null. exemplo final

O tipo de 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₀, Aou B, onde A é o tipo de a (desde que a tenha um tipo), B é o tipo de b(desde que b tenha um tipo) e A₀ é o tipo subjacente de A se A for um tipo de valor anulável, ou A caso contrário. Especificamente, a ?? b é processado da seguinte forma:

  • Se A existir e não for um tipo de valor anulável ou um tipo de referência, ocorrerá um erro em 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 tempo de execução, a é avaliada primeiro. Se a não é null, a é convertido em dynamic, e isso se torna o resultado. Caso contrário, b é avaliado, e esse torna-se o resultado.
  • Caso contrário, se A existe e é um tipo de valor anulável e uma conversão implícita existe de b para A₀, o tipo de resultado é A₀. Em tempo de execução, a é avaliada primeiro. Se a não for null, a é convertido para o tipo A₀, e isso se torna o resultado. Caso contrário, b é avaliado e convertido para o tipo A₀, e isso se torna o resultado.
  • Caso contrário, se A existir e existir uma conversão implícita de b para A, o tipo de resultado será A. Em tempo de execução, a é avaliada pela primeira vez. Se a não é nulo, a torna-se o resultado. Caso contrário, b é avaliado e convertido para o tipo A, e isso se torna o resultado.
  • Caso contrário, se A existe e é um tipo de valor anulável, b tem um tipo B e existe uma conversão implícita de A₀ para B, o tipo de resultado é B. Em tempo de execução, a é avaliada primeiro. Se a não for null, a é desempacotado para o tipo A₀ e convertido para o tipo B, e este é o resultado. Caso contrário, b é avaliada e torna-se o resultado.
  • Caso contrário, se b tiver um tipo B e existir uma conversão implícita de a para B, o tipo de resultado será B. Em tempo de execução, a é avaliada primeiro. Se a não for null, a é convertido para o tipo B, e isso se torna o resultado. Caso contrário, b é avaliado e passa a ser o resultado.

Caso contrário, a e b são incompatíveis, e ocorre erro a em tempo de compilação.

12.16 O operador de expressão throw

throw_expression
    : 'throw' null_coalescing_expression
    ;

Um throw_expression lança o valor produzido pela avaliação da null_coalescing_expression. A expressão deve ser implicitamente convertível em System.Exception, e o resultado da avaliação da expressão é convertido em System.Exception antes de ser lançada. O comportamento em tempo de execução da avaliação de uma expressão de lançamento é o mesmo que o especificado para uma instrução de lançamento (§13.10.6).

Um throw_expression não possui um tipo. Um throw_expression é convertível para todos os tipos por uma conversão de lançamento implícita .

Uma expressão de lançamento só deve ocorrer nos seguintes contextos sintáticos:

  • Como segundo ou terceiro operando de um operador condicional ternário (?:).
  • Como segundo operando de um operador coalescente nulo (??).
  • Como o corpo de uma lambda ou membro com corpo de expressão.

12.17 Expressões da declaração

Uma expressão de declaração declara uma variável local.

declaration_expression
    : local_variable_type identifier
    ;

local_variable_type
    : type
    | 'var'
    ;

O simple_name_ também é considerado uma expressão de declaração se a simples pesquisa de nomes não encontrou uma declaração associada (§12.8.4). Quando usado como uma expressão de declaração, _ é chamado de simples descarte. É semanticamente equivalente a var _, mas é permitido em mais lugares.

Uma expressão de declaração só deve ocorrer nos seguintes contextos sintáticos:

  • Como um outargument_value em um argument_list.
  • Como um simples descartar _ que compreende o lado esquerdo de uma atribuição simples (§12.21.2).
  • Como um tuple_element em um ou mais tuple_expressionrecursivamente aninhados, o mais externo dos quais compreende o lado esquerdo de uma tarefa de desconstrução. Um deconstruction_expression origina expressões de declaração nesta posição, mesmo que as expressões de declaração não estejam presentes na sintaxe.

Nota: Isto significa que uma expressão de declaração não pode ser colocada entre parênteses. nota final

É um erro que uma variável implicitamente tipada declarada com um declaration_expression seja referenciada dentro do argument_list no lugar onde é declarada.

É um erro que uma variável declarada com um declaration_expression seja referenciada dentro da atribuição de desconstrução em que ocorre.

Uma expressão de declaração que é um descarte simples ou onde o local_variable_type é o identificador é classificada como uma variável digitada implicitamente digitada. A expressão não tem tipo, e o tipo da variável local é inferido com base no contexto sintático da seguinte forma:

  • Em um 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 correspondente da tupla no lado direito da atribuição (após a desconstrução).

Caso contrário, a expressão de declaração é classificada como uma variável com tipo explícito , e o tipo da expressão, bem como a variável declarada, deve ser aquele dado 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 _ introduz esse nome no espaço de declaração variável local mais próximo (§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 digitadas explícita e implicitamente. O tipo inferido de b1 é bool porque esse é o tipo do parâmetro de saída correspondente em M1. O subsequente WriteLine pode aceder a i1 e b1, que foram introduzidos no âmbito envolvente.

A declaração de s2 mostra uma tentativa de usar i2 na chamada aninhada para M, que não é permitida, porque a referência ocorre dentro da lista de argumentos onde i2 foi declarada. Por outro lado, a referência a b2 no argumento final é permitida, porque ocorre após o final da lista de argumentos aninhados onde b2 foi declarado.

A declaração de s3 mostra o uso de expressões de declaração tipadas implicitamente e explicitamente que são descartadas. Como os descartes não declaram uma variável nomeada, as múltiplas ocorrências do identificador _ são permitidas.

(int i1, int _, (var i2, var _), _) = (1, 2, (3, 4), 5);

Este exemplo mostra o uso de expressões de declaração digitadas implícita e explicitamente para variáveis e descartes em uma atribuição de desconstrução. 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 demonstra o uso de var _ para fornecer um descarte de tipo implícito quando _ não está disponível, porque designa uma variável no escopo envolvente.

exemplo final

12.18 Operador condicional

O operador ?: é chamado de operador condicional. Às vezes também é chamado 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 throw (§12.16) não é permitida em um operador condicional se ref estiver presente.

Uma expressão condicional do formulário b ? x : y primeiro avalia a condição b. Então, se b é true, x é avaliado e se torna o resultado da operação. Caso contrário, y é avaliado e torna-se 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 da forma a ? b : c ? d : e é avaliada como a ? b : (c ? d : e). exemplo final

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 implemente operator true. Se nenhum desses requisitos for atendido, ocorrerá um erro em tempo de compilação.

Se ref estiver presente:

  • Deve existir uma conversão de identidade entre os tipos das duas referências de variável , e o tipo de resultado pode ser qualquer um dos tipos. Se qualquer um dos tipos for dynamic, a inferência de tipo prefere dynamic (§8.7). Se um dos tipos for um tipo de tupla (§8.3.11), a inferência de tipo inclui os nomes dos elementos, quando estes nomes na mesma posição ordinal coincidem em ambas as tuplas.
  • O resultado é uma referência variável, que pode ser escrita se ambas as referências variáveis eforem graváveis.

Nota: Quando ref está presente, o conditional_expression retorna uma referência de variável, que pode ser atribuída a uma variável de referência usando o operador = ref ou passada como um parâmetro de referência/entrada/saída. nota final

Se ref não estiver presente, o segundo e terceiro operandos, x e y, do operador ?: controlam o tipo da expressão condicional:

  • Se x tem tipo X e y tem tipo Y então,
    • Se existir uma conversão de identidade entre X e Y, então o resultado é o melhor tipo comum de um conjunto de expressões (§12.6.3.15). Se qualquer um dos tipos for dynamic, a inferência de tipo prefere dynamic (§8.7). Se um dos tipos for um tipo de tupla (§8.3.11), a inferência de tipo inclui os nomes dos elementos quando os nomes dos elementos na mesma posição ordinal coincidem em ambas as tuplas.
    • Caso contrário, se uma conversão implícita (§10.2) existe de X para Y, mas não de Y para X, então Y é 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, então Y é 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, então X é o tipo da expressão condicional.
    • Caso contrário, se uma conversão implícita (§10.2) existe de Y para X, mas não de X para Y, então X é o tipo da expressão condicional.
    • Caso contrário, nenhum tipo de expressão pode ser determinado e ocorre um erro em tempo de compilação.
  • Se apenas um dos x e y tem um tipo, e tanto x quanto y são implicitamente conversíveis para esse tipo, então esse é o tipo da expressão condicional.
  • Caso contrário, nenhum tipo de expressão pode ser determinado e ocorre um erro em tempo de compilação.

O processamento em tempo de execução de uma expressão condicional ref do formulário b ? ref x : ref y consiste nas seguintes etapas:

  • Primeiro, b é avaliado e o valor bool de b é determinado:
    • Se existir uma conversão implícita do tipo de b para bool, então essa conversão implícita é realizada para produzir um valor bool.
    • Caso contrário, o operator true definido pelo tipo de b é invocado para produzir um valor bool.
  • Se o valor bool produzido pela etapa acima for true, então x é avaliado e a referência da variável resultante torna-se o resultado da expressão condicional.
  • Caso contrário, y é avaliada e a referência da variável resultante torna-se o resultado da expressão condicional.

O processamento em tempo de execução de uma expressão condicional do formulário b ? x : y consiste nas seguintes etapas:

  • Primeiro, b é avaliado e o valor bool de b é determinado:
    • Se existir uma conversão implícita do tipo de b para bool, então essa conversão implícita é realizada para produzir um valor bool.
    • Caso contrário, o operator true definido pelo tipo de b é invocado para produzir um valor bool.
  • Se o valor bool produzido pela etapa acima for true, então x é avaliado e convertido para o tipo da expressão condicional, e isso se torna o resultado da expressão condicional.
  • Caso contrário, y é avaliada e convertida para o tipo da expressão condicional, e isso se torna o resultado da expressão condicional.

12.19 Expressões de função anónimas

12.19.1 Generalidades

Uma função anônima é uma expressão que representa uma definição de método inline. Uma função anônima não tem um valor ou tipo em si, mas é conversível em um delegado compatível ou tipo de árvore de expressão. A avaliação de uma conversão de função anônima depende do tipo de destino da conversão: Se for um tipo delegado, a conversão será avaliada para um valor de delegado fazendo referência ao método que a função anônima define. Se for um tipo de árvore de expressão, a conversão resulta numa árvore de expressão que representa a estrutura do método como uma estrutura de objeto.

Nota: Por razões históricas, existem dois sabores sintáticos de funções anónimas, a saber, lambda_expressions e anonymous_method_expressions. Para quase todos os efeitos, lambda_expressions são mais concisos e expressivos do que anonymous_method_expressions, que permanecem na linguagem para retrocompatibilidade. nota final

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 reconhecer um anonymous_function_body, se tanto a null_conditional_invocation_expression como a alternativa de expressão forem aplicáveis, então deve ser escolhida a primeira.

Nota: A sobreposição e a prioridade entre as alternativas aqui é apenas por conveniência descritiva; as regras gramaticais poderiam ser elaboradas para eliminar a sobreposição. ANTLR, e outros sistemas gramaticais, adotam a mesma conveniência e, portanto, anonymous_function_body tem a semântica especificada automaticamente. nota final

Nota: Quando tratada como uma expressão , uma forma sintática como x?.M() seria um erro se o tipo de resultado de M for void (§12.8.13). Mas quando tratado como um null_conditional_invocation_expression, o tipo de resultado pode ser void. nota final

Exemplo: O tipo de resultado de List<T>.Reverse é void. No código a seguir, o corpo da expressão anónima é um null_conditional_invocation_expression, portanto, não é um erro.

Action<List<int>> a = x => x?.Reverse();

exemplo final

O operador => tem a mesma precedência que a atribuição (=) e é associativo à direita.

Uma função anónima com o modificador async é uma função assíncrona e segue as regras descritas no §15.15.

Os parâmetros de uma função anônima na forma de um lambda_expression podem ser digitados explícita ou implicitamente. Em uma lista de parâmetros explicitamente digitada, o tipo de cada parâmetro é explicitamente declarado. Em uma lista de parâmetros digitada implicitamente, os tipos dos parâmetros são inferidos a partir do contexto em que a função anônima ocorre — especificamente, quando a função anônima é convertida em um tipo de delegado compatível ou tipo de árvore de expressão, esse tipo fornece os tipos de parâmetro (§10.7).

Em um lambda_expression com um único parâmetro digitado implicitamente, os parênteses podem ser omitidos da lista de parâmetros. Por outras palavras, uma função anónima da forma

( «param» ) => «expr»

pode ser abreviado para

«param» => «expr»

A lista de parâmetros de uma função anônima na forma de um anonymous_method_expression é opcional. Se forem indicados, os parâmetros devem ser explicitamente dactilografados. Caso contrário, a função anônima é convertível em 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 está sempre atingível (§13.2).

Exemplo: Seguem-se alguns exemplos de funções anónimas:

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

exemplo final

O comportamento de lambda_expressions e anonymous_method_expressions é o mesmo, exceto para os seguintes pontos:

  • anonymous_method_expressions permitem a omissão completa da lista de parâmetros, permitindo a conversão para tipos delegados de qualquer lista de parâmetros de valor.
  • lambda_expressions permitem que tipos de parâmetros sejam omitidos e inferidos, enquanto anonymous_method_expressions exigem que os tipos de parâmetros sejam explicitamente declarados.
  • O corpo de um lambda_expression pode ser uma expressão ou um bloco, enquanto o corpo de um anonymous_method_expression deve ser um bloco.
  • Apenas lambda_expressions têm conversões para tipos de árvore de expressão compatíveis (§8.6).

12.19.2 Assinaturas de funções anónimas

O 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 âmbito dos parâmetros da função anónima é o anonymous_function_body (§7.7). Juntamente 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 em tempo de compilação que o nome de um parâmetro de uma função anónima coincida com o nome de uma variável local, constante local, ou parâmetro cujo âmbito inclua a anonymous_method_expression ou a lambda_expression.

Se uma função anônima tiver um explicit_anonymous_function_signature, o conjunto de tipos delegados compatíveis e tipos de árvore de expressão será restrito àqueles que têm os mesmos tipos de parâmetros e modificadores na mesma ordem (§10.7). Em contraste com as conversões de grupo de método (§10.8), não há suporte para contravariância de tipos de parâmetros de função anônimos. Se uma função anônima não tiver um anonymous_function_signature, o conjunto de tipos delegados compatíveis e tipos de árvore de expressão será restrito àqueles que não têm parâmetros de saída.

Observe que um anonymous_function_signature não pode incluir atributos ou uma matriz de parâmetros. No entanto, um anonymous_function_signature pode ser compatível com um tipo delegado cuja lista de parâmetros contém uma matriz de parâmetros.

Observe também que a conversão para um tipo de árvore de expressão, mesmo que compatível, ainda pode falhar em tempo de compilação (§8.6).

12.19.3 Corpos de funções anónimas

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 pode ser convertida em um tipo delegado ou tipo de expressão com parâmetros (§10.7), mas os parâmetros não podem ser acessados no corpo.
  • Exceto para parâmetros por referência especificados na assinatura (se houver) da função anônima anexa mais próxima, é um erro em tempo de compilação para o corpo acessar um parâmetro por referência.
  • Exceto para os parâmetros especificados na assinatura (se houver) da função anônima anexa mais próxima, é um erro em tempo de compilação para o corpo acessar um parâmetro de um tipo de ref struct.
  • Quando o tipo de this é um tipo struct, é um erro de compilação quando o corpo tenta aceder a this. Isso é verdadeiro se o acesso for explícito (como em this.x) ou implícito (como em x onde x é um membro de instância do struct). Esta regra simplesmente proíbe esse acesso e não afeta se a pesquisa de membros resulta 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 em que o lambda_expression ou anonymous_method_expression é avaliado (§12.19.7).
  • É um erro em tempo de compilação para o corpo conter uma instrução goto, uma instrução break ou uma instrução continue cujo alvo 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 de fechamento mais próxima, não do membro da função que o inclui.

Não é especificado de forma explícita se existe alguma maneira de executar o bloco de uma função anónima, exceto através da avaliação e invocação da lambda_expression ou anonymous_method_expression. Em particular, um compilador pode optar por implementar uma função anônima sintetizando um ou mais métodos ou tipos nomeados. Os nomes de tais elementos sintetizados devem ser de uma forma reservada para uso do compilador (§6.4.3).

12.19.4 Resolução de sobrecarga

As funções anónimas numa lista de argumentos participam na inferência de tipos e na resolução de sobrecargas. Consulte §12.6.3 e §12.6.4 para obter as regras exatas.

Exemplo: O exemplo a seguir ilustra o efeito de 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 utiliza um argumento selector, que extrai o valor a ser somado sobre os itens de uma lista. O valor extraído pode ser um int ou um double e a soma resultante é igualmente um int ou um double.

Os métodos Sum poderiam, por exemplo, ser usados para calcular somas a partir de uma lista de linhas de detalhe de 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, ambos os 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, apenas 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.

exemplo final

12.19.5 Funções anónimas e ligação dinâmica

Uma função anônima não pode ser um recetor, argumento ou operando de uma operação dinamicamente vinculada.

12.19.6 Variáveis externas

12.19.6.1 Generalidades

Qualquer variável local, parâmetro de valor ou matriz de parâmetros cujo escopo inclui a lambda_expression ou anonymous_method_expression é chamada de variável externa da função anônima. Em um membro da função de instância de uma classe, este 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á associada (§9.2.9.1). No entanto, o tempo de vida de uma variável externa capturada é estendido pelo menos até que o delegado ou a árvore de 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 do x é estendido pelo menos até que o delegado retornado de F se torne elegível 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

exemplo final

Quando uma variável local ou um parâmetro de valor é capturado por uma função anónima, a variável ou parâmetro local deixa de ser considerada uma variável fixa (§23.4), passando a ser considerada uma variável móvel. No entanto, as 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.

Nota: 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. nota final

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 para 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;
        ...
    }
}

exemplo final

Quando não capturada, não há como observar exatamente com que frequência uma variável local é instanciada—como os tempos de vida das instanciações são separados, é possível que cada instantaçã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 tornam-se 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 ciclo:

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 delegada (§10.7.2).

exemplo final

Se um for-loop declara uma variável de iteração, essa variável em si é considerada declarada fora do loop.

Exemplo: Assim, se o exemplo for alterado para capturar a própria variável de iteração:

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 é capturada, o que produz a saída:

3
3
3

exemplo final

É possível que delegados de função anônimos 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 instâncias separadas de y, e o resultado é:

1 1
2 1
3 1

exemplo final

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 podem assim "comunicar" através dessa variável. O resultado do exemplo é:

5
10

exemplo final

12.19.7 Avaliação de expressões de função anónimas

Uma função anónima F deve ser sempre convertida num tipo delegado D ou num tipo de árvore de expressões E, quer diretamente, quer através da execução de uma expressão de criação delegada new D(F). Esta conversão determina o resultado da função anónima, conforme descrito no §10.7.

12.19.8 Exemplo de implementação

Esta subcláusula é informativa.

Esta subcláusula descreve uma possível implementação de conversões de função anônimas em termos de outras construções C#. A implementação descrita aqui é baseada nos mesmos princípios usados por um compilador C# comercial, mas não é de forma alguma uma implementação obrigatória, nem é a única possível. Ele menciona apenas brevemente as conversões para árvores de expressão, pois sua semântica exata está fora do escopo desta especificação.

O restante desta subcláusula dá vários exemplos de código que contém funções anônimas com características diferentes. Para cada exemplo, uma tradução correspondente para código que usa apenas outras construções C# é fornecida. Nos exemplos, o identificador D é assumido 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 variáveis externas:

delegate void D();

class Test
{
    static void F()
    {
        D d = () => Console.WriteLine("test");
    }
}

Isso pode ser traduzido para uma instanciação delegada 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 a seguir, a função anônima faz referência a membros da 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 contendo 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 pelo menos o tempo de vida do delegado de função anônimo. Isso pode ser conseguido promovendo 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 então à 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 seguinte função anônima 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 no qual as variáveis locais são capturadas, de modo que possam ter tempos de vida independentes nos diferentes blocos. 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 de delimitação. Com essas estruturas de dados, é possível alcançar todas as variáveis externas capturadas através de uma instância de __Local2, e 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 variáveis locais também pode ser usada ao converter funções anônimas em árvores de expressão: as referências aos objetos gerados pelo compilador podem ser armazenadas na árvore de expressões 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 "levantadas" sejam compartilhadas entre delegados e árvores de expressão.

Fim do texto informativo.

12.20 Expressões de consulta

12.20.1 Generalidades

Expressões de consulta fornecem uma sintaxe integrada à linguagem para consultas, semelhante 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_clauses? select_or_group_clause query_continuation?
    ;

query_body_clauses
    : query_body_clause
    | query_body_clauses query_body_clause
    ;

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 inicial from pode ser seguida de 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 sobre os elementos de uma sequência . Cada cláusula let introduz uma variável de intervalo que representa um valor calculado por meio de variáveis de intervalo anteriores. Cada cláusula where é um filtro que exclui itens do resultado. Cada cláusula join compara chaves especificadas da sequência de origem com chaves de outra sequência, produzindo pares correspondentes. Cada cláusula orderby reordena itens de acordo com critérios especificados. A cláusula final select ou group especifica a forma do resultado em termos das variáveis de intervalo. Finalmente, uma cláusula into pode ser usada para "emendar" consultas, tratando o resultado de uma consulta como um gerador numa consulta posterior.

12.20.2 Ambiguidades nas 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 poderiam surgir do uso desses identificadores como palavras-chave e nomes simples, esses 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), caso em que são considerados identificadores. Para este propósito, uma expressão de consulta é qualquer expressão que começa com "fromidentificador" seguido por qualquer token, exceto ";", "=" ou ",".

12.20.3 Tradução de expressões de consulta

12.20.3.1 Generalidades

A linguagem C# não especifica a semântica de execução de expressões de consulta. Em vez disso, as expressões de consulta são traduzidas em invocações de métodos que aderem ao padrão query-expression (§12.20.4). Especificamente, as expressões de consulta são traduzidas em invocações de métodos denominados Where, Select, SelectMany, Join, GroupJoin, OrderBy, OrderByDescending, ThenBy, ThenByDescending, GroupBye Cast. Espera-se que esses métodos tenham assinaturas e tipos de retorno específicos, conforme descrito no §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 conversão de expressões de consulta para invocações de método é um mapeamento sintático que ocorre antes de qualquer ligação de tipo ou resolução de sobrecarga ter sido executada. Após a tradução de expressões de consulta, as invocações de método resultantes são processadas como invocações de método regulares, e isso, por sua vez, pode revelar erros de tempo de compilação. Essas condições de erro incluem, mas não estão limitadas a, métodos que não existem, argumentos dos tipos errados e métodos genéricos onde a inferência de tipo falha.

Uma expressão de consulta é processada aplicando repetidamente as seguintes traduções até que não sejam possíveis mais reduções. As traduções são listadas por ordem de aplicação: cada seção assume que as traduções nas seções anteriores foram realizadas exaustivamente e, uma vez esgotadas, uma seção não será revisitada posteriormente no processamento da mesma expressão de consulta.

É um erro em tempo de compilação quando uma expressão de consulta inclui uma atribuição a uma variável de alcance ou o uso de uma variável de alcance como argumento para um parâmetro de referência ou de saída.

Certas traduções injetam variáveis de intervalo com identificadores transparentes indicados por *. Estes são descritos mais detalhadamente 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 após o corpo da consulta

from «x1» in «e1» «b1» into «x2» «b2»

é traduzido em

from «x2» in ( from «x1» in «e1» «b1» ) «b2»

As traduções nas seções a seguir assumem 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() }

é traduzido em:

from g in
   (from c in customers
   group c by c.Country)
select new { Country = g.Key, CustCount = g.Count() }

cuja tradução final é:

customers.
GroupBy(c => c.Country).
Select(g => new { Country = g.Key, CustCount = g.Count() })

exemplo final

12.20.3.3 Tipos explícitos de variáveis de intervalo

Uma cláusula from que especifica explicitamente um tipo de variável de intervalo

from «T» «x» in «e»

é traduzido 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»

é traduzido em

join «x» in ( «e» ) . Cast < «T» > ( ) on «k1» equals «k2»

As traduções nas seções a seguir assumem que as consultas não têm tipos explícitos de variáveis de intervalo.

Exemplo: O exemplo

from Customer c in customers
where c.City == "London"
select c

é traduzido 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")

exemplo final

Nota: Os tipos de variáveis de intervalo explícitos são úteis para consultar coleções que implementam a interface IEnumerable não genérica, mas não a interface IEnumerable<T> genérica. No exemplo acima, tal seria o caso se os clientes fossem do tipo ArrayList. nota final

12.20.3.4 Expressões de consulta degeneradas

Uma expressão de consulta do formulário

from «x» in «e» select «x»

é traduzido em

( «e» ) . Select ( «x» => «x» )

Exemplo: O exemplo

from c in customers
select c

é traduzido em

(customers).Select(c => c)

fim do exemplo

Uma expressão de consulta degenerada é aquela que seleciona trivialmente os elementos da fonte.

Nota: As fases posteriores da tradução (§12.20.3.6 e §12.20.3.7) removem consultas degeneradas introduzidas por outras etapas de tradução, substituindo-as pela sua fonte. É importante, no entanto, garantir que o resultado de uma expressão de consulta nunca seja o objeto de origem em si. Caso contrário, retornar o resultado de tal consulta pode expor inadvertidamente dados privados (por exemplo, uma matriz de elementos) a um chamador. Portanto, esta etapa protege consultas degeneradas escritas diretamente no código-fonte, chamando explicitamente Select na fonte. Em seguida, cabe aos implementadores de Select e outros operadores de consulta garantir que esses métodos nunca retornem o objeto de origem em si. nota final

12.20.3.5 De, deixar, onde, juntar-se e ordenar por cláusulas

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»

é traduzido 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 }

é traduzido em

(customers).
SelectMany(c => c.Orders,
(c,o) => new { c.Name, o.OrderID, o.Total }
)

exemplo final

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 de corpo de consulta:

from «x1» in «e1»
from «x2» in «e2»
Q

é traduzido 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 }

é traduzido 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, de outra forma, é invisível e inacessível.

exemplo final

Uma expressão let e a sua cláusula from anterior:

from «x» in «e»  
let «y» = «f»  
...

é traduzido 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 }

é traduzido 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, de outra forma, é invisível e inacessível.

exemplo final

Uma expressão where juntamente com a cláusula from anterior:

from «x» in «e»  
where «f»  
...

é traduzido em

from «x» in ( «e» ) . Where ( «x» => «f» )  
...

Uma cláusula join imediatamente seguida de uma cláusula select

from «x1» in «e1»  
join «x2» in «e2» on «k1» equals «k2»  
select «v»

é traduzido como

( «e1» ) . Join( «e2» , «x1» => «k1» , «x2» => «k2» , ( «x1» , «x2» ) => «v» )

Exemplo: O exemplo

from c in customersh
join o in orders on c.CustomerID equals o.CustomerID
select new { c.Name, o.OrderDate, o.Total }

é traduzido em

(customers).Join(
   orders,
   c => c.CustomerID, o => o.CustomerID,
   (c, o) => new { c.Name, o.OrderDate, o.Total })

exemplo final

Uma cláusula join seguida de uma cláusula do corpo da consulta:

from «x1» in «e1»  
join «x2» in «e2» on «k1» equals «k2»  
...

é traduzido em

from * in ( «e1» ) . Join(  
«e2» , «x1» => «k1» , «x2» => «k2» ,
( «x1» , «x2» ) => new { «x1» , «x2» })  
...

Uma cláusula join-into imediatamente seguida de uma cláusula select

from «x1» in «e1»  
join «x2» in «e2» on «k1» equals «k2» into «g»  
select «v»

é traduzido em

( «e1» ) . GroupJoin( «e2» , «x1» => «k1» , «x2» => «k2» ,
                     ( «x1» , «g» ) => «v» )

Uma cláusula join into seguida por uma cláusula de corpo de consulta

from «x1» in «e1»  
join «x2» in «e2» on «k1» equals «k2» into *g»  
...

é traduzido 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 }

é traduzido 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 })

onde x e y são identificadores gerados pelo compilador que, de outra forma, são invisíveis e inacessíveis.

exemplo final

Uma cláusula orderby e a sua cláusula from precedente:

from «x» in «e»  
orderby «k1» , «k2» , ... , «kn»  
...

é traduzido 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

possui a tradução final

(orders)
    .OrderBy(o => o.Customer.Name)
    .ThenByDescending(o => o.Total)

exemplo final

As traduções a seguir pressupõem que não há let, where, join ou orderby cláusulas e não mais do que uma cláusula from inicial em cada expressão de consulta.

12.20.3.6 Selecionar cláusulas

Uma expressão de consulta do formulário

from «x» in «e» select «v»

é traduzido em

( «e» ) . Select ( «x» => «v» )

exceto quando «v» é o identificador «x», a tradução é simplesmente

( «e» )

Exemplo: O exemplo

from c in customers.Where(c => c.City == "London")
select c

é simplesmente traduzido em

(customers).Where(c => c.City == "London")

exemplo final

12.20.3.7 Cláusulas de grupo

Uma cláusula group

from «x» in «e» group «v» by «k»

é traduzido em

( «e» ) . GroupBy ( «x» => «k» , «x» => «v» )

exceto quando «v» é o identificador «x», a tradução é

( «e» ) . GroupBy ( «x» => «k» )

Exemplo: O exemplo

from c in customers
group c.Name by c.Country

é traduzido em

(customers).GroupBy(c => c.Country, c => c.Name)

exemplo final

12.20.3.8 Identificadores transparentes

Certas traduções injetam variáveis de intervalo com identificadores transparentes indicados por *. Os identificadores transparentes existem apenas como uma etapa intermediária no processo de tradução de expressões de consulta.

Quando uma conversão de consulta injeta um identificador transparente, outras etapas de tradução propagam o identificador transparente em funções anônimas e inicializadores de objeto anônimos. Nesses contextos, os identificadores transparentes têm o seguinte comportamento:

  • Quando um identificador transparente ocorre como um parâmetro numa função anónima, os membros do tipo anónimo associado estão automaticamente à disposição no 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 tradução descritas acima, identificadores transparentes são sempre introduzidos juntamente com 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 para agrupar várias variáveis de intervalo. Os exemplos de tradução a seguir assumem que tipos anônimos são usados e mostram uma possível traduçã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 }

é traduzido em

from * in (customers).SelectMany(c => c.Orders, (c,o) => new { c, o })
orderby o.Total descending
select new { c.Name, o.Total }

que é posteriormente traduzido em

customers
    .SelectMany(c => c.Orders, (c,o) => new { c, o })
    .OrderByDescending(* => o.Total)
    .Select(\* => new { c.Name, o.Total })

que, quando os 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, de outra forma, é 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 }

é traduzido 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 })

onde x e y são identificadores gerados pelo compilador que, de outra forma, são invisíveis e inacessíveis. exemplo final

12.20.4 O padrão de expressão de consulta

O padrão Query-expression estabelece um padrão de métodos que os tipos podem implementar para dar suporte a expressões de consulta.

Um tipo genérico C<T> suporta o padrão de expressão de consulta se os seus métodos de membro públicos e os métodos de extensão publicamente acessíveis puderem ser substituídos pela seguinte definição de classe. A "forma" de um tipo genérico C<T>é definida pelos membros e métodos de extensão acessíveis. Um tipo genérico é usado para ilustrar as relações adequadas entre os tipos de parâmetro e retorno, mas também é possível implementar o padrão para 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 eles poderiam igualmente ter usado outros tipos delegados ou de árvore de expressão com as mesmas relações em tipos de parâmetro e retorno.

Nota: 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 uma OrderBy ou OrderByDescending. nota final

Nota: A forma recomendada do resultado de GroupBy—uma sequência de sequências, onde cada sequência interna tem uma propriedade Key adicional. nota final

Nota: Como as expressões de consulta são traduzidas para invocações de método por meio de um mapeamento sintático, os tipos têm flexibilidade considerável em como implementam qualquer um ou todo o padrão de 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 delegados ou árvores de expressão porque funções anônimas são conversíveis para ambos. Os tipos que implementam apenas parte do padrão de expressão de consulta suportam apenas traduções de expressões de consulta mapeadas para os métodos suportados pelo tipo. nota final

Observação: O namespace System.Linq fornece uma implementação do padrão query-expression para qualquer tipo que implemente a interface System.Collections.Generic.IEnumerable<T>. nota final

12.21 Operadores de atribuição

12.21.1 Generalidades

Todos os operadores de atribuição, exceto um, atribuem um novo valor a uma variável, uma propriedade, um evento ou um 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, exceto para = ref, um acesso a uma propriedade, um acesso a um indexador, um acesso a um evento ou uma tupla. Uma expressão de declaração não é diretamente permitida 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 valores do operando direito à variável, propriedade, elemento indexador ou elementos de tupla fornecidos pelo operando esquerdo. O operando esquerdo do operador de atribuição simples não deve ser um acesso a um evento (exceto conforme descrito no §15.8.2). O operador de atribuição simples é descrito no §12.21.2.

O operador = ref é designado como operador de atribuição ref . Faz do operando direito, que deve ser um variable_reference (§9.5), o referente da variável de referência designada pelo operando esquerdo. O operador de atribuição ref é descrito no §12.21.3.

Os operadores de atribuição diferentes dos operadores = e = ref são chamados de operadores de atribuição compostos . Esses operadores executam a operação indicada nos dois operandos e, em seguida, atribuem o valor resultante à variável, propriedade ou elemento indexador fornecido pelo operando esquerdo. Os operadores de atribuição compostos são descritos em §12.21.4.

Os operadores += e -= com uma expressão de acesso a eventos como operando esquerdo são chamados de operadores de atribuição a eventos . Nenhum outro operador de atribuição é válido com um acesso a eventos como o operando esquerdo. Os operadores de atribuição de eventos são descritos em §12.21.5.

Os operadores de atribuição têm associação à direita, o que significa que as operações são agrupadas da direita para a esquerda.

Exemplo: Uma expressão da forma a = b = c é avaliada como a = (b = c). exemplo final

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 da forma E.P ou E[Ei] em que E tem o tipo de tempo de compilação dynamic, então a atribuição é vinculada dinamicamente (§12.3.3). Nesse caso, o tipo de tempo de compilação da expressão de atribuição é dynamic, e a resolução descrita abaixo ocorrerá em tempo de execução com base no tipo de tempo de execução do E. Se o operando esquerdo for da forma E[Ei] em que pelo menos um elemento de Ei tem o tipo de tempo de compilação dynamic, e o tipo de tempo de compilação de E não é uma matriz, o acesso do indexador resultante é vinculado dinamicamente, mas com verificação limitada em tempo de compilação (§12.6.5).

Uma atribuição simples onde o operando esquerdo é classificado como uma tupla também é chamada de desconstruindo a atribuição. Se qualquer um dos elementos de tupla do operando esquerdo tiver um nome de elemento, ocorrerá um erro em tempo de compilação. Se algum dos elementos da tupla do operando esquerdo for um declaration_expression e algum outro elemento não for um 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 a x de y, que é determinado recursivamente da seguinte forma:

  • Se x é uma expressão de tupla (x1, ..., xn), e y pode ser desconstruída para uma expressão de tupla (y1, ..., yn) com n elementos (§12.7), e cada atribuição a xi de yi tem o tipo Ti, então a atribuição tem o tipo (T1, ..., Tn).
  • Caso contrário, se x é classificada como uma variável, a variável não é readonly, x tem um tipo T, e y tem uma conversão implícita para T, então a atribuição tem o tipo T.
  • Caso contrário, se x é classificada como uma variável digitada implicitamente (ou seja, uma expressão de declaração digitada implicitamente) e y tem um tipo T, então o tipo inferido da variável é T, e a atribuição tem o tipo T.
  • Caso contrário, se x for classificado como uma propriedade ou acesso indexador, a propriedade ou indexador tiver um acessador de conjunto acessível, x tiver um tipo Te y tiver uma conversão implícita para T, a atribuição terá o tipo T.
  • Caso contrário, a atribuição não é válida e ocorre um erro de tempo de vinculação.

O processamento em tempo de execução de uma atribuição simples do formulário x = y com o tipo T é realizado como uma atribuição para x de y com o tipo T, que consiste nas seguintes etapas recursivas:

  • x é avaliado se ainda não era.
  • Se x é classificada como variável, y é avaliada e, se necessário, convertida em T através de uma conversão implícita (§10.2).
    • Se a variável fornecida por x for um elemento de matriz de um reference_type, uma verificação em tempo de execução será executada para garantir que o valor calculado para y seja compatível com a instância de matriz da qual x é um elemento. A verificação será bem-sucedida se y for null, ou se existir uma conversão de referência implícita (§10.2.8) do 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 é lançada.
    • O valor resultante da avaliação e conversão de y é armazenado no local determinado pela avaliação de xe é retornado como resultado da atribuição.
  • Se o x foi classificado como uma propriedade ou acesso de indexador:
    • y é avaliada e, se necessário, convertida em T através de uma conversão implícita (§10.2).
    • O acessor de conjunto de x é invocado com o valor resultante da avaliação e conversão de y como argumento de valor.
    • O valor resultante da avaliação e conversão de y é gerado como resultado da atribuição.
  • Se x é classificado como uma tupla (x1, ..., xn) com aridade n:
    • y é desconstruída com n elementos para uma expressão de tupla e.
    • Um tuplo de resultado t é criado ao converter e em T usando uma conversão implícita de tuplo.
    • Para cada xi, em ordem da esquerda para a direita, é feita uma atribuição de t.Itemi a xi, exceto que os xi não são avaliados novamente.
    • t é gerada como resultado da atribuição.

Nota: se o tipo de tempo de compilação do x for dynamic e houver uma conversão implícita do tipo de tempo de compilação de y para dynamic, nenhuma resolução de tempo de execução será necessária. nota final

Nota: 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 em tempo de execução para garantir que o valor que está sendo 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 um System.ArrayTypeMismatchException seja lançado porque uma referência a um ArrayList não pode ser armazenada num elemento de um string[].

nota final

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 ligação.

Nota: Por causa do §12.8.7, a mesma regra também se aplica aos campos. nota final

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.Ae 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, uma vez que r.A e r.B não são variáveis.

exemplo final

12.21.3 Atribuição de referência

O operador = ref é conhecido como o operador de atribuição ref .

O operando esquerdo deve ser uma expressão que se liga a uma variável de referência (§9.7), a um parâmetro de referência (diferente de this), a um parâmetro de saída ou a um parâmetro de entrada. O operando direito deve ser uma expressão que produz um variable_reference designando um valor do mesmo tipo que o operando esquerdo.

É um erro de tempo de compilação se o ref-safe-context (§9.7.2) do operando esquerdo for mais amplo do que o ref-safe-context do operando direito.

O operando direito deve ser definitivamente atribuído no ponto da atribuição de ref.

Quando o operando esquerdo se liga a um parâmetro de saída, é 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 uma referência gravável (ou seja, designar qualquer coisa que não seja um parâmetro local ou de entrada ref readonly), então o operando direito deve ser uma variável de referência gravável. Se a variável do operando direito for modificável, o operando esquerdo pode ser uma referência modificável ou apenas de leitura.

A operação torna o operando esquerdo um alias da variável do operando direito. O alias pode ser tornado somente leitura mesmo se a variável do operando direito for gravável.

O operador de atribuição de referência produz uma referência_de_variável do tipo atribuído. É editável se o operando esquerdo for editável.

O operador de atribuição de ref não deve ler o local de armazenamento referenciado pelo operando direito.

Exemplo: Aqui estão alguns exemplos de uso de = 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
}

exemplo de encerramento

Nota: Ao ler o código usando um operador = ref, pode ser tentador ler a parte ref como parte do operando. Isso é particularmente confuso quando o operando é uma expressão ?: condicional. Por exemplo, ao ler ref int a = ref b ? ref x : ref y; é importante ler isso como = ref sendo o operador e b ? ref x : ref y sendo o operando certo: ref int a = ref (b ? ref x : ref y);. É importante ressaltar que a expressão ref bnão faz parte dessa afirmação, embora possa parecer assim à primeira vista. nota final

12.21.4 Atribuição composta

Se o operando esquerdo de uma atribuição composta for da forma E.P ou E[Ei] em que E tem o tipo de tempo de compilação dynamic, então a atribuição está dinamicamente vinculada (§12.3.3). Nesse caso, o tipo de tempo de compilação da expressão de atribuição é dynamic, e a resolução descrita abaixo ocorrerá em tempo de execução com base no tipo de tempo de execução do E. Se o operando esquerdo for da forma E[Ei] em que pelo menos um elemento de Ei tem o tipo de tempo de compilação dynamic, e o tipo de tempo de compilação de E não é uma matriz, o acesso do indexador resultante é vinculado dinamicamente, mas com verificação limitada em tempo de compilação (§12.6.5).

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 fosse escrita x «op» y. Em seguida,

  • Se o tipo de retorno do operador selecionado é implicitamente conversível para o tipo de x, a operação é avaliada como x = x «op» y, exceto que x é avaliada apenas 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 de x , e se y for implicitamente conversível para o tipo de x ou se o operador for um operador de turno, então a operação é avaliada como x = (T)(x «op» y), onde T é o tipo de x, só que x é avaliada apenas uma vez.
  • Caso contrário, a atribuição composta é inválida e ocorre um erro de tempo de compilação.

O termo "avaliado apenas uma vez" significa que, na avaliação de x «op» y, os resultados de qualquer expressão constituinte de x são temporariamente guardados e depois reutilizados quando se faz a atribuição a x.

Exemplo: Na atribuição A()[B()] += C(), onde 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. exemplo final

Quando o operando esquerdo de uma atribuição composta é um acesso de propriedade ou acesso de indexador, a propriedade ou indexador deve ter um acessador get e um acessador set. Se este não for o caso, ocorre um erro de tempo de ligação.

A segunda regra acima permite que x «op»= y sejam avaliadas como x = (T)(x «op» y) em determinados contextos. A regra existe de tal forma que os operadores predefinidos podem ser usados como operadores compostos quando o operando esquerdo é do tipo sbyte, byte, short, ushortou char. Mesmo quando ambos os argumentos são de um desses tipos, os operadores predefinidos produzem um resultado do tipo int, conforme descrito no §12.4.7.3. Assim, sem um elenco não seria possível atribuir o resultado ao operando esquerdo.

O efeito intuitivo da regra para operadores predefinidos é simplesmente que x «op»= y é permitido se tanto x «op» y como 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

A razão intuitiva para cada erro é que uma atribuição simples correspondente também teria sido um erro.

exemplo final

Nota: Isso também significa que as operações de atribuição composta suportam operadores elevados. Uma vez que uma atribuição composta x «op»= y é avaliada quer como x = x «op» y, quer como x = (T)(x «op» y), as regras de avaliação abrangem implicitamente os operadores elevados. nota final

12.21.5 Atribuição de eventos

Se o operando esquerdo do operador a += or -= for classificado como um acesso de evento, a expressão será avaliada da seguinte forma:

  • A expressão de instância, se houver, do acesso ao evento é avaliada.
  • O operando direito do operador += ou -= é avaliado e, se necessário, convertido para o 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 do valor calculado na etapa anterior. Se o operador foi +=, o acessador de adição é invocado; Se o operador foi -=, o acessador de remoção é invocado.

Uma expressão de atribuição de evento não produz um valor. Assim, uma expressão de atribuição de eventos só é válida no contexto de um statement_expression (§13.7).

12.22 Expressão

Uma expressão é uma atribuição non_assignment_expression ou .

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.

Apenas as seguintes construções são permitidas em expressões constantes:

  • Literais (incluindo o null literal).
  • Referências a const membros dos 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 próprias expressões constantes.
  • Transmitir expressões.
  • Expressões checked e unchecked.
  • nameof expressões.
  • Os operadores unários predefinidos +, -, ! (negação lógica) e ~.
  • Os operadores binários predefinidos +, -, *, /, %, <<, >>, &, |, ^, &&, ||, ==, !=, <, >, <=e >=.
  • O operador condicional ?:.
  • O ! operador de perdão nulo (§12.8.9).
  • sizeof expressões, desde que o tipo não gerenciado seja um dos tipos especificados em §23.6.9 para o qual sizeof retorna um valor constante.
  • Expressões de valor padrão, desde que o tipo seja um dos tipos listados acima.

As seguintes conversões são permitidas em expressões constantes:

  • Conversões de identidade
  • Conversões numéricas
  • Conversões de enumeração
  • Conversões de expressão constantes
  • Conversões de referência implícitas e explícitas, desde que a origem das conversões seja uma expressão constante que avalia o valor null.

Nota: Outras conversões: incluindo boxing, unboxing e conversões de referência implícitas de valores que não sãonull, não são permitidas em expressões constantes. nota final

No exemplo: No código seguinte

class C
{
    const object i = 5;         // error: boxing conversion not permitted
    const object str = "hello"; // error: implicit reference conversion
}

A inicialização do i é um erro porque uma conversão de boxe é necessária. A inicialização de str é um erro porque uma conversão de referência implícita de um valor nãonull é necessária.

exemplo final

Sempre que uma expressão preenche os requisitos listados acima, a expressão é avaliada em tempo de compilação. Isso é verdadeiro mesmo se a expressão for uma subexpressão de uma expressão maior que contém 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 em tempo de execução de expressões não constantes, exceto que, quando a avaliação em tempo de execução teria lançado uma exceção, a avaliação em tempo de compilação faz com que ocorra um erro em tempo de compilação.

A menos que uma expressão constante seja explicitamente colocada num 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 de 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 constant_expression. Nesses contextos, um erro em tempo de compilação ocorre se uma expressão não puder ser totalmente avaliada em tempo de compilação.

  • Declarações constantes (§15.4)
  • Declarações de membro da enumeração (§19.4)
  • Argumentos padrão de listas de parâmetros (§15.6.2)
  • case rótulos de uma declaração switch (§13.8.3).
  • goto case declarações (§13.10.4)
  • Comprimentos de dimensão em uma expressão de criação de matriz (§12.8.17.5) que inclui um inicializador.
  • Atributos (§22)
  • Numa constant_pattern (§11.2.3)

Uma conversão implícita de expressão constante (§10.2.11) permite que uma expressão constante do tipo int seja convertida em sbyte, byte, short, ushort, uintou ulong, desde que o valor da expressão constante esteja dentro do intervalo do tipo de destino.

12.24 Expressões booleanas

Um boolean_expression é uma expressão que produz um resultado do tipo bool; quer diretamente, quer através da aplicação de operator true em determinados contextos, tal como especificado no seguinte:

boolean_expression
    : expression
    ;

A expressão condicional de controlo 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 controlo do operador ?: (§12.18) segue as mesmas regras que uma expressão booleana , mas por razões de precedência é classificada como uma expressão de coalescência de nulos .

É necessário um boolean_expressionE para poder produzir um valor do tipo bool, como se segue:

  • Se E é implicitamente conversível em bool então em tempo de execução essa conversão implícita é aplicada.
  • Caso contrário, a resolução de sobrecarga do operador unário (§12.4.4) é usada para encontrar uma melhor implementação exclusiva de operator true em E, e essa implementação é aplicada em tempo de execução.
  • Se nenhum operador for encontrado, ocorrerá um erro de tempo de ligação.