dotnet / csharplang

The official repo for the design of the C# programming language
11.37k stars 1.02k forks source link

[Proposal] Allow foreach for IEnumerator/IAsyncEnumerator #3867

Closed Kir-Antipov closed 4 years ago

Kir-Antipov commented 4 years ago

For the sake of simplicity, I'll only talk about IEnumerator, but this issue also applies to the IEnumerator<T> and IAsyncEnumerator<T>

The problem

Let's imagine that we need to treat the first element of the enumeration differently than the rest. How can we do this today?

1. Condition inside the foreach-loop:

bool isFirst = true;
foreach (var x in enumeration)
{
    if (isFirst)
    {
        isFirst = false;
        DoSomeWork(x);
    }
    else
    {
        DoAnotherWork(x);
    }
}

Pros:

Cons:

2. Reevaluating an enumeration:

DoSomeWork(enumeration.First());
foreach (var x in enumeration.Skip(1))
    DoAnotherWork(x);

Pros:

Cons:

3. Extension method that wraps IEnumerator in new IEnumerable

public static IEnumerable<T> ToEnumerable<T>(this IEnumerator<T> enumerator)
{
    _ = enumerator ?? throw new ArgumentNullException(nameof(enumerator));

    while (enumerator.MoveNext())
        yield return enumerator.Current;
}

...

using IEnumerator<object> e = enumeration.GetEnumerator();
DoSomeWork(e.ToEnumerable().First());
foreach (var x in e.ToEnumerable())
   DoAnotherWork(x);

Pros:

Cons:

confused-jackie-chan

4. Hello darkness boilerplate, my old friend:

IEnumerator e = enumeration.GetEnumerator();
try
{
    if (!e.MoveNext())
        throw new InvalidOperationException()
    DoSomeWork(e.Current);
    while (e.MoveNext())
        DoAnotherWork(e.Current);
}
finally
{
    if (e is IDisposable disposable)
        disposable.Dispose();
}

Pros:

Cons:

Possible solution

foreach needs an IEnumerator to work, right? So why not let him work with it directly and without intermediaries? This would allow us to do things like this:

using IEnumerator<int> e = new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }.Cast<int>().GetEnumerator();

// .TakeNext - LINQ-like extension method, that returns new enumerator,
// which can be implemented as a structure and be completely allocation-free 
foreach (int x in e.TakeNext(1))
    Console.WriteLine("First block:  {0}", x);

foreach (int x in e.TakeNext(2))
    Console.WriteLine("Second block: {0}", x);

foreach (int x in e.TakeNext(3))
    Console.WriteLine("Third block:  {0}", x);

foreach (int x in e)
    Console.WriteLine("Last block:   {0}", x);

// First block:  1
// Second block: 2
// Second block: 3
// Third block:  4
// Third block:  5
// Third block:  6
// Last block:   7
// Last block:   8
// Last block:   9
// Last block:   10
Kir-Antipov commented 4 years ago

These days, these same applications have their own set of DLLs, not relying on this shared space. It involved a hit on installation sizes, on users' hard drives, but I would never want to go back to how it was.

@spydacarnage, why not to take goods from the both worlds? Cause "hit on installation sizes, on users' hard drives" is not something insignificant, we should care about users too.

CyrusNajmabadi commented 4 years ago

, we should care about users too.

In one area you have dll hell, and it's enormously painful and almost impossible for either users, developers, or platforms to effectively solve. In hte other, you have isolation, and extra space used. It works well, and only comes at hte cost of HD space, a quantity that is both cheap and continuously dropping in cost.

Caring about users leads us to adapting to this and designing accordingly. If HD space is cheap, plentiful, and continuing trends in that direction, then it's a better state to optimize for that, and help users avoid those enormously painful situations that we used to have before.

CyrusNajmabadi commented 4 years ago

If everyone ships there apps with the same libraries, users will spent in

The issue is that apps do not ship the same libraries. All apps use different versions of those libraries. You cannot share because incompatibilities in libs will cause breaks in the different apps.

Cause "hit on installation sizes, on users' hard drives" is not something insignificant,

The hit there is vastly less significant or impactful than the hit caused by trying to share. With sharing we used to literally have apps simply stop functioning because a user upgraded another app.

All modern platforms have moved to an approach where apps are extremely isolated. This helps on a multitude of different dimensions (for example: much easier to install/uninstall; one app doesn't impact another; etc. etc.) at the cost of something which has been extremely commoditization.

If HD space were exceptionally expensive and every bit of usage was at a premium, we would see teh economics play out differently, and our development systems would be designed accordingly.

Kir-Antipov commented 4 years ago

@CyrusNajmabadi, well that makes sense :)

svick commented 4 years ago

@Kir-Antipov

Imagine that everyone ships their apps with the whole .NET

That's completely normal with .Net Core, it's called self-contained deployment.

Kir-Antipov commented 4 years ago

@svick, yep, I know (btw, read the first paragraph of the "Disadvantages" section), but I also know nobody who uses this for anything except loaded ASP.NET Core applications

jnm2 commented 4 years ago

@Kir-Antipov Not true, this is great for desktop apps too.

Kir-Antipov commented 4 years ago

@jnm2, what's "not true"? That I don't know anybody who uses it? Sorry, didn't know you were me.

Guys, I've already said, that this whole thing makes sense to me. It doesn't mean that I agree with this (and I have reasons not to, but they go a little outside the scope of development-only environment, so I'm not trying to give them as arguments), this means that I do not reject it, so there's no need to convince me ¯\(ツ)

jnm2 commented 4 years ago

Misread as "I know nobody uses this for anything" etc, I'm sorry.