while retornar net gotoif detener como c# initialization goto do-while

net - retornar c#



Otras formas de tratar con la "inicialización de bucle" en C# (9)

¿Por qué no mover 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) }

Para empezar, diré que estoy de acuerdo en que las declaraciones de goto son en gran medida irrelevantes por los constructos de nivel superior en los lenguajes de programación modernos y no deben usarse cuando hay un sustituto adecuado disponible.

Estaba releyendo una edición original de Steve McConnell''s Code Complete recientemente y me había olvidado de su sugerencia para un problema de codificación común. Lo había leído hace años cuando comencé y no creo que me haya dado cuenta de lo útil que sería la receta. El problema de codificación es el siguiente: cuando ejecuta un bucle, a menudo necesita ejecutar parte del bucle para inicializar el estado y luego ejecutar el bucle con otra lógica y terminar 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 toma de todo el mundo sobre el problema es la siguiente. Suponga que el método de adición se define 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: Una ligera optimización para esto es eliminar el else y ponerlo al final del bucle. Una asignación generalmente es una instrucción única y equivalente a un else y disminuye el número de bloques básicos en 1 y aumenta el tamaño del bloque básico de la parte principal. El resultado es que se ejecuta una condición en cada bucle para determinar si debe agregar el delimitador o no.

También he visto y usado otras tomas para tratar este problema de bucle común. Puede ejecutar el código del elemento inicial primero fuera del bucle, luego realizar 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 completa 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 la lógica de la secuencia de inicialización cambia alguna vez, no tiene que acordarse de corregirla en dos lugares. Sin embargo, requiere un "trabajo" extra para hacer algo y luego deshacerlo, causando al menos ciclos de CPU adicionales y, en muchos casos, como nuestro ejemplo String.Join también requiere memoria adicional.

Estaba emocionado entonces de leer este constructo

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

El beneficio aquí es que no obtiene ningún código duplicado y no obtiene trabajo adicional. Comienzas tu ciclo hasta la mitad de la ejecución de tu primer ciclo y esa es tu inicialización. Está limitado a simular otros bucles con el comando do mientras construye, pero la traducción es fácil y su lectura 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 funcionó. Funciona muy bien en C, C ++, Básico, pero en C # no se puede saltar a una etiqueta dentro de un ámbito léxico diferente que no sea un ámbito principal. Yo estaba muy decepcionado. Así que me pregunté, ¿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 #?

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

  • No duplicar código
  • No hagas trabajo innecesario
  • No sea más de 2 o 3 veces más lento que otro código
  • Ser legible

Creo que la legibilidad es lo único que podría sufrir con la receta que mencioné. Sin embargo, no funciona en C # así que, ¿cuál es la mejor opción?

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

La razón por la que no me gustan las implementaciones alternativas que sugiero es porque duplican el 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 un pensamiento y tiempo adicionales para deshacer la cosa que acabas de hacer En particular, con la manipulación de cadenas, esto generalmente te deja abierto por uno o por no tener en cuenta una matriz vacía y tratar de deshacer algo que no sucedió.


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

Usando tu ejemplo,

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

[Esto supone que hay al menos un elemento en la matriz, una prueba fácil de agregar si se debe evitar.]

Usar F # sería otra sugerencia :-)


Ciertamente, puede crear una solución goto en C # (nota: no null cheques null ):

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 ejemplo específico , esto me parece bastante sencillo (y es una de las soluciones que describió):

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 quieres ponerte en funcionamiento, puedes intentar 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 está utilizando un StringBuilder , por lo que es posible que desee abusar un poco del Aggregate 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 usar 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(); }


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

En general tiendo a ir por el enfoque:

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

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

U otro enfoque que utilizo a veces donde la eficiencia no es importante:

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

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

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


Para su ejemplo específico hay una solución estándar: string.Join . Esto se encarga de agregar el delimitador correctamente para que no tenga que escribir el bucle usted mismo.

Si realmente desea escribir esto, un enfoque que puede utilizar es el siguiente:

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

Esto debería ser razonablemente eficiente y creo que es razonable leerlo. La cadena constante "," está internada, por lo que no se creará una cadena nueva en cada iteración. Por supuesto, si el rendimiento es crítico para su aplicación, debe hacer una evaluación comparativa en lugar de adivinar.


Personalmente, me gusta la opción de Mark Byer, pero siempre puedes escribir tu 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 ... dar una última acción especial 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 se preocupó por el desempeño de esto, reiteraré mi comentario en esta respuesta: si bien este problema general es bastante común, no es común que sea un cuello de botella de rendimiento que valga la pena realizar una micro optimización. 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 eso no es "común". Si alguna vez me encuentro con él, le daré un caso especial a ese código en particular, y la mejor solución dependerá de lo que el código necesita hacer.

En general, sin embargo, valoro la legibilidad y la reutilización mucho más que la microoptimización.


Prefiero el first método variable. Probablemente no sea la forma más limpia pero más eficiente. Alternativamente, puedes usar la Length de la cosa que agregas y compararla con cero. Funciona bien con StringBuilder .


Si desea ir a la ruta funcional, puede definir String.Join como la construcción LINQ que es reutilizable en todos los tipos.

Personalmente, casi siempre preferiría la claridad del código al guardar algunas ejecuciones de código de operación.

P.EJ:

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


Ya estás dispuesto a renunciar a foreach. Así que esto debería ser adecuado:

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