Compartir vía


12 Expresiones

12.1 General

Una expresión es una secuencia de operadores y operandos. Esta cláusula define la sintaxis, el orden de evaluación de los operandos y operadores, y el significado de las expresiones.

12.2 Clasificación de las expresiones

12.2.1 General

El resultado de una expresión se clasifica como uno de los siguientes:

  • Un valor. Todos los valores tienen un tipo asociado.
  • Una variable. A menos que se especifique lo contrario, una variable está explícitamente tipada y tiene un tipo asociado, a saber, el tipo declarado de la variable. Una variable implícitamente tipada no tiene tipo asociado.
  • Un null-literal. Una expresión con esta clasificación se puede convertir implícitamente en un tipo de referencia o un tipo de valor que acepta valores nulos.
  • Una función anónima. Una expresión con esta clasificación puede convertirse implícitamente a un tipo delegado o a un tipo de árbol de expresión compatible.
  • Una tupla. Cada tupla tiene un número fijo de elementos, cada uno con una expresión y un nombre opcional para el elemento de la tupla.
  • Un acceso a propiedad. Todo acceso a una propiedad tiene un tipo asociado, a saber, el tipo de la propiedad. Además, un acceso a una propiedad puede tener asociada una expresión de instancia. Cuando se invoca un accessor de un acceso de propiedad de instancia, el resultado de evaluar la expresión de instancia se convierte en la instancia representada por this (sección 12.8.14).
  • Un acceso a un indexador. Todo acceso a un indexador tiene un tipo asociado, a saber, el tipo de elemento del indexador. Además, un acceso a un indexador tiene una expresión de instancia asociada y una lista de argumentos asociada. Cuando se invoca un accessor de un access indexer, el resultado de evaluar la expresión de la instancia se convierte en la instancia representada por this (sección 12.8.14), y el resultado de evaluar la lista de argumentos se convierte en la lista de parámetros de la invocación.
  • Nada. Esto ocurre cuando la expresión es una invocación a un método cuyo tipo de retorno es void. Una expresión clasificada como nada solo es válida en el contexto de una statement_expression (sección 13.7) o como cuerpo de una lambda_expression (sección 12.19).

Para las expresiones que aparecen como subexpresiones de expresiones mayores, con las restricciones indicadas, el resultado también puede clasificarse como uno de los siguientes:

  • Espacio de nombres. Una expresión con esta clasificación solo puede aparecer como lado izquierdo de un member_access (sección 12.8.7). En cualquier otro contexto, una expresión clasificada como espacio de nombres provoca un error de compilación.
  • Un tipo. Una expresión con esta clasificación solo puede aparecer como lado izquierdo de un member_access (sección 12.8.7). En cualquier otro contexto, una expresión clasificada como tipo provoca un error de compilación.
  • Un grupo de métodos, que es un conjunto de métodos sobrecargados resultantes de una búsqueda de miembros (§12.5). Un grupo de métodos puede tener una expresión de instancia asociada y una lista de argumentos de tipo asociada. Cuando se invoca un método de instancia, el resultado de la evaluación de la expresión de instancia se convierte en la instancia representada por this (sección 12.8.14). Se permite un grupo de métodos en un invocation_expression (§12.8.10) o en un delegate_creation_expression (§12.8.17.6) y puede convertirse implícitamente en un tipo de delegado compatible (§10.8). En cualquier otro contexto, una expresión clasificada como grupo de métodos provoca un error de compilación.
  • Un acceso a un evento. Cada acceso a un evento tiene un tipo asociado, a saber, el tipo del evento. Además, un acceso de evento puede tener una expresión de instancia asociada. Un acceso de evento puede aparecer como operando izquierdo de los operadores += y -= (§12.21.5). En cualquier otro contexto, una expresión clasificada como acceso de evento provoca un error de compilación. Cuando se invoca un descriptor de acceso de un acceso de instancia, el resultado de la evaluación de la expresión de instancia se convierte en la instancia representada por this (§12.8.14).
  • Expresión throw, que se puede usar en varios contextos para producir una excepción en una expresión. Una expresión throw puede convertirse mediante una conversión implícita a cualquier tipo.

El acceso a una propiedad o a un indexador siempre se reclasifica como un valor al invocar el método de acceso get o el método de acceso set. El descriptor de acceso concreto viene determinado por el contexto del acceso a la propiedad o al indexador: Si el acceso es el objetivo de una asignación, se invoca al descriptor de acceso set para asignar un nuevo valor (sección 12.21.2). En caso contrario, se invoca al descriptor de acceso get para obtener el valor actual (sección 12.2.2).

Un descriptor de acceso de instancia es un acceso a una propiedad de una instancia, un acceso a un evento de una instancia o un acceso a un indexador.

12.2.2 Valores de las expresiones

La mayoría de las construcciones que implican una expresión requieren en última instancia que la expresión indique un valor. En estos casos, si la expresión denota un espacio de nombres, un tipo, un grupo de métodos o nada, se produce un error de compilación. Sin embargo, si la expresión denota un acceso a una propiedad, un acceso a un indexador o una variable, el valor de la propiedad, indexador o variable se sustituye implícitamente:

  • El valor de una variable es simplemente el valor almacenado actualmente en la ubicación de almacenamiento identificada por la variable. Una variable debe considerarse definitivamente asignada (sección 9.4) antes de que pueda obtenerse su valor, o de lo contrario se produce un error en tiempo de compilación.
  • El valor de una expresión de acceso a una propiedad se obtiene invocando al descriptor de acceso get de la propiedad. Si la propiedad no tiene descriptor de acceso get, se produce un error de compilación. En caso contrario, se realiza una invocación a un miembro de función (§12.6.6), y el resultado de la invocación se convierte en el valor de la expresión de acceso a la propiedad.
  • El valor de una expresión de acceso a un indexador se obtiene invocando al descriptor de acceso get del indexador. Si el indexador no tiene descriptor de acceso get, se produce un error de compilación. En caso contrario, se realiza una invocación a un miembro de función (sección 12.6.6) con la lista de argumentos asociada a la expresión de acceso al indexador, y el resultado de la invocación se convierte en el valor de la expresión de acceso al indexador.
  • El valor de una expresión de tupla se obtiene aplicando una conversión implícita de tupla (§10.2.13) al tipo de la expresión de tupla. Es un error obtener el valor de una expresión de tupla que no tiene un tipo.

12.3 Vinculación estática y dinámica

12.3.1 General

Vinculación es el proceso de determinar a qué se refiere una operación, en función del tipo o valor de las expresiones (argumentos, operandos, receptores). Por ejemplo, la vinculación de una llamada a un método se determina en función del tipo del receptor y de los argumentos. La vinculación de un operador se determina en función del tipo de sus operandos.

En C#, la vinculación de una operación suele determinarse en tiempo de compilación, basándose en el tipo en tiempo de compilación de sus subexpresiones. Del mismo modo, si una expresión contiene un error, este se detecta y se notifica en tiempo de compilación. Este enfoque se conoce como vinculación estática.

Sin embargo, si una expresión es una expresión dinámica (es decir, tiene el tipo dynamic) esto indica que cualquier vinculación en la que participe debe basarse en su tipo en tiempo de ejecución en lugar del tipo que tiene en tiempo de compilación. Por lo tanto, la vinculación de dicha operación se pospone hasta el momento en que la operación deba ejecutarse durante la ejecución del programa. Esto se denomina vinculación dinámica.

Cuando una operación se vincula dinámicamente, se realiza poca o ninguna comprobación en tiempo de compilación. En cambio, si la vinculación en tiempo de ejecución falla, los errores se notifican como excepciones en tiempo de ejecución.

Las siguientes operaciones en C# están sujetas a vinculación:

  • Acceso a miembros: e.M
  • Invocación de métodos: e.M(e₁,...,eᵥ)
  • Invocación de delegados: e(e₁,...,eᵥ)
  • Acceso a elementos: e[e₁,...,eᵥ]
  • Creación de objetos: nuevo C(e₁,...,eᵥ)
  • Operadores unarios sobrecargados: +, -, ! (solo negación lógica), ~, ++, --, true, false
  • Operadores binarios sobrecargados: +, -, *, /, %, &, &&, |, ||, ??, ^, <<, >>, ==, !=, >, <, >=, <=
  • Operadores de asignación: =, = ref, +=, -=, *=, /=, %=, &=, |=, ^=, <<=, >>=
  • Conversiones implícitas y explícitas

Cuando no hay expresiones dinámicas implicadas, C# utiliza por defecto la vinculación estática, lo que significa que los tipos de las subexpresiones en tiempo de compilación se usan en el proceso de selección. Sin embargo, cuando una de las subexpresiones de las operaciones enumeradas anteriormente es una expresión dinámica, la operación se vincula dinámicamente.

Es un error en tiempo de compilación si la invocación a un método está ligada dinámicamente y cualquiera de los parámetros, incluido el receptor, son parámetros de entrada.

12.3.2 Tiempo de enlace

La vinculación estática se produce en tiempo de compilación, mientras que la dinámica tiene lugar en tiempo de ejecución. En las siguientes subcláusulas, el término tiempo de vinculación se refiere al tiempo de compilación o al tiempo de ejecución, dependiendo de cuándo tenga lugar la vinculación.

Ejemplo: A continuación se ilustran las nociones de vinculación estática y dinámica y de tiempo de vinculación:

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)

Las dos primeras llamadas están vinculadas estáticamente: la sobrecarga de Console.WriteLine se elige en función del tipo en tiempo de compilación de su argumento. Por lo tanto, el tiempo de enlace es el tiempo de compilación.

La tercera llamada está vinculada dinámicamente: la sobrecarga de Console.WriteLine se elige en función del tipo de su argumento en tiempo de ejecución. Esto sucede porque el argumento es una expresión dinámica: su tipo en tiempo de compilación es dinámico. Por lo tanto, el tiempo de enlace de la tercera llamada es el tiempo de ejecución.

ejemplo final

12.3.3 Vinculación dinámica

Esta subcláusula es informativa.

La vinculación dinámica permite a los programas de C# interactuar con objetos dinámicos, es decir, objetos que no siguen las reglas normales del sistema de tipos de C#. Los objetos dinámicos pueden ser objetos de otros lenguajes de programación con diferentes sistemas de tipos, o pueden ser objetos que se configuran mediante programación para implementar su propia semántica de enlace para diferentes operaciones.

El mecanismo mediante el cual un objeto dinámico implementa su propia semántica está definido por la implementación. Los objetos dinámicos implementan una interfaz determinada (también definida por la implementación) para indicar al motor de ejecución de C# que tienen una semántica especial. Por lo tanto, cada vez que las operaciones de un objeto dinámico están enlazadas dinámicamente, su propia semántica de enlace, en lugar de las especificadas en esta especificación para C#, prevalecen.

Aunque el propósito de la vinculación dinámica es permitir la interoperabilidad con objetos dinámicos, C# permite la vinculación dinámica en todos los objetos, sean dinámicos o no. Esto permite una integración más suave de los objetos dinámicos, ya que los resultados de las operaciones sobre ellos pueden no ser objetos dinámicos, pero siguen siendo de un tipo desconocido para el programador en tiempo de compilación. Además, la vinculación dinámica puede ayudar a eliminar el código basado en la reflexión, propenso a errores, incluso cuando los objetos implicados no son objetos dinámicos.

12.3.4 Tipos de subexpresiones

Cuando una operación está ligada estáticamente, el tipo de una subexpresión (por ejemplo, un receptor, un argumento, un índice o un operando) se considera siempre el tipo de dicha expresión en tiempo de compilación.

Cuando una operación está ligada dinámicamente, el tipo de una subexpresión se determina de distintas maneras en función del tipo en tiempo de compilación de la subexpresión:

  • Se considera que una subexpresión de tipo dinámico en tiempo de compilación tiene el tipo del valor real al que se evalúa la expresión en tiempo de ejecución.
  • Se considera que una subexpresión cuyo tipo en tiempo de compilación es un parámetro de tipo tiene el tipo al que está ligado el parámetro de tipo en tiempo de ejecución.
  • De lo contrario, se considera que la subexpresión tiene su tipo de tiempo de compilación.

12.4 Operadores

12.4.1 General

Las expresiones se construyen con operandos y operadores. Los operadores de una expresión indican qué operaciones se aplican a los operandos.

Ejemplo: Algunos ejemplos de operadores son +, -, *, / y new. Algunos ejemplos de operandos son literales, campos, variables locales y expresiones. ejemplo final

Existen tres tipos de operadores:

  • Operadores unarios. Los operadores unarios toman un operando y utilizan notación de prefijo (como –x) o notación de sufijo (como x++).
  • Operadores binarios. Los operadores binarios toman dos operandos y todos utilizan la notación infija (como x + y).
  • Operador ternario. Solo existe un operador ternario, ?:, que toma tres operandos y utiliza la notación infija (c ? x : y).

El orden de evaluación de los operadores en una expresión viene determinado por la precedencia y asociatividad de los operadores (§12.4.2).

Los operandos de una expresión se evalúan de izquierda a derecha.

Ejemplo: En F(i) + G(i++) * H(i), se llama al método F usando el valor antiguo de i, luego se llama al método G con el valor antiguo de i, y, finalmente, se llama al método H con el nuevo valor de i. Esto es independiente y no está relacionado con la precedencia de operadores. ejemplo final

Algunos operadores pueden sobrecargarse. La sobrecarga de operadores (sección 12.4.3) permite especificar implementaciones de operadores definidas por el usuario para operaciones en las que uno o ambos operandos son de una clase o tipo struct definidos por el usuario.

12.4.2 Precedencia y asociatividad de operadores

Cuando una expresión contiene varios operadores, la precedencia de los operadores controla el orden en que se evalúan los operadores individuales.

Nota: Por ejemplo, la expresión x + y * z se evalúa como x + (y * z) porque el operador * tiene mayor precedencia que el operador binario +. nota final

La precedencia de un operador se establece mediante la definición de su producción gramatical asociada.

Nota: por ejemplo, un additive_expression consiste en una secuencia de multiplicative_expressionseparada por operadores + o -, lo que proporciona a los operadores + y - menor precedencia que los operadores *, /y %. nota final

Nota: la siguiente tabla resume todos los operadores en orden de precedencia de mayor a menor:

Subcláusula Categoría Operadores
§12.8 Principal x.yx?.yf(x)a[x]a?[x]x++x--x!newtypeofdefaultcheckeduncheckeddelegatestackalloc
sección 12.9 Unario +-!x~++x--x(T)xawait x
§12.10 Multiplicativo */%
§12.10 Aditivo +-
§12.11 Shift <<>>
§12.12 Comprobación de tipos y relacional <><=>=isas
§12.12 Igualdad ==!=
sección 12.13 Y lógico &
sección 12.13 XOR lógico ^
sección 12.13 O lógico \|
sección 12.14 AND condicional &&
sección 12.14 OR condicional \|\|
sección 12.15 y sección 12.16 Operación de fusión de NULL y expresión throw ??throw x
§12.18 Condicional ?:
sección 12.21 y sección 12.19 Asignación y expresión lambda == ref*=/=%=+=-=<<=>>=&=^=\|==>

nota final

Cuando un operando se encuentra entre dos operadores con la misma precedencia, la asociatividad de los operadores controla el orden en que se realizan las operaciones:

  • Excepto los operadores de asignación y el operador de coalescencia nula, todos los operadores binarios son asociativos por la izquierda, lo que significa que las operaciones se realizan de izquierda a derecha.

    Ejemplo: x + y + z se evalúa como (x + y) + z. ejemplo final

  • Los operadores de asignación, el operador de fusión NULL o y el operador condicional (?:) son asociativos a la derecha, lo que significa que las operaciones se realizan de derecha a izquierda.

    Ejemplo: x = y = z se evalúa como x = (y = z). ejemplo final

La precedencia y la asociatividad pueden controlarse mediante paréntesis.

Ejemplo: x + y * z primero multiplica y por z y luego suma el resultado por x, pero (x + y) * z primero suma x y y y luego multiplica el resultado por z. ejemplo final

12.4.3 Sobrecarga de operadores

Todos los operadores unarios y binarios tienen implementaciones predefinidas. Además, se pueden introducir implementaciones definidas por el usuario incluyendo declaraciones de operadores (sección 15.10) en clases y structs. Las implementaciones de operadores definidas por el usuario siempre tienen prioridad sobre las implementaciones de operadores predefinidas: Solo cuando no existan implementaciones aplicables de operadores definidos por el usuario se considerarán las implementaciones de operadores predefinidos, tal como se describe en la sección 12.4.4 y sección 12.4.5.

Los operadores unarios sobrecargables son:

+ - ! (solo negación lógica) ~ ++ -- true false

Nota: Aunque true y false no se utilizan explícitamente en expresiones (y, por tanto, no se incluyen en la tabla de precedencias de sección 12.4.2), se consideran operadores porque se invocan en varios contextos de expresión: Expresiones booleanas (sección 12.24) y expresiones en las que intervienen los operadores condicional (sección 12.18) y lógico condicional (sección 12.14). nota final

Nota: El operador de perdón de nulos (postfijo !, sección 12.8.9) no es un operador sobrecargable. nota final

Los operadores binarios sobrecargables sí lo son:

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

Solo los operadores mencionados pueden sobrecargarse. En concreto, no es posible sobrecargar el acceso a miembros, la invocación de método o los operadores =, &&, ||, ??, ?:, =>, checked, unchecked, new, typeof, default, asy is.

Cuando se sobrecarga un operador binario, el operador de asignación compuesto correspondiente, si existe, también se sobrecarga implícitamente.

Ejemplo: una sobrecarga de operador * también es una sobrecarga de operador *=. Esto se describe con más detalle en la sección 12.21. ejemplo final

El operador de asignación (=) no puede sobrecargarse. Una asignación siempre realiza un almacenamiento simple de un valor en una variable (§12.21.2).

Las operaciones de conversión, como (T)x, se sobrecargan proporcionando conversiones definidas por el usuario (§10.5).

Nota: Las conversiones definidas por el usuario no afectan al comportamiento de los operadores is o as. nota final

El acceso a elementos, como a[x], no se considera un operador sobrecargable. En cambio, la indexación definida por el usuario se soporta a través de indexadores (sección 15.9).

En las expresiones, los operadores se referencian utilizando la notación de operador, y en las declaraciones, los operadores se referencian utilizando la notación funcional. La siguiente tabla muestra la relación entre las notaciones de operador y funcional para los operadores unarios y binarios. En la primera entrada, "op" denota cualquier operador de prefijo unario sobrecargable. En la segunda entrada, "op" denota el postfijo unario y los operadores ++ y --. En la tercera entrada, "op" denota cualquier operador binario sobrecargable.

Nota: Para un ejemplo de sobrecarga de los operadores ++ y --, consulte sección 15.10.2. nota final

Notación de operador Notación funcional
«op» x operator «op»(x)
x «op» operator «op»(x)
x «op» y operator «op»(x, y)

Las declaraciones de operadores definidos por el usuario siempre requieren que al menos uno de los parámetros sea del tipo de clase o estructura que contiene la declaración del operador.

Nota: por lo tanto, no es posible que un operador definido por el usuario tenga la misma firma que un operador predefinido. nota final

Las declaraciones de operadores definidos por el usuario no pueden modificar la sintaxis, precedencia o asociatividad de un operador.

Ejemplo: El operador / es siempre un operador binario, tiene siempre el nivel de precedencia especificado en la sección 12.4.2 y es siempre asociativo a la izquierda. ejemplo final

Nota: Aunque es posible que un operador definido por el usuario realice cualquier cálculo que desee, se desaconsejan firmemente las implementaciones que producen resultados diferentes de los que se esperan de forma intuitiva. Por ejemplo, una implementación de operador == debe comparar los dos operandos para la igualdad y devolver un resultado apropiado bool. nota final

Las descripciones de operadores individuales desde §12.9 hasta §12.21 especifican las implementaciones predefinidas de los operadores y las reglas adicionales que se aplican a cada operador. Las descripciones hacen uso de los términos resolución de sobrecarga de operador unario, resolución de sobrecarga de operador binario, promoción numérica y operador elevado, cuyas definiciones se encuentran en las siguientes subcláusulas.

12.4.4 Resolución de sobrecarga de operadores unarios

Una operación de la forma «op» x o x «op», donde "op" es un operador unario sobrecargable, y x es una expresión de tipo X, se procesa como sigue:

  • El conjunto de operadores candidatos definidos por el usuario proporcionado por para X la operación operator «op»(x) se determina utilizando las reglas de sección 12.4.6.
  • Si el conjunto de operadores candidatos definidos por el usuario no está vacío, se convierte en el conjunto de operadores candidatos para la operación. En caso contrario, las implementaciones binarias predefinidas operator «op», incluidas sus formas elevadas, se convierten en el conjunto de operadores candidatos para la operación. Las implementaciones predefinidas de un operador dado se especifican en la descripción del operador. Los operadores predefinidos proporcionados por un tipo de enumeración o un tipo delegado solo se incluyen en este conjunto cuando el tipo en tiempo de enlace de cualquiera de los operandos, o el tipo subyacente si es un tipo anulable, es el tipo de enumeración o el tipo delegado.
  • Las reglas de resolución de sobrecargas de sección 12.6.4 se aplican al conjunto de operadores candidatos para seleccionar el mejor operador con respecto a la lista de argumentos (x), y este operador se convierte en el resultado del proceso de resolución de sobrecargas. Si la resolución de sobrecarga no consigue seleccionar el mejor operador, se produce un error de vinculación.

12.4.5 Resolución de sobrecarga de operadores binarios

Una operación de la forma x «op» y, donde "op" es un operador binario sobrecargable, x es una expresión de tipo X, y y es una expresión de tipo Y, se procesa como sigue:

  • Se determina el conjunto de operadores candidatos definidos por el usuario proporcionados por X y Y para la operación operator «op»(x, y). El conjunto consiste en la unión de los operadores candidatos proporcionados por X y los operadores candidatos proporcionados por Y, cada uno determinado utilizando las reglas de sección 12.4.6. Para el conjunto combinado, los candidatos se fusionan de la siguiente manera:
    • Si X y Y son convertibles en identidad, o si X y Y se derivan de un tipo base común, entonces los operadores candidatos compartidos solo aparecen en el conjunto combinado una vez.
    • Si hay una conversión de identidad entre X y Y, un operador «op»Y proporcionado por Y tiene el mismo tipo de retorno que un «op»X proporcionado por X y los tipos de operando de «op»Y tienen una conversión de identidad a los correspondientes tipos de operando «op»X de entonces solo «op»X se produce en el conjunto.
  • Si el conjunto de operadores candidatos definidos por el usuario no está vacío, se convierte en el conjunto de operadores candidatos para la operación. En caso contrario, las implementaciones binarias predefinidas operator «op», incluidas sus formas elevadas, se convierten en el conjunto de operadores candidatos para la operación. Las implementaciones predefinidas de un operador dado se especifican en la descripción del operador. Para los operadores predefinidos de enumeración y delegado, los únicos operadores considerados son los proporcionados por un tipo enumeración o delegado que sea el tipo vinculante de uno de los operandos.
  • Las reglas de resolución de sobrecargas de sección 12.6.4 se aplican al conjunto de operadores candidatos para seleccionar el mejor operador con respecto a la lista de argumentos (x, y), y este operador se convierte en el resultado del proceso de resolución de sobrecargas. Si la resolución de sobrecarga no consigue seleccionar el mejor operador, se produce un error de vinculación.

12.4.6 Operadores candidatos definidos por el usuario

Dado un tipo T y una operación operator «op»(A), donde "op" es un operador sobrecargable y A es una lista de argumentos, el conjunto de operadores candidatos definidos por el usuario proporcionados por T para el operador «op»(A) se determina como sigue:

  • Determinar el tipo T₀. Si T es un tipo de valor anulable, T₀ es su tipo subyacente; en caso contrario, T₀ es igual a T.
  • Para todas operator «op» las declaraciones en T₀ y todas las formas elevadas de dichos operadores, si al menos un operador es aplicable (sección 12.6.4.2) con respecto a la lista de argumentos A, entonces el conjunto de operadores candidatos está formado por todos los operadores aplicables en T₀.
  • En caso contrario, si T₀ es object, el conjunto de operadores candidatos está vacío.
  • En caso contrario, el conjunto de operadores candidatos proporcionado por T₀ es el conjunto de operadores candidatos proporcionado por la clase base directa de T₀, o la clase base efectiva de T₀ si T₀ es un parámetro de tipo.

12.4.7 Promociones numéricas

12.4.7.1 General

Esta subcláusula es informativa.

Sección 12.4.7 y sus subcláusulas son un resumen del efecto combinado de:

La promoción numérica consiste en realizar automáticamente ciertas conversiones implícitas de los operandos de los operadores numéricos unarios y binarios predefinidos. La promoción numérica no es un mecanismo distinto, sino un efecto de la aplicación de la resolución de sobrecarga a los operadores predefinidos. La promoción numérica no afecta específicamente a la evaluación de los operadores definidos por el usuario, aunque los operadores definidos por el usuario pueden implementarse para mostrar efectos similares.

Como ejemplo de promoción numérica, considere las implementaciones predefinidas del operador binario *:

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

Cuando se aplican las reglas de resolución de sobrecarga (sección 12.6.4) a este conjunto de operadores, el efecto es seleccionar el primero de los operadores para los que existen conversiones implícitas a partir de los tipos de operando.

Ejemplo: Para la operación b * s, donde b es un byte y s es un short, la resolución de sobrecarga selecciona operator *(int, int) como mejor operador. Así, el efecto es que b y s se convierten a int, y el tipo del resultado es int. Del mismo modo, para la operación i * d, donde i es un int y d es un double, overload la resolución selecciona operator *(double, double) como mejor operador. ejemplo final

Fin del texto informativo.

12.4.7.2 Promociones numéricas unarias

Esta subcláusula es informativa.

La promoción numérica unaria se produce para los operandos de los operadores predefinidos y unarios +, -y ~. La promoción numérica unaria consiste simplemente en convertir operandos de tipo sbyte, byte, short, ushort o char al tipo int. Además, para el operador unario, la promoción numérica unaria convierte operandos de tipo uint a tipo long.

Fin del texto informativo.

12.4.7.3 Promociones numéricas binarias

Esta subcláusula es informativa.

La promoción numérica binaria se produce para los operandos de los operadores predefinidos y binarios +, -, *, /, %, &, |, ^, ==, !=, >, <, >= y <=. La promoción numérica binaria convierte implícitamente ambos operandos a un tipo común que, en el caso de los operadores no relacionales, se convierte también en el tipo resultado de la operación. La promoción numérica binaria consiste en aplicar las siguientes reglas, en el orden en que aparecen aquí:

  • Si cualquiera de los operandos es del tipo decimal, el otro operando se convierte al tipo decimal, o se produce un error de vinculación si el otro operando es del tipo float o double.
  • De lo contrario, si cualquiera de los operandos es del tipo double, el otro operando se convierte al tipo double.
  • De lo contrario, si cualquiera de los operandos es del tipo float, el otro operando se convierte al tipo float.
  • De lo contrario, si cualquiera de los operandos es del tipo ulong, el otro operando se convierte al tipo ulong, o se produce un error de vinculación si el otro operando es del tipo type sbyte, short, int o long.
  • De lo contrario, si cualquiera de los operandos es del tipo long, el otro operando se convierte al tipo long.
  • En caso contrario, si uno de los operandos es del tipo uint y el otro es del tipo sbyte, short o int, ambos operandos se convierten al tipo long.
  • De lo contrario, si cualquiera de los operandos es del tipo uint, el otro operando se convierte al tipo uint.
  • En caso contrario, ambos operandos se convierten al tipo int.

Nota: La primera regla prohíbe cualquier operación que mezcle el tipo decimal con los tipos double y float. La regla se deduce del hecho de que no hay conversiones implícitas entre el tipo decimal y los tipos double y float. nota final

Nota: Observe también que no es posible que un operando sea de tipo ulong cuando el otro operando es de tipo integral con signo. La razón es que no existe ningún tipo integral que pueda representar el rango completo de ulong así como los tipos integrales con signo. nota final

En los dos casos anteriores, se puede utilizar una expresión cast para convertir explícitamente un operando a un tipo compatible con el otro operando.

Example: En el código de ejemplo siguiente

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

se produce un error de vinculación porque un decimal no puede multiplicarse por un double. El error se resuelve convirtiendo explícitamente el segundo operando en decimal, como se indica a continuación:

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

ejemplo final

Fin del texto informativo.

12.4.8 Operadores elevados

Operadores elevados permiten que los operadores predefinidos y definidos por el usuario, que operan en tipos de valor no anulables, también se usen con formas anulables de esos tipos. Los operadores elevados se construyen a partir de operadores predefinidos y definidos por el usuario que cumplen ciertos requisitos, como se describe a continuación:

  • Para los operadores unarios +, ++, -, --, !(negación lógica) y ~, existe una forma elevada de un operador si los tipos de operando y resultado son ambos tipos de valor no nulos. La forma elevada se construye añadiendo un único modificador ? a los tipos del operando y del resultado. El operador elevado genera un valor de null si el operando es null. En caso contrario, el operador elevado desenvuelve el operando, aplica el operador subyacente y envuelve el resultado.
  • Para los operadores binarios +, -, *, /, %, &, |, ^, << y >>, existe una forma elevada de un operador si los tipos de operando y resultado son todos tipos de valor no nulos. La forma elevada se construye añadiendo un único modificador ? a cada operando y tipo de resultado. El operador elevado genera un valor de null si uno o ambos operandos son null, con la excepción de los operadores & y | del tipo bool?, tal como se describe en §12.13.5. De lo contrario, el operador elevado desenvuelve los operandos, aplica el operador subyacente y envuelve el resultado.
  • Para los operadores de igualdad == y !=, existe una forma elevada de un operador si los tipos de los operandos son ambos tipos de valores que no admiten valores NULL y si el tipo del resultado es bool. La forma elevada se construye añadiendo un único modificador ? a cada tipo de operando. El operador levantado considera dos valores iguales null, y un valor null desigual a cualquier valor que no sea null. Si ambos operandos son no null, el operador elevado desenvuelve los operandos y aplica el operador subyacente para producir el resultado bool.
  • Para los operadores relacionales <, >, <=y >=, existe una forma elevada de un operador si los tipos de operando son tipos de valor no anulables y si el tipo de resultado es bool. La forma elevada se construye añadiendo un único modificador ? a cada tipo de operando. El operador elevado genera el valor false si uno o ambos operandos son null. En caso contrario, el operador elevado desenvuelve los operandos y aplica el operador subyacente para obtener el resultado bool.

12.5 Búsqueda de miembros

12.5.1 General

La búsqueda de miembros es el proceso mediante el cual se determina el significado de un nombre en el contexto de un tipo. Una búsqueda de miembro puede producirse como parte de la evaluación de un simple_name (§12.8.4) o un member_access (§12.8.7) en una expresión. Si el simple_name o member_access ocurre como la primary_expression de una invocation_expression (§12.8.10.2), se dice que el miembro es invocado.

Si un miembro es un método o un evento, o si es una constante, un campo o una propiedad de un tipo delegado (sección 20) o del tipo dynamic (sección 8.2.4), se dice que el miembro es invocable.

La búsqueda de miembros no solo tiene en cuenta el nombre de un miembro, sino también el número de parámetros de tipo que tiene y si el miembro es accesible. A efectos de la búsqueda de miembros, los métodos genéricos y los tipos genéricos anidados tienen el número de parámetros de tipo indicado en sus respectivas declaraciones y todos los demás miembros tienen cero parámetros de tipo.

Una búsqueda de miembros de un nombre N con argumentos de tipo K en un tipo T se procesa como sigue:

  • En primer lugar, se determina un conjunto de miembros accesibles denominados N:
    • Si T es un parámetro de tipo, entonces el conjunto es la unión de los conjuntos de miembros accesibles nombrados N en cada uno de los tipos especificados como restricción primaria o restricción secundaria (sección 15.2.5) para T, junto con el conjunto de miembros accesibles nombrados N en object.
    • En caso contrario, el conjunto está formado por todos los miembros accesibles (sección 7.5) nombrados N en T, incluidos los miembros heredados y los miembros accesibles nombrados N en object. Si T es un tipo construido, el conjunto de miembros se obtiene sustituyendo los argumentos de tipo como se describe en la sección 15.3.3. Los miembros que incluyen un modificador override se excluyen del conjunto.
  • A continuación, si K es cero, se eliminan todos los tipos anidados cuyas declaraciones incluyan parámetros de tipo. Si K no es cero, se eliminan todos los miembros con un número diferente de parámetros de tipo. Cuando K es cero, los métodos que tienen parámetros de tipo no se eliminan, ya que el proceso de inferencia de tipos (sección 12.6.3) podría ser capaz de inferir los argumentos de tipo.
  • A continuación, si se invoca el miembro, se eliminan del conjunto todos los miembros no invocables.
  • A continuación, los miembros ocultos por otros miembros se eliminan del conjunto. Para cada miembro S.M del conjunto, donde S es el tipo en el que se declara el miembro M, se aplican las siguientes reglas:
    • Si M es un miembro constante, campo, propiedad, evento o enumeración, entonces todos los miembros declarados en un tipo base de S son eliminados del conjunto.
    • Si M es una declaración de tipo, todos los no tipos declarados en un tipo base de S se eliminan del conjunto, y todas las declaraciones de tipo con el mismo número de parámetros de tipo que los declarados como M en un tipo base de S se eliminan del conjunto.
    • Si M es un método, todos los miembros no-método declarados en un tipo base de S se eliminan del conjunto.
  • A continuación, se eliminan del conjunto los miembros de la interfaz que están ocultos por miembros de la clase. Este paso solo tiene efecto si T es un parámetro de tipo y T tiene una clase base efectiva distinta de object y un conjunto de interfaces efectivas no vacío (sección 15.2.5). Para cada miembro del conjunto S.M, donde S es el tipo en el que se declara el miembro M, se aplican las siguientes reglas si S es una declaración de clase distinta de object:
    • Si M es una constante, campo, propiedad, evento, miembro de enumeración o declaración de tipo, todos los miembros declarados en una declaración de interfaz se eliminan del conjunto.
    • Si M es un método, se eliminan del conjunto todos los miembros no-método declarados en una declaración de interfaz, y se eliminan del conjunto todos los métodos con la misma firma M que la declarada en una declaración de interfaz.
  • Por último, una vez eliminados los miembros ocultos, se determina el resultado de la búsqueda:
    • Si el conjunto consta de un único miembro que no es un método, este miembro es el resultado de la búsqueda.
    • En caso contrario, si el conjunto solo contiene métodos, el resultado de la búsqueda será este grupo de métodos.
    • En caso contrario, la búsqueda es ambigua y se produce un error de vinculación.

Para las búsquedas de miembros en tipos que no sean parámetros de tipo e interfaces, y las búsquedas de miembros en interfaces que son estrictamente de herencia única (cada interfaz en la cadena de herencia tiene exactamente cero o una interfaz base directa), el efecto de las reglas de búsqueda es simplemente que los miembros derivados ocultan a los miembros base con el mismo nombre o firma. Estas búsquedas de herencia única nunca son ambiguas. Las ambigüedades que pueden surgir de la búsqueda de miembros en interfaces de herencia múltiple se describen en la sección 18.4.6.

Nota: Esta fase solo tiene en cuenta un tipo de ambigüedad. Si la búsqueda de miembros da como resultado un grupo de métodos, es posible que se produzca un error en el uso adicional del grupo de métodos debido a su ambigüedad, como se describe en §12.6.4.1 y §12.6.6.2. nota final

12.5.2 Tipos base

Para propósitos de búsqueda de miembros, se considera que un tipo T tiene los siguientes tipos base:

  • Si T es object o dynamic, entonces T no tiene tipo base.
  • Si T es un enum_type, los tipos base de T son los tipos de clase System.Enum, System.ValueType y object.
  • Si T es un struct_type, los tipos base de T son los tipos de clase System.ValueType y object.

    Nota: un nullable_value_type es un struct_type (§8.3.1). nota final

  • Si T es un class_type, los tipos base de T son las clases base de T, incluido el tipo de clase object.
  • Si T es un interface_type, los tipos base de T son las interfaces base de T y el tipo de clase object.
  • Si T es un array_type, los tipos base de T son los tipos de clase System.Array y object.
  • Si T es un delegate_type, los tipos base de T son los tipos de clase System.Delegate y object.

12.6 Miembros de función

12.6.1 General

Los miembros de función son aquellos que contienen instrucciones ejecutables. Los miembros de función son siempre miembros de tipos y no pueden ser miembros de namespaces. C# define las siguientes categorías de miembros de función:

  • Métodos
  • Propiedades
  • Eventos
  • Indizadores
  • Operadores definidos por el usuario
  • Constructores de instancias
  • Constructores estáticos
  • Finalizadores

Excepto los finalizadores y los constructores estáticos (que no pueden invocarse explícitamente), las instrucciones contenidas en los miembros de función se ejecutan mediante invocaciones a miembros de función. La sintaxis real para escribir una invocación a un miembro de función depende de la categoría particular del miembro de función.

La lista de argumentos (sección 12.6.2) de una invocación a un miembro funcional proporciona valores reales o referencias a variables para los parámetros del miembro funcional.

Las invocaciones de métodos genéricos pueden emplear la inferencia de tipo para determinar el conjunto de argumentos de tipo que deben pasarse al método. Este proceso se describe en la sección 12.6.3.

Las invocaciones de métodos, indizadores, operadores y constructores de instancia emplean la resolución de sobrecarga para determinar cuál miembro de funciones de un conjunto candidato se debe invocar. Este proceso se describe en la sección 12.6.4.

Una vez que se ha identificado un miembro de función concreto en tiempo de enlace, posiblemente mediante la resolución de sobrecarga, el proceso en tiempo de ejecución real de invocar al miembro de función se describe en §12.6.6.

Nota: La siguiente tabla resume el procesamiento que tiene lugar en las construcciones que implican las seis categorías de miembros de función que pueden invocarse explícitamente. En la tabla, e, x, y y value y indican expresiones clasificadas como variables o valores, T indica una expresión clasificada como tipo, F es el nombre simple de un método y P es el nombre simple de una propiedad.

