✅ Return List<Entity> from method with return type of List<T> where T : Entity
This is probably a dumb question, but why can't I do this?
I can't get my head around why this isn't allowed.
25 Replies
So
entities
is List<Entity>
, right?Ah yes, sorry. Or more specifically,
List<SomeTypeDerivedFromEntity>
Well since this looks like a dictionary value, I suppose you shoved them all in a
List<Entity>
, so your dict is likely Dictionary<Type, List<Entity>>
, right?Edited. I really did not include enough information lol
What you are asking for here is not unheard of, it's called variance, when
T<U>
can become T<V>
in some cases where U
and V
are in some relation (based on the nature of the relation this is either called co- or contravariance)
BUT C# only supports variance where it's more or less safe (except arrays but that's legacy behavior at this point)
The problem is, you instantiated a List<Entity>
. Let's assume you want to promise it only stores some derived type T
of Entity
and you lend it out as List<T>
. The problem is, internally this could have contained any Entity
, you can not be sure it's not just T
s in there, breaking the type system
What you want can be solved tho, just with a bit more type erasure, or with collection manipulation. What's the end goal of this method, where would it be used?
Is it going to be called often, are entities of a certain type often queried? Are there many kinds of entities?
Oh also, do you want to allow the user calling this to mutate the list, like adding and removing entities?
So from the outside, do you expect me to do GetEntitiesByType<Foo>().Add(new())
if it's returning List<T> instead of IReadOnlyList<T> I'd say we have to assume so
it probably doesn't make any difference, unless this is being used in a parallel context
I wanna be sure before I propose a solution because if not, it's easily fixed by returning
.OfType<T>()
(with IEnumerable<T>
as a return type)it amounts to the same thing, either way
it's a cast
It's a less nasty cast than if we have to go the full type-map route
which is... not insane
No it's not intended to be modifed from outside. I was planning to use
IEnumerable
or IReadOnlyList
, I just didn't get that far.but you DO need to modify them, internally?
In that case, I'd do this:
You can also store the lists as
object
and then instantiate them as List<T>
, cast them as such when accessing them. I personally like that less 😄me, I like that more
A) I would recommend IReadOnlyList<T> over IEnumerable<T>, it exposes more info
B) .OfType<>() actually does a cast and type check per item in the list
onstead of just a single cast
neither is particularly clean though
So I didn't realise this (assume
BeachBallEntity : Entity
):
I'm not really sure why this is. Even if T
is MoreSpecificBeachBallEntity
, it's generic, so it's not like I can try to call methods that would only exist for MoreSpecificBeachBallEntity
, for instance. T
is only known to be Entity
so it should be safe to cast any subclass of Entity
to T
, should it not?
Ha, I already changed it to add an empty list to the dictionary if it doesn't exist rather than allocating a new one each time. I just didn't edit the original postthe reason is that casts are ultimately function calls
and the compiler needs to know what function call to put there when it compiles the code
(in this case, compilation of generic methods doesn't fully happen until runtime)
The first one is a runtime cast
The second is just an upcast
The third should also be a runtime cast, so I think the snippet you sent should compile technically
Your problem here really is generic variance
And that C# only supports them through interfaces and even then, only the direction where it's safe
in a non-generic context, the compiler has to KNOW that the cast function exists, to encode a call to it
(A relevant, decent piece of doc: https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/concepts/covariance-contravariance/variance-in-generic-interfaces)
in a generic context, the compiler has to know that the function WILL exist, for all T
and your generic constraints don't guarantee that
you said that T must inherit from Entity
so, if you have classes A and B that both inherit from Entity
there's no guarantee that there's a cast operator to convert A to B
ah true
and even if there was, that doesn't cover ALL possible T's
I guess I just assumed the compiler would treat
T
as being Entity
since that's the constraint. But I also don't really know how generic methods are compiled. So using this as the example:
...in my mind, T
is known to be Entity
, and BeachBallEntity
is known to be Entity
, so why am I not allowed to assign a reference to an instance of BeachBallEntity
to a variable of type T
?
But then, you mention function calls for casting and I don't know why that would be necessary either. It seems like the cast to T
could be entirely compiletime. Casting doesn't actually change the data (I assume?), it just changes what you're allowed to do with it. It seems the compiler wouldn't have to actually do anything.
On the other hand there are obviously there are some casts that have to be runtime ((T)(object)new BeachBallEntity();
, for instance). Hmm. This is quite unintuitive to me.
Ultimately it's not important, I just wanted to understand why I wasn't allowed to return List<Entity>
in a method with a return type of List<T> where T : Entity
because it surprised me.
Thanks for trying to explain it seems to be glancing off my smooth brain loltry
T thing3 = new BeachBallEntity() as T;
as for the List<Entity>
issue
your Entity List could have instances that are other than BeachBallEntity
eg FootBallEntity
if the provided T is BeachBallEntity
you can't expect the compiler to allow you to return a list that has the possibility to contain FootBallEntity
instances
it is not surprising
how ever
you can convert List<Entity> into List<T> eg into List<BeachBallEntity>
list.OfType<T>().ToList();
will remove all instances that are not T or does not inherit from T example will remove all FootBallEntity
instances, keeping only BeachBallEntity
instancesyou can't expect the compiler to allow you to return a list that has the possibility to contain FootBallEntity instancesOh... yeah... that's super obvious. Idk why I didn't realise that immediately. Thanks