4

I noticed something interesting today when I was making changes for a pull request. Below is my code:

    public List<MatColor> Colors { get; set; }

    public List<Customer> Customers { get; set; }

    public ProductViewModel()
    {
        this.Colors = new List<MatColor>();
        this.Customers = new List<Customer>();

        var products = this.LoadProductViewList();

        this.LoadColorComboBox(products);
        this.LoadCustomerComboBox(products);
    }

    public void LoadColorComboBox(IEnumerable<Product> products)
    {
        this.Colors.Clear();

        this.Colors = products.Select(p => new MatColor()
        { Code = p.ColorCode, Description = p.ColorDescription })
                                            .DistinctBy(p => m.Code).DistinctBy(p => p.Description).ToList();
    }

    private void LoadCustomerComboBox(IEnumerable<Product> products)
    {
        this.Customers.Clear();

        this.Customers = products.Select(p => new Customer()
        { Name = p.CustomerName, Number = p.CustomerNumber })
                                              .DistinctBy(p => p.Number).DistinctBy(p => p.Name).ToList();
    }

This code does everything I want it to. It successfully populates both the Colors and Customers lists. I understand why it would always successfully populate the Colors list. That's because LoadColorComboBox(...) gets called first.

But an IEnumerable<T> can only get enumerated, ONCE, right? So once it gets enumerated in LoadColorComboBox(...), how is it successfully getting reset and thus enumerated again in LoadCustomerComboBox(...)? I've already checked the underlying type being returned by LoadProductViewList() -- it calls a REST service which returns a Task<IEnumerable<Product>>. Is my IEnumerable<Product> somehow getting passed as a value? It's not a primitive so I was under the impression it's a reference type, thus, would get passed in by reference as default, which would cause the second method to blow up. Can someone please tell me what I'm not seeing here?

KSwift87
  • 1,843
  • 5
  • 23
  • 40
  • 4
    `IEnumerable` can be enumerated as many times as you want. I'm not sure where you're getting the idea that it can't be. – BJ Myers Apr 04 '17 at 21:32
  • Why can an IEnumerable only get enumerated once. It is just one of the most simplified interfaces for enumerable collections. List also implements IEnumerable and also can be enumerated unlimited times. You might mix-up enumeration with deferred execution. That mostly happens once. – Björn Boxstart Apr 04 '17 at 21:33
  • @BjörnBoxstart What do you mean by the statement that deferred execution happens just once? Deferred execution just means the computation isn't performed until it's needed; if you iterate a sequence that is deferring execution multiple times, it'll perform those computations multiple times. – Servy Apr 04 '17 at 21:38
  • @BJ Myers -- I'm studying the answers since I don't understand all the finer workings of IEnumerable vs other types of collections (like IList). Anyway despite regretting asking this question, I'm not sure where I got the idea that an IEnumerable could only be enumerated once without some type of position-reset. – KSwift87 Apr 04 '17 at 21:46
  • And the hate on my question continues, but here's an idea of where I got the "you can only enumerate an IEnumerable once" so people understand where my thinking originated: http://stackoverflow.com/questions/8240844/handling-warning-for-possible-multiple-enumeration-of-ienumerable – KSwift87 Apr 05 '17 at 14:42

2 Answers2

11

And IEnumerable is an object with a GetEnumerator method, that is able to get you an enumerator. Generally one would expect it to be able to give you any number of enumerators. Some specific implementations might not support it, but the contract of the interface is that it should be able to give you as many as you want.

As for the IEnumerator instances that it spits out, they do technically have a Reset method, which is supposed to set it back to the "start" of the sequence. In practice however, most implementations tend to not support the "reset" operator and just throw an exception when you call the method.

In your case, you're not actually reseting an IEnumerator and trying to use it to get the values of a sequence twice (which wouldn't work, as none of the iterators from the LINQ methods that you're using to create your sequence support being reset). What you're doing is simply getting multiple different enumerators, which those LINQ methods all support.

Servy
  • 202,030
  • 26
  • 332
  • 449
  • Thank you for spelling this out for me. – KSwift87 Apr 04 '17 at 21:52
  • ...And especially for pointing out the Reset method. I swear I had received errors before (a not-insignificant amount during QuickWatch) where the message stated that the Enumerable (or perhaps it was "Enumerator"?) had already been enumerated -- hence where I got the impression an IEnumerable could only be enumerated once. – KSwift87 Apr 04 '17 at 21:58