Construcción Ejemplo Descripción
Invocación de método F(x, y) La resolución de sobrecarga se aplica para seleccionar el mejor método F de la clase o estructura contenedora. El método se invoca con la lista de argumentos (x, y). Si el método no es static, la expresión de instancia es this.
T.F(x, y) La resolución de sobrecarga se aplica para seleccionar el mejor método F de la clase o estructura T. Si el método no es static. El método se invoca con la lista de argumentos (x, y).
e.F(x, y) La resolución de sobrecarga se aplica para seleccionar el mejor método F de la clase, estructura o interfaz dada por el tipo de e. Se produce un error de vinculación si el método es static. El método se invoca con la expresión de instancia e y la lista de argumentos (x, y).
Property Access P Se invoca el descriptor de acceso get de la propiedad P en la clase o struct contenedora. Se produce un error de compilación si P es de solo escritura. Si P no es static, la expresión de instancia es this.
P = value Se invoca al descriptor de acceso set de la propiedad P en la clase o struct contenedora con la lista de argumentos (value). Se produce un error de compilación si P es de solo lectura. Si P no es static, la expresión de instancia es this.
T.P Se invoca al descriptor de acceso get de la propiedad P en la clase o struct contenedora T. Se produce un error de compilación si P no es static o si P es de solo escritura.
T.P = value El accesor 'set' de la propiedad P en la clase o estructura T se invoca con la lista de argumentos (value). Se produce un error de compilación si P no es static o si P es de solo lectura.
e.P El descriptor de acceso get de la propiedad P en la clase, struct o interfaz dada por el tipo de E se invoca con la expresión de instancia e. Se produce un error de vinculación si P es static o si P es de solo escritura.
e.P = value El descriptor de acceso set de la propiedad P en la clase, struct o interfaz dada por el tipo de E se invoca con la expresión de instancia e y la lista de argumentos (value). Se produce un error de vinculación si P es static o si P es de solo lectura.
Acceso a eventos E += value Se invoca al descriptor de acceso add del evento E en la clase o struct contenedora. Si E no es static, la expresión de instancia es this.
E -= value Se invoca al descriptor de acceso remove del evento E en la clase o struct contenedora. Si E no es static, la expresión de instancia es this.
T.E += value Se invoca al descriptor de acceso add del evento E en la clase o struct T. Se produce un error de vinculación si E no es static.
T.E -= value Se invoca al descriptor de acceso remove del evento E en la clase o struct T. Se produce un error de vinculación si E no es static.
e.E += value El descriptor de acceso add del evento E en la clase, struct o interfaz dada por el tipo de E se invoca con la expresión de instancia e. Se produce un error de vinculación si E es static.
e.E -= value El descriptor de acceso remove del evento E en la clase, struct o interfaz dada por el tipo de E se invoca con la expresión de instancia e. Se produce un error de vinculación si E es static.
Acceso a indizador e[x, y] La resolución de sobrecarga se aplica para seleccionar el mejor indexador en la clase, estructura o interfaz dada por el tipo de e. El descriptor de acceso get del indexador se invoca con la expresión de instancia e y la lista de argumentos (x, y). Se produce un error de vinculación si es o si es de solo escritura.
e[x, y] = value La resolución de sobrecarga se aplica para seleccionar el mejor indexador en la clase, estructura o interfaz dada por el tipo de e. El descriptor de acceso set del indexador se invoca con la expresión de instancia e y la lista de argumentos (x, y, value). Se produce un error de vinculación si es o si es de solo lectura.
Invocación del operador -x La resolución de sobrecarga se aplica para seleccionar el mejor operador unario en la clase o estructura dada por el tipo de x. El operador seleccionado se invoca con la lista de argumentos (x).
x + y La resolución de sobrecarga se aplica para seleccionar el mejor operador binario en las clases o estructuras dadas por los tipos de x y y. El operador seleccionado se invoca con la lista de argumentos (x, y).
Invocación del constructor de instancia new T(x, y) La resolución de sobrecarga se aplica para seleccionar el mejor constructor de instancia en la clase o estructura T. El constructor de instancia se invoca con la lista de argumentos (x, y).

nota final

12.6.2 Listas de argumentos

12.6.2.1 General

Cada miembro de función e invocación de delegado incluye una lista de argumentos, que proporciona valores reales o referencias a variables para los parámetros del miembro de función. La sintaxis para especificar la lista de argumentos de una invocación a un miembro funcional depende de la categoría del miembro funcional:

  • Por ejemplo, para constructores, métodos, indizadores y delegados, los argumentos se especifican como argument_list, como se describe a continuación. En el caso de los indexadores, cuando se invoca al descriptor de acceso de conjunto, la lista de argumentos incluye además la expresión especificada como operando derecho del operador de asignación.

    Nota: Este argumento adicional no se usa para la resolución de sobrecargas, solo durante la invocación del accesor 'set'. nota final

  • En el caso de las propiedades, la lista de argumentos está vacía cuando se invoca al descriptor de acceso get, y consiste en la expresión especificada como operando derecho del operador de asignación cuando se invoca al descriptor de acceso set.
  • Para los eventos, la lista de argumentos está formada por la expresión especificada como operando derecho del operador += o -=.
  • Para los operadores definidos por el usuario, la lista de argumentos consiste en el operando único del operador unario o los dos operandos del operador binario.

Los argumentos de propiedades (sección 15.7) y eventos (sección 15.8) se pasan siempre como parámetros de valor (sección 15.6.2.2). Los argumentos de los operadores definidos por el usuario (sección 15.10) se pasan siempre como parámetros de valor (sección 15.6.2.2) o parámetros de entrada (sección 9.2.8). Los argumentos de los indexadores (sección 15.9) siempre se pasan como parámetros de valor (sección 15.6.2.2), parámetros de entrada (sección 9.2.8) o matrices de parámetros (sección 15.6.2.4). Estas categorías de miembros de función no admiten parámetros de salida ni de referencia.

Los argumentos de un constructor de instancia, método, indexador o invocación de delegado se especifican como argument_list:

argument_list
    : argument (',' argument)*
    ;

argument
    : argument_name? argument_value
    ;

argument_name
    : identifier ':'
    ;

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

Una argument_list consta de uno o más argumentos, separados por comas. Cada argumento consta de un argument_name opcional seguido de un argument_value. Un argumento con un argument_name se denomina argumento con nombre, mientras que un argumento sin argument_name es un argumento posicional.

El argument_value puede adoptar una de las siguientes formas:

  • Una expresión , que indica que el argumento se pasa como un parámetro de valor o se transforma en un parámetro de entrada y se pasa de esa forma, como se determina en§12.6.4.2 y se describe en §12.6.2.3.
  • La palabra clave in seguida de una variable_reference (sección 9.5), que indica que el argumento se pasa como parámetro de entrada (sección 15.6.2.3.2). Una variable debe asignarse definitivamente (sección 9.4) antes de poder pasarse como parámetro de entrada.
  • La palabra clave ref seguida de una variable_reference (sección 9.5), que indica que el argumento se pasa como parámetro de referencia (sección 15.6.2.3.3). Una variable deberá estar asignada definitivamente (sección 9.4) antes de que pueda pasarse como parámetro de referencia.
  • La palabra clave out seguida de una variable_reference (sección 9.5), que indica que el argumento se pasa como parámetro de salida (sección 15.6.2.3.4). Una variable se considera definitivamente asignada (sección 9.4) tras una invocación a un miembro de función en la que la variable se pasa como parámetro de salida.

La forma determina el modo de paso de parámetros del argumento: valor, entrada, referencia o salida, respectivamente. Sin embargo, como se ha mencionado anteriormente, un argumento con modo de paso de valor, puede transformarse en uno con modo de paso de entrada.

Pasar un campo volátil (sección 15.5.4) como parámetro de entrada, salida o referencia provoca una advertencia, ya que el campo puede no ser tratado como volátil por el método invocado.

12.6.2.2 Parámetros correspondientes

Para cada argumento de una lista de argumentos debe existir un parámetro correspondiente en el miembro de función o delegado invocado.

La lista de parámetros utilizada a continuación se determina del siguiente modo:

  • Para los métodos virtuales y los indexadores definidos en clases, la lista de parámetros se extrae de la primera declaración o anulación del miembro de la función que se encuentra al comenzar con el tipo estático del receptor y buscar a través de sus clases base.
  • Para los métodos parciales se utiliza la lista de parámetros de la declaración del método parcial que los define.
  • Para todos los demás miembros de función y delegados existe una única lista de parámetros, que es la que se utiliza.

La posición de un argumento o parámetro se define como el número de argumentos o parámetros que le preceden en la lista de argumentos o la lista de parámetros.

Los parámetros correspondientes a los argumentos miembros de una función se establecen del siguiente modo:

  • Argumentos en la argument_list de constructores de instancia, métodos, indexadores y delegados:
    • Un argumento posicional en el que un parámetro aparece en la misma posición en la lista de parámetros corresponde a ese parámetro, a menos que el parámetro sea una matriz de parámetros y el miembro de la función se invoque en su forma expandida.
    • Un argumento posicional de un miembro de función con una matriz de parámetros invocada en su forma expandida, que aparece en o después de la posición de la matriz de parámetros en la lista de parámetros, corresponde a un elemento de la matriz de parámetros.
    • Un argumento con nombre corresponde al parámetro del mismo nombre en la lista de parámetros.
    • Para los indexadores, cuando se invoca al descriptor de acceso set, la expresión especificada como operando derecho del operador de asignación corresponde al parámetro implícito value de la declaración del descriptor de acceso set.
  • Para las propiedades, cuando se invoca al descriptor de acceso get no hay argumentos. Al invocar el accesor 'set', la expresión especificada como operando derecho del operador de asignación corresponde al parámetro implícito de valor de la declaración del accesor 'set'.
  • Para los operadores unarios definidos por el usuario (incluidas las conversiones), el operando único corresponde al parámetro único de la declaración del operador.
  • Para los operadores binarios definidos por el usuario, el operando izquierdo corresponde al primer parámetro, y el operando derecho corresponde al segundo parámetro de la declaración del operador.
  • Un argumento sin nombre no corresponde a ningún parámetro cuando sigue a un argumento con nombre desordenado o a un argumento con nombre que corresponde a un array de parámetros.

    Nota: esto impide que void M(bool a = true, bool b = true, bool c = true); sea invocado por M(c: false, valueB);. El primer argumento se utiliza fuera de posición (el argumento se utiliza en primera posición, pero el parámetro nombrado c está en tercera posición), por lo que los argumentos siguientes deben nombrarse. En otras palabras, solo se permiten argumentos con nombre no final cuando el nombre y la posición dan como resultado encontrar el mismo parámetro correspondiente. nota final

12.6.2.3 Evaluación en tiempo de ejecución de listas de argumentos

Durante el procesamiento en tiempo de ejecución de una invocación a un miembro de una función (sección 12.6.6), las expresiones o referencias a variables de una lista de argumentos se evalúan en orden, de izquierda a derecha, de la siguiente manera:

  • Para un argumento de valor, si el modo de paso del parámetro es valor

    • se evalúa la expresión del argumento y se realiza una conversión implícita (sección 10.2) al tipo de parámetro correspondiente. El valor resultante se convierte en el valor inicial del parámetro de valor en la invocación del miembro de la función.

    • en caso contrario, el modo de paso del parámetro es input. Si el argumento es una referencia variable y existe una conversión de identidad (sección 10.2.2) entre el tipo del argumento y el tipo del parámetro, el almacén resultante se convierte en el almacén representado por el parámetro en la invocación del miembro de la función. En caso contrario, se crea un almacén con el mismo tipo que el del parámetro correspondiente. Se evalúa la expresión del argumento y se realiza una conversión implícita (sección 10.2) al tipo de parámetro correspondiente. El valor resultante se almacena en esa ubicación de almacenamiento. Ese almacén está representado por el parámetro de entrada en la invocación del miembro de la función.

      Ejemplo: Dadas las siguientes declaraciones y llamadas a métodos:

      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
      

      En la llamada al método M1(i), i se pasa como argumento de entrada, porque se clasifica como variable y tiene el mismo tipo int que el parámetro de entrada. En la llamada al método M1(i + 5), se crea una variable sin nombre int, se inicializa con el valor del argumento y se pasa como argumento de entrada. Consulte sección 12.6.4.2 y sección 12.6.4.4.

      ejemplo final

  • Para un argumento de entrada, salida o referencia, se evalúa la referencia de la variable y el almacén resultante se convierte en el almacén representado por el parámetro en la invocación del miembro de la función. Para un argumento de entrada o de referencia, la variable se asignará definitivamente en el punto de la llamada al método. Si la referencia variable se da como argumento de salida, o es un elemento de matriz de reference_type, se realiza una comprobación en tiempo de ejecución para asegurar que el tipo de elemento de la matriz es idéntico al tipo del parámetro. Si esta comprobación falla, se lanza un System.ArrayTypeMismatchException.

Nota: esta comprobación en tiempo de ejecución es necesaria debido a la covarianza de matrices (sección 17.6). nota final

Example: En el código de ejemplo siguiente

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

la segunda invocación F de hace que se lance un System.ArrayTypeMismatchException porque el tipo real de elemento de b es string y no object.

ejemplo final

Los métodos, indexadores y constructores de instancia pueden declarar que su parámetro situado más a la derecha es una matriz de parámetros (sección 15.6.2.4). Estos miembros de función se invocan en su forma normal o en su forma expandida, dependiendo de cuál sea aplicable (sección 12.6.4.2):

  • Cuando un miembro de función con una matriz de parámetros se invoca en su forma normal, el argumento dado para la matriz de parámetros deberá ser una única expresión que sea convertible implícitamente (sección 10.2) al tipo de matriz de parámetros. En este caso, la matriz de parámetros actúa exactamente como un parámetro de valor.
  • Cuando un miembro de función con una matriz de parámetros se invoca en su forma expandida, la invocación deberá especificar cero o más argumentos posicionales para la matriz de parámetros, donde cada argumento es una expresión que es implícitamente convertible (sección 10.2) al tipo de elemento de la matriz de parámetros. En este caso, la invocación crea una instancia del tipo de matriz de parámetros con una longitud correspondiente al número de argumentos, inicializa los elementos de la instancia de matriz con los valores de los argumentos dados y utiliza la instancia de matriz recién creada como argumento real.

Las expresiones de una lista de argumentos se evalúan siempre en orden textual.

Ejemplo: Así, el ejemplo

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++);
    }
}

genera el resultado

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

ejemplo final

Cuando un miembro de función con una matriz de parámetros se invoca en su forma expandida con al menos un argumento expandido, la invocación se procesa como si se hubiera insertado una expresión de creación de matriz con un inicializador de matriz (sección 12.8.17.5) alrededor de los argumentos expandidos. Se pasa un array vacío cuando no hay argumentos para el parámetro array; no se especifica si la referencia pasada es a un array vacío recién asignado o existente.

Ejemplo: Dada la declaración

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

las siguientes invocaciones de la forma expandida del método

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

corresponden exactamente a

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

ejemplo final

Cuando se omiten los argumentos de un miembro de función con parámetros opcionales correspondientes, se pasan implícitamente los argumentos por defecto de la declaración del miembro de función. (Esto puede implicar la creación de un almacén, como se ha descrito anteriormente).

Nota: Dado que estos son siempre constantes, su evaluación no afectará a la evaluación del resto de argumentos. nota final

12.6.3 Inferencia de tipo

12.6.3.1 General

Cuando se llama a un método genérico sin especificar argumentos de tipo, un proceso de inferencia de tipos intenta deducir argumentos de tipo para la llamada. La presencia de la inferencia de tipo permite utilizar una sintaxis más conveniente para llamar a un método genérico, y permite al programador evitar especificar información de tipo redundante.

Ejemplo:

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

A través de la inferencia de tipo, los argumentos de tipo int y string se determinan a partir de los argumentos del método.

ejemplo final

La inferencia de tipo se produce como parte del procesamiento en tiempo de enlace de la invocación a un método (sección 12.8.10.2) y tiene lugar antes del paso de resolución de sobrecarga de la invocación. Cuando se especifica un grupo de métodos concreto en una invocación de método, y no se especifican argumentos de tipo como parte de la invocación de método, se aplica la inferencia de tipo a cada método genérico del grupo de métodos. Si la inferencia de tipo tiene éxito, los argumentos de tipo inferidos se utilizan para determinar los tipos de argumentos para la posterior resolución de sobrecarga. Si la resolución de sobrecarga elige un método genérico como el que se va a invocar, entonces los argumentos de tipo inferidos se utilizan como los argumentos de tipo para la invocación. Si la inferencia de tipo para un método en particular falla, ese método no participa en la resolución de sobrecarga. El fallo de la inferencia de tipos, en sí mismo, no causa un error de vinculación. Sin embargo, a menudo provoca un error de vinculación cuando la resolución de sobrecargas no encuentra ningún método aplicable.

Si cada argumento suministrado no corresponde exactamente a un parámetro del método (sección 12.6.2.2), o hay un parámetro no opcional sin argumento correspondiente, la inferencia falla inmediatamente. En caso contrario, supongamos que el método genérico tiene la siguiente firma:

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

Con una llamada a un método de la forma la tarea de la inferencia M(E₁ ...Eₓ) de tipo es encontrar argumentos de tipo único S₁...Sᵥ para cada uno de los parámetros de tipo X₁...Xᵥ para que la llamada M<S₁...Sᵥ>(E₁...Eₓ) sea válida.

El proceso de inferencia de tipo se describe a continuación como un algoritmo. Un compilador conforme puede implementarse utilizando un enfoque alternativo, siempre que llegue al mismo resultado en todos los casos.

Durante el proceso de inferencia, cada parámetro Xᵢ de tipo se fija a un tipo particular Sᵢ o no se fija con un conjunto asociado de límites. Cada uno de los límites es de un tipo T. Inicialmente cada variable de tipo Xᵢ no está fijada con un conjunto vacío de límites.

La inferencia de tipos tiene lugar en fases. Cada fase intentará inferir argumentos de tipo para más variables de tipo basándose en los resultados de la fase anterior. La primera fase realiza algunas inferencias iniciales de límites, mientras que la segunda fase asigna variables de tipo a tipos específicos e infiere límites adicionales. La segunda fase puede tener que repetirse varias veces.

Nota: La inferencia de tipo también se utiliza en otros contextos, como la conversión de grupos de métodos (sección 12.6.3.14) y la búsqueda del mejor tipo común de un conjunto de expresiones (sección 12.6.3.15). nota final

12.6.3.2 La primera fase

Para cada uno de los argumentos del método Eᵢ:

  • Si Eᵢ es una función anónima, se realiza una inferencia explícita del tipo del parámetro (§12.6.3.8) deEᵢaTᵢ
  • En caso contrario, si Eᵢ tiene un tipo U y el parámetro correspondiente es un parámetro de valor (§15.6.2.2), se realiza una inferencia de límite inferior (§12.6.3.10) deUaTᵢ.
  • En caso contrario, si Eᵢ tiene un tipo U y el parámetro correspondiente es un parámetro de referencia (§15.6.2.3.3) o un parámetro de salida (§15.6.2.3.4), se hace una inferencia exacta (§12.6.3.9) deUaTᵢ.
  • En caso contrario, si Eᵢ tiene un tipo U y el parámetro correspondiente es un parámetro de entrada (sección 15.6.2.3.2) y Eᵢ es un argumento de entrada, se hace una inferencia exacta (sección 12.6.3.9) deUaTᵢ.
  • En caso contrario, si Eᵢ tiene un tipo U y el parámetro correspondiente es un parámetro de entrada (§15.6.2.3.2), se realiza una inferencia de límite inferior (§12.6.3.10) deUaTᵢ.
  • En caso contrario, no se realiza ninguna inferencia para este argumento.

12.6.3.3 Segunda fase

La segunda fase procede como sigue:

  • Todas las variables de tipo no fijas Xᵢ que no dependen de (§12.6.3.6) cualquier Xₑ se fija (§12.6.3.12).
  • Si no existen tales variables de tipo, todas las variables de tipo no fijasXᵢ son fijas para las que se cumplen todas las siguientes condiciones:
    • Hay al menos una variable de tipo Xₑ de la cual depende deXᵢ
    • Xᵢ tiene un conjunto no vacío de límites
  • Si no existen tales variables de tipo y sigue habiendo variables de tipo no fijas, la inferencia de tipo falla.
  • De lo contrario, si no existen más variables de tipo no fijadas, la inferencia de tipos tiene éxito.
  • En caso contrario, para todos los argumentos Eᵢ con el tipo de parámetro Tᵢ correspondiente en los que los tipos de salida (§12.6.3.5) contengan variables de tipo no fijas Xₑ pero los tipos de entrada (§12.6.3.4) no, se realiza una inferencia de tipo de salida (§12.6.3.7) deEᵢaTᵢ. A continuación se repite la segunda fase.

12.6.3.4 Tipos de entrada

Si E es un grupo de métodos o una función anónima con tipo implícito y T es un tipo delegado o de árbol de expresión, todos los tipos de parámetros de T son tipos de entrada deEcon tipoT.

12.6.3.5 Tipos de salida

Si E es un grupo de métodos o una función anónima y T es de tipo delegado o de tipo árbol de expresión, entonces el tipo de devolución de T es un tipo de salidaEcon tipoT.

12.6.3.6 Dependencia

Una variable de tipo no fijo Xᵢdepende directamente de una variable de tipo no fijo Xₑ si para algún argumento Eᵥ con tipo TᵥXₑ ocurre en un tipo de entrada de Eᵥ con tipo Tᵥ y Xᵢ ocurre en un tipo de salida de Eᵥ con tipo Tᵥ.

Xₑ depende deXᵢ si Xₑdepende directamente deXᵢ o si Xᵢdepende directamente deXᵥ y Xᵥdepende deXₑ. Así, "depende de" es el cierre transitivo pero no reflexivo de "depende directamente de".

12.6.3.7 Inferencias de tipo de salida

Una inferencia de tipo de salida se hace de una expresión a un tipo T Ede la siguiente manera:

  • Si E es una función anónima con tipo de retorno inferido U (§12.6.3.13) y T es un tipo delegado o un tipo de árbol de expresión con tipo de retorno Tₓ, entonces se hace una inferencia de límite inferior (§12.6.3.10) deUaTₓ.
  • De lo contrario, si es un grupo de métodos y es un tipo delegado o tipo de árbol de expresión con tipos de parámetros y tipo de valor devuelto , y la resolución de sobrecarga de con los tipos produce un único método con tipo de valor devuelto , entonces se realiza una inferencia de límite inferior dea.
  • En caso contrario, si E es una expresión de tipo U, se realiza una inferencia de límite inferior deUaT.
  • En caso contrario, no se realizan inferencias.

12.6.3.8 Inferencias explícitas de tipo de parámetro

Una inferencia explícita de tipo de parámetro se hace de una expresión Ea un tipo T de la siguiente manera:

  • Si E es una función anónima explícitamente con tipos de parámetros U₁...Uᵥ y T es un tipo delegado o un tipo de árbol de expresión con tipos de parámetros V₁...Vᵥ, entonces para cada Uᵢ se hace una inferencia exacta (§12.6.3.9) deUᵢa la correspondiente Vᵢ.

12.6.3.9 Inferencias exactas

La inferencia exactade un tipo Ua otro tipo V se realiza del siguiente modo:

  • Si V es uno de los Xᵢ no fijos, entonces U se añade al conjunto de límites exactos para Xᵢ.
  • En caso contrario, los conjuntos V₁...Vₑ y U₁...Uₑ se determinan comprobando si se da alguno de los siguientes casos:
    • V es un tipo de matriz V₁[...] y U es un tipo de matriz U₁[...] del mismo rango
    • V es el tipo V₁? y U es el tipo U₁
    • V es un tipo construido C<V₁...Vₑ> y U es un tipo construido C<U₁...Uₑ>
      Si se da alguno de estos casos, se hace una inferencia exacta de cada uno de Uᵢ al correspondiente Vᵢ.
  • En caso contrario, no se realizan inferencias.

12.6.3.10 Inferencias de límite inferior

Una inferencia de límite inferior de un tipo Ua otro tipo V se hace como sigue:

  • Si V es uno de los Xᵢ no fijados, entonces se agrega U al conjunto de límites inferiores para Xᵢ.
  • De lo contrario, si V es el tipo V₁? y U es el tipo U₁? entonces se hace una inferencia de límite inferior de U₁ a V₁.
  • En caso contrario, los conjuntos U₁...Uₑ y V₁...Vₑ se determinan comprobando si se da alguno de los siguientes casos:
    • V es un tipo de matriz V₁[...] y U es un tipo de matriz U₁[...] del mismo rango
    • V es uno de IEnumerable<V₁>, ICollection<V₁>, IReadOnlyList<V₁>>, IReadOnlyCollection<V₁> o IList<V₁> y U es un tipo de matriz unidimensional U₁[]
    • V es un tipo construido class, struct, interface o delegate C<V₁...Vₑ> y hay un tipo único C<U₁...Uₑ> de modo que U (o, si U es un tipo parameter, su clase base efectiva o cualquier miembro de su conjunto de interfaz eficaz) es idéntica a, inherits de (directa o indirectamente) o implementa (directa o indirectamente) C<U₁...Uₑ>.
    • (La restricción de "unicidad" significa que en el caso interfaz C<T>{} class U: C<X>, C<Y>{}, entonces no se hace ninguna inferencia al inferir de U a C<T> porque U₁ podría ser X o Y.)
      Si se aplica cualquiera de estos casos, se realiza una inferencia de cada Uᵢ al correspondiente Vᵢ como se indica a continuación:
    • Si no se sabe que Uᵢ es un tipo de referencia, se hace una inferencia exacta de .
    • De lo contrario, si U es un tipo de matriz, se realiza una inferencia de límite inferior .
    • En caso contrario, si V es C<V₁...Vₑ> entonces la inferencia depende del parámetro de tipo i-th de C:
      • Si es covariante, se realiza una inferencia de límite inferior.
      • Si es contravariante, se realiza una inferencia de límite superior.
      • Si es invariable, se realiza una inferencia exacta de .
  • En caso contrario, no se realizan inferencias.

12.6.3.11 Inferencias de límite superior

Una inferencia de límite superior de un tipo Ua un tipo V se hace como sigue:

  • Si V es uno de los Xᵢ no fijados, U se agrega al conjunto de límites superiores para Xᵢ.
  • En caso contrario, los conjuntos V₁...Vₑ y U₁...Uₑ se determinan comprobando si se da alguno de los siguientes casos:
    • U es un tipo de matriz U₁[...] y V es un tipo de matriz V₁[...] del mismo rango
    • U es uno de IEnumerable<Uₑ>, ICollection<Uₑ>, IReadOnlyList<Uₑ>, IReadOnlyCollection<Uₑ> o IList<Uₑ> y V es un tipo de matriz unidimensional Vₑ[]
    • U es el tipo U1? y V es el tipo V1?
    • U es de tipo clase, struct, interfaz o delegado construido C<U₁...Uₑ> y V es un tipo class, struct, interface o delegate que es identical a, inherits desde (directa o indirectamente) o implementa (directa o indirectamente) un tipo único C<V₁...Vₑ>
    • (La restricción de "unicidad" significa que dada una interfaz C<T>{} class V<Z>: C<X<Z>>, C<Y<Z>>{}, no se realiza ninguna inferencia cuando se infiere de C<U₁> a V<Q>. No se realizan inferencias de U₁ a ni X<Q> o Y<Q>.)
      Si se aplica cualquiera de estos casos, se realiza una inferencia de cada Uᵢ al correspondiente Vᵢ como se indica a continuación:
    • Si no se sabe que Uᵢ es un tipo de referencia, se hace una inferencia exacta de .
    • De lo contrario, si V es un tipo de matriz, se realiza una inferencia de límite superior .
    • En caso contrario, si U es C<U₁...Uₑ> entonces la inferencia depende del parámetro de tipo i-th de C:
      • Si es covariante, se realiza una inferencia de límite superior.
      • Si es contravariante, se realiza una inferencia de límite inferior .
      • Si es invariable, se realiza una inferencia exacta de .
  • En caso contrario, no se realizan inferencias.

12.6.3.12 Corrección

Una variable de tipo no fijadaXᵢ con un conjunto de límites se fija de la siguiente manera:

  • El conjunto de tipos candidatosUₑ comienza como el conjunto de todos los tipos en el conjunto de límites establecidos para Xᵢ.
  • Cada límite para Xᵢ se examina uno por uno: para cada límite exacto U de Xᵢ, todos los tipos Uₑ que no son idénticos a U se eliminan del conjunto candidato. En cada límite inferior U de Xᵢ, se eliminan del conjunto de candidatos todos los tipos Uₑ que no sean el resultado de una conversión implícita a partir de U. En cada límite superior U de Xᵢ, se eliminan del conjunto de candidatos todos los tipos Uₑ que no sean una conversión implícita a U.
  • Si entre los tipos candidatos restantes Uₑ hay un único tipo V al que existe una conversión implícita desde todos los demás tipos candidatos, entonces Xᵢ se fija a V.
  • En caso contrario, la inferencia de tipo falla.

12.6.3.13 Tipo de valor devuelto inferido

El tipo de devolución inferido de una función anónima F se utiliza durante la inferencia de tipos y la resolución de sobrecargas. El tipo de retorno inferido solo puede determinarse para una función anónima en la que se conocen todos los tipos de parámetros, ya sea porque se dan explícitamente, porque se proporcionan a través de una conversión de función anónima o porque se infieren durante la inferencia de tipos en una invocación a un método genérico adyacente.

El tipo de retorno efectivo inferido se determina como sigue:

  • Si el cuerpo de F es una expresión que tiene un tipo, entonces el tipo de retorno efectivo inferido de es el tipo F de esa expresión.
  • Si el cuerpo de F es un bloque y el conjunto de expresiones de las instrucciones return del bloque tiene un tipo más común T (§12.6.3.15), el tipo de valor devuelto efectivo inferido de F es T.
  • De lo contrario, no se puede deducir un tipo de retorno efectivo para F.

El tipo de retorno inferido se determina como sigue:

  • Si F es asíncrono y el cuerpo de F es una expresión clasificada como nada (§12.2) o un bloque donde ninguna de las instrucciones return tiene expresiones, el tipo de valor devuelto inferido es «TaskType» (§15.15.1).
  • Si F es asincrónico y tiene un tipo de retorno efectivo inferido T, el tipo de devolución inferido es «TaskType»<T>»(sección 15.15.1).
  • Si F no es asincrónico y tiene un tipo de retorno efectivo inferido T, el tipo de retorno inferido es T.
  • De lo contrario, no se puede deducir un tipo de retorno para F.

Ejemplo: Como ejemplo de inferencia de tipo que involucra funciones anónimas, considere el método de extensión Select declarado en la clase 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);
            }
        }
   }
}

Suponiendo que el espacio de nombres System.Linq se importó con una directiva using namespace, y dada una clase Customer con una propiedad de tipo Name string, el método Select puede utilizarse para seleccionar los nombres de una lista de clientes:

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

La invocación al método de extensión (sección 12.8.10.3) de Select se procesa reescribiendo la invocación a una invocación a un método estático:

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

Dado que los argumentos de tipo no se especificaron explícitamente, se utiliza la inferencia de tipo para inferir los argumentos de tipo. En primer lugar, el argumento de los clientes está relacionado con el parámetro de origen, infiriendo que TSource es Customer. A continuación, mediante el proceso de inferencia de tipos de función anónima descrito anteriormente, a c se le asigna el tipo Customer, y la expresión c.Name se relaciona con el tipo de valor de retorno del parámetro selector, infiriendo que TResult es string. Así, la invocación es equivalente a

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

y el resultado es de tipo IEnumerable<string>.

El siguiente ejemplo demuestra cómo la inferencia de tipo de función anónima permite que la información de tipo "fluya" entre los argumentos en una invocación de método genérico. Dado el siguiente método e invocación:

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);
    }
}

La inferencia de tipos para la invocación procede de la siguiente manera: En primer lugar, el argumento "1:15:30" se relaciona con el parámetro de valor, infiriéndose que X es string. A continuación, al parámetro de la primera función anónima, s, se le asigna el tipo inferido string, y la expresión TimeSpan.Parse(s) está relacionada con el tipo de retorno de f1, infiriendo que Y es System.TimeSpan. Por último, al parámetro de la segunda función anónima, t, se le asigna el tipo inferido System.TimeSpan, y la expresión t.TotalHours está relacionada con el tipo de retorno de f2, inferido Z como double. Así, el resultado de la invocación es del tipo double.

ejemplo final

12.6.3.14 Inferencia de tipo para la conversión de grupos de métodos

De forma similar a las llamadas de métodos genéricos, la inferencia de tipo también se aplicará cuando un grupo de métodos M que contenga un método genérico se convierta a un tipo delegado determinado D (sección 10.8). Dado que un método

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

y al asignarse el grupo de métodos M al tipo de delegado D, la tarea de inferencia de tipo es buscar argumentos de tipo S₁...Sᵥ para que la expresión:

M<S₁...Sᵥ>

se hace compatible (sección 20.2) con D.

A diferencia del algoritmo de inferencia de tipos para las llamadas a métodos genéricos, en este caso, solo existen tipos de argumentos , sin que haya expresiones de argumentos . En particular, no hay funciones anónimas y por lo tanto no hay necesidad de múltiples fases de inferencia.

En su lugar, todos los Xᵢ se consideran sin fijar y se realiza una inferencia de límite inferior de cada tipo de argumento Uₑ de Dal tipo de parámetro correspondiente Tₑ de M. Si para cualquiera de las Xᵢ no se encontraron límites, la inferencia de tipo falla. De lo contrario, todos los Xᵢ se fijan a las Sᵢ correspondientes, que son el resultado de la inferencia de tipos.

12.6.3.15 Encontrar el mejor tipo común de un conjunto de expresiones

En algunos casos, es necesario inferir un tipo común para un conjunto de expresiones. En particular, los tipos de elementos de matrices implícitamente introducidas y los tipos de retorno de funciones anónimas con cuerpos de bloque se encuentran de esta manera.

El mejor tipo común para un conjunto de expresiones E₁...Eᵥ se determina del siguiente modo:

  • Se introduce una nueva variable de tipo no fijado X.
  • Para cada expresión Ei una inferencia de tipo de salida (§12.6.3.7) se realiza desde ella a X.
  • X es fijo (§12.6.3.12), si es posible, y el tipo resultante es el mejor tipo común.
  • En caso contrario, la inferencia falla.

Nota: Intuitivamente esta inferencia es equivalente a llamar a un método void M<X>(X x₁ ... X xᵥ) con los Eᵢ como argumentos e inferir X. nota final

12.6.4 Resolución de sobrecarga

12.6.4.1 General

La resolución de sobrecargas es un mecanismo de tiempo de enlace para seleccionar el mejor miembro de función para invocar dados una lista de argumentos y un conjunto de miembros de función candidatos. La resolución de sobrecarga selecciona el miembro de función a invocar en los siguientes contextos distintos dentro de C#:

  • Invocación de un método nombrado en una invocation_expression (§12.8.10).
  • Invocación de un constructor de instancia con nombre en una object_creation_expression (§12.8.17.2).
  • Invocación de un descriptor de acceso de indexador mediante un element_access (§12.8.12).
  • Invocación de un operador predefinido o definido por el usuario al que se hace referencia en una expresión (sección 12.4.4 y sección 12.4.5).

Cada uno de estos contextos define el conjunto de miembros candidatos de la función y la lista de argumentos de forma única. Por ejemplo, el conjunto de candidatos para la invocación de un método no incluye los métodos marcados como override (sección 12.5), y los métodos de una clase base no son candidatos si cualquier método de una clase derivada es aplicable (sección 12.8.10.2).

Una vez identificados los miembros de función candidatos y la lista de argumentos, la selección del mejor miembro de función es la misma en todos los casos:

  • En primer lugar, el conjunto de miembros de función candidatos se reduce a aquellos miembros de función que son aplicables con respecto a la lista de argumentos dada (§12.6.4.2). Si este conjunto reducido está vacío, se produce un error de compilación.
  • A continuación, se localiza el mejor miembro de función del conjunto de miembros de función candidatos aplicables. Si el conjunto solo contiene un miembro, este es el mejor. En caso contrario, el mejor miembro de la función es el miembro de la función que es mejor que todos los demás miembros de la función con respecto a la lista de argumentos dada, siempre que cada miembro de la función se compare con todos los demás miembros de la función utilizando las reglas de sección 12.6.4.3. Si no hay exactamente un miembro de función que sea mejor que todos los demás miembros de función, la invocación al miembro de función es ambigua y se produce un error de vinculación.

Las siguientes subcláusulas definen los significados exactos de los términos miembro de función aplicable y miembro de función mejor .

12.6.4.2 Miembro funcional aplicable

Se dice que un miembro de función es un miembro de función aplicable con respecto a una lista de argumentos A cuando se cumplen todas las condiciones siguientes:

  • Cada argumento en A corresponde a un parámetro en la declaración de miembro de función como se describe en la sección 12.6.2.2, como máximo un argumento corresponde a cada parámetro, y cualquier parámetro al que no corresponde ningún argumento es un parámetro opcional.
  • Para cada argumento en A, el modo de paso de parámetros del argumento es idéntico al modo de paso de parámetros del parámetro correspondiente, y
    • para un parámetro de valor o una matriz de parámetros, existe una conversión implícita (sección 10.2) de la expresión del argumento al tipo del parámetro correspondiente, o bien
    • para un parámetro de referencia o de salida, existe una conversión de identidad entre el tipo de la expresión del argumento (si existe) y el tipo del parámetro correspondiente, o
    • para un parámetro de entrada cuando el argumento correspondiente tiene el modificador in, hay una conversión de identidad entre el tipo de la expresión del argumento (si existe) y el tipo del parámetro correspondiente, o
    • para un parámetro de entrada cuando el argumento correspondiente omite el modificador in, existe una conversión implícita (sección 10.2) entre la expresión del argumento y el tipo del parámetro correspondiente.

Para un miembro de función que incluye una matriz de parámetros, si el miembro de función es aplicable según las reglas anteriores, se dice que es aplicable en su forma normal. Si un miembro de función que incluye una matriz de parámetros no es aplicable en su forma normal, el miembro de función puede ser aplicable en su forma expandida:

  • La forma expandida se construye sustituyendo la matriz de parámetros en la declaración del miembro de la función por cero o más parámetros de valor del tipo de elemento de la matriz de parámetros de forma que el número de argumentos de la lista de argumentos A coincida con el número total de parámetros. Si A tiene menos argumentos que el número de parámetros fijos en la declaración del miembro funcional, la forma expandida del miembro funcional no puede construirse y, por tanto, no es aplicable.
  • En caso contrario, la forma expandida es aplicable si para cada argumento en A, se cumple una de las siguientes condiciones:
    • El modo de paso de parámetros del argumento es idéntico al modo de paso de parámetros del parámetro correspondiente y:
      • para un parámetro de valor fijo o un parámetro de valor creado por la expansión, existe una conversión implícita (§10.2) desde la expresión de argumento al tipo del parámetro correspondiente; o
      • para un parámetro por referencia, el tipo de la expresión del argumento es idéntico al tipo del parámetro correspondiente.
    • El modo de paso de parámetros del argumento es por valor, y el modo de paso de parámetros del parámetro correspondiente es de entrada, y existe una conversión implícita (§10.2) desde la expresión de argumento al tipo del parámetro correspondiente.

Cuando la conversión implícita del tipo del argumento al tipo del parámetro de un parámetro de entrada es una conversión implícita dinámica (sección 10.2.10), los resultados son indefinidos.

Ejemplo: Dadas las siguientes declaraciones y llamadas a métodos:

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
}

ejemplo final

  • Un método estático solo es aplicable si el grupo de métodos resulta de un simple_name o de un member_access a través de un tipo.
  • Un método de instancia solo es aplicable si el grupo de métodos resulta de un simple_name, un member_access a través de una variable o valor, o un base_access.
    • Si el grupo de métodos resulta de un simple_name, un método de instancia solo es aplicable si se permite el acceso this §12.8.14.
  • Cuando el grupo de métodos resulta de un member_access que puede ser a través de una instancia o de un tipo, tal como se describe en la sección 12.8.7.2, son aplicables tanto los métodos de instancia como los estáticos.
  • No es aplicable un método genérico cuyos argumentos de tipo (especificados explícitamente o inferidos) no satisfagan todos sus restricciones.
  • En el contexto de una conversión de grupo de métodos, deberá existir una conversión de identidad (sección 10.2.2) o una conversión de referencia implícita (sección 10.2.8) del tipo de retorno del método al tipo de retorno del delegado. En caso contrario, el método candidato no es aplicable.

12.6.4.3 Mejor miembro de función

Para determinar el mejor miembro de la función, se construye una lista simplificada de argumentos A que contiene solo las expresiones de argumento en sí mismas en el orden en que aparecen en la lista de argumentos original, dejando fuera los argumentos out o ref.

