java loops while-loop idioms

java - Mejor Loom Idiom para carcasa especial el último elemento



loops while-loop (18)

Me encuentro con este caso muchas veces cuando hago un procesamiento de texto simple e imprimo declaraciones donde estoy pasando por encima de una colección y quiero un caso especial del último elemento (por ejemplo, cada elemento normal estará separado por comas, excepto en el último caso).

¿Hay algún modismo de práctica recomendada o forma elegante que no requiera duplicar el código o empujar en un if, sino en el ciclo?

Por ejemplo, tengo una lista de cadenas que quiero imprimir en una lista separada por comas. (la solución do while ya asume que la lista tiene 2 o más elementos; de lo contrario, sería tan malo como el más correcto para el ciclo condicional).

por ejemplo, List = ("perro", "gato", "murciélago")

Quiero imprimir "[perro, gato, murciélago]"

Presento 2 métodos

  1. Para bucle con condicional

    public static String forLoopConditional(String[] items) { String itemOutput = "["; for (int i = 0; i < items.length; i++) { // Check if we''re not at the last element if (i < (items.length - 1)) { itemOutput += items[i] + ", "; } else { // last element itemOutput += items[i]; } } itemOutput += "]"; return itemOutput; }

  2. hacer mientras el bucle ceba el bucle

    public static String doWhileLoopPrime(String[] items) { String itemOutput = "["; int i = 0; itemOutput += items[i++]; if (i < (items.length)) { do { itemOutput += ", " + items[i++]; } while (i < items.length); } itemOutput += "]"; return itemOutput; }

    Clase del probador:

    public static void main(String[] args) { String[] items = { "dog", "cat", "bat" }; System.out.println(forLoopConditional(items)); System.out.println(doWhileLoopPrime(items)); }

En la clase Java AbstractCollection tiene la siguiente implementación (un poco detallado porque contiene todas las comprobaciones de errores de mayúsculas y minúsculas, pero no está mal).

public String toString() { Iterator<E> i = iterator(); if (! i.hasNext()) return "[]"; StringBuilder sb = new StringBuilder(); sb.append(''[''); for (;;) { E e = i.next(); sb.append(e == this ? "(this Collection)" : e); if (! i.hasNext()) return sb.append('']'').toString(); sb.append(", "); } }


...

String[] items = { "dog", "cat", "bat" }; String res = "["; for (String s : items) { res += (res.length == 1 ? "" : ", ") + s; } res += "]";

o eso es bastante legible Puedes poner el condicional en una cláusula if independiente, por supuesto. Lo que hace idiomático (creo que sí, al menos) es que usa un bucle foreach y no usa un encabezado de bucle complicado.

Además, no se duplica ninguna lógica (es decir, hay un solo lugar donde un elemento de los items se agrega a la cadena de salida; en una aplicación del mundo real esta podría ser una operación de formateo más complicada y larga, por lo que no quisiera repetir el código).


Como su caso simplemente procesa texto, no necesita el condicional dentro del ciclo. Ejemplo de CA:

char* items[] = {"dog", "cat", "bat"}; char* output[STRING_LENGTH] = {0}; char* pStr = &output[1]; int i; output[0] = ''[''; for (i=0; i < (sizeof(items) / sizeof(char*)); ++i) { sprintf(pStr,"%s,",items[i]); pStr = &output[0] + strlen(output); } output[strlen(output)-1] = '']'';

En lugar de agregar un condicional para evitar generar la coma final, adelante y genere (para mantener su bucle simple y libre de condiciones) y simplemente sobrescribirlo al final. Muchas veces, me parece más claro generar el caso especial como cualquier otra iteración de ciclo y luego reemplazarlo manualmente al final (aunque si el código "reemplazarlo" es más que un par de líneas, este método puede ser más difícil de leer).


Creo que es más fácil pensar en el primer elemento como en el caso especial porque es mucho más fácil saber si una iteración es la primera y no la última. No se necesita una lógica compleja o costosa para saber si se está haciendo algo por primera vez.

public static String prettyPrint(String[] items) { String itemOutput = "["; boolean first = true; for (int i = 0; i < items.length; i++) { if (!first) { itemOutput += ", "; } itemOutput += items[i]; first = false; } itemOutput += "]"; return itemOutput; }


Creo que hay dos respuestas a esta pregunta: la mejor expresión idiomática para este problema en cualquier idioma, y ​​la mejor expresión idiomática para este problema en Java. También creo que la intención de este problema no fue la tarea de unir cadenas, sino el patrón en general, por lo que realmente no ayuda a mostrar las funciones de la biblioteca que pueden hacer eso.

En primer lugar, las acciones de rodear una cadena con [] y crear una cadena separada por comas son dos acciones separadas, e idealmente serían dos funciones separadas.

Para cualquier idioma, creo que la combinación de recurrencia y coincidencia de patrones funciona mejor. Por ejemplo, en Haskell haría esto:

join [] = "" join [x] = x join (x:xs) = concat [x, ",", join xs] surround before after str = concat [before, str, after] yourFunc = surround "[" "]" . join -- example usage: yourFunc ["dog", "cat"] will output "[dog,cat]"

El beneficio de escribirlo así es que enumera claramente las diferentes situaciones que enfrentará la función y cómo la manejará.

Otra forma muy buena de hacerlo es con una función tipo acumulador. P.ej:

join [] = "" join strings = foldr1 (/a b -> concat [a, ",", b]) strings

Esto también se puede hacer en otros idiomas, por ejemplo, c #:

public static string Join(List<string> strings) { if (!strings.Any()) return string.Empty; return strings.Aggregate((acc, val) => acc + "," + val); }

No es muy eficiente en esta situación, pero puede ser útil en otros casos (o la eficiencia puede no ser importante).

Desafortunadamente, Java no puede usar ninguno de esos métodos. Entonces, en este caso, creo que la mejor manera es tener controles en la parte superior de la función para los casos de excepción (0 o 1 elementos) y luego usar un ciclo for para manejar el caso con más de 1 elemento:

public static String join(String[] items) { if (items.length == 0) return ""; if (items.length == 1) return items[0]; StringBuilder result = new StringBuilder(); for(int i = 0; i < items.length - 1; i++) { result.append(items[i]); result.append(","); } result.append(items[items.length - 1]); return result.toString(); }

Esta función muestra claramente lo que sucede en las dos cajas de borde (0 o 1 elemento). A continuación, utiliza un ciclo para todos los elementos excepto los últimos, y finalmente agrega el último elemento sin una coma. La manera inversa de manejar el elemento que no es una coma al inicio también es fácil de hacer.

Tenga en cuenta que if (items.length == 1) return items[0]; línea no es realmente necesario, sin embargo, creo que hace que lo que hace la función sea más fácil de determinar de un vistazo.

(Tenga en cuenta que si alguien quiere más explicaciones sobre las funciones de haskell / c # pregunte y lo agregaré)


En este caso, esencialmente concatena una lista de cadenas usando alguna cadena de separación. Quizás puedas escribir algo tú mismo que hace esto. Entonces obtendrás algo como:

String[] items = { "dog", "cat", "bat" }; String result = "[" + joinListOfStrings(items, ", ") + "]"

con

public static String joinListOfStrings(String[] items, String sep) { StringBuffer result; for (int i=0; i<items.length; i++) { result.append(items[i]); if (i < items.length-1) buffer.append(sep); } return result.toString(); }

Si tiene una Collection lugar de una String[] también puede usar iteradores y el método hasNext() para verificar si esta es la última o no.


En general, mi favorito es la salida de varios niveles. Cambio

for ( s1; exit-condition; s2 ) { doForAll(); if ( !modified-exit-condition ) doForAllButLast(); }

a

for ( s1;; s2 ) { doForAll(); if ( modified-exit-condition ) break; doForAllButLast(); }

Elimina cualquier código duplicado o verificaciones redundantes.

Tu ejemplo:

for (int i = 0;; i++) { itemOutput.append(items[i]); if ( i == items.length - 1) break; itemOutput.append(", "); }

Funciona para algunas cosas mejor que otras. No soy un gran admirador de esto para este ejemplo específico.

Por supuesto, se vuelve realmente complicado para los escenarios donde la condición de salida depende de lo que sucede en doForAll() y no solo en s2 . Usar un Iterator es un caso así.

Aquí hay un documento del prof que lo promovió desvergonzadamente a sus estudiantes :-). Lea la sección 5 para ver exactamente de lo que está hablando.


Hay muchos bucles for en estas respuestas, pero encuentro que un Iterator y while loop se leen mucho más fácilmente. P.ej:

Iterator<String> itemIterator = Arrays.asList(items).iterator(); if (itemIterator.hasNext()) { // special-case first item. in this case, no comma while (itemIterator.hasNext()) { // process the rest } }

Este es el enfoque adoptado por Joiner en las colecciones de Google y me parece muy legible.


Me gusta usar una bandera para el primer artículo.

ArrayList<String> list = new ArrayList()<String>{{ add("dog"); add("cat"); add("bat"); }}; String output = "["; boolean first = true; for(String word: list){ if(!first) output += ", "; output+= word; first = false; } output += "]";


Me gustaría ir con su segundo ejemplo, es decir. maneje el caso especial fuera del ciclo, solo escríbalo un poco más directo:

String itemOutput = "["; if (items.length > 0) { itemOutput += items[0]; for (int i = 1; i < items.length; i++) { itemOutput += ", " + items[i]; } } itemOutput += "]";


Mi toma habitual es probar si la variable de índice es cero, por ejemplo:

var result = "[ "; for (var i = 0; i < list.length; ++i) { if (i != 0) result += ", "; result += list[i]; } result += " ]";

Pero, por supuesto, eso es solo si hablamos de idiomas que no tienen algún método Array.join (","). ;-)


Normalmente escribo un ciclo for como este:

public static String forLoopConditional(String[] items) { StringBuilder builder = new StringBuilder(); builder.append("["); for (int i = 0; i < items.length - 1; i++) { builder.append(items[i] + ", "); } if (items.length > 0) { builder.append(items[items.length - 1]); } builder.append("]"); return builder.toString(); }


Normalmente lo escribo así:

static String commaSeparated(String[] items) { StringBuilder sb = new StringBuilder(); String sep = ""; for (String item: items) { sb.append(sep); sb.append(item); sep = ","; } return sb.toString(); }


Se puede lograr utilizando Java 8 lambda y Collectors.joining () como -

List<String> items = Arrays.asList("dog", "cat", "bat"); String result = items.stream().collect(Collectors.joining(", ", "[", "]")); System.out.println(result);


Si está construyendo una cadena dinámicamente así, no debería estar usando el operador + =. La clase StringBuilder funciona mucho mejor para la concatenación de cadenas dinámicas repetidas.

public String commaSeparate(String[] items, String delim){ StringBuilder bob = new StringBuilder(); for(int i=0;i<items.length;i++){ bob.append(items[i]); if(i+1<items.length){ bob.append(delim); } } return bob.toString(); }

Entonces llamar es así

String[] items = {"one","two","three"}; StringBuilder bob = new StringBuilder(); bob.append("["); bob.append(commaSeperate(items,",")); bob.append("]"); System.out.print(bob.toString());


Si solo está buscando una lista separada por comas de esta manera: "[The, Cat, in, the, Hat]", no pierda el tiempo escribiendo su propio método. Solo use List.toString:

List<String> strings = Arrays.asList("The", "Cat", "in", "the", "Hat); System.out.println(strings.toString());

Siempre que el tipo genérico de la Lista tenga un toString con el valor que desea visualizar, simplemente llame a List.toString:

public class Dog { private String name; public Dog(String name){ this.name = name; } public String toString(){ return name; } }

Entonces puedes hacer:

List<Dog> dogs = Arrays.asList(new Dog("Frank"), new Dog("Hal")); System.out.println(dogs);

Y obtendrás: [Frank, Hal]


Solución Java 8, en caso de que alguien lo esté buscando:

String res = Arrays.stream(items).reduce((t, u) -> t + "," + u).get();


Una tercera alternativa es la siguiente

StringBuilder output = new StringBuilder(); for (int i = 0; i < items.length - 1; i++) { output.append(items[i]); output.append(","); } if (items.length > 0) output.append(items[items.length - 1]);

Pero lo mejor es usar un método join (). Para Java hay un String.join en bibliotecas de terceros, de esta forma su código se convierte en:

StringUtils.join(items,'','');

FWIW, el método join () (línea 3232 en adelante) en Apache Commons sí utiliza un if dentro de un ciclo:

public static String join(Object[] array, char separator, int startIndex, int endIndex) { if (array == null) { return null; } int bufSize = (endIndex - startIndex); if (bufSize <= 0) { return EMPTY; } bufSize *= ((array[startIndex] == null ? 16 : array[startIndex].toString().length()) + 1); StringBuilder buf = new StringBuilder(bufSize); for (int i = startIndex; i < endIndex; i++) { if (i > startIndex) { buf.append(separator); } if (array[i] != null) { buf.append(array[i]); } } return buf.toString(); }


string value = "[" + StringUtils.join( items, '','' ) + "]";