Search code examples
c#linqdeferred-execution

LINQ deferred execution with a function's result as source (e.g. Console.ReadLine)


A function's result is the source for a LINQ query. I want it to be evaluated lazily, every time I use the query, not be locked when I create it. This is an example of what I mean:

var query = from c in Console.ReadLine()
            group c by char.IsDigit(c) into gr
            select new { IsDigit = gr.Key, Count = gr.Count() };

Console.WriteLine() runs only once - when query is created, even without calling a terminating method on it like ToList(). What I would want is for Console.WriteLine() (or any other function in its place) to be executed only when I use the query with ToList() or Count() etc.


Solution

  • If you don't mind a bit of extra infrastructure, it's not too bad - you can create a DeferredEnumerable<T> class that just executes the given delegate every time it's asked for an iterator. A static non-generic class can then help with type inference. Complete example:

    using System;
    using System.Collections;
    using System.Collections.Generic;
    using System.Linq;
    
    // Just for type inference...
    public static class DeferredEnumerable
    {
        public static IEnumerable<T> For<T>(Func<IEnumerable<T>> func) =>
            new DeferredEnumerable<T>(func);
    }
    
    public sealed class DeferredEnumerable<T> : IEnumerable<T>
    {
        private readonly Func<IEnumerable<T>> func;
    
        public DeferredEnumerable(Func<IEnumerable<T>> func)
        {
            this.func = func;
        }
    
        public IEnumerator<T> GetEnumerator() => func().GetEnumerator();
    
        IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
    }
    
    class Test
    {
        static void Main()
        {
            var query = 
                from c in DeferredEnumerable.For(Console.ReadLine)
                group c by char.IsDigit(c) into gr
                select new { IsDigit = gr.Key, Count = gr.Count() };
    
    
            Console.WriteLine("First go round");
            Console.WriteLine(string.Join(Environment.NewLine, query));
    
            Console.WriteLine("Second go round");
            Console.WriteLine(string.Join(Environment.NewLine, query));
        }
    }