IEnumerable foreach, do something different for the last element
Asked Answered
T

6

12

I have an IEnumerable<T>. I want to do one thing for each item of the collection, except the last item, to which I want to do something else. How can I code this neatly? In Pseudocode

foreach (var item in collection)
{
    if ( final )
    {
        g(item)
    }
    else
    {
        f(item)
    }
}

So if my IEnumerable were Enumerable.Range(1,4) I'd do f(1) f(2) f(3) g(4). NB. If my IEnumerable happens to be length 1, I want g(1).

My IEnumerable happens to be kind of crappy, making Count() as expensive as looping over the whole thing.

Tritanopia answered 24/5, 2012 at 10:32 Comment(0)
J
23

Since you mention IEnumerable[<T>] (not IList[<T>] etc), we can't rely on counts etc: so I would be tempted to unroll the foreach:

using(var iter = source.GetEnumerator()) {
    if(iter.MoveNext()) {
        T last = iter.Current;
        while(iter.MoveNext()) {
            // here, "last" is a non-final value; do something with "last"
            last = iter.Current;
        }
        // here, "last" is the FINAL one; do something else with "last"
    }
}

Note the above is technically only valid for IEnuemerable<T>; for non-generic, you'd need:

var iter = source.GetEnumerator();
using(iter as IDisposable) {
    if(iter.MoveNext()) {
        SomeType last = (SomeType) iter.Current;
        while(iter.MoveNext()) {
            // here, "last" is a non-final value; do something with "last"
            last = (SomeType) iter.Current;
        }
        // here, "last" is the FINAL one; do something else with "last"
    }
}
Jamnes answered 24/5, 2012 at 10:38 Comment(1)
@OJay nope; the first element is processed once we know it isn't the final element, so: when we have read the second - note that in the first comment we work with last, aka the previous item, not the current oneJamnes
B
3

Similar to Marc's answer, but you could write an extension method to wrap it up.

public static class LastEnumerator
{
    public static IEnumerable<MetaEnumerableItem<T>> GetLastEnumerable<T>(this IEnumerable<T> blah)
    {
        bool isFirst = true;
        using (var enumerator = blah.GetEnumerator())
        {
            if (enumerator.MoveNext())
            {
                bool isLast;
                do
                {
                    var current = enumerator.Current;
                    isLast = !enumerator.MoveNext();
                    yield return new MetaEnumerableItem<T>
                        {
                            Value = current,
                            IsLast = isLast,
                            IsFirst = isFirst
                        };
                    isFirst = false;
                } while (!isLast);
            }
        }

    }
}

public class MetaEnumerableItem<T>
{
    public T Value { get; set; }
    public bool IsLast { get; set; }
    public bool IsFirst { get; set; }
}

Then call it like so:

foreach (var row in records.GetLastEnumerable())
{
    output(row.Value);
    if(row.IsLast)
    {
        outputLastStuff(row.Value);
    }
}
Bonsai answered 19/2, 2013 at 2:36 Comment(1)
With this overhead, I wonder if using .ToList() and iterating by index would be more efficient.Zealous
F
1

If you want to do this as efficiently as possible there is no other choice than effectively looking at not only the current but also the "next" or "previous" item, so you can defer the decision of what to do after you have that information. For example, assuming T is the type of items in the collection:

if (collection.Any()) {
    var seenFirst = false;
    T prev = default(T);
    foreach (var current in collection) {
        if (seenFirst) Foo(prev);
        seenFirst = true;
        prev = current;
    }
    Bar(prev);
}

See it in action.

Flinty answered 24/5, 2012 at 10:41 Comment(2)
reverse and tolist are both buffering operations... for a long sequence, this could be really really painful.Jamnes
@MarcGravell: Sure. I thought to give the quick and dirty solutions first and the good one last, but it turns out the good one isn't any longer than the quickies so out the window they go. :)Flinty
A
0

I wouldn't really recommend it, but I guess you could do something like this...

object m_item = notPartOfListFlag = new object();
foreach(var item in enumerator){
   if(m_item != notPartOfListFlag)
   {
      //do stuff to m_item;
   }
   m_item = item;
}
//do stuff to last item aka m_item;

But I would try to use some kind of collection that exposes the position of the items in the list, then use

if(collection.IndexOf(item) == collection.Count-1) do stuff
Apuleius answered 24/5, 2012 at 10:43 Comment(1)
That's true, give me a sec and I'll update it to account for that.Apuleius
T
0

not 100% sure I like this but you could always delay the use of item until you've moved one step in to the IEnumerable array, that way when you get to the end you've not used the last entry.

it avoids having to force a count on the enumerator.

object item = null;
foreach (var a in items)
{
  // if item is set then we can use it.
  if (item != null)
  {
      // not final item
      f(item);
  }
  item = a;
}

// final item.
g(item);
Tewell answered 24/5, 2012 at 10:46 Comment(2)
The initial value doesn't actually have to be null, you could set it to anything that's not a valid value and just check that it's not equal to that. Or use a flag to say it's the first loop.Tewell
It's possible that there are no invalid values.Deary
N
0

This might be a little too much, but I think it's elegant.

Creating an extension method for IEnumerable<T> that takes an Action<T> for each type of item, we can make a loop like this:

int sum = 0;

collection.IEnumForEachItem(

    firstItem =>
    {
        //do things for first item
        sum = firstItem.SomeProperty;
    },

    midItem =>
    {
        doThingsForIntermediate(midItem);
        sum += midItem.SomeProperty * 2;
    },

    lastItem =>
    {
        if (lastItem.SomeProperty > 10);
            sum += 10;
        else
            sum += lastItem.SomeProperty;
    }
);

This is the extension method (it must be in a static class):

public static void IEnumForEachItem<T>(this IEnumerable<T> items, 
    Action<T> actionForFirstItem, Action<T> actionForMidItems, Action<T> actionForLastItem)
{
    using (IEnumerator<T> enumerator = items.GetEnumerator())
    {
        if (enumerator.MoveNext())
            actionForFirstItem(enumerator.Current);

        if (!enumerator.MoveNext())
            throw new InvalidOperationException("IEnumerable must have at least 2 items");


        T currentItem = enumerator.Current;
        while (enumerator.MoveNext())
        {
            actionForMidItems(currentItem);
            currentItem = enumerator.Current;
        }
        actionForLastItem(currentItem);
    }
}

From this it's easy to make methods that take only "first and others" and also "others and last".

Neuter answered 15/3 at 20:14 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.