tags:

views:

171

answers:

3

I just finished debugging a problem, where our program crashed on a production server, but never on development machines.

I have made this small program, which I could reproduce the issue with:

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

namespace RunTimeBug
{
    class Program
    {
        static void Main(string[] args)
        {
            var coll = new Collection {{"Test", new Data()}, {"Test2", new Data()}};

            var dataSequence = coll.Cast<Data>().ToList();
            Console.WriteLine(dataSequence.Count);
        }
    }

    class Collection : Dictionary<string,Data>, IEnumerable<Data>
    {
        public new IEnumerator<Data> GetEnumerator()
        {
            foreach(var v in Values)
                yield return v;
        }
    }

    class Data { }
}

When running on my machine, this code prints "2". When running on the production server, it fails with the following exception:

Unhandled Exception: System.InvalidCastException: Unable to cast object of type 'System.Collections.Generic.KeyValuePair
`2[System.String,RunTimeBug.Data]' to type 'RunTimeBug.Data'.
   at System.Linq.Enumerable.<CastIterator>d__b0`1.MoveNext()
   at System.Collections.Generic.List`1..ctor(IEnumerable`1 collection)
   at System.Linq.Enumerable.ToList[TSource](IEnumerable`1 source)
   at RunTimeBug.Program.Main(String[] args)

The only difference I can find on these machines, is that the CLR Runtime is version 2.0.50727.4927 on machines, where it works, and version 2.0.50727.1433 on machines where it does not work.

As far as I can tell, the Cast extension method gets the wrong version of IEnumerable on the older machines, but the "right" one on newer machines.

Can anyone explain why I am seeing this ? What has changed between the 2 CLR Runtime versions, that might be causing this ?

Please note, I have already deployed a fix, and I am aware that the Collection class in the code above is poor design because it implements 2 different IEnumerable's. This was found "in the wild" in our product, so I would really like to know the exact cause.

+2  A: 

Why do you think there is a "right" version to pick? Personally I'd be happy for it to throw am ambiguous compiler error at this point. Rather than Cast, perhaps just a regular cast?

var dataSequence = ((IEnumerable<Data>)coll).ToList();

Cast<T> casts the data, not the variable.

BTW, are you recompiling for each? I'm wondering if the issue is the compiler rather than the CLR or BCL.

Marc Gravell
I would also have preferred a compiler warning in this situation - then we would have caught it sooner - when I am talking about the "right" version, I meant the version, that the original developer intended.I only compile one version of the program, it is the same executable running on the different machines.
driis
There is clearly a right version of the interface implementation to pick here, and it's the "most derived" one.
Pavel Minaev
Most derived what? `IEnumerable<TValue>` vs `IEnumerable<KeyValuePair<TKey,TValue>>` - neither is "more derived". Arguably the one with fewer generic types is a better candidate, but I'd need to read the spec very carefully (or ask the oracle).
Marc Gravell
+2  A: 

CLR version 2.0.50727.1433 is .NET 2.0 SP1 whereas CLR version 2.0.50727.4927 includes .NET 3.5 SP1 (for reference, see the Version History of the CLR).

The behavior of Enumerable.Cast changed from .NET 3.5 to .NET 3.5 SP1 as outlined here.

Jason
for explicit typed structs; the example above is an implicit typed class
Marc Gravell
The behavior was changed to fix the problem with value type casting, but that change itself changed behavior for reference types. See nobugz answer for an explanation.
Pavel Minaev
+3  A: 

I think the fix mentioned in the article that Jason linked to has something to do with your code working on later releases. I don't have access to the pre-SP1 version of Enumerable.Cast so it is a bit hard to guess.

One thing's for sure: once the code makes it into Enumerable.CastIterator() then it is guaranteed not to work in your case. That sets up an iterator to convert IEnumerable to IEnumerable<> and that iterator calls GetEnumerator() to initialize the iterator block. That can only call Dictionary.GetEnumerator(), not yours. Casting the KeyValuePair that this IEnumerable returns to Data will of course fail, as the exception tells you.

The current version of Enumerable.Cast() first tries to cast IEnumerable to IEnumerable<>. And that works. CastIterator() isn't used.

Hans Passant
Long story short, if you reimplement an interface, be sure to reimplement all "tied" members. In case of `IEnumerable<T>`, this means implementing both `IEnumerable<T>.GetEnumerator()`, and `IEnumerable.GetEnumerator()`.
Pavel Minaev