Traducido por Octavio Hernández y Daniel Carbajal
Este artículo no ha sido traducido por Microsoft
Este artículo y la veracidad de su traducción no ha sido revisado o verificado por Microsoft.
Microsoft no acepta responsabilidades sobre la veracidad o la información de este artículo que se proporciona tal cual por la comunidad.
C# 3.0 (“C# Orcas”) introduce diversas extensiones del lenguaje que se apoyan en C# 2.0 para dar soporte a la creación y uso de librerías clases de orden superior y estilo funcional. Estas extensiones posibilitan la construcción de API composicionales que ofrecen un poder expresivo similar al de los lenguajes de consulta en dominios como las bases de datos relacionales y el XML. Entre las extensiones se incluyen:
| • | Variables locales de tipo implícito, que permiten que el tipo de variables locales sea inferido (deducido) a partir de las expresiones utilizadas para inicializarlas |
| • | Métodos extensores, que hacen posible extender tipos existentes y tipos construidos con métodos adicionales |
| • | Expresiones lambda, una evolución de los métodos anónimos que ofrece una inferencia de tipos mejorada y conversiones tanto a tipos delegados como a árboles de expresiones |
| • | Inicializadores de objetos, que facilitan la construcción e inicialización de objetos |
| • | Tipos anónimos, tipos de tupla que se infieren y crean automáticamente a partir de inicializadores de objetos |
| • | Arrays de tipo implícito, una forma de creación e inicialización de arrays que deduce el tipo de los elementos de un array a partir de un inicializador de array |
| • | Expresiones de consulta, que ofrecen una sintaxis integrada en el lenguaje para consultas similar a los lenguajes de consulta relacionales y jerárquicos tales como SQL y XQuery |
| • | Árboles de expresiones, que permiten representar las expresiones lambda como datos (árboles de expresiones) en lugar de código (delegados). |
Este documento es una descripción técnica de dichas características. El documento hace referencia a la Especificación del Lenguaje C# versión 1.2 (§1 al §18) y a la Especificación del Lenguaje C# versión 2.0 (§19 al §25), ambas disponibles en la página principal del lenguaje C# (http://msdn.microsoft.com/vcsharp/language).
En una declaración de variable local de tipo implícito, el tipo de la variable local que se declara se deduce a partir de la expresión utilizada para inicializar la variable. Cuando una declaración de variable local especifica var como tipo y no hay ningún tipo llamado var en ámbito, la declaración es una declaración de variable local de tipo implícito. Por ejemplo:
var i = 5;
var s = "Hello";
var d = 1.0;
var numbers = new int[] {1, 2, 3};
var orders = new Dictionary<int,Order>();
Las declaraciones de variables locales de tipo implícito anteriores son exactamente equivalentes a las siguientes declaraciones explícitamente tipadas:
int i = 5;
string s = "Hello";
double d = 1.0;
int[] numbers = new int[] {1, 2, 3};
Dictionary<int,Order> orders = new
Dictionary<int,Order>();
Un declarador de variable local en una declaración de variable local de tipo implícito está sujeto a las siguientes restricciones:
| • | El declarador debe incluir un inicializador |
| • | El inicializador debe ser una expresión. El inicializador no puede ser un inicializador de objeto o colección (§CNDJ6nn5us4RjIIAqgBLqQsCAAAACAAAAA0AAABfAFIAZQBmADkANgA0ADgAOAA5ADIAMQAAAA== 26.4) en sí mismo, pero sí una expresión new que incluya un inicializador de objeto o colección |
| • | El tipo de tiempo de compilación de la expresión del inicializador no puede ser el tipo nulo |
| • | Si la declaración de variable local incluye múltiples declaradores, todos los inicializadores deberán tener el mismo tipo de tiempo de compilación |
Los siguientes son ejemplos de declaraciones incorrectas de variables locales de tipo implícito:
var x;// Error, no hay inicializador del
que deducir el tipo
var y = {1, 2, 3};// Error, inicializador
de colección no permitido
var z = null;// Error, tipo nulo no permitido
Por razones de compatibilidad descendente, cuando una declaración de variable local especifica var como tipo y hay un tipo llamado var en ámbito, la declaración hace referencia a ese tipo; no obstante, se generará una advertencia para llamar la atención sobre tal ambigüedad. Dado que un tipo llamado var violaría el convenio establecido de comenzar los nombres de tipos con una letra mayúscula, esta situación es poco probable que ocurra.
El inicializador-for de una sentencia for (§8.8.3) y la adquisición-de-recurso de una sentencia using (§8.13) puede ser una declaración de variable local de tipo implícito. Del mismo modo, la variable de iteración de una sentencia foreach (§8.8.4) puede ser declarada como una variable local de tipo implícito, en cuyo caso el tipo de la variable de iteración será deducido a partir del tipo de los elementos de la colección que se enumera. En el ejemplo
int[] numbers = { 1, 3, 5, 7, 9 };
foreach (var n in numbers)
Console.WriteLine(n);
el tipo de n es deducido como int, el tipo de los elementos de numbers.
Los métodos extensores son métodos estáticos que pueden ser invocados utilizando la sintaxis de métodos de instancia. De hecho, los métodos extensores hacen posible extender tipos existentes y tipos construidos con métodos adicionales
26.2.1 Declaración de métodos extensores
Los métodos extensores se declaran especificando la palabra reservada this como modificador del primer parámetro de los métodos. Los métodos extensores sólo pueden declararse dentro de clases estáticas. El siguiente es un ejemplo de una clase estática que declara dos métodos extensores:
namespace Acme.Utilities
{
public static class Extensions
{
public static int ToInt32(this string s) {
return Int32.Parse(s);
}
public static T[] Slice<T>(this T[]
source, int index, int count) {
if (index < 0 || count < 0 || source.Length
– index < count)
throw new ArgumentException();
T[] result = new T[count];
Array.Copy(source, index, result, 0,
count);
return result;
}
}
}
Los métodos extensores tienen todas las capacidades de los métodos estáticos regulares. Adicionalmente, una vez que han sido importados, los métodos extensores pueden ser invocados utilizando la sintaxis de método de instancia.
26.2.2 Importación de métodos extensores
Los métodos extensores se importan a través de directivas-using-para-espacios-de-nombres (§9.3.2). Además de importar los tipos contenidos en un espacio de nombres, una directiva-using-para-espacios-de-nombres importa todos los métodos extensores de todas las clases estáticas del espacio de nombres. Efectivamente, los métodos extensores importados se comportan como métodos adicionales de los tipos indicados por su primer parámetro y tienen una precedencia inferior a la de los métodos de instancia regulares. Por ejemplo, cuando el espacio de nombres Acme.Utilities del ejemplo anterior sea importado mediante la directiva-using-para-espacios-de-nombres
using Acme.Utilities;
será posible invocar a los métodos extensores definidos en la clase estática Extensions utilizando la sintaxis de método de instancia:
string s = "1234";
int i = s.ToInt32();// Equivale a
Extensions.ToInt32(s)
int[] digits = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
int[] a = digits.Slice(4, 3);// Equivale a
Extensions.Slice(digits, 4, 3)
26.2.3 Llamadas a métodos extensores
Las reglas detalladas para las llamadas a métodos extensores se describen a continuación. En una invocación (llamada) a método (§7.5.5.1) de una de las formas
expr . identificador ( ) expr . identificador ( args ) expr . identificador < args-tipo > ( ) expr . identificador < args-tipo > ( args )
si el procesamiento normal de la llamada no encuentra métodos de instancia aplicables (específicamente, si el conjunto de métodos candidatos para la llamada es vacío), se hace el intento de procesar la construcción como una llamada a un método extensor. La llamada a método se reescribe primero de una de las siguientes formas, respectivamente:
identificador ( expr ) identificador ( expr , args ) identificador < args-tipo > ( expr ) identificador < args-tipo > ( expr , args )
La forma rescrita es entonces procesada como una llamada a un método estático, excepto por la manera en que se resuelve el identificador: comenzando por la declaración de espacio de nombres más interna, continuando con cada declaración de espacio de nombres que engloba a la anterior, y terminando con la unidad de compilación contenedora, se realizan intentos sucesivos de procesar la llamada a método rescrita utilizando el grupo de métodos formado por todos los métodos extensores accesibles mediante el nombre indicado por identificador importados por las directivas-using-para-espacios-de-nombres de la declaración del espacio de nombres. El primer grupo de métodos que produzca un conjunto no vacío de métodos candidatos será el elegido para la llamada a método rescrita. Si todos los intentos producen conjuntos vacíos de métodos candidatos, se producirá un error de compilación.
Las reglas anteriores significan que los métodos de instancia tienen precedencia sobre los métodos extensores, y que los métodos extensores importados en declaraciones de espacios de nombres más internas tienen precedencia sobre los métodos extensores importados en declaraciones de espacios de nombres más externas. Por ejemplo:
using N1;
namespace N1
{
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 la clase B tiene precedencia sobre el primer método extensor, y el método de C tiene precedencia sobre ambos métodos extensores.
C# 2.0 introdujo los métodos anónimos, que permiten la escritura de bloques de código “en línea” donde se esperan instancias de delegados. Aunque los métodos anónimos ofrecen mucha de la potencia de los lenguajes de programación funcionales, la sintaxis de los métodos anónimos es verbosa y de naturaleza imperativa. Las expresiones lambda ofrecen una sintaxis más concisa y funcional para escribir métodos anónimos.
Una expresión lambda se escribe como una lista de parámetros, seguida por el lexema =>, seguida por una expresión o bloque de sentencias.
expresión: asignación expresión-no-de-asignación expresión-no-de-asignación: expresión-condicional expresión-lambda expresión-de-consulta expresión-lambda: ( lista-parámetros-lambdaopc ) => cuerpo- expresión-lambda parámetro-lambda- implícitamente-tipado => cuerpo- expresión-lambda lista-parámetros-lambda: lista-parámetros-lambda-explícitamente-tipados lista-parámetros-lambda-implícitamente-tipados lista-parámetros-lambda-explícitamente-tipados parámetro-lambda-explícitamente-tipado lista-parámetros-lambda-explícitamente-tipados , parámetro-lambda-explícitamente-tipado parámetro-lambda-explícitamente-tipado: modificador-de-parámetroopc tipo identificador lista-parámetros-lambda-implícitamente-tipados parámetro-lambda- implícitamente-tipado lista-parámetros-lambda-implícitamente-tipados , parámetro-lambda- implícitamente-tipado parámetro-lambda- implícitamente-tipado: identificador cuerpo-expresión-lambda: expresión bloque
Los parámetros de una expresión lambda pueden ser tipados explícita o implícitamente. En una lista de parámetros explícitamente tipados, el tipo de cada parámetro es indicado explícitamente. En una lista de parámetros tipados implícitamente, los tipos de los parámetros se deducen del contexto en el que aparece la expresión lambda — específicamente, cuando la expresión lambda es convertida a un tipo delegado compatible, ese tipo delegado suministra los tipos de los parámetros (§CNDJ6nn5us4RjIIAqgBLqQsCAAAACAAAAA4AAABfAFIAZQBmADEAMAA4ADkANgA1ADQAOQA4AAAA 26.3.1).
En una expresión lambda con un único parámetro de tipo implícito, los paréntesis pueden ser omitidos de la lista de parámetros. En otras palabras, una expresión lambda de la forma
( param ) => expr
puede ser abreviada a
param => expr
A continuación se muestran varios ejemplos de expresiones lambda:
x => x + 1// Tipo –
implícito, cuerpo – expresión
x => { return x + 1; }// Tipo - implícito, cuerpo –
sentencia
(int x) => x + 1// Tipo - explícito, cuerpo –
expresión
(int x) => { return x + 1; }// Tipo - explícito, cuerpo –
sentencia
(x, y) => x * y// Múltiples parámetros
() => Console.WriteLine()// Sin parámetros
En general, la especificación de métodos anónimos, que se brinda en §21 de la Especificación de C# 2.0, también es aplicable a las expresiones lambda. Las expresiones lambda son un supraconjunto funcional de los métodos anónimos, que ofrecen las siguientes funcionalidades adicionales:
| • | Las expresiones lambda permiten que los tipos de los parámetros sean omitidos y deducidos, mientras que los métodos anónimos exigen que los tipos de los parámetros sean indicados explícitamente |
| • | El cuerpo de una expresión lambda puede ser una expresión o un bloque de sentencias, mientras que el cuerpo de un método anónimo puede ser solamente un bloque de sentencias |
| • | Las expresiones lambda pasadas como argumentos participan en la inferencia de tipos de argumentos (§26.3.2) y en la resolución de sobrecargas de métodos (§26.3.3) |
| • | Las expresiones lambda con una expresión como cuerpo pueden ser convertidas en árboles de expresiones (§26.8). |
26.3.1 Conversión de expresiones lambda
De modo similar a una expresión-de-método-anónimo, una expresión-lambda se clasifica como un valor con reglas de conversión especiales. El valor en sí no tiene un tipo, pero puede ser convertido implícitamente a un tipo delegado compatible. Específicamente, un tipo delegado D es compatible con una expresión-lambda L si se cumple que:
| • | D y L tienen la misma cantidad de parámetros |
| • | Si L tiene una lista de parámetros explícitamente tipados, cada parámetro de D tiene el mismo tipo y modificadores que el parámetro correspondiente de L |
| • | Si L tiene una lista de parámetros tipados implícitamente, D no tiene parámetros ref u out. |
| • | Si el tipo de retorno de D es void y el cuerpo de L es una expresión, cuando a cada parámetro de L se le asocia el tipo del parámetro correspondiente en D, el cuerpo de L es una expresión válida que se aceptaría como una sentencia-expresión (§8.6) |
| • | Si el tipo de retorno de D es void y el cuerpo de L es un bloque de sentencias, cuando a cada parámetro de L se le asocia el tipo del parámetro correspondiente en D, el cuerpo de L es un bloque de sentencias válido en el que ninguna sentencia return especifica una expresión |
| • | Si D tiene un tipo de retorno distinto de void y el cuerpo de L es una expresión, cuando a cada parámetro de L se le asocia el tipo del parámetro correspondiente en D, el cuerpo de L es una expresión válida que implícitamente convertible al tipo de retorno de D |
| • | Si D tiene un tipo de retorno distinto de void y el cuerpo de L es un bloque de sentencias, cuando a cada parámetro de L se le asocia el tipo del parámetro correspondiente en D, el cuerpo de L es un bloque de sentencias válido con un punto final no alcanzable en el que cada sentencia return especifica una expresión que es implícitamente convertible al tipo de retorno de D |
Los ejemplos que siguen utilizan un tipo delegado genérico Func<A,R>, que representa una función que recibe argumento de tipo A y devuelve un valor de tipo R:
delegate R Func<A,R>(A arg);
En las asignaciones
Func<int,int> f1 = x => x + 1;// Ok Func<int,double> f2 = x => x + 1;// Ok Func<double,int> f3 = x => x + 1;// Error
los parámetros y tipos de retorno de cada expresión lambda se determinan a partir del tipo de la variable a la que la expresión lambda es asignada. La primera asignación convierte con éxito la expresión lambda al tipo delegado Func<int,int>, porque cuando a x se le asocia el tipo int, x + 1 es una expresión válida que es implícitamente convertible al tipo int. Del mismo modo, la segunda asignación convierte con éxito la expresión lambda al tipo delegado Func<int,double>, porque el resultado de x + 1 (de tipo int) es implícitamente convertible al tipo double. Sin embargo, la tercera asignación produce un error de compilación, porque cuando a x se le asocia el tipo double, el resultado de x + 1 (de tipo double) no es implícitamente convertible al tipo int.
26.3.2 Inferencia de tipos
Cuando un método genérico es llamado sin especificar argumentos de tipos, un proceso de inferencia de tipos intenta deducir los argumentos de tipos para la llamada. Las expresiones lambda que se pasan como argumentos al método genérico participan en este proceso de inferencia de tipos.
Como se describe en §20.6.4, la inferencia de tipos ocurre inicialmente de manera independiente para cada argumento. En esta fase inicial, no se deduce nada a partir de los argumentos que son expresiones lambda. Sin embargo, después de la fase inicial se realizan inferencias (deducciones) adicionales a partir de las expresiones lambda utilizando un proceso iterativo. Específicamente, se realizan inferencias mientras exista uno o más argumentos para los que todas las siguientes condiciones sean verdaderas:
| • | El argumento es una expresión lambda, en lo adelante llamada L, a partir de la que no se ha realizado inferencia alguna todavía |
| • | El tipo del parámetro correspondiente, en lo adelante llamado P, es un tipo delegado con un tipo de retorno que involucra a uno o más parámetros de tipo del método |
| • | P y L tienen la misma cantidad de parámetros, y cada parámetro de P tiene los mismos modificadores que el parámetro correspondiente de L, o ningún modificador si L tiene una lista de parámetros tipados implícitamente |
| • | Los tipos de parámetros de P no involucran parámetros de tipo de método o involucran solamente parámetros de tipo de método para los que ya se ha realizado un conjunto consistente de inferencias |
| • | Si L tiene una lista de parámetros explícitamente tipados, cuando se sustituyen los parámetros de tipo del método en P por los tipos inferidos, cada parámetro de P tiene el mismo tipo que el correspondiente parámetro de L |
| • | Si L tiene una lista de parámetros tipados implícitamente, cuando se sustituyen los parámetros de tipo del método en P por los tipos inferidos y los tipos de parámetros resultantes se asignan a los parámetros de L, el cuerpo de L es una expresión o bloque de sentencias válido |
| • | Un tipo de retorno puede ser inferido para L, como se describe más abajo |
Para cada uno de esos argumentos, se realizan inferencias sobre ese argumento relacionando el tipo de retorno de P con el tipo de retorno inferido de L, y las nuevas inferencias se añaden al conjunto de inferencias acumuladas. Este proceso se repite hasta que no puedan ser hechas más inferencias.
Para propósitos de inferencia de tipos y resolución de sobrecarga, el tipo de retorno inferido de una expresión lambda L se determina de la siguiente forma:
| • | Si el cuerpo de L es una expresión, el tipo de esa expresión es el tipo de retorno inferido de L |
| • | Si el cuerpo de L es un bloque de sentencias, si el conjunto formado por los tipos de las expresiones presentes en las sentencias return del bloque contiene exactamente un tipo al que cada tipo en el conjunto es implícitamente convertible, y si ese tipo no es el tipo nulo, entonces ese tipo es el tipo de retorno inferido de L |
| • | n cualquier otro caso, no es posible inferir un tipo de retorno para L |
Como ejemplo de inferencia de tipos relacionada con las expresiones lambda, considere el método extensor Select declarado en la clase System.Query.Sequence:
namespace System.Query
{
public static class Sequence
{
public static IEnumerable<S> Select<T,S>(
this IEnumerable<T> source,
Func<T,S> selector)
{
foreach (T elemento in source) yield return
selector(elemento);
}
}
}
Asumiendo que el espacio de nombres System.Query ha sido importado mediante una cláusula using, y dada una clase Customer que tiene una propiedad Name de tipo string, el método Select puede ser utilizado para seleccionar los nombres de una lista de objetos de la clase:
List<Customer> customers = GetCustomerList(); IEnumerable<string> names = customers.Select(c => c.Name);
La llamada a método extensor (§26.2.3) de Select es procesada reescribiendo la llamada como una llamada a un método estático:
IEnumerable<string> names = Sequence.Select(customers, c => c.Name);
Como los argumentos de tipo no han sido especificados explícitamente, se hará uso de la inferencia de tipos para inferir los argumentos de tipo. En primer lugar, el argumento customers es asociado al parámetro source, infiriéndose que T debe ser Customer. A continuación, utilizando el proceso de inferencia de tipos para expresiones lambda descrito anteriormente, a c se le asocia el tipo Customer, y la expresión c.Name es asociada al tipo de retorno del parámetro selector, infiriéndose que S es string. Por lo tanto, la llamada es equivalente a
Sequence.Select<Customer,string>(customers, (Customer c) => c.Name)
Y el resultado es de tipo IEnumerable<string>.
El siguiente ejemplo muestra cómo la inferencia de tipos para expresiones lambda permite que la información de tipos “fluya” entre argumentos en una llamada a un método genérico. Dado el método
static Z F<X,Y,Z>(X value, Func<X,Y> f1, Func<Y,Z> f2) {
return f2(f1(value));
}
La inferencia de tipos para la llamada
double seconds = F("1:15:30", s => TimeSpan.Parse(s), t =>
t.TotalSeconds);
se produce de la siguiente forma: en primer lugar, el argumento "1:15:30" es asociado al parámetro value, infiriéndose que X es string. A continuación, al parámetro de la primera expresión lambda, s, se le asocia el tipo inferido string, y la expresión TimeSpan.Parse(s) es asociada al tipo de retorno de f1, infiriéndose que Y es System.TimeSpan. Finalmente, al parámetro de la segunda expresión lambda, t, se le asocia el tipo inferido System.TimeSpan, y la expresión t.TotalSeconds es asociada al tipo de retorno de f2, infiriéndose que Z es double. Por lo tanto, el resultado de la llamada es de tipo double.
26.3.3 Resolución de sobrecarga
La presencia de expresiones lambda en una lista de argumentos afecta a la resolución de sobrecarga en ciertas situaciones
La siguiente regla aumenta a §7.4.2.3: Dada una expresión lambda L para la que existe un tipo de retorno inferido (§26.3.2), una conversión implícita de L a un tipo delegado D1 es una mejor conversión que una conversión implícita de L a un tipo delegado D2 si D1 y D2 tienen listas de parámetros idénticas y la conversión implícita del tipo de retorno inferido de L al tipo de retorno de D1 es una mejor conversión que la conversión implícita del tipo de retorno inferido de L al tipo de retorno de D2. Si tales condiciones no son ciertas, ninguna conversión es mejor que la otra.
El siguiente ejemplo ilustra el efecto de esta regla.
class ItemList<T>: List<T>
{
public int Sum<T>(Func<T,int> selector) {
int sum = 0;
foreach (T item in this) sum += selector(item);
return sum;
}
public double Sum<T>(Func<T,double> selector) {
double sum = 0;
foreach (T item in this) sum += selector(item);
return sum;
}
}
La clase ItemList<T> tiene dos métodos Sum. Cada uno recibe un argumento selector, que extrae el valor a sumar de un elemento de la lista. El valor extraído puede ser int o double, y del mismo modo la suma resultante es int o double.
Los métodos Sum podrían por ejemplo ser utilizados para calcular sumas de líneas de detalle en un pedido.
class Detail
{
public int UnitCount;
public double UnitPrice;
...
}
void ComputeSums() {
ItemList<Detail> orderDetails = GetOrderDetails(...);
int totalUnits = orderDetails.Sum(d => d.UnitCount);
double orderTotal = orderDetails.Sum(d => d.UnitPrice *
d.UnitCount);
...
}
En la primera llamada a orderDetails.Sum, ambos métodos Sum son aplicables porque la expresión lambda d => d.UnitCount es compatible tanto con Func<Detail,int> como con Func<Detail,double>. No obstante, la resolución de sobrecarga elegirá el primer método Sum, porque la conversión a Func<Detail,int> es mejor que la conversión a Func<Detail,double>.
En la segunda llamada a orderDetails.Sum, sólo el segundo método Sum es aplicable porque la expresión lambda d => d.UnitPrice * d.UnitCount produce un valor de tipo double. Por lo tanto, la resolución de sobrecarga elegirá el segundo método Sum para esa llamada.
Una expresión de creación de objeto (§7.5.10.1) puede incluir un inicializador de objeto o colección que inicialice los miembros del objeto recién creado o los elementos de la colección recién creada.
expresión-de-creación-de-objeto: new tipo ( lista-de-argumentosopc ) inicializador-de-objeto-o-colecciónopc new tipo inicializador-de-objeto-o-colección inicializador-de-objeto-o-colección: inicializador-de-objeto inicializador-de-colección
Una expresión de creación de objeto puede omitir la lista de argumentos del constructor y los paréntesis que encierran a éstos en caso de que incluya un inicializador de objeto o colección. La omisión de la lista de argumentos del constructor y de los paréntesis correspondientes es equivalente a especificar una lista de argumentos vacía
La ejecución de una expresión de creación de objeto que incluye un inicializador de objeto o colección consiste en invocar inicialmente al constructor de instancia y luego ejecutar las inicializaciones de miembros o elementos especificadas por el inicializador de objeto o colección
No es posible hacer referencia a la instancia que está siendo inicializada desde un inicializador de objeto o colección
26.1.1 Inicializadores de objetos
Un inicializador de objeto especifica los valores para uno o más campos o propiedades de un objeto.
inicializador-de-objeto:
{ lista-de-inicializadores-de-miembrosopc }
{ lista-de-inicializadores-de-miembros , }
lista-de-inicializadores-de-miembros:
inicializador-de-miembro
lista-de-inicializadores-de-miembros ,
inicializador-de-miembro
inicializador-de-miembro:
identificador = valor-inicializador
valor-inicializador:
expresión
inicializador-de-objeto-o-colección
Un inicializador de objeto consiste en una secuencia de inicializadores de miembros, encerrada entre lexemas { y } y con los miembros separados por comas. Cada inicializador de miembro debe nombrar un campo o propiedad accesible del objeto que se está inicializando, seguido de un signo de igualdad y una expresión o inicializador de objeto o colección. Es un error que un inicializador de objeto incluya más de un inicializador de miembro para un mismo campo o propiedad.
Un inicializador de miembro que especifica una expresión detrás del signo de igualdad se procesa de la misma manera que una asignación (§7.13.1) al campo o propiedad.
Un inicializador de miembro que especifica un inicializador de objeto detrás del signo de igualdad es una inicialización de un objeto embebido. En vez de asignar un nuevo valor al campo o propiedad, las asignaciones en el inicializador de objeto son tratadas como asignaciones a miembros del campo o propiedad. Una propiedad de un tipo valor no puede ser inicializada utilizando esta construcción.
Un inicializador de miembro que especifica un inicializador de colección detrás del signo de igualdad es una inicialización de una colección embebida. En vez de asignar una nueva colección al campo o propiedad, los elementos especificados en el inicializador son añadidos a la colección referenciada por el campo o propiedad. El campo o propiedad debe ser de un tipo de colección que satisfaga los requisitos especificados en §CNDJ6nn5us4RjIIAqgBLqQsCAAAACAAAAA4AAABfAFIAZQBmADEAMQAyADcAMQA3ADUANAA1AAAA 26.4.2.
La siguiente clase representa un punto con dos coordenadas:
public class Point
{
int x, y;
public int X { get { return x; } set { x = value; } }
public int Y { get { return y; } set { y = value; } }
}
Se puede crear e inicializar una instancia de Point de la siguiente forma:
var a = new Point { X = 0, Y = 1 };
que produce el mismo efecto que
var a = new Point(); a.X = 0; a.Y = 1;
La siguiente clase representa un rectángulo creado a partir de dos puntos:
public class Rectangle
{
Point p1, p2;
public Point P1 { get { return p1; } set { p1 = value; } }
public Point P2 { get { return p2; } set { p2 = value; } }
}
Se puede crear e inicializar una instancia de Rectangle de la siguiente forma:
var r = new Rectangle {
P1 = new Point { X = 0, Y = 1 },
P2 = new Point { X = 2, Y = 3 }
};
que produce el mismo efecto que
var r = new Rectangle(); var __p1 = new Point(); __p1.X = 0; __p1.Y = 1; r.P1 = __p1; var __p2 = new Point(); __p2.X = 2; __p2.Y = 3; r.P2 = __p2;
donde __p1 y __p2 son variables temporales de otro modo invisibles e inaccesibles
Si el constructor de Rectangle reservara la memoria para las dos instancias de Point embebidas
public class Rectangle
{
Point p1 = new Point();
Point p2 = new Point();
public Point P1 { get { return p1; } }
public Point P2 { get { return p2; } }
}
se podría utilizar la siguiente construcción para inicializar las instancias de Point embebidas en vez de asignar nuevas instancias:
var r = new Rectangle {
P1 = { X = 0, Y = 1 },
P2 = { X = 2, Y = 3 }
};
que produce el mismo efecto que
var r = new Rectangle(); r.P1.X = 0; r.P1.Y = 1; r.P2.X = 2; r.P2.Y = 3;
Inicializadores de colecciones
Un inicializador de colección especifica los elementos de una colección
inicializador-de-colección:
{ lista-de-inicializadores-de-elementosopc }
{ lista-de-inicializadores-de-elementos , }
lista-de-inicializadores-de-elementos:
inicializador-de-elemento
lista-de-inicializadores-de-elementos , inicializador-de-elemento
inicializador-de-elemento:
expresión-no-de-asignación
Un inicializador de colección consiste en una secuencia de inicializadores de elementos, encerrada entre lexemas { y } y con los elementos separados por comas. Cada inicializador de elemento especifica un elemento a añadir a la colección que se está inicializando. Para evitar la ambigüedad con los inicializadores de miembros, los inicializadores de elementos no pueden ser expresiones de asignación. La regla de producción expresión-no-de-asignación se define en §26.3
El siguiente es un ejemplo de una expresión de creación de objeto que incluye un inicializador de colección:
List<int> digits = new List<int> { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
El objeto de colección al que se aplica un inicializador de colección debe ser de un tipo que implemente System.Collections.Generic.ICollection<T> para exactamente un tipo T. Adicionalmente, debe existir una conversión implícita (§6.1) del tipo de cada inicializador de elemento a T. Si esos requisitos no se satisfacen, se produce un error de compilación. Un inicializador de colección llama secuencialmente al método ICollection<T>.Add(T) para cada elemento especificado.
La siguiente clase representa un contacto, con un nombre y una lista de teléfonos:
public class Contact
{
string name;
List<string> phoneNumbers = new List<string>();
public string Name { get { return name; } set { name =
value; } }
public List<string> PhoneNumbers { get { return
phoneNumbers; } }
}
Una colección List<Contact> puede ser creada e inicializada de la siguiente forma:
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 produce el mismo efecto que
var contacts = new List<Contact>();
var __c1 = new Contact();
__c1.Name = "Chris Smith";
__c1.PhoneNumbers.Add("206-555-0101");
__c1.PhoneNumbers.Add("425-882-8080");
contacts.Add(__c1);
var __c2 = new Contact();
__c2.Name = "Bob Harris";
__c2.PhoneNumbers.Add("650-555-0199");
contacts.Add(__c2);
donde __c1 y __c2 son variables temporales de otro modo invisibles e inaccesibles
C# 3.0 permite que el operador new sea utilizado conjuntamente con un inicializador de objeto anónimo para crear un objeto de un tipo anónimo
expresión-de-creación-primaria-no-array:
…
expresión-anónima-de-creación-de-objeto
expresión-anónima-de-creación-de-objeto:
new inicializador-de-objeto-anónimo
inicializador-de-objeto-anónimo:
{ lista-de-declaradores-de-miembrosopc }
{ lista-de-declaradores-de-miembros , }
lista-de-declaradores-de-miembros:
declarador-de-miembro
lista-de-declaradores-de-miembros ,
declarador-de-miembro
declarador-de-miembro:
nombre-simple
acceso-a-miembro
identificador = expresión
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 lectura/escritura inferidas del inicializador o inicializadores de objeto utilizados para crear instancias del tipo. Específicamente, un inicializador de objeto anónimo de la forma
new { p1 = e1 , p2 = e2 , … pn = en }
declara un tipo anónimo de la forma
class __Anonymous1
{
private T1 f1 ;
private T2 f2 ;
…
private Tn fn ;
public T1 p1 { get { return f1 ; } set { f1 = value ; } }
public T2 p2 { get { return f2 ; } set { f2 = value ; } }
…
public T1 p1 { get { return f1 ; } set { f1 = value ; } }
}
donde cada Tx es el tipo de la expresión correspondiente ex. Es un error de compilación el que una expresión en un inicializador de objeto anónimo sea del tipo nulo.
El nombre de un tipo anónimo es generado automáticamente por el compilador y no puede ser referenciado en el texto del programa.
Dentro del mismo programa, dos inicializadores de objetos anónimos que especifiquen una secuencia de propiedades de los mismos nombres y tipos en el mismo orden producirán instancias del mismo tipo anónimo. (Esta definición incluye el orden de las propiedades porque es algo observable y material en ciertas circunstancias, tales como la reflexión).
En el ejemplo
var p1 = new { Name = "Lawnmower", Price = 495.00 };
var p2 = new { Name = "Shovel", Price = 26.95 };
p1 = p2;
la asignación de la última línea es válida porque p1 y p2 son del mismo tipo anónimo.
Un declarador de miembro puede ser abreviado a un nombre simple (§7.5.2) o a un acceso a miembro (§7.5.4). Esto se conoce como inicializador de proyección y es una notación más compacta para una declaración y una asignación de valor a una misma propiedad. Específicamente, los declaradores de miembros de las formas
identificadorexpr . identificador
son equivalentes a las siguientes, respectivamente:
identificador = identificadoridentificador = expr . identificador
De esta manera, en un inicializador de proyección el identificador selecciona tanto el valor como el campo o propiedad a la que el valor es asignado. Intuitivamente, un inicializador de proyección proyecta no sólo un valor, sino también el nombre del valor
La sintaxis de las expresiones de creación de array (§7.5.10.2) ha sido extendida para soportar las expresiones de creación de arrays de tipo implícito:
expresión-de-creación-de-array: … new [ ] inicializador-de-array
En una expresión de creación de array de tipo implícito, el tipo de la instancia de array es inferido de los elementos especificados en el inicializador del array. Específicamente, el conjunto formado por los tipos de las expresiones presentes en el inicializador del array debe contener exactamente un tipo al que cada uno de los tipos del conjunto es implícitamente convertible, y si ese tipo no es el tipo nulo, se creará un array de ese tipo. Si no se puede inferir exactamente un tipo, o si el tipo inferido es el tipo nulo, se produce un error de compilación
Los siguientes son ejemplos de expresiones de creación de array de tipo 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 de compilación porque ni int ni string son implícitamente convertibles el uno al otro. En este caso, se deberá utilizar una expresión de creación de array explícitamente tipado, por ejemplo especificando que el tipo sea object[]. De manera alternativa, uno de los elementos puede ser convertido a un tipo base común, que se convertiría entonces en el tipo inferido para los elementos.
Las expresiones de creación de arrays de tipo implícito pueden ser combinadas con los inicializadores de objetos anónimos para crear estructuras de datos anónimamente tipadas. Por 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" }
}
};
Las expresiones de consulta proveen una sintaxis integrada en el lenguaje para consultas que es similar a los lenguajes de consulta relacionales y jerárquicos como SQL y XQuery.
expresión-de-consulta: cláusula-from cuerpo-de-consulta cláusula-from: from tipoopc identificador in expresión cláusulas- joinopc cláusulas-join: cláusula-join cláusulas-join cláusula-join cláusula-join: join tipoopc identificador in expresión on expresión equals expresión join tipoopc identificador in expresión on expresión equals expresión into identificador cuerpo-de-consulta: cláusulas-from-let-whereopc cláusula-orderbyopc cláusula-select-o-group continuación-consultaopc cláusulas-from-let-where: cláusula-from-let-where cláusulas-from-let-where cláusula-from-let-o-where cláusula-from-let-o-where: cláusula-from cláusula-let cláusula-where cláusula-let: let identificador = expresión cláusula-where: where expresión-booleana cláusula-orderby: orderby cláusulas-de-ordenación cláusulas-de-ordenación: cláusula-de-ordenación cláusulas-de-ordenación , cláusula-de-ordenación cláusula-de-ordenación: expresión dirección-de-ordenaciónopc dirección-de-ordenación: ascending descending cláusula-select-o-group: cláusula-select cláusula-group cláusula-select: select expresión cláusula-group: group expresión by expresión continuación-consulta: into identificador cláusulas-joinopt cuerpo-de- consulta
Una expresión-de-consulta se clasifica como una expresión-no-de-asignación, cuya definición se da en §26.3.
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 cláusulas from, let o where. Cada cláusula from es un generador que introduce una o más variables de iteración sobre una secuencia o un encuentro de múltiples secuencias, cada cláusula let calcula un valor e introduce un identificador que representa a ese valor, y cada cláusula where es un filtro que excluye elementos del resultado. La cláusula select o group final especifica la forma del resultado en términos de la(s) variable(s) de iteración. La cláusula select o group puede ir precedida de una cláusula orderby que especifica un criterio de ordenación para el resultado. Finalmente, una cláusula into puede ser utilizada para “empalmar” consultas, tratando el resultado de una consulta como generador para una consulta subsiguiente
26.7.1 Traducción de expresiones de consulta
El lenguaje C# 3.0 no especifica la semántica exacta de ejecución de las expresiones de consulta. En vez de eso, C# 3.0 traduce las expresiones de consulta en llamadas a métodos que se adhieren al patrón de expresiones de consulta. Específicamente, las expresiones de consulta son traducidas en llamadas a métodos llamados Where, Select, SelectMany, Join, GroupJoin, OrderBy, OrderByDescending, ThenBy, ThenByDescending, GroupBy y Cast, que deben tener unas firmas y tipos de resultado específicos, como se describe en §26.7.2. Estos métodos pueden ser métodos de instancia del objeto consultado o métodos extensores externos al objeto, y son los que implementan la ejecución real de la consulta.
La traducción de las expresiones de consulta en llamadas a método es un mapeado sintáctico que se produce antes de que cualquier asociación de tipos o resolución de sobrecarga haya sido efectuada. Se garantiza que la traducción es sintácticamente correcta, pero no que produzca código C# semánticamente correcto. Después de la traducción de las expresiones de consulta, las llamadas a método resultantes son procesadas como llamadas a método normales, y esto puede a su vez descubrir errores, por ejemplo si los métodos no existen, si los argumentos tienen tipos erróneos, o si los métodos son genéricos y la inferencia de tipos fracasa.
Una expresión de consulta se procesa aplicando repetidamente las siguientes traducciones hasta que no sea posible realizar más reducciones. Las traducciones se listan en orden de precedencia: cada sección asume que las traducciones de las secciones anteriores han sido realizadas de forma exhaustiva.
Ciertas traducciones inyectan variables de iteración mediante identificadores transparentes denotados mediante *. Las propiedades especiales de los identificadores transparentes se discuten en §26.7.1.9.
26.7.1.1 Cláusulas select y groupby con continuaciones
Una expresión de consulta con una continuación
from … into x …
se traduce en
from x in ( from … ) …
Las traducciones de las siguientes secciones asumen que las consultas no tienen continuaciones into.
El ejemplo
from c in customers
group c by c.Country into g
select new { Country = g.Key, CustCount = g.Count() }
se traduce en
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() })
26.7.1.2 Tipos de variable de iteración explícitos
El compilador de C# 3.0 incluido en el CTP de Mayo de 2006 no soporta las variables de iteración explícitamente tipadas. El tipado explícito se puede obtener llamando manualmente al operador Cast<T>.
Una cláusula from que especifica explícitamente el tipo de una variable de iteración
from T x in e
se traduce en
from x in ( e ) . Cast < T > ( )
Una cláusula join que especifica explícitamente el tipo de una variable de iteración
join T x in e on k1 equals k2
se traduce en
join x in ( e ) . Cast < T > ( ) on k1 equals k2
Las traducciones de las siguientes secciones asumen que las consultas no tienen tipos de variable de iteración explícitos.
El ejemplo
from Customer c in customers where c.City == "London" select c
se traduce en
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")
Los tipos de variable de iteración explícitos son útiles para consultar colecciones que implementan la interfaz no genérica IEnumerable, pero no la interfaz genérica IEnumerable<T>. En el ejemplo anterior, éste sería el caso si customers fuera de tipo ArrayList.
26.7.1.3 Cláusulas join
Una cláusula from seguida de una cláusula join sin into seguida de una cláusula select
from x1 in e1 join x2 in e2 on k1 equals k2 select v
se traduce en
from t in ( e1 ) . Join ( e2 , x1 => k1 , x2 => k2 , ( x1 , x2 ) => v ) select t
donde t es un identificador único generado por el compilador.
Una cláusula from seguida de una cláusula join con into seguida de una cláusula select
from x1 in e1 join x2 in e2 on k1 equals k2 into g select v
se traduce en
from t in ( e1 ) . GroupJoin ( e2 , x1 => k1 , x2 => k2 , ( x1 , g ) => v ) select t
donde t es un identificador único generado por el compilador.
Una cláusula from seguida de una cláusula join sin into seguida de algo que no sea una cláusula select
from x1 in e1 join x2 in e2 on k1 equals k2
se traduce en
from * in (
from x1 in e1 join x2 in e2 on k1 equals k2 select
new { x1 , x2 }
)
Una cláusula from seguida de una cláusula join con into seguida de algo que no sea una cláusula select
from x1 in e1 join x2 in e2 on k1 equals k2 into g
se traduce en
from * in (
from x1 in e1 join x2 in e2 on k1 equals k2 into g
select new { x1 , g }
)
Las traducciones de las siguientes secciones asumen que las consultas no tienen cláusulas join.
El ejemplo
from c in customers
join o in orders on c.CustomerID equals o.CustomerID
select new { c.Name, o.OrderDate, o.Total }
tiene la traducción final
customers.Join(orders, c => c.CustomerID, o =>
o.CustomerID,
(c, o) => new { c.Name, o.OrderDate, o.Total })
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 en
from * in
from c in customers
join o in orders on c.CustomerID equals o.CustomerID into
co
select 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 e y son identificadores generados por el compilador que son de otro modo invisibles e inaccesibles.
26.7.1.4 Cláusulas let y where
Una cláusula from seguida inmediatamente por una cláusula let
from x in e let y = f
se traduce en
from * in (
from x in e select new { x , y = f }
)
Una cláusula from seguida inmediatamente por una cláusula where
from x in e where f
se traduce en
from x in ( e ) . Where ( x => f )
Las traducciones de las siguientes secciones asumen que las consultas no tienen cláusulas let o where
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 en
from * in
from o in orders
select new { o, t = o.Details.Sum(d => d.UnitPrice * d.Quantity) }
where t >= 1000
select new { o.OrderID, Total = t }
cuya traducción 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 es de otro modo invisible e inaccesible.
26.7.1.5 Múltiples generadores
Dos cláusulas from seguidas de una cláusula select
from x1 in e1 from x2 in e2 select v
se traduce en
from t in ( e1 ) . SelectMany ( x1 => from x2 in e2 select v ) select t
donde t es un identificador único generado por el compilador.
Dos o más cláusulas from seguidas de algo que no sea una cláusula select
from x1 in e1 from x2 in e2 …
se traduce en
from * in (
from x1 in e1 from x2 in e2 … select new { x1 , x2 … }
)
Las traducciones de las siguientes secciones asumen que las consultas tienen una única cláusula from.
El ejemplo
from c in customers
from o in c.Orders
select new { c.Name, o.OrderID, o.Total }
se traduce en
from t in
customers.SelectMany(c =>
from o in c.Orders
select new { c.Name, o.OrderID, o.Total }
)
select t
cuya traducción final es
customers.
SelectMany(c =>
c.Orders.
Select(o => new { c.Name, o.OrderID, o.Total })
)
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 en
from * in
from c in customers
from o in c.Orders
select 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.
Select(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 es de otro modo invisible e inaccesible.
26.7.1.6 Cláusulas orderby
Una expresión de consulta con una cláusula orderby
from x in e orderby k1 , k2 …
se traduce en
from x in ( e ) . OrderBy ( x => k1 ) . ThenBy ( x => k2 ) …
Si una cláusula de ordenación especifica un indicador de dirección descending, se produce en vez de lo anterior una llamada a OrderByDescending o ThenByDescending.
Las traducciones de las siguientes secciones asumen que las consultas no tienen ninguna cláusula orderby.
El ejemplo
from o in orders orderby o.Customer.Name, o.Total descending select o
tiene la traducción final
orders. OrderBy(o => o.Customer.Name). ThenByDescending(o => o.Total)
26.7.1.7 Cláusulas select
Una expresión de consulta de la forma
from x in e select v
se traduce en
( e ) . Select ( x => v )
excepto cuando v es el identificador x, cuando la traducción es simplemente
( e )
Por ejemplo
from c in customers select c
se traduce simplemente en
customers
26.7.1.8 Cláusulas groupby
Una expresión de consulta de la forma
from x in e group v by k
se traduce en
( e ) . GroupBy ( x => k , x => v )
excepto cuando v es el identificador x, cuando la traducción es
( e ) . GroupBy ( x => k )
El ejemplo
from c in customers group c.Name by c.Country
se traduce en
customers. GroupBy(c => c.Country, c => c.Name)
26.7.1.9 Identificadores transparentes
Ciertas traducciones inyectan variables de iteración mediante identificadores transparentes denotados mediante *. Los identificadores transparentes no son una característica del lenguaje; existen solo como un paso intermedio en el proceso de traducción de expresiones de consulta
Cuando la traducción de una consulta inyecta un identificador transparente, pasos de traducción adicionales propagan el identificador transparente a las expresiones lambda y los inicializadores de objetos anónimos. En esos contextos, los identificadores transparentes tienen el siguiente comportamiento:
| • | Cuando un identificador transparente ocurre como parámetro de una expresión lambda, los miembros del tipo anónimo asociado están automáticamente en ámbito dentro del cuerpo de la expresión lambda. |
| • | Cuando un miembro de un identificador transparente está en ámbito, sus miembros están en ámbito también. |
| • | Cuando un identificador transparente ocurre como declarador de miembro en un inicializador de objeto anónimo, ello introduce un miembro con un identificador transparente. |
El ejemplo
from c in customers
from o in c.Orders
orderby o.Total descending
select new { c.Name, o.Total }
se traduce en
from * in
from c in customers
from o in c.Orders
select new { c, o }
orderby o.Total descending
select new { c.Name, o.Total }
que más adelante se traduce en
customers.
SelectMany(c => c.Orders.Select(o => new { c, o })).
OrderByDescending(* => o.Total).
Select(* => new { c.Name, o.Total })
que, una vez que los identificadores transparentes son eliminados, es equivalente a
customers.
SelectMany(c => c.Orders.Select(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 es de otro modo invisible e inaccesible.
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 en
from * in
from * in
from * in
from c in customers
join o in orders o c.CustomerID equals o.CustomerID
select new { c, o }
join d in details on o.OrderID equals d.OrderID
select new { *, d }
join p in products on d.ProductID equals p.ProductID
select new { *, p }
select new { c.Name, o.OrderDate, p.ProductName }
que más adelante se traduce en
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 { *, p }).
Select(* => 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, p }).
Select(z => new { z.y.x.c.Name, z.y.x.o.OrderDate, z.p.ProductName })
donde x, y y z son identificadores generados por el compilador de otro modo invisibles e inaccesibles.
26.7.2 El patrón de expresiones de consulta
El patrón de expresiones de consulta establece un patrón de métodos que los tipos pueden implementar para dar soporte a las expresiones de consulta. Debido a que las expresiones de consulta se traducen en llamadas a métodos por medio de un mapeado sintáctico, los tipos tienen una flexibilidad considerable a la hora de implementar el patrón de expresiones de consulta. Por ejemplo, los métodos del patrón pueden ser implementados como métodos de instancia o como métodos extensores ya que ambos comparten la misma sintaxis de llamada, y los métodos pueden solicitar delegados o árboles de expresiones ya que las expresiones lambda son convertibles a ambos.
La estructura recomendada para un tipo genérico C<T> que dé soporte al patrón de expresiones de consulta se muestra más abajo. Se utiliza un tipo genérico para ilustrar las relaciones apropiadas entre los tipos de los parámetros y resultados, pero es posible implementar el patrón para tipos no genéricos también.
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>
{
public C<T> Where(Func<T,bool> predicate);
public C<U> Select<U>(Func<T,U> selector);
public C<U> SelectMany<U>(Func<T,C<U> selector);
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 delegados genéricos Func<A, R> y Func<T1, T2, R>, pero podrían igualmente haber utilizado otros tipos delegados o de árboles de expresión con las mismas relaciones entre los tipos de parámetros y resultados.
Observe la relación recomendada entre C<T> y O<T>, que garantiza que los métodos ThenBy y ThenByDescending están disponibles sólo para el resultado de OrderBy u OrderByDescending. Observe además la forma recomendada del resultado de GroupBy — una secuencia de secuencias en la que cada secuencia interna tiene una propiedad Key adicional.
Los operadores de consulta estándar (que se describen en una especificación independiente) ofrecen una implementación del patrón de expresiones de consulta para cualquier tipo que implemente la interfaz System.Collections.Generic.IEnumerable<T>.
Los árboles de expresiones permiten representar las expresiones lambda como estructuras de datos en vez de código ejecutable. Una expresión lambda que es convertible a un tipo delegado D es también convertible a un árbol de expresión de tipo System.Query.Expression<D>. A diferencia de la conversión de una expresión lambda a un tipo delegado, que hace que se genere código ejecutable y sea referenciado por un delegado, la conversión a un tipo de árbol de expresión provoca la emisión del código que crea una instancia de un árbol de expresión. Los árboles de expresiones son representaciones eficientes en forma de datos en memoria de las expresiones lambda, y hacen la estructura de la expresión transparente y explícita.
El siguiente ejemplo representa una expresión lambda como código ejecutable y como un árbol de expresión. Debido a que existe una conversión a Func<int,int>, también existe una conversión a Expression<Func<int,int>.
Func<int,int> f = x => x + 1;// Código Expression<Func<int,int> e = x => x + 1;// Datos
Después de la ejecución de esas asignaciones, el delegado f hace referencia a un método que devuelve x + 1, y el árbol de expresión e hace referencia a una estructura de datos que describe la expresión x + 1.
La estructura de los árboles de expresiones se describe en una especificación independiente