views:

198

answers:

11

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();
        }
    }
}
A: 
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...

Bob Fincheimer
+7  A: 

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.

Carra
+1 for string join -- but there are better ways than inserting a branch inside the FOR to handle this problem.
Billy ONeal
For this problem sure, you can add the ">>> " and " <<<" before and after the loop.
Carra
Answered the example, not the general question...
Henk Holterman
@Henk Holterman: The answer to the general question is to use a for loop. That doesn't mean that it's the best for the OP given this problem.
Billy ONeal
+1  A: 

Not answering your question but for your purpose I would use

return String.Format(">>> {0} <<<",String.Join(lines.ToArray(),","));
Gregoire
+1 for string join -- but there are better ways than inserting a branch inside the FOR to handle this problem.
Billy ONeal
A: 

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);
Daniel Joseph
A: 

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);
}
Stephen Cleary
There are better ways to do this than using a first flag.
Billy ONeal
@Billy: such as your answer? Is it much "better" than using `foreach`?
Stephen Cleary
@Stephen Cleary: Yes, because there is no branch required inside the loop.
Billy ONeal
@Billy: but mine handles `IEnumerable<string>`. I suppose I could do the same by pulling out the `IEnumerator<string>`, though...
Stephen Cleary
@Stephen Cleary: Sounds like a job for <s>Template Metaprogramming</s>Generics! :)
Billy ONeal
A: 

You could do the following:

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

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

Adrian Regan
A: 

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(" >>>");
}
Anax
A: 

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....

Billy ONeal
Fails for empty lists.
Sorin Comanescu
@Sorin Comanescu: So put one if statement above it. Considering the lines variable is supplied in the code, I didn't think one needed to bother to check for empty lists. The point here is to demonstrate the concept, not provide a drop-in piece of code to use.
Billy ONeal
I thought starting at index 1 was part of the "optimizing" strategy, otherwise it'd be pretty close to OP's implementation.
Sorin Comanescu
@Sorin Comanescu: What does that have to do with empty lists? A list with one element is taken care of by the statements before and after the loop. The advantage here is that you have removed the branch dependency inside the loop.
Billy ONeal
You removed the branch by directly accessing the 0-indexed element, which fails for empty lists.
Sorin Comanescu
@Sorin Comanescu: No, you do need a branch if you need to support empty lists. The difference here is that the branch is outside the loop, rather than inside it.
Billy ONeal
http://blogs.msdn.com/b/ericlippert/archive/2009/04/13/restating-the-problem.aspx and http://blogs.msdn.com/b/ericlippert/archive/2009/04/15/comma-quibbling.aspx
Eric Lippert
Thanks Mr. Lippert :)
Billy ONeal
+3  A: 

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);
        }
    }
Dan Bryant
A: 

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.

Panagiotis Kanavos
A *reference* comparison?? Aren't line and lines.First() both of type string? And isn't == overloaded for strings to compare the actual string values?
Conrad Albrecht
Not exactly. Equals first checks for reference equality, then checks for string equality. From Reflector: public static bool Equals(string a, string b) { return ((a == b) || (((a != null) }
Panagiotis Kanavos
A: 

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;
        }            
    }
}
Brian Gideon