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

Question Make an interface for a list of generic type

Discussion in 'Scripting' started by Magnilum, Sep 8, 2023.

  1. Magnilum

    Magnilum

    Joined:
    Jul 1, 2019
    Posts:
    143
    I have created a class as ScriptableObject which is name Collection<T> and takes a generic type.

    Code (CSharp):
    1. public abstract class Collection<T> : ScriptableObject where T : class {
    2.  
    3.     [SerializeField] List<T> list = new List<T>();
    4.  
    5.     #region Getter & Setter
    6.  
    7.     public List<T> List => list;
    8.  
    9.     #endregion
    10.  
    11.     public Action<T> OnAdded;
    12.     public Action<T> OnRemoved;
    13.     public Action OnChanged;
    14.  
    15.     public void Add(T obj)
    16.     {
    17.         list.Add(obj);
    18.         OnAdded?.Invoke(obj);
    19.         OnChanged?.Invoke();
    20.     }
    21.  
    22.     public void Remove(T obj)
    23.     {
    24.         list.Remove(obj);
    25.         OnRemoved?.Invoke(obj);
    26.         OnChanged?.Invoke();
    27.     }
    28.  
    29.     public bool Contains(T obj)
    30.     {
    31.         return list.Contains(obj);
    32.     }
    33. }
    I have other classes which inherit from this class.

    Code (CSharp):
    1. [CreateAssetMenu(fileName = "New Template Collection", menuName = "Prefab Placer/Collections/Template")]
    2. public class TemplateCollection : Collection<Template> {
    3.  
    4.  
    5. }
    6.  
    7. [CreateAssetMenu(fileName = "New Large Collection", menuName = "Prefab Placer/Collections/Large Collection")]
    8. public class LargeCollection : Collection<TemplateCollection> {
    9.  
    10.  
    11. }
    And I would like to do the following:

    Code (CSharp):
    1. [SerializeField] Object obj;
    2.  
    3. void Start()
    4. {
    5.     if (obj is GameObject gameObject)
    6.     {
    7.         // Do what ever ...
    8.     }
    9.     else if (obj is Collection<> collection)
    10.     {
    11.         foreach (var item in collection.List)
    12.             Debug.Log(item.name);
    13.     }
    14. }
    I get an error on the "Collection<>" because I do not specify the type of the collection. And If I specify the type of the Collection, it means that I will get only the Collection of this type instead of having any collection.

    I am looking for an thing to just know if my obj is a collection and if it is, treat it as a collection of templates of a collection of collection of templates
     
  2. Bunny83

    Bunny83

    Joined:
    Oct 18, 2010
    Posts:
    3,495
    That's because you have a misconception of what generics are. I always said that generics are in some sense the opposite of abstraction / inheritance. Object abstraction is about to define a common base, have several different decendent classes which ARE all of the same base type. This allows us to treat every derived class as just that base type (could be a base class or an interface). So a derived class shares and inherits all the DATA that the base class is composed of since derived classes ARE actually those base classes. Though individual instances could override / change the METHODS and behaviour or the class while keeping the same basic interface

    Generics on the other do the exact opposite. Generics implement a certain algorithm / method / executable code that is fix and can be reused for different data types. When you actually bind a generic type with a type argument, that newly created type is completely isolated from the generic base class or any other implementations. Those are fundamentally incompatible with each other. So generics use the same METHOD with different data.

    Generics allow certain level of compatibility but only in very specific cases. Those are called covariance and contravariance. Though this is a more complex and niche topic that almost never applies. A List or any generic type where the generic parameter is used for both data-inflow and data-outflow is never compatible with co- / contravariance. If you want to read more about that, I wrote this answer over here. I thought I had a more complete / better formatted response somewhere. Though google refuses to help me to find it ^^.

    In short a
    List<int>
    and a
    List<string>
    are not in any way compatible. Even a
    List<object>
    is not compatible. For example if you have a
    List<string>
    and would treat it as a List<object>, you could Add any kind of object to the List which of course doesn't work since the actual List can only contain strings. Likewise when you have a
    List<object>
    you can not treat it as a
    List<string>
    because when you do list[5] it should return a string but the object list could contain any kind of object, not just strings. So this is impossible.
     
  3. Bunny83

    Bunny83

    Joined:
    Oct 18, 2010
    Posts:
    3,495
    ps: The only way would be to split the interface into separate parts like a pure reading and a pure writing part. That way the individual interfaces would define the generic arguments as
    in / out
    in order to support covariance / contravariance. Though I don't think that's a good approach.
     
  4. MelvMay

    MelvMay

    Unity Technologies

    Joined:
    May 24, 2013
    Posts:
    10,468
    What @Bunny83 is correct. Purely for educational purposes though, you'd have to drive down the type hierarchy checking for generic type definitions like this:

    Hopefully this is correct, my type/reflection mind is a bit rusty:
    Code (CSharp):
    1. using UnityEngine;
    2.  
    3. public class CollectionTest : MonoBehaviour
    4. {
    5.     public class CorrectCollection<T> : ScriptableObject { }
    6.     public class WrongCollection<T> : ScriptableObject { }
    7.  
    8.     public class CorrectCollection : CorrectCollection<int> { }
    9.     public class WrongCollection : WrongCollection<int> { }
    10.  
    11.     private void Start()
    12.     {
    13.         var correctCollection = ScriptableObject.CreateInstance<CorrectCollection>();
    14.         var wrongCollection = ScriptableObject.CreateInstance<WrongCollection>();
    15.  
    16.         var collectionToCheck = typeof(CorrectCollection<>);
    17.  
    18.         Debug.Log(ImplementsSpecifiedGenericType(correctCollection.GetType(), collectionToCheck));
    19.         Debug.Log(ImplementsSpecifiedGenericType(wrongCollection.GetType(), collectionToCheck));
    20.  
    21.         Destroy(correctCollection);
    22.         Destroy(wrongCollection);
    23.     }
    24.  
    25.  
    26.     private static bool ImplementsSpecifiedGenericType(System.Type type, System.Type genericType)
    27.     {
    28.         var genericToCheck = genericType.GetGenericTypeDefinition();
    29.         do
    30.         {
    31.             if (type.IsGenericType && type.GetGenericTypeDefinition() == genericToCheck)
    32.                 return true;
    33.  
    34.         } while ((type = type.BaseType) != null);
    35.  
    36.         return false;
    37.     }
    38. }
    39.  
    This doesn't help you work with it though, only discovere its heritage.
     
  5. Magnilum

    Magnilum

    Joined:
    Jul 1, 2019
    Posts:
    143
    There are very rich answers but I have to be honnest that it does not really help me. I totally understand the difference between generic class and generic class but I do not know how to apply to my case.

    I wanted to do this because I have 3 collections:
    - WeightedObject
    - Template
    - Collection of Templates

    Code (CSharp):
    1. [System.Serializable]
    2. public class WeightObject
    3. {
    4.     public Object obj;
    5.     public uint weight;
    6.  
    7.     public event Action OnWeightChanged;
    8.  
    9.     #region Getter & Setter
    10.  
    11.     public Object Object => obj;
    12.     public uint Weight => weight;
    13.  
    14.     #endregion
    15.  
    16.     #region Constructors
    17.  
    18.     public WeightObject(Object obj)
    19.     {
    20.         this.obj = obj;
    21.         weight = 1;
    22.     }
    23.  
    24.     public WeightObject(Object obj, uint weight)
    25.     {
    26.         this.obj = obj;
    27.         this.weight = weight;
    28.     }
    29.  
    30.     #endregion
    31.  
    32.     public void SetWeight(uint weight)
    33.     {
    34.         this.weight = weight;
    35.         OnWeightChanged?.Invoke();
    36.     }
    37.  
    38.     public bool IsEmpty()
    39.     {
    40.         return obj == null;
    41.     }
    42. }
    Since I want a list doing always the same thing with the 3 different types, I went with generic because my WeightObject class is not derived from Object.

    And instead of writing many if statement to compare the initial obj to the different types (Game object, Collection of WeightObject, Collection Template or Collection of Collection of Templates), I tried to find a way to make the task easier and as less repetitive as possible with a maximum of reusability.
     
  6. Bunny83

    Bunny83

    Joined:
    Oct 18, 2010
    Posts:
    3,495
    You said:
    As I explained, you can not treat any of your 3 collections in an abstract way as they are completely separate types which are not compatible with each other. So you can not write any code that somehow works with those 3 things at once. So your only option is what you don't want to do. That is check for each type separately. Again, those types are not compatible with each other. So the only solution is

    Code (CSharp):
    1. void Start()
    2. {
    3.     if (obj is GameObject gameObject)
    4.     {
    5.         // Do what ever ...
    6.     }
    7.     else if (obj is TemplateCollection collection)
    8.     {
    9.         foreach (var item in collection.List)
    10.             Debug.Log(item.name);
    11.     }
    12.     else if (obj is LargeCollection collection)
    13.     {
    14.         foreach (var item in collection.List)
    15.             Debug.Log(item.name);
    16.     }
    17.     // [ ... ]
    18. }
    Just once more: Just because TemplateCollection and LargeCollection are based on the same generic base class, does not mean that they are related in any way. It's literally like you have a class "A" and a class "B". They are completely separated things and need to be treated separately.
     
    Magnilum, Ryiah and MelvMay like this.
  7. Magnilum

    Magnilum

    Joined:
    Jul 1, 2019
    Posts:
    143
    Thank you for this answer.

    I did not understand that was not possible at all, I thought there was a way to do it, may be a bit tricky but no as you mentioned, no.

    Does it mean that I have to rework something in my general code? Or just some times, it does go where we think and just follow this new path? I would be interested to have your opinion.
     
  8. spiney199

    spiney199

    Joined:
    Feb 11, 2021
    Posts:
    5,769
    One solution involves having a non-generic base type that the generic base type inherits from. Said base type expresses the bare-minimum that you can without needing a generic type.

    Rough idea from my own implementation along this idea:
    Code (CSharp):
    1. public abstract class AssetContainerBase : ScriptableObject
    2. {
    3.     public abstract Type AssetType { get; }
    4.    
    5.     public abstract int AssetCount { get; }
    6. }
    7.  
    8. public abstract class AssetContainerBase<T> : AssetContainerBase, IEnumerable<T>
    9.     where T : UnityEngine.Object
    10. {
    11.     #region Inspector Fields
    12.  
    13.     [SerializeField]
    14.     private T[] _assets;
    15.  
    16.     #endregion
    17.  
    18.     #region Properties
    19.  
    20.     public sealed override Type AssetType => typeof(T);
    21.    
    22.     public sealed override int AssetCount => _assets == null ? 0 : _assets.Length;
    23.  
    24.     public T this[int index] => _assets[index];
    25.  
    26.     #endregion
    27.    
    28.     // more stuff here
    29. }
     
    Magnilum and CodeRonnie like this.
  9. wideeyenow_unity

    wideeyenow_unity

    Joined:
    Oct 7, 2020
    Posts:
    728
    I would personally just have it as inheritance, and use the classes as the things that need sorted:
    Code (CSharp):
    1. public class ThingsToSort : MonoBehaviour { }
    2.  
    3. public class ThingOne : ThingsToSort
    4. {
    5.     public GameObject myObject;
    6.     public int variable1;
    7. }
    8.  
    9. public class ThingTwo : ThingsToSort
    10. {
    11.     public List<Template> myTemplates;
    12. }
    13.  
    14. public class ThingThree : ThingsToSort
    15. {
    16.     public List<TemplateCollection> myTemplateCollections;
    17. }
    18.  
    19. public class HandlerOfThings : MonoBehaviour
    20. {
    21.     void Start()
    22.     {
    23.         List<ThingsToSort> currentList; // get
    24.         if (currentList[index] is ThingOne)
    25.         { DoThingOneLogic(); }
    26.         else if (currentList[index] is ThingTwo)
    27.         { DoThingTwoLogic(); }
    28.         else { DoThingThreeLogic(); }
    29.     }
    30. }
    Pseudo Code! ^ ^ ^, as it's just the concept since I'm only using NotePad++ at this moment.

    But I too had an issue trying to sum up certain functions, to be able to handle many different classes, instead of writing out multiple functions that basically did the same thing with one small variance in each. And it was because I wasn't viewing them as classes, and also wasn't grasping inheritance properly.

    Not sure if that's technically what you're trying to do, but that would be one way. With another way to mention, would be just to have everything in one class(if it makes sense to group them), then just have a "type" that's either an 'int' or 'enum' to set them apart to be handled(sorted) by a "genericized" method.

    But my way assumes no use of Scriptable Objects, so with not being comfortable with them I can only assume Spineys method is perfect for your situation. I too tried handling <T> before, to no avail, so I personally use classes as a mediator(if needed in that way).
     
  10. spiney199

    spiney199

    Joined:
    Feb 11, 2021
    Posts:
    5,769
    There's no point to inheritance in this example, and is honestly a complete anti-pattern. Having to specifically know the types will not scale at a all.

    The point of inheritance is to be able to substitute derived types for their base type without regard to their implementation. This is what the L in SOLID represents.

    (Yes I know Unity's own components breaks this rule wide open, but that's part of the component architecture).
     
    Bunny83 and wideeyenow_unity like this.
  11. wideeyenow_unity

    wideeyenow_unity

    Joined:
    Oct 7, 2020
    Posts:
    728
    Ohh sorry, I thought he wanted one function to be able to handle multiple things, especially with different types of data. But true, I confuse that with having those in one List to be sorted and handled, without knowing what they are.

    I felt my issue was related and gave insight on another way of doing things, but I am mistaken. :)
     
  12. Bunny83

    Bunny83

    Joined:
    Oct 18, 2010
    Posts:
    3,495
    To get this back on track, you said this:
    It's not really clear what the actual goal is here. The code in your else if block doesn't make sense with all the types you have in mind, despite they come from the same generic base class. you iterate through the list that is wrapped in your collection and print the "name" of it. We don't know how your Template object looks like, it may have a name field / property. However "T" doesn't have it. Also your "WeightObject" doesn't have a "name". So what is the actual code you want to do that should actually work on ALL those collection in the same way? If you can't find common ground between your element types there is literally no hope to somehow treat them in the same way. If all potential element types have for example a common interface, you could create a read-only interface for the collection and since all collection element types would be restricted to that common interface, it would be possible to treat all those collections in the same way. However since we would use abstraction / inhertitance here it of course means you loose access to all things that do not exist in the common interface.

    So your example code I quoted here doesn't make much sense. So what exactly do you want to do? As I explained, you can not print the name of things that don't have names (like your WeightObject). Even if it had a name, C# is a strongly typed language. You can not duct-type objects like loosely typed scripting languages.
     
    CodeRonnie and wideeyenow_unity like this.
  13. spiney199

    spiney199

    Joined:
    Feb 11, 2021
    Posts:
    5,769
    I assume they just want to iterate through all objects referenced in each collection asset.

    It can probably be done by implementing the non-generic
    IEnumerable
    interface on the non-generic base class in my example above. I believe you can have a typed
    foreach
    loop with IEnumerable objects, and while it won't be a compiler error, you will get a little reminder saying that each element is being cast and that you might run right into a big error.
     
    SisusCo likes this.
  14. SisusCo

    SisusCo

    Joined:
    Jan 29, 2019
    Posts:
    1,104
    This exactly! The key is to make Collection<T> implement a non-generic interface (or derive from a non-generic abstract base class), which can then be used to perform operations on any derived types regardless of the generic type.

    For the purpose of enumerating through the list, the most convenient option would be to implement IEnumerable through the list that the class wraps.

    I would also remove the public List property, as having that leaks implementation details to all clients. It would be better to implement ICollection<T> or IReadOnlyCollection<T> or IEnumerable<T> through the wrapped list, so that clients can work with the collection directly.
     
    Last edited: Sep 9, 2023
    spiney199 likes this.
  15. SisusCo

    SisusCo

    Joined:
    Jan 29, 2019
    Posts:
    1,104
    So this can be implemented like this:
    Code (CSharp):
    1. using System;
    2. using System.Collections;
    3. using System.Collections.Generic;
    4. using UnityEngine;
    5. using Object = UnityEngine.Object;
    6.  
    7. public abstract class Collection : ScriptableObject, ICollection
    8. {
    9.     public abstract object this[int index] { get; set; }
    10.  
    11.     public abstract int Count { get; }
    12.     public abstract bool IsSynchronized { get; }
    13.     public abstract object SyncRoot { get; }
    14.  
    15.     public abstract void CopyTo(Array array, int index);
    16.     IEnumerator IEnumerable.GetEnumerator() => GetEnumeratorInternal();
    17.     protected abstract IEnumerator GetEnumeratorInternal();
    18. }
    Code (CSharp):
    1. public abstract class Collection<T> : Collection, ICollection<T>
    2. {
    3.     public event Action<T> OnAdded;
    4.     public event Action<T> OnRemoved;
    5.     public event Action OnChanged;
    6.  
    7.     [SerializeField] List<T> list = new List<T>();
    8.  
    9.     bool ICollection<T>.IsReadOnly => false;
    10.     public sealed override int Count => list.Count;
    11.     public sealed override bool IsSynchronized => false;
    12.     public sealed override object SyncRoot => ((ICollection)list).SyncRoot;
    13.  
    14.     public sealed override object this[int index]
    15.     {
    16.         get => list[index];
    17.         set => list[index] = (T)value;
    18.     }
    19.  
    20.     public bool Contains(T item) => list.Contains(item);
    21.  
    22.     public void Add(T item)
    23.     {
    24.         list.Add(item);
    25.         OnAdded?.Invoke(item);
    26.         OnChanged?.Invoke();
    27.     }
    28.  
    29.     public bool Remove(T item)
    30.     {
    31.         if(list.Remove(item))
    32.         {
    33.             OnRemoved?.Invoke(item);
    34.             OnChanged?.Invoke();
    35.             return true;
    36.         }
    37.  
    38.         return false;
    39.     }
    40.  
    41.     public void Clear()
    42.     {
    43.         for(int i = list.Count - 1; i >= 0; i--)
    44.         {
    45.             var item = list[i];
    46.             list.RemoveAt(i);
    47.             OnRemoved?.Invoke(item);
    48.         }
    49.  
    50.         OnChanged?.Invoke();
    51.     }
    52.  
    53.     public List<T>.Enumerator GetEnumerator() => list.GetEnumerator();
    54.     IEnumerator<T> IEnumerable<T>.GetEnumerator() => list.GetEnumerator();
    55.     protected sealed override IEnumerator GetEnumeratorInternal() => list.GetEnumerator();
    56.  
    57.     void ICollection<T>.CopyTo(T[] array, int arrayIndex) => list.CopyTo(array, arrayIndex);
    58.     public sealed override void CopyTo(Array array, int index) => list.CopyTo((T[])array, index);
    59. }
    Code (CSharp):
    1. [CreateAssetMenu]
    2. public class UnityObjectCollection : Collection<Object> { }
    3.  
    4. [CreateAssetMenu]
    5. public sealed class GameObjectCollection : UnityObjectCollection { }
    And then used like this:
    Code (CSharp):
    1. [SerializeField] Object obj;
    2.  
    3. void Start()
    4. {
    5.     if (obj is GameObject gameObject)
    6.     {
    7.         // Do what ever ...
    8.     }
    9.     else if (obj is UnityObjectCollection unityObjectCollection)
    10.     {
    11.         foreach(var item in unityObjectCollection)
    12.             Debug.Log(item.name);
    13.     }
    14. }
     
    Last edited: Sep 9, 2023
    spiney199 likes this.
  16. Magnilum

    Magnilum

    Joined:
    Jul 1, 2019
    Posts:
    143
    Thank you for all of your answers !

    It seems to be a really good idea and very close to what I had in mind so I will definitly try this.

    I did not want to be precise to get more general solution but I see that is not a good point so let me explain.
    I doing a tool in Unity named Prefab Painter. The goal of this tool is to some how, replace the painting tool (trees and foliage) we have in the terrain due to the lake of performance (too many batches ...).

    To do it, I base my tool on an template system, each template will have a Game Object or a collection of Game Objects with different settings. The collection of Game Objects has a weight system to pull out a random Game Object from the list giving weight to each of them. This was the WeightObject class seen before. So a template could have many types of rocks, trees and more. Also. Also, An ObjectCollection could have ObjectCollection inside.

    Then, I want to make a collection of template, to make some king of biome brush. And a collection of collection of template to represent maybe a planet biomes or anything that people want.

    So I have 3 collections:
    - WeightObject (GameObject + weight)
    - Templates
    - Collection of Templates

    I want to find a way to regroup them because I am using the UI Builder to make the UI of my tool and I have done a display icon for an item in a collection. This display shows the name of the item in the collection and if it is from a WeightObject so it also shows the weight of the object.

    It is just to create a common custom CollectionField to display the content of a collection despite what is inside.

    So the purpose of my question is: Is there a way to just have a variable of function returning the list of the collection which I can forloop then cast the variable and treat it.

    Code (CSharp):
    1. [SerializeField] Object obj;
    2.  
    3. void Start()
    4. {
    5.     if (obj is GameObject gameObject)
    6.     {
    7.         Display(gameObject);
    8.     }
    9.     else (obj is Collection collection)
    10.     {
    11.         foreach (var item in collection.List)
    12.         {
    13.             if (item is WeightObject weightObject)
    14.             {
    15.                 Display(weightObject.Object, weightObject.Weight);
    16.             }
    17.             else if (item is Template template)
    18.             {
    19.                 Display(template);
    20.             }
    21.             else if (item is TemplateCollection templateCollection)
    22.             {
    23.                 Display(templateCollection);
    24.             }
    25.         }
    26.     }
    27. }
    I hope, it brings you some clarity of what I would like to do with this.


    This seems also very nice but a little bit more complex that the previous solution by spiney199.



    To be clear, I still trying to lear unity and code methods to improve my skills so I am listening to you guys to understand what is feasible or not and if not why and by what could it be replaced.
     
    SisusCo likes this.
  17. SisusCo

    SisusCo

    Joined:
    Jan 29, 2019
    Posts:
    1,104
    For sure. It made sense to me intuitively that the class would implement ICollection<T>, since it has Add, Remove and Contains methods and is named "Collection" - but it does mean having to implement quite a number of extra members.

    I'm personally a fan of the The Martyr Principle and pushing complexity down, and hiding it behind super simple interfaces. But it only really pays off well in situations where the class is used in many places :)
     
    CodeRonnie likes this.
  18. Magnilum

    Magnilum

    Joined:
    Jul 1, 2019
    Posts:
    143
    I have never heard about this but thanks, I think it is going to help me to better understand some things.
     
    SisusCo likes this.