如何简洁地处理 foreach 的第一次循环?

22

在 foreach 循环中,我经常发现自己需要做以下索引计数混乱的操作,以判断当前是否为第一个元素。在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++;
}

我认为方法还不错,简单易用等等。 你看过Spark视图引擎吗?猜测一下你在做什么... PK :-) - Paul Kohler
只是一个快速的想法...你能否单独处理第一个情况,然后在循环中处理其余的情况?(使用 do while 循环实现同样的想法) - aggietech
@Paul,是的,Scott Hanselman在Spark视图引擎上有一个很好的播客节目,谢谢你提醒,我会重新访问:http://www.hanselminutes.com/default.aspx?showID=210 - Edward Tanguay
4
为什么不在CSS中使用li:first-child,而要给第一个li添加一个类呢? - Joel Mueller
12个回答

13

另一种方法是接受“丑陋部分”必须在某个地方实现,并提供一个抽象来隐藏“丑陋部分”,这样您就不必在多个地方重复它,可以专注于特定算法。可以使用 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表达式的存在,抽象层的结构完全由您自己决定。

1
出于可读性的考虑,我宁愿坚持原始帖子中的索引方法。一般来说,我不太喜欢lambda表达式,因为它们往往很难阅读,除非它们真的很好格式化(或是一行代码)。我几乎从不使用它们(除了IEnumerable<T>扩展方法),在上面的例子中,我认为它们让事情更加复杂。仅仅为了节省一两行代码而已。然而,这是我的个人观点,我相信还有其他观点存在 :) - gehho

11
稍微简洁一些:
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);

看起来有点啰嗦,但这主要是因为我把非常长的一行分成了许多短行。


9
虽然我非常喜欢LINQ,但我不认同这段代码比原始代码更好。我怀疑世界上没有一个开发者会觉得这段代码更易读。 - Stilgar
1
+1 对于第一个建议,尽管它仅适用于感兴趣的索引为0。 - Joel
+1 第一个建议。它的另一个好处是,它消除了循环内部出现错误导致“init”操作被执行不恰当次数的可能性。 - kyoryu

4
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
@Stilgar:你说得对,通常我会这样做,但我是在模仿他的原始代码。我会改正它。 - John Knoeller
我也在考虑建议这种方法,但它与原始方法几乎一样冗长,并且仅适用于第一个元素。 - Joel

4
你可以使用for循环代替foreach循环。在这种情况下,你的for循环可以从1开始索引,并且如果长度大于0,则可以在循环外处理第一个元素。
至少在这种情况下,你不需要在每次迭代中进行额外的比较。

是啊,我在想为什么其他人似乎都卡在使用foreach和手动计数/标志来查找第一个的问题上?那样做是否比使用直接的for循环还要快? - slugster
如果您要使用迭代器并保持计数,那么效率肯定会降低。 - Brian R. Bondy
2
如果你只拥有一个IEnumerable或其他不支持按索引访问的类,例如链接列表,那么你无法使用for循环。 - Nevermind
在某些情况下,这个答案可能不适用。 - Brian R. Bondy

3
另一种方法是使用 jQuery的第一个选择器设置类,而不是使用服务器端代码。
$(document).ready(function(){ 
     $("#yourListId li:first").addClass("first");
}

+1,因为这对于我生成网站代码很有帮助,以前不知道如何在CSS中实现,这是一个很好的提醒,jQuery可以完成它,可能会进行切换。 - Edward Tanguay
很高兴能帮忙。这就是jQuery(和其他JavaScript框架)设计的类型。 - jrummell

2
如果您只对第一个元素感兴趣,最好(也就是最易读的方法)是使用 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循环。只有在for不可用时(即集合是IEnumerable而不是列表或其他类型),才使用foreach


2
如果你只是针对第一个索引进行操作,可以在foreach循环之前完成操作。

1

这个怎么样?

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

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

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