-3

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
*/
Harold
  • 170
  • 7
  • What is the goal of this code? – ProgrammingLlama Sep 16 '22 at 03:39
  • Does it matter? Im just curious about the different behaviors for the codes i tried. Anyway, this code aims to flip number string for fun. Thanks. – Harold Sep 16 '22 at 03:41
  • I believe @DiplomacyNotWar was asking "what did you expect to see?". Is one of the 2 functions behaving as you expect? Maybe it's me and maybe it's because im tryind to decipherit on a phone, but I find your code very hard to follow. You are changing `n` while your `Enumerable.Range(0 n.Length)` is executing. I have no idea what happens then – Flydog57 Sep 16 '22 at 03:56
  • @Flydog57 Thanks. I edited the description and hope it is clearer now. I just wonder why `Select` only executed once in `FlipNumber1`. – Harold Sep 16 '22 at 04:04
  • 1
    I learned some time back that reassigning an argument can lead to some really non-obvious behavior. Here the argument to the function is `n`, and then you're reassigning a value to it. I suspect that if you change `n = new string...` to `s = new string...`, and of course return `s`, that you'll see the behavior you expect. See https://stackoverflow.com/questions/31993443/why-does-this-method-result-in-an-infinite-loop for what I think is a similar problem. – Jim Mischel Sep 16 '22 at 04:10
  • @JimMischel Thanks. Yes, but i think in C#, `n` is just a reference, assigned it a `new string` only changed what it refers to but not itself. That's why i tried `FlipNumber2`. What really interests me is the behavior of `Select` in `FlipNumber1`. And I do think it has some relationship with the `defer` evaluation of the Linq. Thanks. – Harold Sep 16 '22 at 04:16
  • I'm not following. `Select` isn't a normal function; it's an iterator block (i.e., it has a `yield return` statement). In my head, it executes just once (even though it is entered many times) – Flydog57 Sep 16 '22 at 04:18
  • Yes, `n` is a variable of reference type, and it initially refers to an immutable string. But you reassign it. I'm pretty sure that `Enumerable.Range` is also an interator block (with a yield return). I have no idea what happens when you reassign `n` whe it's the variable that is used to terminate the iteration – Flydog57 Sep 16 '22 at 04:18
  • @Flydog57 Yes, `Select` is used to iterated the former `Enumerable.Range()`, but compared with `Select` in `FlipNumber`, i only changed to iterate the item instead of index, then its havior changed. That's why i feel it stopped eariler than expected. Thank. – Harold Sep 16 '22 at 04:22
  • I've tried to look in to the generated `IL` and `JITasm` code, but i did not see any significant difference between `FlipNumber` and `FlipNumber1` :< . – Harold Sep 16 '22 at 04:32
  • It matters because how can we understand what it's doing wrong if you don't tell us what it's supposed to do? – ProgrammingLlama Sep 16 '22 at 04:33
  • @DiplomacyNotWar Hi buddy, take easy... I may make a mistake in asking manner, sorry for that. – Harold Sep 16 '22 at 04:35
  • @DiplomacyNotWar Yes, and i've revised the desciption and try to make my point clearly. Just inform me if it still not clear enough to you. Thanks. – Harold Sep 16 '22 at 05:11
  • It doesn't seem to be related to reassigning a value to `n`, but rather using `.Last()`. If you change it to `.ToList().Last()` then you get the same result as `FlipNumber`. It seems like some optimization is occuring here. – ProgrammingLlama Sep 16 '22 at 05:26
  • @DiplomacyNotWar Yes and i think it's about the defer evaluation behavior. I wonder whether there's a way to see its real execution pattern. Thanks. – Harold Sep 16 '22 at 05:37
  • I think all bets are off when you have `n.Select(...` and `n = ...`. There may be some incredibly cool algorithm going on here or it's just a mind-bending frig around. My suggestion is that the OP remove the `n =...` and ask a simple question about enumerable laziness. – Enigmativity Sep 16 '22 at 07:56
  • 1
    With Linq commands (which have loads of overloads), it is always hard to work out what code you are actually running. If you look at the type of object that `.Last()` is operating on, you will see that FlipNumber1 is the only one where the type is `System.Linq.Enumerable+SelectRangeIterator``1[System.String]` and looking in ILSpy at the code, it appears Last will call TryGetLast which in this case will go straight to the last result. I presume the others are using the `System.Linq.Enumerable.Last` which in these cases goes through all the results until it reaches the end. – sgmoore Sep 16 '22 at 08:48
  • There is only supposed to be one back-tick in that answer above, but if I just use one, it removes it and terminates the formatting, so I have added a second. – sgmoore Sep 16 '22 at 08:52
  • Of course, the question is now why does the Enumerable.Range in FlipNumber1 produce a different type from the other methods? – sgmoore Sep 16 '22 at 08:56
  • @sgmoore Yes, i noticed the diff in IL code and wonder why and when this happens. Im a newbee for C# and would like to know how to investigate those internal behavior of its Linq facilities. Thanks for your reply. – Harold Sep 17 '22 at 04:17
  • @Enigmativity Thanks. I just encountered this issue and wanna find its internal mechanism, that's why i asked this question here. It may or may not get dedicated answer and helpful discussion is enough. – Harold Sep 17 '22 at 04:21
  • @Harold - It's answered. – Enigmativity Sep 17 '22 at 05:25

1 Answers1

0

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.

Enigmativity
  • 113,464
  • 11
  • 89
  • 172