IEnumerable vs IEnumerable<T> inconsistency

  • Thread starter Wiktor Zychla [C# MVP]
  • Start date
W

Wiktor Zychla [C# MVP]

The IEnumerable's documentation says:

"After an enumerator is created or after the Reset method is called, the
MoveNext method must be called to advance the enumerator to the first
element of the collection before reading the value of the Current property;
otherwise, Current is undefined.

Current also throws an exception if the last call to MoveNext returned
false, which indicates the end of the collection."

Note the "throws an exception" clause.

In case of the IEnumeable<T> this behavior is softened:

"Current is undefined under any of the following conditions:

The enumerator is positioned before the first element in the collection,
immediately after the enumerator is created. MoveNext must be called to
advance the enumerator to the first element of the collection before reading
the value of Current.

The last call to MoveNext returned false, which indicates the end of the
collection.

The enumerator is invalidated due to changes made in the collection, such as
adding, modifying, or deleting elements."

How does it work in real-life examples? What does it mean "is undefined"?
Let's see:

ArrayList a = new ArrayList();
a.Add( "ala ma kota" );
IEnumerator ea = a.GetEnumerator();
Console.WriteLine( ea.Current.ToString() ); // *
while ( ea.MoveNext() )
{
Console.WriteLine( ea.Current.ToString() );
}
Console.WriteLine( ea.Current.ToString() ); // **

Accessing the Current property BEFORE the enumeration starts (*) or just
after it finishes (**) ALWAYS throws an exception with detailed description
of the issue.
However, the generic interface behaves completely different:

List<string> ls = new List<string> { "ala ma kota", "qwe", "rrq" };
IEnumerator<string> es = ls.GetEnumerator();

Console.WriteLine( es.Current ); //*
while ( es.MoveNext() )
{
Console.WriteLine( es.Current );
}
Console.WriteLine( es.Current ); // **

This time there's NO EXCEPTION (the docs does not mention any exceptions,
that's why I said "softened") and instead the default(T) is returned.
In case of reference types, the default(T) is null so there's a chance to
catch this unwanted accessing of Current, however in case of value types,
the default(T) is 0!

Is it only myself who expects that accessing the Current property BEFORE the
enumeration starts or AFTER it finishes should ALWAYS throw an exception?
are there any significant reasons for such inconsistency between IEnumerable
and IEnumerable<T> implementations?

I kindly expect your comments on this issue.

Regards,
Wiktor Zychla
 
M

Marc Gravell

Actually the docs for both interfaces claim "undefined":

http://msdn2.microsoft.com/en-us/library/system.collections.ienumerable.getenumerator.aspx
http://msdn2.microsoft.com/en-us/library/s793z9y2.aspx
Is it only myself who expects that accessing the Current property BEFORE
the enumeration starts or AFTER it finishes should ALWAYS throw an
exception? are there any significant reasons for such inconsistency
between IEnumerable and IEnumerable<T> implementations?

Well, IEnumerable and IEnumerable<T> *aren't* implementations; they are
interfaces. I fully expect that if you use the IEnumerable facet of
List<T> (which it does implement) you will get the same behaviour as
with IEnumerable<T> on List<T> [they are almost certainly the same
implementation].

IMO, this isn't a biggie - if you try reading Current of an
IEnumerator[<T>] that you haven't initialized, then you aren't using it
correctly;

As an extension of this - note that Reset() is also now largely
deprecated and will throw in many implementations (especially "yield"
implementations); I'm just highlighting that usage changes over time...

Marc
 
W

Wiktor Zychla [C# MVP]

http://msdn2.microsoft.com/en-us/library/system.collections.ienumerable.getenumerator.aspx

according to this page which is more specific since it describes the Current
property:
http://msdn2.microsoft.com/en-us/library/system.collections.ienumerator.current.aspx

"Current also throws an exception if the last call to MoveNext returned
false, which indicates the end of the collection."

I've cited this source in my initial post.
Well, IEnumerable and IEnumerable<T> *aren't* implementations; they are

of course they are interfaces. did you get the impression that I think they
are implementations? I just said that I see an inconsistency between
IMPLEMENTATIONS of both interfaces, not between interfaces themselves.
interfaces. I fully expect that if you use the IEnumerable facet of
List<T> (which it does implement) you will get the same behaviour as with
IEnumerable<T> on List<T> [they are almost certainly the same
implementation].

IMO, this isn't a biggie - if you try reading Current of an
IEnumerator[<T>] that you haven't initialized, then you aren't using it
correctly;

that's the point!

the question is "whether or not the misusage should be reported to the
client code"? in my opinion, the lack of the exception is significant!

I could sarcastically say that "if the value is undefined" then let's return
a random value from misused Current property. why default(T) is better than
ANY value from misused call?

:)

Regards,
Wiktor
 
M

Marc Gravell

"Current also throws an exception if the last call to MoveNext returned
false, which indicates the end of the collection."

I've cited this source in my initial post.

Then the docs are out of sync; since "Current" clearly doesn't guarantee
to do this, I'd suggest that the GetEnumerator() doc is the correct one.

And even more curious - you are correct, in that even within just the
List<T> GetEnumerator(), it uses explicit implementation of (separately)
IEnumerator.Current and IEnumerator<T>.Current to do exactly what you
describe (i.e. if you use IEnumerator (not <T>) on List<T>, it throws) -
but this isn't the case all the time; for example, an iterator block
doesn't exhibit this; Current returns happily for both typed and untyped.

I would recommend following the "undefined" version of things... might
be worth logging a documentation issue, however...

Marc
 
M

Marc Gravell

Just additional; while MSDN says two different things, the language spec
itself actually makes it impossible to guarantee honouring the exception
behavior, so it is only reasonable to assume the "unspecified" behavior.

* it states "unspecified" for "before" and "after"
* it states that IEnumerator.Current is implemented via
IEnumerator<T>.Current and a cast to object

As such, an iterator block does not guarantee the exception on
IEnumerator.Current - so I think the only reasonable (and pragmatic)
approach is to simply not rely on the behavior of "before" and "after",
and just treat it as "undefined".

Marc

[ECMA 334 v4]
26.2.2 The Current property
An enumerator object's Current property is affected by yield return
statements in the iterator block.
When an enumerator object is in the suspended state, the value of
Current is the value set by the previous
call to MoveNext. When an enumerator object is in the before, running,
or after states, the result of
accessing Current is unspecified.
For an iterator block with a yield type other than object, the result of
accessing Current through the
enumerator object's IEnumerable implementation corresponds to accessing
Current through the
enumerator object's IEnumerator<T> implementation and casting the result
to object.
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Top