C
C#11mo ago
Clonkex

✅ 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?
public static readonly Dictionary<Type, List<Entity>> EntitiesByType = new();

public static List<T> GetEntitiesByType<T>() where T : Entity
{
if(EntitiesByType.TryGetValue(typeof(T), out var entities))
{
return entities;
}
return new();
}
public static readonly Dictionary<Type, List<Entity>> EntitiesByType = new();

public static List<T> GetEntitiesByType<T>() where T : Entity
{
if(EntitiesByType.TryGetValue(typeof(T), out var entities))
{
return entities;
}
return new();
}
I can't get my head around why this isn't allowed.
25 Replies
LPeter1997
LPeter199711mo ago
So entities is List<Entity>, right?
Clonkex
Clonkex11mo ago
Ah yes, sorry. Or more specifically, List<SomeTypeDerivedFromEntity>
LPeter1997
LPeter199711mo ago
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?
Clonkex
Clonkex11mo ago
Edited. I really did not include enough information lol
LPeter1997
LPeter199711mo ago
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 Ts 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())
JakenVeina
JakenVeina11mo ago
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
LPeter1997
LPeter199711mo ago
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)
JakenVeina
JakenVeina11mo ago
it amounts to the same thing, either way it's a cast
LPeter1997
LPeter199711mo ago
It's a less nasty cast than if we have to go the full type-map route
JakenVeina
JakenVeina11mo ago
which is... not insane
Clonkex
Clonkex11mo ago
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.
JakenVeina
JakenVeina11mo ago
but you DO need to modify them, internally?
LPeter1997
LPeter199711mo ago
In that case, I'd do this:
public static IEnumerable<T> GetEntitiesByType<T>() where T : Entity
{
if (!EntitiesByType.TryGetValue(typeof(T), out var entities))
{
entities = new List<Entity>();
EntitiesByType.Add(typeof(T), entities);
}
return entities.OfType<T>();
}
public static IEnumerable<T> GetEntitiesByType<T>() where T : Entity
{
if (!EntitiesByType.TryGetValue(typeof(T), out var entities))
{
entities = new List<Entity>();
EntitiesByType.Add(typeof(T), entities);
}
return entities.OfType<T>();
}
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 😄
JakenVeina
JakenVeina11mo ago
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
Clonkex
Clonkex11mo ago
So I didn't realise this (assume BeachBallEntity : Entity):
T thing = (T)new Entity(); // Allowed
Entity thing2 = new T(); // Allowed
T thing3 = (T)new BeachBallEntity(); // Not allowed?
T thing = (T)new Entity(); // Allowed
Entity thing2 = new T(); // Allowed
T thing3 = (T)new BeachBallEntity(); // Not allowed?
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 post
JakenVeina
JakenVeina11mo ago
the 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)
LPeter1997
LPeter199711mo ago
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
JakenVeina
JakenVeina11mo ago
in a non-generic context, the compiler has to KNOW that the cast function exists, to encode a call to it
JakenVeina
JakenVeina11mo ago
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
Clonkex
Clonkex11mo ago
ah true
JakenVeina
JakenVeina11mo ago
and even if there was, that doesn't cover ALL possible T's
Clonkex
Clonkex11mo ago
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:
T thing3 = (T)new BeachBallEntity();
T thing3 = (T)new BeachBallEntity();
...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 lol
TheRanger
TheRanger11mo ago
try 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 instances
Clonkex
Clonkex11mo ago
you can't expect the compiler to allow you to return a list that has the possibility to contain FootBallEntity instances
Oh... yeah... that's super obvious. Idk why I didn't realise that immediately. Thanks