-1
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?

codymanix
  • 28,510
  • 21
  • 92
  • 151
  • 8
    In the future, ask a question, don't hang a question on a very controversial and inflammatory rant. – Nick Craver Aug 06 '10 at 13:16
  • You mean they should add special type conversion rules for `foreach`specifically? The problem has nothing to do with `foreach`. It is C#'s typesystem as a whole that allows certain conversions. It does so because they're useful in some cases. – jalf Aug 06 '10 at 13:17
  • Agreed, "Can foreach be fixed w/o breaking existing code" would have been a question. – H H Aug 06 '10 at 13:18
  • For anyone curious as to my last comment, besides the question the title was previously "Why does foreach in C# suck so much?" – Nick Craver Aug 06 '10 at 13:21
  • 16
    `foreach` isn't broken in C#. Your understanding of what it should do is. I could cause the same behavior in your single assignments by emulating what happens in the `foreach` statement: `int i1 = (int) b[0];` or `Customer c = (Customer)p[0];`. By specifying a type in the `foreach` statement, you're telling the compiler to explicitly cast each element to your desired type and it's simply listening to you. – Justin Niessner Aug 06 '10 at 13:22
  • I know that foreach automatically makes a type conversions, which is imho a bad desicion that should be fixed – codymanix Aug 06 '10 at 13:24
  • Where's Eric Lippert's Bat-signal when you need it? :) – Adam V Aug 06 '10 at 13:57
  • It is not a bad decission at all, it's even a very good decission. You are just using it in a wrong way – Nealv Aug 06 '10 at 13:58
  • 1
    @codymanix - the bad decision is in your code, not in the compiler. You said in your foreach that you wanted longs typecast to ints. It did it. I don't see the problem. If you wanted longs in your foreach, you should have cast them to longs. -1 – Joel Etherton Aug 06 '10 at 15:36
  • @Joel Etherton: Sometimes you make mistakes. – Greg Aug 06 '10 at 16:17
  • @Greg - True dat, and I've made more than my share of them. However, I never complained that the programming language was insufficient because of my errors. – Joel Etherton Aug 06 '10 at 17:06
  • Here's some online code that demonstrates the issue. Compiles fine but runtime exception thrown: https://dotnetfiddle.net/5zGgIZ. – Colm Bhandal Aug 19 '20 at 13:06

5 Answers5

17
foreach (var c in p) 
{
}

avoids the problem nicely, doesn't it?

jalf
  • 243,077
  • 51
  • 345
  • 550
  • foreach (Person c in p) also would "solve" it, that is not the point.. – codymanix Aug 06 '10 at 13:18
  • You would need to add a check for `if (c is Customer)` inside in either case. – Justin Aug 06 '10 at 13:19
  • 1
    No. The "point" apparently wasn't to ask a question either. Which makes me wonder why you posted your rant on a Q&A site. My point is that the language allows you to omit the type entirely, and the compiler will just use the correct one. If the simplest solution does the right thing, why waste your time complaining that a more complex one requires a minimum of thought to get right? – jalf Aug 06 '10 at 13:21
  • @jalf Calm down. It's a legitimate question, just poorly worded. – Josh Stodola Aug 06 '10 at 13:22
  • 3
    using "var" everywhere won't make programs easier to read. – codymanix Aug 06 '10 at 13:26
  • 1
    @codymanix - Now I'm really confused. You don't want to use type inference using `var` but you also don't want the compiler to try typecasting even after you explicitly specify a different type in the `foreach` loop. What exactly do you want the behavior to be? (keep in mind that foreach can iterate over collections that have varying types as well). – Justin Niessner Aug 06 '10 at 13:29
  • 1
    @Josh: legitimate questions aren't tagged with `[rant]` as this was. @codymanix: I think `var` is perfectly readable. – jalf Aug 06 '10 at 13:31
  • To the [rant] keyword: Sadly to see that nobody here has my kind of humour... – codymanix Aug 06 '10 at 13:40
  • I like to see the types Iam operating on and do not want to have the need to browse into the collection to see what kind of type it actually will return. – codymanix Aug 06 '10 at 13:41
  • @codymanix, I started using var a while ago, and my code is stille very readable, without using comment. Just give your variables Logical names like: personList etc – Nealv Aug 06 '10 at 14:01
  • Ah I got it. Instead of using explicit typing you use var and make the intended type part of the variable name.. – codymanix Aug 06 '10 at 14:30
  • @jaff: In response to your first comment, that's why the Subjective and Argumentative close reason exists. – Powerlord Aug 06 '10 at 15:37
  • I always type `foreach( var f in foo )` and use Resharper to "specify type explicitly" to change `var` into whatever type it is. – Greg Aug 06 '10 at 16:19
  • @R. Bernrose: and that's why I voted to close as Subjective and Argumentative before it was reopened. ;) – jalf Aug 06 '10 at 17:34
6

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.

Justin Niessner
  • 242,243
  • 40
  • 408
  • 536
4

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.

Paul Ruane
  • 37,459
  • 12
  • 63
  • 82
  • I've never seen the @ before, potentially stupid question but what's that do? – AndrewC Aug 06 '10 at 13:22
  • 2
    @AndyC http://stackoverflow.com/questions/91817/whats-the-use-meaning-of-the-character-in-variable-names-in-c – Justin Aug 06 '10 at 13:27
  • 1
    @ allows you to use a C# keyword as a variable name. So you can do questionable things like: int @foreach = 1; long @if = 2; string @class = "foo"; – AlfredBr Aug 06 '10 at 13:28
  • You can use keywords as normal variables with the @ sign. normally you use it when interoperating with other languages which expose members that is a keyword in the other language. – codymanix Aug 06 '10 at 13:28
  • It is simply added to let you use a reserved word as a variable name. – JohannesH Aug 06 '10 at 13:29
  • Cheers guys, never knew that! – AndrewC Aug 06 '10 at 13:36
1

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();
    }
}
  • I don't believe that foreach gets translated into an simple if-statement. Maybe you mean a for-statement :) – codymanix Aug 09 '10 at 11:04
  • I have posted an answer before comparing while, for, foreach, and goto to the point that they will all compile to the same IL. See it here http://stackoverflow.com/questions/2447559/c-does-function-get-called-for-each-iteration-of-a-foreach-loop/2447646#2447646 – Matthew Whited Aug 09 '10 at 16:56
  • @codymanix Ops sorry, should be a while loop :) corrected – Roland Sommer Aug 09 '10 at 22:27
  • @Matthew Whited: Indeed, the IL is pretty much the same for a lot of statement blocks. I have choosen the while loop for demonstration because I think it's the easiest way to show the cast. – Roland Sommer Aug 09 '10 at 22:35
0

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;

            }
Community
  • 1
  • 1
Conrad Frix
  • 51,984
  • 12
  • 96
  • 155
  • Firstly it is wrong that values above int.MaxValue are converted to -1, they convert to the corresponding negative value instead. Secondly, foreach on arrays does *not* create a GetEnumerator call but instead a simple for-loop. Besides, the double-cast to Person and then to Customer is superfluous. – codymanix Aug 11 '10 at 11:30
  • @codymax You're right about the conversion so I updated my answer. Why do you think it doesn't do a GetEnumerator? The double cast may be superfluous in some cases but perhaps not in all. – Conrad Frix Aug 11 '10 at 15:01