This has been bugging me a little because it seems so peculiar so I've done a little research. Skimming through the
C# language spec it seems that the whole point of generics is to remove the very behavior that we are used to seeing. Reducing the costs associated with boxing/unboxing objects seems to supersede the convenience of inheritance in the case of generics. At least that's what the language in the spec seems to imply, though the example under disscussion is never explicitly addressed.
In a pre-generics world I would have expected a Generic.List(Of Employee) to be the types:
- Generic.List(Of Employee)
- Generic.List(Of Person) because an employee is a person
- Generic.List(Of Object) because everything inherits from object.
But when you think about the problem that generics are meant to solve, specifically
1. Finding compile time errors before they become runtime errors (preventing bad cast exceptions) and
2. Decreasing the performance impact of boxing/unboxing specific types to and from Object (by adding an Employee to a regular ol' ArrayList for example)
then really it doesn't make sense to suddenly be able to start casting objects from a generic list.? By reason two, you'd be allowing the very thing you are trying to prevent. And by reason 1, you've already shot yourself in the foot by declaring your generic list as an object.
So, for the kind of flexibility you might want while still dealing with strongly typed lists, in an example similar to this where you have a well understood base class with multiple inheriting classes, it looks like you have to implement a strongly typed list the "old way", by inheriting from CollecitonBase.
At the risk of making this post way too long, I'm going to propose a new way of thinking for generics.
One of the things you can do with generics is enforce constraints on types that a generic class accepts. Instead of casting an employee to a person (and taking the casting hit) you declare your generic list so that it only accepts objects that implement the person interface or person class.
I'm going to flesh the example out a little bit further to show the way I think generics are intended to be used to get what we want.
Visual Basic:
Class Person
Public ReadOnly Property name() As String
Get
Return "Generic Joe"
End Get
End Property
End Class
'
Class Employee
Inherits Person
Public ReadOnly Property Job() As String
Get
Return "Make things difficult when learning generics."
End Get
End Property
End Class
'
Class PersonList(Of T As Person)
Inherits List(Of T)
End Class
When we do the same gambit of tests from the fist post, this time with the personList, we'll still get the same results. The reason this is ok, is because now we know that no matter what happens, whatever comes out of that list MUST be a person as well as whatever it actually is. It's not quite the same as being able to cast, but remember that the whole point is to avoid casting around so I think this is the best we can do.
The only problem I see with this is that you aren't going to be able to create a single method that you can hand a generic list Of Employees that will treat them as a generic list Of Person so you'll be stuck overloading out the wazoo to handle each kind of PersonList you need to handle, right? Luckily, the dev put one more trick up our sleeves to help us out: Generic.List.ConvertAll()
Visual Basic:
Private Sub Button2_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button2.Click
Dim employees As New EmployeeList(Of Employee)
employees.Add(New Employee)
'
MessageBox.Show(employees(0).name)
MessageBox.Show(employees(0).Job)
'
Dim people As Object = employees.ConvertAll(New Converter(Of Employee, Person)(AddressOf EmployeeToPerson)) ''normally, you'd strongly type, of course.
'
Dim isSameType = TypeOf people Is Generic.List(Of Employee)
Console.WriteLine(isSameType.ToString) ' <-- Returns False
'
isSameType = TypeOf people Is Generic.List(Of Person)
Console.WriteLine(isSameType.ToString) ' <-- Returns True
'
End Sub
'
Private Function EmployeeToPerson(ByVal worker As Employee) As Person
Return DirectCast(worker, Person)
End Function
So, now with the power to convert a generic type to whatever we need it to be (in this case the base class) we can still write a method that will take a list of people and use it on lists of employees, clients, whatever. I think this is the best way to use generics while dealing with the constraints that generics place on the types they hold. It's not quite the same as we are used to, but it is certainly similarly enough. And think about all the extra compile time type safety you get...
From a best practices standpoint, where would you put your conversion function and how might you do it? I would be inclined to put the above conversion method in the Employee class and make it static. Any other thougts?