一种简洁的编写循环的方法,可针对集合中的第一个项目进行特殊逻辑处理。

13

经常情况下我需要编写循环,为第一个项目提供特殊情况的处理,但代码似乎从来都不像理想情况下那么清晰。

除了重新设计C#语言之外,编写这些循环的最佳方式是什么?

// this is more code to read then I would like for such a common concept
// and it is to easy to forget to update "firstItem"
foreach (x in yyy)
{
  if (firstItem)
  {
     firstItem = false;
     // other code when first item
  }
  // normal processing code
}

// this code is even harder to understand
if (yyy.Length > 0)
{
   //Process first item;
   for (int i = 1; i < yyy.Length; i++)
   {  
      // process the other items.
   }
}

3
我认为检查布尔值(你的第一个例子)没有任何问题,任何人看到它都能迅速知道你正在做什么。 - JD Isaacks
12个回答

13

如何这样做:

using (var erator = enumerable.GetEnumerator())
{
    if (erator.MoveNext())
    {
        ProcessFirst(erator.Current);
        //ProcessOther(erator.Current); // Include if appropriate.

        while (erator.MoveNext())
            ProcessOther(erator.Current);
    }
}

如果您想的话,可以将其转换为扩展功能:

public static void Do<T>(this IEnumerable<T> source, 
                         Action<T> firstItemAction,
                         Action<T> otherItemAction)
{
   // null-checks omitted

    using (var erator = source.GetEnumerator())
    {
        if (!erator.MoveNext())
            return;

        firstItemAction(erator.Current);

        while (erator.MoveNext())
           otherItemAction(erator.Current);            
    }
}

3
要是有人能够想出一个清晰明了的这个扩展方法名称就好了。 - Ian Ringrose
1
@Ian,DoForFirstThen()怎么样? - Frédéric Hamidi
1
+1:我认为应该称其为“ForEach”,参数名称清楚地说明了正在发生的事情,特别是如果它是ForEach<T>(this IEnumerable<T> source, Action<T> actionForFirstItem, Action<T> actionForAllOtherItems) - Binary Worrier

5

我会倾向于使用一些linq。

using System.Linq;

var theCollectionImWorkingOn = ...

var firstItem = theCollectionImWorkingOn.First();
firstItem.DoSomeWork();

foreach(var item in theCollectionImWorkingOn.Skip(1))
{
    item.DoSomeOtherWork();
}

这似乎有点过头了,只是为了避免在循环中检查一个 bool 标志。 - Phil Hunt
3
应该在检查集合是否有任何元素的前提下,对“firstItem”逻辑进行包装处理... - jball
它易于阅读且具有较低的圆形复杂度。我不确定过度设计在哪里。 - ilivewithian
假设在 foreach 中使用 if 和标记,而不是您的示例中使用的方法,并且在 firstItem 逻辑周围有某种鲁棒性检查,则环路复杂度将与您的示例相同。 - jball

5
你可以尝试以下方法:
collection.first(x=>
{
    //...
}).rest(x=>
{
    //...
}).run();

first / rest 的样子如下:

FirstPart<T> first<T>(this IEnumerable<T> c, Action<T> a)
{
    return new FirstPart<T>(c, a);
}

FirstRest rest<T>(this FirstPart<T> fp, Action<T> a)
{
    return new FirstRest(fp.Collection, fp.Action, a);
}

你需要定义类FirstPart和FirstRest。 FirstRest需要一个类似于以下方式的run方法(Collection、FirstAction和RestAction是属性):

void run()
{
    bool first = true;
    foreach (var x in Collection)
    {
        if (first) {
            FirstAction(x);
            first = false;
        }
        else {
             RestAction(x);
        }
    }
}

2
你可以使用“head”和“tail”代替“first”和“rest”,以使其更具功能性:) - Sergey Mirvoda
我选择这个作为选定的答案,因为它可以解决 https://dev59.com/bXA75IYBdhLWcg3wy8Qi 的一个小扩展。 - Ian Ringrose

4

我经常使用first变量方法,这对我来说似乎很正常。 如果您更喜欢,也可以使用LINQ的First()Skip(1)

var firstItem = yyy.First();
// do the whatever on first item

foreach (var y in yyy.Skip(1))
{
// process the rest of the collection
}

2
在这种情况下,我会像这样使用for循环:
for(int i = 0;  i < yyy.Count; i++){
      if(i == 0){
          //special logic here
      }
}

使用for循环也可以让你在其他情况下执行特殊操作,比如在最后一个项目上,在序列中的偶数项上等等。

2

在我看来,最清晰的方法是:尽量避免对第一个项目使用特殊情况。当然,这并不适用于每种情况,但“特殊情况”可能表明您的程序逻辑比必要的更为复杂。

顺便说一句,我不会编写以下代码:

if (yyy.Length > 0)
{
   for(int i = 1; i <yyy.Length; i++)
   {  
      // ...
   }
}

但是相反
   for(int i = 1; i <yyy.Length; i++)
   {  
      // ...
   }

(这本身就是避免不必要处理特殊情况的简单示例。)

尝试避免第一个项目的特殊情况。 - digEmAll
1
你是指这个网址吗:https://dev59.com/A2855IYBdhLWcg3wXC9-?如果是的话,最好的答案是循环遍历整个列表,并准确地显示我所说的内容。 - Doc Brown

2
你写的方式可能是最干净的方式。毕竟,第一个元素具有特定的逻辑,因此必须以某种方式表示它。

同意。 OP最初的代码片段的第二部分最清晰。 - dashton

1
这是一个稍微简单一些的扩展方法,能够完成任务。它结合了KeithS的解决方案我对相关Java问题的回答
public static void ForEach<T>(this IEnumerable<T> elements,
                              Action<T> firstElementAction,
                              Action<T> standardAction)
{
    var currentAction = firstElementAction;
    foreach(T element in elements)
    {
        currentAction(element);
        currentAction = standardAction;
    }
}

0

这两种算法都可以很好地处理第一个元素,实际上也没有其他不同的方法。如果这种模式重复出现很多次,你可以通过 ForEach() 的重载来隐藏它:

public static void ForEach<T>(this IEnumerable<T> elements, Action<T> firstElementAction, Action<T> standardAction)
{
    var firstItem = true;
    foreach(T element in elements)
    {
        if(firstItem)
        {
            firstItem = false;
            firstElementAction(element)
        }
        else
            standardAction(element)
    }
}

...

//usage
yyy.ForEach(t=>(other code when first item), t=>(normal processing code));

Linq 让代码看起来更简洁:

PerformActionOnFirstElement(yyy.FirstOrDefault());
yyy.Skip(1).ForEach(x=>(normal processing code));

0

虽然我个人不会这样做,但还有另一种方法使用枚举器,它减轻了对条件逻辑的需要。像这样:

void Main()
{
    var numbers = Enumerable.Range(1, 5);
    IEnumerator num = numbers.GetEnumerator();

    num.MoveNext();
    ProcessFirstItem(num.Current); // First item

    while(num.MoveNext()) // Iterate rest
    {
        Console.WriteLine(num.Current);
    }

}

    void ProcessFirstItem(object first)
    {
        Console.WriteLine("First is: " + first);
    }

示例输出将是:

First is: 1
2
3
4
5

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