TLDR: Exploring C#’s yield keyword for efficient code usage. Understanding its history, usage, and essential requirements to make cleaner code and reduce memory usage.
Programming in C# offers many features. One of them, introduced in C# version 2.0, is a powerful feature called yield. Despite its complexity, understanding yield correctly can greatly benefit your projects.
In this blog, we’ll delve into the yield keyword and its usage in C#, focusing specifically on memory efficiency. We’ll explore how yield can help you reduce memory consumption while handling large data sets.
Yield was introduced in C# 2.0. Before its introduction, iterating over a collection required implementing a custom iterator using the IEnumerator interface. This involved creating a class that implemented IEnumerator and IEnumerable interfaces and managing the iterator’s state manually.
The yield keyword is used for stateful iteration over a collection in a customized manner. The yield keyword informs the compiler that the method that contains it is an iterator block.
In other words, yield will provide the next element or value from the sequence when iterating a sequence. We don’t need to wait to complete the iteration to get the items from the iterating sequence. It will return one element in the collection at a time. Simply, when using the yield return statement, we don’t need to create a temporary collection (array, list, etc.) to store the data and eventually return all the data together.
The yield statement has the two following forms:
Let’s understand these concepts with some examples.
First, let’s discuss the example using the classic approach in C#.
var numList = new List<int>() { 1, 4, 5, 7, 4, 10 }; var sumResults = SumOfNums(numList); foreach (var sumResult in sumResults) { Console.WriteLine(sumResult); } IEnumerable<int> SumOfNums(List<int> nums) { var results = new List<int>(); var sum = 0; foreach (var num in nums) { sum += num; results.Add(sum); } return results; }
In this example, we calculated the sum of the values in the numList list using the SumOfNums method. Inside the SumOfNums method, we created a list to return the results list.
After running the previous code examples, we’ll get the following output.
Output : 1 5 10 17 21 31
If we have thousands of elements in the array, we must wait until it returns the results list using this approach. It would slow the computation and require a significant amount of memory to execute.
Now, let’s understand how this classic approach works using a simplified flow for the previous example.
In this flow chart:
Refer to the following code example to understand how using the yield keyword improves this approach.
var numList = new List<int>() { 1, 4, 5, 7, 4, 10 }; foreach (var result in SumOfNums(numList)) { Console.WriteLine(result); } IEnumerable<int> SumOfNums(List<int> nums) { var sum = 0; foreach (var num in nums) { sum += num; yield return sum; } }
We have used the yield keyword, so we don’t need to wait until the SumOfNums method is completed to print the results like in the previous example.
When the SumOfNums method is called, it executes until it reaches the yield return statement. At that point, the control shifts to the preceding foreach loop, which runs the SumOfNums method. Then, it will print the result and proceed to the next iteration of the SumOfNums method. This process continues until the iteration of the entire list is completed.
Following is the output of the example. There isn’t any change in the output.
Output : 1 5 10 17 21 31
For the previous example, let’s understand how this yield approach functions through a simplified flow.
In this flow chart:
Let’s understand how memory usage works with each approach.
First, consider an example of handling a huge array of elements using the classic approach.
Refer to the following code example.
var arr = new int[1000]; Stopwatch stopwatch = new Stopwatch(); stopwatch.Start(); var result = SumOfNums(arr.ToList()); stopwatch.Stop(); Console.WriteLine("Time elapsed using classic approach: " + stopwatch.ElapsedMilliseconds + "ms"); long memoryBefore = GC.GetTotalMemory(true); result = SumOfNums(arr.ToList()); long memoryAfter = GC.GetTotalMemory(true); Console.WriteLine("Memory used using classic approach: " + (memoryAfter - memoryBefore) + "bytes");
In the previous example, we executed the SumOfNums method for the classic approach and then measured the time and memory consumption for it. Refer to the following output.
Time elapsed using classic approach: 0ms Memory used using classic approach: 8508bytes
Let’s also consider an example with a large array of elements using the yield approach to measure the time and memory consumption.
var arr = new int[1000]; Stopwatch stopwatch = new Stopwatch(); stopwatch.Start(); var result = SumOfNums(arr.ToList()); stopwatch.Stop(); Console.WriteLine("Time elapsed using yield approach: " + stopwatch.ElapsedMilliseconds + "ms"); long memoryBefore = GC.GetTotalMemory(true); result = SumOfNums(arr.ToList()); long memoryAfter = GC.GetTotalMemory(true); Console.WriteLine("Memory used using yield approach: " + (memoryAfter - memoryBefore) + "bytes");
In this example, we’ve executed the SumOfNums method in the yield approach and then measured the time and memory consumption using this approach to compare it with the classic approach.
Refer to the following output for the measured results of time and memory consumption using the yield approach.
Time elapsed using yield approach: 0ms Memory used using yield approach: 4112bytes
As you can see, the yield approach is much more memory-efficient, consuming only 4,112 bytes of memory compared to the classic approach’s 8,508 bytes. This memory optimization becomes increasingly crucial when handling large data sets or sequences. While the elapsed time may be similar in both approaches, the memory savings provided by the yield approach can enhance the overall app performance.
The yield break statement serves to terminate the iterator (array, list, etc.) of the block. Let’s explore the function of the yield break statement with an illustrative example.
var numList = new List<int>() { 1, 4, 5, 7, 4, 10 }; foreach (var result in SumOfNums(numList)) { Console.WriteLine(result); } IEnumerable<int> SumOfNums(List<int> nums) { var sum = 0; for (int i = 0; i < nums.Count; i++) { if (i == 2) { yield break; } sum += nums[i]; yield return sum; } }
In the code example, an if condition statement is used to check whether the value of i is equal to 2. Upon satisfying this condition, the yield break statement is executed, effectively halting the iteration of the block. Consequently, no further items are returned. This usage of the yield break statement enables the termination of the iterator within the block.
After executing the previous code examples, we’ll get the following output:
Output : 1 5
From this output, we observe that only two results are printed. This outcome arises from the termination of the for loop when the value of i equals 2, thereby concluding the iteration.
The yield keyword is a powerful tool for memory-efficient programming, especially in scenarios involving large data sets or sequences.
Let’s delve deeper into how yield influences memory usage:
Certain prerequisites must be met to utilize the yield keyword effectively:
Thanks for reading! This article has discussed the yield keyword in C# with a few examples. We have also listed a few requirements that should be met to use the yield keyword. The point to highlight is that when we use the yield keyword with a huge iteration process, it will require less memory than usual. Furthermore, it will help to reduce the lines of code and maintain the code cleanly.
I hope this article helped you learn about the yield keyword in C#. Feel free to share your opinions or experiences in the comment section below. For questions, you can contact us through our support forum, support portal, or feedback portal. We are happy to assist you!