Generic variance and List(Of T)

This post has been sitting in my drafts for a while, so I thought I should post it, mainly because I want to talk about this and generic variance and arrays in more detail in the days ahead.  The reason this post was put on hiatus was I was waiting for my article on arrays to appear in Visual Studio Magazine, as that deals with many of the details as to why this works 😉

 

Have you ever wanted to cast a List(Of Customer) to a List(Of BusinessBase), where Customer Inherits BusinessBase, only to find that you can’t… well you can 😉

 

This extension will return an IList of BusinessBase for an input of a List(Of Customer).  Be aware it is the underlying array, so you will need to get the actual count from the original input.

 
   <Runtime.CompilerServices.Extension()> _
   Function ToIList(Of T, TBase)(ByVal list As List(Of T)) As IList(Of TBase)
      Dim fi = GetType(List(Of T)).GetField("_items", Reflection.BindingFlags.Instance Or _
                                                      Reflection.BindingFlags.GetField Or _
                                                      Reflection.BindingFlags.NonPublic)
      Return CType(fi.GetValue(list), IList(Of TBase))
   End Function

 

 

And a quick test:

 
 
Module Module1
 
   Sub Main()
      Dim myApples As New List(Of Apple)
      myApples.Add(New Apple With {.Name = "Golden Delicious"})
      myApples.Add(New Apple With {.Name = "Red Delicious"})
      myApples.Add(New Apple With {.Name = "Granny Smith"})
 
 
      Dim fruits As IList(Of Fruit) = myApples.ToIList(Of Fruit)()
 
      For i = 0 To myApples.Count - 1
         fruits(i).Name = "Fruit : " & fruits(i).Name
      Next
 
      For Each a In myApples
         Console.WriteLine(a.Name)
      Next
 
      Console.WriteLine("finished")
 
      Console.ReadLine()
 
   End Sub
 
 
 
   <Runtime.CompilerServices.Extension()> _
   Function ToIList(Of T, TBase)(ByVal list As List(Of T)) As IList(Of TBase)
      Dim fi = GetType(List(Of T)).GetField("_items", _
                                         Reflection.BindingFlags.Instance Or _
                                         Reflection.BindingFlags.GetField Or _
                                         Reflection.BindingFlags.NonPublic)
      Return CType(fi.GetValue(list), IList(Of TBase))
   End Function
 
End Module
 
 
 
 
Class Fruit
 
 
   Private _Name As String
 
   Public Property Name() As String
      Get
         Return _Name
      End Get
      Set(ByVal value As String)
         _Name = value
      End Set
   End Property
 
 
End Class
 
Class Apple : Inherits Fruit
 
End Class


4 Comments so far

  1.   David M. Kean on August 8th, 2008          

    Be aware that you should *never* rely on the internal and private members of types. These are free to change from version to version (even in service packs). Instead, simply use the Enumerable.Cast() method from System.Linq instead.

  2.   bill on August 8th, 2008          

    Hi David,

    Enumerable.Cast(Of T) is completely different. It returns only an IEnumerable(Of T), not an IList(Of T). The point of this example is to show generic variance of arrays, not creating new iterators.

    And yes it is unfortunate that List(Of T) doesn’t let any derived class get at the internal store, hence the need for reflection.

  3.   Hoop Somuah on August 10th, 2008          

    You seem to be piggybacking on the fact that Arrays i nthe CLR are covariant and that they implement IList and that List uses an array for internal storage. That last one could change.

    Why wouldn’t you write yiour own implementation of Cast for IList that wraps the list (if necessary). If you got fancy enough, you could avoid recursive wrapping which is one concern and IT wouldn’t require you to rely on the underlying store being an Array. The only reason Arrays in the .Net CLR support covariance is because Java supports it and running Java on the CLR was desired.

    BTW: What would you expect to have happen if someone Casts IList to IList and then passes that to a function that expects and IList which then proceeds to try and add an Orange to the list? Seems like a messy thing to support generaly in the framework.

    In your case you’ll get an ArrayTypeMismatchException.

    I wonder how the casting to IList is implemented internally by Arrays in the CLR. I can cast a strin[] to IListwithout a problem…

  4.   bill on August 10th, 2008          

    Hi Hoop,

    Yes the above is just messing with the generic varaince of arrays. It’s fragile, and I really don’t recommend it.

    As to your question on what happens when you have an IList(Of Apple) cast to IList(Of Fruit), that issue exists today with arrays anyway. Anywhere you have a parameter defiend as IList(Of T) you should be checking for the possibility of array variance if you want to avoid throwing the mismatch exception.

    The same applies to ICollection(Of T), although there you are more likely ot hit a not implemented exception rather than have any variance issues.