119

The following is okay:

try
{
    Console.WriteLine("Before");

    yield return 1;

    Console.WriteLine("After");
}
finally
{
    Console.WriteLine("Done");
}

The finally block runs when the whole thing has finished executing (IEnumerator<T> supports IDisposable to provide a way to ensure this even when the enumeration is abandoned before it finishes).

But this is not okay:

try
{
    Console.WriteLine("Before");

    yield return 1;  // error CS1626: Cannot yield a value in the body of a try block with a catch clause

    Console.WriteLine("After");
}
catch (Exception e)
{
    Console.WriteLine(e.Message);
}

Suppose (for the sake of argument) that an exception is thrown by one or other of the WriteLine calls inside the try block. What's the problem with continuing the execution in catch block?

Of course, the yield return part is (currently) unable to throw anything, but why should that stop us from having an enclosing try/catch to deal with exceptions thrown before or after a yield return?

Update: There's an interesting comment from Eric Lippert here - seems that they already have enough problems implementing the try/finally behaviour correctly!

EDIT: The MSDN page on this error is: http://msdn.microsoft.com/en-us/library/cs1x15az.aspx. It doesn't explain why, though.

Mike Christiansen
  • 1,104
  • 2
  • 13
  • 30
Daniel Earwicker
  • 114,894
  • 38
  • 205
  • 284

4 Answers4

60

I suspect this is a matter of practicality rather than feasibility. I suspect there are very, very few times where this restriction is actually an issue that can't be worked around - but the added complexity in the compiler would be very significant.

There are a few things like this that I've already encountered:

  • Attributes not being able to be generic
  • Inability for X to derive from X.Y (a nested class in X)
  • Iterator blocks using public fields in the generated classes

In each of these cases it would be possible to gain a little bit more freedom, at the cost of extra complexity in the compiler. The team made the pragmatic choice, for which I applaud them - I'd rather have a slightly more restrictive language with a 99.9% accurate compiler (yes, there are bugs; I ran into one on SO just the other day) than a more flexible language which couldn't compile correctly.

EDIT: Here's a pseudo-proof of how it why it's feasible.

Consider that:

  • You can make sure that the yield return part itself doesn't throw an exception (precalculate the value, and then you're just setting a field and returning "true")
  • You're allowed try/catch which doesn't use yield return in an iterator block.
  • All local variables in the iterator block are instance variables in the generated type, so you can freely move code to new methods

Now transform:

try
{
    Console.WriteLine("a");
    yield return 10;
    Console.WriteLine("b");
}
catch (Something e)
{
    Console.WriteLine("Catch block");
}
Console.WriteLine("Post");

into (sort of pseudo-code):

case just_before_try_state:
    try
    {
        Console.WriteLine("a");
    }
    catch (Something e)
    {
        CatchBlock();
        goto case post;
    }
    __current = 10;
    return true;

case just_after_yield_return:
    try
    {
        Console.WriteLine("b");
    }
    catch (Something e)
    {
        CatchBlock();
    }
    goto case post;

case post;
    Console.WriteLine("Post");


void CatchBlock()
{
    Console.WriteLine("Catch block");
}

The only duplication is in setting up try/catch blocks - but that's something the compiler can certainly do.

I may well have missed something here - if so, please let me know!

Jon Skeet
  • 1,421,763
  • 867
  • 9,128
  • 9,194
  • 11
    A good proof-of-concept, but this strategy gets painful (probably more for a C# programmer than for a C# compiler writer) once you you start creating scopes with things like `using` and `foreach`. For example: `try{foreach (string s in c){yield return s;}}catch(Exception){}` – Brian Oct 26 '10 at 20:28
  • 1
    The normal semantics of "try/catch" imply that if any part of a try/catch block is skipped because of an exception, control will transfer to a suitable "catch" block if one exists. Unfortunately, if an exception occurs "during" a yield return, there's no way for the iterator to distinguish the cases where it's being Disposed because of an exception from those where it's being Disposed because the owner retrieved all the data of interest. – supercat Oct 27 '11 at 20:53
  • 9
    "I suspect there are very, very few times where this restriction is actually an issue that can't be worked around" That's kind of like saying you don't need exceptions because you can use the error code returning strategy commonly used in C so many years ago. I admit the technical difficulties may be significant, but this still severely limits the usefulness of `yield`, in my opinion, because of the spaghetti code you have to write to work around it. – jpmc26 Sep 26 '13 at 18:50
  • @jpmc26: No, it really isn't like saying that at all. I can't remember this *ever* biting me, and I've used iterator blocks plenty of times. It *slightly* limits the usefulness of `yield` IMO - it's a long way from *severely*. – Jon Skeet Sep 26 '13 at 18:56
  • 3
    This 'feature' actually requires some rather ugly code in some cases to workaround, see http://stackoverflow.com/questions/5067188/yield-return-with-try-catch-how-can-i-solve-it – namey Apr 02 '16 at 22:05
  • 1
    @ jon- "I can't remember this ever biting me" is unpleasant to read. I regularly run into situations where I'm yielding to someone else's code and sometimes an exception is possible - this missing feature in the compiler prevents me from doing the obvious, right, simple thing: wrapping their code with a try/catch. Instead we have to write long, error-prone boilerplate. Net effect: like @jpmc26 I've learned to try to ban yield from all C# projects at design stage, it's a great half-feature that isn't really supported enough in depth. Often it's not possible to drop it:(. Back to square one! :) – Adam Sep 28 '20 at 19:16
  • Ah, but I misread some of this discussion - here everyone is only talking about random non-yield'ed methods throwing exceptions. My more common situation is yield'ed methods throwing exceptions. Which is even worse :) – Adam Sep 28 '20 at 19:26
  • @Adam: I would say that wrapping code with a try/catch is *not* the "obvious, right, simple thing" - I've found that over time, I've been catching fewer and fewer exceptions, preferring to let them bubble up in almost all cases. So still, in my experience, the feature is absolutely fine. Additionally, without the feature of `yield`, if you still want lazily iterated sequences, you'd have exactly the same possibility of an exception occurring - you'd just need to write more code first. – Jon Skeet Sep 28 '20 at 19:31
  • @JonSkeet in many cases I'd agree 100%. But a common exception I see is code that is acting as some kind of interpreter/wrapper for embedded code or 3rd party code, which is itself using yield. e.g. most recently: a core gameloop inside a game that allows any yieldable code to be added to its list of code to run once per cycle/gametick (which is the basis of almost all modern games). Or e.g. a proprietary server/socket-listener doing similar: hosting/wrapping multiple yieldable code chunks. In both cases: don't want the outer loop/wrapper to die! Want to kill the code chunk and drop from list – Adam Sep 30 '20 at 23:26
  • (and if that seems to you worthy of being a separate, unique, question - especially if there's good answers for that set of subcases - then I'm happy to post it - but it felt ot me like it wasn't that unique, was merely a simple example of where this yield/exception misalignment is problematic. I currently make my iterator-wrappers more complex to workaround this, try to isolate the code and document "here be dragons - don't alter it!", and suck it down, but it feels like it shouldn't be so ugly/hard to maintain :)) – Adam Sep 30 '20 at 23:28
  • @Adam: Yes, I think it's worth a different question to make it a lot clearer. (In particular, I still don't see how it's a problem with `yield` itself - I think you'd get the same kind of problem with manually written iterators.) – Jon Skeet Oct 01 '20 at 06:01
