Edward Tanguay
Edward Tanguay

Reputation: 193462

Is there a more elegant way to act on the first and last items in a foreach enumeration than count++?

Is there a more elegant way to act on the first and last items when iterating through a foreach loop than incrementing a separate counter and checking it each time?

For instance, the following code outputs:

>>> [line1], [line2], [line3], [line4] <<<

which requires knowing when you are acting on the first and last item. Is there a more elegant way to do this now in C# 3 / C# 4? It seems like I could use .Last() or .First() or something like that.

using System;
using System.Collections.Generic;
using System.Text;

namespace TestForNext29343
{
    class Program
    {
        static void Main(string[] args)
        {
            StringBuilder sb = new StringBuilder();
            List<string> lines = new List<string>
            {
                "line1",
                "line2",
                "line3",
                "line4"
            };
            int index = 0;
            foreach (var line in lines)
            {
                if (index == 0)
                    sb.Append(">>> ");

                sb.Append("[" + line + "]");

                if (index < lines.Count - 1)
                    sb.Append(", ");
                else
                    sb.Append(" <<<");

                index++;
            }

            Console.WriteLine(sb.ToString());
            Console.ReadLine();
        }
    }
}

Upvotes: 2

Views: 623

Answers (11)

Brian Gideon
Brian Gideon

Reputation: 48969

Try the following code.

foreach (var item in ForEachHelper.WithIndex(collection))
{
    Console.Write("Index=" + item.Index);
    Console.Write(";Value= " + item.Value);
    Console.Write(";IsLast=" + item.IsLast);
    Console.WriteLine();
}

Here is the code for the ForEachHelper class.

public static class ForEachHelper
{
    public sealed class Item<T>
    {
        public int Index { get; set; }
        public T Value { get; set; }
        public bool IsLast { get; set; }
    }

    public static IEnumerable<Item<T>> WithIndex<T>(IEnumerable<T> enumerable)
    {
        Item<T> item = null;
        foreach (T value in enumerable)
        {
            Item<T> next = new Item<T>();
            next.Index = 0;
            next.Value = value;
            next.IsLast = false;
            if (item != null)
            {
                next.Index = item.Index + 1;
                yield return item;
            }
            item = next;
        }
        if (item != null)
        {
            item.IsLast = true;
            yield return item;
        }            
    }
}

Upvotes: 1

Panagiotis Kanavos
Panagiotis Kanavos

Reputation: 131728

List uses an array internally to store items (called _items), so lines[i] is essentially as fast as accessing an array member. Enumerable.First() and Enumerable.Last() access the first and last members of the List using the Lists's indexer, so lines.First() is essentially lines[0] and lines.Last is essentially lines[lines.Count-1], plus some range checking.

What this means is that the cost of line==lines.First() amounts to an array member reference plus a reference comparison. Unless you perform a LOT of iterations, this shouldn't bother you.

If you need something faster you can use a LinkedList. In this case First() and Last() return the first and last items directly, but that seems like overkill.

Upvotes: 0

Dan Bryant
Dan Bryant

Reputation: 27515

For the general question of how to handle First and Last cases differently when you only have an IEnumerable<T>, one way you can do this is by using the enumerator directly:

    public static void MyForEach<T>(this IEnumerable<T> items, Action<T> onFirst, Action<T> onMiddle, Action<T> onLast)
    {
        using (var enumerator = items.GetEnumerator())
        {
            if (enumerator.MoveNext())
            {
                onFirst(enumerator.Current);
            }
            else
            {
                return;
            }

            //If there is only a single item in the list, we treat it as the first (ignoring middle and last)
            if (!enumerator.MoveNext())
                return;

            do
            {
                var current = enumerator.Current;
                if (enumerator.MoveNext())
                {
                    onMiddle(current);
                }
                else
                {
                    onLast(current);
                    return;
                }
            } while (true);
        }
    }

Upvotes: 4

Billy ONeal
Billy ONeal

Reputation: 106609

My C# is a bit rusty, but it should be something like:

