.NET Framework - C# compiler challenge - see if you can answer why...

Asked By JimatFCSM on 21-Feb-08 10:42 AM
[Apologies for the bad code formatting - such is posting on the Web.]

The following code produces an infinite loop in its second while loop.

using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace Foo
{
class Program
{
static void Main(string[] args)
{
var categories = new List<string> { "Meetings", "Coding", "Debugging", "Web
surfing" }.GetEnumerator();

while (categories.MoveNext())
{
Console.WriteLine(categories.Current);
}

var categories2 = new { cats = new List<string> { "Meetings", "Coding",

while (categories2.cats.MoveNext())
{
Console.WriteLine(categories2.cats.Current);
}
}
}
}

This code fixes it:

using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace Foo
{
class Program
{
static void Main(string[] args)
{
var categories = new List<string> { "Meetings", "Coding", "Debugging",

while (categories.MoveNext())
{
Console.WriteLine(categories.Current);
}

var categories2 = new { cats = new List<string> { "Meetings", "Coding",

while (categories2.cats.MoveNext())
{
Console.WriteLine(categories2.cats.Current);
}
}
}
}

For a full write-up including diffs between the generated MSIL that explains
WHAT is happening and gives some other clues, see my blog post here -
http://ednortonengineeringsociety.blogspot.com/2008/02/earlier-i-had-posted-about-some-c.html
. What I want to know is WHY it is happening? Education appreciated.




Jon Skeet [C# MVP] replied on 21-Feb-08 10:53 AM
It's because List<T>.Enumerator is a struct, rather than a class.

In the never-ending loop, the initial values (where it's not been
started) are fetched each time.

When you reference is "as IEnumerator<string>" then it's boxed a single
time, and the value changes within the box.

Basically what we've got here is a mutable struct - which is almost
always a bad idea, partly because it gives rise to tricky situations
like this.

I shall mail Eric Lippert to confirm that this is indeed the problem -
he may well blog about it :)

--
Jon Skeet - <skeet@pobox.com>
http://www.pobox.com/~skeet   Blog: http://www.msmvps.com/jon.skeet
World class .NET training in the UK: http://iterativetraining.co.uk
Jon Skeet [C# MVP] replied on 21-Feb-08 10:59 AM
Here's a short but complete program which demonstrates the same sort of
behaviour without needing to know about List<T>.Enumerator:

using System;

interface IFoo
{
void Bar();
int Count { get; }
}

struct MutableFooStruct : IFoo
{
int count;

public int Count
{
get { return count; }
}

public void Bar()
{
count++;
}
}

class OddnessDemonstration
{
static void Main()
{
Console.WriteLine ("As MutableFooStruct:");
var x = new { Foo = new MutableFooStruct() };
for (int i=0; i < 5; i++)
{
Console.WriteLine (x.Foo.Count);
x.Foo.Bar();
}

Console.WriteLine ("As IFoo:");
var y = new { Foo = new MutableFooStruct() as IFoo };
for (int i=0; i < 5; i++)
{
Console.WriteLine (y.Foo.Count);
y.Foo.Bar();
}
}
}

Hopefully that makes it a bit clearer :)

--
Jon Skeet - <skeet@pobox.com>
http://www.pobox.com/~skeet   Blog: http://www.msmvps.com/jon.skeet
World class .NET training in the UK: http://iterativetraining.co.uk
JimatFCSM replied on 21-Feb-08 11:23 AM
Thanks, Jon. But can you explain why the first GetEnumerator call, which is
also assigned to a var (categories), works as is without the cast? I know in
the second loop it is a member (cats) that is created through initialization
syntax inside an anonymous class then assigned to categories2, but I would
think the compiler could still figure it out, as it did with the first var.
Do you see my confusion?
Jon Skeet [C# MVP] replied on 21-Feb-08 11:28 AM
Yes - because you've got a *variable* there, instead of the result of
asking for a property. When you ask for a property, you get a copy each
time. When you call a method directly on the variable, that can change
the value of the variable.

Here's another example, which doesn't use anonymous types at all:

using System;

public class MutableFooStructHolder
{
MutableFooStruct foo = new MutableFooStruct();

public MutableFooStruct Foo
{
get { return foo; }
}
}

interface IFoo
{
void Bar();
int Count { get; }
}

public struct MutableFooStruct : IFoo
{
int count;

public int Count
{
get { return count; }
}

public void Bar()
{
count++;
}
}

class Test
{
static void Main()
{
MutableFooStructHolder holder = new MutableFooStructHolder();
for (int i=0; i < 5; i++)
{
Console.WriteLine(holder.Foo.Count);
holder.Foo.Bar();
}

MutableFooStruct x = new MutableFooStruct();
for (int i=0; i < 5; i++)
{
Console.WriteLine(x.Count);
x.Bar();
}
}
}


I certainly see why it's confusing, but it's basically just because of
a broken framework decision, not a compiler bug.

--
Jon Skeet - <skeet@pobox.com>
http://www.pobox.com/~skeet   Blog: http://www.msmvps.com/jon.skeet
World class .NET training in the UK: http://iterativetraining.co.uk
lass replied on 21-Feb-08 11:31 AM
When you return a struct from a property, you're getting a copy of
whatever it is the property is reading to return a value from. Since
structs are value types, if you modify your copy, the original value the
get-accessor of the property read from is still untouched, unless they
share reference types internally.

Which means it is your copy that is being modified, but the next time
you're reading the property again, you get the same original copy once more.

code is similar to this:

List<T>.Enumerator enumerator = categories2.cats;
// enumerator is a struct
enumerator.MoveNext();
// since enumerator is a struct, you're modifying enumerator, but
categories2.cats is still holding the original unmodified value

List<T>.GetEnumerator() does not return IEnumerator, it returns
List<T>.Enumerator, which is defined as a struct, and hence the problem.

--
Lasse Vågsæther Karlsen
mailto:lasse@vkarlsen.no
http://presentationmode.blogspot.com/
PGP KeyID: 0xBCDEA2E3
JimatFCSM replied on 21-Feb-08 12:08 PM
Thanks Jon and Lasse. I think I've got it now. It certainly wasn't intuitive
(to me) when I first hit it, though. I have to think many developers would
find this "obscure" - at least as manifested in my simple test program.
Anyway, it is less so to me now, and I appreciate the explanations.
Jon Skeet [C# MVP] replied on 21-Feb-08 01:01 PM
It takes a while to explain why, yes - but then I can't remember the
last time I explicitly called GetEnumerator() from my code (other than
for demonstration purposes). So while I agree that the answer is
obscure, I'd say the problem is too.

--
Jon Skeet - <skeet@pobox.com>
http://www.pobox.com/~skeet   Blog: http://www.msmvps.com/jon.skeet
World class .NET training in the UK: http://iterativetraining.co.uk