Should a GetEnumerator method still be idempotent when the class does not implement IEnumerable
Asked Answered
E

4

2

This question piggy backs of another question which I raised regarding abusing the IEnumerable interface by modifying an object as you iterate over it.

The general consensus is that no anything that Implements IEnumerable should be idempotent. But .net supports compile time duck typing with the foreach statement. Any object that provides an IEnumerator GetEnumerator() method can be used inside a foreach statement.

So should the GetEnumerator method be idempotent or is it when it implements IEnumerable?

EDIT (Added context)

To put some context round this what I am suggesting is that when iterating over a queue each item is dequeued as it goes. Additionally any new objects pushed onto the queue after the call to GetEnumerator would still be iterated over.

Elf answered 16/11, 2010 at 13:58 Comment(1)
I wouldn't add GetEnumerator to the Queue itself, but instead define a function IEnumerable<T> DequeueOnEnumeration() which can be used like foreach(T elem in queue.DequeueOnEnumeration()). That way the semantics are much clearer.Watkin
R
4

It's not the type which is idempotent - that doesn't even make much sense; you may mean immutable, but that's not clear. It's the GetEnumerator method itself which is typically idempotent.

While I'd say that's typically the case, I can envisage special cases where it makes sense to have a non-idempotent GetEnumerator method. For example, it could be that you've got data which can only be read once (because it's streaming from a web server which won't service the same request again, or something like that). In that case, GetEnumerator would have to effectively invalidate the data source, so that future calls would throw an exception.

Such types and methods should be documented very carefully, of course, but I think they're reasonable.

Relativistic answered 16/11, 2010 at 14:6 Comment(2)
Thought you might be the one to answer, I saw your response to a similar post and thought you might have an opinion which was not a hard and fast rule. Not sure if you read the other question which was about a blocking queue, I feel this is similar to your stream example. Strictly speaking you should not be able to view the contents of a queue and using the foreach was "a way" of popping all of the items out. I have clarified what I mean when I said idempotent, thanks.Elf
Could you clarify whether you think that implementing IEnumerable is relevant or not?Elf
C
3

This discussion is an old one and to my knowledge there's no common consensus.

Please do not confuse the concept of (runtime) Duck-Typing with abusing the compiler supported foreach to support your desired semantics.

Another concept you seem to confuse is Idempotence vs. Immutability. According to your wording you try to describe the second, which means the object providing the enumerator gets modified during enumeration. Idempotence on the other hand means your enumerator, when called twice will yield the same results.

Now that we're clear on this, you need to carefully decide on the semantics your IEnumerable operation should support. Certain kind of enumerations are hard to make idempotent (i.e. involve caching), and do usually fall into one of the following categories:

  • Enumerating over randomly changing data (i.e. a random number generator, sensor streams)
  • Enumerating over shared state (e.g. files, databases, streams etc.)

On the other hand, this only accounts for "source" operations. If you are implementing filter or transformation operations using enumerators, you should always try to make them idempotent.

Chun answered 16/11, 2010 at 14:15 Comment(2)
I was trying to remove my scenario from the question to keep the question open. What I was suggesting in my previous question was when iterating over a blocking queue each item was dequeued as it went. Additionally any new objects pushed onto the queue after the call to GetEnumerator would still be iterated over.Elf
Could you clarify whether you think that implementing IEnumerable is relevant or not?Elf
C
0

It seems you want a queue class from which you can dequeue all items in a nice one-liner.

There's nothing wrong with this idea per se; I'd just question your preference to specifically use GetEnumerator to achieve what you're after.

Why not simply write a method that is more explicit in terms of what it does? For example, DequeueAll, or something of the sort.

Example:

// Just a simplistic example. Not the way I'd actually write it.
class CustomQueue<T> : Queue<T>
{
    public IEnumerable<T> DequeueAll()
    {
        while (Count > 0)
        {
            yield return Dequeue();
        }
    }
}

(Note that the above could even be an extension method, if it represents literally the only functionality you'd want above and beyond what is already provided by Queue<T>.)

This way you could still get the "clean"-looking code I suspect you're after, without the (potential) confusion of a non-idempotent GetEnumerator:

// Pretty clean, right?
foreach (T item in queue.DequeueAll())
{
    Console.WriteLine(item);
}
Companionate answered 16/11, 2010 at 14:56 Comment(2)
That is pretty much what I came to. In fact someone pointed out that .net 4 introduced a IProducerConsumerCollection interface and BlockingCollection implementation with a GetConsumingEnumerable method. This question was more about whether it was bad practice because the class has a GetEnumerator method, because the class implements IEnumerable or neither.Elf
BTW I like the idea of the extension method.Elf
P
0

I would suggest that using ForEach on a collection shouldn't change it unless the name of the collection type implies that's going to happen. The issue in my mind would be what should be returned if a method is performed to consume a collection into something enumerable (e.g. to allow "For Each Foo in MyThing.DequeueAsEnum"). If DequeueAsEnum returns an iEnumerable, then someone could expect to get away with "Dim myIEnumerable As IEnumerable = MyThing.DequeueAsEnum" and then use MyIEnumerable in two disjoint For-Each loops. If DequeueAsEnum returns a type EnumerableOnlyOnce, then it would be a little clearer that its return should only be enumerated once. To be sure, the existence of implicit typing in newer C# and VB.Net dialects makes it a bit more likely that someone might assign the function return to a variable when they shouldn't, but I don't know how to prevent that.

BTW, there are a number of circumstances where it would be helpful to prevent a class reference from being stored into a variable; is there any way to declare a class in such a way that outside code can use expressions of that class type, but cannot declare variables of it?

Pneumo answered 27/11, 2010 at 18:23 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.