StringBuilder sb;
List<string> lines = ....;
sb.Append(">>> [").Append(lines[0]);
for (int idx = 1; idx < lines.Count; idx++)
    sb.Append("], [").Append(lines[idx]);
sb.Append("] <<<");

It's much easier to exclude the first item from the loop (by starting the index at one) than it is to exclude the last. I got this idea from Eric Lippert's blog some time ago but I can't for the life of me find the post at the moment....

Upvotes: 0

Anax
Anax

Reputation: 9382

Why not just append at the end of the foreach loop if the StringBuilder isn't empty?

...
for (int i=0; i<lines.Count; i++)
{
    sb.Append("[" + lines[i] + "]");

    if (i < lines.Count - 1)
        sb.Append(", ");

}

if (sb.Length != 0)
{
    sb.Insert(0, ">>> ");
    sb.Append(" >>>");
}

Upvotes: 0

Carra
Carra

Reputation: 17964

Your current example can be done without iterating.

Console.WriteLine(">>> " + String.Join(lines, ", ") + " <<<);

If you're just iterating I find it easier to just replace it with a regular for loop and check the boundaries.

for(int i=0; i<list.count; i++)
{
  if(i == 0)
   //First one
  else if(i == list.count -1)
   //Last one
}

It'll be a lot faster than using the .First() and .Last() extension methods. Besides, if you have two items in your list with the same (string) value comparing to Last or First won't work.

Upvotes: 7

Adrian Regan
Adrian Regan

Reputation: 2250

You could do the following:

Console.WriteLine(">>>> [" + String.Join("], [", lines.ToArray()) + "] <<<<");

I know this does not answer your question but it solves your problem...

Upvotes: 0

Stephen Cleary
Stephen Cleary

Reputation: 457382

There may be a more "elegant" way of coding it using First and Last, but the inefficiency makes it not worth it.

I coded up my own Join operator for IEnumerable<string>s (from Nito.KitchenSink). It's fully reusable (in .NET 3.5 or 4.0):

/// <summary>
/// Concatenates a separator between each element of a string enumeration.
/// </summary>
/// <param name="source">The string enumeration.</param>
/// <param name="separator">The separator string. This may not be null.</param>
/// <returns>The concatenated string.</returns>
public static string Join(this IEnumerable<string> source, string separator)
{
    StringBuilder ret = new StringBuilder();
    bool first = true;
    foreach (string str in source)
    {
        if (first)
        {
            first = false;
        }
        else
        {
            ret.Append(separator);
        }

        ret.Append(str);
    }

    return ret.ToString();
}

/// <summary>
/// Concatenates a sequence of strings.
/// </summary>
/// <param name="source">The sequence of strings.</param>
/// <returns>The concatenated string.</returns>
public static string Join(this IEnumerable<string> source)
{
    return source.Join(string.Empty);
}

Upvotes: 0

Dan Joseph
Dan Joseph

Reputation: 685

I would recommend putting your >>> and <<< outside of the loop.

StringBuilder sb = new StringBuilder();
sb.Append(">>> ");

bool first = true;
foreach(var line in lines)
{
    if (!first) sb.Append(", ");
    sb.Append("[" + line + "]");
    first = false;
}
sb.Append(" <<<");

You could also use a String.Join instead of a foreach loop.

String.Join(", ", lines);

Upvotes: 0

Gregoire
Gregoire

Reputation: 24872

Not answering your question but for your purpose I would use

return String.Format(">>> {0} <<<",String.Join(lines.ToArray(),","));

Upvotes: 2

Bob Fincheimer
Bob Fincheimer

Reputation: 18076

int i, iMax;
i = iMax = lines.length;
sb.Append(">>> "); // ACT ON THE FIRST ITEM
while(i--) {
  sb.Append("[" + lines[length - i] + "]"); // ACT ON ITEM
  if(i) {
    sb.Append(", ");  // ACT ON NOT THE LAST ITEM
  } else {
    sb.Append(" <<<");  // ACT ON LAST ITEM
  }
}

I usually take this approach, handle one boundary before the loop, then handle the other inside the loop. And i also like decrementing instead of incrementing. I think it is mostly user preference though...

Upvotes: 0

Related Questions