tags:

views:

115

answers:

3

Had a problem in a complex linq query so I simplified it in LINQPad:

void Main()
{
    List<basetype> items = new List<basetype>()
    {
        new typeA() { baseproperty = "1", extendedproperty = 1 },
        new typeB() { baseproperty = "2", extendedproperty = 1.1 },
        new typeA() { baseproperty = "3", extendedproperty = 1 },
    };

    items.Dump();

    (from typeA item in items
     where item is typeA
     select item).Dump();
}

public abstract class basetype
{
    public string baseproperty { get; set; }
    public string type { get; set; }
}

public class typeA : basetype
{
    public int extendedproperty { get; set; }
    public typeA() { type = "A"; }
}

public class typeB : basetype
{
    public double extendedproperty { get; set; }
    public typeB() { type = "B"; }
}

The first Dump works fine and returns:

extendedproperty baseproperty type 
1                1            A
1.1              2            B
1                3            A

However the second Dump errors with:

InInvalidCastException: Unable to cast object of type 'typeB' to type 'typeA'.

I can fix this by just removing the "typeA" but I wouldn't want to do that in the original statement as I would have to cast the type all over the place:

from item in items

Interestingly enough, moving the where also fixes this though you might agree that's a bit ugly:

from typeA item in items.Where(i => i is typeA)

My question is: why is the original where not filtering out the invalid item before the cast is evaluated?

+10  A: 

Reason #1:

The cast to the type happens before the filter because it comes to the left. In C# it is almost always the case that the thing to the left happens before the thing to the right.

Reason #2:

Suppose we did it your way. You have a List<object> and you say

from string s in myObjects where s.Length > 100

and you get an error saying that object doesn't have a Length property - because of course with your way the cast to string happens after the filter, and therefore the filter cannot depend upon the invariant determined by the cast. Presumably you put the cast in there because you want to use the properties of the target type. But you can't have it both ways; either the left operation runs first or the right operation runs first. They can't both run first.

Reason #3:

There already is a way to do what you want:

... from foos.OfType<Bar>() ...

That is equivalent to filtering first and then providing a sequence of just the filtered values of the right type.

Eric Lippert
I can understand this is normal code but when dealing with sets in Linq, much like SQL, it pays to filter the set before operating on each item. It surprises me that Linq does not do this. I would guess that if this were a Linq-to-sql statement it would work.
Chris Simpson
The OfType extension method is interesting though, thanks.
Chris Simpson
@Chris You seem to be missing the fact that *you are defining the filter in your where statement*. You are explicitly asking that every item be checked for `is typeA`. Otherwise the `where` statement would be moot, because you would iterate over a collection of `typeA` objects checking whether they are `typeA` objects.
Jay
@Jay, not sure I follow your comment. I'm certainly not missing the fact my filter is in the where statement, my issue is that the filter is not applied before the cast.
Chris Simpson
@Chris: I believe the confusion is because there's a cast implied in the `from` statement as written. The source of the LINQ statement is not `items`; it's `items` with each item cast to a `typeA`.
Stephen Cleary
@Chris There is no cast. It is equivalent to `foreach (typeA item in items)`.
Jay
@Jay, fair point. maybe I should have said declaration not cast@Stephen, I do believe you've hit the nail on the head in that the "from" is the expression "typeA item in items" and not just items. Can you please put that in an answer?
Chris Simpson
The type coercion in both "foreach(X x in xs)" and "from X x in xs" is *logically* a cast, though no explicit cast operator appears in the source code. In the foreach case, the compiler transforms the code into (X)enumerator.Current, and in the second case, into xs.Cast<X>(). It therefore seems reasonable to refer to both cases as a cast.
Eric Lippert
A: 

…in addition to the other answers, note that you can cast the result set in one shot (instead of "all over the place" using

.Cast<typeA>()

Example:

class Program
{
    static void Main(string[] args)
    {
        var list = new List<BaseType> {new TypeA(), new TypeB()};
        IEnumerable<TypeA> results = list.Where(x => x is TypeA).Cast<TypeA>();
        Console.WriteLine("Succeeded. Press any key to quit.");
        Console.ReadKey();
    }

    public class BaseType{}
    public class TypeA : BaseType {}
    public class TypeB : BaseType {}
}
Jay
@David Would you care to elaborate? There is no invalid cast exception in this case.
Jay
+1  A: 

why is the original where not filtering out the invalid item before the cast is evaluated?

Before the where runs, the from must run.

(from typeA item in items 

You have inadvertently cast in your from expression. Remove the TypeA (it is optional) from from and you'll be all set. This is the same as the implicit cast in a foreach statement:

foreach(TypeA item in items) //will throw for same reason
{
  if (item is TypeA)

I can fix this by just removing the "typeA" but I wouldn't want to do that in the original statement as I would have to cast the type all over the place:

You can use either of these solutions:

(items.OfType<TypeA>()).Dump();

(from item in items
where item is TypeA
let itemA = item as TypeA
select itemA).Dump();
David B