特殊套管最后一个元素的最佳循环成语

Posted

技术标签:

【中文标题】特殊套管最后一个元素的最佳循环成语【英文标题】:Best Loop Idiom for special casing the last element 【发布时间】:2011-03-07 13:03:54 【问题描述】:

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

是否有一些最佳实践习惯用法或优雅的形式不需要重复代码或在 if、else 循环中插入。

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

例如List = ("狗", "猫", "蝙蝠")

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

我介绍了两种方法

    带条件的 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;
     
    

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


【问题讨论】:

值得注意的是,Java 类 java.util.AbstractCollection 在第 1 节中使用了 [ while(all) $value if(not_last) $separator ] 成语。 这个除了附加文本和分隔符还有什么用?真的有那么多吗?换句话说,这真的应该成为一个众所周知的习惯用法,还是我们都应该使用已经为我们实现了它的库? 我认为就构建协议而言,它超越了简单的文本处理,其中二进制数据具有分隔符和某些指定格式(就像正则表达式比匹配有效的电子邮件更强大)。甚至更复杂的文本处理,例如构建由 '&' 分隔的查询字符串,除了最后一个元素。 相关:***.com/questions/1068110/… 【参考方案1】:

...

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

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

res += "]";

左右是相当可读的。当然,您可以将条件放在单独的if 子句中。它使惯用的(至少我认为如此)是它使用 foreach 循环并且不使用复杂的循环头。

此外,没有重复的逻辑(即只有一个地方将来自items 的项目实际附加到输出字符串 - 在现实世界的应用程序中,这可能更复杂且冗长的格式化操作,所以我不想重复代码。

【讨论】:

+1 我发现这很优雅,因为测试 res.length 比检测第一次或最终迭代并在特定迭代期间表现不同的语义更好。即使每次迭代都有可能不向列表中添加任何项目,上面的代码也可以工作。 -1 用于在循环中使用字符串 +=。当您每次重建字符串时,您刚刚将 n 阶运算转换为 n 阶平方运算。 :-(【参考方案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 += "]";

【讨论】:

【参考方案3】:

第三种选择如下

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,Apache Commons 中的 join() method(从第 3232 行开始)确实在循环中使用了 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();
    

【讨论】:

这里有关于java join的讨论:***.com/questions/794248/… output.append(items[i]).append(','); 应该更快,因为它避免了中间字符串的创建。【参考方案4】:

如果您要像这样动态构建字符串,则不应使用 += 运算符。 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());

【讨论】:

【参考方案5】:

我通常这样写一个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();                           
       

【讨论】:

如果您出于某种原因碰巧不提前知道长度,您总是可以说在项目前添加一个逗号,除非它是第一项。 在 Java 中,您总是知道数组的大小。在 Java 5 中,您还知道 vararg 的大小(如“void functionName(String...args)”),因为 vararg 是具有简化符号的数组。使用列表,您可以使用方法 size() 和 get() 来获得相同的结果。集合的唯一问题是为使用的实现使用正确的算法。 IE。我的算法使用 ArrayList 会更有效,但使用 LinkedList 会效率低下。【参考方案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))。不一定优雅但简单易读【参考方案7】:

在这种情况下,您实际上是使用一些分隔符字符串连接字符串列表。你也许可以自己写一些东西来做到这一点。然后你会得到类似的东西:

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() 方法来检查这是否是最后一个。

【讨论】:

你应该使用 StringBuilder 而不是 StringBuffer。它更快,因为它避免了同步的开销【参考方案8】:

由于您的案例只是处理文本,因此您不需要循环内的条件。一个 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] = ']';

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

【讨论】:

值得注意的是,如果您的语言中的字符串是不可变的(C#、Java 等),您必须从 0 到 len-2 取一个子字符串,而不是替换最后一个字符。在这些情况下,它缺乏某种优雅。 没错,解决方案部分取决于语言的选择。虽然如果您想要优雅,请使用 Ruby 解决方案:["dog","cat","bat"].join(',') 我很困惑为什么你使用sprintf 而不是strcat。或者更好的是,strncat @Stephen C- 没有真正的原因,只是我想到的第一个解决方案。绝对不是最有效的。 如果你有一个零长度的列表,这个解决方案会失败。观察那些极端案例,这就是安全漏洞的来源。【参考方案9】:
string value = "[" + StringUtils.join( items, ',' ) + "]";

【讨论】:

需要额外的 java 库。【参考方案10】:

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

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

result += " ]";

当然,这只是在我们谈论没有某些 Array.join(", ") 方法的语言时。 ;-)

【讨论】:

【参考方案11】:

我通常是这样写的:

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

【讨论】:

我认为这就是我正在寻找的优雅类型,我花了一秒钟才意识到 sep 在进入循环体时用于更改状态。 我认为这个解决方案是最好的,因为它在循环内不包含任何条件检查,而只是有一个重复的赋值 sep = ",",我想这将是最有效的如果您正在迭代一个大列表,则解决方案。 @Dougnukem:我认为这里介绍的几乎所有替代方案的表现都非常相似,JIT 会处理任何潜在的显着差异。您必须进行测试以确认这实际上是最有效的解决方案。相反,并不是我不喜欢这个解决方案。 这在没有条件检查的情况下很聪明。虽然需要注意sep的简单技巧 @Peavers 它在项目前附加分隔符。分隔符是第一次绕过的空字符串。【参考方案12】:

我会选择你的第二个例子,即。处理循环外的特殊情况,写得简单一点:

String itemOutput = "[";

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

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


itemOutput += "]";

【讨论】:

【参考方案13】:

如果您只是在寻找这样的逗号分隔列表:“[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);

你会得到: [弗兰克,哈尔]

【讨论】:

【参考方案14】:

一般来说,我最喜欢的是多级出口。改变

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 就是这种情况。

Here's a paper 来自无耻地向他的学生推广它的教授 :-)。请阅读第 5 节,了解您在说什么。

【讨论】:

【参考方案15】:

这些答案中有很多 for 循环,但我发现 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在谷歌收藏中采用的方法,我觉得它非常易读。

【讨论】:

我喜欢这种方法,因为特殊情况是在循环之外有条件地处理,这意味着条件检查对大型数据集没有影响,而对于小型数据集来说是微不足道且必要的。 我不明白这是如何工作的。我们要删除最后一个逗号,而不是第一个。第一项不是特例。 一个逗号分隔的列表可以被看作是在每个之后除了最后一个之外有逗号的项目,或者在每个之前除了第一个之外有逗号的项目。本方案选择后者。【参考方案16】:

我认为这个问题有两个答案:任何语言中这个问题的最佳习语,以及 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# 函数的解释,请询问,我会添加它)

【讨论】:

【参考方案17】:

Java 8 解决方案,以防有人在寻找它:

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

【讨论】:

【参考方案18】:

可以使用 Java 8 lambda 和 Collectors.joining() as 来实现 -

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

【讨论】:

以上是关于特殊套管最后一个元素的最佳循环成语的主要内容,如果未能解决你的问题,请参考以下文章

在 Node.JS 中应该避免循环还是有特殊的方法来处理它们?

java中for循环的特殊:foreach的使用

C#语言基础——特殊集合

ten 特殊集合与函数

ArrayList集合&特殊集合

特殊字体的兼容--图片代替