2

But an IEnumerable can only get enumerated, ONCE, right?

No. An IEnumerable<T> can be enumerated any number of times. But for each enumeration, each item can be yielded only once.

In your case, products will be enumerated once for each LINQ query that needs the list of products... so once in LoadColorComboBox and once in LoadCustomerComboBox.

John Wu
  • 50,556
  • 8
  • 44
  • 80
  • You can create an `IEnumerator` that yields an item multiple time. It's as easy as `new []{ "hi", "hi"}`. Iterate that and you get the same value twice. – Servy Apr 04 '17 at 21:35
  • Two items with the same value =/= the same item twice. Although "hi" as a string constant will be interned, there are two distinct memory locations that point at the same string, and the enumerator will return each one exactly once. – John Wu Apr 04 '17 at 21:40
  • IEnumerator has no concept of an "item". It has values that it yields. That's it. It can yield values that are distinct, or unique, or whatever it wants. You can write an enumerator to yield anything. – Servy Apr 04 '17 at 21:53
  • Sure. It's your code. You could also write an implementation that crashes the O/S. I would suggest, however, that implementations of `IEnumerable` that crash the O/S or yield the same item twice both have an implementation defect. Thankfully, your example uses a CLR type which does not have either issue, so I stand by my answer. – John Wu Apr 04 '17 at 22:27
  • Again, the very **idea** of an "item" doesn't exist when dealing with an `IEnumerator`. The statement itself has no meaning. Sure, if you have an `IEnumerator` that's designed to iterate some collection that has some concept of items, then such an iterator is likely going to be designed to yield each item in the collection just once, but there are **lots** of enumerators that have *nothing* to do with collections, and as such have no concept of "an item". You're statement only applies to a *tiny* fraction of actual enumerators, yet you incorrectly state it as a generality for all of them. – Servy Apr 05 '17 at 13:16
  • "`IEnumerable interface`: Exposes the enumerator, which supports a simple iteration over a collection of a specified type." -[Link](https://msdn.microsoft.com/en-us/library/9eekhta0(v=vs.110).aspx). A collection of what? Perhaps a collection of items? – John Wu Apr 05 '17 at 14:35
  • If your definition of "item" is, "something returned by the sequence", and you're stating that no enumerator will ever return the same item twice, then my earlier example proves you wrong. It returns the same item twice, given your definition of "item". – Servy Apr 05 '17 at 14:37
  • Nah, your example returns two unique array elements that happen to have the same value. They could just as easily have different values; the enumerator is still exhibiting the same behavior in that case. A better example would be [Enum.Range](https://msdn.microsoft.com/en-us/library/system.linq.enumerable.range(v=vs.110).aspx) which returns a list of integers and has no backing store. But if `Enum.Range` returned the same value twice it would be a defect. – John Wu Apr 05 '17 at 20:04
  • My example *returns the same item twice* because that item is in the underlying collection twice. Your asserted that the same item can't be yielded twice. It was yielded twice. You're literally re-defining the exact same word to mean two *contradictory* things based on whatever happens to suit your mood at the time. If you mean "item" to mean the values yielded, my examples shows that they can be duplicated, if you mean position in a data store, an `IEnumerable` isn't necessarily based off of a collection or other data source at all, so that can't possibly be correct. – Servy Apr 05 '17 at 20:09
  • No, your example returns the same value twice because two items in the array have the same value. This is a stupid argument; we both know how enumerators work, so I am going to move on now. – John Wu Apr 05 '17 at 20:16
  • Yes, my example returns the same value twice because the value is in the array twice. Your answer says that the enumerator can't return the same value twice. If you realize that an enumerator can yield the same value twice then *don't write an answer that says that it can't yield a value twice*. If you know how enumerators work, then why write an answer that makes verifiably false statements about how enumerators work? – Servy Apr 05 '17 at 20:18
  • I'm happy with my post the way it is, because nowhere does it contain the word "value." Please read more carefully next time. Take care. – John Wu Apr 05 '17 at 20:21