Enumerable.Empty<T>
returns an EmptyPartition<T>
:
https://github.com/dotnet/runtime/blob/57bfe474518ab5b7cfe6bf7424a79ce3af9d6657/src/libraries/System.Linq/src/System/Linq/Enumerable.SpeedOpt.cs#L10
This is just a special-case empty "no-op" class that implements an Iterator<T>
but is implemented to not move to the first element when iterated:
https://github.com/dotnet/runtime/blob/57bfe474518ab5b7cfe6bf7424a79ce3af9d6657/src/libraries/System.Linq/src/System/Linq/Partition.SpeedOpt.cs#L35
The first time you call .Append()
you get back an AppendPrepend1Iterator
saved back to you list
variable. The second time you get an AppendPrependN
:
https://github.com/dotnet/runtime/blob/57bfe474518ab5b7cfe6bf7424a79ce3af9d6657/src/libraries/System.Linq/src/System/Linq/AppendPrepend.cs#L18
https://github.com/dotnet/runtime/blob/57bfe474518ab5b7cfe6bf7424a79ce3af9d6657/src/libraries/System.Linq/src/System/Linq/AppendPrepend.cs#L141
Both of these types represent iterators, which hold a reference to the source data structure (EmptyPartition<T>
in the first instance, or one of these iterator types on subsequent instances) and also the new element you're adding.
After all new items are appended you'll have a nested sequence of iterators, where you can think of each iterator as "the operation of appending an single item to a source sequence", which when iterated through (as string.Join
will be doing internally) will step through the elements of the last iterator (the AppendPrependN
in this case), which will invoke each of the nested iterators in turn, resulting in all items across all nested iterators being printed.
Apologies if this is a confusing explanation - there's a lot of engineering that goes into Linq to make it lightweight in terms of memory & speed (while also keeping operations lazy), so the implementation is quite abstract.