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 porthis
(§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
+
,-
,*
,/
ynew
. 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 (comox++
). - 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étodoF
usando el valor antiguo dei
, luego se llama al métodoG
con el valor antiguo dei
, y, finalmente, se llama al métodoH
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 comox + (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.y
x?.y
f(x)
a[x]
a?[x]
x++
x--
x!
new
typeof
default
checked
unchecked
delegate
stackalloc
sección 12.9 Unario +
-
!x
~
++x
--x
(T)x
await x
§12.10 Multiplicativo *
/
%
§12.10 Aditivo +
-
§12.11 Shift <<
>>
§12.12 Comprobación de tipos y relacional <
>
<=
>=
is
as
§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 comox = (y = z)
. ejemplo final
La precedencia y la asociatividad pueden controlarse mediante paréntesis.
Ejemplo:
x + y * z
primero multiplicay
porz
y luego suma el resultado porx
, pero(x + y) * z
primero sumax
yy
y luego multiplica el resultado porz
. 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
yfalse
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
, as
y 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
oas
. 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 apropiadobool
. 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ónoperator «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
yY
para la operaciónoperator «op»(x, y)
. El conjunto consiste en la unión de los operadores candidatos proporcionados porX
y los operadores candidatos proporcionados porY
, 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
yY
son convertibles en identidad, o siX
yY
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
yY
, un operador«op»Y
proporcionado porY
tiene el mismo tipo de retorno que un«op»X
proporcionado porX
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
- 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₀
. SiT
es un tipo de valor anulable,T₀
es su tipo subyacente; en caso contrario,T₀
es igual aT
. - Para todas
operator «op»
las declaraciones enT₀
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 argumentosA
, entonces el conjunto de operadores candidatos está formado por todos los operadores aplicables enT₀
. - En caso contrario, si
T₀
esobject
, 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 deT₀
, o la clase base efectiva deT₀
siT₀
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:
- las reglas para conversiones numéricas implícitas (sección 10.2.3);
- las reglas para una mejor conversión (sección 12.6.4.7); y
- los operadores aritméticos (sección 12.10), relacionales (sección 12.12) y lógicos integrales (sección 12.13.2) disponibles.
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
, dondeb
es unbyte
ys
es unshort
, la resolución de sobrecarga seleccionaoperator *(int, int)
como mejor operador. Así, el efecto es queb
ys
se convierten aint
, y el tipo del resultado esint
. Del mismo modo, para la operacióni * d
, dondei
es unint
yd
es undouble
,overload
la resolución seleccionaoperator *(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 tipodecimal
, o se produce un error de vinculación si el otro operando es del tipofloat
odouble
. - De lo contrario, si cualquiera de los operandos es del tipo
double
, el otro operando se convierte al tipodouble
. - De lo contrario, si cualquiera de los operandos es del tipo
float
, el otro operando se convierte al tipofloat
. - De lo contrario, si cualquiera de los operandos es del tipo
ulong
, el otro operando se convierte al tipoulong
, o se produce un error de vinculación si el otro operando es del tipotype sbyte
,short
,int
olong
. - De lo contrario, si cualquiera de los operandos es del tipo
long
, el otro operando se convierte al tipolong
. - En caso contrario, si uno de los operandos es del tipo
uint
y el otro es del tiposbyte
,short
oint
, ambos operandos se convierten al tipolong
. - De lo contrario, si cualquiera de los operandos es del tipo
uint
, el otro operando se convierte al tipouint
. - 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 tiposdouble
yfloat
. La regla se deduce del hecho de que no hay conversiones implícitas entre el tipodecimal
y los tiposdouble
yfloat
. 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 deulong
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 undouble
. El error se resuelve convirtiendo explícitamente el segundo operando endecimal
, 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 denull
si el operando esnull
. 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 denull
si uno o ambos operandos sonnull
, con la excepción de los operadores&
y|
del tipobool?
, 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 esbool
. La forma elevada se construye añadiendo un único modificador?
a cada tipo de operando. El operador levantado considera dos valores igualesnull
, y un valornull
desigual a cualquier valor que no seanull
. Si ambos operandos son nonull
, el operador elevado desenvuelve los operandos y aplica el operador subyacente para producir el resultadobool
. - 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 esbool
. La forma elevada se construye añadiendo un único modificador?
a cada tipo de operando. El operador elevado genera el valorfalse
si uno o ambos operandos sonnull
. En caso contrario, el operador elevado desenvuelve los operandos y aplica el operador subyacente para obtener el resultadobool
.
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 nombradosN
en cada uno de los tipos especificados como restricción primaria o restricción secundaria (sección 15.2.5) paraT
, junto con el conjunto de miembros accesibles nombradosN
enobject
. - En caso contrario, el conjunto está formado por todos los miembros accesibles (sección 7.5) nombrados
N
enT
, incluidos los miembros heredados y los miembros accesibles nombradosN
enobject
. SiT
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 modificadoroverride
se excluyen del conjunto.
- Si
- A continuación, si
K
es cero, se eliminan todos los tipos anidados cuyas declaraciones incluyan parámetros de tipo. SiK
no es cero, se eliminan todos los miembros con un número diferente de parámetros de tipo. CuandoK
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, dondeS
es el tipo en el que se declara el miembroM
, 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 deS
son eliminados del conjunto. - Si
M
es una declaración de tipo, todos los no tipos declarados en un tipo base deS
se eliminan del conjunto, y todas las declaraciones de tipo con el mismo número de parámetros de tipo que los declarados comoM
en un tipo base deS
se eliminan del conjunto. - Si
M
es un método, todos los miembros no-método declarados en un tipo base deS
se eliminan del conjunto.
- Si
- 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 yT
tiene una clase base efectiva distinta deobject
y un conjunto de interfaces efectivas no vacío (sección 15.2.5). Para cada miembro del conjuntoS.M
, dondeS
es el tipo en el que se declara el miembroM
, se aplican las siguientes reglas siS
es una declaración de clase distinta deobject
:- 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 firmaM
que la declarada en una declaración de interfaz.
- Si
- 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
esobject
odynamic
, entoncesT
no tiene tipo base. - Si
T
es un enum_type, los tipos base deT
son los tipos de claseSystem.Enum
,System.ValueType
yobject
. - Si
T
es un struct_type, los tipos base deT
son los tipos de claseSystem.ValueType
yobject
.Nota: un nullable_value_type es un struct_type (§8.3.1). nota final
- Si
T
es un class_type, los tipos base deT
son las clases base deT
, incluido el tipo de claseobject
. - Si
T
es un interface_type, los tipos base deT
son las interfaces base deT
y el tipo de claseobject
. - Si
T
es un array_type, los tipos base deT
son los tipos de claseSystem.Array
yobject
. - Si
T
es un delegate_type, los tipos base deT
son los tipos de claseSystem.Delegate
yobject
.
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
yvalue
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 yP
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 esstatic
, la expresión de instancia esthis
.T.F(x, y)
La resolución de sobrecarga se aplica para seleccionar el mejor método F
de la clase o estructuraT
. Si el método no esstatic
. 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 dee
. Se produce un error de vinculación si el método esstatic
. El método se invoca con la expresión de instanciae
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 siP
es de solo escritura. SiP
no esstatic
, la expresión de instancia esthis
.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 siP
es de solo lectura. SiP
no esstatic
, la expresión de instancia esthis
.T.P
Se invoca al descriptor de acceso get de la propiedad P
en la clase o struct contenedoraT
. Se produce un error de compilación siP
no esstatic
o siP
es de solo escritura.T.P = value
El accesor 'set' de la propiedad P
en la clase o estructuraT
se invoca con la lista de argumentos(value)
. Se produce un error de compilación siP
no esstatic
o siP
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 deE
se invoca con la expresión de instanciae
. Se produce un error de vinculación siP
esstatic
o siP
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 deE
se invoca con la expresión de instanciae
y la lista de argumentos(value)
. Se produce un error de vinculación siP
esstatic
o siP
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. SiE
no esstatic
, la expresión de instancia esthis
.E -= value
Se invoca al descriptor de acceso remove del evento E
en la clase o struct contenedora. SiE
no esstatic
, la expresión de instancia esthis
.T.E += value
Se invoca al descriptor de acceso add del evento E
en la clase o structT
. Se produce un error de vinculación siE
no esstatic
.T.E -= value
Se invoca al descriptor de acceso remove del evento E
en la clase o structT
. Se produce un error de vinculación siE
no esstatic
.e.E += value
El descriptor de acceso add del evento E
en la clase, struct o interfaz dada por el tipo deE
se invoca con la expresión de instanciae
. Se produce un error de vinculación siE
esstatic
.e.E -= value
El descriptor de acceso remove del evento E
en la clase, struct o interfaz dada por el tipo deE
se invoca con la expresión de instanciae
. Se produce un error de vinculación siE
esstatic
.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 instanciae
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 instanciae
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
yy
. 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 porM(c: false, valueB);
. El primer argumento se utiliza fuera de posición (el argumento se utiliza en primera posición, pero el parámetro nombradoc
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 tipoint
que el parámetro de entrada. En la llamada al métodoM1(i + 5)
, se crea una variable sin nombreint
, 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 unSystem.ArrayTypeMismatchException
porque el tipo real de elemento deb
esstring
y noobject
.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
ystring
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 tipoU
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) deU
aTᵢ
. - En caso contrario, si
Eᵢ
tiene un tipoU
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) deU
aTᵢ
. - En caso contrario, si
Eᵢ
tiene un tipoU
y el parámetro correspondiente es un parámetro de entrada (sección 15.6.2.3.2) yEᵢ
es un argumento de entrada, se hace una inferencia exacta (sección 12.6.3.9) deU
aTᵢ
. - En caso contrario, si
Eᵢ
tiene un tipoU
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) deU
aTᵢ
. - 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) cualquierXₑ
se fija (§12.6.3.12). - Si no existen tales variables de tipo, todas las variables de tipo no fijas
Xᵢ
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
- Hay al menos una variable de tipo
- 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ámetroTᵢ
correspondiente en los que los tipos de salida (§12.6.3.5) contengan variables de tipo no fijasXₑ
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 deE
con 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 salidaE
con 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 E
de la siguiente manera:
- Si
E
es una función anónima con tipo de retorno inferidoU
(§12.6.3.13) yT
es un tipo delegado o un tipo de árbol de expresión con tipo de retornoTₓ
, entonces se hace una inferencia de límite inferior (§12.6.3.10) deU
aTₓ
. - 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 de a . - En caso contrario, si
E
es una expresión de tipoU
, se realiza una inferencia de límite inferior deU
aT
. - 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 E
a un tipo T
de la siguiente manera:
- Si
E
es una función anónima explícitamente con tipos de parámetrosU₁...Uᵥ
yT
es un tipo delegado o un tipo de árbol de expresión con tipos de parámetrosV₁...Vᵥ
, entonces para cadaUᵢ
se hace una inferencia exacta (§12.6.3.9) deUᵢ
a la correspondienteVᵢ
.
12.6.3.9 Inferencias exactas
La inferencia exactade un tipo U
a otro tipo V
se realiza del siguiente modo:
- Si
V
es uno de losXᵢ
no fijos, entoncesU
se añade al conjunto de límites exactos paraXᵢ
. - En caso contrario, los conjuntos
V₁...Vₑ
yU₁...Uₑ
se determinan comprobando si se da alguno de los siguientes casos:-
V
es un tipo de matrizV₁[...]
yU
es un tipo de matrizU₁[...]
del mismo rango -
V
es el tipoV₁?
yU
es el tipoU₁
-
V
es un tipo construidoC<V₁...Vₑ>
yU
es un tipo construidoC<U₁...Uₑ>
Si se da alguno de estos casos, se hace una inferencia exacta de cada uno deUᵢ
al correspondienteVᵢ
.
-
- 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 U
a otro tipo V
se hace como sigue:
- Si
V
es uno de losXᵢ
no fijados, entonces se agregaU
al conjunto de límites inferiores paraXᵢ
. - De lo contrario, si
V
es el tipoV₁?
yU
es el tipoU₁?
entonces se hace una inferencia de límite inferior deU₁
aV₁
. - En caso contrario, los conjuntos
U₁...Uₑ
yV₁...Vₑ
se determinan comprobando si se da alguno de los siguientes casos:-
V
es un tipo de matrizV₁[...]
yU
es un tipo de matrizU₁[...]
del mismo rango -
V
es uno deIEnumerable<V₁>
,ICollection<V₁>
,IReadOnlyList<V₁>>
,IReadOnlyCollection<V₁>
oIList<V₁>
yU
es un tipo de matriz unidimensionalU₁[]
-
V
es un tipo construidoclass
,struct
,interface
odelegate
C<V₁...Vₑ>
y hay un tipo únicoC<U₁...Uₑ>
de modo queU
(o, siU
es un tipoparameter
, 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 deU
aC<T>
porqueU₁
podría serX
oY
.)
Si se aplica cualquiera de estos casos, se realiza una inferencia de cadaUᵢ
al correspondienteVᵢ
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
esC<V₁...Vₑ>
entonces la inferencia depende del parámetro de tipoi-th
deC
:- 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 U
a un tipo V
se hace como sigue:
- Si
V
es uno de losXᵢ
no fijados,U
se agrega al conjunto de límites superiores paraXᵢ
. - En caso contrario, los conjuntos
V₁...Vₑ
yU₁...Uₑ
se determinan comprobando si se da alguno de los siguientes casos:-
U
es un tipo de matrizU₁[...]
yV
es un tipo de matrizV₁[...]
del mismo rango -
U
es uno deIEnumerable<Uₑ>
,ICollection<Uₑ>
,IReadOnlyList<Uₑ>
,IReadOnlyCollection<Uₑ>
oIList<Uₑ>
yV
es un tipo de matriz unidimensionalVₑ[]
-
U
es el tipoU1?
yV
es el tipoV1?
-
U
es de tipo clase, struct, interfaz o delegado construidoC<U₁...Uₑ>
yV
es un tipoclass, struct, interface
odelegate
que esidentical
a,inherits
desde (directa o indirectamente) o implementa (directa o indirectamente) un tipo únicoC<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 deC<U₁>
aV<Q>
. No se realizan inferencias deU₁
a niX<Q>
oY<Q>
.)
Si se aplica cualquiera de estos casos, se realiza una inferencia de cadaUᵢ
al correspondienteVᵢ
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
esC<U₁...Uₑ>
entonces la inferencia depende del parámetro de tipoi-th
deC
:- 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 candidatos
Uₑ
comienza como el conjunto de todos los tipos en el conjunto de límites establecidos paraXᵢ
. - Cada límite para
Xᵢ
se examina uno por uno: para cada límite exacto U deXᵢ
, todos los tiposUₑ
que no son idénticos aU
se eliminan del conjunto candidato. En cada límite inferiorU
deXᵢ
, se eliminan del conjunto de candidatos todos los tiposUₑ
que no sean el resultado de una conversión implícita a partir deU
. En cada límite superior U deXᵢ
, se eliminan del conjunto de candidatos todos los tiposUₑ
que no sean una conversión implícita aU
. - Si entre los tipos candidatos restantes
Uₑ
hay un único tipoV
al que existe una conversión implícita desde todos los demás tipos candidatos, entoncesXᵢ
se fija aV
. - 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 tipoF
de esa expresión. - Si el cuerpo de
F
es un bloque y el conjunto de expresiones de las instruccionesreturn
del bloque tiene un tipo más comúnT
(§12.6.3.15), el tipo de valor devuelto efectivo inferido deF
esT
. - 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 deF
es una expresión clasificada como nada (§12.2) o un bloque donde ninguna de las instruccionesreturn
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 inferidoT
, 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 inferidoT
, el tipo de retorno inferido esT
. - 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 claseSystem.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 directivausing namespace
, y dada una claseCustomer
con una propiedad de tipoName
string
, el métodoSelect
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
esCustomer
. A continuación, mediante el proceso de inferencia de tipos de función anónima descrito anteriormente, ac
se le asigna el tipoCustomer
, y la expresiónc.Name
se relaciona con el tipo de valor de retorno del parámetro selector, infiriendo queTResult
esstring
. Así, la invocación es equivalente aSequence.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
esstring
. A continuación, al parámetro de la primera función anónima,s
, se le asigna el tipo inferidostring
, y la expresiónTimeSpan.Parse(s)
está relacionada con el tipo de retorno def1
, infiriendo queY
esSystem.TimeSpan
. Por último, al parámetro de la segunda función anónima,t
, se le asigna el tipo inferidoSystem.TimeSpan
, y la expresiónt.TotalHours
está relacionada con el tipo de retorno def2
, inferidoZ
comodouble
. Así, el resultado de la invocación es del tipodouble
.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 D
al 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 aX
. -
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 losEᵢ
como argumentos e inferirX
. 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. SiA
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.
- El modo de paso de parámetros del argumento es idéntico al modo de paso de parámetros del parámetro correspondiente y:
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.
- Si el grupo de métodos resulta de un simple_name, un método de instancia solo es aplicable si se permite el acceso
- 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ᵥ
aQᵥ
no es mejor que la conversión implícita deEᵥ
aPᵥ
, y - para al menos un argumento, la conversión de
Eᵥ
aPᵥ
es mejor que la conversión deEᵥ
aQᵥ
.
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 yMₑ
es un método genérico, entoncesMᵢ
es mejor queMₑ
. - En caso contrario, si
Mᵢ
es aplicable en su forma normal yMₑ
tiene una matriz params y solo es aplicable en su forma expandida, entoncesMᵢ
es mejor queMₑ
. - 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 deMₑ
, entoncesMᵢ
es mejor queMₑ
. - En caso contrario, si los tipos de los parámetros de
Mᵥ
son más específicos que los deMₓ
, entoncesMᵥ
es mejor queMₓ
. Deje que{R1, R2, ..., Rn}
y{S1, S2, ..., Sn}
representen los tipos de parámetros no instanciados y no expandidos deMᵥ
yMₓ
. los tipos de parámetro deMᵥ
son más específicos queMₓ
si, para cada parámetro,Rx
no es menos específico queSx
, y, para al menos un parámetro,Rx
es más específico queSx
:- 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 enMₓ
, entoncesMᵥ
es mejor queMₓ
. - 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 enMₓ
y ninguno de los parámetros enMₓ
utiliza la mejor opción de paso de parámetros queMᵥ
,Mᵥ
es mejor queMₓ
. - 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 conT₁
yE
no coincide exactamente conT₂
(sección 12.6.4.6) E
coincide exactamente con ambos o ninguno deT₁
yT₂
, yT₁
es un destino de conversión mejor queT₂
(§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ónC₁
, yT₂
no es compatible con el mejor método individual del grupo de métodos para la conversiónC₂
12.6.4.6 Expresión exactamente coincidente
Dada una expresión E
y un tipo T
, E
coincide exactamente conT
si se cumple una de las siguientes condiciones:
-
E
tiene un tipoS
y existe una conversión de identidad deS
aT
E
es una función anónima,T
es un tipo delegadoD
o un tipo de árbol de expresiónExpression<D>
y se cumple una de las siguientes condiciones:- Existe un tipo de retorno inferido
X
paraE
en el contexto de la lista de parámetros deD
(§12.6.3.12), y existe una conversión de identidad deX
al tipo de retorno deD
-
E
es una lambdaasync
sin valor de devolución, yD
tiene un tipo de devolución que no es genérico«TaskType»
. - O
E
no es asincrónico yD
tiene un tipo de valor devueltoY
, oE
es asincrónico yD
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 conY
- El cuerpo de
E
es un bloque donde cada sentencia de retorno devuelve una expresión que coincide exactamente conY
- El cuerpo de
- Existe un tipo de retorno inferido
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₁
aT₂
y no existe ninguna conversión implícita deT₂
aT₁
-
T₁
es«TaskType»<S₁>
(sección 15.15.1),T₂
es«TaskType»<S₂>
,S₁
y es más especializado queS₂
-
T₁
es«TaskType»<S₁>
(§15.15.1),T₂
es«TaskType»<S₂>
yT₁
es más especializado queT₂
-
T₁
esS₁
oS₁?
dondeS₁
es un tipo entero con signo yT₂
esS₂
oS₂?
dondeS₂
es un tipo entero sin signo. En concreto:-
S₁
essbyte
yS₂
esbyte
,ushort
,uint
oulong
-
S₁
esshort
yS₂
esushort
,uint
oulong
-
S₁
esint
yS₂
esuint
oulong
S₁
eslong
yS₂
esulong
-
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 valorV
yM
se declara o invalida enV
:-
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 casoE
se clasifica como variable. - Si
E
no se clasifica como variable, o siV
no es un tipo struct de solo lectura (sección 16.2.2), yE
es uno de los siguientes:- un parámetro de entrada (sección 15.6.2.3.2) o
- un campo
readonly
(§15.5.3), o - una variable de referencia
readonly
o devolución (§9.7),
entonces se crea una variable local temporal de tipo
E
y el valor deE
se asigna a esa variable.E
se reclasifica como una referencia a esa variable local temporal. La variable temporal es accesible comothis
dentro deM
, pero no de ninguna otra forma. Así, solo cuando se puede escribirE
es posible para el invocador observar los cambios que haceM
athis
.- La lista de argumentos se evalúa como se describe en la sección 12.6.2.
- Se invoca a
M
. La variable referenciada porE
se convierte en la variable referenciada porthis
.
-
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 convertirE
en un class_type yE
se considera que es de ese class_type en los siguientes pasos. Si el value_type es un enum_type, el class_type esSystem.Enum;
; de lo contrario, esSystem.ValueType
. - Se comprueba si el valor de
E
es válido. Si el valor deE
es NULL, se lanza unSystem.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 deM
proporcionada por el tipo de tiempo de ejecución de la instancia a la que hace referenciaE
. Este miembro de función se determina aplicando las reglas de asignación de interfaces (§18.6.5) para determinar la implementación deM
proporcionada por el tipo en tiempo de ejecución de la instancia referenciada porE
. - En caso contrario, si
M
es un miembro de función virtual, el miembro de función a invocar es la implementación deM
proporcionada por el tipo en tiempo de ejecución de la instancia referenciada porE
. Este miembro de función se determina aplicando las reglas para determinar la implementación más derivada (sección 15.6.4) deM
con respecto al tipo de tiempo de ejecución de la instancia referenciada porE
. - En caso contrario,
M
es un miembro de función no virtual, y el miembro de función a invocar es el mismoM
.
- Si el tipo de tiempo de enlace de
- 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
oSystem.Enum
. nota 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 elementosn
, el resultado de la deconstrucción es la expresiónE
. - En caso contrario, si
E
tiene un tipo de tupla(T1, ..., Tn)
con elementosn
, entonces se evalúaE
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
ypointer_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) ySystem.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) ySystem.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 deSystem.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úmeroI
de0
aN-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 (
}
)
- Un carácter de llave izquierda (
- Los caracteres del Interpolated_Regular_String_Mid o Interpolated_Verbatim_String_Mid inmediatamente después de la interpolación correspondiente, si la hay
- Especificación de marcador de posición:
- 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 nombreI
, 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 nombreI
, 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 deT
incluye un parámetro de tipo con nombreI
, 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
enT
con argumentos de tipoe
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 dethis
. 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 formathis.I
. Esto solo puede ocurrir cuandoe
es cero. - De lo contrario, el resultado es el mismo que un acceso de miembro (§12.8.7) de la forma
T.I
oT.I<A₁, ..., Aₑ>
.
- Si
- Si
- 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 yI
es el nombre de un espacio de nombres enN
, 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 nombreI
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
enN
.
- Si la ubicación en la que se produce el simple_name está incluida dentro de una declaración de espacio de nombres para
- De lo contrario, si
N
contiene un tipo accesible cuyo nombre esI
y tienee
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 paraN
y la declaración de espacio de nombres contiene una extern_alias_directive o using_alias_directive que asocia el nombreI
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.
- Si
- 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 nombreI
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 tipoe
, 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 tipoe
, el simple_name es ambiguo y se produce un error en tiempo de compilación.
- Si
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
- Si
- De lo contrario, si
e
es cero yI
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 formatoNi
oE.Ni
oE?.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
oE.Ni
oE?.Ni
o Ni
es del formatoItemX
, dondeX
es una secuencia de dígitos decimales que no se inicia con0
y que podría representar la posición de un elemento de tupla, mientras queX
no representa la posición del elemento.
- Otro elemento de la expresión de tupla tiene el nombre
- 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
yt2
, 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 det2
, la conversión de tupla implícita se basa en las conversiones implícitas de2
along
y denull
astring
. 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 det4
, 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 yE
es un espacio de nombres yE
contiene un espacio de nombres anidado con nombreI
, entonces el resultado es ese espacio de nombres. - En caso contrario, si
E
es un espacio de nombres yE
contiene un tipo accesible con nombreI
y parámetros de tipoK
, entonces el resultado es ese tipo construido con los argumentos de tipo dados. - Si
E
se clasifica como un tipoE
, si no es un parámetro de tipo, y si una búsqueda de miembros (sección 12.5) deI
enE
con parámetros de tipoK
produce una coincidencia, entoncesE.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
enE
. - En caso contrario, el resultado es una variable, es decir, el campo estático
I
enE
.
- 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
- 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 siI
fuera un campo estático. - En caso contrario, el resultado es un acceso a un evento sin expresión de instancia asociada.
- 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
- 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
- Si
E
es un acceso de propiedad, acceso de indexador, variable o valor, cuyo tipo esT
, y una búsqueda de miembros (§12.5) deI
enT
con argumentos de tipoK
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 deE
. - Si
I
identifica una propiedad de instancia, el resultado es un acceso a una propiedad con una expresión de instancia asociada deE
y un tipo asociado que es el tipo de la propiedad. SiT
es un tipo de clase, el tipo asociado se elige de la primera declaración o invalidación de la propiedad encontrada al comenzar conT
y buscar en sus clases base. - Si
T
es un class_type yI
identifica un campo de instancia de ese class_type:- Si el valor de
E
esnull
, se produce unSystem.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 porE
. - En caso contrario, el resultado es una variable, es decir, el campo
I
del objeto referenciado porE
.
- Si el valor de
- Si
T
es un struct_type e identificaI
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 campoI
en la instancia del struct dada porE
. - En caso contrario, el resultado es una variable, es decir, el campo
I
de la instancia struct dado porE
.
- Si
- 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-=
, entoncesE.I
se procesa exactamente como siI
fuera un campo de instancia. - De lo contrario, el resultado es un acceso a eventos con una expresión de instancia asociada de
E
.
- 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
- En primer lugar, si
- 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 identificadorColor
que hacen referencia al tipoColor
están delimitadas por«...»
, y las que hacen referencia al campoColor
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 deP.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 deE
esT?
, y el significado deE
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
esT
, y el significado deE
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ónP.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 deE
esT?
, y el significado deE
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
esT
, y el significado deE
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 comonull
niA₀
niA₁
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
devuelvetrue
,p
puede ser dereferenciada con seguridad para acceder a su propiedadName
, 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, six
esnull
en tiempo de ejecución, se producirá una excepción ya quenull
no se puede convertir aint
.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 enstring?
, conlv
siendo este un parámetro de salida y que realiza una asignación sencilla.El método
M
pasa la variables
, de tipostring
, como parámetro de salida paraAssign
; el compilador utilizado emite una advertencia ya ques
no es una variable que acepta valores nulos. Dado que el segundo argumento deAssign
no 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
dynamic
en 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 conT
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 conT
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étodosM
:- Si
F
no es genéricoF
, es un candidato cuando:-
M
no tiene lista de argumentos de tipo, y -
F
es aplicable con respecto aA
(sección 12.6.4.2).
-
- Si
F
es genérico yM
no tiene lista de argumentos de tipoF
, 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 deF
es aplicable con respecto aA
(sección 12.6.4.2).
- Si
F
es genérico yM
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 deF
es aplicable con respecto aA
(sección 12.6.4.2).
-
- Si
- 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, dondeC
es el tipo en el que se declara el métodoF
, se eliminan del conjunto todos los métodos declarados en un tipo base deC
. Además, siC
es un tipo de clase distinto deobject
, 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 elegiblesMₑ
, 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 elegiblesMₑ
, entonces el conjunto de esos métodos de extensión es el conjunto candidato.
- Si el espacio de nombres o unidad de compilación dado contiene directamente declaraciones de tipos no genéricos
- 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
B
tiene prioridad sobre el primer método de extensión eC
tiene 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 sobreC.G
, yE.F
tiene prioridad sobre tantoD.F
comoC.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 deD
esnull
, se lanza unSystem.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 tiposhort
entonces se realiza una conversión implícita aint
, ya que son posibles las conversiones implícitas deshort
aint
y deshort
along
. 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 deP
esnull
, se lanza unSystem.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 unSystem.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 enT
o un tipo base deT
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 conjuntoS
, donde es el tipo en el que se declara el indexadorI
:- Si
I
no es aplicable con respecto aA
(sección 12.6.4.2), se eliminaI
del conjunto. - Si
I
es aplicable con respecto aA
(sección 12.6.4.2), todos los indexadores declarados en un tipo base deS
se eliminan del conjunto. - Si
I
es aplicable con respecto aA
(sección 12.6.4.2) yS
es un tipo de clase distinto deobject
, todos los indexadores declarados en una interfaz se eliminan del conjunto.
- Si
- 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 deA
, y un tipo asociado que es el tipo del indexador. SiT
es un tipo de clase, el tipo asociado se elige de la primera declaración o sobrescritura del indexador que se encuentra al comenzar conT
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ónP.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 deE
esT?
, y el significado deE
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
esT
, y el significado deE
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ónP[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 deE
esT?
, y el significado deE
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
esT
, y el significado deE
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 comonull
niA₀
niA₁
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ámetroref
del tipo struct. En particular, esto significa que la variable se considera asignada inicialmente.
- Si la declaración del constructor no tiene inicializador de constructor, la variable
- 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 variablethis
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ámetroref
del tipo struct
- Si la estructura es un
- 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.
- 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
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
, decimal
y 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 dex
. - 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 esstatic
) y la lista de argumentos (six
es un acceso indexador) asociada a se evalúan conx
, 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 dex
se invoca con este valor como valor argumental. - El valor guardado de
x
se convierte en el resultado de la operación.
- La expresión de instancia (si
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 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.
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 yA
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 paraT
definido en la sección 8.3.3.
- 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
- En caso contrario, si
T
es un type_parameter yA
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.
- Si no se ha especificado ninguna restricción de tipo de valor o restricción de constructor (sección 15.2.5) para
- 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 aA
(§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.
- Si
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 unSystem.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.
- Se asigna una nueva instancia de clase
- 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.
- Se crea una instancia del tipo
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
Rectangle
aloca las dos instancias incrustadas dePoint
, se pueden usar para inicializar las instancias incrustadas dePoint
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 tipoint[,]
, y la expresión de creación de matriz newint[10][,]
produce una instancia de matriz del tipoint[][,]
. 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 esnull
. No es posible que la misma expresión de creación de matrices cree instancias también de las submatrices y la instrucciónint[][] 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
nistring
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 comoobject[]
. 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) deE
aD
.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) deE
aD
.Si
E
es un valorE
, deberá ser compatible (sección 20.2) conD
, y el resultado es una referencia a un delegado recién creado con una lista de invocación de una sola entrada que invoca aE
.
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) deE
aD
. - 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 deE
aD
(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
esnull
, se lanza unSystem.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 unSystem.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étodoSquare
porque ese método coincide exactamente con la lista de parámetros y el tipo de retorno deDoubleFunc
. Si el segundo métodoSquare
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
yp2
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étodosvoid
, con una instancia deSystem.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
ySystem.Int32
son del mismo tipo. El resultado detypeof(X<>)
no depende del argumento de tipo, pero el resultado detypeof(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
odouble
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ónSystem.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 unSystem.OverflowException
y el métodoG
devuelve –727379968 (los 32 bits inferiores del resultado fuera de rango). El comportamiento del métodoH
depende del contexto de comprobación de desbordamiento predeterminado para la compilación, peroF
es el mismo queG
.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
yH
hacen que se notifiquen errores en tiempo de compilación porque las expresiones se evalúan en un contexto dechecked
. También se produce un desbordamiento al evaluar la expresión constante enG
, pero como la evaluación tiene lugar en un contextounchecked
, 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 dex * y
enMultiply
, por lo quex * 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 rangoint
, sin el operadorunchecked
, la conversión aint
produciría errores de compilación.ejemplo final
Nota: Los operadores e instrucciones
checked
yunchecked
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 resultadostackalloc
es unSpan<int>
, que es convertido por un operador implícito aReadOnlySpan<int>
. De forma similar, paraspan9
, el resultanteSpan<double>
se convierte al tipo definidoWidget<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éricoList<T>
declarado en el espacio de nombresSystem.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 denameof(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 deX
es el valor representable más pequeño del tipo de operando (-2³¹ paraint
o -2⁶³ paralong
), entonces la negación matemática deX
no es representable dentro del tipo de operando. Si esto ocurre dentro de un contexto dechecked
, se lanza unSystem.OverflowException
; si ocurre dentro de un contexto deunchecked
, 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 tipolong
, y el tipo del resultado eslong
. Una excepción es la regla que permite escribir el valorint
−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 valorlong
−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. Six
esNaN
, el resultado también esNaN
.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 tipoSystem.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
, decimal
y 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 dex
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 esstatic
) y la lista de argumentos (six
es un acceso indexador) asociada a se evalúan conx
, 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 dex
se invoca con este valor como su argumento de valor. - Este valor también se convierte en el resultado de la operación.
- La expresión de instancia (si
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 tipox
) o como una additive_expression combinada con una parenthesized_expression (que calcula el valorx – 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), exceptoas
yis
.
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
yy
son identificadores, entoncesx.y
es una gramática correcta para un tipo, aunquex.y
en realidad no denote un tipo. ejemplo final
Nota: de la regla de desambiguación se deduce que, si
x
yy
son identificadores,(x)y
,(x)(y)
y(x)(-y)
son cast_expressions, pero(x)-y
no, aunquex
identifique un tipo. Sin embargo, six
es una palabra clave que identifica un tipo predefinido (comoint
), 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 tipodynamic
en tiempo de compilación t
tiene un método de instancia o extensión accesible llamadoGetAwaiter
que no tiene parámetros ni parámetros de tipo, y un tipo de retornoA
para el cual se cumplen todas las siguientes condiciones:A
implementa la interfazSystem.Runtime.CompilerServices.INotifyCompletion
(en adelante conocido comoINotifyCompletion
para simplificar).A
tiene una propiedad de instancia accesible y legibleIsCompleted
de tipobool
-
A
tiene un método de instancia accesibleGetResult
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 novoid
T
, 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
bool
b
evaluando la expresión(a).IsCompleted
. - Si
b
esfalse
, la evaluación depende de sia
implementa la interfazSystem.Runtime.CompilerServices.ICriticalNotifyCompletion
(a partir de ahora conocida comoICriticalNotifyCompletion
por brevedad). Esta comprobación se realiza en tiempo de enlace; esto es, en tiempo de ejecución sia
tiene el tipo en tiempo de compilacióndynamic
; y en tiempo de compilación de lo contrario. Sear
el delegado de reanudación (§15.15):- Si
a
no implementaICriticalNotifyCompletion
, se evalúa la expresión((a) as INotifyCompletion).OnCompleted(r)
. - Si
a
implementaICriticalNotifyCompletion
, 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.
- Si
- Ya sea inmediatamente después (si
b
fuetrue
) o tras la invocación posterior del delegado de reanudación (sib
fuefalse
), 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 unaSystem.OverflowException
. En un contexto deunchecked
, 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
ey
son valores finitos positivos.z
es el resultado dex * 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 aunquex
niy
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 tipoSystem.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
olong
y el operando derecho es–1
, se produce un desbordamiento. En un contexto dechecked
, esto provoca la generación de unaSystem.ArithmeticException
(o una subclase de esta). En un contexto deunchecked
, la implementación define si se lanza unaSystem.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
ey
son valores finitos positivos.z
es el resultado dex / 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 unSystem.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 dex
menos la escala dey
.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 porx – (x / y) * y
. Siy
es cero, se lanza unSystem.DivideByZeroException
.Si el operando izquierdo es el valor más pequeño de
int
olong
y el operando derecho es–1
, se produce unaSystem.OverflowException
solo six / 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
ey
son valores finitos positivos.z
es el resultado dex % y
y se calcula comox – n * y
, donde n es el mayor número entero posible menor o igual quex / 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 quen
es el entero más próximo ax / 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 unaSystem.ArithmeticException
(o una subclase de esta). Una implementación conforme no lanzará una excepción parax % y
en ningún caso en el quex / 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 dex
.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 unSystem.OverflowException
. En un contexto deunchecked
, 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
ey
son valores finitos distintos de cero yz
es el resultado dex + y
. Six
ey
tienen la misma magnitud pero signos opuestos,z
es cero positivo. Six + y
es demasiado grande para representarlo en el tipo de destino,z
es un infinito con el mismo signo quex + 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, yU
es el tipo subyacente deE
: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 esnull
, se sustituye por una cadena vacía. En caso contrario, cualquier no operandostring
se convierte a su representación de cadena invocando el método virtualToString
heredado del tipoobject
. SiToString
devuelvenull
, 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 valornull
. Se puede producir un errorSystem.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 seanull
). En caso contrario, si el segundo operando esnull
, 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 unaSystem.OverflowException
. En un contexto deunchecked
, 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
ey
son valores finitos distintos de cero yz
es el resultado dex – y
. Six
ey
son iguales,z
es cero positivo. Six – y
es demasiado grande para representarlo en el tipo de destino,z
es un infinito con el mismo signo quex – 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 dey
, 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ónU
, y es el tipo subyacente deE
: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 dex
yy
, 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 esnull
. - 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.
- 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
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
- Si el primer operando es
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
<<
desplazax
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
>>
desplazax
a la derecha por un número de bits calculado como se describe a continuación.Cuando
x
es del tipoint
olong
, los bits de bajo orden dex
se descartan, los bits restantes se desplazan a la derecha y las posiciones de bits vacíos de alto orden se ponen a cero six
es no negativo y a uno six
es negativo.Cuando
x
es de tipouint
oulong
, los bits de orden inferior dex
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
esint
ouint
, el recuento de desplazamientos lo proporcionan los cinco bits de orden bajo decount
. En otras palabras, el número de bits a desplazar se calcula a partir decount & 0x1F
. - Cuando el tipo de
x
eslong
oulong
, el recuento de desplazamientos lo proporcionan los seis bits de orden bajo decount
. En otras palabras, el número de bits a desplazar se calcula a partir decount & 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 tipoint
, la operaciónunchecked ((int)((uint)x >> y))
realiza un desplazamiento lógico a la derecha dex
. 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 |
true si 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
yy
es NaN, entoncesx < y
esfalse
, pero!(x >= y)
estrue
. 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 tipoT
dondeT
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==
esfalse
y el resultado de!=
estrue
. - Si en tiempo de ejecución
T
es un tipo de valor anulable, el resultado se calcula a partir de la propiedad del operandoHasValue
, como se describe en (sección 12.12.10). - Si en tiempo de ejecución
T
es un tipo de referencia, el resultado estrue
si el operando esnull
yfalse
de lo contrario.
- Si en tiempo de ejecución
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
ox != y
, si existe algún operadoroperator ==
ooperator !=
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 tipoobject
.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 aunqueT
pueda representar un tipo de valor no anulable, y el resultado se define simplemente comofalse
cuandoT
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
yt
se refieren a dos instancias de cadena distintas que contienen los mismos caracteres. La primera comparación resultaTrue
porque se selecciona el operador predefinido de igualdad de cadenas (sección 12.12.8) cuando ambos operandos son del tipostring
. Todas las comparaciones restantes producenFalse
porque la sobrecarga deoperator ==
en el tipo destring
no es aplicable cuando alguno de los operandos tiene un tipoobject
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 deint
.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 sonnull
. - 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 no
null
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
yyi
en orden léxico:- Se evalúa el operador
xi == yi
y se obtiene un resultado de tipobool
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 operadorfalse
se invoca dinámicamente sobre él, y el valorbool
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 valorbool
resultante se niega con el operador lógico de negación (!
).
- Si el resultado de la comparación es
- Si el
bool
resultante esfalse
, no se realiza ninguna evaluación adicional y el resultado del operador de igualdad en tuplas esfalse
.
- Se evalúa el operador
- Si todas las comparaciones de elementos arrojaron
true
, el resultado del operador de igualdad de la tupla estrue
.
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
yyi
en orden léxico:- Se evalúa el operador
xi != yi
y se obtiene un resultado de tipobool
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 operadortrue
y el valor resultantebool
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 resultantebool
es el resultado.
- Si el resultado de la comparación es
- Si el
bool
resultante estrue
, no se realiza ninguna evaluación adicional y el resultado del operador de igualdad en tuplas estrue
.
- Se evalúa el operador
- Si todas las comparaciones de elementos arrojaron
false
, el resultado del operador de igualdad de la tupla esfalse
.
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:
- Si
E
es una función anónima o un grupo de métodos, se produce un error en tiempo de compilación. - Si
E
es el literalnull
, o si el valor deE
esnull
, el resultado esfalse
. - De lo contrario:
- Sea
R
el tipo en tiempo de ejecución deE
. - Que
D
se derive deR
de la siguiente manera: - Si
R
es un tipo de valor que acepta valores nulos,D
es el tipo subyacente deR
. - En caso contrario,
D
esR
. - El resultado depende de
D
yT
como sigue: - Si
T
es un tipo de referencia, el resultado estrue
si:- existe una conversión de identidad entre
D
yT
, -
D
es un tipo de referencia y existe una conversión de referencia implícita deD
aT
, o - O bien:
D
es un tipo de valor y existe una conversión boxing deD
aT
.
O bien:D
es un tipo de valor yT
es un tipo de interfaz implementado porD
.
- existe una conversión de identidad entre
- Si
T
es un tipo de valor que acepta valores NULL, el resultado estrue
siD
es el tipo subyacente deT
. - Si
T
es un tipo de valor no anulable, el resultado estrue
siD
yT
son del mismo tipo. - 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, dondeC
es el tipo en tiempo de compilación deE
:
- Si el tipo en tiempo de compilación de
e
es el mismo que el deT
, 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 deE
aT
:
- Si
C
es de un tipo de valor no nulo, el resultado de la operación estrue
.- 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
aT
, o siC
oT
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 tipoT
y el resultado de la operación esfalse
. 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 tipoT
.
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
aT
. - El tipo de
E
oT
es un tipo abierto. -
E
es el literalnull
.
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
deG
se conoce como un tipo de referencia porque tiene la restricción de clase. Sin embargo, el parámetro de tipoU
deH
no es así; por lo tanto, no se permite el uso del operadoras
enH
.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ónx & y
, excepto quey
será evaluado solo six
no esfalse
. - La operación
x || y
corresponde a la operaciónx | y
, excepto quey
será evaluado solo six
no estrue
.
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
devuelvefalse
yoperator false
devuelvefalse
. 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 comox ? y : false
. En otras palabras, primero se evalúax
y se convierte al tipobool
. Entonces, six
estrue
,y
se evalúa y se convierte al tipobool
, y este se convierte en el resultado de la operación. En caso contrario, el resultado de la operación esfalse
. - La operación
x || y
se evalúa comox ? true : y
. En otras palabras, primero se evalúax
y se convierte al tipobool
. Entonces, six
estrue
, el resultado de la operación estrue
. En caso contrario,y
se evalúa y se convierte a tipobool
, 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 tipoT
, y devolverá un resultado de tipoT
. -
T
debe contener declaraciones deoperator true
yoperator 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 comoT.false(x) ? x : T.&(x, y)
, dondeT.false(x)
es una invocación deloperator false
declarado enT
, yT.&(x, y)
es una invocación deloperator &
seleccionado. En otras palabras, primero se evalúax
y luego se invocaoperator false
sobre el resultado para determinar six
es definitivamente falso. Entonces, six
es definitivamente falso, el resultado de la operación es el valor calculado previamente parax
. En caso contrario,y
se evalúa y se invoca unoperator &
seleccionado sobre el valor calculado previamente parax
y el valor calculado paray
para producir el resultado de la operación. - La operación
x || y
se evalúa comoT.true(x) ? x : T.|(x, y)
, dondeT.true(x)
es una invocación deloperator true
declarado enT
, yT.|(x, y)
es una invocación deloperator |
seleccionado. En otras palabras, primero se evalúax
y se invocaoperator true
sobre el resultado para determinar six
es definitivamente verdadero. Entonces, six
es definitivamente cierto, el resultado de la operación es el valor previamente calculado parax
. En caso contrario,y
se evalúa y se invoca unoperator |
seleccionado sobre el valor calculado previamente parax
y el valor calculado paray
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 comoa ?? (b ?? c)
. En términos generales, una expresión de la formaE1 ?? E2 ?? ... ?? EN
devuelve el primero de los operandos que no esnull
onull
si todos los operandos sonnull
. 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 yb
es una expresión dinámica, el tipo resultante esdynamic
. En tiempo de ejecución, primero se evalúaa
. Sia
no esnull
,a
se convierte adynamic
, y este se convierte en el resultado. De lo contrario, se evalúab
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 deb
aA₀
, el tipo de resultado esA₀
. En tiempo de ejecución, primero se evalúaa
. Sia
no esnull
,a
se desencapsula en el tipoA₀
y se convierte en el resultado. De lo contrario,b
se evalúa y se convierte al tipoA₀
, y esto se convierte en el resultado. - De lo contrario, si
A
existe y existe una conversión implícita deb
aA
, el tipo resultante esA
. En tiempo de ejecución, primero se evalúaa
. Sia
no esnull
,a
se convierte en el resultado. De lo contrario,b
se evalúa y se convierte al tipoA
, y esto se convierte en el resultado. - De lo contrario, si
A
existe y es un tipo de valor anulable,b
tiene un tipoB
y existe una conversión implícita deA₀
aB
, el tipo de resultado esB
. En tiempo de ejecución, primero se evalúaa
. Sia
no esnull
,a
se desencapsula en el tipoA₀
y se convierte en el tipoB
, 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 tipoB
y existe una conversión implícita dea
aB
, el tipo resultante esB
. En tiempo de ejecución, primero se evalúaa
. Sia
no esnull
,a
se convierte en el tipoB
, 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
out
argument_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 deb1
esbool
porque ese es el tipo del parámetro de salida correspondiente enM1
. El siguienteWriteLine
puede acceder ai1
yb1
, que se han introducido en el ámbito contenedor.La declaración de
s2
muestra un intento de usari2
en la llamada anidada aM
, que no se permite dado que la referencia ocurre dentro de la lista de argumentos donde se declarói2
. Por otro lado, la referencia ab2
en el argumento final está permitida, porque se produce después del final de la lista de argumentos anidada dondeb2
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 avar _
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 comoa ? 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 tiposdynamic
(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 tipoX
yy
tiene tipoY
,- Si existe una conversión de identidad entre
X
yY
, 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 esdynamic
, se prefiere la inferencia de tiposdynamic
(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
aY
, pero no deY
aX
, entoncesY
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
aY
, entoncesY
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
aX
, entoncesX
es el tipo de la expresión condicional. - En caso contrario, si existe una conversión implícita (sección 10.2) de
Y
aX
, pero no deX
aY
, entoncesX
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 existe una conversión de identidad entre
- Si solo uno de
x
yy
tiene un tipo, y ambosx
yy
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 valorbool
deb
:- Si existe una conversión implícita del tipo de
b
abool
, entonces se realiza esta conversión implícita para producir un valorbool
. - De lo contrario, el
operator true
definido por el tipo deb
se invoca para producir un valor debool
.
- Si existe una conversión implícita del tipo de
- Si el valor
bool
producido por el paso anterior estrue
, entoncesx
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 valorbool
deb
:- Si existe una conversión implícita del tipo de
b
abool
, entonces se realiza esta conversión implícita para producir un valorbool
. - De lo contrario, el
operator true
definido por el tipo deb
se invoca para producir un valor debool
.
- Si existe una conversión implícita del tipo de
- Si el valor
bool
producido por el paso anterior estrue
, entoncesx
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 deM
esvoid
(§12.8.13). Pero cuando se trata como una null_conditional_invocation_expression, se permite que el tipo de resultado pueda servoid
. nota final
Ejemplo: el tipo de resultado de
List<T>.Reverse
esvoid
. 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 athis
. Esto es así tanto si el acceso es explícito (como enthis.x
) como implícito (como enx
dondex
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ónbreak
o una instruccióncontinue
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étodosSum
. Cada uno toma un argumentoselector
, que extrae el valor para sumar de un elemento de la lista. El valor extraído puede ser unint
o undouble
y la suma resultante es igualmente unint
o undouble
.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étodosSum
son aplicables porque la función anónimad => d.UnitCount
es compatible con ambosFunc<Detail,int>
yFunc<Detail,double>
. Sin embargo, la resolución de sobrecarga elige el primer método deSum
porque la conversión aFunc<Detail,int>
es mejor que la conversión aFunc<Detail,double>
.En la segunda invocación de
orderDetails.Sum
, solo es aplicable el segundo métodoSum
porque la función anónimad => d.UnitPrice * d.UnitCount
produce un valor de tipodouble
. Así, la resolución de sobrecargas elige el segundo métodoSum
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 dex
se extiende al menos hasta que el delegado devuelto deF
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 dex
, 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 dex
.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
astatic 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 dey
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 "from
identificador" 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éricaIEnumerable<T>
. En el ejemplo anterior, este sería el caso si los clientes fueran de tipoArrayList
. 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 deSelect
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
yy
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
yy
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>
yO<T>
que asegura que los métodosThenBy
yThenByDescending
están disponibles solo en el resultado de un métodoOrderBy
oOrderByDescending
. nota final
Nota: La forma recomendada del resultado de
GroupBy
—una secuencia de secuencias, donde cada secuencia interior tiene una propiedad adicionalKey
—. 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 interfazSystem.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 comoa = (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)
, yy
puede descomponerse en una expresión de tupla(y1, ..., yn)
con elementosn
(sección 12.7), y cada asignación axi
deyi
tiene el tipoTi
, entonces la asignación tiene el tipo(T1, ..., Tn)
. - En caso contrario, si
x
se clasifica como variable, la variable no esreadonly
,x
tiene un tipoT
, yy
tiene una conversión implícita aT
, entonces la asignación tiene el tipoT
. - De lo contrario, si
x
se clasifica como una variable implícitamente tipada (es decir, una expresión de declaración implícitamente tipada) yy
tiene un tipoT
, entonces el tipo inferido de la variable esT
, y la asignación tiene el tipoT
. - 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 tipoT
, yy
tiene una conversión implícita aT
, entonces la asignación tiene el tipoT
. - 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 aT
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 paray
es compatible con la instancia de matriz de la quex
es un elemento. La comprobación tiene éxito siy
esnull
, o si existe una conversión de referencia implícita (sección 10.2.8) desde el tipo de la instancia referenciada pory
al tipo de elemento real de la instancia de matriz que contienex
. De lo contrario, se produce una excepciónSystem.ArrayTypeMismatchException
. - El valor resultante de la evaluación y conversión de
y
se almacena en la ubicación determinada por la evaluación dex
y se obtiene como resultado de la asignación.
- Si la variable dada por
- Si
x
se clasifica como acceso a una propiedad o indexador:-
y
se evalúa y, si es necesario, se convierte aT
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 dey
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 aridadn
:y
se deconstruye utilizando elementos den
en una expresión de tuplae
.- una tupla de resultados
t
se crea al convertire
aT
mediante una conversión implícita de tupla. - para cada
xi
en orden de izquierda a derecha, se realiza una asignación axi
det.Itemi
, excepto que losxi
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
esdynamic
y existe una conversión implícita del tipo en tiempo de compilación dey
adynamic
, 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 matrizB[]
, siempre que exista una conversión implícita de referencia deB
aA
. 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 ejemplostring[] 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 unaArrayList
no puede almacenarse en un elemento de unastring[]
.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
yr.B
están permitidas porquep
yr
son variables. Sin embargo, en el ejemploRectangle 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
yr.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 parteref
como si fuera parte del operando. Esto es especialmente confuso cuando el operando es una expresión condicional?:
. Por ejemplo, al leerref int a = ref b ? ref x : ref y;
, es importante leerlo como= ref
siendo el operador yb ? 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ónref 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 comox = x «op» y
, excepto quex
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 siy
es implícitamente convertible al tipo dex
o el operador es un operador de desplazamiento, entonces la operación se evalúa comox = (T)(x «op» y)
, dondeT
es el tipo dex
, excepto quex
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, dondeA
es un método que devuelveint[]
, yB
yC
son métodos que devuelvenint
, los métodos se invocan solo una vez, en el ordenA
,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 comox = x «op» y
ox = (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
yunchecked
. - 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 quesizeof
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 destr
es un error porque se requiere una conversión de referencia implícita a partir de un no valornull
.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ónswitch
(§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
enE
, y dicha implementación se aplica en tiempo de ejecución. - Si no se encuentra dicho operador, se produce un error de vinculación.
ECMA C# draft specification