Las listas de parámetros para cada uno de los miembros de la función candidata se construyen de la siguiente manera:

  • La forma expandida se utiliza si el miembro de función solo era aplicable en la forma expandida.
  • Los parámetros opcionales sin argumentos correspondientes se eliminan de la lista de parámetros
  • Los parámetros de referencia y de salida se eliminan de la lista de parámetros.
  • Los parámetros se reordenan para que aparezcan en la misma posición que el argumento correspondiente en la lista de argumentos.

Dada una lista de argumentos A con un conjunto de expresiones de argumentos {E₁, E₂, ..., Eᵥ} y dos miembros de función aplicables Mᵥ y Mₓ con tipos de parámetros {P₁, P₂, ..., Pᵥ} y {Q₁, Q₂, ..., Qᵥ}, Mᵥ se define que es un miembro de función mejor que Mₓ si

  • para cada argumento, la conversión implícita de Eᵥ a Qᵥ no es mejor que la conversión implícita de Eᵥ a Pᵥ, y
  • para al menos un argumento, la conversión de Eᵥ a Pᵥ es mejor que la conversión de Eᵥ a Qᵥ.

En caso de que las secuencias de tipo de parámetro {P₁, P₂, ..., Pᵥ} y {Q₁, Q₂, ..., Qᵥ} sean equivalentes (es decir, cada Pᵢ tiene una conversión de identidad a la Qᵢcorrespondiente), se aplican las siguientes reglas de desempate, para determinar el mejor miembro de función.

  • Si Mᵢ es un método no genérico y Mₑ es un método genérico, entonces Mᵢ es mejor que Mₑ.
  • En caso contrario, si Mᵢ es aplicable en su forma normal y Mₑ tiene una matriz params y solo es aplicable en su forma expandida, entonces Mᵢ es mejor que Mₑ.
  • En caso contrario, si ambos métodos tienen matrices de parámetros y solo son aplicables en sus formas expandidas, y si la matriz de parámetros de Mᵢ tiene menos elementos que la matriz de parámetros de Mₑ, entonces Mᵢ es mejor que Mₑ.
  • En caso contrario, si los tipos de los parámetros de Mᵥ son más específicos que los de Mₓ, entonces Mᵥ es mejor que Mₓ. Deje que {R1, R2, ..., Rn} y {S1, S2, ..., Sn} representen los tipos de parámetros no instanciados y no expandidos de Mᵥ y Mₓ. los tipos de parámetro de Mᵥ son más específicos que Mₓ si, para cada parámetro, Rx no es menos específico que Sx, y, para al menos un parámetro, Rx es más específico que Sx:
    • Un parámetro de tipo es menos específico que un parámetro que no es de tipo.
    • Recursivamente, un tipo construido es más específico que otro tipo construido (con el mismo número de argumentos de tipo) si al menos un argumento de tipo es más específico y ningún argumento de tipo es menos específico que el correspondiente argumento de tipo en el otro.
    • Un tipo de matriz es más específico que otro tipo de matriz (con el mismo número de dimensiones) si el tipo de elemento del primero es más específico que el tipo de elemento del segundo.
  • En caso contrario, si un miembro es un operador no elevado y el otro es un operador elevado, el no elevado es mejor.
  • Si ninguno de los miembros de la función resulta ser mejor y todos los parámetros de Mᵥ tienen un argumento correspondiente mientras que los argumentos por defecto deben sustituirse por al menos un parámetro opcional en Mₓ, entonces Mᵥ es mejor que Mₓ.
  • Si al menos un parámetro Mᵥ utiliza la mejor opción de paso de parámetros (sección 12.6.4.4) que el parámetro correspondiente en Mₓ y ninguno de los parámetros en Mₓ utiliza la mejor opción de paso de parámetros que Mᵥ, Mᵥ es mejor que Mₓ.
  • En caso contrario, ningún miembro de la función es mejor.

12.6.4.4 Mejor modo de paso de parámetros

Se permite que los parámetros correspondientes en dos métodos sobrecargados difieran solo por el modo de paso de parámetros siempre que uno de los dos parámetros tenga modo de paso de valor, como sigue:

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

Dado int i = 10;, según §12.6.4.2, las llamadas M1(i) y M1(i + 5) hacen que ambas sobrecargas sean aplicables. En tales casos, el método con el modo de paso de parámetros de valor es la mejor opción de modo de paso de parámetros.

Nota: No existe tal necesidad de elección para los argumentos de los modos de paso de entrada, salida o referencia, ya que esos argumentos solo coinciden exactamente con los mismos modos de paso de parámetros. nota final

12.6.4.5 Mejor conversión de expresión

Dada una conversión implícita C₁ que convierte de una expresión E a un tipo T₁, y una conversión implícita C₂ que convierte de una expresión E a un tipo T₂, C₁ es una conversión mejor que C₂ si se cumple una de las siguientes condiciones:

  • E coincide exactamente con T₁ y E no coincide exactamente con T₂ (sección 12.6.4.6)
  • E coincide exactamente con ambos o ninguno de T₁ y T₂, y T₁ es un destino de conversión mejor que T₂ (§12.6.4.7)
  • E es un grupo de métodos (sección 12.2), T₁ es compatible (sección 20.4) con el mejor método individual del grupo de métodos para la conversión C₁, y T₂ no es compatible con el mejor método individual del grupo de métodos para la conversión C₂

12.6.4.6 Expresión exactamente coincidente

Dada una expresión E y un tipo T, Ecoincide exactamente conT si se cumple una de las siguientes condiciones:

  • E tiene un tipo S y existe una conversión de identidad de S a T
  • E es una función anónima, T es un tipo delegado D o un tipo de árbol de expresión Expression<D> y se cumple una de las siguientes condiciones:
    • Existe un tipo de retorno inferido X para E en el contexto de la lista de parámetros de D (§12.6.3.12), y existe una conversión de identidad de X al tipo de retorno de D
    • E es una lambda async sin valor de devolución, y D tiene un tipo de devolución que no es genérico «TaskType».
    • O E no es asincrónico y D tiene un tipo de valor devuelto Y, o E es asincrónico y D tiene un tipo de valor devuelto «TaskType»<Y>(§15.15.1), y se cumple una de las siguientes condiciones:
      • El cuerpo de E es una expresión que coincide exactamente con Y
      • El cuerpo de E es un bloque donde cada sentencia de retorno devuelve una expresión que coincide exactamente con Y

12.6.4.7 Mejor objetivo de conversión

Dados dos tipos T₁ y T₂, T₁ es un mejor destino de conversión que T₂ si se cumple una de las siguientes condiciones:

  • Existe una conversión implícita de T₁ a T₂ y no existe ninguna conversión implícita de T₂ a T₁
  • T₁ es «TaskType»<S₁>(sección 15.15.1), T₂ es «TaskType»<S₂>, S₁ y es más especializado que S₂
  • T₁ es «TaskType»<S₁>(§15.15.1), T₂ es «TaskType»<S₂>y T₁ es más especializado que T₂
  • T₁ es S₁ o S₁? donde S₁ es un tipo entero con signo y T₂ es S₂ o S₂? donde S₂ es un tipo entero sin signo. En concreto:
    • S₁ es sbyte y S₂ es byte, ushort, uint o ulong
    • S₁ es short y S₂ es ushort, uint o ulong
    • S₁ es int y S₂ es uint o ulong
    • S₁ es long y S₂ es ulong

12.6.4.8 Sobrecarga en clases genéricas

Nota: Aunque las firmas declaradas deben ser únicas (sección 8.6), es posible que la sustitución de argumentos de tipo dé lugar a firmas idénticas. En tal situación, la resolución de sobrecarga elegirá la más específica (§12.6.4.3) de las firmas originales (antes de la sustitución de argumentos de tipo), si existe; de lo contrario, notificará un error. nota final

Ejemplo: Los siguientes ejemplos muestran sobrecargas que son válidas e inválidas según esta regla:

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);
}

ejemplo final

12.6.5 Comprobación en tiempo de compilación de la invocación de miembros dinámicos

Aunque la resolución de la sobrecarga de una operación ligada dinámicamente tiene lugar en tiempo de ejecución, a veces es posible conocer en tiempo de compilación la lista de miembros de función entre los que se elegirá una sobrecarga:

  • Para una invocación de delegado (§12.8.10.4), la lista es un miembro de función único con la misma lista de parámetros que el delegate_type de la invocación.
  • Para una invocación de método (sección 12.8.10.2) sobre un tipo, o sobre un valor cuyo tipo estático no es dinámico, el conjunto de métodos accesibles en el grupo de métodos se conoce en tiempo de compilación.
  • Para una expresión de creación de objeto (sección 12.8.17.2), el conjunto de constructores accesibles en el tipo se conoce en tiempo de compilación.
  • Para un acceso de indexador (§12.8.12.3), el conjunto de indexadores accesibles en el receptor se conoce en tiempo de compilación.

En estos casos se realiza una comprobación limitada en tiempo de compilación de cada miembro del conjunto conocido de miembros de función, para ver si se puede saber con certeza que nunca será invocado en tiempo de ejecución. Para cada miembro de función F se construye una lista modificada de parámetros y argumentos:

  • En primer lugar, si F es un método genérico y se han proporcionado argumentos de tipo, estos se sustituyen por los parámetros de tipo en la lista de parámetros. Sin embargo, si no se han proporcionado argumentos de tipo, no se produce tal sustitución.
  • A continuación, cualquier parámetro cuyo tipo esté abierto (es decir, contiene un parámetro de tipo; vea §8.4.3) se elimina, junto con sus parámetros correspondientes.

Para que F pase la comprobación, se deberán cumplir todas las siguientes condiciones:

  • La lista de parámetros modificada para F es aplicable a la lista de argumentos modificada en los términos de sección 12.6.4.2.
  • Todos los tipos construidos en la lista de parámetros modificada satisfacen sus restricciones (sección 8.4.5).
  • Si los parámetros de tipo de F se sustituyeron en el paso anterior, se satisfacen sus restricciones.
  • Si F se trata de un método estático, el grupo de métodos no debe ser el resultado de un member_access cuyo receptor se sepa en tiempo de compilación que es una variable o un valor.
  • Si F es un método de instancia, el grupo de métodos no puede haberse originado de un member_access cuyo receptor se conoce en tiempo de compilación como un tipo.

Si ningún candidato supera esta prueba, se produce un error de compilación.

12.6.6 Invocación de miembro de función

12.6.6.1 General

Esta subcláusula describe el proceso que tiene lugar en tiempo de ejecución para invocar un determinado miembro de función. Se supone que un proceso de tiempo de enlace ya ha determinado el miembro determinado que se va a invocar, posiblemente aplicando la resolución de sobrecarga a un conjunto de miembros de función candidatos.

Para describir el proceso de invocación, los miembros de función se dividen en dos categorías:

  • Miembros de función estáticos. Se trata de los métodos estáticos, los descriptores de acceso a propiedades estáticas y los operadores definidos por el usuario. Los miembros de función estáticos siempre son no virtuales.
  • Miembros de la función de instancia. Estos son métodos de instancia, constructores de instancia, accesores de propiedades de instancia y accesores de indexador. Los miembros de la función de instancia son no virtuales o virtuales y siempre se invocan en una instancia determinada. La instancia se calcula mediante una expresión de instancia y se vuelve accesible dentro del miembro de la función como this (§12.8.14). Para un constructor de instancia, se toma la expresión de instancia como el objeto recientemente asignado.

El procesamiento en tiempo de ejecución de la invocación de un miembro de función consiste en los siguientes pasos, donde M es el miembro de función y, si M es un miembro de instancia, E es la expresión de instancia:

  • Si M es un miembro de función estático:

    • La lista de argumentos se evalúa como se describe en la sección 12.6.2.
    • Se invoca a M.
  • De lo contrario, si el tipo de E es un tipo de valor V y M se declara o invalida en V:

    • E se evalúa. Si esta evaluación causa una excepción, entonces no se ejecutan más pasos. Para un constructor de instancia, esta evaluación consiste en asignar almacenamiento (típicamente de una pila de ejecución) para el nuevo objeto. En este caso E se clasifica como variable.
    • Si E no se clasifica como variable, o si V no es un tipo struct de solo lectura (sección 16.2.2), y E es uno de los siguientes:

    entonces se crea una variable local temporal de tipo E y el valor de E se asigna a esa variable. E se reclasifica como una referencia a esa variable local temporal. La variable temporal es accesible como this dentro de M, pero no de ninguna otra forma. Así, solo cuando se puede escribir E es posible para el invocador observar los cambios que hace M a this.

    • La lista de argumentos se evalúa como se describe en la sección 12.6.2.
    • Se invoca a M. La variable referenciada por E se convierte en la variable referenciada por this.
  • De lo contrario:

    • E se evalúa. Si esta evaluación causa una excepción, entonces no se ejecutan más pasos.
    • La lista de argumentos se evalúa como se describe en la sección 12.6.2.
    • Si el tipo de E es un value_type, se realiza una conversión boxing (§10.2.9) para convertir E en un class_type y E se considera que es de ese class_type en los siguientes pasos. Si el value_type es un enum_type, el class_type es System.Enum;; de lo contrario, es System.ValueType.
    • Se comprueba si el valor de E es válido. Si el valor de E es NULL, se lanza un System.NullReferenceException y no se ejecutan más pasos.
    • La implementación del miembro de función que se va a invocar se determina:
      • Si el tipo de tiempo de enlace de E es una interfaz, el miembro de función que se va a invocar es la implementación de M proporcionada por el tipo de tiempo de ejecución de la instancia a la que hace referencia E. Este miembro de función se determina aplicando las reglas de asignación de interfaces (§18.6.5) para determinar la implementación de M proporcionada por el tipo en tiempo de ejecución de la instancia referenciada por E.
      • En caso contrario, si M es un miembro de función virtual, el miembro de función a invocar es la implementación de M proporcionada por el tipo en tiempo de ejecución de la instancia referenciada por E. Este miembro de función se determina aplicando las reglas para determinar la implementación más derivada (sección 15.6.4) de M con respecto al tipo de tiempo de ejecución de la instancia referenciada por E.
      • En caso contrario, M es un miembro de función no virtual, y el miembro de función a invocar es el mismo M.
    • Se invoca la implementación de un miembro de función determinada en el paso anterior. El objeto al que hace referencia E se convierte en el objeto al que hace referencia este.

El resultado de la invocación de un constructor de instancia (sección 12.8.17.2) es el valor creado. El resultado de la invocación de cualquier otro miembro de función es el valor, si existe, devuelto (§13.10.5) de su cuerpo.

12.6.6.2 Invocaciones en instancias encapsuladas

Un miembro de función implementado en un value_type se puede invocar a través de una instancia encapsulada de ese value_type en las situaciones siguientes:

  • Cuando el miembro de función es una invalidación de un método heredado del tipo class_type y se invoca a través de una expresión de instancia de ese class_type.

    Nota: el class_type siempre será uno de System.Object, System.ValueType o System.Enumnota final

  • Cuando el miembro de función es una implementación de un miembro de función de interfaz y se invoca a través de una expresión de instancia de un interface_type.
  • Cuando el miembro de una función se invoca a través de un delegado.

En estas situaciones, la instancia encapsulada se considera que contiene una variable del value_type y esta variable se convierte en la variable referenciada por esta dentro de la invocación al miembro de función.

Nota: en particular, esto significa que cuando se invoca un miembro de función sobre una instancia en caja, es posible que el miembro de función modifique el valor contenido en la instancia en caja. nota final

12.7 Deconstrucción

La deconstrucción es un proceso mediante el cual una expresión se convierte en una tupla de expresiones individuales. La deconstrucción se utiliza cuando el objetivo de una asignación simple es una expresión de tupla, con el fin de obtener valores para asignar a cada uno de los elementos de esa tupla.

Una expresión E se deconstruye en una expresión de tupla con elementos n de la siguiente manera:

  • Si E es una expresión de tupla con elementos n, el resultado de la deconstrucción es la expresión E.
  • En caso contrario, si E tiene un tipo de tupla (T1, ..., Tn) con elementos n, entonces se evalúa E en una variable temporal __v, y el resultado de la deconstrucción es la expresión (__v.Item1, ..., __v.Itemn).
  • De lo contrario, si la expresión E.Deconstruct(out var __v1, ..., out var __vn) se resuelve en tiempo de compilación a una instancia única o método de extensión, esa expresión se evalúa, y el resultado de la deconstrucción es la expresión (__v1, ..., __vn). Este método se conoce como deconstructor.
  • En caso contrario, E no se puede deconstruir.

Aquí, __v y __v1, ..., __vn se refieren a variables temporales que de otro modo serían invisibles e inaccesibles.

Nota: No se puede deconstruir una expresión de tipo dynamic. nota final

12.8. Expresiones primarias

12.8.1 General

Las expresiones primarias incluyen las formas más simples de expresiones.

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 reglas gramaticales no están preparadas para ANTLR ya que forman parte de un conjunto de reglas mutuamente recursivas a la izquierda (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 y pointer_element_access) que ANTLR no controla. Las técnicas estándar se pueden usar para transformar la gramática para eliminar la recursividad mutua a la izquierda. Esto no se ha hecho porque no todas las estrategias de análisis lo requieren (por ejemplo, un analizador LALR no lo haría) y hacerlo ofuscaría la estructura y la descripción. nota final

pointer_member_access (sección 23.6.3) y pointer_element_access (sección 23.6.4) solo están disponibles en código no seguro (sección 23).

Las expresiones primarias se dividen entre array_creation_expression y primary_no_array_creation_expression. Tratar la expresión array_creation_expression de esta manera, en lugar de listarla junto con las otras formas de expresión simples, permite a la gramática no permitir código potencialmente confuso como

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

que de otro modo se interpretaría como

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

12.8.2 Literales

Una primary_expression que consta de un literal (§6.4.5) se clasifica como un valor.

12.8.3 Expresiones de cadena interpoladas

Una interpolated_string_expression consiste en $, $@o $@, seguida inmediatamente por texto dentro de los caracteres ". Dentro del texto entrecomillado hay cero o más interpolaciones , delimitadas por los caracteres { y }, cada una de las cuales incluye una expresión junto con especificaciones de formato opcionales.

Las expresiones de cadena interpoladas tienen dos formas; regular (interpolated_regular_string_expression) y verbatim (interpolated_verbatim_string_expression); que son léxicamente similares a, pero difieren semánticamente de, las dos formas de literales de cadena (§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 de las reglas léxicas definidas anteriormente son sensibles al contexto, como se indica a continuación:

Regla Requisitos Contextuales
Interpolated_Regular_String_Mid Solo se reconoce después de un Interpolated_Regular_String_Start, entre las interpolaciones siguientes y antes del Interpolated_Regular_String_End correspondiente.
Regular_Interpolation_Format Solo se reconoce dentro de una regular_interpolation y cuando los dos puntos iniciales (:) no están anidados dentro de ningún tipo de corchete (paréntesis/llaves/cuadrado).
Interpolated_Regular_String_End Solo se reconoce después de un Interpolated_Regular_String_Start y solo si los tokens intermedios son Interpolated_Regular_String_Mids o tokens que pueden formar parte de regular_interpolations, incluidos los tokens de cualquier interpolated_regular_string_expressions contenidos en dichas interpolaciones.
Interpolated_Verbatim_String_MidVerbatim_Interpolation_FormatInterpolated_Verbatim_String_End El reconocimiento de estas tres reglas sigue el de las reglas correspondientes de arriba, donde cada regla de gramática normal mencionada se reemplaza por la regla textual correspondiente.

Nota: Las reglas anteriores son sensibles al contexto, ya que sus definiciones se superponen con las de otros tokens del lenguaje. nota final

Nota: la gramática anterior no está preparada para ANTLR debido a las reglas léxicas sensibles al contexto. Al igual que otros generadores de lexer, ANTLR soporta reglas léxicas sensibles al contexto, por ejemplo usando sus modos léxicos, pero esto es un detalle de implementación y por lo tanto no forma parte de esta especificación. nota final

Una interpolated_string_expression se clasifica como un valor. Si se convierte inmediatamente a System.IFormattable o System.FormattableString con una conversión implícita de cadena interpolada (sección 10.2.5), la expresión de cadena interpolada tiene ese tipo. En caso contrario, tiene el tipo string.

Nota: las diferencias entre los tipos posibles de interpolated_string_expression se pueden determinar a partir de la documentación de System.String (§C.2) y System.FormattableString (§C.3). nota final

El significado de una interpolación, tanto regular_interpolation como verbatim_interpolation, es dar formato al valor de la expresión como string según el formato especificado por el Regular_Interpolation_Format o Verbatim_Interpolation_Format, o según un formato predeterminado para el tipo de expresión . A continuación, el interpolation_minimum_width modifica la cadena formateada para generar el string final que se va a interpolar en la interpolated_string_expression.

Nota: Cómo se detalla la determinación del formato predeterminado de un tipo se explica en la documentación de System.String (§C.2) y System.FormattableString (§C.3). Las descripciones de formatos estándar, que son idénticas para Regular_Interpolation_Format y Verbatim_Interpolation_Format, se pueden encontrar en la documentación de System.IFormattable (§C.4) y en otros tipos de la biblioteca estándar (§C). nota final

En un interpolation_minimum_width, la constant_expression deberá tener una conversión implícita a int. Deje que el ancho del campo sea el valor absoluto de esta expresión constante y que la alineación sea el signo (positivo o negativo) del valor de esta expresión constante:

  • Si el valor de la anchura de campo es menor o igual que la longitud de la cadena formateada, esta no se modifica.
  • En caso contrario, la cadena formateada se rellena con espacios en blanco para que su longitud sea igual a la anchura del campo:
    • Si la alineación es positiva, la cadena con formato está alineada a la derecha con el relleno,
    • De lo contrario, se alinea a la izquierda añadiendo el relleno.

El significado global de una interpolated_string_expression, incluidos el formateo y el relleno de interpolaciones anteriores, se define mediante una conversión de la expresión a una invocación de método: si el tipo de la expresión es System.IFormattable o System.FormattableString ese método es System.Runtime.CompilerServices.FormattableStringFactory.Create (sección C.3) que devuelve un valor de tipo System.FormattableString; en caso contrario, el tipo será string y el método es string.Format (sección C.2) que devuelve un valor de tipo string.

En ambos casos, la lista de argumentos de la llamada consta de un literal de cadena de formato con especificaciones de formato para cada interpolación, y un argumento para cada expresión correspondiente a las especificaciones de formato.

El literal de cadena de formato se construye de la siguiente manera, donde N es el número de interpolaciones en la interpolated_string_expression. El literal de cadena de formato consta de lo siguiente, en orden:

  • Los caracteres de Interpolated_Regular_String_Start o Interpolated_Verbatim_String_Start
  • Los caracteres de Interpolated_Regular_String_Mid o Interpolated_Verbatim_String_Mid, si los hay
  • A continuación, si N ≥ 1 para cada número I de 0 a N-1:
    • Especificación de marcador de posición:
      • Un carácter de llave izquierda ({)
      • La representación decimal de I
      • A continuación, si la correspondiente regular_interpolation o verbatim_interpolation tiene un interpolation_minimum_width, una coma (,) va seguida de la representación decimal del valor de constant_expression
      • Los caracteres del Regular_Interpolation_Format o Verbatim_Interpolation_Format, si existen, de la correspondiente regular_interpolation o verbatim_interpolation
      • Un carácter de llave derecha (})
    • Los caracteres del Interpolated_Regular_String_Mid o Interpolated_Verbatim_String_Mid inmediatamente después de la interpolación correspondiente, si la hay
  • Por último, los caracteres de Interpolated_Regular_String_End o Interpolated_Verbatim_String_End.

Los argumentos siguientes son las expressions de las interpolaciones, si las hay, en orden.

Cuando una interpolated_string_expression contiene varias interpolaciones, las expresiones de dichas interpolaciones se evalúan en orden textual de izquierda a derecha.

Ejemplo:

Este ejemplo utiliza las siguientes características de especificación de formato:

  • la especificación de formato X que da formato a enteros como hexadecimal en mayúsculas,
  • el formato por defecto de un valor string es el valor mismo,
  • valores de alineación positivos que se justifican a la derecha dentro del ancho mínimo de campo especificado,
  • valores de alineación negativos que se justifican a la izquierda dentro del ancho mínimo de campo especificado,
  • constantes definidas para el interpolation_minimum_width, y
  • que {{ y }} se formatean como { y } respectivamente.

Con estas premisas:

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

Después:

Expresión de cadena 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"

ejemplo final

12.8.4 Nombres simples

Un simple_name consiste en un identificador, opcionalmente seguido de una lista de argumentos de tipo:

simple_name
    : identifier type_argument_list?
    ;

Un simple_name es de la forma I o de la forma I<A₁, ..., Aₑ>, donde I es un identificador único y I<A₁, ..., Aₑ> es un type_argument_listopcional. Si no se especifica ningún type_argument_list, considera que e es cero. El simple_name se evalúa y clasifica del siguiente modo:

  • Si e es cero y el simple_name aparece dentro de un espacio de declaración de variable local (sección 7.3) que contiene directamente una variable, parámetro o constante local con nombre I, entonces el simple_name se refiere a esa variable, parámetro o constante local y se clasifica como variable o valor.
  • Si e es cero y el simple_name aparece dentro de una declaración de método genérico pero fuera de los atributos de su method_declaration, y si esa declaración incluye un parámetro de tipo con nombre I, entonces el simple_name se refiere a ese parámetro de tipo.
  • En caso contrario, para cada tipo de instancia T (§15.3.2), empezando por el tipo de instancia de la declaración de tipo inmediatamente contigua y continuando con el tipo de instancia de cada declaración de clase o estructura contigua (si existe):
    • Si e es cero y la declaración de T incluye un parámetro de tipo con nombre I, entonces el simple_name se refiere a ese parámetro de tipo.
    • De lo contrario, si una búsqueda de miembros (§12.5) de I en T con argumentos de tipo e da como resultado una coincidencia:
      • Si T es el tipo de instancia de la clase o tipo struct que lo encierra inmediatamente y la búsqueda identifica uno o más métodos, el resultado es un grupo de métodos con una expresión de instancia asociada de this. Si se ha especificado una lista de argumentos de tipo, se utiliza para llamar a un método genérico (sección 12.8.10.2).
      • De lo contrario, si T es el tipo de instancia de la clase o el tipo de estructura inmediatamente envolvente, si la búsqueda identifica un miembro de instancia y si la referencia se produce dentro del bloque de un constructor de instancia, un método de instancia o un descriptor de acceso de instancia (§12.2.1), el resultado es el mismo que un acceso de miembro (§12.8.7) de la forma this.I. Esto solo puede ocurrir cuando e es cero.
      • De lo contrario, el resultado es el mismo que un acceso de miembro (§12.8.7) de la forma T.I o T.I<A₁, ..., Aₑ>.
  • De lo contrario, para cada espacio de nombres N, comenzando por el espacio de nombres en el que aparece el simple_name, continuando con cada espacio de nombres adyacente (si lo hay) y terminando con el espacio de nombres global, se evalúan los siguientes pasos hasta que se localiza una entidad:
    • Si e es cero y I es el nombre de un espacio de nombres en N, entonces:
      • Si la ubicación en la que se produce el simple_name está incluida dentro de una declaración de espacio de nombres para N y la declaración de espacio de nombres contiene una extern_alias_directive o una using_alias_directive que asocia el nombre I con un espacio de nombres o un tipo, entonces el simple_name es ambiguo y se produce un error en tiempo de compilación.
      • En caso contrario, el simple_name hace referencia al espacio de nombres nombrado I en N.
    • De lo contrario, si N contiene un tipo accesible cuyo nombre es I y tiene e parámetros de tipo, entonces:
      • Si e es cero y la ubicación donde aparece simple_name está encerrada por una declaración de espacio de nombres para N y la declaración de espacio de nombres contiene una extern_alias_directive o using_alias_directive que asocia el nombre I con un espacio de nombres o tipo, entonces simple_name es ambiguo y se produce un error de compilación.
      • En caso contrario, el namespace_or_type_name hace referencia al tipo construido con los argumentos de tipo dados.
    • De lo contrario, si la ubicación donde ocurre el simple_name está incluida en una declaración de espacio de nombres para N:
      • Si e es cero y la declaración del espacio de nombres contiene una directiva extern_alias_directive o using_alias_directive que asocia el nombre I con un espacio de nombres o tipo importado, entonces el simple_name se refiere a ese espacio de nombres o tipo.
      • De lo contrario, si los espacios de nombres importados por las using_namespace_directives de la declaración de espacio de nombres contienen exactamente un tipo que tiene parámetros de nombre I y tipo e, el simple_name hace referencia a ese tipo construido con los argumentos de tipo especificados.
      • De lo contrario, si los espacios de nombres importados por las using_namespace_directives de la declaración de espacio de nombres contienen más de un tipo con los parámetros de nombre I y tipo e, el simple_name es ambiguo y se produce un error en tiempo de compilación.

    Nota: Todo este paso es exactamente paralelo al paso correspondiente en el procesamiento de un namespace_or_type_name (sección 7.8). nota final

  • De lo contrario, si e es cero y I es el identificador _, el simple_name es un simple descarte, que es una forma de expresión de declaración (§12.17).
  • De lo contrario, el simple_name es indefinido y se produce un error de compilación.

12.8.5 Expresiones entre paréntesis

Una parenthesized_expression consta de una expresión entre paréntesis.

parenthesized_expression
    : '(' expression ')'
    ;

Una parenthesized_expression se evalúa evaluando la expresión dentro de los paréntesis. Si la expresión entre paréntesis denota un espacio de nombres o un tipo, se produce un error de compilación. De lo contrario, el resultado de la parenthesized_expression es el resultado de la evaluación de la expresión contenida.

12.8.6 Expresiones de tupla

Una tuple_expression representa una tupla y consta de dos o más expressions separadas por comas y, opcionalmente, incluidas entre paréntesis. Una deconstruction_expression es una sintaxis abreviada para una tupla que contiene expresiones de declaración con tipos implícitos.

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
    ;

Una tuple_expression se clasifica como una tupla.

Una deconstruction_expressionvar (e1, ..., en) es una abreviatura de la tuple_expression(var e1, ..., var en) y sigue el mismo comportamiento. Esto se aplica de forma recursiva a cualquier deconstruction_tuples que esté anidada en la deconstruction_expression. Cada identificador anidado dentro de una deconstruction_expression introduce una expresión de declaración (sección 12.17). Como resultado, una deconstruction_expression solo puede ocurrir en el lado izquierdo de una asignación simple.

Una expresión de tupla tiene un tipo solo si cada una de sus expresiones de elemento Ei tiene un tipo Ti. El tipo será un tipo de tupla de la misma aridad que la expresión de tupla, donde cada elemento se define por lo siguiente:

  • Si el elemento de la tupla en la posición correspondiente se llama Ni, entonces el elemento de tipo de tupla será Ti Ni.
  • De lo contrario, si Ei tiene el formato Ni o E.Ni o E?.Ni, el elemento de tipo de tupla será Ti Ni, a menos que se cumpla alguna de las siguientes condiciones:
    • Otro elemento de la expresión de tupla tiene el nombre Ni, o
    • Otro elemento de la tupla sin nombre tiene una expresión de elemento de tupla de la forma Ni o E.Ni o E?.Ni o
    • Ni es del formato ItemX, donde X es una secuencia de dígitos decimales que no se inicia con0y que podría representar la posición de un elemento de tupla, mientras que X no representa la posición del elemento.
  • De lo contrario, el elemento de tipo de tupla deberá ser Ti.

Una expresión de tupla se evalúa mediante el análisis de cada una de sus expresiones de elemento en orden de izquierda a derecha.

Un valor de tupla se puede obtener de una expresión de tupla mediante la conversión a un tipo de tupla (§10.2.13), reclasificándolo como un valor (§12.2.2) o como destino de una asignación deconstruida (§12.21.2).

Ejemplo:

(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

En este ejemplo, las cuatro expresiones de tuplas son válidas. Los dos primeras, t1 y t2, no usan el tipo de la expresión de tupla, sino que, en su lugar, aplican una conversión de tupla implícita. En el caso de t2, la conversión de tupla implícita se basa en las conversiones implícitas de 2 a long y de null a string. La tercera expresión de tupla tiene un tipo (int i, string), y por lo tanto puede ser reclasificada como un valor de ese tipo. La declaración de t4, por otro lado, es un error: la expresión de tupla no tiene tipo porque su segundo elemento carece de tipo.

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

En este ejemplo se muestra que las tuplas a veces pueden provocar varias capas de paréntesis, especialmente cuando la expresión de tupla es el único argumento para una invocación de método.

ejemplo final

12.8.7 Acceso a miembros

12.8.7.1 General

Un member_access consiste en una primary_expression, un predefined_type o un qualified_alias_member, seguido de un token ".", seguido de un identificador y opcionalmente seguido de una 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'
    ;

La producción de qualified_alias_member se define en §14.8.

Un member_access es de la forma E.I o de la forma E.I<A₁, ..., Aₑ>, donde E es una primary_expression, predefined_type o qualified_alias_member,I es un identificador único y <A₁, ..., Aₑ> es una type_argument_list opcional. Si no se especifica ningún type_argument_list, considera que e es cero.

Un member_access con una primary_expression del tipo dynamic se vincula dinámicamente (§12.3.3). En este caso, el compilador clasifica el acceso a miembro como un acceso a propiedad de tipo dynamic. Las reglas siguientes para determinar el significado del member_access se aplican en tiempo de ejecución, mediante el tipo en tiempo de ejecución en lugar del tipo en tiempo de compilación de la primary_expression. Si esta clasificación en tiempo de ejecución conduce a un grupo de métodos, el acceso de miembro será la primary_expression de una invocation_expression.

El member_access se evalúa y clasifica de la siguiente manera:

  • Si e es cero y E es un espacio de nombres y E contiene un espacio de nombres anidado con nombre I, entonces el resultado es ese espacio de nombres.
  • En caso contrario, si E es un espacio de nombres y E contiene un tipo accesible con nombre I y parámetros de tipo K, entonces el resultado es ese tipo construido con los argumentos de tipo dados.
  • Si E se clasifica como un tipo E, si no es un parámetro de tipo, y si una búsqueda de miembros (sección 12.5) de I en E con parámetros de tipo K produce una coincidencia, entonces E.I se evalúa y clasifica como sigue:

    Nota: Cuando el resultado de dicha búsqueda de miembros es un grupo de métodos y K es cero, el grupo de métodos puede contener métodos con parámetros de tipo. Esto permite que tales métodos sean considerados para la inferencia de argumentos de tipo. nota final

    • Si I identifica un tipo, entonces el resultado es ese tipo construido con cualquier argumento de tipo dado.
    • Si I identifica uno o más métodos, el resultado es un grupo de métodos sin expresión de instancia asociada.
    • Si I identifica una propiedad estática, el resultado es un acceso a una propiedad sin expresión de instancia asociada.
    • Si I identifica un campo estático:
      • Si el campo es de solo lectura y la referencia se produce fuera del constructor estático de la clase o estructura en la que se declara el campo, el resultado es un valor, es decir, el valor del campo estático I en E.
      • En caso contrario, el resultado es una variable, es decir, el campo estático I en E.
    • Si I identifica un evento estático:
      • Si la referencia ocurre dentro de la clase o estructura en la que se declara el evento, y el evento se declaró sin event_accessor_declarations (sección 15.8.1), entonces E.I se procesa exactamente como si I fuera un campo estático.
      • En caso contrario, el resultado es un acceso a un evento sin expresión de instancia asociada.
    • Si I identifica una constante, el resultado es un valor, es decir, el valor de dicha constante.
    • Si I identifica un miembro de enumeración, el resultado es un valor, es decir, el valor de ese miembro de enumeración.
    • En caso contrario, E.I es una referencia de miembro no válida y se produce un error de compilación.
  • Si E es un acceso de propiedad, acceso de indexador, variable o valor, cuyo tipo es T, y una búsqueda de miembros (§12.5) de I en T con argumentos de tipo K produce una coincidencia, E.I se evalúa y clasifica de la siguiente manera:
    • En primer lugar, si E es una propiedad o un acceso a un indexador, entonces se obtiene el valor de la propiedad o del acceso al indexador (sección 12.2.2) y E se reclasifica como un valor.
    • Si I identifica uno o más métodos, el resultado es un grupo de métodos con una expresión de instancia asociada de E.
    • Si I identifica una propiedad de instancia, el resultado es un acceso a una propiedad con una expresión de instancia asociada de E y un tipo asociado que es el tipo de la propiedad. Si T es un tipo de clase, el tipo asociado se elige de la primera declaración o invalidación de la propiedad encontrada al comenzar con T y buscar en sus clases base.
    • Si T es un class_type y I identifica un campo de instancia de ese class_type:
      • Si el valor de E es null, se produce un System.NullReferenceException.
      • En caso contrario, si el campo es de solo lectura y la referencia se produce fuera de un constructor de instancia de la clase en la que está declarado el campo, el resultado es un valor, es decir, el valor del campo I en el objeto referenciado por E.
      • En caso contrario, el resultado es una variable, es decir, el campo I del objeto referenciado por E.
    • Si T es un struct_type e identifica I un campo de instancia de ese struct_type:
      • Si E es un valor, o si el campo es de solo lectura y la referencia se produce fuera de un constructor de instancia del struct en el que se declara el campo, entonces el resultado es un valor, es decir, el valor del campo I en la instancia del struct dada por E.
      • En caso contrario, el resultado es una variable, es decir, el campo I de la instancia struct dado por E.
    • Si I identifica un evento de instancia:
      • Si la referencia ocurre dentro de la clase o estructura en la que se declara el evento, y el evento fue declarado sin event_accessor_declarations (§15.8.1), y la referencia no ocurre como el lado izquierdo del operador a += o -=, entonces E.I se procesa exactamente como si I fuera un campo de instancia.
      • De lo contrario, el resultado es un acceso a eventos con una expresión de instancia asociada de E.
  • En caso contrario, se intenta procesar E.I como una invocación a un método de extensión (sección 12.8.10.3). Si esto falla, E.I es una referencia de miembro no válida y se produce un error de tiempo de enlace.

12.8.7.2 Nombres simples y nombres de tipo idénticos

En un acceso de miembro del formulario E.I, si E es un identificador único y si el significado de E como un simple_name (§12.8.4) es una constante, campo, propiedad, variable local o parámetro con el mismo tipo que el significado de E como un type_name (§7.8.1), a continuación, se permiten ambos significados posibles de E. La búsqueda de miembros de E.I nunca es ambigua, ya que I será necesariamente un miembro del tipo E en ambos casos. En otras palabras, la regla simplemente permite el acceso a los miembros estáticos y los tipos anidados de E donde de otro modo se habría producido un error en tiempo de compilación.

Ejemplo:

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

Solo para fines expositivos, dentro de la clase A, esas apariciones del identificador Color que hacen referencia al tipo Color están delimitadas por «...», y las que hacen referencia al campo Color no están delimitadas.

ejemplo final

12.8.8 Acceso a miembro condicional nulo

Un null_conditional_member_access es una versión condicional de member_access (§12.8.7) y es un error en tiempo de vinculación si el tipo de resultado es void. Para una expresión condicional nula en la que el tipo de resultado puede ser void consulte (sección 12.8.11).

Un null_conditional_member_access consiste en una primary_expression seguida de los dos tokens "?" y ".", seguido de un identificador con una type_argument_list opcional, seguida de cero o más dependent_accesses, de los cuales cualquiera puede ir precedido de un 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?
    ;

Una expresión null_conditional_member_access E tiene el formato P?.A. El significado de E se determina como sigue:

  • Si el tipo de P es un tipo de valor con nulidad:

    Sea T el tipo de P.Value.A.

    • Si T es un parámetro de tipo que no se sabe si es un tipo de referencia o un tipo de valor no anulable, se produce un error de compilación.

    • Si T es un tipo de valor no anulable, entonces el tipo de E es T?, y el significado de E es el mismo que el significado de:

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

      Excepto que P solo se evalúa una vez.

    • De lo contrario, el tipo de E es T, y el significado de E es el mismo que el significado de:

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

      Excepto que P solo se evalúa una vez.

  • De lo contrario:

    Sea T el tipo de la expresión P.A.

    • Si T es un parámetro de tipo que no se sabe si es un tipo de referencia o un tipo de valor no anulable, se produce un error de compilación.

    • Si T es un tipo de valor no anulable, entonces el tipo de E es T?, y el significado de E es el mismo que el significado de:

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

      Excepto que P solo se evalúa una vez.

    • De lo contrario, el tipo de E es T, y el significado de E es el mismo que el significado de:

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

      Excepto que P solo se evalúa una vez.

Nota: en una expresión de la forma:

P?.A₀?.A₁

después, si P se evalúa como null ni A₀ ni A₁ se evalúan. Lo mismo sucede si una expresión es una secuencia de operaciones null_conditional_member_access o null_conditional_element_access§12.8.13.

nota final

Un null_conditional_projection_initializer es una restricción de null_conditional_member_access y tiene la misma semántica. Solo aparece como inicializador de proyección en una expresión de creación de objeto anónimo (sección 12.8.17.7).

12.8.9 Expresiones que admiten valores NULL

12.8.9.1 General

El valor, tipo, clasificación de una expresión que admite valores NULL (§12.2) y el contexto seguro (§16.4.12) es el valor, el tipo, la clasificación y el contexto seguro de su primary_expression.

null_forgiving_expression
    : primary_expression null_forgiving_operator
    ;

null_forgiving_operator
    : '!'
    ;

Nota: los operadores de negación lógica de prefijo y de postfijo que admiten valores NULL (§12.9.4), aunque se representan mediante el mismo token léxico (!), son distintos. Solo el último puede ser reemplazado (§15.10), la definición del operador que perdona valores nulos está fijada. nota final

Es un error en tiempo de compilación aplicar el operador null-forgiving más de una vez a la misma expresión, independientemente de los paréntesis que se interpongan.

Ejemplo: todo lo siguiente no es válido:

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

ejemplo final

El resto de esta subcláusula y las siguientes subcláusulas hermanas son condicionalmente normativas.

Un compilador que realice un análisis estático del estado nulo (sección 8.9.5) debe ajustarse a la siguiente especificación.

El operador que admite valores NULL es una pseudooperación en tiempo de compilación que se usa para notificar al análisis de estado nulo estático del compilador. Tiene dos usos: anular la determinación de un compilador de que una expresión tal vez NULL; y anular la emisión por parte de un compilador de una advertencia relacionada con la anulabilidad.

Aplicar el operador null-forgiving a una expresión para la cual el análisis estático de estado nulo de un compilador no produce ninguna advertencia no es un error.

12.8.9.2 Anulación de una determinación "tal vez NULL"

En algunas circunstancias, el análisis estático de estado NULL de un compilador puede determinar que una expresión tiene el estado nulo tal vez NULL y emitir una advertencia de diagnóstico cuando otra información indica que la expresión no puede ser NULL. Al aplicar el operador null-forgiving a dicha expresión, se informa al análisis de estado null estático del compilador que el estado null está en y no en null, lo cual impide la advertencia de diagnóstico y puede informar a cualquier análisis en curso.

Ejemplo: Considere lo siguiente:

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

Si IsValid devuelve true, p puede ser dereferenciada con seguridad para acceder a su propiedad Name, y la advertencia de "dereferenciación de un valor posiblemente NULL" puede ser suprimida usando !.

ejemplo final

Ejemplo: El operador null-forgiving debe utilizarse con precaución, considere:

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

Aquí se aplica el operador null-forgiving a un tipo de valor y anula cualquier advertencia sobre x. Sin embargo, si x es null en tiempo de ejecución, se producirá una excepción ya que null no se puede convertir a int.

ejemplo final

12.8.9.3 Invalidación de otras advertencias de análisis nulo

Además de invalidar las determinaciones de puede ser nulo como se indicó anteriormente, puede haber otras circunstancias en las que se desee invalidar la determinación del análisis de estado de nulidad estático de un compilador, para las cuales una expresión podría requerir una o más advertencias. La aplicación del operador null-forgiving a una expresión de este tipo solicita que el compilador no emita ninguna advertencia para la expresión. En respuesta, un compilador puede optar por no emitir advertencias y también puede modificar su análisis posterior.

Ejemplo: Considere lo siguiente:

#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;
}

Los tipos de los parámetros del método Assign, lv & rv, consisten en string?, con lv siendo este un parámetro de salida y que realiza una asignación sencilla.

El método M pasa la variable s, de tipo string, como parámetro de salida para Assign; el compilador utilizado emite una advertencia ya que s no es una variable que acepta valores nulos. Dado que el segundo argumento de Assignno puede ser null, se usa el operador null-forgiving para anular la advertencia.

ejemplo final

Fin del texto normativo condicional.

12.8.10 Expresiones de invocación

12.8.10.1 General

Una invocation_expression se usa para invocar un método.

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

La primary_expression puede ser una null_forgiving_expression solo si tiene un delegate_type.

Un invocation_expression se enlaza dinámicamente (§12.3.3) si se cumple al menos una de las siguientes condiciones:

  • La primary_expression tiene el tipo dynamicen tiempo de compilación.
  • Al menos un argumento de la argument_list opcional tiene el tipo en el tiempo de compilación dynamic.

En este caso, el compilador clasifica la invocation_expression como un valor de tipo dynamic. Las reglas siguientes para determinar el significado de la invocation_expression se aplican en tiempo de ejecución, usando el tipo en tiempo de ejecución en lugar del tipo en tiempo de compilación de la primary_expression y los argumentos que tienen el tipo en tiempo de compilación dynamic. Si la expresión primaria no tiene el tipo en tiempo de compilación dynamic, entonces la invocación del método se somete a una comprobación limitada en tiempo de compilación, tal como se describe en §12.6.5.

La primary_expression de una invocation_expression será un grupo de métodos o un valor de un delegate_type. Si la primary_expression es un grupo de métodos, la invocation_expression es una invocación de método (sección 12.8.10.2) Si la primary_expression es un valor de un delegate_type, la invocation_expression es una invocación de delegado (sección 12.8.10.4). Si la expresión primaria no es un grupo de métodos ni un valor de un tipo delegado , se produce un error en tiempo de vinculación.

El argument_list opcional (sección 12.6.2) proporciona valores o referencias a variables para los parámetros del método.

El resultado de la evaluación de una invocation_expression se clasifica como sigue:

  • Si el invocation_expression invoca un método que no devuelve valor (§15.6.1) o un delegado que no devuelve valor, el resultado es nulo. Una expresión clasificada como nada solo se permite en el contexto de una statement_expression (sección 13.7) o como cuerpo de una lambda_expression (sección 12.19). De lo contrario, se produce un error en tiempo de enlace.
  • En caso contrario, si la invocation_expression invoca un método returns-by-ref (§15.6.1) o un delegado returns-by-ref, el resultado es una variable con un tipo asociado del tipo de retorno del método o delegado. Si la invocación es de un método de instancia y el receptor es de un tipo de clase T, el tipo asociado se elige de la primera declaración o invalidación del método encontrado al comenzar con T y buscar entre sus clases base.
  • De lo contrario, la invocation_expression invoca un método de retorno por valor (§15.6.1) o un delegado de retorno por valor, y el resultado es un valor, con un tipo asociado al tipo de retorno del método o delegado. Si la invocación es de un método de instancia y el receptor es de un tipo de clase T, el tipo asociado se elige de la primera declaración o invalidación del método encontrado al comenzar con T y buscar entre sus clases base.

12.8.10.2 Invocaciones de métodos

Para una invocación de método, el primary_expression del invocation_expression deberá ser un grupo de métodos. El grupo de métodos identifica el método a invocar o el conjunto de métodos sobrecargados entre los que elegir un método específico a invocar. En este último caso, la determinación del método específico a invocar se basa en el contexto proporcionado por los tipos de los argumentos de argument_list.

El procesamiento en tiempo de enlace de una invocación de método de la forma M(A), donde M es un grupo de métodos (posiblemente incluyendo una lista de argumentos de tipo ), y A es una lista de argumentos opcional, consiste en los siguientes pasos:

  • Se construye el conjunto de métodos candidatos para la invocación del método. Para cada método F asociado al grupo de métodos M:
    • Si F no es genérico F, es un candidato cuando:
      • M no tiene lista de argumentos de tipo, y
      • F es aplicable con respecto a A (sección 12.6.4.2).
    • Si F es genérico y M no tiene lista de argumentos de tipo F, es candidato cuando:
      • La inferencia de tipos (§12.6.3) se realiza correctamente, infiriendo una lista de argumentos de tipo para la llamada.
      • Una vez que los argumentos de tipo inferidos se sustituyen por los parámetros de tipo de método correspondientes, todos los tipos construidos en la lista de parámetros de F satisfacen sus restricciones (sección 8.4.5), y la lista de parámetros de F es aplicable con respecto a A (sección 12.6.4.2).
    • Si F es genérico y M incluye una lista de argumentos de tipo, F es un candidato cuando:
      • F tiene el mismo número de parámetros de tipo de método que los suministrados en la lista de argumentos de tipo, y
      • Una vez sustituidos los argumentos de tipo por los correspondientes parámetros de tipo de método, todos los tipos construidos en la lista de parámetros de F satisfacen sus restricciones (sección 8.4.5), y la lista de parámetros de F es aplicable con respecto a A (sección 12.6.4.2).
  • El conjunto de métodos candidatos se reduce para contener solo métodos de los tipos más derivados: Para cada método C.F del conjunto, donde C es el tipo en el que se declara el método F, se eliminan del conjunto todos los métodos declarados en un tipo base de C. Además, si C es un tipo de clase distinto de object, todos los métodos declarados en un tipo de interfaz se eliminan del conjunto.

    Nota: Esta última regla solo tiene efecto cuando el grupo de métodos es el resultado de una búsqueda de miembros en un parámetro de tipo que tiene una clase base efectiva distinta de object y un conjunto de interfaces efectivo no vacío. nota final

  • Si el conjunto de métodos candidatos resultante está vacío, se abandona el procesamiento posterior a lo largo de los pasos siguientes y, en su lugar, se intenta procesar la invocación como una invocación a un método de extensión (sección 12.8.10.3). Si esto falla, no existen métodos aplicables y se produce un error de tiempo de enlace.
  • El mejor método del conjunto de métodos candidatos se identifica utilizando las reglas de resolución de sobrecargas de sección 12.6.4. Si no se puede identificar el mejor método, la invocación del método es ambigua y se produce un error de vinculación. Al realizar la resolución de sobrecarga, los parámetros de un método genérico se consideran después de sustituir los argumentos de tipo (proporcionados o inferidos) por los correspondientes parámetros de tipo de método.

Una vez que se ha seleccionado y validado un método en tiempo de enlace mediante los pasos anteriores, la invocación real en tiempo de ejecución se procesa según las reglas de invocación de miembros de función descritas en la sección 12.6.6.

Nota: El efecto intuitivo de las reglas de resolución descritas anteriormente es el siguiente: Para localizar el método concreto invocado por una invocación de método, comience con el tipo indicado por la invocación de método y proceda hacia arriba en la cadena de herencia hasta que se encuentre al menos una declaración de método aplicable, accesible y no sobrecargado. A continuación, realice la inferencia de tipos y la resolución de sobrecargas en el conjunto de métodos aplicables, accesibles y no anulables declarados en ese tipo e invoque al método seleccionado. Si no se encuentra ningún método, intenta procesar la invocación como una invocación a un método de extensión. nota final

12.8.10.3 Invocaciones a métodos de extensión

En una invocación de método (§12.6.6.2) de uno de los formatos

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

si el procesamiento normal de la invocación no encuentra métodos aplicables, se intenta procesar la construcción como una invocación de método de extensión. Si «expr» o cualquiera de los «args» tiene el tipo de tiempo de compilación dynamic, los métodos de extensión no se aplicarán.

El objetivo es encontrar el mejor type_nameC, para que pueda tener lugar la correspondiente invocación a método estático:

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

Un método de extensión Cᵢ.Mₑ es elegible si:

  • Cᵢ es una clase no genérica y no anidada
  • El nombre de Mₑ es identificador
  • Mₑ es accesible y aplicable cuando se aplique a los argumentos como un método estático como se muestra arriba
  • Existe una conversión implícita de identidad, referencia o boxing desde expr al tipo del primer parámetro de Mₑ.

La búsqueda para C procede como sigue:

  • A partir de la declaración de espacio de nombres más cercana, continuando con cada declaración de espacio de nombres envolvente y finalizando con la unidad de compilación que la contiene, se realizan intentos sucesivos para buscar un conjunto candidato de métodos de extensión.
    • Si el espacio de nombres o unidad de compilación dado contiene directamente declaraciones de tipos no genéricos Cᵢ con métodos de extensión elegibles Mₑ, entonces el conjunto de esos métodos de extensión es el conjunto candidato.
    • Si los espacios de nombres importados mediante directivas de espacio de nombres en el espacio de nombres o unidad de compilación dados contienen directamente declaraciones de tipos no genéricos Cᵢ con métodos de extensión elegibles Mₑ, entonces el conjunto de esos métodos de extensión es el conjunto candidato.
  • Si no se encuentra ningún conjunto candidato en ninguna declaración de espacio de nombres o unidad de compilación adjunta, se produce un error de compilación.
  • En caso contrario, se aplica la resolución de sobrecarga al conjunto candidato como se describe en la sección 12.6.4. Si no se encuentra el mejor método, se produce un error de compilación.
  • C es el tipo dentro del cual se declara el mejor método como método de extensión.

Si se utiliza C como objetivo, la llamada al método se procesa como una invocación a un método estático (sección 12.6.6).

Nota: a diferencia de una invocación a un método de instancia, no se lanza ninguna excepción cuando expr se evalúa a una referencia nula. En su lugar, este valor de null se pasa al método de extensión, tal como se haría a través de una invocación de método estático normal. Depende de la implementación del método de extensión decidir cómo responder a una llamada de este tipo. nota final

Las reglas anteriores significan que los métodos de instancia tienen prioridad sobre los métodos de extensión, que los métodos de extensión disponibles en las declaraciones de espacio de nombres internos tienen prioridad sobre los métodos de extensión disponibles en declaraciones de espacio de nombres externos y que los métodos de extensión declarados directamente en un espacio de nombres tienen prioridad sobre los métodos de extensión importados en ese mismo espacio de nombres mediante una directiva de espacio de nombres.

Ejemplo:

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

En el ejemplo, el método de Btiene prioridad sobre el primer método de extensión e Ctiene prioridad sobre ambos métodos de extensión.

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();
        }
    }
}

