Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

yield return vs. return IEnumerable<T>

I've noticed something curious about reading from an IDataReader within a using statement that I can't comprehend. Though I'm sure the answer is simple.

Why is it that whilst inside the using (SqlDataReader rd) { ... } if I directly perform a yield return the reader stays open for the duration of the read. But if I perform a direct return calling a SqlDataReader extension method (outlined below) that the reader closes before the enumerable can be actualized?

public static IEnumerable<T> Enumerate<T>(this SqlDataReader rd)
{
    while (rd.Read())
        yield return rd.ConvertTo<T>(); //extension method wrapping FastMember

    rd.NextResult();
}

To be absolutely clear of what I'm asking, I'm unsure why the following are fundamentally different:

A fleshed out example, as per @TimSchmelter's request:

/*
 * contrived methods
 */
public IEnumerable<T> ReadSomeProc<T>() {
    using (var db = new SqlConnection("connection string"))
    {
        var cmd = new SqlCommand("dbo.someProc", db);

        using(var rd = cmd.ExecuteReader())
        {
            while(rd.Read())
                yield return rd.ConvertTo<T>(); //extension method wrapping FastMember
        }
    }
}


//vs
public IEnumerable<T> ReadSomeProcExt<T>() {
    using (var db = new SqlConnection("connection string"))
    {
        var cmd = new SqlCommand("dbo.someProc", db);

        using(var rd = cmd.ExecuteReader())
        {
            return rd.Enumerate<T>(); //outlined above
        }
    }
}

/*
 * usage
 */
var lst = ReadSomeProc<SomeObect>();

foreach(var l in lst){
    //this works
}

//vs
var lst2 = ReadSomeProcExt<SomeObect>();

foreach(var l in list){
    //throws exception, invalid attempt to read when reader is closed
}
like image 235
pim Avatar asked Aug 15 '17 15:08

pim


People also ask

Should you return IEnumerable?

If you do not counting in your external code it is always better to return IEnumerable, because later you can change your implementation (without external code impact), for example, for yield iterator logic and conserve memory resources (very good language feature by the way).

What is the difference between return and yield return C#?

The only difference between yield and return is whenever yield statement is encountered in a function, the execution of function is suspended and a value is send back to the caller but because of yield whenever the function is called again, the execution of function begin where it left off previously.

What is a yield return?

The yield is the income the investment returns over time, typically expressed as a percentage, while the return is the amount that was gained or lost on an investment over time, usually expressed as a dollar value.

What is IEnumerable return type?

The return type of the iterator method is IEnumerable, which is an iterator interface type. When the iterator method is called, it returns an enumerable object that contains the powers of a number. The following example demonstrates a get accessor that is an iterator.


Video Answer


2 Answers

Summary: Both versions of the method defer, but because ReadSomeProcExt doesn't defer execution, the reader is disposed before execution is passed back to the caller (i.e. before Enumerate<T> can run). ReadSomeProc, on the other hand, doesn't create the reader until it's been passed back to the caller, so it doesn't dispose the container until all its values have been read.

When your method uses yield return, the compiler actually changes the compiled code to return an IEnumerable<>, and the code in your method will not run until other code starts iterating over the returned IEnumerable<>.

That means that the code below doesn't even run the first line of your Enumerate method before it disposes the reader and returns a value. By the time someone else starts iterating over your returned IEnumerable<>, the reader has already been disposed.

using(SqlDataReader rd = cmd.ExecuteReader()){
    return rd.Enumerate<T>();
}

But this code would execute the entire Enumerate() method in order to produce a List<> of results prior to returning:

using(SqlDataReader rd = cmd.ExecuteReader()){
    return rd.Enumerate<T>().ToList();
}

On the other hand, whoever's calling the method with this code doesn't actually execute the method until the result is evaluated:

using(SqlDataReader rd = cmd.ExecuteReader()){
    while(rd.Read())
        yield return rd.ConvertTo<T>(); //extension method wrapping FastMember
}

But the moment they execute the returned IEnumerable<>, the using block opens up, and it doesn't Dispose() until the IEnumerable<> finishes its iterations, at which point you will have already read everything you need from the data reader.

like image 115
StriplingWarrior Avatar answered Oct 17 '22 17:10

StriplingWarrior


It's because the "yield return" will return one element and continue the iteration, while the "normal" return will finish the invocation.

like image 41
Wilmer Avatar answered Oct 17 '22 16:10

Wilmer