Implementing List<T>, why wont it cast back to MyList after LINQ ? (Unable to cast object of type 'WhereListIterator`1)

69 Views Asked by At

Probably something silly that I'm missing, but so far searching has come up with nothing. I have implemented List<T> on my class, and when using a MyList.Where() function it wont cast back to a MyList object ?

public class VideoList : List<Video>
{
    public VideoList GetVideos(bool onlyShorts)
        => (VideoList)this.Where(x => x.IsShort.Equals(onlyShorts)).ToList();
}

Throws error:

Unable to cast object of type WhereListIterator1[Video] to type VideoList.

Tried some different types of casting as was suggested online, but thus far i haven't got passed this error. Also tried this, but same problem. Probably some semantics ? Is there a way to resolve this error without foreach recreating the list after the linq statement ?

2

There are 2 best solutions below

3
rotabor On BEST ANSWER
this.Where(x => x.IsShort.Equals(onlyShorts)).ToList()

produces "List<Video>" which can't be upcasted to type VideoList.

If you still want GetVideos returns VideoList, you need to have and use the appropriate constructor (as a possible solution):

public VideoList(List<Video> videolist) { ... }

public VideoList GetVideos(bool onlyShorts)
    => new VideoList(this.Where(x => x.IsShort.Equals(onlyShorts)).ToList());
2
BionicCode On

As the extension method's name Enumerable.ToList implies, the return type is a List<T>. Having it return an e.g. ObservableCollection would be semantically wrong as this is not a List<T> (ToList). Such a method would rather be named ConvertTo (from a member naming perspective).

The signature of Enumerable.ToList looks as follows:

public static System.Collections.Generic.List<TSource> ToList<TSource> (this System.Collections.Generic.IEnumerable<TSource> source);

By looking at the signature you may understand that the method cannot know the actual collection type it is working on as the original source collection, which is a VideoList in your case, is downcasted to IEnmumerable<T>. In other words, Enumerable.ToList does not know that the source collection is of type VideoList.
In addition, in order to return an e.g. VideoList the extension method ToList would have to invoke the constructor of VideoList in order to create a new instance. For simplicity and for performance reasons, the Enumerable.ToList avoids this.

To solve your problem, you can make use of deferred execution and the fact that all collections accept an IEnumerable as constructor parameter:

public class VideoList : List<Video>
{
  public VideoList(IEnumerable<Video> videos) : base(videos)
  {
  }

  public VideoList GetVideos(bool onlyShorts)
  {  
    // At this point the query is not executed. It's deffered.
    IEnumerable<Video> videos = this.Where(x => x.IsShort.Equals(onlyShorts));

    // Realize the query (iterate the VideoList only once --> deferred execution)
    return new VideoList(videos);
  }
}

The key point is that LINQ's key feature is that it produces deferred queries, called "deferred execution".
This means that myList.Where() does not immediately execute. For example, this behavior is achieved by using yield return.

Therefore, most of the Enumerable extension methods return an IEnumerable<T> that is only enumerated once the IEnuemrable<T>.GetEnumerator is called and the returned IEnumerator<T> is used. This is called realization: the actual query is executed and the result values are generated.
For example, one way to realize the query is thr iteration of the IEnumerable result using a foreach.
Enumerable also defines extension methods that lead to immediate realization. Those methods are for example, ToList, Sum or OrderBy. These types of methods are not able to return an example without having to iterate the complete source collection. For this reason (performance) we should always make use of deferred execution and avoid ToList and its likes. Better think twice, as ToList can easily lead to having the source collection iterated twice or more (which can have a serious performance impact if the collection is big enough:

var numbers = new List<int> { 1, 2, 3, 4, 5 };
var filteredNumbers = numbers
  .Where(number > 3)
  .ToList(); // First complete iteration

// Second complete iteration
foreach (int number in filteredNumber)
{
}

For that reason, we always use deferred execution when possible:

var numbers = new List<int> { 1, 2, 3, 4, 5 };

// At this point, filteredNumbersQuery still contains all the original values
IEnumerable<int> filteredNumbersQuery = numbers.Where(number => number > 3);

// For example, modifying the source collection before the enumeration of the result collection will also modify the realized result
numbers.Add(6);

// Iterating the result using ToList.
// Realizes: 4, 5, 6! The 6 is added because the source collection 
// was modified before the result has been realized. 
// This resulting behavior is important to know as it can lead to "unexpected" 
// side effects if you pass around the returned query result (the `IEnumerable<T>`) 
// while the source collection is modified. 
// We can also make use of this feature, now that we know that 
// the `IEnumerable<T>` will be kept in sync with the source collection.
List<int> filteredNumbers = filteredNumbersQuery.ToList(); // Realizes: 4, 5, 6!

// Because LINQ also uses "lazy evaluation" 
// the following chained enumeration results in a single iteration (and not two).
// In other words, each value passes directly the chain of queries
// (instead of each query being applied to the complete collection):
IEnumerable<string> filteredTextNumbersQuery = numbers
  .Where(number => number > 3) // First query
  .Select(number => number.ToString()); // Second query

// Because of lazy evaluation 
// the chained query results in a single iteration
// once we realize the deferred query e.g. by calling ToList.
List<string> filteredTextNumbers = numbers.ToList(); // Single iteration realizes: "4", "5", "6"