a concern about yield return and breaking from a foreach
Asked Answered
H

3

25

Is there a proper way to break from a foreach such that the IEnumerable<> knows that I'm done and it should clean up.

Consider the following code:

    private static IEnumerable<Person> getPeople()
    {
        using (SqlConnection sqlConnection = new SqlConnection("..."))
        {
            try
            {
                sqlConnection.Open();
                using (SqlCommand sqlCommand = new SqlCommand("select id, firstName, lastName from people", sqlConnection))
                {

                    using (SqlDataReader reader = sqlCommand.ExecuteReader())
                    {
                        while (reader.Read())
                            yield return new Person(reader.GetGuid(0), reader.GetString(1), reader.GetString(2));
                    }
                }
            }
            finally
            {
                Console.WriteLine("finally disposing of the connection");
                if (sqlConnection.State == System.Data.ConnectionState.Open)
                    sqlConnection.Close();
            }
        }
    }

If he consumer does not break from the foreach then everthing is fine and the reader will return false, the while loop willend and the function cleans up the database command and connection. But what happens if the caller breaks from the foreach before i'm finished?

Harpoon answered 10/9, 2009 at 15:8 Comment(1)
see also #1400646Vidicon
A
38

Excellent question. You do not need to worry about this; the compiler takes care of it for you. Basically, what we do is we put the cleanup code for the finally blocks into a special cleanup method on the generated iterator. When control leaves the caller's foreach block, the compiler generates code which calls the cleanup code on the iterator.

A simplified example:

static IEnumerable<int> GetInts()
{
    try { yield return 1; yield return 2;} 
    finally { Cleanup(); }
}

Your question is basically "Is Cleanup() called in this scenario?"

foreach(int i in GetInts()) { break; }

Yes. The iterator block is generated as a class with a Dispose method that calls Cleanup, and then the foreach loop is generated as something similar to:

{
  IEnumerator<int> enumtor = GetInts().GetEnumerator();
  try
  {
    while(enumtor.MoveNext())
    {
      i = enumtor.Current;
      break;
    }
  }
  finally
  {
    enumtor.Dispose();
  }
}

So when the break happens, the finally takes over and the disposer is called.

See my recent series of articles if you want more information about some of the weird corner cases we considered in the design of this feature.

http://blogs.msdn.com/ericlippert/archive/tags/Iterators/default.aspx

Af answered 10/9, 2009 at 16:23 Comment(0)
L
2

You can use the statement

yield break;

to break out of a yield loop early, but your code reveals a misunderstanding I think... When you use the "using" statement,

using (SqlConnection sqlConnection = new SqlConnection("...")) 
{
   //  other stuff
}

you AUTOMATICALLY get a try finally block in the compiled IL code, and the finnaly block will call to Dispose, and in the Dispose code the connection will be closed...

Libau answered 10/9, 2009 at 15:10 Comment(0)
E
2

Let's see if I get your question.

foreach(Person p in getPeople())
{
    // break here
}

because of the foreach keyword, the Enumerator is properly disposed. During the disposal of Enumerator, the execution of getPeople() is terminated. So the connection is properly cleaned up.

Emunctory answered 10/9, 2009 at 15:17 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.