-1

i have a method that yield returns values. e.g.:

public static IEnumerable<int> GetValues()
{
    for (int i = 0; i < 10; i++)
    {
        yield return i;
    }
}

when i call this method with foreach, yield return i; is being called 10 times

foreach (int i in GetValues())
{
    Console.WriteLine(i);
}

when i call it with a for-loop, yield return i; is being called factorial 10 times

for (int i = 0;i< 10;i++)
{
    Console.WriteLine(GetValues().ElementAt(i));
}

Question: is there a way to keep the for-loop and to avoid those multiple calls, caused by ElementAt(i)? Or... can i call a element from IEnumerable by its index without causing a iteration thourgt its previous elements? The only thing i found was this, but

for (int i = 0; i < 10; i++)
{
    Console.WriteLine(GetValues().Skip(i).First());
}

doesn't work either.

Community
  • 1
  • 1
fubo
  • 44,811
  • 17
  • 103
  • 137
  • Are you always iterating through the entire collection? – Yuval Itzchakov Apr 29 '15 at 08:34
  • 13
    `yield return i` won't be called "factorial 10" times - it's just O(n^2). It's not clear what you're trying to achieve here - why not just use the foreach loop? If you could give more motivation, we'd be more likely to be able to help you. – Jon Skeet Apr 29 '15 at 08:34
  • 1
    The only way is to return an `IList` then `ElementAt` uses the indexer instead of enumerating all. – Tim Schmelter Apr 29 '15 at 08:37
  • No an `IEnumerable` cant be access by index. If that is what you need return `IList` instead. – Magnus Apr 29 '15 at 08:38
  • @JonSkeet - the question is about not to use foreach. I want to improve the performance of my code without changing the for-loop – fubo Apr 29 '15 at 08:42
  • Then @Dennis answer will do exactly what you need :) – Yuval Itzchakov Apr 29 '15 at 08:44
  • 1
    @fubo: But in order to understand the most appropriate solution, we need to know *why* you don't want to use foreach, given that it's the most obvious solution with the code you've given. In particular, this isn't as useful a question for other people coming in the future as you haven't expressed why the most obvious solution isn't appropriate for you. – Jon Skeet Apr 29 '15 at 09:00
  • @JonSkeet this was just a theoretical question. sure i would use foreach in my application but i wanted to know if there is a 'manual' or alternative way to go. – fubo Apr 29 '15 at 09:07
  • 4
    Then I don't think it's actually terribly useful - it feels like a question of "Sure, I *can* take the straight route home - but if I want to go out of my way for no specific reason, what's the best route?" The solution you've accepted is lazy *but* remembers all the values it's already yielded - so it's not the most appropriate one for a situation where you only need the items in order, for example. You *could* have a `for` loop which still just used `MoveNext` rather than an index... but I assume that wouldn't satisfy your question. – Jon Skeet Apr 29 '15 at 09:12

4 Answers4

8

If you want to access items by index and reduce GetValues calls, you have to materialize lazy enumerable, which is produced by GetValues:

var values = GetValues()
    .ToArray();

for (int i = 0; i < values.Length; i++)
{
    Console.WriteLine(values[i]);
}

Otherwise this:

GetValues().Skip(i).First()

will create a new lazy enumerator again and again.

Dennis
  • 37,026
  • 10
  • 82
  • 150
  • the `.ToList()` was my first approach too, but it costs the advantage of lazy loading. I can `Console.WriteLine();` the first element, even if the last one is not returned – fubo Apr 29 '15 at 08:46
6

You can't move backward or refer to a random index in IEnumerable<> object - the collection can be created in various ways including randomness and there's no magic way of getting n-th element without iterating over all previous elements.

The common usage of IEnumerable<> is:

foreach (var value in GetValues())
{
    Console.WriteLine(value);
}

which translates to something like:

using (var enumerator = GetValues().GetEnumerator())
{
    while(enumerator.MoveNext())
    {
        var value = enumerator.Current;
        Console.WriteLine(value);
    }
}

If you want to refer to the specific index, you need to have an IList<> object - you can create one by calling

.ToList()

The .ToArray() mentioned in another response is actually a little bit slower and calls .ToList() internally before making it into an array (because array need to have fixed size and we don't know the number of elements in IEnumerable until we enumerate to the end)

You can create your own proxy, lazy class which will enumerate the enumerator only when needed

    public static IEnumerable<int> GetValues()
    {
        for (int i = 0; i < 10; i++)
        {
            Console.WriteLine("yielding " + i);
            yield return i;
        }
    }


    class LazyList<T>
    {
        IEnumerator<T> enumerator;
        IList<T> list;

        public LazyList(IEnumerable<T> enumerable)
        {
            enumerator = enumerable.GetEnumerator();
            list = new List<T>();
        }

        public T this[int index]
        {
            get
            {
                while (list.Count <= index && enumerator.MoveNext())
                {
                    list.Add(enumerator.Current);
                }

                return list[index];
            }
        }
    }

    static void Main(string[] args)
    {
        var lazy = new LazyList<int>(GetValues());

        Console.WriteLine(lazy[0]);
        Console.WriteLine(lazy[4]);
        Console.WriteLine(lazy[2]);
        Console.WriteLine(lazy[1]);
        Console.WriteLine(lazy[7]);
        Console.WriteLine(lazy[9]);
        Console.WriteLine(lazy[6]);
        Console.Read();
    }

will produce:

yielding 0
0
yielding 1
yielding 2
yielding 3
yielding 4
4
2
1
yielding 5
yielding 6
yielding 7
7
yielding 8
yielding 9
9
6
Adassko
  • 5,201
  • 20
  • 37
  • 1
    `IEnumerator` is `IDisposable` so it will translate into `using (var enumerator = ...) { for (;enumerator.MoveNext();)...}`. Such a behaivior is *very nice treat* of C# (in comparison to *Java*) when enumerating *file lines*, *database records* etc. – Dmitry Bychenko Apr 29 '15 at 09:15
1

An IEnumerable has no indexer. If you use a for-loop the way you do, it will iterate over the IEnumerable for each i.

A good solution is a foreach loop. You can use a counter counting up each loop iteration if you need an index.

nvoigt
  • 75,013
  • 26
  • 93
  • 142
1

If you return an IList<int>(f.e an int[] or List<int>), Enumerable.ElementAt uses the indexer instead of enumerating all.

public static IList<int> GetValues()
{
    int[] ints = Enumerable.Range(0, 10).ToArray();
    return ints;
}

But this isn't using deferred execution, so you're loading all into memory.

Here's the source of ElementAt.

Tim Schmelter
  • 450,073
  • 74
  • 686
  • 939
  • 1
    If the functions returns `IList` the signature should not be `IEnumerable` – Magnus Apr 29 '15 at 08:43
  • @Magnus: changed, although a matter of preference. I wanted to show that it doesn't matter for `ElementAt` if the method returns `IList` or `IEnumerable`, but it matters what's really returned. Maybe OP needs an `IEnumerable` somewhere. – Tim Schmelter Apr 29 '15 at 08:44
  • @TimSchmelter - that approach costs me the advantage of lazy loading too. – fubo Apr 29 '15 at 08:50
  • @fubo: as mentioned in my answer. – Tim Schmelter Apr 29 '15 at 08:53