La salida de este ejemplo es:

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

D.G tiene prioridad sobre C.G, y E.F tiene prioridad sobre tanto D.F como C.F.

ejemplo final

12.8.10.4 invocaciones de delegados

Para una invocación de delegado, la primary_expression de la invocation_expression debe ser un valor de un delegate_type. Además, considerando que el delegate_type sea miembro de función que tenga la misma lista de parámetros que el delegate_type, el delegate_type será aplicable (§12.6.4.2) en relación con la argument_list de la invocation_expression.

El procesamiento en tiempo de ejecución de una invocación de delegado con el formato D(A), donde D es una primary_expression de un delegate_type y A es una argument_list opcional, consta de los pasos siguientes:

  • D se evalúa. Si esta evaluación causa una excepción, no se ejecutan más pasos.
  • Se evalúa la lista de argumentos A. Si esta evaluación causa una excepción, no se ejecutan más pasos.
  • Se comprueba si el valor de D es válido. Si el valor de D es null, se lanza un System.NullReferenceException y no se ejecutarán más pasos.
  • De lo contrario, D es una referencia a una instancia de delegado. Las invocaciones a miembros de funciones (§12.6.6) se realizan en cada una de las entidades invocables de la lista de invocaciones del delegado. Para las entidades invocables que constan de una instancia y un método de instancia, la instancia para la invocación es la instancia contenida en la entidad invocable.

Consulte la sección 20.6 para más detalles sobre las listas de invocación múltiple sin parámetros.

12.8.11 Expresión de invocación condicional nula

Un null_conditional_invocation_expression es, desde el punto de vista sintáctico, un null_conditional_member_access (§12.8.8) o un null_conditional_element_access (§12.8.13) donde el dependent_access final es una expresión de invocación (§12.8.10).

Una null_conditional_invocation_expression se produce dentro del contexto de una statement_expression (sección 13.7), anonymous_function_body (sección 12.19.1) o method_body (sección 15.6.1).

A diferencia del null_conditional_member_access o null_conditional_element_access, que son sintácticamente equivalentes, una null_conditional_invocation_expression puede no tener valor alguno.

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

El null_forgiving_operator opcional se puede incluir solo si el null_conditional_member_access o null_conditional_element_access tiene un delegate_type.

Una expresión null_conditional_invocation_expression E tiene el formato P?A; donde A es el resto del null_conditional_member_access o null_conditional_element_access equivalente sintácticamente,A comenzará por . o [. Que PA signifique la concatenación de P y A.

Cuando E se produce como una statement_expression el significado de E es el mismo que el significado de la instrucción:

if ((object)P != null) PA

salvo que P solo se evalúa una vez.

Cuando E ocurre como un anonymous_function_body o method_body el significado de E depende de su clasificación:

  • Si E se clasifica como nada, su significado es el mismo que el del bloque:

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

    salvo que P solo se evalúa una vez.

  • En caso contrario, el significado de E es el mismo que el del bloque:

    { return E; }
    

    y, a su vez, el significado de este bloque depende de si E es sintácticamente equivalente a un null_conditional_member_access (§12.8.8) o null_conditional_element_access (§12.8.13).

12.8.12 Acceso de elemento

12.8.12.1 General

Un element_access consta de un primary_no_matriz_creation_expression, seguido de un token "[", seguido de un token argument_list, seguido de un token "]". La argument_list consta de uno o varios argumentos, separados por comas.

element_access
    : primary_no_array_creation_expression '[' argument_list ']'
    ;

La argument_list de un element_access no puede contener out ni argumentos ref.

Un element_access se enlaza dinámicamente (§12.3.3) si se cumple al menos una de las siguientes condiciones:

  • La primary_no_array_creation_expression tiene un tipo en tiempo de compilación dynamic.
  • Al menos una expresión de la argument_list tiene tipo en tiempo de compilación dynamic y la primary_no_array_creation_expression no tiene tipo matriz.

En este caso, el compilador clasifica el element_access como un valor de tipo dynamic. Las reglas siguientes para determinar el significado del element_access se aplican en tiempo de ejecución, usando el tipo en tiempo de ejecución en lugar del tipo en tiempo de compilación de las expresiones de primary_no_array_creation_expression y argument_list que tienen el tipo en tiempo de compilación dynamic. Si la primary_no_array_creation_expression no tiene un tipo de tiempo de compilación dynamic, el acceso al elemento se somete a una comprobación de tiempo de compilación limitada, tal como se describe en §12.6.5.

Si el primary_no_matriz_creation_expression de un element_access es un valor de un matriz_type, el element_access es un acceso a matriz (§12.8.12.2). De lo contrario, la primary_no_array_creation_expression deberá ser una variable o valor de un tipo de clase, estructura o interfaz que tenga uno o más miembros indexadores, en cuyo caso el element_access es un acceso indexador (sección 12.8.12.3).

12.8.12.2 Acceso a matrices

Para el acceso a una matriz, la primary_no_array_creation_expression del element_access será un valor de un array_type. Además, no se permite que la argument_list de un acceso a una matriz contenga argumentos con nombre. El número de expresiones de la argument_list será el mismo que el rango del array_type, y cada expresión será del tipo int, uint, long, o ulong, o será implícitamente convertible a uno o más de estos tipos.

El resultado de evaluar el acceso a una matriz es una variable del tipo de elemento de la matriz, es decir, el elemento de matriz seleccionado por los valores de las expresiones de la argument_list.

[...] El procesamiento en tiempo de ejecución de un acceso a una matriz de la forma P[A], donde P es una primary_no_array_creation_expression de un array_type y A es una argument_list, consta de los siguientes pasos:

  • P se evalúa. Si esta evaluación causa una excepción, no se ejecutan más pasos.
  • Las expresiones de índice de la argument_list se evalúan en orden, de izquierda a derecha. Tras la evaluación de cada expresión de índice, se realiza una conversión implícita (sección 10.2) a uno de los siguientes tipos: int, uint, long, ulong. Se elige el primer tipo de la lista para el que existe una conversión implícita. Por ejemplo, si la expresión índice es del tipo short entonces se realiza una conversión implícita a int, ya que son posibles las conversiones implícitas de short a int y de short a long. Si la evaluación de una expresión de índice o la conversión implícita subsiguiente provocan una excepción, no se evalúan más expresiones de índice y no se ejecutan más pasos.
  • Se comprueba si el valor de P es válido. Si el valor de P es null, se lanza un System.NullReferenceException y no se ejecutarán más pasos.
  • El valor de cada expresión de la argument_list se comprueba con los límites reales de cada dimensión de la instancia de matriz referenciada por P. Si uno o más valores están fuera de rango, se lanza un System.IndexOutOfRangeException y no se ejecutan más pasos.
  • Se calcula la ubicación del elemento de la matriz dado por la(s) expresión(es) de índice, y esta ubicación se convierte en el resultado del acceso a la matriz.

12.8.12.3 Acceso a indexadores

Para un acceso al indexador, la primary_no_array_creation_expression del element_access será una variable o un valor de una clase, struct o tipo de interfaz, y este tipo implementará uno o varios indexadores que sean aplicables con respecto a la argument_list del element_access.

El procesamiento en tiempo de enlace de un acceso a indexador con el formato P[A], donde P es una primary_no_array_creation_expression de una clase, struct o tipo de interfaz T, y A es una argument_list, consta de los pasos siguientes:

  • El conjunto de indexadores proporcionados por T se ha construido. El conjunto consiste en todos los indexadores declarados en T o un tipo base de T que no son declaraciones de anulación y son accesibles en el contexto actual (sección 7.5).
  • El conjunto se reduce a los indexadores que son aplicables y que no están ocultos por otros indexadores. Las siguientes reglas se aplican a cada indexador S.I del conjunto S, donde es el tipo en el que se declara el indexador I:
    • Si I no es aplicable con respecto a A (sección 12.6.4.2), se elimina I del conjunto.
    • Si I es aplicable con respecto a A (sección 12.6.4.2), todos los indexadores declarados en un tipo base de S se eliminan del conjunto.
    • Si I es aplicable con respecto a A (sección 12.6.4.2) y S es un tipo de clase distinto de object, todos los indexadores declarados en una interfaz se eliminan del conjunto.
  • Si el conjunto resultante de indexadores candidatos está vacío, entonces no existen indexadores aplicables y se produce un error de vinculación.
  • El mejor indexador del conjunto de indexadores candidatos se identifica utilizando las reglas de resolución de sobrecargas de sección 12.6.4. Si no se puede identificar el mejor indexador, el acceso al indexador es ambiguo y se produce un error de vinculación.
  • Las expresiones de índice de la argument_list se evalúan en orden, de izquierda a derecha. El resultado del procesamiento del acceso al indexador es una expresión clasificada como acceso al indexador. La expresión de acceso al indexador hace referencia al indexador determinado en el paso anterior, y tiene una expresión de instancia asociada de P y una lista de argumentos asociada de A, y un tipo asociado que es el tipo del indexador. Si T es un tipo de clase, el tipo asociado se elige de la primera declaración o sobrescritura del indexador que se encuentra al comenzar con T y al buscar en sus clases base.

Dependiendo del contexto en el que se use, el acceso a un indexador provoca la invocación del método get o del método set del indexador. Si el acceso al indexador es el objetivo de una asignación, se invoca al descriptor de acceso set para asignar un nuevo valor (sección 12.21.2). En todos los demás casos, se invoca al descriptor de acceso get para obtener el valor actual (sección 12.2.2).

12.8.13 Acceso a elementos condicionales NULL

Un null_conditional_element_access consta de una primary_no_array_creation_expression seguida de los dos tokens "?" y "[", seguido de una argument_list, seguida de un token "]", seguido de cero o más dependent_accesses de los cuales cualquiera puede ir precedido por un null_forgiving_operator.

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

Un null_conditional_element_access es una versión condicional de element_access (sección 12.8.12) y es un error en tiempo de vinculación si el tipo de resultado es void. Para una expresión condicional nula en la que el tipo de resultado puede ser void consulte (sección 12.8.11).

Una expresión null_conditional_element_access E tiene el formato P?[A]B; donde B son los dependent_accesses, si existen. El significado de E se determina como sigue:

  • Si el tipo de P es un tipo de valor con nulidad:

    Sea T el tipo de la expresión P.Value[A]B.

    • Si T es un parámetro de tipo que no se sabe si es un tipo de referencia o un tipo de valor no anulable, se produce un error de compilación.

    • Si T es un tipo de valor no anulable, entonces el tipo de E es T?, y el significado de E es el mismo que el significado de:

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

      Excepto que P solo se evalúa una vez.

    • De lo contrario, el tipo de E es T, y el significado de E es el mismo que el significado de:

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

      Excepto que P solo se evalúa una vez.

  • De lo contrario:

    Sea T el tipo de la expresión P[A]B.

    • Si T es un parámetro de tipo que no se sabe si es un tipo de referencia o un tipo de valor no anulable, se produce un error de compilación.

    • Si T es un tipo de valor no anulable, entonces el tipo de E es T?, y el significado de E es el mismo que el significado de:

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

      Excepto que P solo se evalúa una vez.

    • De lo contrario, el tipo de E es T, y el significado de E es el mismo que el significado de:

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

      Excepto que P solo se evalúa una vez.

Nota: en una expresión de la forma:

P?[A₀]?[A₁]

si P se evalúa como null ni A₀ ni A₁ se evalúan. Lo mismo sucede si una expresión es una secuencia de operaciones null_conditional_element_access o null_conditional_member_access§12.8.8.

nota final

12.8.14 Acceso a This

Un this_access consta de la palabra clave this.

this_access
    : 'this'
    ;

Solo se permite un this_access dentro del bloque de un constructor de instancia, un método de instancia, un descriptor de acceso de instancia (§12.2.1) o un finalizador. Tiene uno de los siguientes significados:

  • Cuando se usa this en una primary_expression dentro de un constructor de instancia de una case, se clasifica como un valor El tipo del valor es el tipo de instancia (sección 15.3.2) de la clase dentro de la cual se produce el uso, y el valor es una referencia al objeto que se está construyendo.
  • Cuando se usa this en una expresión primaria dentro de un método de instancia o descriptor de acceso de instancia de una clase, se considera como un valor. El tipo del valor es el tipo de instancia (sección 15.3.2) de la clase dentro de la cual se produce el uso, y el valor es una referencia al objeto para el cual se invocó el método o descriptores de acceso.
  • Cuando se usa this en un primary_expression dentro de un constructor de instancia de una estructura, se clasifica como una variable. El tipo de la variable es el tipo de instancia (sección 15.3.2) de la estructura dentro de la cual se produce el uso, y la variable representa la estructura que se está construyendo.
    • Si la declaración del constructor no tiene inicializador de constructor, la variable this se comporta exactamente igual que un parámetro de salida del tipo struct. En particular, esto significa que la variable se asignará definitivamente en cada ruta de ejecución del constructor de instancia.
    • En caso contrario, la variable this se comporta exactamente igual que un parámetro ref del tipo struct. En particular, esto significa que la variable se considera asignada inicialmente.
  • Cuando se usa this en una primary_expression dentro de un método de instancia o descriptor de acceso de un struct, se clasifica como una variable. El tipo de la variable es el tipo de instancia (sección 15.3.2) del struct dentro del cual se produce la utilización.
    • Si el método o descriptores de acceso no es un iterador (sección 15.14) o una función asíncrona (sección 15.15), la variable this representa la estructura para la que se ha invocado el método o descriptores de acceso.
      • Si la estructura es un readonly struct, la variable this se comporta exactamente igual que un parámetro de entrada de tipo estructura
      • En caso contrario, la variable this se comporta exactamente igual que un parámetro ref del tipo struct
    • Si el método o descriptores de acceso es un iterador o una función asíncrona, la variable this representa una copia de la estructura para la que se invocó el método o descriptores de acceso, y se comporta exactamente igual que un parámetro de valor de tipo estructura.

El uso de this en una primary_expression en un contexto distinto de los enumerados anteriormente es un error en tiempo de compilación. En particular, no es posible referirse a this en un método estático, un descriptor de acceso de propiedad estática, o en un variable_initializer de una declaración de campo.

Acceso a la base 12.8.15

Un base_access consta de la palabra clave base seguida de un token "." y un identificador y una type_argument_list opcional o una argument_list entre corchetes:

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

Un base_access se utiliza para acceder a miembros de la clase base que están ocultos por miembros de nombre similar en la clase o estructura actual. Un base_access solo está permitido en el cuerpo de un constructor de instancia, un método de instancia, un descriptor de acceso de instancia (§12.2.1) o un finalizador. Cuando base.I se produce en una clase o estructura, I indicará un miembro de la clase base de esa clase o estructura. Del mismo modo, cuando base[E] ocurre en una clase, debe existir un indexador aplicable en la clase base.

En tiempo de enlace, las expresiones base_access con formato base.I y base[E] se evalúan exactamente como si estuvieran escritas como ((B)this).I y ((B)this)[E], donde B es la clase base de la clase o struct en la que se encuentra la construcción. Así, base.I y base[E] corresponden a this.I y this[E], excepto this que se considera una instancia de la clase base.

Cuando un base_access hace referencia a un miembro de función virtual (un método, una propiedad o un indexador), se modifica la determinación de qué miembro de función invocar en tiempo de ejecución (sección 12.6.6). El miembro de función invocado se determina mediante la búsqueda de la implementación más derivada (§15.6.4) del miembro de función con respecto a B (en lugar de con respecto al tipo en tiempo de ejecución de this, como sería habitual en un acceso no base). Por lo tanto, dentro de una invalidación de un miembro de función virtual, se puede usar un base_access para invocar la implementación heredada del miembro de función. Si el miembro de función referenciado por un base_access es abstracto, se produce un error de vinculación.

Nota: A diferencia de this, base no es una expresión en sí misma. Es una palabra clave que solo se utiliza en el contexto de un base_access o un constructor_initializer (sección 15.11.2). nota final

12.8.16 Operadores de incremento y decremento postfijos

post_increment_expression
    : primary_expression '++'
    ;

post_decrement_expression
    : primary_expression '--'
    ;

El operando de una operación de incremento o decremento postfija será una expresión clasificada como una variable, un acceso a una propiedad o un acceso a un indizador. El resultado de la operación tiene un valor del mismo tipo que el operando.

Si la primary_expression tiene el tipo dynamic en tiempo de compilación, entonces el operador está ligado dinámicamente (sección 12.3.3), la post_increment_expression o la post_decrement_expression tienen el tipo dynamic en tiempo de compilación y se aplican las siguientes reglas en tiempo de ejecución utilizando el tipo en tiempo de ejecución de la primary_expression.

Si el operando de una operación de incremento o disminución de postfijo es un acceso de propiedad o indexador, la propiedad o el indexador deberán tener tanto un descriptor de acceso get como un descriptor de acceso set. Si no es así, se produce un error en tiempo de enlace.

La resolución de sobrecarga de operador unario (sección 12.4.4) se aplica para seleccionar una implementación de operador específica. Existen operadores de ++ y -- predefinidos para los siguientes tipos: sbyte, byte, short, ushort, int, uint, long, ulong, char, float, double, decimaly cualquier tipo de enumeración. Los operadores predefinidos ++ devuelven el valor producido al sumar 1 al operando, y los operadores predefinidos -- devuelven el valor producido al restar 1 del operando. En un contexto verificado, si el resultado de esta suma o resta está fuera del rango del tipo de resultado y el tipo de resultado es un tipo entero o un tipo de enumeración, se lanza una System.OverflowException.

Debe haber una conversión implícita del tipo de retorno del operador unario seleccionado al tipo de la primary_expression; de lo contrario, se produce un error de compilación.

El procesamiento en tiempo de ejecución de una operación de incremento o decremento de postfijo de la forma x++ o x-- consta de los siguientes pasos:

  • Si x se clasifica como variable:
    • x se evalúa para obtener la variable.
    • Se guarda el valor de x.
    • El valor guardado de x se convierte al tipo de operando del operador seleccionado y se invoca al operador con este valor como argumento.
    • El valor devuelto por el operador se convierte al tipo de x y se almacena en la ubicación dada por la evaluación anterior de x.
    • El valor guardado de x se convierte en el resultado de la operación.
  • Si x se clasifica como acceso a una propiedad o indexador:
    • La expresión de instancia (si x no es static) y la lista de argumentos (si x es un acceso indexador) asociada a se evalúan con x, y los resultados se utilizan en las invocaciones subsiguientes del descriptor de acceso get y set.
    • Se invoca el descriptor de acceso get de x y se guarda el valor devuelto.
    • El valor guardado de x se convierte al tipo de operando del operador seleccionado y se invoca al operador con este valor como argumento.
    • El valor devuelto por el operador se convierte en el tipo de x y el accesor set de x se invoca con este valor como valor argumental.
    • El valor guardado de x se convierte en el resultado de la operación.

Los operadores ++ y -- también admiten la notación prefija (sección 12.9.6). El resultado de x++ o x-- es el valor de xantes de la operación, mientras que el resultado de ++x o --x es el valor de xdespués de la operación. En cualquier caso, x tiene el mismo valor después de la operación.

Se puede invocar la implementación de un operador ++ o un operador -- mediante notación de prefijo o posfijo. No es posible tener implementaciones de operadores separadas para las dos notaciones.

12.8.17 El operador new

12.8.17.1 General

El operador new se utiliza para crear nuevas instancias de tipos.

Existen tres formas de expresiones new:

  • Las expresiones de creación de objetos y las expresiones de creación de objetos anónimos se utilizan para crear nuevas instancias de tipos de clase y tipos de valor.
  • Las expresiones de creación de matrices se utilizan para crear nuevas instancias de tipos de matrices.
  • Las expresiones de creación de delegados se usan para obtener instancias de tipos delegados.

El operador new implica la creación de una instancia de un tipo, pero no implica necesariamente la asignación de memoria. En particular, las instancias de tipos de valor no requieren memoria adicional más allá de las variables en las que residen, y no se produce ninguna asignación cuando se utiliza new para crear instancias de tipos de valor.

Nota: Las expresiones de creación de delegados no siempre crean nuevas instancias. Cuando la expresión se procesa del mismo modo que la conversión de un grupo de métodos (sección 10.8) o la conversión de una función anónima (sección 10.7), es posible que se reutilice una instancia de delegado existente. nota final

12.8.17.2 Expresiones de creación de objetos

Una expresión object_creation_expression se utiliza para crear una nueva instancia de class_type or a 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
    ;

El tipo de una expresión de creación de objeto será una clase tipo , un tipo de valor o un parámetro de tipo . El type no puede ser un tuple_type ni un class_type abstracto o estático.

La argument_list opcional (§12.6.2) solo se permite si el type es un class_type o un struct_type.

Una expresión de creación de objetos puede omitir la lista de argumentos del constructor y los paréntesis que la encierran, siempre que incluya un inicializador de objeto o un inicializador de colección. Omitir la lista de argumentos del constructor y encerrar los paréntesis equivale a especificar una lista de argumentos vacía.

El procesamiento de una expresión de creación de objeto que incluya un inicializador de objeto o de colección consiste en procesar primero el constructor de instancia y después las inicializaciones de miembro o elemento especificadas por el inicializador de objeto (sección 12.8.17.3) o de colección (sección 12.8.17.4).

Si alguno de los argumentos de la lista opcional argument_list tiene el tipo dynamic en tiempo de compilación, la expresión object_creation_expression se vincula dinámicamente (sección 12.3.3) y las siguientes reglas se aplican en tiempo de ejecución utilizando el tipo en tiempo de ejecución de aquellos argumentos de la lista argument_list que tienen el tipo en tiempo de compilación dynamic. No obstante, la creación del objeto se somete a una comprobación limitada en tiempo de compilación, como se describe en la sección 12.6.5.

El procesamiento en tiempo de enlace de una object_creation_expression del formato new T(A), donde T es un class_type o un value_type, y A es una argument_list opcional, consta de los pasos siguientes:

  • Si T es un value_type y A no está presente:
    • La object_creation_expression es una invocación al constructor por defecto. El resultado de la expresión object_creation_expression es un valor de tipo T, es decir, el valor por defecto para T definido en la sección 8.3.3.
  • En caso contrario, si T es un type_parameter y A no está presente:
    • Si no se ha especificado ninguna restricción de tipo de valor o restricción de constructor (sección 15.2.5) para T, se produce un error de vinculación.
    • El resultado de la expresión object_creation_expression es un valor del tipo en tiempo de ejecución al que se ha vinculado el parámetro de tipo, es decir, el resultado de invocar al constructor por defecto de ese tipo. El tipo en tiempo de ejecución puede ser un tipo de referencia o un tipo de valor.
  • En caso contrario, si T es un class_type o un struct_type:
    • Si T es un class_type abstracto o estático, se produce un error en tiempo de compilación.
    • El constructor de instancia a invocar se determina utilizando las reglas de resolución de sobrecargas de sección 12.6.4. El conjunto de constructores de instancias candidatas consta de todos los constructores de instancias accesibles declarados en T, que son aplicables con respecto a A (§12.6.4.2). Si el conjunto de constructores de instancia candidatos está vacío, o si no se puede identificar un único constructor de instancia óptimo, se produce un error de vinculación.
    • El resultado de la expresión object_creation_expression es un valor de tipo T, es decir, el valor producido por la invocación del constructor de instancia determinado en el paso anterior.
    • En caso contrario, la expresión object_creation_expression no es válida y se produce un error de vinculación.

Incluso si el object_creation_expression está enlazado dinámicamente, el tipo en momento de compilación sigue siendo T.

El procesamiento en tiempo de ejecución de una object_creation_expression de la forma new T(A), donde T es class_type o un struct_type y A es una argument_list opcional, consiste en los siguientes pasos:

  • Si T es un class_type:
    • Se asigna una nueva instancia de clase T. Si no hay suficiente memoria disponible para asignar la nueva instancia, se lanza un System.OutOfMemoryException y no se ejecutan más pasos.
    • Todos los campos de la nueva instancia se inicializan con sus valores por defecto (sección 9.3).
    • El constructor de instancia se invoca de acuerdo con las reglas de invocación de miembros de función (sección 12.6.6). Se pasa automáticamente una referencia a la instancia recién asignada al constructor de instancia y se puede acceder a la instancia desde dentro de ese constructor de la siguiente manera.
  • Si T es un struct_type:
    • Se crea una instancia del tipo T asignando una variable local temporal. Dado que se requiere que un constructor de instancia de un struct_type asigne definitivamente un valor a cada campo de la instancia que se está creando, no es necesaria la inicialización de la variable temporal.
    • El constructor de instancia se invoca de acuerdo con las reglas de invocación de miembros de función (sección 12.6.6). Se pasa automáticamente una referencia a la instancia recién asignada al constructor de instancia y se puede acceder a la instancia desde dentro de ese constructor de la siguiente manera.

12.8.17.3 Inicializadores de objetos

Un inicializador de objeto especifica valores para cero o más campos, propiedades o elementos indexados de un 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
    ;

Un inicializador de objeto consta de una secuencia de inicializadores de miembros, delimitada por los tokens { y }, y separados por comas. Cada member_initializer designará un objetivo para la inicialización. Un identificador nombrará un campo o propiedad accesible del objeto que se está inicializando, mientras que una argument_list encerrada entre corchetes especificará argumentos para un indexador accesible en el objeto que se está inicializando. Es un error que un inicializador de objeto incluya más de una inicialización de miembro para el mismo campo o propiedad.

Nota: Mientras que no se permite que un inicializador de objeto establezca el mismo campo o propiedad más de una vez, no existen tales restricciones para los indexadores. Un inicializador de objeto puede contener varios objetivos de inicializador que hagan referencia a indexadores, e incluso puede utilizar los mismos argumentos de indexador varias veces. nota final

Cada initializer_target va seguido de un signo de igual y, a continuación, una expresión, un inicializador de objeto o un inicializador de colección. No es posible que las expresiones dentro del inicializador de objetos hagan referencia al objeto recién creado que está inicializando.

Un inicializador de miembro que se especifica una expresión después del signo de igual se procesa de la misma manera que una asignación (§12.21.2) al destino.

Inicializador de miembro que especifica un inicializador de objeto después del signo de igual es un inicializador de objeto anidado, es decir, una inicialización de un objeto incrustado. En lugar de asignar un nuevo valor al campo o propiedad, las asignaciones del inicializador de objetos anidados se tratan como asignaciones a miembros del campo o propiedad. Los inicializadores de objetos anidados no pueden aplicarse a propiedades con un tipo de valor ni a campos de solo lectura con un tipo de valor.

Un inicializador de miembro que especifica un inicializador de colección después del signo de igual es la inicialización de una colección incrustada. En lugar de asignar una nueva colección al campo, propiedad o indexador objetivo, los elementos dados en el inicializador se añaden a la colección referenciada por el objetivo. El destino será de un tipo de colección que cumpla los requisitos especificados en §12.8.17.4.

Cuando un objetivo inicializador hace referencia a un indexador, los argumentos del indexador siempre se evaluarán exactamente una vez. Por lo tanto, incluso si los argumentos terminan por no ser utilizados (por ejemplo, debido a un inicializador anidado vacío), se evalúan por sus efectos secundarios.

Ejemplo: La siguiente clase representa un punto con dos coordenadas:

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

Se puede crear e inicializar una instancia de Point de la siguiente manera:

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

Esto tiene el mismo efecto que

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

donde __a es una variable temporal invisible e inaccesible.

La siguiente clase muestra un rectángulo creado a partir de dos puntos, y la creación e inicialización de una instancia de Rectangle:

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

Se puede crear e inicializar una instancia de Rectangle de la siguiente manera:

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

Esto tiene el mismo efecto 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;

donde __r, __p1 y __p2 son variables temporales que de otro modo serían invisibles e inaccesibles.

Si el constructor de Rectanglealoca las dos instancias incrustadas de Point, se pueden usar para inicializar las instancias incrustadas de Point en lugar de instanciar nuevas instancias.

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

la siguiente construcción se puede utilizar para inicializar las instancias incrustadas Point en lugar de asignar nuevas instancias:

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

Esto tiene el mismo efecto que

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

ejemplo final

12.8.17.4 Inicializadores de colección

Un inicializador de colección especifica los elementos de una colección.

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
    ;

Un inicializador de colección consta de una serie de inicializadores de elementos, delimitados por los tokens { y }, y separados por comas. Cada inicializador de elemento especifica un elemento que se agregará al objeto de colección que se está inicializando y consta de una lista de expresiones, delimitadas por los tokens { y }, y separadas por comas. Un inicializador de elemento de expresión única se puede escribir sin llaves, pero no puede ser una expresión de asignación, para evitar ambigüedad con inicializadores de miembro. La producción de non_assignment_expression se define en §12.22.

Ejemplo: El siguiente es un ejemplo de una expresión de creación de objetos que incluye un inicializador de colección:

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

ejemplo final

El objeto de colección al que se aplique un inicializador de colección deberá ser de un tipo que implemente a System.Collections.IEnumerable o se producirá un error de compilación. Para cada elemento especificado en orden de izquierda a derecha, se aplica la búsqueda normal de miembros para encontrar un miembro llamado Add. Si el resultado de la búsqueda de miembros no es un grupo de métodos, se produce un error de compilación. En caso contrario, se aplica la resolución de sobrecarga con la lista de expresiones del inicializador del elemento como lista de argumentos, y el inicializador de la colección invoca el método resultante. Así, el objeto de colección contendrá un método de instancia o extensión aplicable con el nombre Add de cada inicializador de elemento.

Ejemplo: a continuación se muestra una clase que representa un contacto con un nombre y una lista de números de teléfono, así como la creación e inicialización de un 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 tiene el mismo efecto 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;

donde __clist, __c1 y __c2 son variables temporales que de otro modo serían invisibles e inaccesibles.

ejemplo final

12.8.17.5 Expresiones de creación de matrices

Se utiliza una expresión de creación de matriz para crear una instancia nueva de un tipo de matriz .

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

Una expresión de creación de array de la primera forma asigna una instancia de array del tipo que resulta de borrar cada una de las expresiones individuales de la lista de expresiones.

Ejemplo: La expresión de creación de matriz new int[10,20] produce una instancia de matriz del tipo int[,], y la expresión de creación de matriz new int[10][,] produce una instancia de matriz del tipo int[][,]. ejemplo final

Cada expresión de la lista de expresiones será de tipo int, uint, long o ulong o implícitamente convertible a uno o más de estos tipos. El valor de cada expresión determina la longitud de la dimensión correspondiente en la nueva instancia de matriz asignada. Dado que la longitud de una dimensión de matriz debe ser no negativa, es un error de compilación tener una expresión constante con un valor negativo en la lista de expresiones.

Excepto en un contexto inseguro (sección 23.2), la disposición de las matrices no está especificada.

Si una expresión de creación de matrices de la primera forma incluye un inicializador de matrices, cada expresión de la lista de expresiones será una constante y las longitudes de rango y dimensión especificadas por la lista de expresiones coincidirán con las del inicializador de matrices.

En una expresión de creación de matriz de la segunda o tercera forma, el rango del tipo de matriz especificado o el especificador de rango coincidirá con el del inicializador de matriz. Las longitudes de las dimensiones individuales se deducen del número de elementos en cada uno de los niveles de anidamiento correspondientes del inicializador de matrices. Por lo tanto, la expresión del inicializador en la siguiente declaración

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

corresponde exactamente a

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

Una expresión de creación de matrices de la tercera forma se denomina expresión de creación de matrices implícitamente tipada. Es similar a la segunda forma, excepto en que el tipo del elemento de la matriz no se indica explícitamente, sino que se determina como el mejor tipo común (sección 12.6.3.15) del conjunto de expresiones del inicializador de matrices. Para una matriz multidimensional, es decir, una en la que el rank_specifier contiene al menos una coma, este conjunto consta de todas las expressions que se encuentran en array_initializers anidados.

Los inicializadores de matrices se describen con más detalle en la sección 17.7.

El resultado de la evaluación de una expresión de creación de matriz se clasifica como un valor, es decir, una referencia a la nueva instancia de matriz asignada. El procesamiento en tiempo de ejecución de una expresión de creación de matriz consta de los siguientes pasos:

  • Las expresiones de longitud de dimensión de la expression_list se evalúan en orden, de izquierda a derecha. Tras la evaluación de cada expresión, se realiza una conversión implícita (sección 10.2) a uno de los siguientes tipos: int, uint, long, ulong. Se elige el primer tipo de la lista para el que existe una conversión implícita. Si la evaluación de una expresión o la conversión implícita posterior provocan una excepción, no se evalúan más expresiones ni se ejecutan más pasos.
  • Los valores calculados para las longitudes de las dimensiones se validan como sigue: Si uno o más de los valores son menores que cero, se lanza un System.OverflowException y no se ejecutan más pasos.
  • Se asigna una instancia de array con las longitudes de dimensión dadas. Si no hay suficiente memoria disponible para asignar la nueva instancia, se lanza un System.OutOfMemoryException y no se ejecutan más pasos.
  • Todos los elementos de la nueva instancia de matriz se inicializan con sus valores por defecto (sección 9.3).
  • Si la expresión de creación de la matriz contiene un inicializador de matriz, cada expresión del inicializador de matriz se evalúa y se asigna a su elemento de matriz correspondiente. Las evaluaciones y asignaciones se realizan en el orden en que las expresiones están escritas en el inicializador de la matriz; en otras palabras, los elementos se inicializan en orden de índice creciente, con la dimensión más a la derecha aumentando primero. Si la evaluación de una expresión dada o la asignación subsiguiente al elemento de matriz correspondiente provoca una excepción, no se inicializan más elementos (y los elementos restantes tendrán sus valores por defecto).

Una expresión de creación de array permite instanciar un array con elementos de un tipo de array, pero los elementos de dicho array se inicializarán manualmente.

Ejemplo: la declaración

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

crea una matriz unidimensional con 100 elementos de tipo int[]. El valor inicial de cada elemento es null. No es posible que la misma expresión de creación de matrices cree instancias también de las submatrices y la instrucción

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

produce un error de compilación. La instanciación de las submatrices se puede realizar manualmente, como en

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

ejemplo final

Nota: Cuando una matriz de matrices tiene forma "rectangular", es decir, cuando las submatrices tienen todas la misma longitud, es más eficiente utilizar una matriz multidimensional. En el ejemplo anterior, la instanciación de la matriz de matrices crea 101 objetos: una matriz externa y 100 submatrices. Por el contrario,

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

crea un único objeto, una matriz bidimensional, y realiza la asignación en una única instrucción.

nota final

Ejemplo: Los siguientes son ejemplos de expresiones de creación de matrices con tipado implícito:

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

La última expresión produce un error en tiempo de compilación porque ni int ni string se convierten implícitamente entre sí, y, por lo tanto, no existe un tipo común mejor. En este caso, se debe usar una expresión de creación de matriz explícitamente tipada, por ejemplo, especificando el tipo como object[]. Alternativamente, uno de los elementos puede ser lanzado a un tipo base común, que se convertiría entonces en el tipo de elemento inferido.

ejemplo final

Las expresiones de creación de matrices de tipado implícito pueden combinarse con inicializadores de objetos anónimos (sección 12.8.17.7) para crear estructuras de datos de tipado anónimo.

Ejemplo:

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

ejemplo final

12.8.17.6 Expresiones de creación de delegados

Una delegate_creation_expression se usa para obtener una instancia de un delegate_type.

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

El argumento de una expresión de creación de delegados debe ser un grupo de métodos, una función anónima o un valor del tipo de tiempo de compilación dynamic o un delegate_type. Si el argumento es un grupo de métodos, identifica el método y, para un método de instancia, el objeto para el que crear un delegado. Si el argumento es una función anónima, define directamente los parámetros y el cuerpo del método objetivo del delegado. Si el argumento es un valor, identifica una instancia de delegado de la que crear una copia.

Si la expresión tiene el tipo en tiempo de compilación dynamic, la delegate_creation_expression se enlaza dinámicamente (§12.8.17.6), y se aplican las siguientes reglas en tiempo de ejecución usando el tipo de ejecución de la expresión. En caso contrario, las reglas se aplican en tiempo de compilación.

El procesamiento en tiempo de enlace de una delegate_creation_expression con el formato nuevo D(E), donde D es un delegate_type y E es una expresión, consta de los pasos siguientes:

  • Si E es un grupo de métodos, la expresión de creación de delegado se procesa del mismo modo que una conversión de grupo de métodos (sección 10.8) de E a D.

  • Si E es una función anónima, la expresión de creación de delegado se procesa del mismo modo que una conversión de función anónima (sección 10.7) de E a D.

  • Si E es un valor E, deberá ser compatible (sección 20.2) con D, y el resultado es una referencia a un delegado recién creado con una lista de invocación de una sola entrada que invoca a E.

El procesamiento en tiempo de ejecución de una delegate_creation_expression con el formato nuevo D(E), donde D es un delegate_type y E es una expresión, consta de los pasos siguientes:

  • Si E es un grupo de métodos, la expresión de creación de delegado se evalúa como una conversión de grupo de métodos (sección 10.8) de E a D.
  • Si E es una función anónima, la creación del delegado se evalúa como una conversión de función anónima de E a D (sección 10.7).
  • Si E es un valor de un delegate_type:
    • E se evalúa. Si esta evaluación causa una excepción, no se ejecutan más pasos.
    • Si el valor de E es null, se lanza un System.NullReferenceException y no se ejecutarán más pasos.
    • Se asigna una nueva instancia del tipo delegado D. Si no hay suficiente memoria disponible para asignar la nueva instancia, se lanza un System.OutOfMemoryException y no se ejecutan más pasos.
    • La nueva instancia de delegado se inicializa con una lista de invocación de entrada única que invoca E.

La lista de invocación de un delegado se establece cuando se instancia el delegado y luego permanece constante durante toda su vida útil. En otras palabras, no es posible cambiar las entidades invocables de destino de un delegado una vez que se haya creado.

Nota: recuerde, cuando se combinan dos delegados o se elimina uno de otro, se obtiene un nuevo delegado; ningún delegado existente ha cambiado su contenido. nota final

No es posible crear un delegado que haga referencia a una propiedad, indexador, operador definido por el usuario, constructor de instancia, finalizador o constructor estático.

Ejemplo: Como se ha descrito anteriormente, cuando se crea un delegado a partir de un grupo de métodos, la lista de parámetros y el tipo de retorno del delegado determinan cuál de los métodos sobrecargados debe seleccionarse. En el ejemplo

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

el campo A.f se inicializa con un delegado que hace referencia al segundo método Square porque ese método coincide exactamente con la lista de parámetros y el tipo de retorno de DoubleFunc. Si el segundo método Square no hubiera estado presente, se habría producido un error de compilación.

ejemplo final

12.8.17.7 Expresiones de creación de objetos anónimos

Una anonymous_object_creation_expression se utiliza para crear un objeto de tipo anónimo.

anonymous_object_creation_expression
    : 'new' anonymous_object_initializer
    ;

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

member_declarator_list
    : member_declarator (',' member_declarator)*
    ;

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

Un inicializador de objeto anónimo declara un tipo anónimo y devuelve una instancia de ese tipo. Un tipo anónimo es un tipo de clase sin nombre que hereda directamente de object. Los miembros de un tipo anónimo son una secuencia de propiedades de solo lectura inferidas del inicializador de objeto anónimo utilizado para crear una instancia del tipo. En concreto, un inicializador de objetos anónimo de la forma

new {p₁=e₁,p₂=e₂,pᵥ=eᵥ}

declara un tipo anónimo de la forma

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() { ... }
}

