Best Loop Idiom для специального корпуса последнего элемента - PullRequest
48 голосов
/ 24 июня 2010

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

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

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

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

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

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

  1. Для цикла с условным кодом

    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. делать, пока цикл заполняет цикл

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

Ответы [ 18 ]

1 голос
/ 24 июня 2010

Если вы строите такую ​​строку динамически, вам не следует использовать оператор + =. Класс 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());
1 голос
/ 25 апреля 2018

Это может быть достигнуто с помощью Java 8 lambda и Collectors.joining () как -

List<String> items = Arrays.asList("dog", "cat", "bat");
String result = items.stream().collect(Collectors.joining(", ", "[", "]"));
System.out.println(result);
1 голос
/ 24 июня 2010

...

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

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

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

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

1 голос
/ 29 июня 2010

Я думаю, что есть два ответа на этот вопрос: лучшая идиома для этой проблемы на любом языке, и лучшая идиома для этой проблемы в 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 

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

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

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

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

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 / c #, спросите, и я добавлю его)

1 голос
/ 26 июня 2010

Вообще, мой фаворит - многоуровневый выход. Изменение

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, чтобы точно понять, о чем вы говорите.

0 голосов
/ 24 июня 2010

Третьим вариантом является следующий

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();
    }
0 голосов
/ 24 июня 2010

Я обычно пишу цикл 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();                           
}       
0 голосов
/ 26 июня 2010

Если вы просто ищете разделенный запятыми список, подобный этому: «[Кошка, шляпа]», даже не тратьте время на написание собственного метода. Просто используйте 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);

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

...