2010-08-29 7 views
22

Para empezar diré que estoy de acuerdo en que las declaraciones goto son irrelevantes en construcciones de alto nivel en los lenguajes de programación modernos y no deberían usarse cuando un sustituto adecuado es disponible.Otras formas de lidiar con la "inicialización de bucle" en C#

Estaba volviendo a leer una edición original del Código Completo de Steve McConnell recientemente y había olvidado su sugerencia de un problema de codificación común. Lo había leído hace años cuando comencé y no creo haberme dado cuenta de lo útil que sería la receta. El problema de codificación es el siguiente: al ejecutar un bucle, a menudo es necesario ejecutar parte del bucle para inicializar el estado y luego ejecutar el bucle con otra lógica y finalizar cada bucle con la misma lógica de inicialización. Un ejemplo concreto es implementar el método String.Join (delimitador, matriz).

Creo que la primera opinión de todos sobre el problema es esta. Supongamos que el método de agregar está definido para agregar el argumento a su valor de retorno.

bool isFirst = true; 
foreach (var element in array) 
{ 
    if (!isFirst) 
    { 
    append(delimiter); 
    } 
    else 
    { 
    isFirst = false; 
    } 

    append(element); 
} 

Nota: Poca optimización para esto es para quitar el otro y ponerlo al final del bucle. Una asignación que generalmente es una instrucción única y equivalente a otro y disminuye el número de bloques básicos en 1 y aumenta el tamaño de bloque básico de la parte principal. El resultado es que ejecuta una condición en cada ciclo para determinar si debe agregar el delimitador o no.

También he visto y utilizado otras tomas para resolver este problema común de bucle. Puede ejecutar el código de elemento inicial primero fuera del bucle, luego realice su bucle desde el segundo elemento hasta el final. También puede cambiar la lógica para agregar siempre el elemento, luego el delimitador y una vez que se complete el ciclo, simplemente puede eliminar el último delimitador que agregó.

La última solución tiende a ser la que prefiero solo porque no duplica ningún código. Si alguna vez cambia la lógica de la secuencia de inicialización, no tiene que recordar fijarla en dos lugares. Sin embargo, requiere un "trabajo" adicional para hacer algo y luego deshacerlo, causando al menos ciclos de CPU adicionales y en muchos casos, como nuestro ejemplo de String.Join también requiere memoria extra.

que estaba emocionado luego de leer esta construcción

var enumerator = array.GetEnumerator(); 
if (enumerator.MoveNext()) 
{ 
    goto start; 
    do { 
    append(delimiter); 

    start: 
    append(enumerator.Current); 
    } while (enumerator.MoveNext()); 
} 

La ventaja aquí es que no se obtiene ningún código duplicado y se obtiene ningún trabajo adicional. Comienza su ciclo a la mitad de la ejecución de su primer ciclo y esa es su inicialización. Está limitado a simular otros bucles con el constructo do while, pero la traducción es fácil y leerla no es difícil.

Entonces, ahora la pregunta. Felizmente fui a intentar agregar esto a un código en el que estaba trabajando y descubrí que no funcionaba. Funciona muy bien en C, C++, Básico, pero resulta que en C# no se puede saltar a una etiqueta dentro de un alcance léxico diferente que no sea un alcance principal. Yo estaba muy decepcionado. Entonces me quedé pensando, ¿cuál es la mejor manera de lidiar con este problema de codificación muy común (lo veo principalmente en la generación de cadenas) en C#?

quizá Para ser más específicos con los requisitos:

  • No se debe copiar el código
  • No hacer trabajo innecesario
  • No tenga más de 2 o 3 veces más lento que otro código
  • ser legible

Creo que la lectura es la única cosa que podría posiblemente sufrir con la receta he dicho. Sin embargo, no funciona en C#, entonces, ¿cuál es la siguiente mejor opción?

* Editar * Cambié mis criterios de rendimiento debido a algo de la discusión. El rendimiento generalmente no es un factor limitante aquí, por lo que el objetivo más correcto debería ser no ser irracional, no ser el más rápido.

