Why does foreach in C# behave this way and is there a way it gets fixed in the future? [closed]
long[] b = new long[1];
int i1 = b[0]; // compile error as it should
// no warning at all, large values gets converted to negative values silently
foreach (int i2 in b)
{开发者_如何学运维
}
class Customer : Person{}
Person[]p = new Person[]{mypers};
// no warning at all, throws typecastexception at runtime
foreach (Customer c in p)
{
}
I know they cannot simply fix it, because it would break existing programs.
But why don't they make a compatibility option in c# compiler where I can turn on typesafe foreach block generation at least for new programs or programs where I am sure it works?
foreach (var c in p)
{
}
avoids the problem nicely, doesn't it?
foreach
is behaving the only way it can.
When foreach
iterates over a collection, it doesn't necessarily know anything about the types that will be returned (it could by a collection of interface implementations, or simply objects in the case of the older .NET 'bag' style collections).
Consider this example:
var bag = new ArrayList();
bag.Add("Some string");
bag.Add(1);
bag.Add(2d);
How would foreach
behave in this instance (the collection has a string, an int, and a double)? You could force foreach
to iterate over the most compatible type which, in this case, is Object:
foreach(object o in bag)
{
// Do work here
}
Interestingly enough, using var will do exactly that:
foreach(var o in bag)
{
// o is an object here
}
But the members of object won't really allow you to do anything useful.
The only way to do anything useful is to rely on the developer to specify the exact type they want to use and then attempt to cast to that type.
You are at liberty to create an extension method on IEnumerable
that is type safe:
public static void Each(this IEnumerable @this, Action action)
{
if (@this == null) throw new ArgumentNullException("this");
if (action == null) throw new ArgumentNullException("action");
foreach (TElement element in @this)
{
action(element);
}
}
It is subtly different to the foreach loop and will occur additional overhead, but will ensure type safety.
As for the why:
Using arrays, the compiler in fact translates the foreach statement into something that looks like the following.
private static void OriginalCode(long[] elements)
{
foreach (int element in elements)
{
Console.WriteLine(element);
}
}
private static void TranslatedCode(long[] elements)
{
int element;
long[] tmp1 = elements;
int tmp2 = 0;
while (tmp2 < elements.Length)
{
// the cast avoids an error
element = (int)elements[tmp2++];
Console.WriteLine(element);
}
}
As you can see, the generated cast avoids the runtime error and of course leads to the semantic error in your case.
Btw, the same goes for IEnumerable and foreach which translates into the following code leading to the same behaviour and problem.
private static void OriginalCode(IEnumerable<long> elements)
{
foreach (int element in elements)
{
Console.WriteLine(element);
}
}
private static void TranslatedCode(IEnumerable<long> elements)
{
int element;
IEnumerator<long> tmp1 = elements.GetEnumerator();
try
{
while (tmp1.MoveNext())
{
element = (int)tmp1.Current;
Console.WriteLine(element);
}
}
finally
{
(tmp1 as IDisposable).Dispose();
}
}
EDIT: Clarified and corrected based on codymax's comment
Based on my interpretation of section 8.8.4 in the C# Spec foreach is expanded as as below. (See the part that begins "If the type X of expression is an array type then there is an implicit reference conversion from X to the System.Collections.IEnumerable interface.."
The first case converts values above int.MaxValue to -1. This is because operations don't throw overflow by default .
Since the first case the conversion is in an unchecked context section 7.6.12 describes what's supposed to happen which is
the result is truncated by discarding any high-order bits that do not fit in the destination type.
The second does as expected throw a runtime InvalidCastException. This is probably because the type returned by Enumerator.Current. There's probably a section that describes this but I didn't look for it.
long[] b = long[0] ;
System.Collections.IEnumerator le = ((long[])(b)).GetEnumerator();
int i2;
while (le.MoveNext())
{
i2 = (int)(long)le.Current;
}
Person[] p = new Person[1] { mypers };
System.Collections.IEnumerator e = ((Person[])(p)).GetEnumerator();
Customer c;
while (e.MoveNext())
{
c = (Customer)(Person)e.Current;
}
精彩评论