6

All the yield statements in an iterator definition are converted to a state in a state machine which effectively uses a switch statement to advance states. If it did generate code for yield statements in a try/catch it would have to duplicate everything in the try block for each yield statement while excluding every other yield statement for that block. This isn't always possible, particularly if one yield statement is dependant on an earlier one.

Mark Cidade
  • 98,437
  • 31
  • 224
  • 236
  • 2
    I don't think I buy that. I reckon it would be entirely feasible - but very complicated. – Jon Skeet Dec 06 '08 at 21:00
  • 2
    Try/catch blocks in C# aren't meant to be re-entrant. If you split them up then it's possible to call MoveNext() after an exception and continue the try block with a possibly invalid state. – Mark Cidade Dec 09 '08 at 05:44
4

I've accepted THE INVINCIBLE SKEET's answer until someone from Microsoft comes along to pour cold water on the idea. But I don't agree with the matter-of-opinion part - of course a correct compiler is more important than a complete one, but the C# compiler is already very clever in sorting out this transformation for us as far as it does. A little more completeness in this case would make the language easier to use, teach, explain, with fewer edge cases or gotchas. So I think it would be worth the extra effort. A few guys in Redmond scratch their heads for a fortnight, and as a result millions of coders over the next decade can relax a little more.

(I also harbour a sordid desire for there to be a way to make yield return throw an exception that has been stuffed into the state machine "from the outside", by the code driving the iteration. But my reasons for wanting this are quite obscure.)

Actually one query I have about Jon's answer is to do with the yield return expression throwing.

Obviously yield return 10 isn't so bad. But this would be bad:

yield return File.ReadAllText("c:\\missing.txt").Length;

So wouldn't it make more sense to evaluate this inside the preceeding try/catch block:

case just_before_try_state:
    try
    {
        Console.WriteLine("a");
        __current = File.ReadAllText("c:\\missing.txt").Length;
    }
    catch (Something e)
    {
        CatchBlock();
        goto case post;
    }
    return true;

The next problem would be nested try/catch blocks and rethrown exceptions:

try
{
    Console.WriteLine("x");

    try
    {
        Console.WriteLine("a");
        yield return 10;
        Console.WriteLine("b");
    }
    catch (Something e)
    {
        Console.WriteLine("y");

        if ((DateTime.Now.Second % 2) == 0)
            throw;
    }
}
catch (Something e)
{
    Console.WriteLine("Catch block");
}
Console.WriteLine("Post");

But I'm sure it's possible...

Daniel Earwicker
  • 114,894
  • 38
  • 205
  • 284
  • 1
    Yes, you'd put the evaluation into the try/catch. It doesn't really matter where you put the variable setting. The main point is that you can effectively break a single try/catch with a yield return in it into two try/catches with a yield return between them. – Jon Skeet Dec 07 '08 at 08:46
2

I would speculate that because of the way the call stack gets wound/unwound when you yield return from an enumerator it becomes impossible for a try/catch block to actually "catch" the exception. (because the yield return block is not on the stack, even though he originated the iteration block)

To get an ideea of what I'm talking about setup an iterator block and a foreach using that iterator. Check what the Call Stack looks like inside the foreach block and then check it inside the iterator try/finally block.

StingyJack
  • 19,041
  • 10
  • 63
  • 122
Radu094
  • 28,068
  • 16
  • 63
  • 80
  • I'm familiar with stack unwinding in C++, where destructors are called on local objects that go out of scope. The corresponding thing in C# would be try/finally. But that unwinding does not occur when yield return happens. And for try/catch there is no need for it to interact with yield return. – Daniel Earwicker Dec 06 '08 at 16:12
  • Check what happens to the call stack when looping over an iterator and you'll understand what I mean – Radu094 Dec 06 '08 at 18:58
  • @Radu094: No, I'm sure it would be possible. Don't forget that it already handles finally, which is at least somewhat similar. – Jon Skeet Dec 06 '08 at 21:17