Идиома Best Loop для специального корпуса последнего элемента

Я часто сталкиваюсь с этим случаем при выполнении простых операций обработки текста и печати, когда я перебираю цикл, и я хочу, чтобы в специальном случае был последний элемент (например, каждый нормальный элемент будет разделен запятой, за исключением последнего случай).

Есть ли какая-либо передовая практика идиома или элегантная форма, которая не требует дублирования кода или перетаскивания в if, else в цикле.

Например, у меня есть список строк, которые я хочу напечатать в списке, разделенном запятыми. (в то время как решение уже предполагает, что список имеет 2 или более элементов, в противном случае это было бы так же плохо, как более правильный для цикла с условным).

например. List = ( "собака", "кошка", "летучая мышь" )

Я хочу напечатать "[собака, кошка, летучая мышь]"

Я представляю 2 метода

  • Для цикла с условным

    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;
     }
    
  • do while цикл, заправляющий цикл

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

    Класс тестера:

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

В классе Java AbstractCollection он имеет следующую реализацию (немного подробный, потому что он содержит всю проверку ошибок в случае краев, но неплохо).

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

Ответ 1

В этих ответах много циклов, но я считаю, что цикл Iterator и while читается намного легче. Например:.

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
  }
}

Это подход, сделанный Joiner в коллекциях Google, и я считаю его очень читаемым.

Ответ 2

Я обычно пишу это так:

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

Ответ 3

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

Ответ 4

Моим обычным явлением является проверка, если индексная переменная равна нулю, например:

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

Но, конечно, это только если мы говорим о языках, у которых нет некоторого метода Array.join( "," ).; -)

Ответ 5

Я думаю, что первый элемент можно рассматривать как особый случай, потому что гораздо легче узнать, является ли итерация первой, а не последней. Не требуется сложной или дорогостоящей логики, чтобы знать, происходит ли что-то в первый раз.

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

Ответ 6

Мне нравится использовать флаг для первого элемента.

 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 += "]";

Ответ 7

Поскольку ваш случай просто обрабатывает текст, вам не нужен условный код внутри цикла. Пример C:

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] = ']';

Вместо добавления условного выражения, чтобы избежать генерации конечной запятой, продолжайте и сгенерируйте его (чтобы ваш цикл был простым и условным) и просто перезаписывайте его в конце. Много раз я нахожу более ясным создание специального случая, как и любая другая итерация цикла, а затем вручную заменяю его в конце (хотя, если код "заменить" более чем на пару строк, этот метод может стать более сложным чтения).

Ответ 8

Решение Java 8, если кто-то ищет его:

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

Ответ 9

...

String[] items = { "dog", "cat", "bat" };
String res = "[";

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

или так вполне читаемо. Конечно, вы можете поместить условное выражение в отдельное предложение if. Что он делает идиоматическим (я думаю, так, по крайней мере) является то, что он использует цикл foreach и не использует сложный заголовок цикла.

Кроме того, никакая логика не дублируется (т.е. есть только одно место, где элемент из items фактически добавляется к выходной строке - в реальном мире это может быть более сложным и длительная операция форматирования, поэтому я не хочу повторять код).

Ответ 10

Если вы строите строку динамически так, вы не должны использовать оператор + =. Класс StringBuilder работает намного лучше для повторной динамической конкатенации строк.

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

Затем вызов выглядит следующим образом

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

Ответ 11

В этом случае вы по существу объединяете список строк, используя некоторую строку разделителя. Вы можете написать что-то само собой, что делает это. Тогда вы получите что-то вроде:

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

с

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

Если у вас есть Collection вместо String[], вы также можете использовать итераторы и метод hasNext(), чтобы проверить, является ли это последним или нет.

Ответ 12

Я бы пошел со вторым примером, т.е. обрабатывать специальный случай вне цикла, просто напишите его немного проще:

String itemOutput = "[";

if (items.length > 0) {
    itemOutput += items[0];

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

itemOutput += "]";

Ответ 13

Как правило, моим любимым является многоуровневый выход. Изменение

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

к

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

Он устраняет любые дубликаты кода или избыточные проверки.

Ваш пример:

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

Он работает для некоторых вещей лучше других. Я не большой поклонник этого для этого конкретного примера.

Конечно, это очень сложно для сценариев, где условие выхода зависит от того, что происходит в doForAll(), а не только s2. Использование Iterator - такой случай.

Вот статья от профессора, которая бесстыдно продвигала его к своим ученикам:-). Прочитайте раздел 5, о чем вы говорите.

Ответ 14

Я думаю, что есть два ответа на этот вопрос: лучшая идиома для этой проблемы на любом языке и лучшая идиома для этой проблемы в java. Я также думаю, что цель этой проблемы заключалась не в задачах объединения строк, а в общем в целом, поэтому на самом деле это не помогает показать библиотечные функции, которые могут это сделать.

Во-первых, хотя действия окружения строки с помощью [] и создания строки, разделенной запятыми, являются двумя отдельными действиями и в идеале должны быть двумя отдельными функциями.

Для любого языка, я думаю, комбинация рекурсии и соответствия шаблонов работает лучше всего. Например, в haskell я бы сделал следующее:

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]"

Преимущество такого написания заключается в том, что в нем четко перечислены различные ситуации, с которыми столкнутся функции, и как они справятся с этим.

Еще один очень хороший способ сделать это - с помощью функции типа аккумулятора. Например:

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

Это можно сделать и на других языках, например С#:

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

Не очень эффективен в этой ситуации, но может быть полезен в других случаях (или эффективность может не иметь значения).

К сожалению, java не может использовать любой из этих методов. Поэтому в этом случае я считаю, что наилучшим способом является проверка в верхней части функции для случаев исключения (0 или 1 элемент), а затем использование цикла for для обработки случая с более чем одним элементом:

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

Эта функция четко показывает, что происходит в двух краевых случаях (0 или 1 элемент). Затем он использует цикл для всех, кроме последних элементов, и, наконец, добавляет последний элемент без запятой. Простой способ обработки элемента без запятой в начале также легко сделать.

Обратите внимание, что строка if (items.length == 1) return items[0]; на самом деле не нужна, однако я думаю, что она делает то, что функция легче определить с первого взгляда.

(Обратите внимание, что если кто-то хочет больше объяснений в функциях haskell/С#, спросите, и я добавлю его)

Ответ 15

Третий вариант:

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]);

Но лучше всего использовать метод join(). Для Java там String.join в сторонних библиотеках, таким образом ваш код будет выглядеть следующим образом:

StringUtils.join(items,',');

FWIW, метод join() (строка 3232 и далее) в Apache Commons использует if внутри цикла:

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

Ответ 16

Я обычно пишу цикл for следующим образом:

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

Ответ 17

Если вы просто ищете список разделенных запятыми: "[The, Cat, in, the, Hat]", даже не теряйте время, записывая свой собственный метод. Просто используйте List.toString:

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

System.out.println(strings.toString());

Если общий тип List имеет значение toString со значением, которое вы хотите отобразить, просто вызовите List.toString:

public class Dog {
    private String name;

    public Dog(String name){
         this.name = name;
    }

    public String toString(){
        return name;
    }
}

Затем вы можете сделать:

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

И вы получите: [Фрэнк, Хэл]