Home > Back-end >  C# Linq - How to understand this lazy evaluation behavior of Select?
C# Linq - How to understand this lazy evaluation behavior of Select?

Time:09-17

I'm a newbee for C# and encountereed an interesting Linq behavior as below.

FlipNumber has the expected behavior, select by index Select((_, i) ...) shown the correct behavior to flip the number string n.Length - 1 times, while FlipNumber1 only evaluated once as it select by item and with Last() to obtain the last iteration result.

Can anyone help to explain it?

Simple test with the following code (forget the efficiency plz)

using System;
using System.Linq;

public class Test
{
    public static string FlipNumber(string n)
    {
        Console.WriteLine($"FlipNumber");
        return Enumerable.Range(0, n.Length - 1).Select((_, i) => {
            n = new string(n.Take(i).Concat(n.Skip(i).Reverse()).ToArray());
            Console.WriteLine($"{i} : {n}");
            return n;
        }).Last();
    }
    
    public static string FlipNumber1(string n)
    {
        Console.WriteLine($"FlipNumber1");
        return Enumerable.Range(0, n.Length - 1).Select(i => {
            n = new string(n.Take(i).Concat(n.Skip(i).Reverse()).ToArray());
            Console.WriteLine($"{i} : {n}");
            return n;
        }).Last();
    }

    // suggested by @DiplomacyNotWar
    // add `ToList()` before `Last()` makes the result same with FlipNumber
    public static string FlipNumber11(string n)
    {
        Console.WriteLine($"FlipNumber11");
        return Enumerable.Range(0, n.Length - 1).Select(i => {
            n = new string(n.Take(i).Concat(n.Skip(i).Reverse()).ToArray());
            Console.WriteLine($"{i} : {n}");
            return n;
        }).ToList().Last();
    }
    
    public static string FlipNumber2(string n)
    {
        Console.WriteLine($"FlipNumber2");
        return n.Select(i => {
            n = new string(n.Take(i).Concat(n.Skip(i).Reverse()).ToArray());
            Console.WriteLine($"{i} : {n}");
            return n;
        }).Last();
    }
  
    public static void Main(string[] args)
    {
        FlipNumber("123456789");
        FlipNumber1("123456789");
        FlipNumber11("123456789");
        FlipNumber2("123456789");
    }
}

/*
Results:

FlipNumber
0 : 987654321
1 : 912345678
2 : 918765432
3 : 918234567
4 : 918276543
5 : 918273456
6 : 918273654
7 : 918273645
FlipNumber1
7 : 123456798
FlipNumber11
0 : 987654321
1 : 912345678
2 : 918765432
3 : 918234567
4 : 918276543
5 : 918273456
6 : 918273654
7 : 918273645
FlipNumber2
1 : 123456789
2 : 123456789
3 : 123456789
4 : 123456789
5 : 123456789
6 : 123456789
7 : 123456789
8 : 123456789
9 : 123456789
*/

CodePudding user response:

This boils down to a number of optimisations that are in LINQ. When we examine the source for Last we see it is this:

public static TSource Last<TSource>(this IEnumerable<TSource> source)
{
    bool found;
    TSource result = source.TryGetLast(out found);
    if (!found)
    {
        ThrowHelper.ThrowNoElementsException();
    }
    return result;
}

We follow that to TryGetLast and see it is this:

private static TSource TryGetLast<TSource>(this IEnumerable<TSource> source, out bool found)
{
    if (source == null)
    {
        ThrowHelper.ThrowArgumentNullException(ExceptionArgument.source);
    }
    IPartition<TSource> partition = source as IPartition<TSource>;
    if (partition != null)
    {
        return partition.TryGetLast(out found);
    }
    IList<TSource> list = source as IList<TSource>;
    if (list != null)
    {
        int count = list.Count;
        if (count > 0)
        {
            found = true;
            return list[count - 1];
        }
    }
    else
    {
        using IEnumerator<TSource> enumerator = source.GetEnumerator();
        if (enumerator.MoveNext())
        {
            TSource current;
            do
            {
                current = enumerator.Current;
            }
            while (enumerator.MoveNext());
            found = true;
            return current;
        }
    }
    found = false;
    return default(TSource);
}

And then finally the IPartition<TSource>:

internal interface IPartition<TElement> : IIListProvider<TElement>, IEnumerable<TElement>, IEnumerable
{
    IPartition<TElement> Skip(int count);

    IPartition<TElement> Take(int count);

    TElement TryGetElementAt(int index, out bool found);

    TElement TryGetFirst(out bool found);

    TElement TryGetLast(out bool found);
}

The Select(i => returns a SelectIPartitionIterator under the hood, which implements IPartition<TElement> so it is able to jump to the last element.

When you do Select((_, i) => it's not creating a IPartition<TSource> so it is forced to do a full iteration.

The whole use of n = in this question was just a furphy and should have been avoided. It turns an excellent question into one that hides what's truly going on.

  • Related