donde cada "Tx" es el tipo de la expresión "ex" correspondiente. La expresión usada en un member_declarator deberá tener un tipo. Por lo tanto, es un error en tiempo de compilación para que una expresión de un member_declarator sea null o una función anónima.

Los nombres de un tipo anónimo y del parámetro de su método Equals son generados automáticamente por el compilador y no pueden ser referenciados en el texto del programa.

Dentro del mismo programa, dos inicializadores de objetos anónimos que especifiquen una secuencia de propiedades con los mismos nombres y tipos en tiempo de compilación en el mismo orden producirán instancias del mismo tipo anónimo.

Ejemplo: en el ejemplo

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

se permite la asignación en la última línea porque p1 y p2 son del mismo tipo anónimo.

ejemplo final

Los métodos Equals y GetHashcode de los tipos anónimos anulan los métodos heredados de object, y se definen en términos de Equals y GetHashcode de las propiedades, de modo que dos instancias del mismo tipo anónimo son iguales si y solo si todas sus propiedades son iguales.

Un declarador de miembro puede abreviarse en un nombre simple (sección 12.8.4), un acceso a miembro (sección 12.8.7), un inicializador de proyección condicional nulo sección 12.8.8 o un acceso base (sección 12.8.15). Esto se denomina inicializador de proyección y es una forma abreviada de declarar y asignar una propiedad con el mismo nombre. En concreto, declaradores de miembros de los formatos

«identifier», «expr» . «identifier» y «expr» ? . «identifier»

son precisamente equivalentes a los siguientes, respectivamente:

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

Así, en un inicializador de proyección el identificador selecciona tanto el valor como el campo o propiedad al que se asigna el valor. Intuitivamente, un inicializador de proyección proyecta no solo un valor, sino también el nombre del valor.

12.8.18 El operador typeof

El operador typeof se utiliza para obtener el objeto System.Type de un 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
    : ','
    ;

La primera forma de typeof_expression consiste en una palabra clave typeof seguida de un tipo entre paréntesis. El resultado de una expresión de esta forma es el objeto System.Type para el tipo indicado. Solo hay un objeto System.Type para cada tipo. Esto significa que para un tipo T, typeof(T) == typeof(T) siempre es verdadero. El tipo no puede ser dynamic.

La segunda forma de typeof_expression consiste en una palabra clave typeof seguida de un unbound_type_name entre paréntesis.

Nota: un unbound_type_name es muy similar a un type_name (§7.8), excepto que un unbound_type_name contiene generic_dimension_specifier donde un type_name contiene type_argument_list. nota final

Cuando el operando de un typeof_expression es una secuencia de tokens que satisface las gramáticas de unbound_type_name y type_name, es decir, cuando no contiene ni un generic_dimension_specifier ni un type_argument_list, se considera que la secuencia de tokens es un type_name. El significado de un unbound_type_name se determina como sigue:

  • Convierta la secuencia de tokens en un type_name reemplazando cada generic_dimension_specifier por un type_argument_list que tenga el mismo número de comas y la palabra clave object que cada type_argument.
  • Evalúe el type_name resultante, ignorando todas las restricciones de los parámetros de tipo.
  • El unbound_type_name se resuelve en el tipo genérico sin enlazar asociado al tipo construido resultante (§8.4).

Es un error que el nombre de tipo sea un tipo de referencia anulable.

El resultado del typeof_expression es el objeto System.Type para el tipo genérico sin enlazar resultante.

La tercera forma de typeof_expression consta de una palabra clave typeof seguida de una palabra clave void entre paréntesis. El resultado de una expresión de esta forma es el objeto System.Type que representa la ausencia de un tipo. El objeto de tipo devuelto por typeof(void) es distinto del objeto de tipo devuelto para cualquier tipo.

Nota: este objeto especial System.Type es útil en las bibliotecas de clases que permiten la reflexión sobre los métodos en el lenguaje, donde esos métodos desean representar el tipo de retorno de cualquier método, incluidos los métodos void, con una instancia de System.Type. nota final

El operador typeof puede utilizarse en un parámetro de tipo. Es un error en tiempo de compilación si se sabe que el nombre del tipo es un tipo de referencia anulable. El resultado es el objeto System.Type para el tipo en tiempo de ejecución que estaba enlazado al parámetro de tipo. Si el tipo en tiempo de ejecución es un tipo de referencia anulable, el resultado es el tipo de referencia no anulable correspondiente. El operador typeof también puede utilizarse en un tipo construido o en un tipo genérico no ligado (sección 8.4.4). El objeto System.Type de un tipo genérico no enlazado no es el mismo que el objeto System.Type del tipo instancia (sección 15.3.2). El tipo instancia es siempre un tipo construido cerrado en tiempo de ejecución, por lo que su objeto System.Type depende de los argumentos de tipo en tiempo de ejecución que se utilicen. El tipo genérico no enlazado, por otra parte, no tiene argumentos de tipo, y produce el mismo objeto System.Type independientemente de los argumentos de tipo en tiempo de ejecución.

Ejemplo: el ejemplo

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();
    }
}

genera el siguiente resultado:

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]

Nótese que int y System.Int32 son del mismo tipo. El resultado de typeof(X<>) no depende del argumento de tipo, pero el resultado de typeof(X<T>) sí.

ejemplo final

12.8.19 El operador sizeof

El operador sizeof devuelve el número de bytes de 8 bits ocupados por una variable de un tipo dado. El tipo especificado como operando de sizeof debe ser un unmanaged_type (§8.8).

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

Para ciertos tipos predefinidos el operador sizeof devuelve un valor constante int como se muestra en la siguiente tabla:

Expresión 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 un tipo enum T, el resultado de la expresión sizeof(T) es un valor constante igual al tamaño de su tipo subyacente, como se indica más arriba. Para todos los demás tipos de operandos, el operador sizeof se especifica en la sección 23.6.9.

12.8.20 Operadores comprobados y no comprobados

Los operadores checked y unchecked sirven para controlar el contexto de comprobación de desbordamiento para conversiones y operaciones aritméticas de tipo integral.

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

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

El operador checked evalúa la expresión contenida en un contexto verificado, y el operador unchecked evalúa la expresión contenida en un contexto no verificado. Una checked_expression o una unchecked_expression corresponde exactamente a una parenthesized_expression (§12.8.5), excepto que la expresión contenida se evalúa en el contexto dado de comprobación de desbordamiento.

El contexto de comprobación de desbordamiento también se puede controlar a través de las instrucciones checked y unchecked (§13.12).

Las siguientes operaciones se ven afectadas por el contexto de comprobación de desbordamiento establecido por los operadores y instrucciones comprobados y no comprobados:

  • Los operadores predefinidos ++ y -- (sección 12.8.16 y sección 12.9.6), cuando el operando es de tipo integral o enum.
  • El operador unario predefinido - (sección 12.9.3), cuando el operando es de tipo integral.
  • Los operadores predefinidos +, -, * y / y binario (sección 12.10), cuando ambos operandos son de tipo integral o enum.
  • Conversiones numéricas explícitas (§10.3.2) desde un tipo entero o de enumeración a otro tipo entero o de enumeración, o desde float o double a un tipo entero o de enumeración.

Cuando una de las operaciones anteriores produce un resultado demasiado grande para representarlo en el tipo de destino, el contexto en el que se realiza la operación controla el comportamiento resultante:

  • En un contexto checked, si la operación es una expresión constante (sección 12.23), se produce un error de compilación. De lo contrario, si la operación se realiza en tiempo de ejecución, se inicia una excepción System.OverflowException.
  • En un contexto unchecked, el resultado se trunca descartando los bits de orden superior que no caben en el tipo de destino.

Para las expresiones no constantes (sección 12.23) (expresiones que se evalúan en tiempo de ejecución) que no están encerradas por ningún operador o instrucción checked o unchecked, el contexto de comprobación de desbordamiento por defecto no está marcado, a menos que factores externos (como las opciones del compilador y la configuración del entorno de ejecución) requieran una evaluación marcada.

Para las expresiones constantes (sección 12.23) (expresiones que pueden evaluarse completamente en tiempo de compilación), el contexto de comprobación de desbordamiento por defecto siempre está marcado. A menos que una expresión constante se coloque explícitamente en un contexto unchecked, los desbordamientos que se producen durante la evaluación en tiempo de compilación de la expresión siempre provocan errores en tiempo de compilación.

El cuerpo de una función anónima no se ve afectado por los contextos checked o unchecked en los que ocurre la función anónima.

Example: En el código de ejemplo siguiente

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
}

no se notifica ningún error de compilación, ya que ninguna de las expresiones puede evaluarse en tiempo de compilación. En tiempo de ejecución, el método F lanza un System.OverflowExceptiony el método G devuelve –727379968 (los 32 bits inferiores del resultado fuera de rango). El comportamiento del método H depende del contexto de comprobación de desbordamiento predeterminado para la compilación, pero F es el mismo que G.

ejemplo final

Example: En el código de ejemplo siguiente

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
}

los desbordamientos que se producen al evaluar las expresiones de constantes en F y H hacen que se notifiquen errores en tiempo de compilación porque las expresiones se evalúan en un contexto de checked. También se produce un desbordamiento al evaluar la expresión constante en G, pero como la evaluación tiene lugar en un contexto unchecked, no se informa del desbordamiento.

ejemplo final

Los operadores checked y unchecked solo afectan al contexto de comprobación de desbordamiento de las operaciones que están textualmente contenidas en los tokens "(" y ")". Los operadores no tienen ningún efecto en los miembros de función que se invocan como resultado de evaluar la expresión contenida.

Example: En el código de ejemplo siguiente

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

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

el uso de checked en F no afecta a la evaluación de x * y en Multiply, por lo que x * y se evalúa en el contexto de comprobación de desbordamiento por defecto.

ejemplo final

El operador unchecked es conveniente cuando se escriben constantes de los tipos integrales con signo en notación hexadecimal.

Ejemplo:

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

Las dos constantes hexadecimales anteriores son del tipo uint. Debido a que las constantes están fuera del rango int, sin el operador unchecked, la conversión a int produciría errores de compilación.

ejemplo final

Nota: Los operadores e instrucciones checked y unchecked permiten a los programadores controlar ciertos aspectos de algunos cálculos numéricos. Sin embargo, el comportamiento de algunos operadores numéricos depende de los tipos de datos de sus operandos. Por ejemplo, la multiplicación de dos decimales siempre produce una excepción por desbordamiento, incluso dentro de una construcción explícitamente no comprobada. De forma similar, la multiplicación de dos flotantes nunca produce una excepción por desbordamiento, incluso dentro de una construcción explícitamente comprobada. Además, otros operadores nunca se ven afectados por el modo de comprobación, ya sea por defecto o explícito. nota final

12.8.21 Expresiones de valor por defecto

Una expresión de valor por defecto se utiliza para obtener el valor por defecto (sección 9.3) de un tipo.

default_value_expression
    : explictly_typed_default
    | default_literal
    ;

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

default_literal
    : 'default'
    ;

Un default_literal representa un valor predeterminado (sección 9.3). No tiene tipo, pero puede convertirse a cualquier tipo mediante la conversión de un literal por defecto (sección 10.2.16).

El resultado de una default_value_expression es el valor predeterminado (§9.3) del tipo explícito en un explictly_typed_default o el tipo de destino de la conversión para una default_value_expression.

Un default_value_expression es una expresión constante (sección 12.23) si el tipo es uno de:

  • un tipo de referencia
  • un parámetro de tipo que se sabe que es un tipo de referencia (sección 8.2);
  • uno de los siguientes tipos de valor: sbyte, byte, short, ushort, int, uint, long, ulong, char, float, double, decimal, bool, o
  • cualquier tipo de enumeración.

12.8.22 Asignación de pila

Una expresión de asignación de pila asigna un bloque de memoria de la pila de ejecución. La pila de ejecución es un área de memoria donde se almacenan las variables locales. La pila de ejecución no forma parte del montón administrado. La memoria usada para el almacenamiento de variables locales se recupera automáticamente cuando la función actual retorna.

Las reglas de contexto seguro para una expresión de asignación de pila se describen en la sección 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
    ;

El unmanaged_type (sección 8.8) indica el tipo de los elementos que se almacenarán en la nueva ubicación asignada, y la expresión indica el número de estos elementos. En conjunto, especifican el tamaño de asignación necesario. El tipo de expresión debe ser convertible implícitamente al tipo int.

Dado que el tamaño de una asignación de pila no puede ser negativo, se trata de un error en tiempo de compilación especificar el número de elementos como una constant_expression que se evalúa como un valor negativo.

En tiempo de ejecución, si el número de elementos a asignar es negativo, el comportamiento es indefinido. Si es cero, no se realiza ninguna asignación y el valor devuelto está definido por la implementación. Si no hay suficiente memoria disponible para asignar los elementos se lanza un System.StackOverflowException.

Cuando está presente un stackalloc_initializer:

  • Si se omite unmanaged_type, se deduce que sigue las reglas para el mejor tipo común (§12.6.3.15) para el conjunto de stackalloc_element_initializers.
  • Si constant_expression se omite, se deduce que es el número de stackalloc_element_initializers.
  • Si constant_expression está presente, será igual al número de stackalloc_element_initializers.

Cada stackalloc_element_initializer tendrá una conversión implícita a unmanaged_type (sección 10.2). Los stackalloc_element_initializers inicializan elementos en la memoria asignada en orden creciente, comenzando con el elemento en el índice cero. En ausencia de un stackalloc_initializer, el contenido de la memoria recién asignada es indefinido.

Si se produce una expresión stackalloc_expression directamente como expresión inicializadora de una declaración local_variable_declaration (sección 13.6.2), donde local_variable_type es un tipo puntero (sección 23.3) o inferido (var), entonces el resultado de la expresión stackalloc_expression es un tipo puntero T* (sección 23.9). En este caso la expresión stackalloc_expression debe aparecer en código no seguro. En otro caso el resultado de una expresión stackalloc_expression es una instancia del tipo Span<T>, donde T es el tipo no administrado unmanaged_type:

  • Span<T> (sección C.3) es un tipo ref struct (sección 16.2.3), que presenta un bloque de memoria, aquí el bloque asignado por la expresión stackalloc_expression, como una colección indexable de elementos tipados (T).
  • La propiedad Length del objeto resultado devuelve el número de elementos que han sido asignados.
  • El indexador del resultado (§15.9) devuelve una variable_reference (§9.5) a un elemento del bloque asignado y se comprueba el intervalo.

Los inicializadores de asignación de pila no están permitidos en bloques catch o finally (sección 13.11).

Nota: No hay forma de liberar explícitamente la memoria asignada usando stackalloc. nota final

Todos los bloques de memoria asignados a la pila creados durante la ejecución de un miembro de función se descartan automáticamente cuando ese miembro de función devuelve.

Excepto para el operador stackalloc, C# no proporciona ninguna construcción predefinida para administrar la memoria recopilada sin elementos no utilizados. Tales servicios son típicamente proporcionados por bibliotecas de clases de soporte o importados directamente del sistema operativo subyacente.

Ejemplo:

// 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; }
}

En el caso de span8, el resultado stackalloc es un Span<int>, que es convertido por un operador implícito a ReadOnlySpan<int>. De forma similar, para span9, el resultante Span<double> se convierte al tipo definido Widget<double> por el usuario utilizando la conversión, como se muestra. ejemplo final

12.8.23 El operador nameof

Una nameof_expression se utiliza para obtener el nombre de una entidad de programa como una cadena 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
    ;

Dado que nameof no es una palabra clave, una nameof_expression es siempre sintácticamente ambigua con una invocación del nombre simple nameof. Por razones de compatibilidad, si una búsqueda de nombre (sección 12.8.4) del nombre nameof tiene éxito, la expresión se trata como una invocation_expression —independientemente de si la invocación es válida—. De lo contrario, es una nameof_expression.

Las búsquedas de nombres y accesos a miembros simples se realizan en el named_entity en tiempo de compilación, siguiendo las reglas descritas en §12.8.4 y §12.8.7. Sin embargo, cuando la búsqueda descrita en la sección 12.8.4 y sección 12.8.7 da lugar a un error porque se ha encontrado un miembro de instancia en un contexto estático, una expresión nameof_expression no produce tal error.

Se trata de un error en tiempo de compilación que una named_entity designe que un grupo de métodos tenga una type_argument_list. Es un error de tiempo de compilación que un named_entity_target tenga el tipo dynamic.

Una expresión nameof_expression es una expresión constante de tipo string, y no tiene ningún efecto en tiempo de ejecución. En concreto, su named_entity no se evalúa y se omite para los fines del análisis de asignación definitiva (§9.4.4.22). Su valor es el último identificador de la named_entity antes de la type_argument_list final opcional, transformada de la siguiente manera:

  • El prefijo "@", si se utiliza, se elimina.
  • Cada unicode_escape_sequence se transforma en su carácter Unicode correspondiente.
  • Se quitan todos los formatting_characters.

Estas son las mismas transformaciones que se aplican en la sección 6.4.3 cuando se comprueba la igualdad entre identificadores.

Ejemplo: A continuación se muestran los resultados de varias expresiones de nameof, suponiendo que hay un tipo genérico List<T> declarado en el espacio de nombres 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 { }
}

Las partes potencialmente sorprendentes de este ejemplo son la resolución de nameof(System.Collections.Generic) a solo "Genérico" en lugar del espacio de nombres completo, y de nameof(TestAlias) a "TestAlias" en lugar de "String". ejemplo final

12.8.24 Expresiones de método anónimas

Una anonymous_method_expression es una de las dos formas de definir una función anónima. Se describen con más detalle en la sección 12.19.

12.9 Operadores unarios

12.9.1 General

Los operadores +, -, ! (solo negación lógica sección 12.9.4), ~, ++, --, cast, y await se denominan operadores unarios.

Nota: el operador postfijo que admite valores NULL (§12.8.9), !, debido a su naturaleza en tiempo de compilación y no sobrecargable, se excluye de la lista anterior. 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 (sección 23.6.2) y addressof_expression (sección 23.6.5) solo están disponibles en código no seguro (sección 23).

Si el operando de una unary_expression tiene un tipo de tiempo de compilación dynamic, se enlaza dinámicamente (§12.3.3). En este caso, el tipo en tiempo de compilación de la unary_expression es dynamic, y la resolución descrita a continuación tendrá lugar en tiempo de ejecución mediante el tipo en tiempo de ejecución del operando.

12.9.2 Operador de suma unario

Para una operación de la forma +x, se aplica la resolución de sobrecarga de operador unario (sección 12.4.4) para seleccionar una implementación de operador específica. El operando se convierte al tipo de parámetro del operador seleccionado, y el tipo del resultado es el tipo de retorno del operador. Los operadores de suma unarios predefinidos son:

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 uno de estos operadores, el resultado es simplemente el valor del operando.

Las formas elevadas (§12.4.8) de los operadores de suma unarios predefinidos sin elevar definidos anteriormente también están predefinidas.

12.9.3 Operador de resta unario

Para una operación de la forma –x, se aplica la resolución de sobrecarga de operador unario (sección 12.4.4) para seleccionar una implementación de operador específica. El operando se convierte al tipo de parámetro del operador seleccionado, y el tipo del resultado es el tipo de retorno del operador. Los operadores de resta unarios predefinidos son:

  • Negación de enteros:

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

    El resultado se calcula restando de cero X. Si el valor de X es el valor representable más pequeño del tipo de operando (-2³¹ para int o -2⁶³ para long), entonces la negación matemática de X no es representable dentro del tipo de operando. Si esto ocurre dentro de un contexto de checked, se lanza un System.OverflowException; si ocurre dentro de un contexto de unchecked, el resultado es el valor del operando y no se notifica el desbordamiento.

    Si el operando del operador de negación es del tipo uint, se convierte al tipo long, y el tipo del resultado es long. Una excepción es la regla que permite escribir el valor int −2147483648 (-2³¹) como un literal entero decimal (sección 6.4.5.3).

    Si el operando del operador de negación es del tipo ulong, se produce un error de compilación. Una excepción es la regla que permite escribir el valor long −9223372036854775808 (-2⁶³) como un literal entero decimal (sección 6.4.5.3)

  • Negación en coma flotante:

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

    El resultado es el valor de X con su signo invertido. Si x es NaN, el resultado también es NaN.

  • Negación decimal:

    decimal operator –(decimal x);
    

    El resultado se calcula restando de cero X. La negación decimal es equivalente a utilizar el operador menos unario de tipo System.Decimal.

Las formas elevadas (§12.4.8) de los operadores de resta unarios predefinidos sin elevar definidos anteriormente también están predefinidas.

12.9.4 Operador lógico de negación

Para una operación de la forma !x, se aplica la resolución de sobrecarga de operador unario (sección 12.4.4) para seleccionar una implementación de operador específica. El operando se convierte al tipo de parámetro del operador seleccionado, y el tipo del resultado es el tipo de retorno del operador. Solo existe un operador de negación lógica predefinido:

bool operator !(bool x);

Este operador calcula la negación lógica del operando: Si el operando es true, el resultado es false. Si el operando es false, el resultado es true.

Las formas elevadas (§12.4.8) del operador de negación lógica predefinido sin elevar definido anteriormente también están predefinidas.

Nota: los operadores de negación lógica de prefijo y de postfijo que admiten valores NULL (§12.8.9), aunque se representan mediante el mismo token léxico (!), son distintos. nota final

12.9.5 Operador de complemento bit a bit

Para una operación de la forma ~x, se aplica la resolución de sobrecarga de operador unario (sección 12.4.4) para seleccionar una implementación de operador específica. El operando se convierte al tipo de parámetro del operador seleccionado, y el tipo del resultado es el tipo de retorno del operador. Los operadores de complemento a nivel de bit predefinidos son:

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

Para cada uno de estos operadores, el resultado de la operación es el complemento bit a bit de x.

Cada tipo de enumeración E proporciona implícitamente el siguiente operador de complemento a nivel de bits:

E operator ~(E x);

El resultado de evaluar ~x, donde X es una expresión de un tipo de enumeración E con un tipo subyacente U, es exactamente el mismo que evaluar (E)(~(U)x), salvo que la conversión a E se realiza siempre como si fuera en un contexto unchecked (sección 12.8.20).

Las formas elevadas (§12.4.8) de los operadores de complemento bit a bit predefinidos sin elevar definidos anteriormente también están predefinidas.

12.9.6 Operadores de incremento y decremento prefijos

pre_increment_expression
    : '++' unary_expression
    ;

pre_decrement_expression
    : '--' unary_expression
    ;

El operando de una operación de incremento o decremento de prefijo será una expresión clasificada como variable, acceso a una propiedad o acceso a un indexador. El resultado de la operación tiene un valor del mismo tipo que el operando.

Si el operando de una operación de incremento o disminución de prefijo es un acceso de propiedad o indexador, la propiedad o el indexador deberán tener tanto un descriptor de acceso get como un descriptor de acceso set. Si no es así, se produce un error en tiempo de enlace.

La resolución de sobrecarga de operador unario (sección 12.4.4) se aplica para seleccionar una implementación de operador específica. Existen operadores de ++ y -- predefinidos para los siguientes tipos: sbyte, byte, short, ushort, int, uint, long, ulong, char, float, double, decimaly cualquier tipo de enumeración. Los operadores predefinidos ++ devuelven el valor producido al sumar 1 al operando, y los operadores predefinidos -- devuelven el valor producido al restar 1 del operando. En un contexto checked, si el resultado de esta suma o resta está fuera del rango del tipo de resultado y el tipo de resultado es un tipo entero o un tipo de enumeración, se lanza una System.OverflowException.

Debe haber una conversión implícita del tipo de retorno del operador unario seleccionado al tipo de la unary_expression; de lo contrario, se produce un error de compilación.

El procesamiento en tiempo de ejecución de una operación de incremento o decremento de prefijo de la forma ++x o --x consta de los siguientes pasos:

  • Si x se clasifica como variable:
    • x se evalúa para obtener la variable.
    • El valor de x se convierte al tipo de operando del operador seleccionado y se invoca al operador con este valor como argumento.
    • El valor devuelto por el operador se convierte al tipo de x. El valor resultante se almacena en la ubicación dada por la evaluación de x y se convierte en el resultado de la operación.
  • Si x se clasifica como acceso a una propiedad o indexador:
    • La expresión de instancia (si x no es static) y la lista de argumentos (si x es un acceso indexador) asociada a se evalúan con x, y los resultados se utilizan en las invocaciones subsiguientes del descriptor de acceso get y set.
    • Se invoca al descriptor de acceso get de x.
    • El valor devuelto por el descriptor de acceso get se convierte al tipo de operando del operador seleccionado y el operador se invoca con este valor como argumento.
    • El valor devuelto por el operador se convierte al tipo de x. El descriptor de acceso set de x se invoca con este valor como su argumento de valor.
    • Este valor también se convierte en el resultado de la operación.

Los operadores ++ y -- también admiten la notación postfija (sección 12.8.16). El resultado de x++ o x-- es el valor de x antes de la operación, mientras que el resultado de ++x o --x es el valor de x después de la operación. En cualquier caso, x tiene el mismo valor después de la operación.

Se puede invocar la implementación de un operador ++ o un operador -- mediante notación de prefijo o posfijo. No es posible tener implementaciones de operadores separadas para las dos notaciones.

