What you want is basically called covariance. You’re expecting that you should be able to assign a more derived generic type’s instance to that of a less derived generic. This is similar to how you’re able to assign an array of strings to a variable that’s typed as an array of objects. The code as posted doesn’t work, as you said, but something close can be approximated - there are at least two ways to approach this.
Note that it’s C# convention to capitalize the generic type parameter name.
First approach: if you’re only going to be reading from the list (and your writing logic is still based on specific instances, you can specify something like this.
IAttMgr<GrandAttr> mgr = new SpecialAttMgr();
public interface IAttMgr<out T> where T : IAttribute
{
T GetAttributeByName(string name);
}
public class GeneralAttMgr<T> : MonoBehaviour, IAttMgr<T> where T : IAttribute
{
public List<T> AttributesList;
public T GetAttributeByName(string name)
{
foreach (var att in AttributesList)
if (att.AttName == name)
return att;
return default;
}
}
public class CommonAttMgr : GeneralAttMgr<CommonAttr>
{
}
public class SpecialAttMgr : GeneralAttMgr<SpecialAttr>
{
}
public interface IAttribute
{
string AttName { get; set; }
}
public class GrandAttr : IAttribute
{
public string AttName { get; set; }
public Sprite AttSprite { get; set; }
}
public class CommonAttr : GrandAttr
{
public string CommonString { get; set; }
}
public class SpecialAttr : GrandAttr
{
public string SpecialString { get; set; }
}
In this example, I’m using an interface that specifically applies the out modifier to the type parameter, making it so that for a variable typed as IAttrMgr, any object of a type that implements IAttrMgr or IAttrMgr can be assigned to that variable.
The 2 main limitations with this is that you effectively lose write access when using a variable using the interface type, since the out modifier makes it so the type parameter’s type can only be used for method returns, though you can still do whatever you want with the instance inside the class itself or with a variable that’s at least as specific as GeneralAttMgr. The second limitation is that as I understand it, you also lose Unity’s semantics with null checking if the reference isn’t of a type derived from UnityEngine.Object, though you can still do something along the lines of this:
if (attMgr is UnityEngine.Object attMgrObj && attMgrObj != null)
Second approach: you can use a non-generic base class for the manager type.
GeneralAttMgr AttMgr = new CommonAttMgr();
public abstract class GeneralAttMgr : MonoBehaviour
{
public abstract TValue GetAttributeByName<TValue>(string name) where TValue : IAttribute;
}
public class GeneralAttMgr<T> : GeneralAttMgr where T : IAttribute
{
public List<T> AttributesList;
public override TValue GetAttributeByName<TValue>(string name)
{
/*foreach (var att in AttributesList.OfType<TValue>())
if (att.AttName == name)
return att;*/
foreach (var att in AttributesList)
if (att is TValue att2 && att2.AttName == name)
return att2;
return default;
}
}
public class CommonAttMgr : GeneralAttMgr<CommonAttr>
{
}
public class SpecialAttMgr : GeneralAttMgr<SpecialAttr>
{
}
public interface IAttribute
{
string AttName{get;set;}
}
public class GrandAttr : IAttribute
{
public string AttName { get; set; }
public Sprite AttSprite { get; set; }
}
public class CommonAttr : GrandAttr
{
public string CommonString { get; set; }
}
public class SpecialAttr : GrandAttr
{
public string SpecialString { get; set; }
}
In this scenario, instead of using the same type parameter for the stored list and the return for GetAttributeByName, you simply make GetAttributeByName a typed method itself, and use the type testing operator to check each stored element in the list. If you had a GeneralAttrMgr variable that referenced a CommonAttMgr, you’d want to call GetAttributeByName to get any attributes of type GrandAttr (or derived) or GetAttributeByName to get any attributes of type CommonAttr (or derived). I don’t pretend to understand the entirety of what your code will be doing from just the code posted here, the choice would be up to you.
In general, I’d also recommend that you also implement the Try method pattern where your method returns a bool for success instead of returning default, especially for reference types. That would look like this:
public bool TryGetAttributeByName<TValue>(string name, out TValue value)
This simplifies cases where you need to do something with a valid instance of something and want to bail out to another code path when the thing you wanted doesn’t exist.