1

I've looked at lots of stackoverflow Q&As about ToList and generic constraints, but I haven't found one that explains the "Syntax Error" in the final return below. Why do I have to explicitly Select and cast the elements ("B")?

public interface I1
{
}
public class C2 : I1
{
    public static List<I1> DowncastListA( List<C2> list )
    {
        // "A": A concrete class works.
        return list == null ? null : list.ToList<I1>();
    }

    public static List<I1> DowncastListB<T2>( List<T2> list ) where T2 : I1
    {
        // "B": WORKS, if explicitly Select and Cast each element.
        return list == null ? null : list.Select( a => (I1)a ).ToList();
    }

    public static List<I1> DowncastListC<T2>( List<T2> list ) where T2 : I1
    {
        // "C": Syntax Error: 'List<T2>' does not contain a definition for 'ToList' and the best extension method overload 'ParallelEnumerable.ToList<I1>(ParallelQuery<I1>)' requires a receiver of type 'ParallelQuery<I1>'
        return list == null ? null : list.ToList<I1>();
    }
}

Some related Qs:
https://stackoverflow.com/a/1541173/199364
How to Cast List<T> To List<ClassName>

Daniel A. White
  • 187,200
  • 47
  • 362
  • 445
ToolmakerSteve
  • 18,547
  • 14
  • 94
  • 196
  • NOTE: could use more modern syntax `return list?.ToList();` in each version, with the same result. I deliberately avoided more "modern" syntax choices, in case it was a subtle bug with LINQ using the newer or more advanced syntaxes. – ToolmakerSteve Dec 27 '17 at 23:06
  • 2
    Add a reference type, `where T : class`, constraint and use a variant type like `IEnumerable` or `IReadOnlyList` – Aluan Haddad Dec 27 '17 at 23:18
  • @AluanHaddad - I figured out what you are saying, so adding it as an answer. Note that `List` implements `IEnumerable`, so is covariant, when `where T : class` is added as a constraint. – ToolmakerSteve Dec 28 '17 at 00:09
  • `List` is not covariant. – Aluan Haddad Dec 28 '17 at 00:11
  • @AluanHaddad - Then why does it find ToList, and no longer give a syntax error? My answer compiles. [At first I thought you were correct, but that was a different version that failed.] – ToolmakerSteve Dec 28 '17 at 00:24
  • 1
    Covariant means that `Covariant` is a subtype of `Covariant` if `S` is a subtype of `T`. – Aluan Haddad Dec 28 '17 at 00:27
  • @AluanHaddad - I know. What I thought you were saying, is that the reason your proposal works, is because it involves a covariant type, which is better able to "match" what the compiler is searching for. However, I see that I misunderstood what you were saying, as you did not specify whether you were suggesting a different type for the incoming parameter, or for the outgoing result. Anyway, I've documented what works in an answer. – ToolmakerSteve Dec 28 '17 at 00:31

2 Answers2

4

The extension method IEnumerable<T>.ToList<T>() doesn't allow to specify a target type. T is the type of the source IEnumerable (which is implicitly known from the source collection).

Instead you can use this:

public static List<I1> DowncastListC<T2>( List<T2> list ) where T2 : I1
{
    return list == null ? null : list.Cast<I1>().ToList();
}

I.e. you first cast each element (resulting in an IEnumerable<I1>), then create a list from that.

BTW: you could even make that an extension method to simplify its usage:

