Click here to Skip to main content
15,867,568 members
Articles / Programming Languages / C#

Build A Smarter Loop With C#

Rate me:
Please Sign up or sign in to vote.
5.00/5 (1 vote)
2 Sep 2009CC (ASA 2.5)2 min read 22.2K   24   4
Loops are a standard item in any programmer's toolbox. But as often as they make their way into our code, you have to wonder why you don't see more improvements to using them. This post discusses some ideas to improve your loops by using Extension Methods.

Introduction

One of the first things every programmer learns is how to loop through an array of information. We learn how to do for, foreach, while loops, but then we never really improve on them. For the most part, the standard loop is as far as a programmer goes.

It seems to me that loops should be more intelligent than that. In fact, we often write code that uses the loop for additional information. It isn’t uncommon to see a loop check if it is an even or odd index or the first or last value.

Modestly Smarter Loops

I really like lambdas a lot – in fact, probably, too much. Being able to pass a delegate around like an argument definitely can be abused, but in some cases, it can make for some really elegant code. Let’s look at some code that can improve working with arrays.

C#
namespace LoopExtensions {
//Some comments removed for brevity...

    #region Extension Methods

    /// <summary>
    /// Handles looping through collections with additional details
    /// </summary>
    public static class LoopExtensionMethods {

        public static IEnumerable<T> Each<T>(this IEnumerable<T> collection,
            Action<ElementDetail<T>> each) {
            return LoopExtensionMethods.Each<T>(collection, 0, collection.Count(), each);
        }

        public static IEnumerable<T> Each<T>(this IEnumerable<T> collection,
            int start,
            Action<ElementDetail<T>> each) {
            return LoopExtensionMethods.Each<T>(collection, start, collection.Count(), each);
        }

        public static IEnumerable<T> Each<T>(this IEnumerable<T> collection,
            int start,
            int end,
            Action<ElementDetail<T>> each) {
            Action<ElementDetail<T>, T> handle = (detail, item) => { each(detail); };
            return LoopExtensionMethods.Each<T>(collection, start, end, handle);
        }

        public static IEnumerable<T> Each<T>(this IEnumerable<T> collection,
            Action<ElementDetail<T>, T> each) {
            return LoopExtensionMethods.Each<T>(collection, 0, collection.Count(), each);
        }

        public static IEnumerable<T> Each<T>(this IEnumerable<T> collection,
            int start,
            Action<ElementDetail<T>, T> each) {
            return LoopExtensionMethods.Each<T>(collection, start, collection.Count(), each);
        }

        public static IEnumerable<T> Each<T>(this IEnumerable<T> collection,
            int start,
            int end,
            Action<ElementDetail<T>, T> each) {

            //verify the ranges
            if (start < 0 || end > collection.Count()) {
                throw new ArgumentOutOfRangeException();
            }

            //perform the work
            foreach (T value in collection) {
                each(new ElementDetail<T>(value, start++, end, collection), value);
                if (start == end) { break; }
            }
            return collection;
        }
    }

    #endregion

    #region Detail Information Class

    /// <summary>
    /// Contains a summary of information about the item current in the loop
    /// </summary>
    public class ElementDetail<T> {

        #region Constructors

        internal ElementDetail(T value, int index, int total, IEnumerable<T> collection) {
            this.Value = value;
            this.Index = index;
            this.Total = total;
            this.Collection = collection;
        }

        #endregion

        #region Loop Information

        public int Index { get; private set; }
        public int Total { get; private set; }
        public T Value { get; private set; }
        public IEnumerable<T> Collection { get; private set; }

        #endregion

        #region Value Properties

        public T Previous {
            get {
                return !this.First
                    ? this.Collection.ElementAt(this.Index - 1)
                    : default(T);
            }
        }

        public T Next {
            get {
                return !this.Last
                    ? this.Collection.ElementAt(this.Index + 1)
                    : default(T);
            }
        }

        public bool Last {
            get { return this.Index == (this.Total - 1); }
        }

        public bool First {
            get { return this.Index == 0; }
        }

        public bool Outer {
            get { return this.First || this.Last; }
        }

        public bool Inner {
            get { return !this.Outer; }
        }

        public bool Even {
            get { return this.Index % 2 == 0; }
        }

        public bool Odd {
            get { return !this.Even; }
        }

        #endregion

        #region Collection Properties

        public int StepNumber {
            get { return this.Index + 1; }
        }

        public float PercentCompleted {
            get { return (((float)this.Index / (float)this.Total) * 100); }
        }

