Search Unity

  1. Welcome to the Unity Forums! Please take the time to read our Code of Conduct to familiarize yourself with the forum rules and how to post constructively.
  2. Dismiss Notice

Generics and Polymorphism

Discussion in 'Scripting' started by Piflik, Oct 22, 2016.

  1. Piflik

    Piflik

    Joined:
    Sep 11, 2011
    Posts:
    289
    I also asked this on the Q&A forum, but I figured this might be something to merrit more of a discussion than a quick answer.

    I am trying to create a generic class where the type parameter class has a reference to the generic 'parent'

    Something like this:

    Code (CSharp):
    1. public class A<T> where T : B {
    2.     T item;
    3.  
    4.     public A<T> (GameObject item){
    5.         this.item = item;
    6.         this.item.GetComponent<T>().parent = this;
    7.     }
    8. }
    9.  
    10. public class B {
    11.     public A<B> parent;
    12. }
    This obviously does not work, since I cannot assign an object of type A<T> to a variable of type A<B>, even though T has to be derived from B. Polymorphism of the type parameter sadly doesn't mean that the generic types are polymorphic. Is it possible to do something like this, or am I SOL?
     
    Last edited: Oct 22, 2016
  2. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,376
    It's not polymorphic, because of the way generics work.

    A<T> defines a blueprint of a type, where T is whatever it is.

    A<X> and A<Y> are distinct types, they don't inherit from a similar type (no A<T> is not a type to inherit from). Even if X and Y were polymorphic, A<X> and A<Y> are not, because they don't inherit from the same types (regardless of X and Y).

    Now, if you made them inherit from the same type... THEN they would become polymorphic to that type. The other option is to have them implement the same interface.

    Examples:

    With an abstract base class (good if your base class needs to implement any code)
    Code (csharp):
    1.  
    2.     public abstract class A
    3.     {
    4.         public abstract B Item { get; }
    5.     }
    6.  
    7.     public class A<T> : A where T : B
    8.     {
    9.         private T item;
    10.  
    11.         public A(T item)
    12.         {
    13.             this.item = item;
    14.             this.item.parent = this;
    15.         }
    16.  
    17.         public override B Item
    18.         {
    19.             get { return item; }
    20.         }
    21.     }
    22.  
    23.     public class B
    24.     {
    25.         public A parent;
    26.     }
    27.  
    With an interface (good if no implementation in base type is needed)
    Code (csharp):
    1.  
    2.     public interface IA
    3.     {
    4.         B Item { get; }
    5.     }
    6.  
    7.     public class A<T> : IA where T : B
    8.     {
    9.         private T item;
    10.  
    11.         public A(T item)
    12.         {
    13.             this.item = item;
    14.             this.item.parent = this;
    15.         }
    16.  
    17.         public B Item
    18.         {
    19.             get { return item; }
    20.         }
    21.     }
    22.  
    23.     public class B
    24.     {
    25.         public IA parent;
    26.     }
    27.  
    Though I will say, the overall design of this is a little lacking. Primarily because 'parent' field can change on any child, while a parent still thinks they're a child.

    What is it you're attempting to do anyways?
     
    Kiwasi likes this.
  3. Piflik

    Piflik

    Joined:
    Sep 11, 2011
    Posts:
    289
    I have written a generic object pool that returns the MonoBehaviours I want from an object. Under the hood it still stores complete GameObjects, but for convenience all checks for null etc are done inside this class and when I get an object from a pool, I already have the component I want to work with and I can be sure it is a valid object.

    It works as intended, but now I want to extend this with a central dictionary to store all pools I have, identified by the GameObject used as the template for each pool. Most of it works, but I want every pool object to have a reference to its pool in order to release itself.

    It is not a prohibitive problem, since I can store a reference to the template object instead of a reference to the pool, and then use this to get the pool from the dictionary, but I thought having a reference to the pool itself would have been a bit mor elegant.
     
  4. Piflik

    Piflik

    Joined:
    Sep 11, 2011
    Posts:
    289
    After a bit more thinking I adapted your Interface idea:

    Code (CSharp):
    1. public interface IA {}
    2.    
    3. public class A<T> : IA where T : B {
    4.     T item;
    5.    
    6.     public A<T> (GameObject item) {
    7.         this.item = item;
    8.         this.item.GetComponent<T>().parent = this;
    9.     }
    10.    
    11.     public void DoSomethingWith(T item) {
    12.         //do something
    13.     }
    14. }
    15.    
    16. public class B {
    17.     IA parent;
    18.    
    19.     public void CallToParent() {
    20.         ((A<B>) parent).DoSomethingWith(this);
    21.     }
    22. }
    I know I could pull the DoSomethingWith(T item) function into the interface and avoid the cast, if I used B instead of the generic type parameter T, but this way I can prevent passing a different class that derives from B to the function.
     
  5. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,376
    The problem in your example you posted is that there is no guarantee that the 'parent' in B is a A<B>. This could throw an runtime exception.

    You should probably check first.

    Or make DoSomethingWith a member of IA.

    I'd go with something like this:

    Code (csharp):
    1.  
    2. public interface IPoolable
    3. {
    4.     void Initialize(IPool pool);
    5.     void Deinitialize();
    6. }
    7.  
    8. public interface IPool
    9. {
    10.     IPoolable Create();
    11.     bool Release(IPoolable obj);
    12. }
    13.  
    14. public class Pool<T> : IPool, where T : IPoolable
    15. {
    16.  
    17.     public IPoolable Create()
    18.     {
    19.         IPoolable obj = /* get pooled object however */
    20.         obj.Initialize(this);
    21.         return obj;
    22.     }
    23.  
    24.     public bool Release(IPoolable obj)
    25.     {
    26.         if(!managesObject(obj)) return false;
    27.      
    28.         obj.Deinitialize();
    29.         //return object to pool for later use
    30.      
    31.         return true;
    32.     }
    33.  
    34. }
    35.  
    36. public class Blargh : IPoolable
    37. {
    38.  
    39.     private IPool _pool;
    40.  
    41.     public void Initialize(IPool pool)
    42.     {
    43.         //store for later use if needed
    44.         _pool = pool;
    45.     }
    46.  
    47.     public void Deinitialize()
    48.     {
    49.         _pool = null;
    50.         //do to self whatever is necessary on deinit
    51.         this.isVisible = false; //example - make self invisible
    52.     }
    53.  
    54.     public void OnDeath()
    55.     {
    56.         if(_pool != null) _pool.Release(this);
    57.     }
    58.  
    59. }
    60.  
     
    Piflik likes this.
  6. Piflik

    Piflik

    Joined:
    Sep 11, 2011
    Posts:
    289
    As I said, I am aware that I could get rid of the cast, but then Release() would accept anything that derives from PoolObject (or implements IPoolable) and not only the actual type that the ObjectPool is supposed to contain. I do check before casting, though, I just posted the most abstract sample-code, since nobody needs to read my specific implementation when they have a similar problem.
     
  7. Kiwasi

    Kiwasi

    Joined:
    Dec 5, 2013
    Posts:
    16,860
    I approach the task of returning an object to a pool completely differently. Each time a GameObject is used from a pool, it gets a component added that marks where in the pool it comes from. Then when the object is disabled it automatically goes back to the pool.

    There is no real need for a generic return to pool method, as nothing ever needs to be returned to a pool that didn't come out of the pool in the first place.

    I may be missing the point in most of these abstractions, but it seems that adding in the abstractions is massively over complicating things.
     
    lordofduct likes this.
  8. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,376
    Yeah, that's how I personally do it as well in my own code.

    Since the code can't guarantee that there isn't multiple IPool<Blargh>, you might return something to a pool it didn't come from anyways in the first place.

    Typing the release of it doesn't protect that. What protects that is the pool recognizing if it's the pool that manages that object.

    Overall though, I would agree with BoredMormon, you're way over thinking this and cluttering it with a lot of abstractions.
     
    Kiwasi likes this.
  9. Piflik

    Piflik

    Joined:
    Sep 11, 2011
    Posts:
    289
    You are probably right. If you want to rip my code apart, here it is in its entirety. I am sure it has tons of problems (aside from the ObjectPool.Create function silently returning null if the template does not contain the desired component and not being documented at all), but it works for my needs.

    Code (CSharp):
    1. public abstract class PoolObject : MonoBehaviour {
    2.     private IObjectPool _pool;
    3.  
    4.     public void SetPool(IObjectPool pool) {
    5.         _pool = pool;
    6.     }
    7.  
    8.     public void Release() {
    9.         ObjectPool<PoolObject> pool = _pool as ObjectPool<PoolObject>;
    10.         if (pool != null) {
    11.             pool.ReleaseItem(this);
    12.         } else {
    13.             Destroy(gameObject);
    14.         }
    15.     }
    16. }
    17.  
    18. public static class PoolSupervisor {
    19.     private static readonly Dictionary<GameObject, ObjectPool<PoolObject>> pools = new Dictionary<GameObject, ObjectPool<PoolObject>>();
    20.     private const int DEFAULT_SIZE = 5;
    21.     private const int DEFAULT_MAX_SIZE = 15;
    22.  
    23.  
    24.     public static T Get<T>(GameObject template) where T : PoolObject {
    25.         if (!pools.ContainsKey(template)) {
    26.             CreatePool<T>(template, DEFAULT_SIZE, DEFAULT_MAX_SIZE);
    27.         }
    28.  
    29.         return pools[template].getObject as T;
    30.     }
    31.  
    32.     public static void CreatePool<T>(GameObject template, int initialSize, int maxSize) {
    33.         if (pools.ContainsKey(template)) {
    34.              pools[template].SetMaxNum(maxSize);
    35.              return;
    36.          }
    37.  
    38.         pools[template] = ObjectPool<PoolObject>.Create(template, initialSize, maxSize);
    39.     }
    40.  
    41. }
    42.  
    43.  public interface IObjectPool {
    44.      bool objectAvailable { get; }
    45.      int remainingObjects { get; }
    46.      void SetMaxNum(int num);
    47.  }
    48.  
    49. public class ObjectPool<T> : IObjectPool where T : PoolObject {
    50.  
    51.     private readonly GameObject _sourceObject;
    52.     private readonly Stack<GameObject> _objects;
    53.     private int _maxNum;
    54.     private int _currentNum;
    55.     private int _excessNum;
    56.  
    57.     private ObjectPool(GameObject sourceObject, int startNum, int maxNum) {
    58.         _sourceObject = sourceObject;
    59.         _maxNum = maxNum;
    60.         _currentNum = startNum;
    61.  
    62.         _objects = new Stack<GameObject>(startNum);
    63.  
    64.         for (int i = 0; i < startNum; ++i) {
    65.             GameObject newObj = GameObject.Instantiate(_sourceObject);
    66.             newObj.GetComponent<T>().SetPool(this);
    67.             newObj.SetActive(false);
    68.             GameObject.DontDestroyOnLoad(newObj);
    69.             _objects.Push(newObj);
    70.         }
    71.     }
    72.  
    73.     public static ObjectPool<T> Create(GameObject sourceObject, int initialNumber, int maxNumber) {
    74.         if (sourceObject.GetComponent<T>() == null) return null;
    75.  
    76.         return new ObjectPool<T>(sourceObject, initialNumber, maxNumber);
    77.     }
    78.  
    79.     public bool objectAvailable {
    80.         get { return _objects.Count > 0 || _currentNum < _maxNum; }
    81.     }
    82.  
    83.     public int remainingObjects {
    84.         get { return _objects.Count; }
    85.     }
    86.  
    87.     public T getObject {
    88.         get {
    89.             if (_objects.Count > 0) {
    90.                 GameObject obj = _objects.Pop();
    91.                 obj.SetActive(true);
    92.                 return obj.GetComponent<T>();
    93.             }
    94.  
    95.             GameObject newObj = GameObject.Instantiate(_sourceObject);
    96.             newObj.GetComponent<T>().SetPool(this);
    97.             GameObject.DontDestroyOnLoad(newObj);
    98.             _currentNum++;
    99.  
    100.             if (_currentNum > _maxNum) {
    101.                 _excessNum = _currentNum - _maxNum;
    102.             }
    103.  
    104.             return newObj.GetComponent<T>();
    105.         }
    106.     }
    107.  
    108.     public void ReleaseItem(T item) {
    109.         item.gameObject.SetActive(false);
    110.         if (_excessNum > 0) {
    111.             GameObject.Destroy(item.gameObject);
    112.             _excessNum--;
    113.             _currentNum--;
    114.         }
    115.         else _objects.Push(item.gameObject);
    116.     }
    117.  
    118.     public void SetMaxNum(int num) {
    119.         _maxNum = num;
    120.         if (_currentNum > _maxNum) {
    121.             _excessNum = _currentNum - _maxNum;
    122.  
    123.             while (_excessNum > 0 && _objects.Count > 0) {
    124.                 GameObject.Destroy(_objects.Pop());
    125.                 _excessNum--;
    126.             }
    127.         }
    128.     }
    129. }
    Pools can both be created and handled individually, or via the central PoolSupervisor.

    And yes, I still can theoretically release objects from two different pools into the wrong one if their types are compatible, but (at the moment) I can live with that (In C++ I would make PoolObject a friend of ObjectPool and make the Release function private or protected, so it can only be called from the PoolObject itself, but C# doesn't have that and internal is not restrictive enough and doing it via Reflection is a tad silly).
     
    Last edited: Oct 23, 2016