La razón por la que no me gustan las implementaciones alternativas que sugiero es porque duplican código que deja espacio para cambiar una parte y no la otra o para la que generalmente elijo requiere "deshacer" la operación que requiere pensamiento y tiempo extra para deshacer lo que acabas de hacer Con la manipulación de cadenas en particular, esto generalmente te deja abierto por un error o por no dar cuenta de una matriz vacía e intentar deshacer algo que no sucedió.

+0

Pero una vez que sabes de string.join, ¿con qué frecuencia usted se encuentra realmente escribir dichos bucles más? –

+1

Encuentro todo tipo de razones para no usarlo. Actualmente estoy trabajando en algo que hace mucha manipulación de cadenas con un árbol de objetos. Tengo que pasar un generador de cadenas para no tener muchas asignaciones innecesarias a medida que traduzco un gráfico de árbol arbitrario en una cadena. No es posible usar esta sobrecarga porque no existe para el generador de cadenas, y aunque lo hiciera, no podría usar algo como sb.Join ("delim", Children.Select (child => child.Build (sb)) porque los hijos se agregarían al generador de cuerdas en el orden incorrecto. –

+2

@ Peter Oehlert: Puede escribir una función que devuelva IEnumerable, luego use yield return para devolver los resultados en el orden que desee. –

Respuesta

11

Para su ejemplo específico hay una solución estándar: string.Join. Esto maneja la adición del delimitador correctamente para que no tenga que escribir el bucle usted mismo.

Si realmente desea escribir esto por sí mismo un enfoque que puede utilizar es la siguiente:

string delimiter = ""; 
foreach (var element in array) 
{ 
    append(delimiter); 
    append(element); 
    delimiter = ","; 
} 

Esto debe ser razonablemente eficiente y creo que es razonable interpretar. La cadena constante "," se interna, por lo que no se creará una nueva cadena en cada iteración. Por supuesto, si el rendimiento es crítico para su aplicación, debe comparar en lugar de adivinar.

+8

Sólo está usando 'String.Join' como un ejemplo ... –

+0

usar' string.join() 'primero tiene que tener una matriz de cadenas, si usted no tiene que dada entonces usted está buscando en el edificio de' Enumere 'y luego conviértalo en' cadena [] 'que es más feo de lo que comenzó. –

+0

Me gusta esto. Siempre molesto por "primero" ... ¿Qué tan seguro puede estar de que el compilador optimizará las asignaciones repetidas al delimitador? – Nicolas78

0

Prefiero first método variable. Probablemente no sea la manera más limpia pero más eficiente. Alternativamente, puede usar Length de lo que anexa y compararlo con cero. Funciona bien con StringBuilder.

18

Personalmente me gusta la opción de Mark Byers, pero siempre se puede escribir su propio método genérico para esto:

public static void IterateWithSpecialFirst<T>(this IEnumerable<T> source, 
    Action<T> firstAction, 
    Action<T> subsequentActions) 
{ 
    using (IEnumerator<T> iterator = source.GetEnumerator()) 
    { 
     if (iterator.MoveNext()) 
     { 
      firstAction(iterator.Current); 
     } 
     while (iterator.MoveNext()) 
     { 
      subsequentActions(iterator.Current); 
     } 
    } 
} 

Eso es relativamente sencillo ... dando una especial última acción es un poco más difícil:

public static void IterateWithSpecialLast<T>(this IEnumerable<T> source, 
    Action<T> allButLastAction, 
    Action<T> lastAction) 
{ 
    using (IEnumerator<T> iterator = source.GetEnumerator()) 
    { 
     if (!iterator.MoveNext()) 
     { 
      return; 
     }    
     T previous = iterator.Current; 
     while (iterator.MoveNext()) 
     { 
      allButLastAction(previous); 
      previous = iterator.Current; 
     } 
     lastAction(previous); 
    } 
} 

EDITAR: Como su comentario estaba preocupado con el rendimiento de esto, voy a reiterar mi comentario en esta respuesta: aunque este problema general es razonablemente común, es no es un cuello de botella de rendimiento que vale la pena micro-optimizar. De hecho, no recuerdo haber encontrado una situación en la que la maquinaria de bucle se convirtiera en un cuello de botella. Estoy seguro de que sucede, pero que no es "común". Si alguna vez me encuentro con él, voy a hacer un caso especial de ese código en particular, y la mejor solución dependerá de exactamente lo que el código tiene que hacer.

En general, sin embargo, valoro la legibilidad y la reutilizabilidad mucho más que la micro-optimización.

+2

+1 para mostrar la forma correcta de usar un enumerador directamente: 'using'. –

+1

Pero llamar a un delegado es una acción bastante costosa (delegar 'Acción') por lo que esta solución debe ser legible, pero es a expensas del rendimiento. ¿Estoy en lo cierto o hay un truco? –

+1

@MartyIX: su pregunta se refiere a un problema de codificación * common *, pero también especifica el rendimiento como un requisito clave. ¿Cuán a menudo surge esto de tal manera que el costo de ejecutar un delegado va a ser un cuello de botella? No puedo pensar en que * ever * me suceda. Si alguna vez lo encuentro, trataré esa situación específica como una anomalía y escribiré un código específico para ella. No trataría de pensar en una solución de propósito general para una situación tan inusual. El "propósito general" a menudo es contrario al tipo de micro-optimización que parece que busca. –

0

¿Por qué no se mueve al tratar con el primer elemento fuera de un bucle?

StringBuilder sb = new StrindBuilder() 
sb.append(array.first) 
foreach (var elem in array.skip(1)) { 
    sb.append(",") 
    sb.append(elem) 
} 
+2

En realidad, Peter menciona brevemente esta solución en su pregunta ya. –

+0

Si 'array' es realmente una consulta DB, ¿este método no ejecutará la consulta * dos veces *? – Gabe

+0

@Gabe: sí, como está escrito, lo haría. En ese caso, probablemente debería guardar el resultado de la consulta de antemano con una llamada a 'ToList()'. –

7

Ya estás dispuesto a renunciar a foreach. Por lo que este debe ser adecuado:

 using (var enumerator = array.GetEnumerator()) { 
      if (enumerator.MoveNext()) { 
       for (;;) { 
        append(enumerator.Current); 
        if (!enumerator.MoveNext()) break; 
        append(delimiter); 
       } 
      } 
     } 
+0

recuerdo haber visto esta construcción en un idioma en alguna parte: un bucle con el cheque, no al principio, no al final, pero en el medio. No recuerdo qué idioma era ... –

+0

Esto funciona en casi cualquier idioma. –

+1

+1 Esto se denomina ciclo de bucle con salida en Code Complete y es mi solución preferida para este tipo de problema. –

4

veces uso de LINQ .First() y .Skip(1) de manejar esto ... Esto puede dar una solución relativamente limpia (y muy legible).

que Usando ejemplo,

append(array.First()); 
foreach(var x in array.Skip(1)) 
{ 
    append(delimiter); 
    append (x); 
} 

[Esto supone que hay por lo menos un elemento de la matriz, una prueba sencilla para añadir si es que hay que evitar.]

Uso Fa # sería otra sugerencia :-)

+0

He usado un foreach para "probar" el primero y he usado "Take (1)" en lugar de primero. un poco funky, pero hace el trabajo. –

+0

Confieso que todavía no he aprendido a aprender F # (está en la lista); ¿Cómo se haría esto en F #? –

+0

F # tiene coincidencia de patrón. El 'Cons Pattern' le permite descomponer una lista en' head :: tail'. Consulte http://msdn.microsoft.com/en-us/library/dd547125.aspx –

2

Hay formas de "evitar" el código duplicado, pero en la mayoría de los casos el código duplicado es mucho menos feo/peligroso que las soluciones posibles. La solución "goto" que cita no me parece una mejora: realmente no creo que gane nada significativo (compacidad, legibilidad o eficiencia) al usarla, mientras que aumenta el riesgo de que un programador se equivoque. en algún momento en la vida del código.

En general, tiendo a ir para el enfoque:

  • Un caso especial de la primera (o última) la acción
  • de bucle para las otras acciones.

Esto elimina las ineficiencias introducidas al comprobar si el bucle está en la primera iteración cada vez que pasa, y es realmente fácil de entender. Para casos no triviales, usar un método de delegado o ayudante para aplicar la acción puede minimizar la duplicación de código.

O otro enfoque que utilizo en ocasiones donde la eficiencia no es importante:

  • bucle y prueba si la cadena está vacía para determinar si se requiere un delimitador.

Esto se puede escribir para ser más compacto y legible que el enfoque goto, y no requiere ninguna variable adicional/almacenamiento/pruebas para detectar la iteración de "caso especial".

Pero creo que el enfoque de Mark Byers es una buena solución limpia para su ejemplo en particular.

5

Por supuesto que puede crear una solución goto en C# (nota: No añadí null controles):

string Join(string[] array, string delimiter) { 
    var sb = new StringBuilder(); 
    var enumerator = array.GetEnumerator(); 
    if (enumerator.MoveNext()) { 
    goto start; 
    loop: 
     sb.Append(delimiter); 
     start: sb.Append(enumerator.Current); 
     if (enumerator.MoveNext()) goto loop; 
    } 
    return sb.ToString(); 
} 

Para su específica ejemplo, esto parece bastante directo conducir a mí (y es una de las soluciones que ha descrito):

string Join(string[] array, string delimiter) { 
    var sb = new StringBuilder(); 
    foreach (string element in array) { 
    sb.Append(element); 
    sb.Append(delimiter); 
    } 
    if (sb.Length >= delimiter.Length) sb.Length -= delimiter.Length; 
    return sb.ToString(); 
} 

Si desea obtener funcional, se puede tratar de usar este método de plegado:

string Join(string[] array, string delimiter) { 
    return array.Aggregate((left, right) => left + delimiter + right); 
} 

Aunque se lee muy bien, no es el uso de un StringBuilder, lo que podría querer abusar Aggregate un poco para usarlo:

string Join(string[] array, string delimiter) { 
    var sb = new StringBuilder(); 
    array.Aggregate((left, right) => { 
    sb.Append(left).Append(delimiter).Append(right); 
    return ""; 
    }); 
    return sb.ToString(); 
} 

O puede utilizar esto (tomando prestada la idea de otras respuestas aquí):

string Join(string[] array, string delimiter) { 
    return array. 
    Skip(1). 
    Aggregate(new StringBuilder(array.FirstOrDefault()), 
     (acc, s) => acc.Append(delimiter).Append(s)). 
    ToString(); 
} 
+3

Reemplazar un bucle con un goto es terriblemente feo y nunca debe usarse sin una * muy * buena razón, pero +1 para pensar fuera del caja y encontrar una manera de implementar la construcción en C#. – Heinzi

0

Si quieres ir a la ruta funcional, se podría definir como LINQ string.join construcción que se puede reutilizar en tipos.

Personalmente, casi siempre optaba por la claridad del código en lugar de guardar algunas ejecuciones del código de operación.

Ejem:

namespace Play 
{ 
    public static class LinqExtensions { 
     public static U JoinElements<T, U>(this IEnumerable<T> list, Func<T, U> initializer, Func<U, T, U> joiner) 
     { 
      U joined = default(U); 
      bool first = true; 
      foreach (var item in list) 
      { 
       if (first) 
       { 
        joined = initializer(item); 
        first = false; 
       } 
       else 
       { 
        joined = joiner(joined, item); 
       } 
      } 
      return joined; 
     } 
    } 

    class Program 
    { 

     static void Main(string[] args) 
     { 
      List<int> nums = new List<int>() { 1, 2, 3 }; 
      var sum = nums.JoinElements(a => a, (a, b) => a + b); 
      Console.WriteLine(sum); // outputs 6 

      List<string> words = new List<string>() { "a", "b", "c" }; 
      var buffer = words.JoinElements(
       a => new StringBuilder(a), 
       (a, b) => a.Append(",").Append(b) 
       ); 

      Console.WriteLine(buffer); // outputs "a,b,c" 

      Console.ReadKey(); 
     } 

    } 
} 
+0

Linq tiene esto, se llama 'Agregado' – joshperry

+0

@josh, esto es sutilmente diferente a Agregado: words.Aggregate ( new StringBuilder(), (a, b) => a.Append (", "). Añadir (b) ); ... regresa (", a, b, c") –

Cuestiones relacionadas