Sunday, July 20, 2014

Understanding the C# Yield

We have all seen the new "yield" keyword in C# at one point or another, but if you are like me, sometimes, we just say, "what's the big deal, I can do that myself" and move on.

But this weekend, I took time to get a bit more understanding of "yield."  I looked around on the net to to see various examples, but none of them still made sense to me.  So I decided to write a bit of code to experiment. The example included on this post contains the complete code that you can run as a console app.

I found that most examples show the use of the yielding function from within another loop, and that did not make much sense to me, since if you do that, then I would think that why not even bother having a separate loop. But hardly no examples showed me to use an Enumerator to iterate through, throughout a part of program, and that's what I plan to demonstrate here.

First, a bit about the example. Let's say that we need a program for the shipping department of a computer store and you want to make sure the employees will not forget to pack each part in the right box. You would have a list of parts like monitor, keyboard etc., and display the instructions what goes where etc.

What I wanted to get out of this example is to see if my assumptions are correct, and in one case I was not, and glad I caught that by writing this example.

So here is what I have learned;

It's a Way To Implement the Deferred Computation Pattern

  • The yield consutruction is probably good for performing complex calculations within a loop and especially the computation for a specific part of iteration is not performed until you request it, you might save resource or time.

    This approach appears to be very effective if I create an Enumerator then use MoveNext() to the next item, and that's where its power lies. For example, accessing a database or looking up something from a web service. You can defer the computation like this just until you call MoveNext(), and you do not need to write a special state management between the calls. Come to think of it, that's how LINQ works and that's the core of IEnumerable is about. Makes sense!

    We can certainly do this without yield, but to do the same thing, I have to keep track of the states and either store them as private data members or in a separate class structure and pass that back and forth. Also some loops take a decent effort to initialize, and in the yeild approach, we can certainly reduce that effort.

    As the functional programming approch recommends, the more states we create and must manage locally, they tend to cause hard-to find/fix bugs.

    So the advantage with this is a much simpler (hence reliable and testable) design.

My Bad Assumption Cleared

  • If you create a foreach loop again, the iteration will reset from the position 0 for each loop. I misunderstood this aspect. I thought if I have left the loop (say picked up first 3 items) and then did another loop for the next set, I thought I can pick up from where I left off. It does not work that way. So you have also been warned now.

The Code

using System;
using System.Collections.Generic;
using System.Linq;

namespace Yield
{
    class Program
    {
        static void Main(string[] args)
        {
            var sample = new YeldSample();
            sample.Run();
        }
    }

    public class YeldSample
    {
        public void Run()
        {
            var packingList = new List<string>
            {
                "A: Memory""A: Disk""A: DVD Drive",
                "B: Display",  "B: Monitor Cable""B: Monitor Power Cord",
                "C: Mouse""C: Memory Card""C: Keyboard",
            };

            var itemCount = 0;
            var iterator = YieldPart(packingList);

            var enumerable = iterator as string[] ?? iterator.ToArray();
            foreach (var item in enumerable)
            {
                Console.WriteLine("{0} Done!", item);
                itemCount++;
                if (itemCount == 3) break;
            }

            Console.WriteLine("\r\nBad Example Below. This will begin the loop from the start again.\r\n");
            itemCount = 0;

            foreach (var item in enumerable)
            {
                Console.WriteLine("{0} packed again!", item);
                itemCount++;
                if (itemCount == 3) break;
            }

            Console.WriteLine("\r\nExample Using the Enumerator\r\n");

            var items = enumerable.GetEnumerator();

            items.MoveNext();
            Console.WriteLine("{0} Done!", items.Current);
            items.MoveNext();
            Console.WriteLine("{0} Done!", items.Current);
            items.MoveNext();
            Console.WriteLine("{0} Done!", items.Current);

            Console.WriteLine("\r\nTake a break!\r\n");

            items.MoveNext();
            Console.WriteLine("{0} Done!", items.Current);
            items.MoveNext();
            Console.WriteLine("{0} Done!", items.Current);
            items.MoveNext();
            Console.WriteLine("{0} Done!", items.Current);

            Console.WriteLine("\r\nTake a break!\r\n");

            items.MoveNext();
            Console.WriteLine("{0} Done!", items.Current);
            items.MoveNext();
            Console.WriteLine("{0} Done!", items.Current);
            items.MoveNext();
            Console.WriteLine("{0} Done!", items.Current);

        }

        public IEnumerable<String> YieldPart(List<String> parts)
        {
            foreach (var part in parts)
            {
                var dest = "";
                if (part.StartsWith(("A:"))) dest = "main carton.";
                if (part.StartsWith(("B:"))) dest = "monitor carton.";
                if (part.StartsWith(("C:"))) dest = "accessory carton.";

                var phrase = String.Format("Put {0} in the {1}", part.Substring(3), dest);
                yield return phrase;
            }
        }
    }
}
Post a Comment