        public float PercentRemaining {
            get { return 100 - this.PercentCompleted; }
        }

        public int StepsCompleted {
            get { return this.Index; }
        }

        public int StepsRemaining {
            get { return this.Total - this.Index; }
        }

        #endregion
    } 

    #endregion
}

Now, that is a lot of code, but if you review it, you’ll find that it is a set of extension methods that can be used with IEnumerable. The basic idea is that we can perform our loop within a delegate that accepts additional information about the elements in our loop.

So, here is a quick example…

C#
string[] items = {
    "Apple",
    "Orange",
    "Grape",
    "Watermellon",
    "Kiwi"
};

items.Each((item) => {
    Console.Write("{0} > ", item.Inner ? "Inner" : "Outer");
    if (!item.First) { Console.Write("Previous: {0}, ", item.Previous); }
    Console.Write("Current: {0} ({1})", item.Value, item.StepNumber);
    if (!item.Last) { Console.Write(", Next: {0}", item.Next); }
    Console.WriteLine(" -- {0}% remaining", item.PercentRemaining);
});

// Output
// Outer > Current: Apple (1), Next: Orange -- 100% remaining
// Inner > Previous: Apple, Current: Orange (2), Next: Grape -- 80% remaining
// Inner > Previous: Orange, Current: Grape (3), Next: Watermelon -- 60% remaining
// Inner > Previous: Grape, Current: Watermellon (4), Next: Kiwi -- 40% remaining
// Outer > Previous: Watermelon, Current: Kiwi (5) -- 20% remaining

Normally, you would simply write all the comparisons within your loop and then use them as needed. Instead, in this code, we pass in an additional parameter that contains shortcuts to many common comparisons you might find inside loops. By doing this, we improve readability and focus on what the loop is doing.

What's Wrong with the Old Way

Nothing! Writing your comparisons inline can be advantageous since you aren’t doing any more work than you need to do. However, in some cases, the improved readability would make a big difference in the quality of the code. Consider the two snippets of MVC code and decide with one is easier to read:

ASP.NET
<ul>
<% foreach(SiteMapNode node in breadcrumb) { %>
    <li class="item <% =(breadcrumb.IndexOf(node) % 2 == 0 ? "item-even" : "item-odd") %>" >
    <% if (breadcrumb.First().Equals(node) || breadcrumb.Last().Equals(node)) { %>
        <strong>
    <% } %>
        <a href="<% =node.Url %>" >
            (<% =(breadcrumb.IndexOf(node) + 1) %>) : <% =node.Text %>
        </a>
    <% if (breadcrumb.First().Equals(node) || breadcrumb.Last().Equals(node)) { %>
        </strong>
    <% } %>
    </li>
<% } %>
</ul>

Or the same code using a loop helper…

ASP.NET
<ul>
<% breadcrumb.Each((node) => { %>
    <li class="item <% =(node.Even ? "item-even" : "item-odd") %>" >
    <% if (node.Outer) { %><strong><% } %>
        <a href="<% =node.Value.Url %>" >
            (<% = node.StepNumber %>) : <% =node.Value.Text %>
        </a>
    <% if (node.Outer) { %></strong><% } %>
    </li>
<% }); %>
</ul>

This example shows using only the ElementDetail class, but of course, you could use the other extension method that also supplies the value by itself.

Write Code for Humans Not for Computers

Everyone has heard the advice before – more or less, readability is probably the most important part of your code. The computer can read both of those examples easily, whereas a human might need to think for a moment before they are positive what the code does.

In any case, this simple but effective approach can transform previously unreadable code into works of programming art… or something like that…

License

This article, along with any associated source code and files, is licensed under The Creative Commons Attribution-ShareAlike 2.5 License


Written By
United States United States
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
QuestionCaveat or no caveat? Pin
philippe dykmans21-Sep-10 1:31
philippe dykmans21-Sep-10 1:31 
GeneralNice code, but caveat emptor Pin
tonyt7-Sep-09 20:31
tonyt7-Sep-09 20:31 
The Count() method is trivial if the actual source is an ICollection<T> or an array, but if it only implements IEnuemrable<T>, Count() must walk the entire sequence to obtain a result.

Just something that others might want to keep in mind when using this code.
GeneralRe: Nice code, but caveat emptor Pin
webdev_hb8-Sep-09 2:15
webdev_hb8-Sep-09 2:15 
GeneralRe: Nice code, but caveat emptor Pin
Richard Deeming9-Sep-09 4:09
mveRichard Deeming9-Sep-09 4:09 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.