Fundamentally, the issue with your code is that you are trying to return a reference to the wrong member of the object. You are returning the member with the dynamic
type, when you actually need the member having the type T
.
Your original example is equivalent to this simpler one:
class Base
{
public int Id;
public dynamic Value;
public Base(int id, dynamic value)
{
Id = id;
Value = value;
}
public override string ToString() => $"{{Id: {Id}, Value: {Value}}}";
}
class Derived<T> : Base
{
public new int Id;
public new T Value;
public Derived(int id, T value) : base(id, value) { }
}
static void Main(string[] args)
{
Derived<string>[] array = new[]
{
new Derived<string>(0, "Zero"),
new Derived<string>(1, "One"),
new Derived<string>(2, "Two"),
};
ref string valueRef = ref GetElement<string>(array, 1);
valueRef = "Three";
WriteLine(string.Join<Base>(Environment.NewLine, array));
}
private static ref T GetElement<T>(Base[] array, int id)
{
// error CS8151: The return expression must be of type 'T' because this method returns by reference
return ref array.Where(x => x.Id == id).First().Value;
}
The problem is simply that you are trying to return the base class field, which has the type dynamic
rather than T
.
To fix that, you need to cast the object so that you get the field from the derived type instead:
private static ref T GetElement<T>(Base[] array, int id)
{
return ref ((Derived<T>)array.Where(x => x.Id == id).First()).Value;
}
Or, in your original example:
public ref T GetComponentOnEntity<T>(Entity entity) where T : new()
{
return ref ((Component<T>)components.Where(x => x.EntityID == entity.Id && x.Type == typeof(T)).First()).Value;
}
Now, all that said: I urge you to rethink the design. Perhaps you've omitted some code that initializes the base Value
field identically to the derived Value
field. But even if so (and that's wasteful enough), there's nothing that prevents those fields from being assigned differently later on. It's not just wasteful, it's also likely to lead to bugs to have an object that has two copies of every value in it, especially when those values are not read-only.
Member hiding (i.e. using the new
keyword as you have in your data structures) should be avoided at all costs. It only leads to confusion and will almost certainly result in any number of hard-to-find, hard-to-fix bugs later down the road. The issue you've run into here is just the tip of the iceberg, but is a good example of how confusing things can be when an object has two different members that have the same name.
In addition, ref return values should be used very sparingly, and only when absolutely necessary (e.g. a critical performance issue that can be fixed only using ref return). There's context missing from your question of course, and so maybe you do have a really good reason for using that feature here. But just based on the names in the code that's present, it seems likely to me that you could get the code to work just fine without either of the oddities in the code now (the member hiding and the ref return values).