Quick LINQ question



  • Ok, so I have a couple classes that implement the same interface:

    IBlarp
    DogBlarp : IBlarp
    CatBlarp : IBlarp
    

    I have a function that gives me a list of all blarps that belong to a user:

    List<IBlarp> allBlarps = user.GetAllBlarps();
    

    To process this list, I need sublists based on type. (Basically, I need to split the list of IBlarps into two lists, one of DogBlarps and one of CatBlarps.)

    I tried this in LINQ:

    var catBlarps = (List<CatBlarp>)allBlarps.Where( b => b is CatBlarp );
    

    Looks like it should work, right? Should be completely unambiguous what I'm doing, right?

    Unfortunately, no. It produces an illegal cast exception because it's trying to cast List<IBlarp> to a List<catBlarp>. Hm.

    So the question is: how do I make this work? Hopefully in a one-liner.



  • Interesting, I would expect this to work.

    I do have vague recollections about weirdness surrounding casting ILists, but I'm too out of C# to remember.

    Now I'm curios, starting LinqPad...



  • @cartman82 said:

    Interesting, I would expect this to work.

    Me too! The code is 100% unambiguous!

    It's when the magic of C# + LINQ breaks down that you realize the magic isn't magic, but actually hundreds of guys somewhere figuring out edge-cases and they missed one.

    Anyway, it looks like the LINQ "OfType<Type>" function is what I need, I'm trying that now, but it takes a few minutes to test as it's a runtime error.



  • Wait a sec. Shouldn't it be

    var catBlarps = (List<CatBlarp>)allBlarps.Where( b => b is CatBlarp ).ToList();
    

    ...? Testing....



  • The same difference....

    Cannot convert type 'System.Collections.Generic.List<UserQuery.IBlarg>' to 'System.Collections.Generic.List<UserQuery.Blarg>'
    

    Hmm...



  • @blakeyrat said:

    var catBlarps = (List<CatBlarp>)allBlarps.Where( b => b is CatBlarp );

    This seems to work:

    var catBlarps = allBlarps.Where( b => b is CatBlarp ).OfType<CatBlarp>();
    

    Note you can't use just OfType without also checking type within the LINQ statement (even though that also seems like it should work), or you'll get invalid casts.



  • I came up with this

    void Main()
    {
    	var iblargs = new List<IBlarg>();
    	var blargs = iblargs.Where(x => x is Blarg).Cast<Blarg>().ToList();
    	Console.WriteLine(blargs);
    }
    
    public interface IBlarg {}
    
    public class Blarg : IBlarg {}
    


  • That works too, but you have to run .ToList() earlier than absolutely necessary which enumerates the entire list at that point.



  • var blargs = iblargs.Where(x => x is Blarg).Select(x => (Blarg)x)?



  • void Main()
    {
    	var iblargs = new List<IBlarg>();
    	var blargs = iblargs.OfType<Blarg>().ToList();
    	Console.WriteLine(blargs);
    }
    
    public interface IBlarg {}
    
    public class Blarg : IBlarg {}
    

    Slightly simplier.

    You maybe:

    Dictionary<Type, List<IBlarg>> dict = iblargs.GroupBy(x => x.GetType()).ToDictionary(x => x.Key, x => x.ToList())

    totally untested



  • @blakeyrat said:

    That works too, but you have to run .ToList() earlier than absolutely necessary which enumerates the entire list at that point.

    I thought you wanted a list, but now I see you end up just getting an enumerable.

    @blakeyrat said:

    Note you can't use just OfType without also checking type within the LINQ statement (even though that also seems like it should work), or you'll get invalid casts.

    Actually, it seems you can do without the Where part.

    void Main()
    {
    	var iblargs = new List<IBlarg> {
    		new Blarg1(),
    		new Blarg2()
    	};
    	
    	var blargs = iblargs.OfType<Blarg1>().ToList();
    	Console.WriteLine(blargs.GetType());
    	Console.WriteLine(blargs.Count);
    }
    
    public interface IBlarg {}
    
    public class Blarg1 : IBlarg {}
    
    public class Blarg2 : IBlarg {}
    
    

    Output:

    System.Collections.Generic.List`1[UserQuery+Blarg1]
    1



  • @blakeyrat said:

    Note you can't use just OfType without also checking type within the LINQ statement (even though that also seems like it should work), or you'll get invalid casts.

    Odd. Works for me...



  • @blakeyrat said:

    So the question is: how do I make this work? Hopefully in a one-liner.

    var catBlarps = from b in allBlarps where b is CatBlarp select (CatBlarp)b;
    


  • As far as I know, that's the whole point of OfType.



  • @blakeyrat said:

    var catBlarps = allBlarps.Where( b => b is CatBlarp ).OfType<CatBlarp>();

    Note you can't use just OfType without also checking type within the LINQ statement (even though that also seems like it should work), or you'll get invalid casts.

    Taken from a decompile of System.Core.dll:

    private static IEnumerable<TResult> OfTypeIterator<TResult>(IEnumerable source)
    {
    	foreach (object current in source)
    	{
    		if (current is TResult)
    		{
    			yield return (TResult)((object)current);
    		}
    	}
    	yield break;
    }
    


  • Ok well, the source I read on OfType said it didn't, and I have actual work to complete instead of wacking off in dotnetfiddle so congratulations U R MR SMARTY MAN



  • In that case, it reduces to:

    var catBlarps = allBlarps.OfType<CatBlarp>();
    

    It works in my testing.



  • Gotcha.

    It still bugs me that the first version I tried didn't work, though. But now that I know about OfType<blah> I guess that's a better way of solving the problem.



  • Well, the reason it didn't work is that is doesn't actually do the cast. So the items remain IBlarp, and List<T> doesn't know that an IBlarp can be cast into a CatBlarp safely, so it doesn't think something generic on them can be cast that way either. If you'd done allBlarps.Select(blarp => blarp as CatBlarp).Where(blarp => blarp != null);, it'd be cast correctly already.

    This makes me want to read up on A<out T> and such again, since I remember none of that.



  • Hindler-Milney doesn't (automatically) have dependent types, which you would need in order to have a stream with multiple element types. (In fact, if you have the full power of dependent types, your language is total.)

    If you really want to do something like that, you'll need to reify the interface as a type, and dispatch based on the data constructors.

    Otherwise, your only option is to build a stream/list for each type.



  • @Magus said:

    Well, the reason it didn't work is that is doesn't actually do the cast.

    Well duh.

    But that doesn't change the fact that the code as written is completely unambiguous and Linq/Intellisense should have figured it the hell out.



  • completely unambiguous

    It's not. It's not even well-typed. http://en.wikipedia.org/wiki/Parametric_polymorphism


  • SockDev

    It's unambiguous to a human, but the compiler doesn't have that level of intelligence



  • Oh I see, is this thread in "pile-on Blakeyrat" mode now?

    I'll just mute it.



  • I was hoping you would learn something, since you don't seem to understand Linq's type system.

    I.e., I was trying to be genuinely helpful. But it is you, of course. Silly me!



  • It's not ambiguous, no, but you're also not using some kind of imaginary IEnumerable<in T>, so it's unambiguously wrong.



  • The root problem seems to be that blakeyrat doesn't distinguish between List<CatBlarp> and List<IBlarp> which holds only CatBlarps. Too bad he already ragequitted, because I think I'd enjoy this rant about how they're the same.


  • area_deu

    I think VB.NET wouldn't error out on that.



  • Does it do type erasure?


  • SockDev

    @aliceif said:

    I think VB.NET wouldn't error out on that.

    I think VB is a little more liberal about what it accepts; C#, on the other hand, is pretty strict



  • @Maciejasjmj said:

    Odd. Works for me...

    @blakeyrat said:

    Ok well, the source I read on OfType said it didn't, and I have actual work to complete instead of wacking off in dotnetfiddle so congratulations U R MR SMARTY MAN

    Do not help this man.



  • @CreatedToDislikeThis said:

    Do NOT help this man.

    Well I got a compliment, I guess? For Blakey that's something huge.


  • area_deu

    I'm not sure how much Option Strict would affect that, though.



  • .NET is .NET and a list is a list:

    Compilation error (line 19, col 0): Value of type 'System.Collections.Generic.List(Of ITest)' cannot be converted to 'System.Collections.Generic.List(Of Test1)'.
    

  • area_deu

    I guess I misremembered something, then.

    VB.NET lets you treat elements of the List as Test1s (without explicit casts), though - unlike C#.



  • Ooooh yeah I almost missed that (at first I too thought it should work, but now i see it). Casting List<IBlarp> to List<CatBlarp> would not be valid even outside of LINQ (there's no way the compiler could possibly know or guarantee that your List<IBlarp> is only going to contain CatBlarps), which is essentially what is happening (allBlarps.Where returns, IIRC, IEnumerable<IBlarp>, which you then try to cast to List<CatBlarp>, which as expected fails).

    EDIT: Damn you Discourse, for interpreting my post as wonky HTML.



  • @Masaaki_Hosoi said:

    there's no way the compiler could possibly know or guarantee that your List<IBlarp> is only going to contain CatBlarps

    Well it could, technically, attempt to cast every element and give a runtime error on a cast failure. That opens a whole different can of worms, mostly in terms of language spec, but I guess it's not impossible...


  • SockDev

    True, but I'd prefer it to fail at compile-time. I mean, it's not like it's hard to make right :smile:



  • @Maciejasjmj said:

    Well it could, technically, attempt to cast every element and give a runtime error on a cast failure. That opens a whole different can of worms, mostly in terms of language spec, but I guess it's not impossible...

    It could, but then that wouldn't be the compiler, that'd be the runtime. And one of the main things I think people really like about C# is how fantastic the compile time safety is.



  • The can of worms was opened years ago. If you make a new collection, say, class InsaneCollection<in T> where T : IBlarp, it will actually do that.



  • @CreatedToDislikeThis said:

    Do not help this man.

    Yeah, he acts like he's entitled to free consulting work!


Log in to reply
 

Looks like your connection to What the Daily WTF? was lost, please wait while we try to reconnect.