Preface
When we write C# code, we often need to deal with large amounts of data collections. In the traditional way, we often need to load the entire data collection into memory before doing it. However, if the data set is very large, this method will cause excessive memory usage and may even cause the program to crash.
In C#yield return
The mechanism can help us solve this problem. By usingyield return
, we can generate data sets on demand instead of generating the entire data set at once. This can greatly reduce memory usage and improve program performance.
In this article, we will discuss in depth in C#yield return
The mechanism and usage of this powerful feature can help you better understand this powerful feature and use it flexibly in actual development.
How to use
We mentioned aboveyield return
Generate the data set as needed instead of generating the entire data set at once. Next, with a simple example, we look at how it works so that we can deepen our understanding of it
foreach (var num in GetInts()) { ("External traversal:{0}", num); } IEnumerable<int> GetInts() { for (int i = 0; i < 5; i++) { ("Internal traversal:{0}", i); yield return i; } }
First, inGetInts
In the method, we useyield return
Keywords to define an iterator. This iterator can generate sequences of integers on demand. On each cycle, useyield return
Returns the current integer. By 1foreach
Loop to traverseGetInts
The sequence of integers returned by the method. During iterationGetInts
The method will be executed, but the entire sequence will not be loaded into memory. Instead, when needed, each element in the sequence is generated as needed. At each iteration, the information corresponding to the integer of the current iteration is output. So the output result is
Internal traversal: 0
External traversal: 0
Internal traversal: 1
External traversal: 1
Internal traversal: 2
External traversal: 2
Internal traversal: 3
External traversal: 3
Internal traversal: 4
External traversal: 4
As you can see, the integer sequence is generated on demand, and the corresponding information is output every time it is generated. This method can greatly reduce memory usage and improve program performance. Of course fromc# 8
The way to start asynchronous iteration is also supported
await foreach (var num in GetIntsAsync()) { ("External traversal:{0}", num); } async IAsyncEnumerable<int> GetIntsAsync() { for (int i = 0; i < 5; i++) { await (); ("Internal traversal:{0}", i); yield return i; } }
Unlike the above, if we need to use an asynchronous method, we need to return itIAsyncEnumerable
Type, the execution results of this method are consistent with the execution results of the synchronous method above, so we will not show it. Our examples above are based on continuous iteration of loops, and in fact they useyield return
The method can also be output as needed, which is suitable for flexible iteration. As shown in the following example
foreach (var num in GetInts()) { ("External traversal:{0}", num); } IEnumerable<int> GetInts() { ("Internal traversal: 0"); yield return 0; ("Internal traversal: 1"); yield return 1; ("Internal traversal: 2"); yield return 2; }
foreach
The loop will be called every timeGetInts()
method,GetInts()
The method is used internallyyield return
The keyword returns a result. Each time I go through the next oneyield return
. So the result of the above code is
Internal traversal: 0
External traversal: 0
Internal traversal: 1
External traversal: 1
Internal traversal: 2
External traversal: 2
Explore the essence
We've shown aboveyield return
How to use, it is a lazy loading mechanism that allows us to process data one by one, rather than reading all data into memory at once. Next, let’s explore how the magical operation is implemented so that everyone can understand the iterative system related to a clearer understanding.
foreach nature
First let's take a lookforeach
Why can it be traversed, that is, if it can beforeach
What conditions do the traversed object need to meet? At this time, we can decompile the tool to see what the compiled code looks like. I believe everyone is most familiar with it.List<T>
If we traverse the collection, we will useList<T>
Examples to demonstrate
List<int> ints = new List<int>(); foreach(int item in ints) { (item); }
The above code is very simple, and we did not give it any initialization data, which can eliminate interference, allowing us to see the results of decompilation more clearly and eliminate other interferences. Its decompiled code is like this
List<int> list = new List<int>(); List<int>.Enumerator enumerator = (); try { while (()) { int current = ; (current); } } finally { ((IDisposable)enumerator).Dispose(); }
There are many tools that can decompile code, and I use more of them.ILSpy
、dnSpy
、dotPeek
and onlinec#
Decompile the website,indnSpy
Decompiled code can also be debugged.
Through the above decompiled code we can seeforeach
It will be compiled into a fixed structure, which is the iterator pattern structure in the design pattern we often mention
Enumerator enumerator = (); while (()) { var current = ; }
Through this fixed structure, let's summarize itforeach
How it works
- Can be
foreach
The object needs to be includedGetEnumerator()
method - Iterator object contains
MoveNext()
Methods andCurrent
property -
MoveNext()
Method returnbool
Type, determine whether it can continue to iterate.Current
The property returns the current iteration result.
We can take a lookList<T>
How is the iterable source code structure of class implemented
public class List<T> : IList<T>, IList, IReadOnlyList<T> { public Enumerator GetEnumerator() => new Enumerator(this); IEnumerator<T> IEnumerable<T>.GetEnumerator() => Count == 0 ? SZGenericArrayEnumerator<T>.Empty : GetEnumerator(); IEnumerator () => ((IEnumerable<T>)this).GetEnumerator(); public struct Enumerator : IEnumerator<T>, IEnumerator { public T Current => _current!; public bool MoveNext() { } } }
There are two core interfaces involvedIEnumerable<
andIEnumerator
,The two of them define the abstraction of the ability that can implement iteration, and the implementation method is as follows
public interface IEnumerable { IEnumerator GetEnumerator(); } public interface IEnumerator { bool MoveNext(); object Current{ get; } void Reset(); }
If the class is implementedIEnumerable
The interface and implementedGetEnumerator()
The method can beforeach
, the iterated object isIEnumerator
Type, including oneMoveNext()
Methods andCurrent
property. The above interface is the original object method, and this operation is aimed atobject
Type collection object. Most of our actual development process use generic collections, and of course there are corresponding implementation methods, as shown below
public interface IEnumerable<out T> : IEnumerable { new IEnumerator<T> GetEnumerator(); } public interface IEnumerator<out T> : IDisposable, IEnumerator { new T Current{ get; } }
Can beforeach
Iteration does not mean that it must be implementedIEnumerable
Interface, this just provides us with an abstraction that can be iterated. As long as the class containsGetEnumerator()
Method and return an iterator, which contains the returnbool
Type ofMoveNext()
Methods and get the current iterative objectCurrent
Just attributes.
yield return nature
We saw above that it can beforeach
What is the essence of iteration, thenyield return
The return value can beIEnumerable<T>
There must be something strange about the reception instructions. Let's decompile our example above and look at the code after decompilation. In order to facilitate everyone to compare the decompilation results, I will paste the above example again here.
foreach (var num in GetInts()) { ("External traversal:{0}", num); } IEnumerable<int> GetInts() { for (int i = 0; i < 5; i++) { ("Internal traversal:{0}", i); yield return i; } }
We will not show all its decompilation results here, we will only show the core logic
//Foreach compiled resultsIEnumerator<int> enumerator = GetInts().GetEnumerator(); try { while (()) { int current = ; ("External traversal:{0}", current); } } finally { if (enumerator != null) { (); } } //The result after the GetInts method is compiledprivate IEnumerable<int> GetInts() { <GetInts>d__1 <GetInts>d__ = new <GetInts>d__1(-2); <GetInts>d__.<>4__this = this; return <GetInts>d__; }
Here we can seeGetInts()
The original code in the method is gone, but one more<GetInts>d__1
Type l, that is,yield return
The essence isSyntactic sugar
. Let's take a look<GetInts>d__1
Class implementation
//The generated class implements the IEnumerable interface and also implements the IEnumerator interface//Indicate that it contains both the GetEnumerator() method, the MoveNext() method and the Current attributeprivate sealed class <>GetIntsd__1 : IEnumerable<int>, IEnumerable, IEnumerator<int>, IEnumerator, IDisposable { private int <>1__state; //The current iteration result private int <>2__current; private int <>l__initialThreadId; public C <>4__this; private int <i>5__1; //The results of the current iteration int IEnumerator<int>.Current { get{ return <>2__current; } } //The results of the current iteration object { get{ return <>2__current; } } //The constructor contains state fields, and the direction change means that the state machine can realize core process flow through the state machine. public <GetInts>d__1(int <>1__state) { this.<>1__state = <>1__state; <>l__initialThreadId = ; } //Core method MoveNext private bool MoveNext() { int num = <>1__state; if (num != 0) { if (num != 1) { return false; } //Control status <>1__state = -1; //Self-increase, i++ loop in the code <i>5__1++; } else { <>1__state = -1; <i>5__1 = 0; } //Cycle termination condition i<5 in the above loop if (<i>5__1 < 5) { ("Internal traversal:{0}", <i>5__1); // Assign the current iteration result to the Current attribute <>2__current = <i>5__1; <>1__state = 1; //It is stated that it can continue to iterate return true; } //Iteration ends return false; } //IEnumerator's MoveNext method bool () { return (); } //IEnumerable IEnumerable method IEnumerator<int> IEnumerable<int>.IEnumerable() { //Instantiate <GetInts>d__1 instance <GetInts>d__1 <GetInts>d__; if (<>1__state == -2 && <>l__initialThreadId == ) { <>1__state = 0; <GetInts>d__ = this; } else { //Initialize the state machine <GetInts>d__ = new <GetInts>d__1(0); <GetInts>d__.<>4__this = <>4__this; } //Because <GetInts>d__1 implements the IEnumerator interface, you can return it directly return <GetInts>d__; } IEnumerator () { //Because <GetInts>d__1 implements the IEnumerator interface, it can be converted directly return ((IEnumerable<int>)this).GetEnumerator(); } void () { } void () { } }
Through the class it generates, we can see that the class is implementedIEnumerable
The interface has also been implementedIEnumerator
The interface indicates that it contains bothGetEnumerator()
Methods, also includeMoveNext()
Methods andCurrent
property. This class can satisfy thefoeach
The core structure of iteration. We wrote it manuallyfor
The code is includedMoveNext()
In the method, it contains the defined state mechanism code and moves iteration to the next element according to the current state machine code. Let's briefly explain oursfor
The code is translated toMoveNext()
Execution process in the method
- During the first iteration
<>1__state
Initialized to 0, representing the first iterated element, at this timeCurrent
The initial value is 0, and the loop control variable is<i>5__1
The initial value is also 0. - Determine whether the termination condition is met. If it is not met, the logic in the loop will be executed. And change the loader
<>1__state
is 1, which means that the first iteration execution is completed. - Loop control variables
<i>5__1
Continue to increase and change the loader<>1__state
is -1, which represents sustainable iteration. And loop to execute the custom logic of the loop body. - If the iteration condition is not met, return
false
, that is,MoveNext()
Iterative conditions are not metwhile (())
Logical termination.
We also show anotheryield return
The way is to include multipleyield return
Form of
IEnumerable<int> GetInts() { ("Internal traversal: 0"); yield return 0; ("Internal traversal: 1"); yield return 1; ("Internal traversal: 2"); yield return 2; }
The result of decompiling the above code is as follows. Here we only show the core methodMoveNext()
Implementation
private bool MoveNext() { switch (<>1__state) { default: return false; case 0: <>1__state = -1; ("Internal traversal: 0"); <>2__current = 0; <>1__state = 1; return true; case 1: <>1__state = -1; ("Internal traversal: 1"); <>2__current = 1; <>1__state = 2; return true; case 2: <>1__state = -1; ("Internal traversal: 2"); <>2__current = 2; <>1__state = 3; return true; case 3: <>1__state = -1; return false; } }
Through the compiled code, we can see that multipleyield return
The form will be compiled intoswitch...case
There are several forms ofyield return
It will be compiled inton+1
indivualcase
, one extracase
It representsMoveNext()
Termination condition, that is, returnfalse
conditions. Otherscase
Then returntrue
It means iterating can continue.
IAsyncEnumerable interface
We show the synchronization aboveyield return
Way,c# 8
New additions startedIAsyncEnumerable<T>
Interface is used to complete asynchronous iteration, that is, scenarios where the iterator logic contains asynchronous logic.IAsyncEnumerable<T>
The implementation code of the interface is as follows
public interface IAsyncEnumerable<out T> { IAsyncEnumerator<T> GetAsyncEnumerator(CancellationToken cancellationToken = default); } public interface IAsyncEnumerator<out T> : IAsyncDisposable { ValueTask<bool> MoveNextAsync(); T Current { get; } }
Its biggest difference is synchronousIEnumerator
What is included isMoveNext()
The method returnsbool
,IAsyncEnumerator
The interface containsMoveNextAsync()
Asynchronous method, the return isValueTask<bool>
type. So the above example code
await foreach (var num in GetIntsAsync()) { ("External traversal:{0}", num); }
So hereawait
Although it is addedforeach
The above, but the actual function is to execute every iterationMoveNextAsync()
method. It can be roughly understood as the following working method
IAsyncEnumerator<int> enumerator = (); while (().GetAwaiter().GetResult()) { var current = ; }
Of course, the actual compiled code does not look like this, we used in previous articles<Summary of research on async await state machine asynchronous operation>As explained in one articleasync await
Will be compiled intoIAsyncStateMachine
Asynchronous state machine, soIAsyncEnumerator<T>
Combinedyield return
The implementation is more complex and contains more code than the synchronization method. However, the implementation principle can be compared with the synchronization method. However, we will not show more about the implementation of the asynchronous state machine at the same time.Asynchronous yield return
After compilation, the students who are interested can learn about it.
foreach enhancement
c# 9
Added the enhanced function of foreach, that is, through the form of an extension method, the original inclusionforeach
Increased Objects of CapabilitiesGetEnumerator()
Methods so that ordinary classes do not haveforeach
The ability can also be used to iterate. It is used as follows
Foo foo = new Foo(); foreach (int item in foo) { (item); } public class Foo { public List<int> Ints { get; set; } = new List<int>(); } public static class Bar { //Define the extension method for Foo public static IEnumerator<int> GetEnumerator(this Foo foo) { foreach (int item in ) { yield return item; } } }
This function is indeed relatively powerful and meets the principle of openness and closure. We can enhance the function of the code without modifying the original code. It can be said that it is very practical. Let's take a look at what the compiled result is
Foo foo = new Foo(); IEnumerator<int> enumerator = (foo); try { while (()) { int current = ; (current); } } finally { if (enumerator != null) { (); } }
Here we see the extension methodGetEnumerator()
It is essentially syntactic sugar, which will compile the extension ability intoExtended class.GetEnumerator (extended instance)
way. That is the original way we write code, but the compiler helps us generate its calling method. Next, let's take a lookGetEnumerator()
What does the extension method compile into
public static IEnumerator<int> GetEnumerator(Foo foo) { <GetEnumerator>d__0 <GetEnumerator>d__ = new <GetEnumerator>d__0(0); <GetEnumerator>d__.foo = foo; return <GetEnumerator>d__; }
Do you think this code looks familiar? It's good and aboveyield return nature
The syntax sugar generation method mentioned in this section is the same. When compiling, a corresponding class is generated. The class here is<GetEnumerator>d__0
, Let's take a look at the structure of this class
private sealed class <GetEnumerator>d__0 : IEnumerator<int>, IEnumerator, IDisposable { private int <>1__state; private int <>2__current; public Foo foo; private List<int>.Enumerator <>s__1; private int <item>5__2; int IEnumerator<int>.Current { get{ return <>2__current; } } object { get{ return <>2__current; } } public <GetEnumerator>d__0(int <>1__state) { this.<>1__state = <>1__state; } private bool MoveNext() { try { int num = <>1__state; if (num != 0) { if (num != 1) { return false; } <>1__state = -3; } else { <>1__state = -1; //Because of the Ints in the example we are using List<T> <>s__1 = (); <>1__state = -3; } //Because the above extension method uses the foreach traversal method //It has also been compiled into the actual production method if (<>s__1.MoveNext()) { <item>5__2 = <>s__1.Current; <>2__current = <item>5__2; <>1__state = 1; return true; } <>m__Finally1(); <>s__1 = default(List<int>.Enumerator); return false; } catch { ((IDisposable)this).Dispose(); throw; } } bool () { return (); } void () { } void () { } private void <>m__Finally1() { } }
Seeing the code generated by the compiler, we can seeyield return
The generated code structure is the same, justMoveNext()
The logic in it depends on the specific logic we write the code, and different logics generate different codes. Here we will not explain the code generated, because the logic is similar to the code we explained above.
Summarize
Through this article we have introducedc#
In-houseyield return
grammar and explore some of the thoughts brought by it. We have shown through some simple examplesyield return
How to use iterators to process large amounts of data on demand. At the same time, we analyzeforeach
Iterative andyield return
The nature of grammar explains their implementation principles and underlying mechanisms. Fortunately, the knowledge involved is generally simple. If you read the relevant implementation code carefully, I believe you will understand the implementation principle behind it. I will not elaborate on it here.
This is the end of this article about thinking about the usage of yield return in C#. For more related C# yield return content, please search for my previous articles or continue browsing the related articles below. I hope everyone will support me in the future!