Working Around Iterator Function Limitations
As you use iterator functions (and yield
) more and more, you’ll start to run into some limitations in the C# language. For instance, you can’t yield
inside a try
block that has a catch
block. And the foreach
loop doesn’t provide a very good way to catch exceptions when looping over an iterator function, either. Today’s article goes into detail to find solutions to these issues and make iterator functions usable in even the trickiest scenarios!
Let’s jump right in to the first scenario: you’re not allowed to yield
in a try
block that has a catch
block. If you do, you’ll get this error:
Cannot yield a value in the body of a try block with a catch clause.
Here’s an example function that triggers that error:
private IEnumerable IteratorFunctionBroken() { try { yield return "a"; OtherFunction(); yield return "b"; } catch (Exception ex) { Debug.Log("exception: " + ex); } }
The solution, clearly, is to move the yield
statements out of the try
block. In a simple function like the one above, that’s pretty easy:
private IEnumerable IteratorFunctionWorking() { yield return "a"; try { OtherFunction(); } catch (Exception ex) { Debug.Log("exception: " + ex); yield break; } yield return "b"; }
Notice that an extra yield break
had to be added in the catch
block because an exception should have ended the execution of the try
block.
As the try
block grows, you’ll need to split the code into multiple try-catch
blocks. Consider this slightly more complicated function:
private IEnumerable IteratorFunctionBroken() { try { yield return "a"; OtherFunction(); yield return "b"; YetOtherFunction(); yield return "c"; } catch (Exception ex) { Debug.Log("exception: " + ex); } }
It needs to get split into two try-catch
blocks:
private IEnumerable IteratorFunctionWorking() { yield return "a"; try { OtherFunction(); } catch (Exception ex) { Debug.Log("exception: " + ex); yield break; } yield return "b"; try { YetOtherFunction(); } catch (Exception ex) { Debug.Log("exception: " + ex); yield break; } yield return "c"; }
Unfortunately, we now have duplicated code because both catch
blocks are exactly the same. To solve this, you can either create a function with the catch
block code in it, or use a goto
to keep the exception handling within the same function. Here are both versions:
// 'catch' block replacement private void HandleException(Exception ex) { Debug.Log("exception: " + ex); } private IEnumerable IteratorFunctionWorking() { yield return "a"; try { OtherFunction(); } catch (Exception ex) { // Call the 'catch' block code HandleException(ex); yield break; } yield return "b"; try { YetOtherFunction(); } catch (Exception ex) { // Call the 'catch' block code HandleException(ex); yield break; } yield return "c"; } private IEnumerable IteratorFunctionWorking() { yield return "a"; // Holds the exception if one occurs Exception caughtException; try { OtherFunction(); } catch (Exception ex) { // Assign to the exception at the function level caughtException = ex; // Go to the exception-handling code below goto handle_exception; } yield return "b"; try { YetOtherFunction(); } catch (Exception ex) { // Assign to the exception at the function level caughtException = ex; // Go to the exception-handling code below goto handle_exception; } yield return "c"; // Break so we don't handle the exception when it hasn't // been caught yield break; // 'catch' block replacement handle_exception: Debug.Log("exception: " + ex); }
The complexity of the workarounds grow with the complexity of the iterator function. For example, if we want to perform more code after the try-catch
then we can’t yield break
in the catch
. Here’s an example:
private IEnumerable IteratorFunctionBroken() { try { yield return "a"; OtherFunction(); yield return "b"; } catch (Exception ex) { Debug.Log("exception: " + ex); } // This is something we'd like to do after the try-catch yield return "c"; }
To work around this we can once again employ goto
to skip around in the function:
private IEnumerable IteratorFunctionWorking() { yield return "a"; try { OtherFunction(); } catch (Exception ex) { Debug.Log("exception: " + ex); // Can't just 'yield break' here // That would skip the 'yield return c' // Skip the 'yield return b' using 'goto' goto after_try_catch; } yield return "b"; // This label is for what came after the original function's try-catch after_try_catch: yield return "c"; }
With this in mind, we can solve a related problem. When we need to loop over an iterator function that returns IEnumerable
or IEnumerable<T>
it’s very nice to be able to use a foreach
loop like so:
// Yield the values that IteratorFunction() yields // This is an example of a nested iterator function foreach (var cur in IteratorFunction()) { yield return cur; }
What do we do if we want to catch exceptions that IteratorFunction
throws? If we simply wrap the loop in a try-catch
block then we’ll run into the above problem. Here’s how it’d look:
try { foreach (var cur in IteratorFunction()) { // This line will cause a compiler error // Not allowed to yield in a 'try' block that has a 'catch' block yield return cur; } } catch (Exception ex) { Debug.Log("IteratorFunction() threw exception: " + ex); }
The solution is to ditch the convenience of the foreach
loop and replace it with a for
loop. This allows us to isolate the calls to MoveNext()
on the IEnumerator
from the rest of the loop logic. Here’s how that solution looks:
IEnumerable enumerable = IteratorFunction(); IEnumerator enumerator = enumerable.GetEnumerator(); for (bool more = true; more; ) { // Catch exceptions only on executing/resuming the iterator function try { more = enumerator.MoveNext(); } catch (Exception ex) { Debug.Log("IteratorFunction() threw exception: " + ex); } // Yielding and other loop logic is moved outside of the try-catch yield return enumerator.Current; }
And with that we have an effective workaround for some of the most common C# language issues with iterator functions. It’s unfortunate that we need to add so much complexity to our code and consequently make it much more difficult to read, write, and maintain. If you know of a different or better way to work around these issues than what I’ve presented in the article, please let me know by posting your solution in the comments!
#1 by Vladimir Nesterovsky on August 7th, 2017 ·
Thanks! A small correction.
I would wrap all cycle into:
using(var enumerator = IteratorFunction().GetEnumerator())
{
for …
}
#2 by jackson on August 8th, 2017 ·
That’s a good, minor point. Some enumerators may need to be disposed, so you should make sure to either call
Dispose
manually or viausing
. Thanks!#3 by Dan on October 26th, 2017 ·
One issue I ran into using your workaround for a foreach loop was that I was yielding the last item in my collection twice as MoveNext() returned false but there was still another yield before it got back to the check in the for loop.
I got around it by simply putting
if (more)
in front of my yield return statement.