【问题标题】:Less-verbose way of handling the first pass through a foreach?处理第一次通过 foreach 的不那么冗长的方式?
【发布时间】:2010-02-21 23:39:11
【问题描述】:

我经常发现自己在 foreach 循环中执行以下 index-counter messiness 以确定我是否在 第一个元素 上。在 C# 中是否有一种更优雅的方式来做到这一点,类似于 if(this.foreach.Pass == 1) 等?

int index = 0;
foreach (var websitePage in websitePages) {
    if(index == 0)
        classAttributePart = " class=\"first\"";
    sb.AppendLine(String.Format("<li" + classAttributePart + ">" + 
        "<a href=\"{0}\">{1}</a></li>", 
        websitePage.GetFileName(), websitePage.Title));
    index++;
}

【问题讨论】:

  • 我认为老实说方法很好,很简单等。你看过火花视图引擎吗?只是猜测你在做什么...... PK :-)
  • 只是一个快速的想法......你可以单独处理第一个案例,然后在循环中处理其余的案例吗? (在 do while 循环中的相同想法)
  • @Paul 是的 scott hanselman 在 spark 视图引擎上的播客很好,感谢提醒,将重访:hanselminutes.com/default.aspx?showID=210
  • 与其在你的第一个 li 中添加一个类,为什么不在你的 CSS 中使用 li:first-child 呢?

标签: c# foreach


【解决方案1】:

另一种方法是接受“丑陋的部分”必须在某个地方实现,并提供隐藏“丑陋的部分”的抽象,这样您就不必在多个地方重复它,而可以专注于特定的算法.这可以使用 C# lambda 表达式来完成(如果您受限于 .NET 2.0,则可以使用 C# 2.0 匿名委托):

void ForEachWithFirst<T>(IEnumerable<T> en, 
     Action<T> firstRun, Action<T> nextRun) {
  bool first = true;
  foreach(var e in en) {
    if (first) { first = false; firstRun(e); } else nextRun(e);
  }
}

现在您可以使用这种可重用的方法来实现您的算法,如下所示:

ForEachWithFirst(websitePages,
  (wp => sb.AppendLine(String.Format("<li class=\"first\">" +
         "<a href=\"{0}\">{1}</a></li>", wp.GetFileName(), wp.Title)))
  (wp => sb.AppendLine(String.Format("<li>" + 
         "<a href=\"{0}\">{1}</a></li>", wp.GetFileName(), wp.Title))) );

您可以根据确切的重复模式设计不同的抽象。好消息是——多亏了 lambda 表达式——抽象的结构完全取决于你。

【讨论】:

  • 出于可读性原因,我宁愿坚持使用原始帖子中的索引方法。一般来说,我不太喜欢 lambda 表达式,因为它们通常很难阅读,除非它们的格式非常好(或单行)。我几乎不使用它们(除了 IEnumerable 扩展方法),在上述情况下,我认为它们使事情变得更加复杂。只是为了节省一两行代码。但是,这是我个人的看法,我敢肯定,还有其他意见。 :)
【解决方案2】:

略显冗长:

string classAttributePart = " class=\"first\"";
foreach (var websitePage in websitePages)
{
    sb.AppendLine(String.Format("<li" + classAttributePart + "><a href=\"{0}\">{1}</a></li>", websitePage.GetFileName(), websitePage.Title));
    classAttributePart = string.Empty;
}

如果您使用的是 .NET 3.5,则可以使用 Select 的重载来为您提供索引并对其进行测试。那么你也不需要StringBuilder。这是代码:

string[] s = websitePages.Select((websitePage, i) =>
        String.Format("<li{0}><a href=\"{1}\">{2}</a></li>\n",
                      i == 0 ? " class=\"first\"" : "",
                      websitePage.GetFileName(),
                      websitePage.Title)).ToArray();

string result = string.Join("", s);

它看起来有点冗长,但这主要是因为我将很长的行打破了许多较短的行。

【讨论】:

  • 尽管我是 LINQ 的忠实粉丝,但我不同意这段代码比原来的代码更好。我怀疑地球上是否有任何开发人员会发现它更易于阅读。
  • +1 表示第一个建议,但它仅在感兴趣的索引为 0 时才有效。
  • 第一个建议+1。它的另一个好处是它消除了循环内错误导致“init”内容执行次数不适当的可能性。
【解决方案3】:
if (websitePages.IndexOf(websitePage) == 0)
    classAttributePart = " class=\"last\"";

这可能更优雅,但性能可能会降低,因为它必须检查每个元素的索引。

