Troy
Troy

Reputation: 688

do something foreach Index in a Range

C# 8.0 introduced the structs System.Index and System.Range

What is the most concise way to loop through a System.Range?

var owners = new string[] {"Alice", "Bob", "Charlie"};
var pets = new string[] {"Dog", "Cat", "Bird"};

foreach (var index in 1..3) {
    var pet = pets[index];
    var owner = owners[index];
    Console.WriteLine($"{owner} owns a {pet}");
}

The above line foreach (var index in 1..3) { is a compile error.

Type 'System.Range' cannot be used in 'foreach' statement because it neither implements 'IEnumerable' or 'IEnumerable', nor has suitable 'GetEnumerator' method which return type has 'Current' property and 'MoveNext' method

Upvotes: 2

Views: 3742

Answers (3)

Serge
Serge

Reputation: 43860

You can't use foreach loop with Range since it doesn't implement IEnumerable interface. You can only use Range if you want to show a part of array for example

Range range = 0..2;

var i=range.Start.Value;
foreach (var owner in owners[range])
{
    Console.WriteLine($"{owner} owns a {pets[i]}");
    i++;
}

result

Alice owns a Dog
Bob owns a Cat

or

Range range = 1..3;

var i=range.Start.Value;
foreach (var owner in owners[range])
{
    Console.WriteLine($"{owner} owns a {pets[i]}");
    i++;
}

result

Bob owns a Cat
Charlie owns a Bird

but in your case it is much easier to use for loop

for (var i=0; i <3; i++) Console.WriteLine($"{owners[i]} owns a {pets[i]}");

Upvotes: 0

Dmitrii Bychenko
Dmitrii Bychenko

Reputation: 186668

If you want to enumerate (owner, pet) tuples you can do it with a help of Linq Zip instead of Range:

using System.Linq;

...

foreach (var (owner, pet) in owners.Zip(pets, (o, p) => (o, p))) {
  Console.WriteLine($"{owner} owns a {pet}");
}

If you want to Skip the very 1st owner (Alice) and her pet (Dog) just add Skip (in your code you have range starting from 1, not from 0):

foreach (var (owner, pet) in owners.Zip(pets, (o, p) => (o, p)).Skip(1)) {
  Console.WriteLine($"{owner} owns a {pet}");
}

Upvotes: 4

Tim Schmelter
Tim Schmelter

Reputation: 460058

I'm afraid you can't enumerate a System.Range(you can, see my edit) because it doesn't implement IEnumerable<int>. The reason is that it could contain indexes from the end of a collection.

If you want that you need to use a for-loop or Enumerable.Range:

foreach (var index in Enumerable.Range(0, 3)) {
    var pet = pets[index];
    var owner = owners[index];
    Console.WriteLine($"{owner} owns a {pet}");
}

If all you want is to get the items of two collections at the same index use Enumerable.Zip as Dmitry has shown.


Edit: Actually you can do it with an extension(credits here, note my bugfix)

public static class RangeEx
{
    public static RangeEnumerator GetEnumerator(this Range range)
    {
        if (range.Start.IsFromEnd || range.End.IsFromEnd)
        {
            throw new ArgumentException(nameof(range));
        }

        return new RangeEnumerator(range.Start.Value, range.End.Value);
    }

    public struct RangeEnumerator : IEnumerator<int>
    {
        private readonly int _end;
        private int _current;

        public RangeEnumerator(int start, int end)
        {
            _current = start - 1; // - 1 fixes a bug in the original code
            _end = end;
        }

        public int Current => _current;
        object IEnumerator.Current => Current;

        public bool MoveNext() => ++_current < _end;

        public void Dispose() { }
        public void Reset()
        {
            throw new NotImplementedException();
        }
    }
}

With this you can use your code:

foreach (int index in 1..3)
{
    Console.WriteLine($"{owners[index]} owns a {pets[index]}");
}

Upvotes: 11

Related Questions