-2

A for loop like

for (T i = a; i < b; i++)
{
    // do something
}

looks quite normal, right?

On a second glance, you'll notice T instead of a primitive datatype - a generic type. Still shouldn't be a problem, you will think. T could be anything,   not limited to   a number (a string, even a complex object), as long as it is comparable and enumerable (i.e. you need to apply the constraint where T: IEnumerable, IComparable to it).

Because of the fact that it is not limited to numbers, and since Eric gave an answer which is applicable to any type of object I don't believe this question is a duplicate of the question "Is there a constraint that restricts my generic method to numeric types?"

But back to this question: I thought the same way, then I started playing around a bit with that and tried to write something like

IEnumerable<T> Numbers<T>(T a, T b)
where T: IEnumerable, IComparable
{
    for (T i = a; i < b; i++)
    {
        yield return i;
    }
}

to implement a number generator which can be used for double, int, ... which would be a generic for-loop. The constraints IEnumerable and IComparable are just there to tell the compiler that elements of this type can be enumerated and compared (because of the i < b expression in the for loop and the increment), and if C# would have that, INumeric would be a quite useful constraint, too (which would then implicitly be comparable and enumerable of course).

To my surprise, this example doesn't compile, it generates the following errors:

CS0019: Operator '<' cannot be applied to operands of type 'T' and 'T'
CS0023: Operator '++' cannot be applied to operand of type 'T'

The reason seems to be that there is no constraint for numerics, but there doesn't seem to be any practical solution available, as some of the answers here are explaining.

Note: A similar (non-generic) version of this does compile:

IEnumerable<double> Numbers(double a, double b)
{
    for (var i = a; i < b; i++)
    {
        yield return i;
    }
}

IEnumerable<int> Numbers(int a, int b)
{
    for (var i = a; i < b; i++)
    {
        yield return i;
    }
}

If you have both, you can invoke it like

var intNumbers = Numbers((int)1, 10);
var doubleNumbers = Numbers((double)1, 10);

and due to the signature of the parameters the correct version is chosen.


In essence, the questions are:

1. Is it possible to write a generic function as the one which can be invoked like

var intNumbers = Numbers<int>(1, 10);
var doubleNumbers = Numbers<double>(1, 10);

as shown in my first example?

(I am not sure about the correct constraint, I thought that where T: IEnumerable, IComparable would do because you need to compare i < b and you need to iterate to the next bigger number).

2. a) How can I write a generic constraint that will allow me to increment and compare variables of type T?

2. b) If there's no such constraint, is there a way to simulate a for-loop with generic arguments?

3. How can a for-loop be made generic?

Matt
  • 23,275
  • 15
  • 108
  • 168

2 Answers2

4

A for loop has four parts:

  • Initialize the current state
  • Test the current state and stop if the test fails
  • Execute an action
  • Create a new current state

We'll leave break and continue out of it, since they complicate things considerably.

You wish to restrict the action to producing a value. Fine. What we want then is a new version of Aggregate that yields a value:

public static IEnumerable<R> MyFor<S, R>(
  S initial, 
  Func<S, bool> test, 
  Func<S, S> increment, 
  Func<S, R> select) 
{

    for (S current = initial; test(current); current = increment(current))
        yield return select(current);

}

And we're done. You can now make any for loop you like by simply supplying the necessary lambdas:

static IEnumerable<double> MakeDoubles() => 
  MyFor(0.0, x => x <= 10.0, x => x + 1.0, x => x);
Matt
  • 23,275
  • 15
  • 108
  • 168
Eric Lippert
  • 630,995
  • 172
  • 1,214
  • 2,051
  • Great, answer Eric. Thank you! :-) – Matt Jun 28 '18 at 15:33
  • @Matt: You're welcome! I note that there are already LINQ versions of many of the operations that we'd normally think of as loops, but of course not every possible combination has been created. It seems like we ought to be able to build this thing out of `Select` and `TakeWhile`, but it's easier and more efficient to simply write your own wrapper like this. – Eric Lippert Jun 28 '18 at 17:18
  • Exactly! This is why I asked the question, I felt that some nice wrapper was missing. Now with the help of your code it is easy to write similar useful things. You might have noticed there is also a NuGet package "MoreLINQ" out there with a similar idea behind. – Matt Jun 28 '18 at 18:01
  • One way to simplify this is to take the select out of it: `public static IEnumerable MyFor( S initial, Func test, Func increment) { for (S current = initial; test(current); current = increment(current)) yield return current; }` You can always plunk a `Select` after this new `MyFor`. – Eric Lippert Jun 28 '18 at 19:12
  • That's right, I think that is even better, and it doesn't limit flexibility. In most cases, if you use `MyFor` as a number generator, you would pass `x => x` anyway - and if not, as you said, just append a `.Select(...)`. – Matt Jun 29 '18 at 11:00
0

The answer below is a summarization of the discussion from the previous comments under the question. Thanks to MistyK who found a way to solve it.


The issue is that there is currently no "numeric" constraint existing in C# that one could use in a generic way.

Hence, a for loop like this which uses a generic type T does not work, because the compiler doesn't allow it: for (T i = a; i < b; i++)

As a workaround we have to use a while loop and as constraint struct together with IComparable. The yield keyword doesn't have any restriction and is working as expected.

Depending on the implementation, i.e. whether or not you want to have a flexible increment, there are two functions solving this issue.

Consider the following code:

using System;
using System.Collections.Generic;

public class Program
{
    // Answer 1:
    // The downside is that you need to provide your increment 
    // func and you can pass any type implementing 
    // IComparable.Usage: var xd = Numbers<int>(1, 5, x => { x++; return x; });
    public static IEnumerable<T> Numbers<T>(T a, T b, Func<T, T> increment) 
    where T : struct, IComparable 
    { 
        var i = a; while (i.CompareTo(b) < 0) 
        { 
            yield return i; i = increment(i); 
        } 
    }

    // Answer 2:
    // Idea without passing increment function but can cause runtime error 
    // if increment is not implemented: 
    public static IEnumerable<T> Numbers<T>(T a, T b)
    where T : struct, IComparable
    {
        dynamic i = a; while (i.CompareTo(b) < 0)
        {
            yield return i; i++;
        }
    }

    public static void Main()
    {
        // implicit increment:
        Numbers<double>(1, 5).Dump();

        // explicit increment (+1), example 1:
        Numbers<int>(1, 5, x => {x++; return x;}).Dump();

        // explicit increment (+0.75), example 2:
        Numbers<float>((float)1.5, (float)7.5, x => { x += (float)0.75; return x; }).Dump();

    }
}

Use DotNetFiddle to execute it

The examples above are invoking Numbers with int, double and float, and the 3rd example uses an increment of 0.75.

Samuel Liew
  • 72,637
  • 105
  • 156
  • 238
Matt
  • 23,275
  • 15
  • 108
  • 168