Las formas elevadas (§12.4.8) de los operadores de incremento y disminución predefinidos sin elevar también están predefinidas.

12.9.7 Expresiones de conversión

Una cast_expression se utiliza para convertir explícitamente una expresión a un tipo dado.

cast_expression
    : '(' type ')' unary_expression
    ;

Una cast_expression con el formato (T)E, donde T es un tipo y E es una unary_expression, realiza una conversión explícita (§10.3) del valor de E al tipo T. Si no existe una conversión explícita de E a T, se produce un error de vinculación. En caso contrario, el resultado es el valor producido por la conversión explícita. El resultado siempre se clasifica como un valor, incluso si E denota una variable.

La gramática para una cast_expression conduce a ciertas ambigüedades sintácticas.

Ejemplo: La expresión (x)–y podría interpretarse como una cast_expression (una conversión del tipo –y a tipo x) o como una additive_expression combinada con una parenthesized_expression (que calcula el valor x – y). ejemplo final

Para resolver las ambigüedades de cast_expression, existe la siguiente regla: Una secuencia de uno o más tokens (sección 6.4) encerrada entre paréntesis se considera el comienzo de una cast_expression solo si al menos una de las siguientes condiciones es cierta:

  • La secuencia de tokens es gramática correcta para un tipo, pero no para una expresión.
  • La secuencia de tokens es una gramática correcta para un tipo, y el token que sigue inmediatamente después de los paréntesis de cierre es el token "~", el token "!", el token "(", un identificador (§6.4.3), un literal (§6.4.5) o cualquier palabra clave (§6.4.4), excepto as y is.

El término "gramática correcta" anterior solo significa que la secuencia de tokens se ajustará a la producción gramatical particular. En concreto, no tiene en cuenta el significado real de ninguno de los identificadores constituyentes.

Ejemplo: Si x y y son identificadores, entonces x.y es una gramática correcta para un tipo, aunque x.y en realidad no denote un tipo. ejemplo final

Nota: de la regla de desambiguación se deduce que, si x y y son identificadores, (x)y, (x)(y) y (x)(-y) son cast_expressions, pero (x)-y no, aunque x identifique un tipo. Sin embargo, si x es una palabra clave que identifica un tipo predefinido (como int), entonces todas las cuatro formas son cast_expressions (porque tal palabra clave no podría ser posiblemente una expresión por sí misma). nota final

12.9.8 Expresiones await

12.9.8.1 General

El operador await se usa para suspender la evaluación de la función asincrónica envolvente hasta que se haya completado la operación asincrónica representada por el operando.

await_expression
    : 'await' unary_expression
    ;

Solo se permite una await_expression en el cuerpo de una función asincrónica (§15.15). Dentro de la función asincrónica de cierre más cercana, no se producirá una await_expression en estos lugares:

  • Dentro de una función anónima anidada (no asíncrona)
  • Dentro del bloque de una lock_statement
  • En una conversión de función anónima a un tipo de árbol de expresión (sección 10.7.3)
  • En un contexto inseguro

Nota: no se puede producir una await_expression en la mayoría de los lugares de una query_expression, ya que se transforman sintácticamente para usar expresiones lambda no asincrónicas. nota final

Dentro de una función asíncrona, no se usará await como available_identifier aunque se pueda usar el identificador textual @await. Por lo tanto, no hay ambigüedad sintáctica entre await_expressions y varias expresiones que implican identificadores. Fuera de las funciones asíncronas, await actúa como un identificador normal.

El operando de una await_expression se denomina tarea . Representa una operación asíncrona que puede o no estar completa en el momento en que se evalúa la await_expression. El propósito del operador await es suspender la ejecución de la función asíncrona adjunta hasta que la tarea esperada se complete, y luego obtener su resultado.

12.9.8.2 Expresiones que admiten await

Es necesario que la tarea de una await_expressionadmita await. Una expresión t es esperable si se cumple una de las siguientes condiciones:

  • t es del tipo dynamic en tiempo de compilación
  • t tiene un método de instancia o extensión accesible llamado GetAwaiter que no tiene parámetros ni parámetros de tipo, y un tipo de retorno A para el cual se cumplen todas las siguientes condiciones:
    • A implementa la interfaz System.Runtime.CompilerServices.INotifyCompletion (en adelante conocido como INotifyCompletion para simplificar).
    • A tiene una propiedad de instancia accesible y legible IsCompleted de tipo bool
    • A tiene un método de instancia accesible GetResult sin parámetros y sin parámetros de tipo

El propósito del método GetAwaiter es obtener un elemento await para la tarea. El tipo A se denomina tipo awaiter para la expresión await.

El propósito de la propiedad IsCompleted es determinar si la tarea ya se ha completado. En caso afirmativo, no es necesario suspender la evaluación.

La finalidad del método INotifyCompletion.OnCompleted es dar de alta una "continuación" a la tarea; es decir, un delegado (de tipo System.Action) que será invocado una vez finalizada la tarea.

La finalidad del método GetResult es obtener el resultado de la tarea una vez finalizada. Este resultado puede ser una finalización correcta, posiblemente con un valor de resultado, o puede ser una excepción lanzada por el método GetResult.

12.9.8.3 Clasificación de expresiones await

La expresión await t se clasifica del mismo modo que la expresión (t).GetAwaiter().GetResult(). Así, si el tipo de devolución de GetResult es void, la await_expression se clasifica como nada. Si tiene un tipo de retorno novoidT, el await_expression se clasifica como un valor de tipo T.

12.9.8.4 Evaluación en tiempo de ejecución de expresiones await

En tiempo de ejecución, la expresión await t se evalúa de la siguiente manera:

  • Se obtiene un elemento await a al evaluar la expresión (t).GetAwaiter().
  • Se obtiene un boolb evaluando la expresión (a).IsCompleted.
  • Si b es false, la evaluación depende de si a implementa la interfaz System.Runtime.CompilerServices.ICriticalNotifyCompletion (a partir de ahora conocida como ICriticalNotifyCompletion por brevedad). Esta comprobación se realiza en tiempo de enlace; esto es, en tiempo de ejecución si a tiene el tipo en tiempo de compilación dynamic; y en tiempo de compilación de lo contrario. Sea r el delegado de reanudación (§15.15):
    • Si a no implementa ICriticalNotifyCompletion, se evalúa la expresión ((a) as INotifyCompletion).OnCompleted(r).
    • Si a implementa ICriticalNotifyCompletion, se evalúa la expresión ((a) as ICriticalNotifyCompletion).UnsafeOnCompleted(r).
    • La evaluación se suspende y el control se devuelve al invocador actual de la función asíncrona.
  • Ya sea inmediatamente después (si b fue true) o tras la invocación posterior del delegado de reanudación (si b fue false), se evalúa la expresión (a).GetResult(). Si devuelve un valor, ese valor es el resultado de la await_expression. De lo contrario, el resultado será nulo.

La implementación que un elemento await haga de los métodos de interfaz INotifyCompletion.OnCompleted y ICriticalNotifyCompletion.UnsafeOnCompleted debe lograr que el delegado r se invoque como máximo una vez. En caso contrario, el comportamiento de la función asíncrona es indefinido.

12.10 Operadores aritméticos

12.10.1 General

Los operadores *, /, %, + y - y se denominan 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
    ;

Si un operando de un operador aritmético tiene el tipo en tiempo de compilación dynamic, entonces la expresión está ligada dinámicamente (sección 12.3.3). En este caso, el tipo en tiempo de compilación de la expresión es dynamic, y la resolución descrita a continuación tendrá lugar en tiempo de ejecución utilizando el tipo en tiempo de ejecución de aquellos operandos que tengan el tipo en tiempo de compilación dynamic.

12.10.2 Operador de multiplicación

Para una operación de la forma x * y, se aplica la resolución de sobrecarga de operador binaria (sección 12.4.5) para seleccionar una implementación de operador específica. Los operandos se convierten a los tipos de parámetros del operador seleccionado, y el tipo del resultado es el tipo de retorno del operador.

A continuación se enumeran los operadores de multiplicación predefinidos. Todos los operadores calculan el producto de x y y.

  • Multiplicación de enteros:

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

    En un contexto de checked, si el producto está fuera del intervalo del tipo de resultado, se lanza una System.OverflowException. En un contexto de unchecked, los desbordamientos no se notifican y se descartan los bits importantes de orden superior fuera del rango del tipo de resultado.

  • Multiplicación en coma flotante:

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

    El producto se calcula según las reglas de la aritmética IEC 60559. La siguiente tabla enumera los resultados de todas las combinaciones posibles de valores finitos distintos de cero, ceros, infinitos y NaNs. En la tabla, x e y son valores finitos positivos. z es el resultado de x * y, redondeado al valor representable más cercano. Si la magnitud del resultado es demasiado grande para el tipo de destino, z es infinito. Debido al redondeo, z puede ser cero aunque x ni y sea cero.

    +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

    (Excepto cuando se indique lo contrario, en las tablas de punto flotante de §12.10.2§12.10.6 el uso de "+" significa que el valor es positivo; el uso de "-" significa que el valor es negativo; y la falta de un signo significa que el valor puede ser positivo o negativo o no tiene ningún signo (NaN).

  • Multiplicación decimal:

    decimal operator *(decimal x, decimal y);
    

    Si la magnitud del valor resultante es demasiado grande para representarla en el formato decimal, se lanza un System.OverflowException. Debido al redondeo, el resultado puede ser cero aunque ninguno de los operandos sea cero. La escala del resultado, antes de cualquier redondeo, es la suma de las escalas de los dos operandos. La multiplicación decimal es equivalente a utilizar el operador de multiplicación de tipo System.Decimal.

Las formas elevadas (§12.4.8) de los operadores de multiplicación predefinidos no elevados definidos anteriormente.

12.10.3 Operador de división

Para una operación de la forma x / y, se aplica la resolución de sobrecarga de operador binaria (sección 12.4.5) para seleccionar una implementación de operador específica. Los operandos se convierten a los tipos de parámetros del operador seleccionado, y el tipo del resultado es el tipo de retorno del operador.

A continuación se enumeran los operadores de división predefinidos. Todos los operadores calculan el cociente de x y y.

  • División entera:

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

    Si el valor del operando derecho es cero, se lanza una System.DivideByZeroException.

    La división redondea el resultado hacia cero. Así, el valor absoluto del resultado es el mayor número entero posible menor o igual que el valor absoluto del cociente de los dos operandos. El resultado es cero o positivo cuando los dos operandos tienen el mismo signo y cero o negativo cuando los dos operandos tienen signos opuestos.

    Si el operando izquierdo es el valor mínimo representable de int o long y el operando derecho es –1, se produce un desbordamiento. En un contexto de checked, esto provoca la generación de una System.ArithmeticException (o una subclase de esta). En un contexto de unchecked, la implementación define si se lanza una System.ArithmeticException (o una subclase de esta) o si el desbordamiento no se notifica, lo que hace que el valor sea el del operando izquierdo.

  • División de punto flotante

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

    El cociente se calcula según las reglas de la aritmética IEC 60559. La siguiente tabla enumera los resultados de todas las combinaciones posibles de valores finitos distintos de cero, ceros, infinitos y NaNs. En la tabla, x e y son valores finitos positivos. z es el resultado de x / y, redondeado al valor representable más cercano.

    +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
  • División decimal:

    decimal operator /(decimal x, decimal y);
    

    Si el valor del operando derecho es cero, se lanza una System.DivideByZeroException. Si la magnitud del valor resultante es demasiado grande para representarla en el formato decimal, se lanza un System.OverflowException. Debido al redondeo, el resultado puede ser cero aunque el primer operando no sea cero. La escala del resultado, antes de cualquier redondeo, es la escala más cercana a la escala preferida que preservará un resultado igual al resultado exacto. La escala preferida es la escala de x menos la escala de y.

    La división decimal es equivalente a utilizar el operador de división de tipo System.Decimal.

Las formas elevadas (§12.4.8) de los operadores de división predefinidos sin elevar definidos anteriormente también están predefinidas.

12.10.4 Operador de resto

Para una operación de la forma x % y, se aplica la resolución de sobrecarga de operador binaria (sección 12.4.5) para seleccionar una implementación de operador específica. Los operandos se convierten a los tipos de parámetros del operador seleccionado, y el tipo del resultado es el tipo de retorno del operador.

A continuación se enumeran los operadores de resto predefinidos. Todos los operadores calculan el resto de la división entre x y y.

  • Resto entero:

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

    El resultado de x % y es el valor producido por x – (x / y) * y. Si y es cero, se lanza un System.DivideByZeroException.

    Si el operando izquierdo es el valor más pequeño de int o long y el operando derecho es –1, se produce una System.OverflowException solo si x / y produciría una excepción.

  • Resto en coma flotante:

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

    La siguiente tabla enumera los resultados de todas las combinaciones posibles de valores finitos distintos de cero, ceros, infinitos y NaNs. En la tabla, x e y son valores finitos positivos. z es el resultado de x % y y se calcula como x – n * y, donde n es el mayor número entero posible menor o igual que x / y. Este método de cálculo del resto es análogo al utilizado para los operandos enteros, pero difiere de la definición de la norma IEC 60559 (en la que n es el entero más próximo a 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
  • Resto decimal:

    decimal operator %(decimal x, decimal y);
    

    Si el valor del operando derecho es cero, se lanza una System.DivideByZeroException. Queda a criterio de la implementación cuándo se lanza una System.ArithmeticException (o una subclase de esta). Una implementación conforme no lanzará una excepción para x % y en ningún caso en el que x / y no lance una excepción. La escala del resultado, antes de cualquier redondeo, es la mayor de las escalas de los dos operandos, y el signo del resultado, si es distinto de cero, es el mismo que el de x.

    El resto decimal es equivalente a usar el operador de resto del tipo System.Decimal.

    Nota: estas reglas garantizan que para todos los tipos, el resultado nunca tenga el signo opuesto del operando izquierdo. nota final

Las formas elevadas (§12.4.8) de los operadores de resto predefinidos sin elevar definidos anteriormente también están predefinidas.

12.10.5 Operador de suma

Para una operación de la forma x + y, se aplica la resolución de sobrecarga de operador binaria (sección 12.4.5) para seleccionar una implementación de operador específica. Los operandos se convierten a los tipos de parámetros del operador seleccionado, y el tipo del resultado es el tipo de retorno del operador.

A continuación se enumeran los operadores de suma predefinidos. Para los tipos numéricos y de enumeración, los operadores de suma predefinidos calculan la suma de los dos operandos. Cuando uno o ambos operandos son de tipo string, los operadores de suma predefinidos concatenan la representación de cadena de los operandos.

  • Suma de números enteros:

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

    En un contexto de checked, si la suma se encuentra fuera del rango del tipo de resultado, se lanza un System.OverflowException. En un contexto de unchecked, los desbordamientos no se notifican y se descartan los bits importantes de orden superior fuera del rango del tipo de resultado.

  • Suma en coma flotante:

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

    La suma se calcula según las reglas de la aritmética IEC 60559. La siguiente tabla enumera los resultados de todas las combinaciones posibles de valores finitos distintos de cero, ceros, infinitos y NaNs. En la tabla, x e y son valores finitos distintos de cero y z es el resultado de x + y. Si x e y tienen la misma magnitud pero signos opuestos, z es cero positivo. Si x + y es demasiado grande para representarlo en el tipo de destino, z es un infinito con el mismo signo 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
  • Suma decimal:

    decimal operator +(decimal x, decimal y);
    

    Si la magnitud del valor resultante es demasiado grande para representarla en el formato decimal, se lanza un System.OverflowException. La escala del resultado, antes de cualquier redondeo, es la mayor entre las escalas de los dos operandos.

    La suma decimal es equivalente a utilizar el operador de suma de tipo System.Decimal.

  • Suma de enumeración. Cada tipo de enumeración proporciona implícitamente los siguientes operadores predefinidos, donde E es el tipo de enumeración, y U es el tipo subyacente de E:

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

    En tiempo de ejecución, estos operadores se evalúan exactamente como (E)((U)x + (U)y).

  • Concatenación de cadenas:

    string operator +(string x, string y);
    string operator +(string x, object y);
    string operator +(object x, string y);
    

    Estas sobrecargas del operador binario + realizan la concatenación de cadenas. Si un operando de la concatenación de cadenas es null, se sustituye por una cadena vacía. En caso contrario, cualquier no operando string se convierte a su representación de cadena invocando el método virtual ToString heredado del tipo object. Si ToString devuelve null, se sustituye por una cadena vacía.

    Ejemplo:

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

    La salida mostrada en los comentarios es el resultado típico en un sistema inglés estadounidense. El resultado exacto puede depender de la configuración regional del entorno de ejecución. El operador de concatenación de cadenas se comporta de la misma manera en todos los casos, pero los métodos ToString llamados implícitamente durante la ejecución pueden verse afectados por la configuración regional.

    ejemplo final

    El resultado del operador de concatenación de cadenas es un string que consta de los caracteres del operando izquierdo seguidos de los caracteres del operando derecho. El operador de concatenación de cadenas nunca devuelve un valor null. Se puede producir un error System.OutOfMemoryException si no hay suficiente memoria disponible para asignar la cadena resultante.

  • Combinación de delegados. Cada tipo de delegado proporciona implícitamente el siguiente operador predefinido, donde D es el tipo de delegado:

    D operator +(D x, D y);
    

    Si el primer operando es null, el resultado de la operación es el valor del segundo operando (aunque este también sea null). En caso contrario, si el segundo operando es null, el resultado de la operación es el valor del primer operando. En caso contrario, el resultado de la operación es una nueva instancia de delegado cuya lista de invocación está formada por los elementos de la lista de invocación del primer operando, seguidos de los elementos de la lista de invocación del segundo operando. Es decir, la lista de invocaciones del delegado resultante es la concatenación de las listas de invocaciones de los dos operandos.

    Nota: Para ejemplos de combinación de delegados, véanse sección 12.10.6 y sección 20.6. Dado System.Delegate que no es un tipo delegado, el operador + no está definido para él. nota final

Las formas elevadas (§12.4.8) de los operadores de suma predefinidos sin elevar definidos anteriormente también están predefinidas.

12.10.6 Operador de resta

Para una operación de la forma x – y, se aplica la resolución de sobrecarga de operador binaria (sección 12.4.5) para seleccionar una implementación de operador específica. Los operandos se convierten a los tipos de parámetros del operador seleccionado, y el tipo del resultado es el tipo de retorno del operador.

A continuación se enumeran los operadores de resta predefinidos. Todos los operadores restan y a x.

  • Resta de números enteros:

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

    En un contexto de checked, si la diferencia está fuera del intervalo del tipo de resultado, se lanza una System.OverflowException. En un contexto de unchecked, los desbordamientos no se notifican y se descartan los bits importantes de orden superior fuera del rango del tipo de resultado.

  • Resta de punto flotante:

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

    La diferencia se calcula según las reglas de la aritmética IEC 60559. La siguiente tabla enumera los resultados de todas las combinaciones posibles de valores finitos distintos de cero, ceros, infinitos y NaNs. En la tabla, x e y son valores finitos distintos de cero y z es el resultado de x – y. Si x e y son iguales, z es cero positivo. Si x – y es demasiado grande para representarlo en el tipo de destino, z es un infinito con el mismo signo 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

    (En la tabla anterior, las entradas -y denotan la negación de y, no que el valor sea negativo).

  • Resta decimal:

    decimal operator –(decimal x, decimal y);
    

    Si la magnitud del valor resultante es demasiado grande para representarla en el formato decimal, se lanza un System.OverflowException. La escala del resultado, antes de cualquier redondeo, es la mayor entre las escalas de los dos operandos.

    La resta decimal es equivalente a utilizar el operador de resta de tipo System.Decimal.

  • Resta de enumeración. Cada tipo de enumeración proporciona implícitamente el siguiente operador predefinido, donde E es el tipo de enumeración U, y es el tipo subyacente de E:

    U operator –(E x, E y);
    

    Este operador se evalúa exactamente como (U)((U)x – (U)y). En otras palabras, el operador calcula la diferencia entre los valores ordinales de x y y, y el tipo del resultado es el tipo subyacente de la enumeración.

    E operator –(E x, U y);
    

    Este operador se evalúa exactamente como (E)((U)x – y). En otras palabras, el operador resta un valor del tipo subyacente de la enumeración, obteniendo un valor de la enumeración.

  • Eliminación de delegados. Cada tipo de delegado proporciona implícitamente el siguiente operador predefinido, donde D es el tipo de delegado:

    D operator –(D x, D y);
    

    La semántica es la siguiente:

    • Si el primer operando es null, el resultado de la operación es null.
    • En caso contrario, si el segundo operando es null, el resultado de la operación es el valor del primer operando.
    • En caso contrario, ambos operandos representan listas de invocación no vacías (sección 20.2).
      • Si las listas son iguales al compararlas, como determina el operador de igualdad del delegado (§12.12.9), el resultado de la operación es null.
      • En caso contrario, el resultado de la operación es una nueva lista de invocación formada por la lista del primer operando de la que se han eliminado las entradas del segundo operando, siempre que la lista del segundo operando sea una sublista de la del primero. (Para determinar la igualdad de la sublista, las entradas correspondientes se comparan como con el operador de igualdad del delegado.) Si la lista del segundo operando coincide con varias sublistas de entradas contiguas de la lista del primer operando, se quita la última sublista coincidente de entradas contiguas.
      • De lo contrario, el resultado de la operación es el valor del operando izquierdo.

    Ninguna de las listas de los operandos (si las hay) se modifica en el proceso.

    Ejemplo:

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

    ejemplo final

Las formas elevadas (§12.4.8) de los operadores de resta predefinidos sin elevar definidos anteriormente también están predefinidas.

12.11 Operadores de desplazamiento

Los operadores << y >> se utilizan para realizar operaciones de desplazamiento de bits.

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

Si un operando de una shift_expression tiene el tipo en tiempo de compilación dynamic, la expresión se enlaza dinámicamente (§12.3.3). En este caso, el tipo en tiempo de compilación de la expresión es dynamic, y la resolución descrita a continuación tendrá lugar en tiempo de ejecución utilizando el tipo en tiempo de ejecución de aquellos operandos que tengan el tipo en tiempo de compilación dynamic.

Para una operación de la forma x << count o x >> count, se aplica la resolución de sobrecarga de operadores binarios (sección 12.4.5) para seleccionar una implementación de operador específica. Los operandos se convierten a los tipos de parámetros del operador seleccionado, y el tipo del resultado es el tipo de retorno del operador.

Al declarar un operador de desplazamiento sobrecargado, el tipo del primer operando será siempre la clase o estructura que contiene la declaración del operador, y el tipo del segundo operando será siempre int.

A continuación se enumeran los operadores de desplazamiento predefinidos.

  • Desplazamiento a la izquierda:

    int operator <<(int x, int count);
    uint operator <<(uint x, int count);
    long operator <<(long x, int count);
    ulong operator <<(ulong x, int count);
    

    El operador << desplaza x a la izquierda por un número de bits calculados como se describe a continuación.

    Los bits de orden superior fuera del rango del tipo de resultado de x se descartan, los bits restantes se desplazan a la izquierda, y las posiciones de bits vacíos de orden inferior se ponen a cero.

  • Desplazamiento a la derecha:

    int operator >>(int x, int count);
    uint operator >>(uint x, int count);
    long operator >>(long x, int count);
    ulong operator >>(ulong x, int count);
    

    El operador >> desplaza x a la derecha por un número de bits calculado como se describe a continuación.

    Cuando x es del tipo int o long, los bits de bajo orden de x se descartan, los bits restantes se desplazan a la derecha y las posiciones de bits vacíos de alto orden se ponen a cero si x es no negativo y a uno si x es negativo.

    Cuando x es de tipo uint o ulong, los bits de orden inferior de x se descartan, los bits restantes se desplazan a la derecha y las posiciones de bits vacíos de orden superior se ponen a cero.

Para los operadores predefinidos, el número de bits a desplazar se calcula de la siguiente manera:

  • Cuando el tipo de x es int o uint, el recuento de desplazamientos lo proporcionan los cinco bits de orden bajo de count. En otras palabras, el número de bits a desplazar se calcula a partir de count & 0x1F.
  • Cuando el tipo de x es long o ulong, el recuento de desplazamientos lo proporcionan los seis bits de orden bajo de count. En otras palabras, el número de bits a desplazar se calcula a partir de count & 0x3F.

Si el recuento de desplazamientos resultante es cero, los operadores de desplazamiento simplemente devuelven el valor de x.

Las operaciones de desplazamiento nunca provocan desbordamientos y generan los mismos resultados en contextos controlados y no controlados.

Cuando el operando izquierdo del operador >> es de un tipo entero con signo, el operador realiza un desplazamiento aritmético a la derecha, donde el valor del bit más significativo (el bit de signo) del operando se propaga a las posiciones de bits vacías de orden alto. Cuando el operando izquierdo del operador >> es de un tipo entero sin signo, el operador realiza un desplazamiento lógico a la derecha, donde las posiciones de bits vacías de orden alto siempre se establecen en cero. Para realizar la operación opuesta a la inferida del tipo de operando, se pueden usar conversiones explícitas.

Ejemplo: Si x es una variable de tipo int, la operación unchecked ((int)((uint)x >> y)) realiza un desplazamiento lógico a la derecha de x. ejemplo final

Las formas elevadas (§12.4.8) de los operadores de desplazamiento predefinidos sin elevar definidos anteriormente también están predefinidas.

12.12 Operadores relacionales y de comprobación de tipo

12.12.1 General

Los operadores ==, !=, <, >, <=, >=, is y as, y se denominan operadores relacionales y de comprobación 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: la búsqueda del operando derecho del operador is debe probar primero como un tipo y, a continuación, como una expresión que puede abarcar varios tokens. En el caso de que el operando sea una expresión , la expresión de patrón debe tener una precedencia como mínimo tan alta como la de shift_expression. nota final

El operador is se describe en la sección 12.12.12 y el operador as se describe en la sección 12.12.13.

Los operadores ==, !=, <, >, <= y >= y son operadores de comparación.

Si se utiliza un default_literal (§12.8.21) como operando de un operador <, >, <= o >=, se produce un error en tiempo de compilación. Si se utiliza un default_literal como ambos operandos de un operador == o !=, se produce un error en tiempo de compilación. Si se utiliza un default_literal como operando izquierdo del operador is o as, se produce un error de compilación.

Si un operando de un operador de comparación tiene el tipo en tiempo de compilación dynamic, entonces la expresión está ligada dinámicamente (sección 12.3.3). En este caso, el tipo en tiempo de compilación de la expresión es dynamic, y la resolución descrita a continuación tendrá lugar en tiempo de ejecución utilizando el tipo en tiempo de ejecución de aquellos operandos que tengan el tipo en tiempo de compilación dynamic.

Para una operación de la forma x «op» y, donde "op" es un operador de comparación, se aplica la resolución de sobrecarga (sección 12.4.5) para seleccionar una implementación específica del operador. Los operandos se convierten a los tipos de parámetros del operador seleccionado, y el tipo del resultado es el tipo de retorno del operador. Si ambos operandos de una equality_expression son el literal null, no se realiza la resolución de sobrecarga y la expresión se evalúa como un valor constante de true o false, dependiendo de si el operador es == o !=.

Los operadores de comparación predefinidos se describen en las siguientes subcláusulas. Todos los operadores de comparación predefinidos devuelven un resultado de tipo bool, como se describe en la siguiente tabla.

Operación Resultado
x == y true si x es igual a y, false en caso contrario
x != y truesi x no es igual a y, en caso contrario es false
x < y true si x es menor que y, false en caso contrario
x > y true si x es mayor que y, false en caso contrario
x <= y true si x es menor o igual que y, false en caso contrario
x >= y true si x es mayor o igual que y, false en caso contrario

12.12.2 Operadores de comparación de enteros

Los operadores de comparación de enteros predefinidos son:

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 uno de estos operadores compara los valores numéricos de los dos operandos enteros y devuelve un valor bool que indica si la relación particular es true o false.

Las formas elevadas (§12.4.8) de los operadores de comparación de enteros predefinidos sin elevar definidos anteriormente también están predefinidas.

12.12.3 Operadores de comparación de coma flotante

Los operadores de comparación de coma flotante predefinidos son:

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

Los operadores comparan los operandos según las reglas de la norma IEC 60559:

Si cualquiera de los operandos es NaN, el resultado es false para todos los operadores excepto !=, para el cual el resultado es true. Para dos operandos cualesquiera, x != y siempre produce el mismo resultado que !(x == y). Sin embargo, cuando uno o ambos operandos son NaN, los operadores <, >, <= y >= no producen los mismos resultados que la negación lógica del operador opuesto.

Ejemplo: Si cualquiera de x y y es NaN, entonces x < y es false, pero !(x >= y) es true. ejemplo final

Cuando ninguno de los operandos es NaN, los operadores comparan los valores de los dos operandos en coma flotante con respecto a la ordenación

–∞ < –max < ... < –min < –0.0 == +0.0 < +min < ... < +max < +∞

donde min y max son los valores finitos positivos más pequeño y más grande que pueden representarse en el formato de coma flotante dado. Los efectos importantes de este orden son:

  • Los ceros negativos y positivos se consideran iguales.
  • Un infinito negativo se considera menor que todos los demás valores, pero igual a otro infinito negativo.
  • Un infinito positivo se considera mayor que todos los demás valores, pero igual a otro infinito positivo.

Las formas elevadas (§12.4.8) de los operadores de comparación de punto flotante predefinidos sin elevar definidos anteriormente también están predefinidas.

12.12.4 Operadores de comparación decimal

Los operadores de comparación decimal predefinidos son:

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 uno de estos operadores compara los valores numéricos de los dos operandos decimales y devuelve un valor bool que indica si la relación concreta es true o false. Cada comparación decimal equivale a utilizar el correspondiente operador relacional o de igualdad de tipo System.Decimal.

Las formas elevadas (§12.4.8) de los operadores de comparación de decimal predefinidos sin elevar definidos anteriormente también están predefinidas.

12.12.5 Operadores booleanos de igualdad

Los operadores de igualdad booleanos predefinidos son:

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

El resultado de == es true si ambos x y y son true o si ambos x y y son false. De lo contrario, el resultado es false.

El resultado de != es false si ambos x y y son true o si ambos x y y son false. De lo contrario, el resultado es true. Cuando los operandos son de tipo bool, el operador != produce el mismo resultado que el operador ^.

Las formas elevadas (§12.4.8) de los operadores de igualdad booleanos predefinidos sin elevar definidos anteriormente también están predefinidas.

12.12.6 Operadores de comparación de enumeraciones

Cada tipo de enumeración proporciona implícitamente los siguientes operadores de comparación 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);

El resultado de evaluar x «op» y, donde x e y son expresiones de un tipo de enumeración E con un tipo subyacente U, y «op» es uno de los operadores de comparación, es exactamente el mismo que evaluar ((U)x) «op» ((U)y). En otras palabras, los operadores de comparación de tipo enumeración simplemente comparan los valores integrales subyacentes de los dos operandos.

Las formas elevadas (§12.4.8) de los operadores de comparación de enumeración predefinidos sin elevar definidos anteriormente también están predefinidas.

12.12.7 Operadores de igualdad de tipo de referencia

Cada tipo de clase C proporciona implícitamente los siguientes operadores de igualdad predefinidos de tipo referencia:

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

a menos que existan operadores de igualdad previamente definidos para C (por ejemplo, cuando C es string o System.Delegate).

Los operadores devuelven el resultado de comparar las dos referencias para igualdad o no igualdad. operator == devuelve true si y solo si x y y se refieren a la misma instancia o son ambos null, mientras operator != que devuelve true si y solo si operator == con los mismos operandos devolvería false.

Además de las reglas de aplicabilidad normales (sección 12.6.4.2), los operadores de igualdad de tipos de referencia predefinidos requieren una de las siguientes condiciones para ser aplicables:

  • Ambos operandos son un valor de un tipo conocido como reference_type o el literal null. Además, existe una conversión de identidad o de referencia explícita (§10.3.5) desde cualquiera de los operandos al tipo del otro operando.
  • Un operando es el literal null, y el otro operando es un valor de tipo T donde T es un type_parameter que no se sabe que es un tipo de valor, y no tiene la restricción de tipo de valor.
    • Si en tiempo de ejecución T es un tipo de valor no anulable, el resultado de == es false y el resultado de != es true.
    • Si en tiempo de ejecución T es un tipo de valor anulable, el resultado se calcula a partir de la propiedad del operando HasValue, como se describe en (sección 12.12.10).
    • Si en tiempo de ejecución T es un tipo de referencia, el resultado es true si el operando es nully false de lo contrario.

A menos que una de estas condiciones sea cierta, se produce un error de vinculación.

Nota: son notables las implicaciones de estas reglas:

  • Es un error durante el tiempo de enlace usar los operadores predefinidos de igualdad de tipos de referencia para comparar dos referencias que se sabe que son diferentes durante el tiempo de enlace. Por ejemplo, si los tipos en tiempo de enlace de los operandos son dos tipos de clase, y si ninguno deriva del otro, entonces sería imposible que los dos operandos hicieran referencia al mismo objeto. Por lo tanto, la operación se considera un error de vinculación.
  • Los operadores predefinidos de igualdad de tipos de referencia no permiten comparar operandos de tipo de valor (excepto cuando los parámetros de tipo se comparan con null, lo cual se maneja de manera especial).
  • Los operandos de los operadores predefinidos de igualdad de tipos de referencia nunca se encapsulan. No tendría sentido realizar tales operaciones de empaquetamiento, ya que las referencias a las nuevas instancias empaquetadas necesariamente diferirían de todas las demás referencias.

Para una operación de la forma x == y o x != y, si existe algún operador operator == o operator != definido por el usuario aplicable, las reglas de resolución de sobrecarga de operadores (sección 12.4.5) seleccionarán ese operador en lugar del operador de igualdad de tipo de referencia predefinido. Siempre es posible seleccionar el operador de igualdad de tipo de referencia predefinido mediante la conversión explícita de uno o ambos operandos al tipo object.

nota final

Ejemplo: En el siguiente ejemplo se verifica si un argumento de un parámetro de tipo no restringido es null.

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

La construcción x == null está permitida aunque T pueda representar un tipo de valor no anulable, y el resultado se define simplemente como false cuando T es un tipo de valor no anulable.

ejemplo final

Para una operación de la forma x == y o x != y, si existe cualquier operator == o operator != aplicable, las reglas de resolución de sobrecarga de operadores (sección 12.4.5) seleccionarán ese operador en lugar del operador de igualdad de tipo de referencia predefinido.

Nota: Siempre es posible seleccionar el operador de igualdad de tipo de referencia predefinido mediante la conversión explícita de ambos operandos al tipo object. nota final

Ejemplo: el ejemplo

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);
    }
}

genera el resultado

True
False
False
False

Las variables s y t se refieren a dos instancias de cadena distintas que contienen los mismos caracteres. La primera comparación resulta True porque se selecciona el operador predefinido de igualdad de cadenas (sección 12.12.8) cuando ambos operandos son del tipo string. Todas las comparaciones restantes producen False porque la sobrecarga de operator == en el tipo de string no es aplicable cuando alguno de los operandos tiene un tipo object de tiempo de enlace.

Tenga en cuenta que la técnica anterior no tiene sentido para los tipos de valor. En el ejemplo

class Test
{
    static void Main()
    {
        int i = 123;
        int j = 123;
        Console.WriteLine((object)i == (object)j);
    }
}

genera False porque las conversiones crean referencias a dos instancias independientes de valores encapsulados de int.

ejemplo final

12.12.8 Operadores de igualdad de cadenas

Los operadores de igualdad de cadena predefinidos son:

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

Dos valores string se consideran iguales cuando se cumple una de las siguientes condiciones:

  • Ambos valores son null.
  • Ambos valores no son referencias null a instancias de cadena que tienen longitudes idénticas y caracteres idénticos en cada posición de carácter.

Los operadores de igualdad de cadenas comparan valores de cadenas en lugar de referencias de cadenas. Cuando dos instancias de cadena separadas contienen exactamente la misma secuencia de caracteres, los valores de las cadenas son iguales, pero las referencias son diferentes.

Nota: Como se describe en la sección 12.12.7, los operadores de igualdad de tipo de referencia pueden utilizarse para comparar referencias de cadena en lugar de valores de cadena. nota final

12.12.9 Operadores de igualdad de delegados

Los operadores de igualdad de delegados predefinidos son:

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

Dos instancias de delegado se consideran iguales de la siguiente manera:

  • Si cualquiera de las instancias de delegado es null, son iguales y solo si ambas son null.
  • Si los delegados tienen tipos de tiempo de ejecución diferentes, nunca son iguales.
  • Si ambas instancias de delegado tienen una lista de invocación (sección 20.2), son iguales si y solo si sus listas de invocación tienen la misma longitud, y cada entrada de la lista de invocación de una es igual (tal y como se define a continuación) a la entrada correspondiente, en orden, de la lista de invocación de la otra.

Las reglas siguientes determinan la igualdad de las entradas en las listas de invocación:

  • Si dos entradas de la lista de invocación hacen referencia al mismo método estático, las entradas son iguales.
  • Si dos entradas de la lista de invocación se refieren al mismo método no estático en el mismo objeto de destino (como se define por los operadores de igualdad de referencia), las entradas son iguales.
  • Las entradas de la lista de invocación generadas a partir de la evaluación de funciones anónimas semánticamente idénticas (§12.19) con el mismo conjunto (posiblemente vacío) de instancias de variables externas capturadas se pueden ser iguales (pero no se requiere).

Si la resolución de sobrecarga del operador se resuelve en cualquier operador de igualdad de delegados y los tipos de tiempo de enlace de ambos operandos son tipos delegados, tal como se describe en §20 en lugar de System.Delegate, y no hay ninguna conversión de identidad entre los tipos de operando de tipo de enlace, se produce un error en tiempo de enlace.

Nota: Esta regla impide comparaciones que nunca pueden considerar valores nonull como iguales, ya que son referencias a instancias de diferentes tipos de delegados. nota final

12.12.10 Operadores de igualdad entre tipos de valor que aceptan valores NULL y el literal nulo

Los operadores == y != permiten que un operando sea un valor de un tipo de valor que acepta valores NULL y el otro sea el literal null, incluso si no existe ningún operador predefinido o definido por el usuario (en forma no elevada o elevada) para la operación.

Para una operación de uno de los formatos

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

donde x es una expresión de un tipo de valor anulable, si la resolución de sobrecarga del operador (§12.4.5) no encuentra un operador aplicable, el resultado se calcula en su lugar a partir de la propiedad HasValue de x. En concreto, las dos primeras formas se convierten en !x.HasValue y las dos últimas en x.HasValue.

12.12.11 Operadores de igualdad de tupla

Los operadores de igualdad de tupla se aplican en pares a los elementos de los operandos de tupla en orden léxico.

Si cada uno de los operandos x y y de un operador == o != se clasifica como una tupla o como un valor con un tipo de tupla (§8.3.11), el operador es un operador de igualdad para tuplas .

Si un operando e se clasifica como una tupla, los elementos e1...en serán los resultados de evaluar las expresiones de elemento de la expresión de tupla. De lo contrario, si e es un valor de un tipo de tupla, los elementos serán t.Item1...t.Itemn donde t es el resultado de evaluar e.

Los operandos x y y de un operador de igualdad de tupla deben tener la misma aridad; de lo contrario, se producirá un error en tiempo de compilación. Para cada par de elementos xi y yi, se aplicará el mismo operador de igualdad, y producirá un resultado de tipo bool, dynamic, un tipo que tenga una conversión implícita a bool o un tipo que defina los operadores true y false.