【讨论】:

    【解决方案4】:

    这可能稍微更好

    bool doInit = true;
    foreach (var websitePage in websitePages)
    {
        if (doInit)
        {
            classAttributePart = " class=\"first\"";
            doInit = false;
        }
        sb.AppendLine(String.Format("<li" + classAttributePart + "><a href=\"{0}\">{1}</a></li>", websitePage.GetFileName(), websitePage.Title));
    }
    

    我最终也经常做这种事情,这也让我很烦恼。

    【讨论】:

    • 你最好把doInit = false;在 if 的主体中,因此不会每次都分配它。当然,这种性能优化在 web 环境中并不重要。
    • @Stilgar:你是对的,通常我会这样做,但我是在模仿他的原始代码。我会改的。
    • 我也在考虑建议这种方法,但它和原来的一样冗长,而且只适用于第一个元素。
    【解决方案5】:

    您可以使用for 循环而不是foreach 循环。在这种情况下,您的 for 循环可以从 1 开始它的索引,如果长度大于 0,您可以在循环之外执行第一个元素。

    至少在这种情况下,您不会在每次迭代中进行额外的比较。

    【讨论】:

    • 是的,我想知道为什么其他人似乎都坚持使用带有手动计数/标志的 foreach 来查找第一个?这样做是否比使用直接 for 循环或其他方法更快?
    • 如果您要使用迭代器并保持计数,我肯定效率会降低。
    • 如果您只有一个 IEnumerable 或其他不支持按索引访问的类,则不能使用 for 循环。例如,像一个链表。
    • 是的,在某些情况下这个答案不适用。
    【解决方案6】:

    另一种方法是使用jQuery's first selector 来设置类而不是服务器端代码。

    $(document).ready(function(){ 
         $("#yourListId li:first").addClass("first");
    }
    

    【讨论】:

    • +1 因为这在我生成网站代码的特定实例中很有帮助,不知道如何在 CSS 中执行此操作,很好地提醒 jquery 可以做到,可以切换它
    • 很高兴为您提供帮助。这就是 jQuery(和其他 javascript 框架)的设计目的。
    【解决方案7】:

    如果您只对第一个索引执行此操作,则可以在 foreach 循环之前执行此操作。

    【讨论】:

      【解决方案8】:

      如果您只对 first 元素感兴趣,最好的(在最易读的)方法是使用 LINQ 找出第一个元素。像这样:

      var first = collection.First();
      // do something with first element
      ....
      foreach(var item in collection){
          // do whatever you need with every element
          ....
          if(item==first){
              // and you can still do special processing here provided there are no duplicates
          }
      }
      

      如果你需要索引的数值,或者非第一个索引,你总是可以做

      foreach (var pair in collection.Select((item,index)=>new{item,index}))
      {
          // do whatever you need with every element
          ....
          if (pair.index == 5)
          {
              // special processing for 5-th element. If you need to do this, your design is bad bad bad
          }
      }
      

      PS 最好的方法是使用for-loop。仅当 for 不可用时才使用 foreach(即集合是 IEnumerable 而不是列表或其他内容)

      【讨论】:

        【解决方案9】:

        这个怎么样?

        var iter = websitePages.GetEnumerator();
        iter.MoveNext();
        //Do stuff with the first element
        do {
            var websitePage = iter.Current;
            //For each element (including the first)...
        } while (iter.MoveNext());
        

        【讨论】:

          【解决方案10】:

          正如我父亲喜欢说的,“为工作使用正确的工具”。

          在这种情况下,看看你的循环需要做什么。

          • 如果它是可以在循环之外完成的事情,移动它并且您使用 foreach 就可以了。
          • 如果您需要处理复杂的案例组合,那么您可能需要考虑为您想要在循环内执行的操作使用不同的模式(例如:状态模式或任何最合适的模式),在这种情况下您可以选择当时最有意义的循环结构。
          • 如果您的循环依赖于能够提取迭代索引以将其传递到其他地方,那么 for 循环可能是更好的选择。

          否则,要回答您的问题,似乎没有一种简单的非详细方法来识别在 foreach 循环中访问的列表项的索引。

          【讨论】:

            【解决方案11】:
            public static class ExtenstionMethods
            {
                public static IEnumerable<KeyValuePair<Int32, T>> Indexed<T>(this IEnumerable<T> collection)
                {
                    Int32 index = 0;
            
                    foreach (var value in collection)
                    {
                        yield return new KeyValuePair<Int32, T>(index, value);
                        ++index;
                    }
                }
            }
            
            foreach (var iter in websitePages.Indexed())
            {
                var websitePage = iter.Value;
                if(iter.Key == 0) classAttributePart = " class=\"first\"";
                sb.AppendLine(String.Format("<li" + classAttributePart + "><a href=\"{0}\">{1}</a></li>", websitePage.GetFileName(), websitePage.Title));
            }
            

            【讨论】:

              【解决方案12】:

              在本例中,您可以通过这种方式摆脱索引检查。

              foreach (var websitePage in websitePages) 
              { 
                  classAttributePart =  classAttributePart ?? " class=\"first\""; 
                  sb.AppendLine(String.Format("<li" + classAttributePart + "><a href=\"{0}\">{1}</a></li>", websitePage.GetFileName(), websitePage.Title)); 
              } 
              

              最好检查结果数据变量以执行此类任务。 在这种情况下,它检查 classAttributePart 字符串的 null 并附加初始值。

              【讨论】:

                猜你喜欢
                • 1970-01-01
                • 2017-06-22
                • 2017-02-23
                • 2015-10-31
                • 2014-01-17
                • 2016-01-24
                • 1970-01-01
                • 2023-02-15
                • 1970-01-01
                相关资源
                最近更新 更多