用于特殊情况下处理最后一个元素的最佳循环习惯用语

52

在进行简单的文本处理和打印语句时,我经常遇到这种情况:循环遍历集合并对最后一个元素进行特殊处理(例如,除了最后一个元素以外,每个普通元素都会用逗号分隔)。

有没有一些最佳实践习惯或优雅的形式,不需要复制代码或将 if else 语句嵌入循环中。

例如,我有一个字符串列表,我想以逗号分隔的列表形式打印出来。(do while 解决方案已经假定列表有两个或更多元素,否则与条件正确的 for 循环一样糟糕)。

例如 List = ("dog", "cat", "bat")

我想打印“[dog,cat,bat]”

我提供了两种方法:

  1. 带条件的 for 循环

    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. 使用do while循环时为循环做准备

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

值得注意的是,Java类java.util.AbstractCollection在第1节中使用了[ while(all) $value if(not_last) $separator ]的习惯用法。 - corsiKa
3
除了用于在文本中添加分隔符之外,还有什么其他用途?实际上有那么多吗?换句话说,这种操作是否应该成为一个众所周知的习语,或者我们应该只使用已经为我们实现了此功能的库? - Kevin Bourrillion
我认为在构建协议时,它已经超越了简单的文本处理,因为二进制数据具有分隔符和特定的格式(就像正则表达式比匹配有效电子邮件更强大一样)。甚至更复杂的文本处理,例如构建由“&”分隔的查询字符串,除了最后一个元素。 - Dougnukem
相关:https://dev59.com/-XNA5IYBdhLWcg3wKae3 - finnw
18个回答

44

我通常这样写:

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

1
我认为这正是我所追求的优雅风格,我花了一些时间才意识到sep被用于在循环体进入时改变状态。 - Dougnukem
1
我认为这个解决方案是最好的,因为它不在循环内包含任何条件检查,而只是具有重复的赋值sep =“,”,如果您正在迭代一个大列表,那么我想这将是最有效的解决方案。 - Dougnukem
@Dougnukem:我认为这里提出的几乎所有替代方案都会表现得非常相似,JIT会处理任何可能存在的显著差异。您需要进行测试以确认这实际上是最有效的解决方案。并不是我不喜欢这个解决方案,相反地。 - Vinko Vrsalovic
1
这是一个聪明的方法,没有条件检查。虽然需要一点注意才能注意到 sep 的简单技巧。 - Anshul
有人能解释一下这个代码是如何防止最后一个项目被处理成“bat”,符合原帖的要求吗? - Chris
2
@Peavers 它正在将分隔符附加到项目之前。第一次运行时,分隔符是一个空字符串。 - Joe McGrath

28

这些答案中有许多使用for循环,但我发现使用迭代器和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
  }
}

这是Google collections中Joiner采用的方法,我发现它非常易读。


1
我喜欢这种方法,因为特殊情况在循环外有条件地处理,这意味着对于大数据集,条件检查没有影响,对于小数据集而言,条件检查是微不足道的、必要的。 - Dougnukem
2
我不明白这是怎么工作的。我们想要删除最后一个逗号,而不是第一个。第一项不是特殊情况。 - Charlie Dalsass
4
一个逗号分隔的列表可以被视为除了最后一个项以外每个项后面都有逗号的项目,或者是在每个项之前都有逗号的项目。这个解决方案选择了后者。 - gk5885

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

1
需要额外的Java库。 - simpleuser

7

我的惯常做法是测试索引变量是否为零,例如:

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

当然,这仅适用于没有“Array.join(“,”)”方法的语言。;-)

6

我认为将第一个元素视为特殊情况更容易,因为我们很容易知道迭代是否是第一个而不是最后一个。判断某个操作是否是第一次执行并不需要复杂或昂贵的逻辑。

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

是的,这正是我会做的方式(除了我会使用 for(String item : items))。虽然不一定优雅,但简单易读。 - Sean Patrick Floyd

3

我会选择你的第二个例子,即在循环外处理特殊情况,只需更加简单明了地书写:

String itemOutput = "[";

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

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

itemOutput += "]";

3

以下是Java 8的解决方案,如果有需要的人可以参考:

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

2

我喜欢在第一项使用一个标志。

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

2

由于您的情况只涉及处理文本,所以您不需要在循环内使用条件语句。以下是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] = ']';

不要添加条件以避免生成尾随逗号,而是继续生成它(使您的循环简单且无条件)并在最后简单地覆盖它。许多时候,我发现将特殊情况生成为任何其他循环迭代一样会更清晰,然后在最后手动替换它(尽管如果“替换它”的代码超过几行,这种方法实际上可能变得更难阅读)。


2
值得注意的是,如果你的语言中的字符串是不可变的(如C#,Java等),你必须从0到len-2取一个子字符串,而不是替换最后一个字符。在这些情况下,它缺乏一定的优雅。 - bmm6o
确实,解决方案在某种程度上取决于语言的选择。但是如果你想要优雅的话,选择Ruby解决方案:["dog","cat","bat"].join(',') - bta
我很困惑你为什么使用了sprintf而不是strcat。或者更好的选择是strncat - Stephen C
@Stephen C- 没有什么特别的原因,只是我想到的第一个解决方案。绝对不是最有效的。 - bta
如果您有一个零长度的列表,这个解决方案会出错。注意那些边缘情况,那是安全漏洞的来源。 - tylerl
@tylerl- 由于所有的数组都是静态声明的,所以我认为是安全的。如果你正在使用任何外部输入,那么是的,你需要在每个地方进行额外的错误检查和输入验证。不过,我的示例只是为了说明目的而已。 - bta

1

可以使用Java 8 lambda和Collectors.joining()来实现,如下所示 -

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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接