El operador de igualdad de tupla x == y se evalúa de la siguiente manera:

  • Se evalúa el operando del lado izquierdo x.
  • Se evalúa el operando del lado derecho, y.
  • Para cada par de elementos xi y yi en orden léxico:
    • Se evalúa el operador xi == yi y se obtiene un resultado de tipo bool de la siguiente manera:
      • Si el resultado de la comparación es bool, este es el resultado.
      • En caso contrario, si el resultado de la comparación es un dynamic, el operador false se invoca dinámicamente sobre él, y el valor bool resultante se niega con el operador lógico de negación (!).
      • En caso contrario, si el tipo de la comparación tiene una conversión implícita a bool, se aplica dicha conversión.
      • En caso contrario, si el tipo de la comparación tiene un operador false, se invoca dicho operador y el valor bool resultante se niega con el operador lógico de negación (!).
    • Si el bool resultante es false, no se realiza ninguna evaluación adicional y el resultado del operador de igualdad en tuplas es false.
  • Si todas las comparaciones de elementos arrojaron true, el resultado del operador de igualdad de la tupla es true.

El operador de igualdad de tupla x != y se evalúa de la siguiente manera:

  • Se evalúa el operando del lado izquierdo x.
  • Se evalúa el operando del lado derecho, y.
  • Para cada par de elementos xi y yi en orden léxico:
    • Se evalúa el operador xi != yi y se obtiene un resultado de tipo bool de la siguiente manera:
      • Si el resultado de la comparación es bool, este es el resultado.
      • Si el resultado de la comparación es dynamic, se invoca dinámicamente el operador true y el valor resultante bool es el resultado.
      • En caso contrario, si el tipo de la comparación tiene una conversión implícita a bool, se aplica dicha conversión.
      • De lo contrario, si el tipo de la comparación tiene un operador true, ese operador se invoca y el valor resultante bool es el resultado.
    • Si el bool resultante es true, no se realiza ninguna evaluación adicional y el resultado del operador de igualdad en tuplas es true.
  • Si todas las comparaciones de elementos arrojaron false, el resultado del operador de igualdad de la tupla es false.

12.12.12 El operador is

Existen dos formas de operador is. Uno de ellos es el operador is-type, que tiene un tipo en el lado derecho. El otro es el operador is-pattern, que tiene un patrón en el lado derecho.

12.12.12.1 Operador is-type

El operador is-type se usa para comprobar si el tipo en tiempo de ejecución de un objeto es compatible con un tipo determinado. La comprobación se realiza en tiempo de ejecución. El resultado de la operación E is T, donde E es una expresión y T es un tipo distinto de dynamic, es un valor booleano que indica si E no es nulo y puede convertirse correctamente al tipo T mediante una conversión de referencia, una conversión boxing, una conversión de desencapsulado, una conversión de ajuste o una conversión de desencapsulado.

La operación se evalúa del siguiente modo:

  1. Si E es una función anónima o un grupo de métodos, se produce un error en tiempo de compilación.
  2. Si E es el literal null, o si el valor de E es null, el resultado es false.
  3. De lo contrario:
  4. Sea R el tipo en tiempo de ejecución de E.
  5. Que D se derive de R de la siguiente manera:
  6. Si R es un tipo de valor que acepta valores nulos, D es el tipo subyacente de R.
  7. En caso contrario, D es R.
  8. El resultado depende de D y T como sigue:
  9. Si T es un tipo de referencia, el resultado es true si:
    • existe una conversión de identidad entre D y T,
    • D es un tipo de referencia y existe una conversión de referencia implícita de D a T, o
    • O bien: D es un tipo de valor y existe una conversión boxing de D a T.
      O bien: D es un tipo de valor y T es un tipo de interfaz implementado por D.
  10. Si T es un tipo de valor que acepta valores NULL, el resultado es true si D es el tipo subyacente de T.
  11. Si T es un tipo de valor no anulable, el resultado es true si D y T son del mismo tipo.
  12. De lo contrario, el resultado es false.

El operador is no tiene en cuenta las conversiones definidas por el usuario.

Nota: Como el operador is se evalúa en tiempo de ejecución, todos los argumentos de tipo han sido sustituidos y no hay tipos abiertos (sección 8.4.3) a considerar. nota final

Nota: El operador is puede entenderse en términos de tipos en tiempo de compilación y conversiones como sigue, donde C es el tipo en tiempo de compilación de E:

  • Si el tipo en tiempo de compilación de e es el mismo que el de T, o si existe una conversión de referencia implícita (§10.2.8), una conversión de caja (§10.2.9), una conversión de envoltura (§10.6) o una conversión de desenvoltura explícita (§10.6) del tipo en tiempo de compilación de E a T:
    • Si C es de un tipo de valor no nulo, el resultado de la operación es true.
    • En caso contrario, el resultado de la operación equivale a evaluar E != null.
  • De lo contrario, si existe una conversión de referencia explícita (§10.3.5) o si una conversión de unboxing (§10.3.7) existe de C a T, o si C o T es un tipo abierto (§8.4.3), se realizarán las comprobaciones en tiempo de ejecución como se han indicado anteriormente.
  • De lo contrario, no es posible realizar ninguna referencia, conversión boxing, ajuste o desajuste de E al tipo T y el resultado de la operación es false. Un compilador puede implementar optimizaciones basadas en el tipo en tiempo de compilación.

nota final

12.12.12.2 El operador is-pattern

El operador is-pattern se utiliza para comprobar si el valor calculado por una expresión coincide con un patrón dado (sección 11). La comprobación se realiza en tiempo de ejecución. El resultado del operador is-pattern es verdadero si el valor coincide con el patrón; en caso contrario, es falso.

Para una expresión de la forma E is P, donde E es una expresión relacional de tipo T y es un patrón P, es un error en tiempo de compilación si se cumple alguna de las siguientes condiciones:

  • E no designa un valor o no tiene un tipo.
  • El patrón P no es aplicable (sección 11.2) al tipo T.

12.12.13 El operador as

El operador as se usa para convertir explícitamente un valor en un tipo de referencia determinado o un tipo de valor anulable. A diferencia de la expresión cast, (§12.9.7) el operador as no genera nunca una excepción. En cambio, si la conversión indicada no es posible, el valor resultante es null.

En una operación de la forma E as T, E será una expresión y T será un tipo de referencia, un parámetro de tipo conocido por ser un tipo de referencia o un tipo de valor anulable. Además, al menos uno de los siguientes debe ser verdadero o, de lo contrario, habrá un error en tiempo de compilación.

  • Una conversión de identidad (§10.2.2), que admite valores NULL implícita (§10.2.6), referencia implícita (§10.2.8), boxing (§10.2.9), que admite valores NULL explícita (§10.3.4), referencia explícita (§10.3.5) o de ajuste (§8.3.12) existe de E a T.
  • El tipo de E o T es un tipo abierto.
  • E es el literal null.

Si el tipo en tiempo de compilación de E no es dynamic, la operación E as T produce el mismo resultado que

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

salvo que E solo se evalúa una vez. Se puede esperar que un compilador optimice E as T para realizar como máximo una comprobación de tipo en tiempo de ejecución en lugar de las dos comprobaciones de tipo en tiempo de ejecución que implica la expansión anterior.

Si el tipo de tiempo de compilación de E es dynamic, a diferencia del operador de conversión, el operador as no está enlazado dinámicamente (§12.3.3). Por lo tanto, la expansión en este caso es:

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

Tenga en cuenta que algunas conversiones, como las conversiones definidas por el usuario, no son posibles con el operador as y, en su lugar, deben realizarse mediante expresiones de casting.

Ejemplo: en el ejemplo

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

El parámetro de tipo T de G se conoce como un tipo de referencia porque tiene la restricción de clase. Sin embargo, el parámetro de tipo U de H no es así; por lo tanto, no se permite el uso del operador as en H.

ejemplo final

12.13 Operadores lógicos

12.13.1 General

Los operadores &, ^y | se denominan 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
    ;

Si un operando de un operador lógico tiene el tipo en tiempo de compilación dynamic, entonces la expresión está ligada dinámicamente (sección 12.3.3). En este caso, el tipo en tiempo de compilación de la expresión es dynamic, y la resolución descrita a continuación tendrá lugar en tiempo de ejecución utilizando el tipo en tiempo de ejecución de aquellos operandos que tengan el tipo en tiempo de compilación dynamic.

Para una operación de la forma x «op» y, donde "op" es uno de los operadores lógicos, se aplica la resolución de sobrecarga (sección 12.4.5) para seleccionar una implementación específica del operador. Los operandos se convierten a los tipos de parámetros del operador seleccionado, y el tipo del resultado es el tipo de retorno del operador.

Los operadores lógicos predefinidos se describen en las siguientes subcláusulas.

12.13.2 Operadores lógicos de enteros

Los operadores lógicos de enteros predefinidos son:

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

El operador & calcula el AND lógico por bits de los dos operandos, el operador | calcula el OR lógico por bits de los dos operandos y el operador ^ calcula el OR lógico exclusivo por bits de los dos operandos. No es posible que estas operaciones se desborden.

Las formas elevadas (§12.4.8) de los operadores lógicos de enteros predefinidos sin elevar definidos anteriormente también están predefinidas.

12.13.3 Operadores lógicos de enumeración

Cada tipo de enumeración E proporciona implícitamente los siguientes operadores lógicos predefinidos:

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

El resultado de evaluar x «op» y, donde x y y son expresiones de un tipo de enumeración E con un tipo subyacente U, y «op» es uno de los operadores lógicos, es exactamente el mismo que el de evaluar (E)((U)x «op» (U)y). En otras palabras, los operadores lógicos de tipo enumeración simplemente realizan la operación lógica sobre el tipo subyacente de los dos operandos.

Las formas elevadas (§12.4.8) de los operadores lógicos de enumeración predefinidos sin elevar definidos anteriormente también están predefinidas.

12.13.4 Operadores lógicos booleanos

Los operadores lógicos booleanos predefinidos son:

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

El resultado de x & y es true si tanto x como y son true. De lo contrario, el resultado es false.

El resultado de x | y es true si x es y o es true. De lo contrario, el resultado es false.

El resultado de x ^ y es true si x es true y y es false, o x es false y y es true. De lo contrario, el resultado es false. Cuando los operandos son de tipo bool, el operador ^ calcula el mismo resultado que el operador !=.

12.13.5 Booleano de tipo NULL & y operadores |

El tipo booleano anulable bool? puede representar tres valores, true, false y null.

Al igual que los demás operadores binarios, también están predefinidas las formas levantadas de los operadores lógicos & y | (sección 12.13.4):

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

La semántica de los operadores levantados & y | se define en la tabla siguiente.

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: El tipo bool? es conceptualmente similar al tipo de tres valores utilizado para las expresiones booleanas en SQL. La tabla anterior sigue la misma semántica que SQL, sin embargo, aplicar las reglas de §12.4.8 a los operadores de & y | no seguiría la misma semántica. Las reglas de §12.4.8 ya proporcionan una semántica similar a la de SQL para el operador elevado de ^. nota final

12.14 Operadores lógicos condicionales

12.14.1 General

Los operadores && y || se denominan operadores lógicos condicionales. También se denominan operadores lógicos de "cortocircuito".

conditional_and_expression
    : inclusive_or_expression
    | conditional_and_expression '&&' inclusive_or_expression
    ;

conditional_or_expression
    : conditional_and_expression
    | conditional_or_expression '||' conditional_and_expression
    ;

Los operadores && y || son versiones condicionales de los operadores & y |:

  • La operación x && y corresponde a la operación x & y, excepto que y será evaluado solo si x no es false.
  • La operación x || y corresponde a la operación x | y, excepto que y será evaluado solo si x no es true.

Nota: La razón por la que el cortocircuitado utiliza las condiciones "not true" y "not false" es para permitir que los operadores condicionales definidos por el usuario establezcan cuándo se aplica el cortocircuitado. Los tipos definidos por el usuario podrían estar en un estado en el que operator true devuelve false y operator false devuelve false. En esos casos, ni && ni || cortocircuitarían. nota final

Si un operando de un operador lógico condicional tiene el tipo en tiempo de compilación dynamic, entonces la expresión está ligada dinámicamente (sección 12.3.3). En este caso, el tipo en tiempo de compilación de la expresión es dynamic, y la resolución descrita a continuación tendrá lugar en tiempo de ejecución utilizando el tipo en tiempo de ejecución de aquellos operandos que tengan el tipo en tiempo de compilación dynamic.

Una operación de la forma x && y o x || y se procesa aplicando la resolución de sobrecarga (sección 12.4.5) como si la operación estuviera escrita x & y o x | y. A continuación,

  • Si la resolución de sobrecarga no encuentra un único operador mejor, o si la resolución de sobrecarga selecciona uno de los operadores lógicos enteros predefinidos o de los operadores lógicos booleanos anulables (sección 12.13.5), se produce un error de vinculación.
  • De lo contrario, si el operador seleccionado es uno de los operadores lógicos booleanos predefinidos (sección 12.13.4), la operación se procesa como se describe en la sección 12.14.2.
  • En caso contrario, si el operador seleccionado es un operador definido por el usuario, la operación se procesa como se describe en la sección 12.14.3.

No es posible sobrecargar directamente los operadores lógicos condicionales. Sin embargo, dado que los operadores lógicos condicionales se evalúan en términos de los operadores lógicos regulares, las sobrecargas de los operadores lógicos regulares se consideran también, con ciertas restricciones, sobrecargas de los operadores lógicos condicionales. Esto se describe con más detalle en la sección 12.14.3.

12.14.2 Operadores lógicos condicionales booleanos

Cuando los operandos de && o || son del tipo bool, o cuando los operandos son de tipos que no definen un operator & o operator | aplicable, pero sí definen conversiones implícitas a bool, la operación se procesa como sigue:

  • La operación x && y se evalúa como x ? y : false. En otras palabras, primero se evalúa x y se convierte al tipo bool. Entonces, si x es true, y se evalúa y se convierte al tipo bool, y este se convierte en el resultado de la operación. En caso contrario, el resultado de la operación es false.
  • La operación x || y se evalúa como x ? true : y. En otras palabras, primero se evalúa x y se convierte al tipo bool. Entonces, si x es true, el resultado de la operación es true. En caso contrario, y se evalúa y se convierte a tipo bool, y este se convierte en el resultado de la operación.

12.14.3 Operadores lógicos condicionales definidos por el usuario

Cuando los operandos de && o || son de tipos que declaran un operator & o operator |definido por el usuario aplicable, ambos de los siguientes deberán ser verdaderos, donde T es el tipo en el que se declara el operador seleccionado:

  • El tipo de valor devuelto y el tipo de cada parámetro del operador seleccionado serán T. En otras palabras, el operador calculará el AND lógico o el OR lógico de dos operandos de tipo T, y devolverá un resultado de tipo T.
  • T debe contener declaraciones de operator true y operator false.

Se produce un error en tiempo de vinculación si no se cumple alguno de estos requisitos. En caso contrario, la operación && o || se evalúa combinando el operador definido por el usuario operator true o operator false con el operador definido por el usuario seleccionado:

  • La operación x && y se evalúa como T.false(x) ? x : T.&(x, y), donde T.false(x) es una invocación del operator false declarado en T, y T.&(x, y) es una invocación del operator &seleccionado. En otras palabras, primero se evalúa x y luego se invoca operator false sobre el resultado para determinar si x es definitivamente falso. Entonces, si x es definitivamente falso, el resultado de la operación es el valor calculado previamente para x. En caso contrario, y se evalúa y se invoca un operator & seleccionado sobre el valor calculado previamente para x y el valor calculado para y para producir el resultado de la operación.
  • La operación x || y se evalúa como T.true(x) ? x : T.|(x, y), donde T.true(x) es una invocación del operator true declarado en T, y T.|(x, y) es una invocación del operator |seleccionado. En otras palabras, primero se evalúa x y se invoca operator true sobre el resultado para determinar si x es definitivamente verdadero. Entonces, si x es definitivamente cierto, el resultado de la operación es el valor previamente calculado para x. En caso contrario, y se evalúa y se invoca un operator | seleccionado sobre el valor calculado previamente para x y el valor calculado para y para producir el resultado de la operación.

En cualquiera de estas operaciones, la expresión dada por x solo se evalúa una vez, y la expresión dada por y no se evalúa o se evalúa exactamente una vez.

12.15 El operador de fusión de NULL

Al operador ?? se le llama el operador de uso combinado de NULL.

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

En una expresión de combinado de NULL de la forma a ?? b, si a no es null, el resultado es a; en caso contrario, el resultado es b. La operación solo evalúa b si a es null.

El operador de coalecencia nula es asociativo hacia la derecha, lo que significa que las operaciones se agrupan de derecha a izquierda.

Ejemplo: una expresión de la forma a ?? b ?? c se evalúa como a ?? (b ?? c). En términos generales, una expresión de la forma E1 ?? E2 ?? ... ?? EN devuelve el primero de los operandos que no es null o null si todos los operandos son null. ejemplo final

El tipo de la expresión a ?? b depende de las conversiones implícitas disponibles en los operandos. En orden de preferencia, el tipo de a ?? b es A₀, A o B, donde A es el tipo de a (siempre que a tenga un tipo), B es el tipo de b (siempre que b tenga un tipo), y A₀ es el tipo subyacente de A si A es un tipo de valor anulable, o A en caso contrario. En concreto, a ?? b se procesa como sigue:

  • Si A existe y no es un tipo de valor que acepta valores NULL o un tipo de referencia, se produce un error en tiempo de compilación.
  • En caso contrario, si A existe y b es una expresión dinámica, el tipo resultante es dynamic. En tiempo de ejecución, primero se evalúa a. Si a no es null, a se convierte a dynamic, y este se convierte en el resultado. De lo contrario, se evalúa b y este se convierte en el resultado.
  • En caso contrario, si A existe y es un tipo de valor anulable y existe una conversión implícita de b a A₀, el tipo de resultado es A₀. En tiempo de ejecución, primero se evalúa a. Si a no es null, a se desencapsula en el tipo A₀ y se convierte en el resultado. De lo contrario, b se evalúa y se convierte al tipo A₀, y esto se convierte en el resultado.
  • De lo contrario, si A existe y existe una conversión implícita de b a A, el tipo resultante es A. En tiempo de ejecución, primero se evalúa a. Si a no es null, a se convierte en el resultado. De lo contrario, b se evalúa y se convierte al tipo A, y esto se convierte en el resultado.
  • De lo contrario, si A existe y es un tipo de valor anulable, b tiene un tipo B y existe una conversión implícita de A₀ a B, el tipo de resultado es B. En tiempo de ejecución, primero se evalúa a. Si a no es null, a se desencapsula en el tipo A₀ y se convierte en el tipo B, y esto se convierte en el resultado. De lo contrario, b se evalúa y se convierte en el resultado.
  • De lo contrario, si b tiene un tipo B y existe una conversión implícita de a a B, el tipo resultante es B. En tiempo de ejecución, primero se evalúa a. Si a no es null, a se convierte en el tipo B, y este se convierte en el resultado. De lo contrario, b se evalúa y se convierte en el resultado.

De lo contrario, a y b son incompatibles y se produce un error en tiempo de compilación.

12.16 Operador de expresión throw

throw_expression
    : 'throw' null_coalescing_expression
    ;

Una throw_expression lanza el valor producido al evaluar la null_coalescing_expression. La expresión se convertirá implícitamente en System.Exception, y el resultado de evaluar la expresión se convierte en System.Exception antes de lanzarse. El comportamiento en tiempo de ejecución de la evaluación de una expresión throw es el mismo que está especificado para una declaración throw (§13.10.6).

Una throw_expression no tiene ningún tipo. Una throw_expression se puede convertir a cualquier tipo mediante una conversión implícita de throw.

Una expresión throw solo se producirá en los siguientes contextos sintácticos:

  • De lo contrario, se evalúa y se convierte en el resultado (?:).
  • Como segundo operando de un operador de fusión null (??).
  • Como cuerpo de una expresión o miembro lambda.

12.17 Expresiones de declaración

Una expresión de declaración declara una variable local.

declaration_expression
    : local_variable_type identifier
    ;

local_variable_type
    : type
    | 'var'
    ;

El simple_name_ también se considera una expresión de declaración si la búsqueda simple de nombres no encontró una declaración asociada (sección 12.8.4). Cuando se usa como expresión de declaración, _ se denomina descarte simple. Es semánticamente equivalente a var _, pero se permite en más lugares.

Una expresión de declaración solo puede aparecer en los siguientes contextos sintácticos:

  • Como outargument_value en una argument_list.
  • Como simple descarte _ que comprende el lado izquierdo de una asignación simple (§12.21.2).
  • Como un tuple_element en una o varias tuple_expressions anidadas recursivamente, la más externa de las cuales compone el lado izquierdo de una asignación deconstruida. Una expresión de deconstrucción produce expresiones de declaración en esta posición, aunque estas no estén presentes de manera sintáctica.

Nota: esto significa que una expresión de declaración no se puede ir entre paréntesis. nota final

Es un error que una variable de tipo implícito declarada con una expresión de declaración sea referenciada dentro de la lista de argumentos donde se declara.

Es un error que se haga referencia a una variable declarada con una declaration_expression dentro de la asignación deconstrucción donde se produce.

Una expresión de declaración que es un descarte simple o donde el local_variable_type es el identificador var se clasifica como una variable con tipo implícito. La expresión no tiene tipo, y el tipo de la variable local se infiere basándose en el contexto sintáctico como sigue:

  • En una argument_list, el tipo inferido de la variable es el tipo declarado del parámetro correspondiente.
  • Como en el lado izquierdo de una asignación simple, el tipo inferido de la variable es el tipo del lado derecho de la asignación.
  • En una tuple_expression del lado izquierdo de una asignación simple, el tipo inferido de la variable es el tipo del elemento de tupla correspondiente en el lado derecho de la asignación (después de la deconstrucción).

De lo contrario, la expresión de declaración se clasifica como una variable con tipo explícito y el tipo de la expresión, así como la variable declarada, será dada por el local_variable_type.

Una expresión de declaración con el identificador _ es un descarte (sección 9.2.9.2), y no introduce un nombre para la variable. Una expresión de declaración con un identificador distinto de _ introduce ese nombre en el espacio de declaración de variables locales más cercano (§7.3).

Ejemplo:

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 _);

La declaración de s1 muestra expresiones de declaración explícita e implícitamente con tipo. El tipo inferido de b1 es bool porque ese es el tipo del parámetro de salida correspondiente en M1. El siguiente WriteLine puede acceder a i1 y b1, que se han introducido en el ámbito contenedor.

La declaración de s2 muestra un intento de usar i2 en la llamada anidada a M, que no se permite dado que la referencia ocurre dentro de la lista de argumentos donde se declaró i2. Por otro lado, la referencia a b2 en el argumento final está permitida, porque se produce después del final de la lista de argumentos anidada donde b2 se declaró.

La declaración de s3 muestra el uso de expresiones de declaración implícita y explícitamente con tipo que se descartan. Debido a que los descartes no declaran una variable con nombre, las múltiples ocurrencias del identificador _ están permitidas.

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

En este ejemplo se muestra el uso de expresiones de declaración implícita y explícitamente con tipo para variables y descartes en una asignación de deconstrucción. El simple_name_ es equivalente a var _ cuando no se encuentra la declaración de _.

void M1(out int i) { ... }

void M2(string _)
{
    M1(out _);      // Error: `_` is a string
    M1(out var _);
}

Este ejemplo muestra el uso de var _ para proporcionar un descarte implícitamente tipado cuando _ no está disponible, porque designa una variable en el ámbito envolvente.

ejemplo final

12.18 Operador condicional

Este operador ?: se denomina operador condicional. A veces también se denomina operador ternario.

conditional_expression
    : null_coalescing_expression
    | null_coalescing_expression '?' expression ':' expression
    | null_coalescing_expression '?' 'ref' variable_reference ':'
      'ref' variable_reference
    ;

No se permite una expresión de lanzamiento (sección 12.16) en un operador condicional si ref está presente.

Una expresión condicional de la forma b ? x : y evalúa primero la condición b. A continuación, si b es true, x se evalúa y se convierte en el resultado de la operación. De lo contrario, y se evalúa y se convierte en el resultado de la operación. Una expresión condicional nunca evalúa tanto x y y.

El operador condicional es de asociación a la derecha, lo que significa que las operaciones se agrupan de derecha a izquierda.

Ejemplo: una expresión de la forma a ? b : c ? d : e se evalúa como a ? b : (c ? d : e). ejemplo final

El primer operando del operador ?: debe ser una expresión que pueda convertirse implícitamente en bool, o una expresión de un tipo que implemente operator true. Si no se cumple ninguno de estos requisitos, se produce un error de compilación.

Si ref está presente:

  • Debe existir una conversión de identidad entre los tipos de las dos variable_reference y el tipo del resultado puede ser cualquiera de los dos. Si cualquiera de los dos tipos es dynamic, se prefiere la inferencia de tipos dynamic (sección 8.7). Si cualquiera de los tipos es un tipo de tupla (§8.3.11), la inferencia de tipos incluye los nombres de los elementos cuando los nombres de los elementos en la misma posición ordinal son iguales en ambas tuplas.
  • El resultado es una referencia de variable, que es escribible si ambas variable_references son escribibles.

Nota: cuando ref está presente, la conditional_expression devuelve una referencia de variable, que se puede asignar a una variable de referencia mediante el operador = ref o pasar como parámetro de referencia/entrada/salida. nota final

Si ref no está presente, el segundo y tercer operandos, x y y, del operador ?: controlan el tipo de la expresión condicional:

  • Si x tiene tipo X y y tiene tipo Y,
    • Si existe una conversión de identidad entre X y Y, entonces el resultado es el mejor tipo común de un conjunto de expresiones (sección 12.6.3.15). Si cualquiera de los dos tipos es dynamic, se prefiere la inferencia de tipos dynamic (sección 8.7). Si cualquiera de los tipos es un tipo de tupla (§8.3.11), la inferencia de tipos incluye los nombres de los elementos cuando los nombres de los elementos en la misma posición ordinal son iguales en ambas tuplas.
    • En caso contrario, si existe una conversión implícita (sección 10.2) de X a Y, pero no de Y a X, entonces Y es el tipo de la expresión condicional.
    • En caso contrario, si existe una conversión implícita de enumeración (sección 10.2.4) de X a Y, entonces Y es el tipo de la expresión condicional.
    • En caso contrario, si existe una conversión implícita de enumeración (sección 10.2.4) de Y a X, entonces X es el tipo de la expresión condicional.
    • En caso contrario, si existe una conversión implícita (sección 10.2) de Y a X, pero no de X a Y, entonces X es el tipo de la expresión condicional.
    • En caso contrario, no se puede determinar el tipo de la expresión y se produce un error de compilación.
  • Si solo uno de x y y tiene un tipo, y ambos x y y son implícitamente convertibles a ese tipo, entonces ese es el tipo de la expresión condicional.
  • En caso contrario, no se puede determinar el tipo de la expresión y se produce un error de compilación.

El procesamiento en tiempo de ejecución de una expresión condicional ref de la forma b ? ref x : ref y consiste en los siguientes pasos:

  • Primero, b se evalúa, y se determina el valor bool de b:
    • Si existe una conversión implícita del tipo de b a bool, entonces se realiza esta conversión implícita para producir un valor bool.
    • De lo contrario, el operator true definido por el tipo de b se invoca para producir un valor de bool.
  • Si el valor bool producido por el paso anterior es true, entonces x se evalúa y la referencia variable resultante se convierte en el resultado de la expresión condicional.
  • De lo contrario, se evalúa y y la referencia a la variable resultante se convierte en el resultado de la expresión condicional.

El procesamiento en tiempo de ejecución de una expresión condicional de la forma b ? x : y consiste en los siguientes pasos:

  • Primero, b se evalúa, y se determina el valor bool de b:
    • Si existe una conversión implícita del tipo de b a bool, entonces se realiza esta conversión implícita para producir un valor bool.
    • De lo contrario, el operator true definido por el tipo de b se invoca para producir un valor de bool.
  • Si el valor bool producido por el paso anterior es true, entonces x se evalúa y se convierte al tipo de la expresión condicional, y este se convierte en el resultado de la expresión condicional.
  • En caso contrario, y se evalúa y se convierte al tipo de la expresión condicional, y este se convierte en el resultado de la expresión condicional.

12.19 Expresiones de función anónimas

12.19.1 General

Una función anónima es una expresión que representa una definición de método "en línea". Una función anónima no tiene un valor o tipo en sí misma, pero es convertible a un tipo de delegado o de árbol de expresiones compatible. La evaluación de una conversión de función anónima depende del tipo de destino de la conversión: Si es un tipo delegado, la conversión se evalúa como un valor delegado que hace referencia al método que define la función anónima. Si es de tipo árbol de expresiones, la conversión se evalúa como un árbol de expresiones que representa la estructura del método como una estructura de objetos.

Nota: Por razones históricas, existen dos tipos sintácticos de funciones anónimas, a saber, lambda_expression y anonymous_method_expression. Para casi todos los propósitos, las lambda_expressions son más concisas y expresivas que las anonymous_method_expressions, que permanecen en el lenguaje para la compatibilidad con versiones anteriores. 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
    ;

Al reconocer un anonymous_function_body, si tanto la null_conditional_invocation_expression como la expresión son aplicables, se elegirá la primera.

Nota: El solapamiento de, y la prioridad entre, alternativas aquí es solo por conveniencia descriptiva; las reglas gramaticales podrían ser elaboradas para eliminar el solapamiento. ANTLR, y otros sistemas gramaticales, adoptan la misma conveniencia y así anonymous_function_body tiene la semántica especificada automáticamente. nota final

Nota: cuando se trata como una expresión , una forma sintáctica como x?.M() sería un error si el tipo de resultado de M es void (§12.8.13). Pero cuando se trata como una null_conditional_invocation_expression, se permite que el tipo de resultado pueda ser void. nota final

Ejemplo: el tipo de resultado de List<T>.Reverse es void. En el código siguiente, el cuerpo de la expresión anónima es una null_conditional_invocation_expression, por lo que no es un error.

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

ejemplo final

El operador => tiene la misma prioridad que la asignación (=) y es asociativo a la derecha.

Una función anónima con el modificador async es una función asíncrona y sigue las reglas descritas en la sección 15.15.

Los parámetros de una función anónima en forma de lambda_expression pueden estar tipados explícita o implícitamente. En una lista de parámetros explícitamente tipada, el tipo de cada parámetro se indica explícitamente. En una lista de parámetros de tipado implícito, los tipos de los parámetros se deducen del contexto en el que se produce la función anónima; en concreto, cuando la función anónima se convierte a un tipo de delegado compatible o a un tipo de árbol de expresión, ese tipo proporciona los tipos de los parámetros (sección 10.7).

En una lambda_expression con un único parámetro de tipo implícito, pueden omitirse los paréntesis de la lista de parámetros. En otras palabras, una función anónima de la forma

( «param» ) => «expr»

puede abreviarse como

«param» => «expr»

La lista de parámetros de una función anónima en forma de anonymous_method_expression es opcional. Si se da, los parámetros se tipificarán explícitamente. Si no, la función anónima es convertible en un delegado con cualquier lista de parámetros que no contenga parámetros de salida.

Un cuerpo block de una función anónima siempre es accesible (§13.2).

Ejemplos: A continuación se muestran algunos ejemplos de funciones 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

ejemplo final

El comportamiento de las lambda_expression y las anonymous_method_expression es el mismo salvo en los siguientes puntos:

  • anonymous_method_expressions permiten que la lista de parámetros se omita por completo, lo que produce la convertibilidad en tipos delegados de cualquier lista de parámetros de valor.
  • lambda_expressions permiten que los tipos de parámetro se omitan e infieran, mientras que anonymous_method_expressions requieren que se indiquen explícitamente los tipos de parámetros.
  • El cuerpo de un lambda_expression puede ser una expresión o un bloque, mientras que el cuerpo de un anonymous_method_expression será un bloque.
  • Solo las lambda_expressions tienen conversiones a tipos de árbol de expresión compatibles (§8.6).

12.19.2 Firmas de función anónima

La anonymous_function_signature de una función anónima define los nombres y, opcionalmente, los tipos de los parámetros de la función anónima. El ámbito de los parámetros de la función anónima es el anonymous_function_body (sección 7.7). Junto con la lista de parámetros (si se da), el cuerpo del método anónimo constituye un espacio de declaración (sección 7.3). Por lo tanto, es un error de tiempo de compilación que el nombre de un parámetro de la función anónima coincida con el nombre de una variable local, una constante local o un parámetro que esté en el ámbito de anonymous_method_expression o lambda_expression.

Si una función anónima tiene una explicit_anonymous_function_signature, el conjunto de tipos de delegado y tipos de árbol de expresión compatibles se restringe a los que tienen los mismos tipos de parámetros y modificadores en el mismo orden (sección 10.7). A diferencia de las conversiones de grupo de métodos (§10.8), no se admite la contravariante de los tipos de parámetros de funciones anónimas. Si una función anónima no tiene una anonymous_function_signature, el conjunto de tipos de delegado y tipos de árbol de expresión compatibles se restringe a los que no tienen parámetros de salida.

Tenga en cuenta que un anonymous_function_signature no puede incluir atributos ni una matriz de parámetros. No obstante, una anonymous_function_signature puede ser compatible con un tipo de delegado cuya lista de parámetros contenga una matriz de parámetros.

Observe también que la conversión a un tipo de árbol de expresión, incluso si es compatible, puede fallar en tiempo de compilación (sección 8.6).

12.19.3 Cuerpos de función anónimos

El cuerpo (expresión o bloque) de una función anónima está sujeto a las siguientes reglas:

  • Si la función anónima incluye una firma, los parámetros especificados en la firma están disponibles en el cuerpo. Si la función anónima no tiene firma, puede convertirse en un tipo delegado o un tipo expresión con parámetros (sección 10.7), pero no se puede acceder a los parámetros en el cuerpo.
  • Salvo los parámetros pasados por referencia especificados en la firma (si los hay) de la función anónima más cercana, es un error de compilación que el cuerpo acceda a un parámetro pasado por referencia.
  • Salvo para los parámetros especificados en la declaración (si existe) de la función anónima de cierre más cercana, es un error en tiempo de compilación que el cuerpo acceda a un parámetro de tipo ref struct.
  • Cuando el tipo de this es un tipo struct, es un error de compilación que el cuerpo acceda a this. Esto es así tanto si el acceso es explícito (como en this.x) como implícito (como en x donde x un miembro de instancia de la estructura). Esta regla simplemente prohíbe tal acceso y no afecta a si la búsqueda de miembros resulta en un miembro de la estructura.
  • El cuerpo tiene acceso a las variables externas (sección 12.19.6) de la función anónima. El acceso de una variable externa hará referencia a la instancia de la variable que está activa en el momento en que se evalúa la lambda_expression o anonymous_method_expression (§12.19.7).
  • Es un error en tiempo de compilación que el cuerpo contenga una instrucción goto, una instrucción break o una instrucción continue cuyo destino esté fuera del cuerpo o dentro del cuerpo de una función anónima contenida.
  • Una instrucción return en el cuerpo devuelve el control de una invocación de la función anónima más cercana que la envuelve, no del miembro de la función contenedora.

No se especifica explícitamente si hay alguna forma de ejecutar el bloque de una función anónima que no sea a través de la evaluación e invocación de la lambda_expression o anonymous_method_expression. En particular, un compilador puede elegir implementar una función anónima sintetizando uno o más métodos o tipos con nombre. Los nombres de tales elementos sintetizados deberán tener una forma reservada para uso del compilador (sección 6.4.3).

12.19.4 Resolución de sobrecarga

Las funciones anónimas de una lista de argumentos participan en la inferencia de tipos y en la resolución de sobrecargas. Consulte las reglas exactas en la sección 12.6.3 y sección 12.6.4.

Ejemplo: El siguiente ejemplo ilustra el efecto de las funciones anónimas en la resolución de sobrecargas.

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

La clase ItemList<T> tiene dos métodos Sum. Cada uno toma un argumento selector, que extrae el valor para sumar de un elemento de la lista. El valor extraído puede ser un int o un double y la suma resultante es igualmente un int o un double.

Los métodos Sum podrían usarse, por ejemplo, para calcular sumas de una lista de líneas detalladas en un 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( ... )
    {
        ...
    }
}

En la primera invocación de orderDetails.Sum, ambos métodos Sum son aplicables porque la función anónima d => d.UnitCount es compatible con ambos Func<Detail,int> y Func<Detail,double>. Sin embargo, la resolución de sobrecarga elige el primer método de Sum porque la conversión a Func<Detail,int> es mejor que la conversión a Func<Detail,double>.

En la segunda invocación de orderDetails.Sum, solo es aplicable el segundo método Sum porque la función anónima d => d.UnitPrice * d.UnitCount produce un valor de tipo double. Así, la resolución de sobrecargas elige el segundo método Sum para esa invocación.

ejemplo final

12.19.5 Funciones anónimas y enlace dinámico

Una función anónima no puede ser receptora, argumento u operando de una operación ligada dinámicamente.

12.19.6 Variables externas

12.19.6.1 General

Cualquier variable local, parámetro de valor o matriz de parámetros cuyo ámbito incluya la lambda_expression o anonymous_method_expression se denomina variable externa de la función anónima. En un miembro de función de instancia de una clase, este valor se considera un parámetro de valor y es una variable externa de cualquier función anónima contenida dentro del miembro de función.

12.19.6.2 Variables externas capturadas

Cuando una función anónima hace referencia a una variable externa, se dice que la variable externa es capturada por la función anónima. Normalmente, el tiempo de vida de una variable local se limita a la ejecución del bloque o sentencia a la que está asociada (sección 9.2.9.1). Sin embargo, la duración de una variable externa capturada se extiende al menos hasta que el delegado o el árbol de expresiones creado a partir de la función anónima sea elegible para la recolección de elementos no utilizados.

Ejemplo: en el ejemplo

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());
    }
}

la función anónima captura la variable local x y la duración de x se extiende al menos hasta que el delegado devuelto de F sea apto para la recolección de elementos no utilizados. Dado que cada invocación de la función anónima opera sobre la misma instancia de x, la salida del ejemplo es:

1
2
3

ejemplo final

Cuando una función anónima captura una variable local o un parámetro de valor, la variable local o el parámetro deja de considerarse una variable fija (sección 23.4) y pasa a considerarse una variable móvil. Sin embargo, las variables externas capturadas no pueden utilizarse en una instrucción fixed (sección 23.7), por lo que no puede tomarse la dirección de una variable externa capturada.

Nota: A diferencia de una variable no capturada, una variable local capturada puede estar expuesta simultáneamente a varios hilos de ejecución. nota final

12.19.6.3 Creación de instancias de variables locales

Una variable local se considera instanciada cuando la ejecución entra en el ámbito de la variable.

Ejemplo: Por ejemplo, cuando el siguiente método es invocado, la variable local x es instanciada e inicializada tres veces (una por cada iteración del bucle).

static void F()
{
    for (int i = 0; i < 3; i++)
    {
        int x = i * 2 + 1;
        ...
    }
}

Sin embargo, mover la declaración de x fuera del bucle resulta en una única instanciación de x.

static void F()
{
    int x;
    for (int i = 0; i < 3; i++)
    {
        x = i * 2 + 1;
        ...
    }
}

ejemplo final

Cuando no se captura, no hay forma de observar exactamente con qué frecuencia se instancia una variable local, ya que los tiempos de vida de las instancias son disjuntos, es posible que cada instanciación utilice simplemente la misma ubicación de almacenamiento. Sin embargo, cuando una función anónima captura una variable local, los efectos de la instanciación se hacen evidentes.

