2010-02-08 8 views
17

¿Es posible escribir el siguiente 'foreach' como una instrucción LINQ, y supongo que la pregunta más general puede reemplazar cualquier bucle for por una instrucción LINQ.¿Se pueden reemplazar todos los bucles 'for' por una instrucción LINQ?

No me interesa ningún costo de rendimiento potencial, solo el potencial de usar enfoques declarativos en lo que tradicionalmente es un código imperativo.

private static string SomeMethod() 
    { 
     if (ListOfResources .Count == 0) 
      return string.Empty; 

     var sb = new StringBuilder(); 
     foreach (var resource in ListOfResources) 
     { 
      if (sb.Length != 0) 
       sb.Append(", "); 

      sb.Append(resource.Id); 
     } 

     return sb.ToString(); 
    } 

Saludos

AWC

Respuesta

22

Sure. Heck, puede reemplazar la aritmética con las consultas LINQ:

http://blogs.msdn.com/ericlippert/archive/2009/12/07/query-transformations-are-syntactic.aspx

Pero no se debe.

El propósito de una expresión de consulta es representar una operación de consulta. El propósito de un bucle "para" es iterar sobre una declaración particular para que sus efectos secundarios se ejecuten varias veces. Esos son frecuentemente muy diferentes. Animo a reemplazar los bucles cuyo propósito es simplemente consultar datos con construcciones de mayor nivel que consultan los datos con mayor claridad. Recomiendo encarecidamente reemplazar el código de generación de efectos secundarios con comprensiones de consultas, aunque hacerlo es posible.

+17

no todo es un clavo ... – AwkwardCoder

+0

@Eric: ¿Ve algo mal con el uso de un método de extensión ForEach para ejecutar código de generación de efectos no secundarios, p. haciendo un WriteLn en cada resultado de la consulta? –

+2

@Tom: No estoy siguiendo tu línea de pensamiento aquí. Sin duda, escribir una línea es un efecto secundario, ¿no? –

2

Técnicamente, sí.

Cualquier foreach lazo se puede convertir a LINQ utilizando un método de extensión ForEach, como el de MoreLinq.

Si sólo desea utilizar "pura" LINQ (sólo los métodos de extensión incorporados), se puede abusar del método Aggregate extensión, de esta manera:

foreach(type item in collection { statements } 

type item; 
collection.Aggregate(true, (j, itemTemp) => { 
    item = itemTemp; 
    statements 
    return true; 
); 

Esto manejar correctamente cualquier foreach loop, incluso la respuesta de JaredPar. EDITAR: A menos que utilice los parámetros ref/out, código inseguro o yield return.
¿No dare utiliza este truco en el código real.


En su caso específico, se debe utilizar un método de extensión de cadena Join, como este:

///<summary>Appends a list of strings to a StringBuilder, separated by a separator string.</summary> 
///<param name="builder">The StringBuilder to append to.</param> 
///<param name="strings">The strings to append.</param> 
///<param name="separator">A string to append between the strings.</param> 
public static StringBuilder AppendJoin(this StringBuilder builder, IEnumerable<string> strings, string separator) { 
    if (builder == null) throw new ArgumentNullException("builder"); 
    if (strings == null) throw new ArgumentNullException("strings"); 
    if (separator == null) throw new ArgumentNullException("separator"); 

    bool first = true; 

    foreach (var str in strings) { 
     if (first) 
      first = false; 
     else 
      builder.Append(separator); 

     builder.Append(str); 
    } 

    return builder; 
} 

///<summary>Combines a collection of strings into a single string.</summary> 
public static string Join<T>(this IEnumerable<T> strings, string separator, Func<T, string> selector) { return strings.Select(selector).Join(separator); } 
///<summary>Combines a collection of strings into a single string.</summary> 
public static string Join(this IEnumerable<string> strings, string separator) { return new StringBuilder().AppendJoin(strings, separator).ToString(); } 
+0

No hay una sola consulta LINQ en el código por lo que no contesta verdadera la cuestión es que ? Language Integrated Query no significa Extension methods :) se basa en los métodos de extensión, pero eso no hace que cada método de extensión forme parte de la sintaxis de Language Integrated Query –

+0

@Rune: estoy al tanto de eso. Sin embargo, el uso común, incluidas todas las demás respuestas aquí, se refiere a los métodos de extensión que siguen el estilo de LINQ. (por ejemplo, MoreLinq) – SLaks

13

En general sí, pero hay casos específicos que son extremadamente difíciles. Por ejemplo, el siguiente código en el caso general no se transfiere a una expresión LINQ sin una buena cantidad de pirateo.

var list = new List<Func<int>>(); 
foreach (var cur in (new int[] {1,2,3})) { 
    list.Add(() => cur); 
} 

La razón por la cual es que con un bucle, es posible ver los efectos secundarios de la forma en que la variable de iteración es capturado en un cierre. Las expresiones LINQ ocultan la semántica de duración de la variable de iteración y le impiden ver los efectos secundarios de capturar su valor.

Nota. El código anterior es no equivalente a la siguiente expresión LINQ.

var list = Enumerable.Range(1,3).Select(x =>() => x).ToList(); 

La muestra foreach produce una lista de Func<int> objetos que todo retorno 3. La versión LINQ produce una lista de Func<int> que devuelven 1,2 y 3 respectivamente. Esto es lo que hace que este estilo de captura sea difícil de transportar.

+1

@Sean, su ejemplo de contador produce un resultado diferente. Mi ejemplo creará una lista de 'Func ' que devolverá 3. La tuya devolverá 1,2 y 3. – JaredPar

+0

Sí, buen punto. Eliminado –

1

En general, se puede escribir una expresión lambda utilizando un delegado que representa el cuerpo de un ciclo foreach, en su caso, algo así como:

resource => { if (sb.Length != 0) sb.Append(", "); sb.Append(resource.Id); } 

y luego simplemente usar dentro de un método de extensión ParaCada.Si esta es una buena idea depende de la complejidad del cuerpo, en caso de que sea demasiado grande y compleja, probablemente no obtenga nada de ella, excepto una posible confusión;)

