Alexander Fuchs
Alexander Fuchs

Reputation: 147

C# Linq extension: for-loop not break- or continue-able

I recently implemented this Linq-extension for usage in automated tests. I know that I can just use a for-loop; however, also working in iOS, I like if you can read your method code almost like an English sentence when using them.

/// <summary>
/// Repeats an action x times
/// </summary>
/// <param name="times">The number of times an action should be repeated</param>
/// <param name="action">The action that should be repeated</param>
public static void DoIt(Int32 times, Action<Int32> action)
{
    for (var i = 0; i < times; i++)
        action(i);
}

This works very well and you can use it like this:

DoIt(5, i => {
    // Here goes the code that should be executed multiple times
});

It is clear to read as "Do it 5 times with current index i". However, now that I wanted to use this call also in my regular code, I noticed that I can not use break; or continue; inside the DoIt-Block.

Is there any possibility to escape the Linq expression using the existing language supported techniques?

Upvotes: 1

Views: 302

Answers (3)

Enigmativity
Enigmativity

Reputation: 117144

I would be inclined to make the DoIt method more powerful by modifying it in this way:

public static void DoIt<T>(
    T initial, Func<T, bool> condition, Func<T, T> iterate, Action<T> action)
{
    var t = initial;
    while (condition(t))
    {
        action(t);
        t = iterate(t);
    }
}

In this way break is handled in the condition predicate - return false and you will break. And continue is handled by calling return in the iterate function.

This allows your DoIt method to iterate on any type - including regular classes or even anonymous types.

You could do this:

DoIt("X", t => t.Length <= 3, t => t + "!", t => Console.WriteLine(t));

Or even this:

DoIt(
    new { counter = 0, sum = 0 },
    t => t.sum < 50,
    t => new { counter = t.counter + 1, sum = t.sum + t.counter },
    t =>
    {
        Console.WriteLine(t.counter);
        Console.WriteLine(t.sum);
    });

Upvotes: 2

willeM_ Van Onsem
willeM_ Van Onsem

Reputation: 477230

You could slightly modify the Action<T> into a Predicate<T>:

public static void DoIt(Int32 times, Predicate<Int32> action) {
    for (var i = 0; i < times; i++)
        if(action(i)) {
            break;
        }
}

Now you can use a return in your block. If you return false; it will emulate a continue;, if you use return true;, it will emulate a break:

DoIt(5, i => {
    if(i == 1) {
        return false;//here we continue
    } else if(i == 3) {
        return true;//we break after the third time
    }
    Console.WriteLine("We run for the "+i+"th time!");
    return false;
});

The downside is that the last line of your block must always contain a return false;. You could see it as the predicate asking "Stop now?".

So to summarize:

return true;  -> break
return false; -> continue

Giving the child a name

The solution is not very clean I admit: first of all, it is a bit confusing to work with booleans. You can solve this problem however by introducing an enum:

public enum LoopResult : byte {
    Break = 1,
    Continue = 0,
    End = 0
}

The End is in fact an alias for Continue, but can be seen as a more "natural" way to express it, in that case one can rewrite the DoIt with a Function<Int32,Result>:

public static void DoIt(Int32 times, Function<Int32,LoopResult> action) {
    for (var i = 0; i < times; i++)
        if(action(i) == LoopResult.Break) {
            break;
        }
}

And rewrite it as:

DoIt(5, i => {
    if(i == 1) {
        return LoopResult.Continue;//here we continue
    } else if(i == 3) {
        return LoopResult.Break;//we break after the third time
    }
    Console.WriteLine("We run for the "+i+"th time!");
    return LoopResult.End;
});

C#s lack of a preprocessor/precompiler

But furthermore it doesn't make good reading to place return parts into your code. Since C# has no (standard) precompiler I think there is however no way to automatically rewrite code such that one can use a break and continue expression. On the other hand a precompiler introduces an additional level of trouble and many programs can get ruined simply by starting to do a bit too much magic with precompilers.

Upvotes: 4

user47589
user47589

Reputation:

This would work. Return false to exit the loop prematurely.

public static void DoIt(Int32 times, Func<int, bool> action)
{
    for (var i = 0; i < times; i++)
    {
        if (!action(i))
        {
            break;
        }
    }
    Console.WriteLine("Done");
    Console.ReadLine();
}

static void Main(string[] args)
{
    DoIt(5, i =>
    {
        Console.WriteLine(i);
        return i != 3;              // returning false exits the loop
    });
}

Upvotes: 2

Related Questions