Ejemplo: el ejemplo

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();
        }
    }
}

genera el resultado:

1
3
5

Sin embargo, cuando la declaración de x se coloca fuera del bucle:

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();
       }
   }
}

la salida es:

5
5
5

Nótese que un compilador puede (pero no está obligado) a optimizar las tres instancias en una sola instancia de delegado (sección 10.7.2).

ejemplo final

Si un bucle for declara una variable de iteración, dicha variable se considera declarada fuera del bucle.

Ejemplo: Así, si se cambia el ejemplo para capturar la propia variable de iteración:

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();
       }
   }
}

solo se captura una instancia de la variable de iteración, que produce la salida:

3
3
3

ejemplo final

Es posible que los delegados de funciones anónimas compartan algunas variables capturadas pero tengan instancias separadas de otras.

Ejemplo: por ejemplo, si se cambia F a

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

los tres delegados capturan la misma instancia de x pero separan instancias de y y la salida es:

1 1
2 1
3 1

ejemplo final

Funciones anónimas separadas pueden capturar la misma instancia de una variable externa.

Ejemplo: En el ejemplo :

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());
    }
}

las dos funciones anónimas capturan la misma instancia de la variable local x, y por lo tanto pueden "comunicarse" a través de esa variable. La salida del ejemplo es:

5
10

ejemplo final

12.19.7 Evaluación de expresiones de funciones anónimas

Una función anónima F siempre se convertirá a un tipo de delegado D o a un tipo de árbol de expresión E, ya sea directamente o mediante la ejecución de una expresión de creación de delegado new D(F). Esta conversión determina el resultado de la función anónima, como se describe en la sección 10.7.

12.19.8 Ejemplo de implementación

Esta subcláusula es informativa.

Esta subcláusula describe una posible implementación de conversiones de funciones anónimas en términos de otras construcciones de C#. La implementación aquí descrita se basa en los mismos principios utilizados por un compilador comercial de C#, pero no es en absoluto una implementación obligatoria, ni la única posible. Solo menciona brevemente las conversiones a árboles de expresión, ya que su semántica exacta queda fuera del ámbito de esta especificación.

El resto de esta subcláusula ofrece varios ejemplos de código que contiene funciones anónimas con diferentes características. Para cada ejemplo, se proporciona una traducción correspondiente a código que solo utiliza otras construcciones de C#. En los ejemplos, se asume que el identificador D representa el siguiente tipo de delegado:

public delegate void D();

La forma más simple de una función anónima es aquella que no captura variables externas:

delegate void D();

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

Esto se puede traducir a una instancia de delegado que hace referencia a un método estático generado por el compilador en el que se coloca el código de la función anónima:

delegate void D();

class Test
{
    static void F()
    {
        D d = new D(__Method1);
    }

    static void __Method1()
    {
        Console.WriteLine("test");
    }
}

En el siguiente ejemplo, la función anónima hace referencia a miembros de instancia de this:

delegate void D();

class Test
{
    int x;

    void F()
    {
        D d = () => Console.WriteLine(x);
    }
}

Esto se puede traducir a un método de instancia generado por el compilador que contiene el código de la función anónima:

delegate void D();

class Test
{
   int x;

   void F()
   {
       D d = new D(__Method1);
   }

   void __Method1()
   {
       Console.WriteLine(x);
   }
}

En este ejemplo, la función anónima captura una variable local:

delegate void D();

class Test
{
    void F()
    {
        int y = 123;
        D d = () => Console.WriteLine(y);
    }
}

El tiempo de vida de la variable local debe extenderse al menos al tiempo de vida del delegado de la función anónima. Esto se puede conseguir "elevando" la variable local a un campo de una clase generada por el compilador. La creación de instancias de la variable local (§12.19.6.3) corresponde a la creación de una instancia de la clase generada por el compilador y el acceso a la variable local corresponde al acceso a un campo en la instancia de la clase generada por el compilador. Además, la función anónima se convierte en un método de instancia de la clase generada por el 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);
        }
    }
}

Por último, la siguiente función anónima captura this además dos variables locales con tiempos 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);
       }
   }
}

En este caso, se crea una clase generada por el compilador para cada bloque en el que se capturan variables locales, de forma que las variables locales de los distintos bloques puedan tener tiempos de vida independientes. Una instancia de __Locals2, la clase generada por el compilador para el bloque interno, contiene la variable local z y un campo que hace referencia a una instancia de __Locals1. Una instancia de __Locals1, la clase generada por el compilador para el bloque externo, contiene la variable local y y un campo que hace referencia a this del miembro de función envolvente. Con estas estructuras de datos, es posible llegar a todas las variables externas capturadas a través de una instancia de __Local2, y el código de la función anónima puede implementarse así como un método de instancia de esa clase.

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);
        }
    }
}

La misma técnica aplicada aquí para capturar variables locales puede utilizarse también al convertir funciones anónimas en árboles de expresión: las referencias a los objetos generados por el compilador pueden almacenarse en el árbol de expresión, y el acceso a las variables locales puede representarse como accesos a campos de estos objetos. La ventaja de este enfoque es que permite compartir las variables locales "levantadas" entre delegados y árboles de expresión.

Fin del texto informativo.

12.20 Expresiones de consulta

12.20.1 General

Las expresiones de consulta proporcionan una sintaxis integrada en el lenguaje para consultas que es similar a los lenguajes de consulta relacionales y jerárquicos como SQL y 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
    ;

Una expresión de consulta comienza con una cláusula from y termina con una cláusula select o group. La cláusula from inicial puede ir seguida de cero o más from, let, where, join o orderby cláusulas. Cada cláusula from es un generador que introduce una variable de rango que recorre los elementos de una secuencia. Cada cláusula let introduce una variable de rango que representa un valor calculado mediante variables de rango anteriores. Cada cláusula where es un filtro que excluye elementos del resultado. Cada cláusula join compara claves especificadas de la secuencia de origen con claves de otra secuencia, obteniendo pares coincidentes. La cláusula orderby final o especifica la forma del resultado en términos de las variables de rango. La cláusula final select o group especifica la forma del resultado en términos de las variables de rango. Por último, se puede usar una cláusula into para “empalmar” consultas, tratando los resultados de una consulta como un generador en una consulta posterior.

12.20.2 Ambigüedades en las expresiones de consulta

Las expresiones de consulta utilizan una serie de palabras clave contextuales (sección 6.4.4): ascending, by, descending, equals, from, group, into, join, let, on, orderby, select y where.

Para evitar las ambigüedades que podrían surgir del uso de estos identificadores tanto como palabras clave como simples nombres, estos identificadores se consideran palabras clave en cualquier lugar dentro de una expresión de consulta, a menos que lleven el prefijo "@" (sección 6.4.4), en cuyo caso se consideran identificadores. A estos efectos, una expresión de consulta es cualquier expresión que empiece por "fromidentificador" seguida de cualquier token excepto “;”, “=” o “,”.

12.20.3 Traducción de expresiones de consulta

12.20.3.1 General

El lenguaje C# no especifica la semántica de ejecución de las expresiones de consulta. En su lugar, las expresiones de consulta se traducen en invocaciones de métodos que se adhieren al patrón de expresiones de consulta (sección 12.20.4). En concreto, las expresiones de consulta se traducen en invocaciones a métodos denominados Where, Select, SelectMany, Join, GroupJoin, OrderBy, OrderByDescending, ThenBy, ThenByDescending, GroupBy y Cast. Se espera que estos métodos tengan firmas y tipos de retorno específicos, tal como se describe en §12.20.4. Estos métodos pueden ser métodos de instancia del objeto consultado o métodos de extensión externos al objeto. Estos métodos implementan la ejecución real de la consulta.

La traducción de expresiones de consulta a invocaciones de métodos es un mapeo sintáctico que ocurre antes de que se haya realizado cualquier vinculación de tipos o resolución de sobrecargas. Tras la traducción de las expresiones de la consulta, las invocaciones a métodos resultantes se procesan como invocaciones a métodos normales, lo que a su vez puede revelar errores en tiempo de compilación. Estas condiciones de error incluyen, entre otras, métodos que no existen, argumentos de tipos erróneos y métodos genéricos en los que falla la inferencia de tipos.

Una expresión de consulta se procesa aplicando repetidamente las siguientes traducciones hasta que no es posible realizar más reducciones. Las traducciones se enumeran por orden de aplicación: cada sección asume que las traducciones de las secciones precedentes se han realizado exhaustivamente, y una vez agotadas, una sección no se volverá a visitar posteriormente en el procesamiento de la misma expresión de consulta.

Es un error en tiempo de compilación que una expresión de consulta incluya una asignación a una variable de rango o el uso de una variable de rango como argumento para un parámetro de referencia o de salida.

Ciertas traducciones inyectan variables de rango con identificadores transparentes denotados por *. Se describen con más detalle en la sección 12.20.3.8.

12.20.3.2 Expresiones de consulta con continuaciones

Expresión de consulta con una continuación del cuerpo de la consulta.

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

se traduce por

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

Las traducciones de las secciones siguientes suponen que las consultas no tienen continuaciones.

Ejemplo: El ejemplo:

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

se traduce por:

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

cuya traducción final es:

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

ejemplo final

12.20.3.3 Tipos explícitos de variables de rango

Una cláusula from que especifica explícitamente un tipo de variable de rango

from «T» «x» in «e»

se traduce por

from «x» in ( «e» ) . Cast < «T» > ( )

Una cláusula join que especifica explícitamente un tipo de variable de rango

join «T» «x» in «e» on «k1» equals «k2»

se traduce por

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

Las traducciones de las secciones siguientes suponen que las consultas no tienen tipos de variables de rango explícitos.

Ejemplo: el ejemplo

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

se traduce por

from c in (customers).Cast<Customer>()
where c.City == "London"
select c

cuya traducción final es

customers.
Cast<Customer>().
Where(c => c.City == "London")

ejemplo final

Nota: Los tipos de variable de rango explícitos son útiles para consultar colecciones que implementan la interfaz no genérica IEnumerable, pero no la interfaz genérica IEnumerable<T>. En el ejemplo anterior, este sería el caso si los clientes fueran de tipo ArrayList. nota final

12.20.3.4 Expresiones de consulta degeneradas

Una expresión de consulta del formulario

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

se traduce por

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

Ejemplo: el ejemplo

from c in customers
select c

se traduce por

(customers).Select(c => c)

ejemplo final

Una expresión de consulta degenerada es aquella que selecciona trivialmente los elementos de la fuente.

Nota: las fases posteriores de la traducción (§12.20.3.6 y §12.20.3.7) eliminan las consultas degeneradas introducidas por otros pasos de traducción reemplazándolas por su origen. Sin embargo, es importante asegurarse de que el resultado de una expresión de consulta nunca sea el propio objeto fuente. De lo contrario, la devolución del resultado de una consulta de este tipo podría exponer inadvertidamente datos privados (por ejemplo, una matriz de elementos) a la persona que llama. Por lo tanto, este paso protege las consultas degeneradas escritas directamente en el código fuente llamando explícitamente a Select en el código fuente. Por tanto, corresponde a los implementadores de Select y otros operadores de consulta garantizar que estos métodos nunca devuelvan el objeto fuente. nota final

12.20.3.5 Cláusulas from, let, where, join y orderby

Una expresión de consulta con una segunda cláusula seguida from de una cláusula select

from «x1» in «e1»  
from «x2» in «e2»  
select «v»

se traduce por

( «e1» ) . SelectMany( «x1» => «e2» , ( «x1» , «x2» ) => «v» )

Ejemplo: el ejemplo

from c in customers
from o in c.Orders
select new { c.Name, o.OrderID, o.Total }

se traduce por

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

ejemplo final

Una expresión de consulta con una segunda cláusula from seguida de un cuerpo de consulta Q que contiene un conjunto no vacío de cláusulas de cuerpo de consulta:

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

se traduce por

from * in («e1») . SelectMany( «x1» => «e2» ,
                              ( «x1» , «x2» ) => new { «x1» , «x2» } )
Q

Ejemplo: el ejemplo

from c in customers
from o in c.Orders
orderby o.Total descending
select new { c.Name, o.OrderID, o.Total }

se traduce por

from * in (customers).
   SelectMany(c => c.Orders, (c,o) => new { c, o })
orderby o.Total descending
select new { c.Name, o.OrderID, o.Total }

cuya traducción final es

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

donde x es un identificador generado por el compilador que, de lo contrario, es invisible e inaccesible.

ejemplo final

La expresión let junto con su cláusula from anterior:

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

se traduce por

from * in ( «e» ) . Select ( «x» => new { «x» , «y» = «f» } )  
...

Ejemplo: el ejemplo

from o in orders
let t = o.Details.Sum(d => d.UnitPrice * d.Quantity)
where t >= 1000
select new { o.OrderID, Total = t }

se traduce por

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 }

cuya traducción final es

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

donde x es un identificador generado por el compilador que, de lo contrario, es invisible e inaccesible.

ejemplo final

La expresión where junto con su cláusula from anterior:

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

se traduce por

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

Una cláusula join inmediatamente seguida de una cláusula select

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

se traduce por

( «e1» ) . Join( «e2» , «x1» => «k1» , «x2» => «k2» , ( «x1» , «x2» ) => «v» )

Ejemplo: el ejemplo

from c in customers
join o in orders on c.CustomerID equals o.CustomerID
select new { c.Name, o.OrderDate, o.Total }

se traduce por

(customers).Join(
   orders,
   c => c.CustomerID, o => o.CustomerID,
   (c, o) => new { c.Name, o.OrderDate, o.Total })

ejemplo final

Una cláusula join seguida de una cláusula de cuerpo de consulta:

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

se traduce por

from * in ( «e1» ) . Join(  
«e2» , «x1» => «k1» , «x2» => «k2» ,
( «x1» , «x2» ) => new { «x1» , «x2» })  
...

Una cláusula join-into inmediatamente seguida de una cláusula select

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

se traduce por

( «e1» ) . GroupJoin( «e2» , «x1» => «k1» , «x2» => «k2» ,
                     ( «x1» , «g» ) => «v» )

Una cláusula join into seguida de una cláusula de cuerpo de consulta

from «x1» in «e1»  
join «x2» in «e2» on «k1» equals «k2» into *g»  
...

se traduce por

from * in ( «e1» ) . GroupJoin(  
   «e2» , «x1» => «k1» , «x2» => «k2» , ( «x1» , «g» ) => new { «x1» , «g» })
...

Ejemplo: el ejemplo

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 }

se traduce por

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 }

cuya traducción final es

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

donde x y y son identificadores generados por el compilador que, de otro modo, serían invisibles e inaccesibles.

ejemplo final

Una cláusula orderby y su cláusula precedente from:

from «x» in «e»  
orderby «k1» , «k2» , ... , «kn»  
...

se traduce por

from «x» in ( «e» ) .
OrderBy ( «x» => «k1» ) .
ThenBy ( «x» => «k2» ) .
... .
ThenBy ( «x» => «kn» )
...

Si una cláusula ordering especifica un indicador de dirección descendente, se produce en su lugar una invocación de OrderByDescending o ThenByDescending.

Ejemplo: el ejemplo

from o in orders
orderby o.Customer.Name, o.Total descending
select o

tiene la traducción definitiva

(orders)
    .OrderBy(o => o.Customer.Name)
    .ThenByDescending(o => o.Total)

ejemplo final

En las traducciones siguientes se supone que no hay cláusulas let, where, join o orderby, y no más de una cláusula inicial from en cada expresión de consulta.

12.20.3.6 Cláusulas Select

Una expresión de consulta del formulario

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

se traduce por

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

excepto cuando «v» es el identificador «x», la traducción es simplemente

( «e» )

Ejemplo: el ejemplo

from c in customers.Where(c => c.City == "London")
select c

se traduce simplemente en

(customers).Where(c => c.City == "London")

ejemplo final

12.20.3.7 Cláusulas de grupo

Una cláusula group

from «x» in «e» group «v» by «k»

se traduce por

( «e» ) . GroupBy ( «x» => «k» , «x» => «v» )

excepto cuando «v» es el identificador «x», la traducción es simplemente

( «e» ) . GroupBy ( «x» => «k» )

Ejemplo: el ejemplo

from c in customers
group c.Name by c.Country

se traduce por

(customers).GroupBy(c => c.Country, c => c.Name)

ejemplo final

12.20.3.8 Identificadores transparentes

Ciertas traducciones inyectan variables de rango con identificadores transparentes denotados por *. Los identificadores transparentes existen solo como un paso intermedio en el proceso de traducción de una expresión de consulta.

Cuando una traducción de consulta inyecta un identificador transparente, otros pasos de traducción propagan el identificador transparente a funciones anónimas e inicializadores de objetos anónimos. En esos contextos, los identificadores transparentes tienen el siguiente comportamiento:

  • Cuando un identificador transparente aparece como parámetro en una función anónima, los miembros del tipo anónimo asociado están automáticamente en el ámbito del cuerpo de la función anónima.
  • Cuando un miembro con un identificador transparente está en el ámbito, los miembros de ese miembro también lo están.
  • Cuando un identificador transparente aparece como declarador de miembro en un inicializador de objeto anónimo, introduce un miembro con un identificador transparente.

En los pasos de traducción descritos anteriormente, los identificadores transparentes siempre se introducen junto con tipos anónimos, con la intención de capturar múltiples variables de ámbito como miembros de un único objeto. Una implementación de C# puede utilizar un mecanismo diferente a los tipos anónimos para agrupar múltiples variables de rango. Los siguientes ejemplos de traducción asumen que se utilizan tipos anónimos, y muestran una posible traducción de identificadores transparentes.

Ejemplo: el ejemplo

from c in customers
from o in c.Orders
orderby o.Total descending
select new { c.Name, o.Total }

se traduce por

from * in (customers).SelectMany(c => c.Orders, (c,o) => new { c, o })
orderby o.Total descending
select new { c.Name, o.Total }

que se traduce además en

customers
    .SelectMany(c => c.Orders, (c,o) => new { c, o })
    .OrderByDescending(* => o.Total)
    .Select(\* => new { c.Name, o.Total })

que, cuando se borran los identificadores transparentes, equivale 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 })

donde x es un identificador generado por el compilador que, de lo contrario, es invisible e inaccesible.

En el ejemplo

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 }

se traduce por

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 se reduce aún más 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 })

cuya traducción final es

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

donde x y y son identificadores generados por el compilador que, de otro modo, serían invisibles e inaccesibles. ejemplo final

12.20.4 El patrón consulta-expresión

El patrón Query-expression establece un patrón de métodos que los tipos pueden implementar para soportar expresiones de consulta.

Un tipo genérico C<T> admite el patrón de expresión de consulta si sus métodos de miembro públicos y los métodos de extensión públicamente accesibles pueden ser reemplazados por la siguiente definición de clase. Los miembros y los métodos de extensión accesibles se denominan "forma" de un tipo genérico C<T>. Se utiliza un tipo genérico para ilustrar las relaciones adecuadas entre los tipos de parámetros y de retorno, pero también es posible implementar el patrón para tipos no 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; }
}

Los métodos anteriores utilizan los tipos de delegado genéricos Func<T1, R> y Func<T1, T2, R>, pero podrían haber utilizado igualmente otros tipos de delegado o expression-tree con las mismas relaciones en los tipos parámetro y devolución.

Nota: La relación recomendada entre C<T> y O<T> que asegura que los métodos ThenBy y ThenByDescending están disponibles solo en el resultado de un método OrderBy o OrderByDescending. nota final

Nota: La forma recomendada del resultado de GroupBy—una secuencia de secuencias, donde cada secuencia interior tiene una propiedad adicional Key—. nota final

Nota: Dado que las expresiones de consulta se traducen a invocaciones de métodos mediante un mapeo sintáctico, los tipos tienen una flexibilidad considerable a la hora de implementar alguno o todos los métodos del patrón de expresiones de consulta. Por ejemplo, los métodos del patrón pueden implementarse como métodos de instancia o como métodos de extensión porque ambos tienen la misma sintaxis de invocación, y los métodos pueden solicitar delegados o árboles de expresiones porque las funciones anónimas son convertibles a ambos. Los tipos que implementan solo algunos patrones de expresión de consulta únicamente admiten traducciones de expresiones que coinciden con los métodos que ese tipo admite. nota final

Nota: El espacio de nombres System.Linq proporciona una implementación del patrón de expresión de consulta para cualquier tipo que implemente la interfaz System.Collections.Generic.IEnumerable<T>. nota final

12.21 Operadores de asignación

12.21.1 General

Todos los operadores de asignación, excepto uno, asignan un nuevo valor a una variable, una propiedad, un evento o un elemento indexador. La excepción, = ref, asigna una referencia de variable (sección 9.5) a una variable de referencia (sección 9.7).

assignment
    : unary_expression assignment_operator expression
    ;

assignment_operator
    : '=' 'ref'? | '+=' | '-=' | '*=' | '/=' | '%=' | '&=' | '|=' | '^=' | '<<='
    | right_shift_assignment
    ;

El operando izquierdo de una asignación debe ser una expresión que se clasifique como una variable o, excepto para = ref, un acceso a una propiedad, un acceso de indexador, un acceso a un evento o una tupla. Una expresión de declaración no se permite directamente como operando izquierdo, pero puede ocurrir como un paso en la evaluación de una asignación deconstruida.

El operador = se denomina operador de asignación simple. Asigna el valor o valores del operando derecho a la variable, propiedad, elemento indexador o elementos de tupla dados por el operando izquierdo. El operando izquierdo del operador de asignación simple no será un acceso a eventos (excepto como se describe en §15.8.2). El operador de asignación simple se describe en la sección 12.21.2.

Este operador = ref se denomina operador de asignación ref. Convierte al operando derecho en un variable_reference (§9.5), que será el referente de la variable de referencia designada por el operando izquierdo. El operador de asignación ref se describe en la sección 12.21.3.

Los operadores de asignación distintos de los operadores = y = ref se denominan operadores de asignación compuesta. Estos operadores realizan la operación indicada en los dos operandos y luego asignan el valor resultante a la variable, propiedad o elemento indexador dado por el operando izquierdo. Los operadores de asignación compuesta se describen en la sección 12.21.4.

Los operadores += y -= con una expresión de acceso a sucesos como operando izquierdo se denominan operadores de asignación de sucesos. Ningún otro operador de asignación es válido con un acceso a eventos como operando izquierdo. Los operadores de asignación de sucesos se describen en la sección 12.21.5.

Los operadores de asignación son asociativos a la derecha, lo que significa que las operaciones se agrupan de derecha a izquierda.

Ejemplo: una expresión de la forma a = b = c se evalúa como a = (b = c). ejemplo final

12.21.2 Asignación simple

El operador = se denomina operador de asignación simple.

Si el operando izquierdo de una asignación simple está en la forma E.P o E[Ei] donde E tiene el tipo de tiempo de compilación dynamic, entonces la asignación se enlaza dinámicamente (§12.3.3). En este caso, el tipo en tiempo de compilación de la expresión de asignación es dynamic y la resolución descrita a continuación se realizará en tiempo de ejecución en función del tipo de tiempo de ejecución de E. Si el operando izquierdo es de la forma E[Ei] en la que al menos un elemento Ei tiene el tipo en tiempo de compilación dynamic, y el tipo en tiempo de compilación de E no es un array, el acceso al indexador resultante está ligado dinámicamente, pero con una comprobación limitada en tiempo de compilación (sección 12.6.5).

Una asignación sencilla en la que el operando izquierdo se clasifica como una tupla también se denomina asignación de deconstrucción. Si alguno de los elementos de la tupla del operando izquierdo tiene un nombre de elemento, se produce un error de compilación. Si alguno de los elementos de tupla del operando izquierdo es una declaration_expression y cualquier otro elemento no es una declaration_expression o un descarte simple, ocurre un error en tiempo de compilación.

El tipo de una asignación simple x = y es el tipo de una asignación a x de y, que se determina recursivamente como sigue:

  • Si x es una expresión de tupla (x1, ..., xn), y y puede descomponerse en una expresión de tupla (y1, ..., yn) con elementos n (sección 12.7), y cada asignación a xi de yi tiene el tipo Ti, entonces la asignación tiene el tipo (T1, ..., Tn).
  • En caso contrario, si x se clasifica como variable, la variable no es readonly, x tiene un tipo T, y y tiene una conversión implícita a T, entonces la asignación tiene el tipo T.
  • De lo contrario, si x se clasifica como una variable implícitamente tipada (es decir, una expresión de declaración implícitamente tipada) y y tiene un tipo T, entonces el tipo inferido de la variable es T, y la asignación tiene el tipo T.
  • En caso contrario, si x se clasifica como un acceso a una propiedad o a un indexador, la propiedad o el indexador tiene un descriptor de acceso de conjunto accesible, x tiene un tipo T, y y tiene una conversión implícita a T, entonces la asignación tiene el tipo T.
  • De lo contrario, la asignación no es válida y se produce un error en tiempo de vinculación.

El procesamiento en tiempo de ejecución de una asignación simple de la forma x = y con tipo T se realiza como una asignación a x de y con tipo T, que consiste en los siguientes pasos recursivos:

  • x se evalúa si no se ha evaluado ya.
  • Si x se clasifica como variable, y se evalúa y, si es necesario, se convierte a T mediante una conversión implícita (sección 10.2).
    • Si la variable dada por x es un elemento de matriz de un reference_type, se realiza una comprobación en tiempo de ejecución para garantizar que el valor calculado para y es compatible con la instancia de matriz de la que x es un elemento. La comprobación tiene éxito si y es null, o si existe una conversión de referencia implícita (sección 10.2.8) desde el tipo de la instancia referenciada por y al tipo de elemento real de la instancia de matriz que contiene x. De lo contrario, se produce una excepción System.ArrayTypeMismatchException.
    • El valor resultante de la evaluación y conversión de y se almacena en la ubicación determinada por la evaluación de xy se obtiene como resultado de la asignación.
  • Si x se clasifica como acceso a una propiedad o indexador:
    • y se evalúa y, si es necesario, se convierte a T mediante una conversión implícita (sección 10.2).
    • El descriptor de acceso set de x se invoca con el valor que resulta de la evaluación y conversión de y como su argumento de valor.
    • El valor resultante de la evaluación y conversión de y se produce como resultado de la asignación.
  • Si x se clasifica como la tupla (x1, ..., xn) con aridad n:
    • y se deconstruye utilizando elementos de n en una expresión de tupla e.
    • una tupla de resultados t se crea al convertir e a T mediante una conversión implícita de tupla.
    • para cada xi en orden de izquierda a derecha, se realiza una asignación a xi de t.Itemi, excepto que los xi no se evalúan de nuevo.
    • t se obtiene como resultado de la asignación.

Nota: si el tipo en tiempo de compilación de x es dynamic y existe una conversión implícita del tipo en tiempo de compilación de y a dynamic, no se requiere resolución en tiempo de ejecución. nota final

Nota: Las reglas de covarianza de matrices (sección 17.6) permiten que un valor de un tipo de matriz A[] sea una referencia a una instancia de un tipo de matriz B[], siempre que exista una conversión implícita de referencia de B a A. Debido a estas reglas, la asignación a un elemento de matriz de un reference_type requiere una comprobación en tiempo de ejecución para garantizar que el valor que se asigna es compatible con la instancia de matriz. En el ejemplo

string[] sa = new string[10];
object[] oa = sa;
oa[0] = null;              // OK
oa[1] = "Hello";           // OK
oa[2] = new ArrayList();   // ArrayTypeMismatchException

la última asignación provoca que se lance una System.ArrayTypeMismatchException porque una referencia a una ArrayList no puede almacenarse en un elemento de una string[].

nota final

Cuando una propiedad o indexador declarado en un struct_type es el objetivo de una asignación, la expresión de instancia asociada con el acceso a la propiedad o indexador se clasificará como una variable. Si la expresión de instancia se clasifica como valor, se produce un error de vinculación.

Nota: Debido a sección 12.8.7, la misma regla se aplica también a los campos. nota final

Ejemplo: Dadas las declaraciones:

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

en el ejemplo

Point p = new Point();
p.X = 100;
p.Y = 100;
Rectangle r = new Rectangle();
r.A = new Point(10, 10);
r.B = p;

las asignaciones a p.X, p.Y, r.A y r.B están permitidas porque p y r son variables. Sin embargo, en el ejemplo

Rectangle r = new Rectangle();
r.A.X = 10;
r.A.Y = 10;
r.B.X = 100;
r.B.Y = 100;

las asignaciones no son válidas, ya que r.A y r.B no son variables.

ejemplo final

12.21.3 Asignación de referencia

El operador = ref se conoce como el operador de asignación de referencia.

El operando izquierdo debe ser una expresión que se vincule a una variable de referencia (sección 9.7), un parámetro de referencia (distinto de this), un parámetro de salida o un parámetro de entrada. El operando derecho será una expresión que produzca una variable_reference que designe un valor del mismo tipo que el operando izquierdo.

Es un error de tiempo de compilación si el ref-safe-context (§9.7.2) del operando izquierdo es más amplio que el ref-safe-context del operando derecho.

El operando derecho debe estar definitivamente asignado en el momento de la asignación de referencia.

Cuando el operando izquierdo se vincula a un parámetro de salida, se produce un error si dicho parámetro de salida no se ha asignado definitivamente al principio del operador de asignación ref.

Si el operando izquierdo es una ref escribible (es decir, designa cualquier cosa que no sea un parámetro local o de entrada ref readonly), entonces el operando derecho será una variable_reference escribible. Si la variable del operando derecho es escribible, el operando izquierdo puede ser una referencia escribible o de solo lectura.

La operación convierte el operando izquierdo en un alias de la variable del operando derecho. El alias puede convertirse en solo lectura incluso si la variable en el operando derecho es escribible.

El operador de asignación de referencia produce una variable_reference del tipo asignado. Se puede escribir si el operando izquierdo es editable.

El operador de asignación de referencia no leerá la ubicación de almacenamiento a la que hace referencia el operando derecho.

Ejemplo: Estos son algunos ejemplos 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
}

ejemplo final

Nota: Cuando se lee código utilizando un operador = ref, puede ser tentador leer la parte ref como si fuera parte del operando. Esto es especialmente confuso cuando el operando es una expresión condicional ?:. Por ejemplo, al leer ref int a = ref b ? ref x : ref y;, es importante leerlo como = ref siendo el operador y b ? ref x : ref y siendo el operando a la derecha: ref int a = ref (b ? ref x : ref y);. Es importante tener en cuenta que la expresión ref b no es parte de esa instrucción, aunque pueda parecerlo a primera vista. nota final

12.21.4 Asignación compuesta

Si el operando izquierdo de una asignación compuesta es de la forma E.P o E[Ei] donde E tiene el tipo en tiempo de compilación dynamic, entonces la asignación está ligada dinámicamente (sección 12.3.3). En este caso, el tipo en tiempo de compilación de la expresión de asignación es dynamic y la resolución descrita a continuación se realizará en tiempo de ejecución en función del tipo de tiempo de ejecución de E. Si el operando izquierdo es de la forma E[Ei] en la que al menos un elemento Ei tiene el tipo en tiempo de compilación dynamic, y el tipo en tiempo de compilación de E no es un array, el acceso al indexador resultante está ligado dinámicamente, pero con una comprobación limitada en tiempo de compilación (sección 12.6.5).

Una operación de la forma x «op»= y se procesa aplicando la resolución de sobrecarga de operadores binarios (sección 12.4.5) como si la operación estuviera escrita x «op» y. A continuación,

  • Si el tipo de retorno del operador seleccionado es implícitamente convertible al tipo de x, la operación se evalúa como x = x «op» y, excepto que x se evalúa una sola vez.
  • En caso contrario, si el operador seleccionado es un operador predefinido, si el tipo de devolución del operador seleccionado es explícitamente convertible al tipo de x, y si y es implícitamente convertible al tipo de x o el operador es un operador de desplazamiento, entonces la operación se evalúa como x = (T)(x «op» y), donde T es el tipo de x, excepto que x se evalúe solo una vez.
  • En caso contrario, la asignación compuesta no es válida y se produce un error de vinculación.

El término "solo se evalúa una vez" significa que en la evaluación de x «op» y, los resultados de cualquier expresión constituyente de x se guardan temporalmente y luego se reutilizan al realizar la asignación a x.

Ejemplo: en el A()[B()] += C()de asignación, donde A es un método que devuelve int[], y B y C son métodos que devuelven int, los métodos se invocan solo una vez, en el orden A, B, C. ejemplo final

Cuando el operando izquierdo de una asignación compuesta es un acceso a propiedad o a indexador, la propiedad o el indexador deberán tener un descriptor de acceso get y un descriptor de acceso set. Si no es así, se produce un error en tiempo de enlace.

La segunda regla anterior permite evaluar x «op»= y como x = (T)(x «op» y) en determinados contextos. La regla existe para que los operadores predefinidos puedan utilizarse como operadores compuestos cuando el operando izquierdo es del tipo sbyte, byte, short, ushort o char. Incluso cuando ambos argumentos son de uno de esos tipos, los operadores predefinidos producen un resultado de tipo int, como se describe en la sección 12.4.7.3. Por lo tanto, sin una conversión no sería posible asignar el resultado al operando izquierdo.

El efecto intuitivo de la regla para los operadores predefinidos es simplemente que x «op»= y está permitido si tanto x «op» y como x = y están permitidos.

Example: En el código de ejemplo siguiente

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

la razón intuitiva de cada error es que una asignación simple correspondiente también habría sido un error.

ejemplo final

Nota: esto también significa que las operaciones de asignación compuestas admiten operadores de elevación. Dado que una asignación compuesta x «op»= y se evalúa como x = x «op» y o x = (T)(x «op» y), las reglas de evaluación cubren implícitamente operadores elevados. nota final

12.21.5 Asignación de evento

Si el operando izquierdo del operador a += or -= se clasifica como un acceso de evento, entonces la expresión se evalúa como sigue:

  • Si existe, la expresión de instancia del acceso al evento se evalúa.
  • Se evalúa el operando derecho del operador += o -= y, si es necesario, se convierte al tipo del operando izquierdo mediante una conversión implícita (sección 10.2).
  • Se invoca el descriptor de acceso del evento, con una lista de argumentos que consta del valor calculado en el paso anterior. Si el operador era +=, se invoca el método de acceso 'add'; si el operador era -=, se invoca el método de acceso 'remove'.

Una expresión de asignación de evento no produce un valor. Por lo tanto, una expresión de asignación de suceso solo es válida en el contexto de una statement_expression (sección 13.7).

12.22 Expresión

Una expresión es una non_assignment_expression o una asignación.

expression
    : non_assignment_expression
    | assignment
    ;

non_assignment_expression
    : declaration_expression
    | conditional_expression
    | lambda_expression
    | query_expression
    ;

12.23 Expresiones constantes

Una expresión constante es una expresión que debe evaluarse completamente en tiempo de compilación.

constant_expression
    : expression
    ;

Una expresión constante debe tener el valor null o uno de los siguientes tipos:

  • sbyte, byte, short, ushort, int, uint, long, ulong, char, float, double, decimal, bool, string;
  • un tipo de enumeración; o
  • una expresión de valor por defecto (sección 12.8.21) para un tipo de referencia.

Solo se permiten las siguientes construcciones en las expresiones constantes:

  • Literales (incluido el literal null).
  • Referencias a miembros const de tipos de clase y struct.
  • Referencias a miembros de tipos de enumeración.
  • Referencias a constantes locales.
  • Subexpresiones entre paréntesis, que son a su vez expresiones constantes.
  • Expresiones de conversión.
  • Expresiones checked y unchecked.
  • Expresiones nameof.
  • Los operadores unarios predefinidos +, -, ! (negación lógica) y ~.
  • Los operadores binarios predefinidos +, -, *, /, %, <<, >>, &, |, ^, &&, ||, ==, !=, <, >, <=y >=.
  • El operador condicional ?:.
  • El operador que acepta valores NULL ! (§12.8.9).
  • Expresiones sizeof, siempre que el tipo no administrado sea uno de los tipos especificados en §23.6.9 para los que sizeof devuelve un valor de constante.
  • Expresiones de valor por defecto, siempre que el tipo sea uno de los enumerados anteriormente.

Las siguientes conversiones están permitidas en las expresiones constantes:

  • Conversiones de identidad
  • Conversiones numéricas
  • Conversiones de enumeración
  • Conversiones de expresiones de constantes
  • Conversiones de referencia implícitas y explícitas, siempre que el origen de las conversiones sea una expresión constante que se evalúe al valor null.

Nota: no se permiten otras conversiones como boxing, unboxing y conversiones implícitas de valores que no son null en expresiones de constantes. nota final

Example: En el código de ejemplo siguiente

class C
{
    const object i = 5;         // error: boxing conversion not permitted
    const object str = "hello"; // error: implicit reference conversion
}

la inicialización de i es un error porque es necesario realizar una conversión boxing. La inicialización de str es un error porque se requiere una conversión de referencia implícita a partir de un no valor null.

ejemplo final

Siempre que una expresión cumpla los requisitos enumerados anteriormente, se evaluará en tiempo de compilación. Esto es cierto incluso si la expresión es una subexpresión de una expresión mayor que contiene construcciones no constantes.

La evaluación en tiempo de compilación de expresiones constantes utiliza las mismas reglas que la evaluación en tiempo de ejecución de expresiones no constantes, con la salvedad de que en los casos en que la evaluación en tiempo de ejecución hubiera lanzado una excepción, la evaluación en tiempo de compilación provoca un error de compilación.

A menos que una expresión constante se coloque explícitamente en un contexto de unchecked, los desbordamientos que se producen en operaciones aritméticas de tipo integral y conversiones durante la evaluación en tiempo de compilación de la expresión siempre provocan errores en tiempo de compilación (§12.8.20).

Las expresiones constantes son necesarias en los contextos enumerados a continuación, lo que se indica en la gramática mediante constant_expression. En estos contextos, se produce un error de compilación si una expresión no puede evaluarse completamente en tiempo de compilación.

  • Declaraciones constantes (sección 15.4)
  • Declaraciones de miembro de enumeración (§19.4)
  • Argumentos por defecto de las listas de parámetros (sección 15.6.2)
  • etiquetas case de una instrucción switch (§13.8.3).
  • instrucciones goto case (§13.10.4)
  • Longitudes de dimensión en una expresión de creación de matriz (§12.8.17.5) que incluye un inicializador.
  • Atributos (sección 22)
  • En un constant_pattern (§11.2.3)

Una conversión implícita de expresiones constantes (sección 10.2.11) permite convertir una expresión constante de tipo int a sbyte, byte, short, ushort, uint o ulong, siempre que el valor de la expresión constante esté dentro del rango del tipo de destino.

12.24 Expresiones booleanas

Una boolean_expression es una expresión que produce un resultado de tipo bool; ya sea directamente o mediante la aplicación de operator true en determinados contextos como se especifica a continuación:

boolean_expression
    : expression
    ;

La expresión condicional controladora de un if_statement (§13.8.2), while_statement (§13.9.2), do_statement (§13.9.3) o for_statement (§13.9.4) es un boolean_expression. La expresión condicional controladora del operador ?: (sección 12.18) sigue las mismas reglas que una boolean_expression, pero por razones de precedencia del operador se clasifica como null_coalescing_expression.

Se requiere que una boolean_expressionE pueda producir un valor de tipo bool, como se indica a continuación:

  • Si E es implícitamente convertible a bool, entonces en tiempo de ejecución se aplica esa conversión implícita.
  • En caso contrario, se utiliza la resolución de sobrecarga de operadores unarios (sección 12.4.4) para encontrar la mejor implementación única de operator true en E, y dicha implementación se aplica en tiempo de ejecución.
  • Si no se encuentra dicho operador, se produce un error de vinculación.