Let’s discuss one among my favourite .NET options: IEnumerable
.
This interface allows iterating over a set. In different phrases, if one thing is an IEnumerable
, you’ll be able to principally consider it like an array or a listing. You need to use a foreach
assertion to loop via it, you should utilize LINQ to map or scale back it in 100 other ways, or you’ll be able to explicitly forged it to an array with .ToArray()
and entry parts by index. However there are some things that make IEnumerable
particular—and some issues that make it tough.
IEnumerable
is the return sort from an iterator. An iterator is a technique that makes use of the yield return
key phrases. yield return
is completely different from a standard return
assertion as a result of, whereas it does return a price from the operate, it doesn’t “shut the e book” on that operate. The subsequent time a price is predicted, the operate will proceed executing statements after the yield return
till it hits one other yield return
(or a yield break
, or the top of the operate block). In different phrases, you might write an iterator that appears like this:
IEnumerable<int> GetOneTwoThree() {
yield return 1;
yield return 2;
yield return 3;
// We might put "yield break;" right here however there isn't any want, the top of the operate alerts the identical factor.
}
If you name GetOneTwoThree()
it can return an IEnumerable<int>
. As talked about above, there are some things you are able to do with this:
var numbers = GetOneTwoThree();
foreach (var quantity in numbers) {
Console.WriteLine(quantity);
// Output:
// 1
// 2
// 3
}
var doubledNumbers = numbers.Choose(num => num * 2);
foreach (var quantity in doubledNumbers) {
Console.WriteLine(quantity);
// Output:
// 2
// 4
// 6
}
var numberArray = numbers.ToArray();
Console.WriteLine(numberArray[0]); // Output: 1
It’s possible you’ll discover that we’re iterating over numbers
a number of instances. For a easy iterator just like the one I’ve written, that’s technically okay to do. Each time we iterate over numbers
, it can begin over in the beginning of the iterator technique and yield all the identical values over once more. (You’ll be able to’t count on this from each iterator; extra on that later.)
Lazy analysis (the alternative of keen analysis) is once we wait to execute a bit of code till we completely, positively must. If you name GetOneTwoThree()
, you’ll get a return worth even supposing not one of the code within the operate has really been executed but! To show it, run the next code in LINQPad.
(If you happen to’re not conversant in LINQPad, you need to test it out—it’s a strong, transportable C# playground. However the one factor it’s essential find out about it right here is that it offers the magic .Dump()
technique, which outputs any worth to the Outcomes pane.)
bool didTheCodeRun = false;
void Fundamental() {
var outcomes = RunTheCode();
didTheCodeRun.Dump();
}
IEnumerable<bool> RunTheCode() {
didTheCodeRun = true;
yield return true;
}
The output of working Fundamental()
within the above snippet is false
. That’s proper, after we run RunTheCode()
, which explicitly units didTheCodeRun
to true
, the worth of didTheCodeRun
continues to be false
. Not one of the code in our iterator runs till we begin iterating via the IEnumerable
. It’s lazily evaluated!
This may increasingly appear counterintuitive, however in quite a lot of circumstances it’s a great factor. What if you happen to by no means find yourself iterating via the IEnumerable
in any respect? Nicely, that’s a bunch of code the pc didn’t must execute. Or what if you happen to’re utilizing a LINQ technique like .First()
to attempt to discover a particular merchandise within the assortment? It’s possible you’ll not must run all the code within the iterator to get the worth you’re on the lookout for–and also you received’t. As soon as .First()
finds a price that matches the predicate, it can cease iterating. If you happen to’re working with an IEnumerable
that probably has hundreds of values (or extra), it can save you quite a lot of CPU cycles by solely iterating so far as it’s essential.
After all, it’s all as much as you. You’ll be able to iterate as a lot or as little as you need. Let’s check out a few of the methods to try this.
// This is a variable to trace execution of code in an iterator
int lastYielded = -1;
// This is an iterator for us to play with
IEnumerable<int> GetOneToTen() {
for (var num = 1; num <= 10; num++) {
lastYielded = num;
yield return num;
}
}
void Fundamental() {
var numbers = GetOneToTen();
lastYielded.Dump(); // Output: -1
// This offers us an 'occasion' of the iteration
var enumerator = numbers.GetEnumerator();
// This executes the iterator till the primary yield return is reached
enumerator.MoveNext();
// This offers us the present (most not too long ago yielded) worth of the iterator
enumerator.Present.Dump(); // Output: 1
lastYielded.Dump(); // Output: 1
// It will iterate from 1 to 4, then cease
foreach (var num in numbers) {
if (num >= 4) {
break;
}
}
lastYielded.Dump(); // Output: 4
// This is not going to execute any code within the iterator.
// LINQ strategies are lazily evaluated as nicely
var numbersTimesTwo = numbers.Choose(num => num * 2);
lastYielded.Dump(); // Output: 4
// It will power the whole iterator to run, yielding all values
var arr = numbers.ToArray();
lastYielded.Dump(); // Output: 10
}
It’s vital to level out that many iterators are usually not so simple as those we’ve been utilizing right here. An iterator might question a database, for instance—together with the unlucky chance that it would alter information, or that iterating via it twice would possibly yield fully completely different outcomes! Some instruments (like ReSharper) will warn you in opposition to a number of enumeration because of this. An iterator is, from one perspective, nothing greater than a synchronous technique that will not execute its code instantly (or in any respect). And to muddy the waters just a bit, not all iterators are synchronous; there’s additionally an IAsyncEnumerable
interface (you’ll be able to loop via it with await foreach
).
More often than not none of this can be a downside. Nearly on a regular basis you’ll be able to deal with an IEnumerable
like a listing. However I’ve discovered the onerous means to not depend on this—I as soon as used .Choose()
to rework a set whereas utilizing an exterior variable to trace a small piece of state on every iteration, then bought very confused when the exterior variable wasn’t up to date later within the technique. Ultimately, I mounted the issue by forcing the iteration to finish with .ToArray()
. (There are a number of methods to method one thing like this, relying on the anticipated measurement of the gathering and what you’re attempting to trace.)
Hopefully this overview helps you keep away from working into points if you create or eat IEnumerable
in your individual code (which is extraordinarily possible, as they’re all over the place in C#). Listed below are a few guidelines to recollect:
- Attempt to keep away from unwanted side effects when writing an iterator technique. Anybody who makes use of the strategy ought to be capable to deal with it as if it synchronously returns an array. If an iterator adjustments or executes something outdoors of itself, the caller could find yourself confused. This is applicable to capabilities handed to LINQ strategies as nicely, since a lot of them return
IEnumerable
and are lazily evaluated. - It’s best to keep away from iterating over the identical
IEnumerable
a number of instances. If you already know you’re going to entry each worth—for instance, if you happen to’re iterating it utilizing aforeach
with nobreak
orreturn
—enumerate it sooner relatively than later by coercing theIEnumerable
to a listing or array sort. Then use that checklist or array for all future operations.
Tags: .internet, C#, IEnumerable