Finally block may not be called when enumerating over yielding method [duplicate]
Asked Answered
A

2

6

I found a situation when finally block is not called.

To the point:

using System;
using System.Collections.Generic;
using System.Threading;
using System.ComponentModel;

    class MainClass{
        static IEnumerable<int> SomeYieldingMethod(){
            try{
                yield return 1;
                yield return 2;
                yield return 3;
            }finally{
                Console.WriteLine("Finally block!");
            }
        }

        static void Main(){
            Example(7);
            Example(2);
            Example(3);
            Example(4);
        }

        static void Example(int iterations){
            Console.WriteLine("\n Example with {0} iterations.", iterations);
            var e = SomeYieldingMethod().GetEnumerator();
            for (int i = 0; i< iterations; ++i) {
                Console.WriteLine(e.Current);
                e.MoveNext();
            }
        }
    }

And the result:

Example with 7 iterations.
0
1
2
3
Finally block!
3
3
3

Example with 2 iterations.
0
1

Example with 3 iterations.
0
1
2

Example with 4 iterations.
0
1
2
3
Finally block!

So, it looks like if someone is using my yielding method and works with it manually using enumerators (not with foreach) then my finally block can never be called.

Is there any way to ensure that my method finalizes it's resources? And is that a bug in "yield syntactic sugar" or does it work as it supposed to be?

Allwein answered 12/11, 2013 at 13:18 Comment(1)
This is not at all a duplicate. This is asking if the finally block is called when the enumerator is disposed. The linked question is asking why you can't use yield in a try/catch block. Note that you can use yield in a try/finally block, as this code does.Backsaw
S
4

That is because they didn't use the iterator correctly. That code should be:

using(var e = SomeYieldingMethod().GetEnumerator())
{
    for (int i = 0; i< iterations; ++i) {
        Console.WriteLine(e.Current);
        e.MoveNext();
    }
}

The using is important here. This is what maps to the finally etc (assuming the iterator hasn't already finished naturally).

Specifically, foreach explicitly calls Dispose() on the iterator if the iterator implements IDisposable. Since IEnumerator<T> : IDisposable, this includes almost all iterators (caveat: foreach does not require IEnumerator[<T>]).

Strut answered 12/11, 2013 at 13:25 Comment(0)
P
1

It's working as intended, finally gets executed on error or when you get to the block, if they never enumerate untill the end, you never get to the end of that block so finally isn't supposed to execute.

Yield creates a class to represent your method as a method + state put into members + place to resume, so you're "resuming" the method each time, you never get to the finally if your finally is placed at the end of the enuemration & you never enumerate untill the end. If you want the finally block to execute everytime, you'll need to do eager evaluation but you can't say something like "i want this to be streamed, and finally to be called at the end, but have the compiler automagically detect in client code if it's never going to Stream to the end and then call finally anyway". There's no "proper" time to call finally, for all you know he may pass along the enumerator to another project & have it keep enumerating later, what then would happen if it kept enumerating & the compiler wrongfully called finally?

Ploce answered 12/11, 2013 at 13:24 Comment(1)
"There's no "proper" time to call finally," - actually, there very much is: that time is well-defined. It is when the iterator is disposed (assuming it hasn't finished already).Strut

© 2022 - 2024 — McMap. All rights reserved.