bpeikes
bpeikes

Reputation: 3705

"yield return" from event handler

I have a class which takes a stream in the constructor. You can then set up callbacks for various events, and then call StartProcessing. The issue is that I want to use it from a function which should return an IEnumerable.

Example:

public class Parser
{
    public Parser(System.IO.Stream s) { // saves stream and does some set up }
    public delegate void OnParsedHandler(List<string> token);
    public event OnParsedHandler OnParsedData;
    public void StartProcessing()
    {
        // reads stream and makes callback when it has a whole record
    }
 }

 public class Application
 {
      public IEnumerable<Thing> GetThings(System.IO.Stream s)
      {
            Parser p = new Parser(s);
            p.OnParsedData += (List<string> str) => 
            {
                Thing t = new Thing(str[0]);
                // here is where I would like to yield
                // but I can't
                yield return t;
            };


            p.StartProcessing();
      }
 }

Right now my solution, which isn't so great, is to put them all the Things into a List which is captured by the lambda, and then iterate over them after calling StartProcessing.

public class Application
 {
      public IEnumerable<Thing> GetThings(System.IO.Stream s)
      {
            Parser p = new Parser(s);
            List<Thing> thingList = new List<Thing>();

            p.OnParsedData += (List<string> str) => 
            {
                Thing t = new Thing(str[0]);
                thingList .Add(t);
            };


            p.StartProcessing();
            foreach(Thing t in thingList )
            {
                  yield return t;
            }
      }
 }

The issue here is that now I have to save all of the Thing objects into list.

Upvotes: 4

Views: 1496

Answers (2)

Leandro
Leandro

Reputation: 1555

Interesting question. I would like to build upon what @servy has said regarding push and pull. In your implementation above, you are effectively adapting a push mechanism to a pull interface.

Now, first things first. You have not specified whether the call to the StartProcessing() method is a blocking call or not. A couple of remarks regarding that:

  • If the method is blocking (synchronous), then there is really no point in adapting it to a pull model anyway. The caller will see all the data processed in a single blocking call.

  • In that regard, receiving the data indirectly via an event handler scatters into two seemingly unrelated constructs what should otherwise be a single, cohesive, explicit operation. For example:

    void ProcessAll(Action<Thing> callback);
    

On the other hand, if the StartProcessing() method actually spawns a new thread (maybe better named BeginProcessing() and follow the Event-based Asynchronous Pattern or another async processing pattern), you could adapt it to a pull machanism by means of a synchronization construct using a wait handle: ManualResetEvent, mutex and the like. Pseudo-code:

public IEnumerable<Thing> GetThings(System.IO.Stream s)
{
    var parser = new Parser(s);
    var waitable = new AutoResetEvent(false);
    Thing item = null;

    parser.OnParsedData += (Thing thing) => 
    {
        item = thing;
        waitable.Set();
    };

    IAsyncResult result = parser.BeginProcessing();
    while (!result.IsCompleted)
    {
        waitable.WaitOne();
        yield return item;            
    }
}

Disclaimer

The above code serves only as a means for presenting an idea. It is not thread-safe and the synchronization mechanics do not work properly. See the producer-consumer pattern for more information.

Upvotes: 2

Servy
Servy

Reputation: 203844

The problem you have here is that you don't fundamentally have a "pull" mechanic here, you're trying to push data from the parser. If the parser is going to push data to you, rather than letting the caller pull the data, then GetThings should return an IObservable, rather than an IEnumerable, so the caller can consume the data when it's ready.

If it really is important to have a pull mechanic here then Parser shouldn't fire an event to indicate that it has new data, but rather the caller should be able to ask it for new data and have it get it; it should either return all of the parsed data, or itself return an IEnumerable.

Upvotes: 4

Related Questions