20

This is a known pitfall for people who are getting their feet wet using LINQ:

public class Program
{
    public static void Main()
    {
        IEnumerable<Record> originalCollection = GenerateRecords(new[] {"Jesse"});
        var newCollection = new List<Record>(originalCollection);

        Console.WriteLine(ContainTheSameSingleObject(originalCollection, newCollection));
    }

    private static IEnumerable<Record> GenerateRecords(string[] listOfNames)
    {
        return listOfNames.Select(x => new Record(Guid.NewGuid(), x));
    }

    private static bool ContainTheSameSingleObject(IEnumerable<Record>
            originalCollection, List<Record> newCollection)
    {
        return originalCollection.Count() == 1 && newCollection.Count() == 1 &&
                originalCollection.Single().Id == newCollection.Single().Id;
    }

    private class Record
    {
        public Guid Id { get; }
        public string SomeValue { get; }

        public Record(Guid id, string someValue)
        {
            Id = id;
            SomeValue = someValue;
        }
    }
}

This will print "False", because for each name supplied to create the original collection, the select function keeps getting reevaluated, and the resulting Record object is created anew. To fix this, a simple call to ToList could be added at the end of GenerateRecords.

What advantage did Microsoft hope to gain by implementing it this way?

Why wouldn't the implementation simply cache the results an internal array? One specific part of what's happening may be deferred execution, but that could still be implemented without this behavior.

Once a given member of a collection returned by LINQ has been evaluated, what advantage is provided by not keeping an internal reference/copy, but instead recalculating the same result, as a default behavior?

In situations where there is a particular need in the logic for the same member of a collection recalculated over and over, it seems like that could be specified through an optional parameter and that the default behavior could do otherwise. In addition, the speed advantage that is gained by deferred execution is ultimately cut back against by the time it takes to continually recalculate the same results. Finally this is confusing block for those that are new to LINQ, and it could lead to subtle bugs in ultimately anyone's program.

What advantage is there to this, and why did Microsoft make this seemingly very deliberate decision?

  • 1
    Just call ToList() in your GenerateRecords() method. return listOfNames.Select(x => new Record(Guid.NewGuid(), x)).ToList(); That gives you your "cached copy." Problem solved. – Robert Harvey Mar 23 '18 at 15:52
  • 1
    I know, but I was wondering why they would've made this necessary in the first place. – Panzercrisis Mar 23 '18 at 15:56
  • 11
    Because lazy evaluation has significant benefits, not the least of which is "oh, by the way, this record changed since the last time you requested it; here's the new version", which is precisely what your code example illustrates. – Robert Harvey Mar 23 '18 at 15:57
  • I could swear I had read an almost identically phrased question here in the last 6 months, but I'm not finding it now. The closest I can find was from 2016 on stackoverflow: stackoverflow.com/q/37437893/391656 – Mr.Mindor Mar 23 '18 at 20:51
  • 29
    We have a name for a cache without an expiration policy: "memory leak". We have a name for a cache without an invalidation policy: "bug farm". If you're not going to propose an always-correct expiration and invalidation policy that works for every possible LINQ query then your question kinda answers itself. – Eric Lippert Mar 23 '18 at 23:24
52

What advantage was gained by implementing LINQ in a way that does not cache the results?

Caching the results would simply not work for everybody. As long as you have tiny amounts of data, great. Good for you. But what if your data is larger than your RAM?

It has nothing to do with LINQ, but with the IEnumerable<T> interface in general.

It is the difference between File.ReadAllLines and File.ReadLines. One will read the whole file into RAM, and the other will give it to you line by line, so you can work with large files (as long as they have line-breaks).

You can easily cache everything you want to cache by materializing your sequence calling either .ToList() or .ToArray() on it. But those of us who do not want to cache it, we have a chance to not do so.

And on a related note: how do you cache the following?

IEnumerable<int> AllTheZeroes()
{
    while(true) yield return 0;
}

