Reputation: 193462
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
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
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
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
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
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
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
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
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
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
Reputation: 24872
Not answering your question but for your purpose I would use
return String.Format(">>> {0} <<<",String.Join(lines.ToArray(),","));
Upvotes: 2
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