views:

4276

answers:

2

I have to be missing something obvious here. I don't get why this cast of the results of a linq query returns null and not the typed list I'm requesting.

IList<IMyDataInterface> list = query.ToList() as IList<IMyDataInterface>;

The complete code to run this is below. This is a knowledge gap I need to bridge. I have tried all kinds of permutations of casts to get it to work. I get no Exceptions, just a null. Of note is that the Linq query is selecting its results into instances of my custom "MyDataClass" which implements IMyDataInterface

class Program
{
    static void Main(string[] args)
    {
        IMyFunctionalInterface myObject = new MyClass();


        //myObject.Get() returns null for some reason...
        IList<IMyDataInterface> list = myObject.Get();

        Debug.Assert(list != null, "Cast List is null");
    }
}

public interface IMyFunctionalInterface
{
    IList<IMyDataInterface> Get();
}

public class MyClass : IMyFunctionalInterface
{
    public IList<IMyDataInterface> Get()
    {
        string[] names = { "Tom", "Dick", "Harry", "Mary", "Jay" };

        var query = from n in names
                    where n.Contains("a")
                    select new MyDataClass
                    {
                        Name = n.ToString()
                    };

        //There IS data in the query result
        Debug.Assert(query != null, "result is null");
        //but the cast here makes it return null
        IList<IMyDataInterface> list = query.ToList() as IList<IMyDataInterface>;

        return list;
    }

}
public interface IMyDataInterface
{
    string Name { get; set; }
}

public class MyDataClass : IMyDataInterface
{
    public string Name { get; set; }
}
+3  A: 

Try this:

var query = from n in names
            where n.Contains("a")
            select new MyDataClass
            {
              Name = n.ToString()
            } as IMyDataInterface;

Your problem is in this line:

IList<IMyDataInterface> list = query.ToList() as IList<IMyDataInterface>;

This can also be written as:

List<MyDataClass> tmp = query.ToList();
IList<IMyDataInterface> list = tmp as IList<IMyDataInterface>;

Unfortunately, in C# the as operator does not work in the way you want it to. The as operator simply casts the list object as a list of a different type; it does not attempt to go through the list and cast every item. To cast a list to something else, you need to call the Cast extension method on it. E.g.:

IList<IMyDataInterface> list = query.ToList().Cast<IMyDataInterface>();

So, your options are: cast ever item in your query as the interface you want (my first example) or cast the entire list after you've executed your query (my second example).

I suggest the former.

Randolpho
I'm surprised the cast in approach 1 works. Every time I try anything like that, I get the same problem described in the question. The only solution I've found is the Cast<>() extension method, which can be called either before or after .ToList().
Ryan Riley
I tested before I posted. The cast works in approach 1 because the select portion can take any valid C# construction expression.
Randolpho
+14  A: 

The problem here is one of covariance.

First, your example is a bit too complicated. I have removed some fluff. Also, I've added some diagnostics that illuminate the problem.

class Program
{
    static void Main(string[] args)
    {
        var names = new[] { "Tom", "Dick", "Harry", "Mary", "Jay" };

        var query = from n in names
                    select new C
                    {
                        S = n
                    };

        //There IS data in the query result
        Debug.Assert(query != null, "result is null");

        //but the conversion here makes it return null
        var list = query.ToList() as IList<I>;
        Console.WriteLine(query.ToList().GetType());

        // this assert fires.
        Debug.Assert(list != null, "Cast List is null");
    }
}

interface I
{
    string S { get; set; }
}

class C : I
{
    public string S { get; set; }
}

The output of this program is:

System.Collections.Generic.List`1[C]

Note that we're trying to cast a List<C> to List<I> which doesn't work in C# 3.0.

In C# 4.0 you should be able to do this, thanks to the new co- and contra-variance of type parameters on generic interfaces.

Also, your original question asked about IQueryable but that's not relevant here: the query expression you supplied creates an IEnumerable<string> not an IQueryable<string>.

EDIT: I want to point out that your "cast" using the as operator is technically not a cast, but is a "type conversion". If you had use a cast, you would have gotten an exception with useful information. If I change to:

    var list = (IList<I>)query.ToList();

I get an InvalidCastException with:

Additional information: Unable to cast object of type 'System.Collections.Generic.List1[C]' to type 'System.Collections.Generic.IList1[I]'.

Jay Bazuzi
Interesting point about C# 4.0 contra-variance.
Randolpho
Note that in C# 4.0 covariance/contravariance applies only to interfaces and delegates, so the cast of List<C> to List<I> in C# 4.0 won't work either, and that's right.
Rafa Castaneda
I am not sure I understand this answer. Should the type conversion / cast work in C# 4.0? (Note: I get the InvalidCastException even though I am using C# 4.0.)
Jan Aagaard
(I've been away from C# for a while, so I'm a little fuzzy.) C# 4.0 adds co- and contra-variance for interface type parameters. The NDP annotated a few interfaces for co-and contra-variance. For example, in my Beta 2 build, I see that `IEnumerable` is now `out T`, but `IList` is not. So in the above code I can write `query.ToList() as IEnumerable<I>` and it works (non-null).
Jay Bazuzi