Let's break this down piece by piece until you understand it. Trust me; take your time and read this and it will be a revelation to you understanding Enumerable
types and answer your question.
Look at the IEnumerable
interface which is the base of IEnumerable<T>
. It contains one method; IEnumerator GetEnumerator();
.
Enumerables are a tricky beast because they can do whatever they want. All that really matters is the call to the GetEnumerator()
that happens automatically in a foreach
loop; or you can do it manually.
What does GetEnumerator()
do? It returns another interface, IEnumerator
.
This is the magic. The IEnumerator
has 1 property and 2 methods.
object Current { get; }
bool MoveNext();
void Reset();
Let's break down the magic.
First let me explain what they are typically, and I say typically because like I mentioned it can be a tricky beast. You're allowed to implement this however you choose... Some types don't follow the standards.
object Current { get; }
is obvious. It gets the current object in the IEnumerator
; by default this might be null.
bool MoveNext();
This returns true
if there is another object in the IEnumerator
and it should set the Current
value to that new object.
void Reset();
tells the type to start over from the beginning.
Now lets implement this. Please take the time to review this IEnumerator
type so that you understand it. Realize that when you reference an IEnumerable
type you are not even referencing the IEnumerator
(this); however, you're referencing a type that returns this IEnumerator
via GetEnumerator()
Note: Be careful not to confuse the names. IEnumerator
is different than IEnumerable
.
IEnumerator
public class MyEnumerator : IEnumerator
{
private string First => nameof(First);
private string Second => nameof(Second);
private string Third => nameof(Third);
private int counter = 0;
public object Current { get; private set; }
public bool MoveNext()
{
if (counter > 2) return false;
counter++;
switch (counter)
{
case 1:
Current = First;
break;
case 2:
Current = Second;
break;
case 3:
Current = Third;
break;
}
return true;
}
public void Reset()
{
counter = 0;
}
}
Now, let's make an IEnumerable
type and use this IEnumerator
.
IEnumerable
public class MyEnumerable : IEnumerable
{
public IEnumerator GetEnumerator() => new MyEnumerator();
}
This is something to soak in... When you make a call like numbers.Select(n => n % 2 == 0 ? n : 0)
you aren't iterating any items... you're returning a type much like the one above. .Select(…)
returns IEnumerable<int>
. Well looky above... IEnumerable
isn't anything but an interface that calls GetEnumerator()
. That happens whenever you enter a looping situation or it can be done manually. So, with that in mind you can already see the iteration never starts until you call GetEnumerator()
and even then it never starts until you call the MoveNext()
method of the result of GetEnumerator()
which is the IEnumerator
type.
So...
In other words, you just have a reference to an IEnumerable<T>
in your call and nothing more. No iterations have taken place. This is why the code jumps back up in yours because it finally does iterate in the ElementAt
method and it's then looking at the lamba expression. Stay with me and I'll later update an example to take this lesson full circle but for now let's continue our simple example:
Let's now make a simple console app to test our new types.
Console App
class Program
{
static void Main(string[] args)
{
var myEnumerable = new MyEnumerable();
foreach (var item in myEnumerable)
Console.WriteLine(item);
Console.ReadKey();
}
// OUTPUT
// First
// Second
// Third
}
Now let's do the same thing but make it generic. I won't write as much but monitor the code closely for changes and you'll get it.
I'm going to copy and paste it all in one.
Entire Console App
using System;
using System.Collections;
using System.Collections.Generic;
namespace Question_Answer_Console_App
{
class Program
{
static void Main(string[] args)
{
var myEnumerable = new MyEnumerable<Person>();
foreach (var person in myEnumerable)
Console.WriteLine(person.Name);
Console.ReadKey();
}
// OUTPUT
// Test 0
// Test 1
// Test 2
}
public class Person
{
static int personCounter = 0;
public string Name { get; } = "Test " + personCounter++;
}
public class MyEnumerator<T> : IEnumerator<T>
{
private T First { get; set; }
private T Second { get; set; }
private T Third { get; set; }
private int counter = 0;
object IEnumerator.Current => (IEnumerator<T>)Current;
public T Current { get; private set; }
public bool MoveNext()
{
if (counter > 2) return false;
counter++;
switch (counter)
{
case 1:
First = Activator.CreateInstance<T>();
Current = First;
break;
case 2:
Second = Activator.CreateInstance<T>();
Current = Second;
break;
case 3:
Third = Activator.CreateInstance<T>();
Current = Third;
break;
}
return true;
}
public void Reset()
{
counter = 0;
First = default;
Second = default;
Third = default;
}
public void Dispose() => Reset();
}
public class MyEnumerable<T> : IEnumerable<T>
{
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
public IEnumerator<T> GetEnumerator() => new MyEnumerator<T>();
}
}
So let's recap... IEnumerable<T>
is a type that has a method that returns an IEnumerator<T>
type. The IEnumerator<T>
type has the T Current { get; }
property as well as the IEnumerator
methods.
Let's break this down one more time in code and call out the pieces manually so that you can see it clearer. This will be only the console part of the app because everything else stays the same.
Console App
class Program
{
static void Main(string[] args)
{
IEnumerable<Person> enumerable = new MyEnumerable<Person>();
IEnumerator<Person> enumerator = enumerable.GetEnumerator();
while (enumerator.MoveNext())
Console.WriteLine(enumerator.Current.Name);
Console.ReadKey();
}
// OUTPUT
// Test 0
// Test 1
// Test 2
}
FYI: One thing to point out is in the answer above there are two versions of Linq. Linq in EF or Linq-to-SQL contain different extension methods than typical linq. The main difference is that query expression in Linq (when referring to a database) will return IQueryable<T>
which implements the IQueryable
interface, which creates SQL expressions that are ran and iterated against. In other words... something like a .Where(…)
clause doesn't query the entire database and then iterate over it. It turns that expression into a SQL expression. That's why things like .Equals()
will not work in those specific Lambda expressions.