1

El ciclo específico en su pregunta se puede hacer declarativamente de esta manera:

var result = ListOfResources 
      .Select<Resource, string>(r => r.Id.ToString()) 
      .Aggregate<string, StringBuilder>(new StringBuilder(), (sb, s) => sb.Append(sb.Length > 0 ? ", " : String.Empty).Append(s)) 
      .ToString(); 

En cuanto al rendimiento, puede esperar una disminución del rendimiento, pero esto es aceptable para la mayoría de las aplicaciones.

2

Creo que lo que es más importante aquí es que, para evitar la confusión semántica, su código debe ser sólo superficialmente funcional cuando es realmente funcional. En otras palabras, no use efectos secundarios en expresiones LINQ.

+0

¿Está insinuando el hecho de que para cada elemento del ciclo cualquier declaración LINQ equivalente debería hacer exactamente lo mismo para todos los elementos ... – AwkwardCoder

+0

No, solo digo que una expresión LINQ no debe considerarse como "hacer "cualquier cosa, pero como" ser "algo, es decir, tener un valor. Si no puede encontrar una manera natural de pensarlo de esa manera, es mejor no formularlo como una expresión LINQ. Como sucede, el ejemplo específico que dio * does * tiene sentido como una expresión funcional, como Konrad mostró anteriormente. editar: er, espera, más o menos. Debería hacer "exactamente lo mismo" porque debería ser sin estado (no debería depender de enunciados que actualicen variables, etc.), si eso es lo que quería decir. –

3

De hecho, su código hace algo que es fundamentalmente muy funcional, es decir, se reduce una lista de cadenas a una sola cadena mediante la concatenación de los elementos de la lista. Lo único imperativo sobre el código es el uso de un StringBuilder.

El código funcional lo hace mucho más fácil, en realidad, porque no requiere un caso especial como lo hace su código. Mejor aún, .NET ya tiene esta operación en particular en práctica, y probablemente más eficiente que su código 1):

return String.Join(", ", ListOfResources.Select(s => s.Id.ToString()).ToArray()); 

(Sí, la llamada a ToArray() es molesto pero Join es un método muy antiguo y . es anterior LINQ)

Por supuesto, una versión “mejor” de Join podría utilizarse como esto:

return ListOfResources.Select(s => s.Id).Join(", "); 

La implementación es bastante sencilla, pero una vez más, usar el StringBuilder (para el rendimiento) lo hace imperativo.

public static String Join<T>(this IEnumerable<T> items, String delimiter) { 
    if (items == null) 
     throw new ArgumentNullException("items"); 
    if (delimiter == null) 
     throw new ArgumentNullException("delimiter"); 

    var strings = items.Select(item => item.ToString()).ToList(); 
    if (strings.Count == 0) 
     return string.Empty; 

    int length = strings.Sum(str => str.Length) + 
       delimiter.Length * (strings.Count - 1); 
    var result = new StringBuilder(length); 

    bool first = true; 

    foreach (string str in strings) { 
     if (first) 
      first = false; 
     else 
      result.Append(delimiter); 
     result.Append(str); 
    } 

    return result.ToString(); 
} 

1) Sin haber visto la puesta en práctica en el reflector, supongo que String.Join hace una primera pasada sobre las cuerdas para determinar la longitud total. Esto se puede utilizar para inicializar el StringBuilder en consecuencia, ahorrándose costosas operaciones de copia más adelante.

EDITAR por SLaks: Aquí está la fuente de referencia para la parte pertinente del String.Join de Net 3.5:

string jointString = FastAllocateString(jointLength); 
fixed (char * pointerToJointString = &jointString.m_firstChar) { 
    UnSafeCharBuffer charBuffer = new UnSafeCharBuffer(pointerToJointString, jointLength); 

    // Append the first string first and then append each following string prefixed by the separator. 
    charBuffer.AppendString(value[startIndex]); 
    for (int stringToJoinIndex = startIndex + 1; stringToJoinIndex <= endIndex; stringToJoinIndex++) { 
     charBuffer.AppendString(separator); 
     charBuffer.AppendString(value[stringToJoinIndex]); 
    } 
    BCLDebug.Assert(*(pointerToJointString + charBuffer.Length) == '\0', "String must be null-terminated!"); 
} 
+1

+1. La única forma sensata de manejar esta situación específica de una manera clara y concisa. – jeroenh

+2

En .NET4, 'String.Join' también toma un IEnumerable :) – JulianR

+1

@Julian: es bueno escuchar eso. *Finalmente*! –

Cuestiones relacionadas