You cannot. That's why IEnumerable<T> exists as it does.

  • 2
    Your last example would be more compelling if it were an actual infinite series (such as Fibonnaci), and not merely an endless string of zeroes, which is not particularly interesting. – Robert Harvey Mar 23 '18 at 15:55
  • 23
    @RobertHarvey That's true, I just thought it's easier to spot that it's an endless stream of zeroes when there is no logic at all to understand. – nvoigt Mar 23 '18 at 16:13
  • 2
    int i=1; while(true) { i++; yield fib(i); } – Robert Harvey Mar 23 '18 at 16:17
  • 2
    The example I was thinking of was Enumerable.Range(1,int.MaxValue) - its very easy to work out a lower bound for how much memory that is going to use. – Chris Mar 23 '18 at 21:23
  • 4
    The other thing I've seen along the lines of while (true) return ... was while (true) return _random.Next(); to generate an infinite stream of random numbers. – Chris Mar 23 '18 at 21:29
24

What advantage did Microsoft hope to gain by implementing it this way?

Correctness? I mean, the core enumerable can change in between calls. Caching it would produce incorrect results and open the entire “when/how do I invalidate that cache?” Can of worms.

And if you consider LINQ was originally designed as a means to do LINQ to data sources (like entity framework, or SQL directly), the enumerable was going to change since that’s what databases do.

On top of that, there is Single Responsibility Principle concerns. It is far easier to make some query code that works and build caching on top of it than to build code that queries and caches but then remove the caching.

  • 3
    It may be worth mentioning that ICollection exists, and probably behaves the way OP is expecting IEnumerable to behave – Caleth Mar 23 '18 at 14:57
  • If you are using IEnumerable<T> to read an open database cursor, your results shouldn't change if you are using a database with ACID transactions. – Doug Mar 24 '18 at 15:56
4

Because LINQ is, and was intended from the beginning to be, a generic implementation of the Monad pattern popular in functional programming languages, and a Monad is not constrained to always yield the same values given the same sequence of calls (in fact, its use in functional programming is popular precisely because of this property, which allows for escaping the deterministic behaviour of pure functions).

4

Another reason that hasn't been mentioned is, the possibility of concatenating different filters and transformations without creating garbage middle results.

Take this for example:

cars.Where(c => c.Year > 2010)
.Select(c => new { c.Model, c.Year, c.Color })
.GroupBy(c => c.Year);

If the LINQ methods computed the results immediately, we would have 3 collections:

  • Where result
  • Select result
  • GroupBy result

Of which we only care about the last one. There's no point in saving the middle results because we don't have access to them, and we only want to know about the cars already filtered and grouped by year.

If there was a need to save any of these results, the solution is simple: break the calls apart and call .ToList() on them and save them in a variable.


Just as a side note, in JavaScript, the Array methods actually return the results immediately, which can lead to more memory consumption if one isn't careful.

3

Fundamentally, this code — putting a Guid.NewGuid () inside a Select statement — is highly suspicious. This is surely a code smell of some kind!

In theory, we would not necessarily expect a Select statement to create new data but to retrieve existing data. While it is reasonable for Select to join data from multiple sources to produce joined content of different shape or even compute additional columns, we might still expect it to be functional & pure. Putting the NewGuid () inside makes it non-functional & non-pure.

The creation of the data could be teased apart from the select and put into a create operation of some sort, so that the select can be remain pure and re-useable, or else the select should be done only once and wrapped/protected — this is the .ToList () suggestion.

However, to be clear, the issue seems to me the mixing of creation inside selection rather than lack of caching. Putting the NewGuid() inside the select appears to me to be an inappropriate mixing of programming models.

0

Deferred execution allows those writing LINQ code (to be precise, using IEnumerable<T>) to explicitly choose whether the result is immediately computed and stored in memory, or not. In other words, it allows programmers to choose the calculation time versus storage space tradeoff that is most appropriate to their application.

It could be argued that the majority of applications want the results immediately, so that should have been the default behaviour of LINQ. But there are numerous other APIs (e.g. List<T>.ConvertAll) that offer this behaviour and have done since the Framework was created, whereas until LINQ was introduced, there was no way to have deferred execution. Which, as other answers have demonstrated, is a prerequisite for enabling certain types of computations that would otherwise by impossible (by exhausting all available storage) when using immediate execution.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.