Search Unity

Feedback Covariance in GetComponent

Discussion in 'Scripting' started by lordofduct, Jan 16, 2022.

  1. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,539
    In newer versions of C# (specifically C# 4 and newer) there is covariance with the IEnumerable<T>/IList<T>/etc.

    Basically if I have a List<Collider>, I can cast it to a IEnumerable<Component>... this is covariance. (in older versions of C#, it was only supported by Array).

    Tonight... I learned that Unity doesn't support covariance in the 'GetComponent' method. Basically I was attempting to use IEnumerable<T> with GetComponent. I wanted to be able to have a Component be a "DataSource" for a list display that I've coded (I was doing this with ScriptableObjects up until now but was like, oh, it'd be nice to wrap this SO with a Component that augments it at runtime with some filters). In the implementation I went to retrieve the component from the GameObject via GetComponent, but since the component was covariant from the type I wanted, it failed to return anything.

    It's not a deal breaker of course... I have many work arounds for it. But it would be nice to have.

    ....

    For a bare bone example here is some code:
    Code (csharp):
    1.     public class CovarianceGetComponentExample : MonoBehaviour, IEnumerable<BoxCollider>
    2.     {
    3.  
    4.         private void Start()
    5.         {
    6.             var o1 = this as IEnumerable<BoxCollider>;
    7.             var o2 = this as IEnumerable<Collider>;
    8.             Debug.Log(o1 != null);
    9.             Debug.Log(o2 != null);
    10.  
    11.             var c1 = this.GetComponent<IEnumerable<BoxCollider>>();
    12.             var c2 = this.GetComponent<IEnumerable<Collider>>();
    13.             Debug.Log(c1 != null);
    14.             Debug.Log(c2 != null);
    15.         }
    16.  
    17.         IEnumerator<BoxCollider> IEnumerable<BoxCollider>.GetEnumerator()
    18.         {
    19.             yield break; //doesn't matter, we're just testing covariance
    20.         }
    21.  
    22.         System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
    23.         {
    24.             yield break; //doesn't matter, we're just testing covariance
    25.         }
    26.     }
    In this you get:
    True
    True
    True
    False

    But I would like it to be all True.

    In my actual use case it's not BoxCollider/Collider. It's a data container called 'IAssignment' and 'IState' where IAssignment inherits from IState. Basically I want to retrieve a datasource that hands out these state objects, loops them, creates a visual element for each one, populates its textfields, and then shoves them in a scrolling container.

    Like I said... I have work arounds for now. Just one of those things I was unaware of until tonight and it took a me moment to figure out why it wasn't working. And was like "huh... well darn", and decided to post a request/feedback about it.
     
  2. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,539
    I just realized I didn't mention... I'm on Unity 2020.3.22f1

    I am unaware if newer versions support this. And I'm kind of tight on my deadline right now so I can't really go rummaging around in newer versions. If anyone is on a new version and notices it does work, let me know, that'd be super helpful to know.
     
  3. koirat

    koirat

    Joined:
    Jul 7, 2012
    Posts:
    2,074
    What I can say is that testing it on 2021.1.17f1
    Gives the same result as your test.

    It would be quite surprising if this will be implemented, after all GetComponent is generally working on types and derived types.


    If I can allow myself some suggestions I would also not obtain non component derived types using GetComponent, and not add any interface to MonoBehaviour.
     
  4. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,539
    I humbly disagree.

    Unity used to not support interfaces with GetComponent years ago, and I had a work around for that too back then. They've since added interface support to GetComponent.

    And I, as well as many others, use interfaces quite a bit with our components. Just dig through the UnityEngine.UI namespace, it's got interfaces all over the place for dealing with UI.

    Or how this works:
    https://docs.unity3d.com/2019.1/Documentation/ScriptReference/EventSystems.IDragHandler.html
     
  5. Bunny83

    Bunny83

    Joined:
    Oct 18, 2010
    Posts:
    4,011
    Well, that's kinda strange. Though we don't know exactly how Unity does check / test components against the type argument. It seems they don't use the usual IsAssignableFrom because it seams it should support covariance out of the box. Since most of the GetComponent logic is on the native side, they may check for interfaces manually and for sure do not check for covariance in that case.

    Maybe you should file a bug report. Though I doubt this would have a high priority ^^. I would say 99% of Unity users do not even know about co and contravariance. And from that 1% that do only a fraction would need support in GetComponent ^^. The simplest workaround would be to include an empty dummy interface that you would use to do your component fetching. You can always do the IEnumerable casting yourself afterwards.

    ps: My test project is currently on version 2021.1.24f1 and I get the same result as you got. So it's still not really supported.

    pps: a dirty workaround would be to implement your own GetComponent method that uses GetComponents and does the filtering manually. This does work:
    Code (CSharp):
    1.     public static T MyGetComponent<T>(this GameObject obj)
    2.     {
    3.         var components = obj.GetComponents<Component>();
    4.         foreach(var comp in components)
    5.         {
    6.             if (comp is T res)
    7.                 return res;
    8.         }
    9.         return default(T);
    10.     }
    11.  
    Of course the performance would be quite bad as GetComponents allocates an array. Though you could use the newer List version with a static list:

    Code (CSharp):
    1.  
    2.     static List<Component> tmpComponents = new List<Component>();
    3.     public static T MyGetComponent<T>(this GameObject obj)
    4.     {
    5.         tmpComponents.Clear();
    6.         try
    7.         {
    8.             obj.GetComponents<Component>(tmpComponents);
    9.             foreach (var comp in tmpComponents)
    10.             {
    11.                 if (comp is T res)
    12.                     return res;
    13.             }
    14.         }
    15.         finally
    16.         {
    17.             tmpComponents.Clear();
    18.         }
    19.         return default(T);
    20.     }
    21.     public static T MyGetComponent<T>(this Component comp)
    22.     {
    23.         return comp.gameObject.MyGetComponent<T>();
    24.     }
     
    lordofduct likes this.
  6. Bunny83

    Bunny83

    Joined:
    Oct 18, 2010
    Posts:
    4,011
    As far as I remember Unity actually did support interfaces even back then, however only the System.Type version did. The generic version did not because the generic type parameter had a constraint to Component in the past. This constraint seemed reasonable at first glance since GetComponent is supposed to only return components. Though they haven't thought about interfaces at that time. Later they simply removed the constraint and now the generic version works with interfaces as well. Though as I said, they probably did not think about covariance :)
     
  7. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,539
    I'm not super concerned, hence why I just flagged it as feedback here.

    That's actually one of my work arounds that I considered doing if this pops up again in the future. Though in mine I use my "TempCollection" that has recyclable lists to avoid the array cost:

    Code (csharp):
    1.         public static T GetComponentWithCovariance<T>(this GameObject go) where T : class
    2.         {
    3.             if(typeof(T).IsInterface) //slight optimization since concrete covariance doesn't work in C#, think List<BoxCollider> vs List<Collider>
    4.             {
    5.                 using (var lst = TempCollection.GetList<Component>())
    6.                 {
    7.                     go.GetComponents<Component>(lst);
    8.                     foreach(var c in lst)
    9.                     {
    10.                         if (c is T tc) return tc;
    11.                     }
    12.                 }
    13.                 return null;
    14.             }
    15.             else
    16.             {
    17.                 return go.GetComponent<T>();
    18.             }
    19.         }
    This relies on this:
    https://github.com/lordofduct/space...ore/Runtime/src/Collections/TempCollection.cs

    https://github.com/lordofduct/space...uppy.core/Runtime/src/Collections/TempList.cs

    But I didn't actually use this as my workaround for now... in 10+ years of using Unity this is the first and only time it's shown itself (hence the low-priority).

    And I just dealt with it by implementing both interfaces:
    Code (csharp):
    1. using UnityEngine;
    2. using System.Collections.Generic;
    3.  
    4. using com.spacepuppy;
    5. using com.spacepuppy.Utils;
    6. using System.Linq;
    7. using com.vivarium.Factories;
    8.  
    9. namespace com.vivarium.Entities.UI.Menus
    10. {
    11.     public class GoalsUIController : SPComponent, IEnumerable<IAssignment>, IEnumerable<IAssetState>
    12.     {
    13.  
    14.         #region Fields
    15.  
    16.         [SerializeField]
    17.         [TypeRestriction(typeof(IEnumerable<IAssignment>), AllowSceneObjects = true)]
    18.         private UnityEngine.Object _dataProvider;
    19.  
    20.         #endregion
    21.  
    22.         #region Properties
    23.  
    24.         public IEnumerable<IAssignment> DataProvider
    25.         {
    26.             get => _dataProvider as IEnumerable<IAssignment>;
    27.             set => _dataProvider = value as UnityEngine.Object;
    28.         }
    29.  
    30.         #endregion
    31.  
    32.         #region IEnumerable Interface
    33.  
    34.         public IEnumerator<IAssignment> GetEnumerator()
    35.         {
    36.             var e = this.DataProvider ?? Services.Get<VivariumAssignments>().AllAssignments;
    37.  
    38.             //TODO - filter
    39.             return e.GetEnumerator();
    40.         }
    41.  
    42.         System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
    43.         {
    44.             return this.GetEnumerator();
    45.         }
    46.  
    47.         IEnumerator<IAssetState> IEnumerable<IAssetState>.GetEnumerator()
    48.         {
    49.             return this.GetEnumerator();
    50.         }
    51.  
    52.         #endregion
    53.  
    54.     }
    55. }
    And what was relying on it:
    Code (csharp):
    1. using UnityEngine;
    2. using UnityEngine.AddressableAssets;
    3. using System.Collections.Generic;
    4. using System.Linq;
    5.  
    6. using com.spacepuppy;
    7. using com.spacepuppy.Events;
    8. using com.spacepuppy.Utils;
    9. using Cysharp.Threading.Tasks;
    10. using com.vivarium.Factories;
    11. using com.spacepuppy.Addressables;
    12.  
    13. namespace com.vivarium.UI.Stampers
    14. {
    15.  
    16.     [Infobox("If 'Stamper Prefab' is left blank the default stamper for the asset type will be used, if that stamper is defined (see: GameSettings in Resources).")]
    17.     public class i_ApplyAssetStamper : AutoTriggerable
    18.     {
    19.  
    20.         #region Fields
    21.  
    22.         [SerializeField]
    23.         [TypeRestriction(typeof(IEnumerable<IAssetState>), typeof(IEnumerable<IAssetTemplate>), typeof(IEnumerable<IAssetSource>), AllowSceneObjects = true)]
    24.         private UnityEngine.Object _dataProvider;
    25.  
    26.         [SerializeField]
    27.         private int _maxVisible = 100;
    28.  
    29.         [SerializeField]
    30.         [DefaultFromSelf]
    31.         private Transform _container;
    32.  
    33.         [SerializeField]
    34.         [Tooltip("If blank, uses default stamper defined in GameSettings.")]
    35.         private AssetReferenceGameObject _stamperPrefab;
    36.  
    37.         #endregion
    38.  
    39.         #region CONSTRUCTOR
    40.  
    41.         public i_ApplyAssetStamper()
    42.         {
    43.             this.ActivateOn = ActivateEvent.OnStartOrEnable;
    44.         }
    45.  
    46.         #endregion
    47.  
    48.         #region AutoTriggerable Interface
    49.  
    50.         public override bool CanTrigger => base.CanTrigger && _dataProvider;
    51.  
    52.         public override bool Trigger(object sender, object arg)
    53.         {
    54.             if (!this.CanTrigger) return false;
    55.  
    56.             switch(_dataProvider)
    57.             {
    58.                 case IEnumerable<IAssetState> states:
    59.                     _ = this.DoAddStampers(_container, states.OrderBy(o => o?.Template?.DisplayName), _stamperPrefab);
    60.                     return true;
    61.                 case IEnumerable<IAssetTemplate> templates:
    62.                     _ = this.DoAddStampers(_container, templates.OrderBy(o => o?.DisplayName), _stamperPrefab);
    63.                     return true;
    64.                 case IEnumerable<IAssetSource> sources:
    65.                     _ = this.DoAddStampers(_container, sources.Select(o => o.AssetState).OrderBy(o => o?.Template?.DisplayName), _stamperPrefab);
    66.                     return true;
    67.                 case IEnumerable<object> e:
    68.                     _ = this.DoAddStampers(_container, e, _stamperPrefab);
    69.                     return true;
    70.                 case System.Collections.IEnumerable e:
    71.                     _ = this.DoAddStampers(_container, e.Cast<object>(), _stamperPrefab);
    72.                     return true;
    73.                 default:
    74.                     return false;
    75.             }
    76.         }
    77.  
    78.         private async UniTaskVoid DoAddStampers(Transform container, IEnumerable<object> dataProvider, AssetReferenceGameObject defaultprefabref)
    79.         {
    80.             if (container && container.childCount > 0)
    81.             {
    82.                 foreach (Transform t in container)
    83.                 {
    84.                     t.gameObject.Kill();
    85.                 }
    86.             }
    87.  
    88.             int index = 0;
    89.             foreach (var item in dataProvider.Take(_maxVisible))
    90.             {
    91.                 GameObject inst = null;
    92.                 if(defaultprefabref.IsConfigured())
    93.                 {
    94.                     inst = await defaultprefabref.InstantiateSPManagedAsync<GameObject>(container);
    95.                 }
    96.                 else
    97.                 {
    98.                     var prefabref = Game.Settings.FindDefaultStamper(item);
    99.                     if(prefabref.IsConfigured())
    100.                     {
    101.                         inst = await prefabref.InstantiateSPManagedAsync<GameObject>(container);
    102.                     }
    103.                 }
    104.                 if (inst == null) continue;
    105.  
    106.                 inst.Broadcast((item, index), _stampFunctor, true, true);
    107.                 index++;
    108.             }
    109.         }
    110.         private static readonly System.Action<IStamper, System.ValueTuple<object, int>> _stampFunctor = (s, t) => s.Stamp(t.Item1, t.Item2);
    111.  
    112.         #endregion
    113.  
    114.     }
    115.  
    116. }
    The whole "GetComponent" issue was actually in my editor... that code is, well... lets just say not easily shown here. I have a custom ObjectField for selecting objects from the scene/assets if they're either a) of type interface or b) filtered by more than 1 type.
     
    Last edited: Jan 16, 2022
  8. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,539
    It was back and forth a lot until they finally made it completely supported in both.

    I was doing this back in Unity 4ish I think... kept breaking between versions when I'd rely on the System.Type overload and so I just went pure retrieve all components and look through them if it was an interface. Other stuff annoyingly swapped back and forth... like Vector3 would randomly lose the 'System.Serializable' attribute... it'd continue working in the unity serializer, but the .Net serializer would stop working between versions. I wrote a surrogate to deal with that as well back then. I actually still use that surrogate cause I'm paranoid on updates, lol.

    Note, I've been using Unity since even before I joined the forums. I think I hopped on in 2008/9 or so.
     
    Last edited: Jan 16, 2022
  9. Bunny83

    Bunny83

    Joined:
    Oct 18, 2010
    Posts:
    4,011
    I started with Unity version 2.6 which was around 2009 / 10. Though at that time I haven't used much interfaces :)
     
    lordofduct likes this.
  10. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,539
    Yeah, those early days were... a janky yet promising time for Unity.

    My partner and I have been with it for so long now that we can't leave... like literally, we have some much tech invested into unity starting over in something like Unreal is not possible.
     
    Bunny83 likes this.
  11. Bunny83

    Bunny83

    Joined:
    Oct 18, 2010
    Posts:
    4,011
    :) Just out of curiosity I just downloaded the earliest Unity version they have in the archive (which is Unity 3.4). I just tried this one:

    Code (CSharp):
    1.     private void Start()
    2.     {
    3.         var o1 = this as IEnumerable<BoxCollider>;
    4.         var o2 = this as IEnumerable<Collider>;
    5.         Debug.Log(o1 != null);
    6.         Debug.Log(o2 != null);
    7.  
    8.         var c1 = (IEnumerable<BoxCollider>)this.GetComponent(typeof(IEnumerable<BoxCollider>));
    9.         var c2 = (IEnumerable<Collider>)this.GetComponent(typeof(IEnumerable<Collider>));
    10.         Debug.Log(c1 != null);
    11.         Debug.Log(c2 != null);
    12.     }
    13.  
    It compiles and runs without errors The results are

    Code (CSharp):
    1. true
    2. false
    3. true
    4. false
    So Covariance was not supported back then. However GetComponent did work with interfaces just fine, even one with generic arguments. It's just a System.Type and that has always worked as far as I can remember.
     
  12. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,539
    My memory might be rusty.

    But like I said... it was one of those things that flipped back and forth on me between versions. I generally stayed on bleeding edge builds back then and between revisions junk would just "poof" and then reappear the next revision. Just like the Serializable attribute on Vector3. It was just one of those weird oddities that randomly popped up on me enough that it stuck in my memory.

    But I mean, when you stay on bleeding edge versions, that sort of stuff is to be expected. Some code commit gets retracted, or some merge blows out a random line, it's not caught by QA until the next revision. So it goes.

    Regardless, my original statement was more about the GetComponent<T> version, and not GetComponent(System.Type) version.