public static class Extensions
{
    public static List<I1> Downcast<T2>(this List<T2> list) where T2 : I1
    {
        return list == null ? null : list.Cast<I1>().ToList();
    }
}
M4N
  • 94,805
  • 45
  • 217
  • 260
  • 1
    Thanks - the subtlety that I was attempting to change the target type of ToList is what I missed. For anyone who uses this, I would use the C#6 null conditional to simplify that line of code (I was deliberately avoiding new syntax): `return list?.Cast().ToList();`. I was pleasantly surprised to find that your solution still compiles even if the target type is also generic: `public static List DowncastList( List list ) where T2 : T1 { return list?.Cast().ToList(); }` – ToolmakerSteve Dec 27 '17 at 23:39
  • Hmm, your explanation raises a question. How is it that my case "A" works? Does that use a *different* ToList declaration, which handles the target type? – ToolmakerSteve Dec 27 '17 at 23:45
  • Since `C2` is an `I1` in C# 4, `List` is a `IEnumerable` is a `IEnumerable` which is acceptable to `ToList` due to covariance: https://stackoverflow.com/a/6734321/2557128 – NetMage Dec 28 '17 at 00:32
  • @NetMage - ah, but that is **exactly** why I posed the question! Observe that I specified `where T2 : I1`, so I was already specifying the covariance that you mention, right? The actual answer must be subtler... – ToolmakerSteve Dec 28 '17 at 00:44
  • I'm confused - there is no `where` for case "A"? – NetMage Dec 28 '17 at 00:44
  • @NetMage - not needed, because both types are *concrete*. The compiler knows `C2 : I1`. I added that in the *other* cases, attempting to specify the needed covariance. – ToolmakerSteve Dec 28 '17 at 00:45
  • Ah. If you add `where T2 : class,I1` the compiler will access case "C". – NetMage Dec 28 '17 at 00:47
  • @NetMage - okay, I see your confusion. What I was trying to ask, is why the compiler was able to correctly locate ToList in case A, but not in case C. In one case, the covariance is implicit. In the other it is explicit. I don't see any reason that should matter to success, yet M4N's *explanation* is about covariance. Yes, one solution is to add "where : class", but why? – ToolmakerSteve Dec 28 '17 at 00:48
  • 1
    Because `T2` could be a `struct` if you don't add the `class` constraint, and covariance is not allowed for a `struct`: https://stackoverflow.com/a/12454932/2557128 – NetMage Dec 28 '17 at 00:51
0

As suggested by @AluanHaddad

    public static IReadOnlyList<I1> DowncastList<T2>( List<T2> list ) where T2 : class, I1
    {
        return list;
    }

Note the added constraint T2 : class.
That answer doesn't require any casting of list, because IReadOnlyList<T> is covariant, and list already has members that implement I1. (Could alternatively make the return type IEnumerable<I1>, but I needed indexing, so chose to expose a higher interface.)

Or as an alternative, if wish to expose the full List functionality:

    public static List<I1> DowncastList<T2>( List<T2> list ) where T2 : class, I1
    {
        return list == null ? null : list.ToList<I1>();
    }

Note the added constraint T2 : class. This gives enough information for IEnumerable<T2> (which List<T2> implements) to find the implementation of ToList<>`.

Now that this works, here is second version above, using C# 6 null conditional:

    public static List<I1> DowncastList<T2>( List<T2> list ) where T2 : class, I1
    {
        return list?.ToList<I1>();
    }
ToolmakerSteve
  • 18,547
  • 14
  • 94
  • 196
  • I don't really recommend that. I would suggest you just return `IEnumerable` then you do not need to cast at all. – Aluan Haddad Dec 28 '17 at 00:12
  • @AluanHaddad - Yes, of course you are right. And that is the correct recommendation to give people Thanks for the reminder! [In my situation, which is updating a legacy program, that would result in having to do the cast in some call sites, which I was trying to avoid.] – ToolmakerSteve Dec 28 '17 at 00:14
  • 1
    Perhaps use `IReadOnlyList` then? That will provide indexing. – Aluan Haddad Dec 28 '17 at 00:15
  • 1
    @AluanHaddad - Ahh, thank you very much -- I have been programming in C# for many years, and did not know about that class. That will definitely help me to move "incrementally" away from old-style code (most of my work is updating/enhancing existing software). – ToolmakerSteve Dec 28 '17 at 00:17