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

J

Jim at FCSMO

[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",
"Debugging", "Web surfing" }.GetEnumerator()};

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",
"Web surfing" }.GetEnumerator();

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

var categories2 = new { cats = new List<string> { "Meetings", "Coding",
"Debugging", "Web surfing" }.GetEnumerator() as IEnumerator<string> };

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.
 
J

Jon Skeet [C# MVP]

Jim at FCSMO said:
[Apologies for the bad code formatting - such is posting on the Web.]

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

<snip>

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 :)
 
J

Jon Skeet [C# MVP]

<snip>

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 :)
 
J

Jim at FCSMO

Jon Skeet said:
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.

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

Jon Skeet [C# MVP]

Jim at FCSMO said:
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?

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

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

Lasse Vågsæther Karlsen

Jim said:
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?

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.

"cats" is an auto-property of your anonymous object, which means your
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.
 
J

Jim at FCSMO

Lasse Vågsæther Karlsen said:
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.

"cats" is an auto-property of your anonymous object, which means your
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.

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.
 
J

Jon Skeet [C# MVP]

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.

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.
 

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